/ src / workspace / provider / mod.rs
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;