mod.rs
1 mod cargo; 2 mod custom; 3 mod lerna; 4 mod npm; 5 mod nx; 6 mod pnpm; 7 mod registry; 8 mod turbo; 9 10 pub use registry::ProviderRegistry; 11 12 use super::context::PackageInfo; 13 use crate::config::MonorepoProviderType; 14 use compact_str::CompactString; 15 use globset::Glob; 16 use std::path::Path; 17 use walkdir::WalkDir; 18 19 pub trait MonorepoProvider: Send + Sync { 20 fn provider_type(&self) -> MonorepoProviderType; 21 fn config_file(&self) -> &'static str; 22 fn detect(&self, root: &Path) -> bool { 23 root.join(self.config_file()).exists() 24 } 25 fn discover_packages(&self, root: &Path) -> crate::Result<Vec<PackageInfo>>; 26 } 27 28 /// Configuration for glob pattern traversal. 29 pub struct GlobTraversalConfig { 30 pub max_depth: usize, 31 pub excluded_dirs: &'static [&'static str], 32 pub marker_file: Option<&'static str>, 33 } 34 35 /// Standard directory exclusions for most providers. 36 pub const STANDARD_EXCLUSIONS: &[&str] = &["node_modules", ".git", "target", "dist", "build"]; 37 38 /// Traverses the root directory matching glob patterns and returns discovered packages. 39 /// 40 /// Handles both inclusion patterns (e.g., "packages/*") and exclusion patterns (e.g., "!packages/internal"). 41 /// The `name_extractor` callback is called for each matched directory to extract the package name. 42 pub fn traverse_glob_patterns<F>( 43 root: &Path, 44 patterns: &[String], 45 config: &GlobTraversalConfig, 46 name_extractor: F, 47 ) -> crate::Result<Vec<PackageInfo>> 48 where 49 F: Fn(&Path) -> Option<CompactString>, 50 { 51 let mut packages = Vec::new(); 52 let mut exclusion_matchers = Vec::new(); 53 let mut inclusion_patterns = Vec::new(); 54 55 // Separate inclusion and exclusion patterns 56 for pattern in patterns { 57 if let Some(excl_pattern) = pattern.strip_prefix('!') { 58 let full_pattern = root.join(excl_pattern); 59 let pattern_str = full_pattern.to_string_lossy(); 60 if let Ok(glob) = Glob::new(&pattern_str) { 61 exclusion_matchers.push(glob.compile_matcher()); 62 } 63 } else { 64 inclusion_patterns.push(pattern.clone()); 65 } 66 } 67 68 for pattern in inclusion_patterns { 69 let full_pattern = root.join(&pattern); 70 let pattern_str = full_pattern.to_string_lossy(); 71 72 if let Ok(glob) = Glob::new(&pattern_str) { 73 let matcher = glob.compile_matcher(); 74 75 for entry in WalkDir::new(root) 76 .max_depth(config.max_depth) 77 .into_iter() 78 .filter_entry(|e| { 79 let name = e.file_name().to_str().unwrap_or(""); 80 !config.excluded_dirs.contains(&name) 81 }) 82 .flatten() 83 { 84 if entry.file_type().is_dir() && matcher.is_match(entry.path()) { 85 // Check exclusion patterns 86 let excluded = exclusion_matchers 87 .iter() 88 .any(|excl| excl.is_match(entry.path())); 89 90 if excluded { 91 continue; 92 } 93 94 // Check for marker file if specified 95 if let Some(marker) = config.marker_file { 96 if !entry.path().join(marker).exists() { 97 continue; 98 } 99 } 100 101 let relative_path = entry 102 .path() 103 .strip_prefix(root) 104 .unwrap_or(entry.path()) 105 .to_string_lossy(); 106 107 packages.push(PackageInfo { 108 root: entry.path().to_path_buf(), 109 name: name_extractor(entry.path()), 110 relative_path: CompactString::new(&relative_path), 111 }); 112 } 113 } 114 } 115 } 116 117 Ok(packages) 118 } 119 120 /// Traverses directories looking for a marker file (e.g., project.json) without glob matching. 121 pub fn traverse_for_marker_file<F>( 122 root: &Path, 123 config: &GlobTraversalConfig, 124 name_extractor: F, 125 ) -> crate::Result<Vec<PackageInfo>> 126 where 127 F: Fn(&Path) -> Option<CompactString>, 128 { 129 let mut packages = Vec::new(); 130 let marker = config.marker_file.expect("marker_file required for traverse_for_marker_file"); 131 132 for entry in WalkDir::new(root) 133 .max_depth(config.max_depth) 134 .into_iter() 135 .filter_entry(|e| { 136 let name = e.file_name().to_str().unwrap_or(""); 137 !config.excluded_dirs.contains(&name) 138 }) 139 .flatten() 140 { 141 if entry.file_name().to_str() == Some(marker) { 142 let project_dir = entry.path().parent().unwrap_or(root); 143 let relative_path = project_dir 144 .strip_prefix(root) 145 .unwrap_or(project_dir) 146 .to_string_lossy(); 147 148 packages.push(PackageInfo { 149 root: project_dir.to_path_buf(), 150 name: name_extractor(entry.path()), 151 relative_path: CompactString::new(&relative_path), 152 }); 153 } 154 } 155 156 Ok(packages) 157 } 158 159 pub use cargo::CargoProvider; 160 pub use custom::CustomProvider; 161 pub use lerna::LernaProvider; 162 pub use npm::NpmProvider; 163 pub use nx::NxProvider; 164 pub use pnpm::PnpmProvider; 165 pub use turbo::TurboProvider;