selection.rs
1 use crate::path_cache::PathCache; 2 use crate::workspace::{PackageInfo, WorkspaceManager}; 3 use std::collections::HashMap; 4 use std::path::{Path, PathBuf}; 5 use std::sync::Arc; 6 7 const AUTO_DISCOVERY_PRIORITY: &[&str] = &[ 8 ".env.local", 9 ".env.development", 10 ".env.dev", 11 ".env", 12 ".env.test", 13 ".env.staging", 14 ".env.production", 15 ".env.prod", 16 ]; 17 18 pub struct ActiveFileSelector { 19 workspace_root: PathBuf, 20 path_cache: Arc<PathCache>, 21 } 22 23 impl ActiveFileSelector { 24 pub fn new(workspace_root: &Path, path_cache: Arc<PathCache>) -> Self { 25 Self { 26 workspace_root: workspace_root.to_path_buf(), 27 path_cache, 28 } 29 } 30 31 pub fn resolve_patterns(&self, base_dir: &Path, patterns: &[String]) -> Vec<PathBuf> { 32 let mut result = Vec::new(); 33 34 for pattern in patterns { 35 let full_pattern = if pattern.starts_with('/') || pattern.starts_with("./") { 36 pattern.clone() 37 } else { 38 format!("{}/{}", base_dir.display(), pattern) 39 }; 40 41 let glob_pattern = if let Some(stripped) = full_pattern.strip_prefix("./") { 42 stripped 43 } else if full_pattern.starts_with('/') { 44 &full_pattern 45 } else { 46 full_pattern.as_str() 47 }; 48 49 let pattern_str = glob_pattern.to_string(); 50 match glob::glob_with( 51 &pattern_str, 52 glob::MatchOptions { 53 case_sensitive: true, 54 require_literal_separator: false, 55 require_literal_leading_dot: false, 56 }, 57 ) { 58 Ok(entries) => { 59 let mut matches: Vec<PathBuf> = entries 60 .filter_map(|entry| entry.ok()) 61 .filter(|path| path.is_file()) 62 .collect(); 63 64 if matches.is_empty() { 65 tracing::warn!( 66 "No files found matching pattern '{}' in '{}', glob pattern was '{}'", 67 pattern, 68 base_dir.display(), 69 pattern_str 70 ); 71 } else { 72 matches.sort(); 73 result.extend(matches); 74 } 75 } 76 Err(e) => { 77 tracing::warn!( 78 "Failed to parse glob pattern '{}': {}", 79 pattern, 80 e.to_string() 81 ); 82 } 83 } 84 } 85 86 result 87 } 88 89 pub fn auto_discover_files( 90 &self, 91 package_root: &Path, 92 packages: Vec<PackageInfo>, 93 ) -> Vec<PathBuf> { 94 let mut result = Vec::new(); 95 96 let is_monorepo = packages.len() > 1 || package_root != self.workspace_root; 97 98 if is_monorepo { 99 for env_file_name in AUTO_DISCOVERY_PRIORITY { 100 let root_env_path = self.workspace_root.join(env_file_name); 101 if root_env_path.exists() { 102 result.push(root_env_path); 103 break; 104 } 105 } 106 } 107 108 for env_file_name in AUTO_DISCOVERY_PRIORITY { 109 let package_env_path = package_root.join(env_file_name); 110 if package_env_path.exists() { 111 result.push(package_env_path); 112 break; 113 } 114 } 115 116 result 117 } 118 119 pub fn compute_active_files( 120 &self, 121 file_path: &Path, 122 global_patterns: Option<&[String]>, 123 directory_scoped: &HashMap<PathBuf, Vec<String>>, 124 workspace: &WorkspaceManager, 125 ) -> Vec<PathBuf> { 126 let canonical_file = self.path_cache.canonicalize(file_path); 127 128 let mut result = Vec::new(); 129 130 if let Some(patterns) = global_patterns { 131 if patterns.is_empty() { 132 let context = workspace.context_for_file(file_path); 133 if let Some(ctx) = context { 134 result.extend( 135 self.auto_discover_files(&ctx.package_root, workspace.packages().to_vec()), 136 ); 137 } 138 } else { 139 result.extend(self.resolve_patterns(&self.workspace_root, patterns)); 140 } 141 } else { 142 let context = workspace.context_for_file(file_path); 143 if let Some(ctx) = context { 144 result.extend(self.auto_discover_files(&ctx.package_root, workspace.packages())); 145 } 146 } 147 148 let mut best_match: Option<(&PathBuf, Vec<String>)> = None; 149 for (scope_dir, patterns) in directory_scoped { 150 let canonical_scope = self.path_cache.canonicalize(scope_dir); 151 if canonical_file.starts_with(&canonical_scope) { 152 match &best_match { 153 None => best_match = Some((scope_dir, patterns.clone())), 154 Some((existing_dir, _)) => { 155 if canonical_scope.as_os_str().len() > existing_dir.as_os_str().len() { 156 best_match = Some((scope_dir, patterns.clone())); 157 } 158 } 159 } 160 } 161 } 162 163 if let Some((scope_dir, patterns)) = best_match { 164 if patterns.is_empty() { 165 let context = workspace.context_for_file(scope_dir); 166 if let Some(ctx) = context { 167 result.extend( 168 self.auto_discover_files(&ctx.package_root, workspace.packages().to_vec()), 169 ); 170 } 171 } else { 172 result.extend(self.resolve_patterns(scope_dir, &patterns)); 173 } 174 } 175 176 result 177 } 178 } 179 180 #[cfg(test)] 181 mod tests { 182 use super::*; 183 use compact_str::CompactString; 184 use std::fs; 185 186 fn setup_test_workspace() -> tempfile::TempDir { 187 let dir = tempfile::tempdir().unwrap(); 188 let workspace_root = dir.path(); 189 190 fs::create_dir_all(workspace_root.join("packages/app1")).unwrap(); 191 fs::create_dir_all(workspace_root.join("packages/app2")).unwrap(); 192 193 dir 194 } 195 196 #[test] 197 fn test_auto_discovery_single_package() { 198 let temp_dir = setup_test_workspace(); 199 let workspace_root = temp_dir.path(); 200 201 let env_path = workspace_root.join(".env"); 202 fs::write(&env_path, "TEST=value").unwrap(); 203 204 let path_cache = Arc::new(PathCache::new()); 205 let selector = ActiveFileSelector::new(workspace_root, path_cache); 206 let packages = vec![PackageInfo { 207 name: Some(CompactString::new("root")), 208 root: workspace_root.to_path_buf(), 209 relative_path: CompactString::new("."), 210 }]; 211 212 let result = selector.auto_discover_files(workspace_root, packages); 213 assert_eq!(result.len(), 1); 214 assert_eq!(result[0], env_path); 215 } 216 217 #[test] 218 fn test_auto_discovery_priority_order() { 219 let temp_dir = setup_test_workspace(); 220 let workspace_root = temp_dir.path(); 221 222 let env_local = workspace_root.join(".env.local"); 223 fs::write(&env_local, "TEST=local").unwrap(); 224 225 let env_dev = workspace_root.join(".env.dev"); 226 fs::write(&env_dev, "TEST=dev").unwrap(); 227 228 let path_cache = Arc::new(PathCache::new()); 229 let selector = ActiveFileSelector::new(workspace_root, path_cache); 230 let packages = vec![PackageInfo { 231 name: Some(CompactString::new("root")), 232 root: workspace_root.to_path_buf(), 233 relative_path: CompactString::new("."), 234 }]; 235 236 let result = selector.auto_discover_files(workspace_root, packages); 237 assert_eq!(result.len(), 1); 238 assert_eq!(result[0], env_local); 239 } 240 241 #[test] 242 fn test_auto_discovery_monorepo() { 243 let temp_dir = setup_test_workspace(); 244 let workspace_root = temp_dir.path(); 245 246 let root_env = workspace_root.join(".env"); 247 fs::write(&root_env, "GLOBAL=value").unwrap(); 248 249 let app1_root = workspace_root.join("packages/app1"); 250 let app1_env = app1_root.join(".env"); 251 fs::write(&app1_env, "APP=app1").unwrap(); 252 253 let path_cache = Arc::new(PathCache::new()); 254 let selector = ActiveFileSelector::new(workspace_root, path_cache); 255 let packages = vec![ 256 PackageInfo { 257 name: Some(CompactString::new("app1")), 258 root: app1_root.clone(), 259 relative_path: CompactString::new("packages/app1"), 260 }, 261 PackageInfo { 262 name: Some(CompactString::new("app2")), 263 root: workspace_root.join("packages/app2"), 264 relative_path: CompactString::new("packages/app2"), 265 }, 266 ]; 267 268 let result = selector.auto_discover_files(&app1_root, packages); 269 assert_eq!(result.len(), 2); 270 assert!(result.contains(&root_env)); 271 assert!(result.contains(&app1_env)); 272 } 273 274 #[test] 275 fn test_resolve_patterns_simple() { 276 let temp_dir = setup_test_workspace(); 277 let workspace_root = temp_dir.path(); 278 279 let env1 = workspace_root.join(".env"); 280 fs::write(&env1, "TEST=1").unwrap(); 281 282 let env2 = workspace_root.join(".env.local"); 283 fs::write(&env2, "TEST=2").unwrap(); 284 285 let path_cache = Arc::new(PathCache::new()); 286 let selector = ActiveFileSelector::new(workspace_root, path_cache); 287 let result = selector.resolve_patterns(workspace_root, &[".env*".to_string()]); 288 289 assert_eq!(result.len(), 2); 290 assert!(result.contains(&env1)); 291 assert!(result.contains(&env2)); 292 } 293 294 #[test] 295 fn test_resolve_patterns_sorting() { 296 let temp_dir = setup_test_workspace(); 297 let workspace_root = temp_dir.path(); 298 299 let env_b = workspace_root.join(".env.b"); 300 fs::write(&env_b, "TEST=b").unwrap(); 301 302 let env_a = workspace_root.join(".env.a"); 303 fs::write(&env_a, "TEST=a").unwrap(); 304 305 let path_cache = Arc::new(PathCache::new()); 306 let selector = ActiveFileSelector::new(workspace_root, path_cache); 307 let result = selector.resolve_patterns(workspace_root, &[".env.*".to_string()]); 308 309 assert_eq!(result.len(), 2); 310 assert_eq!(result[0], env_a); 311 assert_eq!(result[1], env_b); 312 } 313 }