fallow_core/plugins/registry/
mod.rs1#![expect(clippy::excessive_nesting)]
3
4use rustc_hash::FxHashSet;
5use std::path::{Path, PathBuf};
6
7use fallow_config::{ExternalPluginDef, PackageJson};
8
9use super::Plugin;
10
11mod builtin;
12mod helpers;
13
14use helpers::{
15 check_has_config_file, discover_json_config_files, process_config_result,
16 process_external_plugins, process_static_patterns,
17};
18
19pub struct PluginRegistry {
21 plugins: Vec<Box<dyn Plugin>>,
22 external_plugins: Vec<ExternalPluginDef>,
23}
24
25#[derive(Debug, Default)]
27pub struct AggregatedPluginResult {
28 pub entry_patterns: Vec<(String, String)>,
30 pub config_patterns: Vec<String>,
32 pub always_used: Vec<(String, String)>,
34 pub used_exports: Vec<(String, Vec<String>)>,
36 pub referenced_dependencies: Vec<String>,
38 pub discovered_always_used: Vec<(String, String)>,
40 pub setup_files: Vec<(PathBuf, String)>,
42 pub tooling_dependencies: Vec<String>,
44 pub script_used_packages: FxHashSet<String>,
46 pub virtual_module_prefixes: Vec<String>,
49 pub path_aliases: Vec<(String, String)>,
52 pub active_plugins: Vec<String>,
54}
55
56impl PluginRegistry {
57 pub fn new(external: Vec<ExternalPluginDef>) -> Self {
59 Self {
60 plugins: builtin::create_builtin_plugins(),
61 external_plugins: external,
62 }
63 }
64
65 pub fn run(
70 &self,
71 pkg: &PackageJson,
72 root: &Path,
73 discovered_files: &[PathBuf],
74 ) -> AggregatedPluginResult {
75 let _span = tracing::info_span!("run_plugins").entered();
76 let mut result = AggregatedPluginResult::default();
77
78 let all_deps = pkg.all_dependency_names();
81 let active: Vec<&dyn Plugin> = self
82 .plugins
83 .iter()
84 .filter(|p| p.is_enabled_with_deps(&all_deps, root))
85 .map(|p| p.as_ref())
86 .collect();
87
88 tracing::info!(
89 plugins = active
90 .iter()
91 .map(|p| p.name())
92 .collect::<Vec<_>>()
93 .join(", "),
94 "active plugins"
95 );
96
97 for plugin in &active {
99 process_static_patterns(*plugin, root, &mut result);
100 }
101
102 process_external_plugins(
104 &self.external_plugins,
105 &all_deps,
106 root,
107 discovered_files,
108 &mut result,
109 );
110
111 let config_matchers: Vec<(&dyn Plugin, Vec<globset::GlobMatcher>)> = active
114 .iter()
115 .filter(|p| !p.config_patterns().is_empty())
116 .map(|p| {
117 let matchers: Vec<globset::GlobMatcher> = p
118 .config_patterns()
119 .iter()
120 .filter_map(|pat| globset::Glob::new(pat).ok().map(|g| g.compile_matcher()))
121 .collect();
122 (*p, matchers)
123 })
124 .collect();
125
126 let relative_files: Vec<(&PathBuf, String)> = discovered_files
128 .iter()
129 .map(|f| {
130 let rel = f
131 .strip_prefix(root)
132 .unwrap_or(f)
133 .to_string_lossy()
134 .into_owned();
135 (f, rel)
136 })
137 .collect();
138
139 if !config_matchers.is_empty() {
140 let mut resolved_plugins: FxHashSet<&str> = FxHashSet::default();
142
143 for (plugin, matchers) in &config_matchers {
144 for (abs_path, rel_path) in &relative_files {
145 if matchers.iter().any(|m| m.is_match(rel_path.as_str())) {
146 resolved_plugins.insert(plugin.name());
149 if let Ok(source) = std::fs::read_to_string(abs_path) {
150 let plugin_result = plugin.resolve_config(abs_path, &source, root);
151 if !plugin_result.is_empty() {
152 tracing::debug!(
153 plugin = plugin.name(),
154 config = rel_path.as_str(),
155 entries = plugin_result.entry_patterns.len(),
156 deps = plugin_result.referenced_dependencies.len(),
157 "resolved config"
158 );
159 process_config_result(plugin.name(), plugin_result, &mut result);
160 }
161 }
162 }
163 }
164 }
165
166 let json_configs = discover_json_config_files(
170 &config_matchers,
171 &resolved_plugins,
172 &relative_files,
173 root,
174 );
175 for (abs_path, plugin) in &json_configs {
176 if let Ok(source) = std::fs::read_to_string(abs_path) {
177 let plugin_result = plugin.resolve_config(abs_path, &source, root);
178 if !plugin_result.is_empty() {
179 let rel = abs_path
180 .strip_prefix(root)
181 .map(|p| p.to_string_lossy())
182 .unwrap_or_default();
183 tracing::debug!(
184 plugin = plugin.name(),
185 config = %rel,
186 entries = plugin_result.entry_patterns.len(),
187 deps = plugin_result.referenced_dependencies.len(),
188 "resolved config (filesystem fallback)"
189 );
190 process_config_result(plugin.name(), plugin_result, &mut result);
191 }
192 }
193 }
194 }
195
196 for plugin in &active {
200 if let Some(key) = plugin.package_json_config_key()
201 && !check_has_config_file(*plugin, &config_matchers, &relative_files)
202 {
203 let pkg_path = root.join("package.json");
205 if let Ok(content) = std::fs::read_to_string(&pkg_path)
206 && let Ok(json) = serde_json::from_str::<serde_json::Value>(&content)
207 && let Some(config_value) = json.get(key)
208 {
209 let config_json = serde_json::to_string(config_value).unwrap_or_default();
210 let fake_path = root.join(format!("{key}.config.json"));
211 let plugin_result = plugin.resolve_config(&fake_path, &config_json, root);
212 if !plugin_result.is_empty() {
213 tracing::debug!(
214 plugin = plugin.name(),
215 key = key,
216 "resolved inline package.json config"
217 );
218 process_config_result(plugin.name(), plugin_result, &mut result);
219 }
220 }
221 }
222 }
223
224 result
225 }
226
227 pub fn run_workspace_fast(
234 &self,
235 pkg: &PackageJson,
236 root: &Path,
237 project_root: &Path,
238 precompiled_config_matchers: &[(&dyn Plugin, Vec<globset::GlobMatcher>)],
239 relative_files: &[(&PathBuf, String)],
240 ) -> AggregatedPluginResult {
241 let _span = tracing::info_span!("run_plugins").entered();
242 let mut result = AggregatedPluginResult::default();
243
244 let all_deps = pkg.all_dependency_names();
246 let active: Vec<&dyn Plugin> = self
247 .plugins
248 .iter()
249 .filter(|p| p.is_enabled_with_deps(&all_deps, root))
250 .map(|p| p.as_ref())
251 .collect();
252
253 tracing::info!(
254 plugins = active
255 .iter()
256 .map(|p| p.name())
257 .collect::<Vec<_>>()
258 .join(", "),
259 "active plugins"
260 );
261
262 if active.is_empty() {
264 return result;
265 }
266
267 for plugin in &active {
269 process_static_patterns(*plugin, root, &mut result);
270 }
271
272 let active_names: FxHashSet<&str> = active.iter().map(|p| p.name()).collect();
275 let workspace_matchers: Vec<_> = precompiled_config_matchers
276 .iter()
277 .filter(|(p, _)| active_names.contains(p.name()))
278 .collect();
279
280 let mut resolved_ws_plugins: FxHashSet<&str> = FxHashSet::default();
281 if !workspace_matchers.is_empty() {
282 for (plugin, matchers) in &workspace_matchers {
283 for (abs_path, rel_path) in relative_files {
284 if matchers.iter().any(|m| m.is_match(rel_path.as_str()))
285 && let Ok(source) = std::fs::read_to_string(abs_path)
286 {
287 resolved_ws_plugins.insert(plugin.name());
290 let plugin_result = plugin.resolve_config(abs_path, &source, root);
291 if !plugin_result.is_empty() {
292 tracing::debug!(
293 plugin = plugin.name(),
294 config = rel_path.as_str(),
295 entries = plugin_result.entry_patterns.len(),
296 deps = plugin_result.referenced_dependencies.len(),
297 "resolved config"
298 );
299 process_config_result(plugin.name(), plugin_result, &mut result);
300 }
301 }
302 }
303 }
304 }
305
306 let mut ws_json_configs: Vec<(PathBuf, &dyn Plugin)> = Vec::new();
311 let mut ws_seen_paths: FxHashSet<PathBuf> = FxHashSet::default();
312 for plugin in &active {
313 if resolved_ws_plugins.contains(plugin.name()) || plugin.config_patterns().is_empty() {
314 continue;
315 }
316 for pat in plugin.config_patterns() {
317 let has_glob = pat.contains("**") || pat.contains('*') || pat.contains('?');
318 if !has_glob {
319 let check_roots: Vec<&Path> = if root == project_root {
321 vec![root]
322 } else {
323 vec![root, project_root]
324 };
325 for check_root in check_roots {
326 let abs_path = check_root.join(pat);
327 if abs_path.is_file() && ws_seen_paths.insert(abs_path.clone()) {
328 ws_json_configs.push((abs_path, *plugin));
329 break; }
331 }
332 } else {
333 let filename = std::path::Path::new(pat)
336 .file_name()
337 .and_then(|n| n.to_str())
338 .unwrap_or(pat);
339 let matcher = globset::Glob::new(pat).ok().map(|g| g.compile_matcher());
340 if let Some(matcher) = matcher {
341 let mut checked_dirs: FxHashSet<&Path> = FxHashSet::default();
342 checked_dirs.insert(root);
343 if root != project_root {
344 checked_dirs.insert(project_root);
345 }
346 for (abs_path, _) in relative_files {
347 if let Some(parent) = abs_path.parent() {
348 checked_dirs.insert(parent);
349 }
350 }
351 for dir in checked_dirs {
352 let candidate = dir.join(filename);
353 if candidate.is_file() && ws_seen_paths.insert(candidate.clone()) {
354 let rel = candidate
355 .strip_prefix(project_root)
356 .map(|p| p.to_string_lossy())
357 .unwrap_or_default();
358 if matcher.is_match(rel.as_ref()) {
359 ws_json_configs.push((candidate, *plugin));
360 }
361 }
362 }
363 }
364 }
365 }
366 }
367 for (abs_path, plugin) in &ws_json_configs {
369 if let Ok(source) = std::fs::read_to_string(abs_path) {
370 let plugin_result = plugin.resolve_config(abs_path, &source, root);
371 if !plugin_result.is_empty() {
372 let rel = abs_path
373 .strip_prefix(project_root)
374 .map(|p| p.to_string_lossy())
375 .unwrap_or_default();
376 tracing::debug!(
377 plugin = plugin.name(),
378 config = %rel,
379 entries = plugin_result.entry_patterns.len(),
380 deps = plugin_result.referenced_dependencies.len(),
381 "resolved config (workspace filesystem fallback)"
382 );
383 process_config_result(plugin.name(), plugin_result, &mut result);
384 }
385 }
386 }
387
388 result
389 }
390
391 pub fn precompile_config_matchers(&self) -> Vec<(&dyn Plugin, Vec<globset::GlobMatcher>)> {
394 self.plugins
395 .iter()
396 .filter(|p| !p.config_patterns().is_empty())
397 .map(|p| {
398 let matchers: Vec<globset::GlobMatcher> = p
399 .config_patterns()
400 .iter()
401 .filter_map(|pat| globset::Glob::new(pat).ok().map(|g| g.compile_matcher()))
402 .collect();
403 (p.as_ref(), matchers)
404 })
405 .collect()
406 }
407}
408
409impl Default for PluginRegistry {
410 fn default() -> Self {
411 Self::new(vec![])
412 }
413}
414
415#[cfg(test)]
416#[expect(clippy::disallowed_types)]
417mod tests;