fallow_core/plugins/registry/
mod.rs1#![expect(
3 clippy::excessive_nesting,
4 reason = "plugin config parsing requires deep AST matching"
5)]
6
7use rustc_hash::FxHashSet;
8use std::path::{Path, PathBuf};
9
10use fallow_config::{EntryPointRole, ExternalPluginDef, PackageJson};
11
12use super::Plugin;
13
14pub(crate) mod builtin;
15mod helpers;
16
17use helpers::{
18 check_has_config_file, discover_json_config_files, process_config_result,
19 process_external_plugins, process_static_patterns,
20};
21
22pub struct PluginRegistry {
24 plugins: Vec<Box<dyn Plugin>>,
25 external_plugins: Vec<ExternalPluginDef>,
26}
27
28#[derive(Debug, Default)]
30pub struct AggregatedPluginResult {
31 pub entry_patterns: Vec<(String, String)>,
33 pub entry_point_roles: rustc_hash::FxHashMap<String, EntryPointRole>,
35 pub config_patterns: Vec<String>,
37 pub always_used: Vec<(String, String)>,
39 pub used_exports: Vec<(String, Vec<String>)>,
41 pub referenced_dependencies: Vec<String>,
43 pub discovered_always_used: Vec<(String, String)>,
45 pub setup_files: Vec<(PathBuf, String)>,
47 pub tooling_dependencies: Vec<String>,
49 pub script_used_packages: FxHashSet<String>,
51 pub virtual_module_prefixes: Vec<String>,
54 pub generated_import_patterns: Vec<String>,
57 pub path_aliases: Vec<(String, String)>,
60 pub active_plugins: Vec<String>,
62 pub fixture_patterns: Vec<(String, String)>,
64}
65
66impl PluginRegistry {
67 #[must_use]
69 pub fn new(external: Vec<ExternalPluginDef>) -> Self {
70 Self {
71 plugins: builtin::create_builtin_plugins(),
72 external_plugins: external,
73 }
74 }
75
76 pub fn run(
81 &self,
82 pkg: &PackageJson,
83 root: &Path,
84 discovered_files: &[PathBuf],
85 ) -> AggregatedPluginResult {
86 let _span = tracing::info_span!("run_plugins").entered();
87 let mut result = AggregatedPluginResult::default();
88
89 let all_deps = pkg.all_dependency_names();
92 let active: Vec<&dyn Plugin> = self
93 .plugins
94 .iter()
95 .filter(|p| p.is_enabled_with_deps(&all_deps, root))
96 .map(AsRef::as_ref)
97 .collect();
98
99 tracing::info!(
100 plugins = active
101 .iter()
102 .map(|p| p.name())
103 .collect::<Vec<_>>()
104 .join(", "),
105 "active plugins"
106 );
107
108 for plugin in &active {
110 process_static_patterns(*plugin, root, &mut result);
111 }
112
113 process_external_plugins(
115 &self.external_plugins,
116 &all_deps,
117 root,
118 discovered_files,
119 &mut result,
120 );
121
122 let config_matchers: Vec<(&dyn Plugin, Vec<globset::GlobMatcher>)> = active
125 .iter()
126 .filter(|p| !p.config_patterns().is_empty())
127 .map(|p| {
128 let matchers: Vec<globset::GlobMatcher> = p
129 .config_patterns()
130 .iter()
131 .filter_map(|pat| globset::Glob::new(pat).ok().map(|g| g.compile_matcher()))
132 .collect();
133 (*p, matchers)
134 })
135 .collect();
136
137 let relative_files: Vec<(&PathBuf, String)> = discovered_files
139 .iter()
140 .map(|f| {
141 let rel = f
142 .strip_prefix(root)
143 .unwrap_or(f)
144 .to_string_lossy()
145 .into_owned();
146 (f, rel)
147 })
148 .collect();
149
150 if !config_matchers.is_empty() {
151 let mut resolved_plugins: FxHashSet<&str> = FxHashSet::default();
153
154 for (plugin, matchers) in &config_matchers {
155 for (abs_path, rel_path) in &relative_files {
156 if matchers.iter().any(|m| m.is_match(rel_path.as_str())) {
157 resolved_plugins.insert(plugin.name());
160 if let Ok(source) = std::fs::read_to_string(abs_path) {
161 let plugin_result = plugin.resolve_config(abs_path, &source, root);
162 if !plugin_result.is_empty() {
163 tracing::debug!(
164 plugin = plugin.name(),
165 config = rel_path.as_str(),
166 entries = plugin_result.entry_patterns.len(),
167 deps = plugin_result.referenced_dependencies.len(),
168 "resolved config"
169 );
170 process_config_result(plugin.name(), plugin_result, &mut result);
171 }
172 }
173 }
174 }
175 }
176
177 let json_configs = discover_json_config_files(
181 &config_matchers,
182 &resolved_plugins,
183 &relative_files,
184 root,
185 );
186 for (abs_path, plugin) in &json_configs {
187 if let Ok(source) = std::fs::read_to_string(abs_path) {
188 let plugin_result = plugin.resolve_config(abs_path, &source, root);
189 if !plugin_result.is_empty() {
190 let rel = abs_path
191 .strip_prefix(root)
192 .map(|p| p.to_string_lossy())
193 .unwrap_or_default();
194 tracing::debug!(
195 plugin = plugin.name(),
196 config = %rel,
197 entries = plugin_result.entry_patterns.len(),
198 deps = plugin_result.referenced_dependencies.len(),
199 "resolved config (filesystem fallback)"
200 );
201 process_config_result(plugin.name(), plugin_result, &mut result);
202 }
203 }
204 }
205 }
206
207 for plugin in &active {
211 if let Some(key) = plugin.package_json_config_key()
212 && !check_has_config_file(*plugin, &config_matchers, &relative_files)
213 {
214 let pkg_path = root.join("package.json");
216 if let Ok(content) = std::fs::read_to_string(&pkg_path)
217 && let Ok(json) = serde_json::from_str::<serde_json::Value>(&content)
218 && let Some(config_value) = json.get(key)
219 {
220 let config_json = serde_json::to_string(config_value).unwrap_or_default();
221 let fake_path = root.join(format!("{key}.config.json"));
222 let plugin_result = plugin.resolve_config(&fake_path, &config_json, root);
223 if !plugin_result.is_empty() {
224 tracing::debug!(
225 plugin = plugin.name(),
226 key = key,
227 "resolved inline package.json config"
228 );
229 process_config_result(plugin.name(), plugin_result, &mut result);
230 }
231 }
232 }
233 }
234
235 result
236 }
237
238 pub fn run_workspace_fast(
245 &self,
246 pkg: &PackageJson,
247 root: &Path,
248 project_root: &Path,
249 precompiled_config_matchers: &[(&dyn Plugin, Vec<globset::GlobMatcher>)],
250 relative_files: &[(&PathBuf, String)],
251 ) -> AggregatedPluginResult {
252 let _span = tracing::info_span!("run_plugins").entered();
253 let mut result = AggregatedPluginResult::default();
254
255 let all_deps = pkg.all_dependency_names();
257 let active: Vec<&dyn Plugin> = self
258 .plugins
259 .iter()
260 .filter(|p| p.is_enabled_with_deps(&all_deps, root))
261 .map(AsRef::as_ref)
262 .collect();
263
264 tracing::info!(
265 plugins = active
266 .iter()
267 .map(|p| p.name())
268 .collect::<Vec<_>>()
269 .join(", "),
270 "active plugins"
271 );
272
273 if active.is_empty() {
275 return result;
276 }
277
278 for plugin in &active {
280 process_static_patterns(*plugin, root, &mut result);
281 }
282
283 let active_names: FxHashSet<&str> = active.iter().map(|p| p.name()).collect();
286 let workspace_matchers: Vec<_> = precompiled_config_matchers
287 .iter()
288 .filter(|(p, _)| active_names.contains(p.name()))
289 .collect();
290
291 let mut resolved_ws_plugins: FxHashSet<&str> = FxHashSet::default();
292 if !workspace_matchers.is_empty() {
293 for (plugin, matchers) in &workspace_matchers {
294 for (abs_path, rel_path) in relative_files {
295 if matchers.iter().any(|m| m.is_match(rel_path.as_str()))
296 && let Ok(source) = std::fs::read_to_string(abs_path)
297 {
298 resolved_ws_plugins.insert(plugin.name());
301 let plugin_result = plugin.resolve_config(abs_path, &source, root);
302 if !plugin_result.is_empty() {
303 tracing::debug!(
304 plugin = plugin.name(),
305 config = rel_path.as_str(),
306 entries = plugin_result.entry_patterns.len(),
307 deps = plugin_result.referenced_dependencies.len(),
308 "resolved config"
309 );
310 process_config_result(plugin.name(), plugin_result, &mut result);
311 }
312 }
313 }
314 }
315 }
316
317 let mut ws_json_configs: Vec<(PathBuf, &dyn Plugin)> = Vec::new();
322 let mut ws_seen_paths: FxHashSet<PathBuf> = FxHashSet::default();
323 for plugin in &active {
324 if resolved_ws_plugins.contains(plugin.name()) || plugin.config_patterns().is_empty() {
325 continue;
326 }
327 for pat in plugin.config_patterns() {
328 let has_glob = pat.contains("**") || pat.contains('*') || pat.contains('?');
329 if has_glob {
330 let filename = std::path::Path::new(pat)
333 .file_name()
334 .and_then(|n| n.to_str())
335 .unwrap_or(pat);
336 let matcher = globset::Glob::new(pat).ok().map(|g| g.compile_matcher());
337 if let Some(matcher) = matcher {
338 let mut checked_dirs: FxHashSet<&Path> = FxHashSet::default();
339 checked_dirs.insert(root);
340 if root != project_root {
341 checked_dirs.insert(project_root);
342 }
343 for (abs_path, _) in relative_files {
344 if let Some(parent) = abs_path.parent() {
345 checked_dirs.insert(parent);
346 }
347 }
348 for dir in checked_dirs {
349 let candidate = dir.join(filename);
350 if candidate.is_file() && ws_seen_paths.insert(candidate.clone()) {
351 let rel = candidate
352 .strip_prefix(project_root)
353 .map(|p| p.to_string_lossy())
354 .unwrap_or_default();
355 if matcher.is_match(rel.as_ref()) {
356 ws_json_configs.push((candidate, *plugin));
357 }
358 }
359 }
360 }
361 } else {
362 let check_roots: Vec<&Path> = if root == project_root {
364 vec![root]
365 } else {
366 vec![root, project_root]
367 };
368 for check_root in check_roots {
369 let abs_path = check_root.join(pat);
370 if abs_path.is_file() && ws_seen_paths.insert(abs_path.clone()) {
371 ws_json_configs.push((abs_path, *plugin));
372 break; }
374 }
375 }
376 }
377 }
378 for (abs_path, plugin) in &ws_json_configs {
380 if let Ok(source) = std::fs::read_to_string(abs_path) {
381 let plugin_result = plugin.resolve_config(abs_path, &source, root);
382 if !plugin_result.is_empty() {
383 let rel = abs_path
384 .strip_prefix(project_root)
385 .map(|p| p.to_string_lossy())
386 .unwrap_or_default();
387 tracing::debug!(
388 plugin = plugin.name(),
389 config = %rel,
390 entries = plugin_result.entry_patterns.len(),
391 deps = plugin_result.referenced_dependencies.len(),
392 "resolved config (workspace filesystem fallback)"
393 );
394 process_config_result(plugin.name(), plugin_result, &mut result);
395 }
396 }
397 }
398
399 result
400 }
401
402 #[must_use]
405 pub fn precompile_config_matchers(&self) -> Vec<(&dyn Plugin, Vec<globset::GlobMatcher>)> {
406 self.plugins
407 .iter()
408 .filter(|p| !p.config_patterns().is_empty())
409 .map(|p| {
410 let matchers: Vec<globset::GlobMatcher> = p
411 .config_patterns()
412 .iter()
413 .filter_map(|pat| globset::Glob::new(pat).ok().map(|g| g.compile_matcher()))
414 .collect();
415 (p.as_ref(), matchers)
416 })
417 .collect()
418 }
419}
420
421impl Default for PluginRegistry {
422 fn default() -> Self {
423 Self::new(vec![])
424 }
425}
426
427#[cfg(test)]
428mod tests;