1use rustc_hash::{FxHashMap, FxHashSet};
4use std::fmt;
5use std::path::{Path, PathBuf};
6use std::sync::{OnceLock, RwLock};
7
8use fallow_config::{
9 AutoImportRule, EntryPointRole, ExternalPluginDef, JsxImportSourceRule, PackageJson,
10 UsedClassMemberRule,
11};
12
13use crate::scripts;
14
15use super::{PathRule, Plugin, PluginResult, PluginUsedExportRule, ProvidedDependencyRule};
16
17pub(crate) mod builtin;
18mod helpers;
19mod tooling_evidence;
20
21pub(crate) use tooling_evidence::find_config_file;
22
23#[must_use]
28pub fn builtin_plugin_names() -> Vec<&'static str> {
29 builtin::create_builtin_plugins()
30 .iter()
31 .map(|plugin| plugin.name())
32 .collect()
33}
34
35#[must_use]
40pub fn builtin_config_patterns() -> Vec<&'static str> {
41 let mut patterns: Vec<&'static str> = builtin::create_builtin_plugins()
42 .iter()
43 .flat_map(|plugin| plugin.config_patterns().iter().copied())
44 .collect();
45 patterns.sort_unstable();
46 patterns.dedup();
47 patterns
48}
49
50#[must_use]
54pub fn builtin_config_patterns_of(plugin_names: &[String]) -> Vec<&'static str> {
55 let mut patterns: Vec<&'static str> = builtin::create_builtin_plugins()
56 .iter()
57 .filter(|plugin| plugin_names.iter().any(|name| name == plugin.name()))
58 .flat_map(|plugin| plugin.config_patterns().iter().copied())
59 .collect();
60 patterns.sort_unstable();
61 patterns.dedup();
62 patterns
63}
64
65#[must_use]
69pub fn builtin_always_used_patterns_of(plugin_names: &[String]) -> Vec<&'static str> {
70 let mut patterns: Vec<&'static str> = builtin::create_builtin_plugins()
71 .iter()
72 .filter(|plugin| plugin_names.iter().any(|name| name == plugin.name()))
73 .flat_map(|plugin| plugin.always_used().iter().copied())
74 .collect();
75 patterns.sort_unstable();
76 patterns.dedup();
77 patterns
78}
79
80pub use helpers::ConfigCandidateIndex;
81pub use helpers::is_external_plugin_active;
82use helpers::{
83 check_has_config_file, discover_config_files, prepare_config_pattern, process_config_result,
84 process_external_plugins, process_package_json_metadata, process_static_patterns,
85};
86
87fn must_parse_workspace_config_when_root_active(plugin_name: &str) -> bool {
88 matches!(
89 plugin_name,
90 "eslint" | "docusaurus" | "jest" | "storybook" | "tanstack-router" | "vitest"
91 )
92}
93
94fn compile_config_matchers<'a>(
95 active: &[&'a dyn Plugin],
96) -> Vec<(&'a dyn Plugin, Vec<globset::GlobMatcher>)> {
97 active
98 .iter()
99 .filter(|plugin| !plugin.config_patterns().is_empty())
100 .map(|plugin| (*plugin, cached_plugin_config_matchers(*plugin)))
101 .collect()
102}
103
104fn compile_plugin_config_matchers(plugin: &dyn Plugin) -> Vec<globset::GlobMatcher> {
105 plugin
106 .config_patterns()
107 .iter()
108 .filter_map(|pattern| {
109 let prepared = prepare_config_pattern(pattern);
110 globset::Glob::new(&prepared)
111 .ok()
112 .map(|glob| glob.compile_matcher())
113 })
114 .collect()
115}
116
117struct CachedPluginConfigMatchers {
118 patterns: &'static [&'static str],
119 matchers: Vec<globset::GlobMatcher>,
120}
121
122#[derive(Default)]
123struct PluginConfigMatcherCache {
124 by_name: RwLock<FxHashMap<&'static str, Vec<CachedPluginConfigMatchers>>>,
125}
126
127impl PluginConfigMatcherCache {
128 fn get_or_compile(&self, plugin: &dyn Plugin) -> Vec<globset::GlobMatcher> {
129 let patterns = plugin.config_patterns();
130 let cached = self
131 .by_name
132 .read()
133 .unwrap_or_else(std::sync::PoisonError::into_inner)
134 .get(plugin.name())
135 .and_then(|variants| {
136 variants
137 .iter()
138 .find(|entry| entry.patterns == patterns)
139 .map(|entry| entry.matchers.clone())
140 });
141 if let Some(matchers) = cached {
142 return matchers;
143 }
144
145 let matchers = compile_plugin_config_matchers(plugin);
146 {
147 let mut by_name = self
148 .by_name
149 .write()
150 .unwrap_or_else(std::sync::PoisonError::into_inner);
151 let variants = by_name.entry(plugin.name()).or_default();
152 if let Some(entry) = variants.iter().find(|entry| entry.patterns == patterns) {
153 return entry.matchers.clone();
154 }
155 variants.push(CachedPluginConfigMatchers {
156 patterns,
157 matchers: matchers.clone(),
158 });
159 drop(by_name);
160 }
161 matchers
162 }
163}
164
165fn cached_plugin_config_matchers(plugin: &dyn Plugin) -> Vec<globset::GlobMatcher> {
166 static MATCHERS: OnceLock<PluginConfigMatcherCache> = OnceLock::new();
167 MATCHERS
168 .get_or_init(PluginConfigMatcherCache::default)
169 .get_or_compile(plugin)
170}
171
172fn log_active_plugins(active: &[&dyn Plugin]) {
174 tracing::info!(
175 plugins = active
176 .iter()
177 .map(|p| p.name())
178 .collect::<Vec<_>>()
179 .join(", "),
180 "active plugins"
181 );
182}
183
184fn compute_relative_files(
188 config_matchers: &[(&dyn Plugin, Vec<globset::GlobMatcher>)],
189 active: &[&dyn Plugin],
190 discovered_files: &[PathBuf],
191 root: &Path,
192) -> Vec<(PathBuf, String)> {
193 use rayon::prelude::*;
194 let needs_relative_files =
195 !config_matchers.is_empty() || active.iter().any(|p| p.package_json_config_key().is_some());
196 if !needs_relative_files {
197 return Vec::new();
198 }
199 discovered_files
200 .par_iter()
201 .map(|f| {
202 let rel = f
203 .strip_prefix(root)
204 .unwrap_or(f)
205 .to_string_lossy()
206 .into_owned();
207 (f.clone(), rel)
208 })
209 .collect()
210}
211
212pub struct PluginRegistry {
214 plugins: Vec<Box<dyn Plugin>>,
215 external_plugins: Vec<ExternalPluginDef>,
216}
217
218pub(crate) struct WorkspacePluginRunInput<'a> {
220 pub(crate) pkg: &'a PackageJson,
221 pub(crate) root: &'a Path,
222 pub(crate) project_root: &'a Path,
223 pub(crate) precompiled_config_matchers: &'a [(&'a dyn Plugin, Vec<globset::GlobMatcher>)],
224 pub(crate) relative_files: &'a [(PathBuf, String)],
225 pub(crate) skip_config_plugins: &'a FxHashSet<&'a str>,
226 pub(crate) production_mode: bool,
227 pub(crate) candidate_index: Option<&'a ConfigCandidateIndex>,
228}
229
230struct PluginRunContext<'a> {
231 all_deps: Vec<String>,
232 active: Vec<&'a dyn Plugin>,
233}
234
235struct PluginActivationInput<'a> {
237 pkg: &'a PackageJson,
238 root: &'a Path,
239 discovered_files: &'a [PathBuf],
240 all_deps: &'a [String],
241 script_packages: &'a FxHashSet<String>,
242 candidate_index: Option<&'a ConfigCandidateIndex>,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
247pub struct PluginRegexValidationError {
248 plugin_name: String,
249 config_path: Option<PathBuf>,
250 rule_kind: &'static str,
251 field: &'static str,
252 rule_pattern: String,
253 regex_pattern: String,
254 source: String,
255}
256
257impl PluginRegexValidationError {
258 fn new(input: PluginRegexValidationErrorInput<'_>) -> Self {
259 Self {
260 plugin_name: input.plugin_name.to_owned(),
261 config_path: input.config_path.map(Path::to_path_buf),
262 rule_kind: input.rule_kind,
263 field: input.field,
264 rule_pattern: input.rule_pattern.to_owned(),
265 regex_pattern: input.regex_pattern.to_owned(),
266 source: input.source.to_string(),
267 }
268 }
269}
270
271#[derive(Clone, Copy)]
272pub(crate) struct PluginRegexValidationErrorInput<'a> {
273 plugin_name: &'a str,
274 config_path: Option<&'a Path>,
275 rule_kind: &'static str,
276 field: &'static str,
277 rule_pattern: &'a str,
278 regex_pattern: &'a str,
279 source: &'a regex::Error,
280}
281
282impl fmt::Display for PluginRegexValidationError {
283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284 let location = self
285 .config_path
286 .as_ref()
287 .map(|path| format!(" in {}", path.display()))
288 .unwrap_or_default();
289 write!(
290 f,
291 "plugin '{}'{}: invalid regex '{}' in {}.{} for path rule '{}': {}",
292 self.plugin_name,
293 location,
294 self.regex_pattern,
295 self.rule_kind,
296 self.field,
297 self.rule_pattern,
298 self.source
299 )
300 }
301}
302
303#[must_use]
304pub(crate) fn format_plugin_regex_errors(errors: &[PluginRegexValidationError]) -> String {
305 let joined = errors
306 .iter()
307 .map(ToString::to_string)
308 .collect::<Vec<_>>()
309 .join("\n - ");
310 format!(
311 "invalid plugin regex configuration:\n - {joined}\n\nRewrite the plugin config with Rust-compatible regex syntax, or remove unsupported constructs such as JavaScript lookahead and lookbehind."
312 )
313}
314
315#[derive(Debug, Clone, Default, PartialEq, Eq)]
323pub struct PluginToolingDependencies {
324 pub plugin: String,
326 pub dependencies: Vec<String>,
328 pub references: Vec<String>,
332 pub own_config: Option<PathBuf>,
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339pub enum PluginToolingEvidence<'a> {
340 OwnConfig(&'a Path),
342 Reference(&'a str),
344}
345
346impl PluginToolingDependencies {
347 #[must_use]
351 pub fn evidence<'a>(
352 &'a self,
353 script_used_packages: &FxHashSet<String>,
354 ) -> Option<PluginToolingEvidence<'a>> {
355 if let Some(config) = &self.own_config {
356 return Some(PluginToolingEvidence::OwnConfig(config));
357 }
358 self.references
359 .iter()
360 .find(|name| script_used_packages.contains(name.as_str()))
361 .map(|name| PluginToolingEvidence::Reference(name))
362 }
363}
364
365#[derive(Debug, Clone, Default)]
367pub struct AggregatedPluginResult {
368 pub entry_patterns: Vec<(PathRule, String)>,
370 pub replaced_entry_plugins: FxHashSet<String>,
375 pub config_entry_patterns: FxHashSet<(String, String)>,
379 pub entry_point_roles: rustc_hash::FxHashMap<String, EntryPointRole>,
381 pub config_patterns: Vec<String>,
383 pub always_used: Vec<(String, String)>,
385 pub used_exports: Vec<PluginUsedExportRule>,
387 pub config_export_credits: Vec<PluginUsedExportRule>,
395 pub used_class_members: Vec<UsedClassMemberRule>,
399 pub framework_class_member_contracts: Vec<fallow_types::semantic::SemanticFrameworkContract>,
401 pub referenced_dependencies: Vec<String>,
403 pub package_referenced_dependencies: Vec<(PathBuf, String)>,
405 pub discovered_always_used: Vec<(String, String)>,
407 pub setup_files: Vec<(PathBuf, String)>,
409 pub tooling_dependencies: Vec<String>,
413 pub plugin_tooling: Vec<PluginToolingDependencies>,
417 pub script_used_packages: FxHashSet<String>,
419 pub dependency_binaries: crate::scripts::DependencyBinaries,
423 pub dev_dependency_names: FxHashSet<String>,
428 pub virtual_module_prefixes: Vec<String>,
431 pub virtual_package_suffixes: Vec<String>,
434 pub generated_import_patterns: Vec<String>,
437 pub generated_type_import_prefixes: Vec<String>,
440 pub path_aliases: Vec<(String, String)>,
443 pub auto_imports: Vec<AutoImportRule>,
447 pub active_plugins: Vec<String>,
449 pub fixture_patterns: Vec<(String, String)>,
451 pub scss_include_paths: Vec<PathBuf>,
456 pub static_dir_mappings: Vec<(PathBuf, String)>,
458 pub framework_static_dir_mappings: Vec<(PathBuf, String)>,
461 pub provided_dependencies: Vec<ProvidedDependencyRule>,
463 pub config_diagnostics: Vec<super::PluginConfigDiagnostic>,
467 pub federation_sources: Vec<super::FederationSource>,
470 pub jsx_import_sources: Vec<JsxImportSourceRule>,
474 pub jsx_package_credits: Vec<JsxImportSourceRule>,
479 pub tsconfig_jsx_dirs: Vec<PathBuf>,
483}
484
485fn extend_unique(target: &mut Vec<String>, incoming: Vec<String>) {
490 let mut seen: FxHashSet<String> = target.iter().cloned().collect();
491 for item in incoming {
492 if seen.insert(item.clone()) {
493 target.push(item);
494 }
495 }
496}
497
498fn push_missing<T: PartialEq>(target: &mut Vec<T>, incoming: Vec<T>) {
501 for item in incoming {
502 if !target.contains(&item) {
503 target.push(item);
504 }
505 }
506}
507
508fn prefix_if_needed(pat: &str, ws_prefix: &str) -> String {
512 if pat.starts_with(ws_prefix) || pat.starts_with('/') {
513 pat.to_string()
514 } else {
515 format!("{ws_prefix}/{pat}")
516 }
517}
518
519impl AggregatedPluginResult {
520 pub(crate) fn unique_entry_patterns(&self) -> impl Iterator<Item = &(PathRule, String)> {
526 let mut seen: FxHashSet<&(PathRule, String)> = FxHashSet::default();
527 self.entry_patterns
528 .iter()
529 .filter(move |entry| seen.insert(entry))
530 }
531
532 pub(crate) fn apply_workspace_prefix(&mut self, ws_prefix: &str) {
548 for (rule, _) in &mut self.entry_patterns {
549 *rule = rule.prefixed(ws_prefix);
550 }
551 for (pat, _) in &mut self.always_used {
552 *pat = prefix_if_needed(pat, ws_prefix);
553 }
554 for (pat, _) in &mut self.discovered_always_used {
555 *pat = prefix_if_needed(pat, ws_prefix);
556 }
557 for (pat, _) in &mut self.fixture_patterns {
558 *pat = prefix_if_needed(pat, ws_prefix);
559 }
560 for rule in &mut self.used_exports {
561 *rule = rule.prefixed(ws_prefix);
562 }
563 for rule in &mut self.config_export_credits {
564 *rule = rule.prefixed(ws_prefix);
565 }
566 for rule in &mut self.provided_dependencies {
567 *rule = rule.prefixed(ws_prefix);
568 }
569 for source in &mut self.federation_sources {
570 *source = source.prefixed(ws_prefix);
571 }
572 for (_, replacement) in &mut self.path_aliases {
573 *replacement = format!("{ws_prefix}/{replacement}");
574 }
575 }
576
577 pub(crate) fn merge_into(&mut self, other: Self) {
591 let Self {
592 entry_patterns,
593 replaced_entry_plugins: _,
595 config_entry_patterns: _,
596 entry_point_roles,
597 config_patterns,
598 always_used,
599 used_exports,
600 config_export_credits,
601 used_class_members,
602 framework_class_member_contracts,
603 referenced_dependencies,
604 package_referenced_dependencies,
605 discovered_always_used,
606 setup_files,
607 tooling_dependencies,
608 plugin_tooling,
609 script_used_packages,
610 dependency_binaries: _,
611 dev_dependency_names,
612 virtual_module_prefixes,
613 virtual_package_suffixes,
614 generated_import_patterns,
615 generated_type_import_prefixes,
616 path_aliases,
617 auto_imports,
618 active_plugins,
619 fixture_patterns,
620 scss_include_paths,
621 static_dir_mappings,
622 framework_static_dir_mappings,
623 provided_dependencies,
624 config_diagnostics,
625 federation_sources,
626 jsx_import_sources,
627 jsx_package_credits,
628 tsconfig_jsx_dirs,
629 } = other;
630
631 self.entry_patterns.extend(entry_patterns);
632 for (plugin_name, role) in entry_point_roles {
633 self.entry_point_roles.entry(plugin_name).or_insert(role);
634 }
635 self.config_patterns.extend(config_patterns);
636 self.always_used.extend(always_used);
637 self.used_exports.extend(used_exports);
638 self.config_export_credits.extend(config_export_credits);
639 self.used_class_members.extend(used_class_members);
640 for contract in framework_class_member_contracts {
641 if !self.framework_class_member_contracts.contains(&contract) {
642 self.framework_class_member_contracts.push(contract);
643 }
644 }
645 self.referenced_dependencies.extend(referenced_dependencies);
646 self.package_referenced_dependencies
647 .extend(package_referenced_dependencies);
648 self.discovered_always_used.extend(discovered_always_used);
649 self.setup_files.extend(setup_files);
650 self.tooling_dependencies.extend(tooling_dependencies);
651 self.plugin_tooling.extend(plugin_tooling);
652 self.dev_dependency_names.extend(dev_dependency_names);
653 self.script_used_packages.extend(script_used_packages);
654 extend_unique(&mut self.virtual_module_prefixes, virtual_module_prefixes);
655 extend_unique(&mut self.virtual_package_suffixes, virtual_package_suffixes);
656 extend_unique(
657 &mut self.generated_import_patterns,
658 generated_import_patterns,
659 );
660 extend_unique(
661 &mut self.generated_type_import_prefixes,
662 generated_type_import_prefixes,
663 );
664 self.path_aliases.extend(path_aliases);
665 self.auto_imports.extend(auto_imports);
666 extend_unique(&mut self.active_plugins, active_plugins);
667 self.fixture_patterns.extend(fixture_patterns);
668 self.scss_include_paths.extend(scss_include_paths);
669 self.static_dir_mappings.extend(static_dir_mappings);
670 self.framework_static_dir_mappings
671 .extend(framework_static_dir_mappings);
672 self.provided_dependencies.extend(provided_dependencies);
673 self.federation_sources.extend(federation_sources);
674 push_missing(&mut self.jsx_import_sources, jsx_import_sources);
675 push_missing(&mut self.jsx_package_credits, jsx_package_credits);
676 push_missing(&mut self.tsconfig_jsx_dirs, tsconfig_jsx_dirs);
677 push_missing(&mut self.config_diagnostics, config_diagnostics);
678 }
679}
680
681impl PluginRegistry {
682 #[must_use]
684 pub fn new(external: Vec<ExternalPluginDef>) -> Self {
685 Self {
686 plugins: builtin::create_builtin_plugins(),
687 external_plugins: external,
688 }
689 }
690
691 #[must_use]
696 pub fn discovery_hidden_dirs(&self, pkg: &PackageJson, root: &Path) -> Vec<String> {
697 let all_deps = pkg.all_dependency_names();
698 let mut seen = FxHashSet::default();
699 let mut dirs = Vec::new();
700
701 for plugin in &self.plugins {
702 if !plugin.is_enabled_with_deps(&all_deps, root) {
703 continue;
704 }
705 for dir in plugin.discovery_hidden_dirs() {
706 if seen.insert(*dir) {
707 dirs.push((*dir).to_string());
708 }
709 }
710 }
711
712 dirs
713 }
714
715 #[cfg(test)]
720 fn run(
721 &self,
722 pkg: &PackageJson,
723 root: &Path,
724 discovered_files: &[PathBuf],
725 ) -> AggregatedPluginResult {
726 self.try_run(pkg, root, discovered_files)
727 .unwrap_or_else(|errors| panic!("{}", format_plugin_regex_errors(&errors)))
728 }
729
730 pub fn try_run(
732 &self,
733 pkg: &PackageJson,
734 root: &Path,
735 discovered_files: &[PathBuf],
736 ) -> Result<AggregatedPluginResult, Vec<PluginRegexValidationError>> {
737 self.try_run_with_search_roots(pkg, root, discovered_files, &[root], false, None)
738 }
739
740 #[expect(
743 clippy::too_many_arguments,
744 reason = "public PluginRegistry API; signature is part of the crate surface for embedders"
745 )]
746 pub(crate) fn try_run_with_search_roots(
747 &self,
748 pkg: &PackageJson,
749 root: &Path,
750 discovered_files: &[PathBuf],
751 config_search_roots: &[&Path],
752 production_mode: bool,
753 candidate_index: Option<&ConfigCandidateIndex>,
754 ) -> Result<AggregatedPluginResult, Vec<PluginRegexValidationError>> {
755 let _span = tracing::info_span!("run_plugins").entered();
756 let mut result = AggregatedPluginResult::default();
757 let mut regex_errors = Vec::new();
758
759 let PluginRunContext { all_deps, active } = self.prepare_plugin_run_context(
760 pkg,
761 root,
762 discovered_files,
763 production_mode,
764 candidate_index,
765 );
766
767 self.run_plugin_preflight(&active, &all_deps, root, discovered_files);
768
769 for plugin in &active {
770 process_static_patterns(*plugin, root, &mut result);
771 }
772 process_package_json_metadata(&active, pkg, root, &mut result, &mut regex_errors);
773
774 process_external_plugins(
775 &self.external_plugins,
776 &all_deps,
777 root,
778 discovered_files,
779 &mut result,
780 );
781 tooling_evidence::record_plugin_tooling(
782 &tooling_evidence::ToolingEvidenceInput {
783 active: &active,
784 external_plugins: &self.external_plugins,
785 all_deps: &all_deps,
786 roots: &[root],
787 extra_roots: if production_mode {
791 &[]
792 } else {
793 config_search_roots
794 },
795 candidate_index,
796 discovered_files,
797 },
798 &mut result,
799 );
800
801 let config_matchers = compile_config_matchers(&active);
802 let relative_files =
803 compute_relative_files(&config_matchers, &active, discovered_files, root);
804
805 resolve_plugin_config_files(&mut PluginConfigResolutionInput {
806 config_matchers: &config_matchers,
807 relative_files: &relative_files,
808 config_search_roots,
809 production_mode,
810 candidate_index,
811 root,
812 result: &mut result,
813 regex_errors: &mut regex_errors,
814 });
815
816 process_package_json_inline_configs(
817 &active,
818 &config_matchers,
819 &relative_files,
820 root,
821 &mut result,
822 &mut regex_errors,
823 );
824
825 if regex_errors.is_empty() {
826 Ok(result)
827 } else {
828 Err(regex_errors)
829 }
830 }
831
832 #[cfg(test)]
838 fn run_workspace_fast(&self, input: &WorkspacePluginRunInput<'_>) -> AggregatedPluginResult {
839 self.try_run_workspace_fast(input)
840 .unwrap_or_else(|errors| panic!("{}", format_plugin_regex_errors(&errors)))
841 }
842
843 pub(crate) fn try_run_workspace_fast(
849 &self,
850 input: &WorkspacePluginRunInput<'_>,
851 ) -> Result<AggregatedPluginResult, Vec<PluginRegexValidationError>> {
852 let _span = tracing::info_span!("run_plugins").entered();
853 let mut result = AggregatedPluginResult::default();
854 let mut regex_errors = Vec::new();
855
856 let all_deps = input.pkg.all_dependency_names();
857 let script_packages =
858 script_activation_packages(input.pkg, input.root, &all_deps, input.production_mode);
859 let workspace_files: Vec<PathBuf> = input
860 .relative_files
861 .iter()
862 .map(|(abs_path, _)| abs_path.clone())
863 .collect();
864
865 let active = self.collect_active_plugins(&PluginActivationInput {
866 pkg: input.pkg,
867 root: input.root,
868 discovered_files: &workspace_files,
869 all_deps: &all_deps,
870 script_packages: &script_packages,
871 candidate_index: input.candidate_index,
872 });
873
874 log_active_plugins(&active);
875
876 self.emit_silent_fail_diagnostics(&active, &all_deps, input.root, &workspace_files);
877
878 process_external_plugins(
879 &self.external_plugins,
880 &all_deps,
881 input.root,
882 &workspace_files,
883 &mut result,
884 );
885
886 if active.is_empty() && result.active_plugins.is_empty() {
887 return Ok(result);
888 }
889
890 process_workspace_active_plugins(&active, input, &mut result, &mut regex_errors);
891 let evidence_roots: &[&Path] = if input.root == input.project_root {
893 &[input.root]
894 } else {
895 &[input.root, input.project_root]
896 };
897 tooling_evidence::record_plugin_tooling(
898 &tooling_evidence::ToolingEvidenceInput {
899 active: &active,
900 external_plugins: &self.external_plugins,
901 all_deps: &all_deps,
902 roots: evidence_roots,
903 extra_roots: &[],
904 candidate_index: input.candidate_index,
905 discovered_files: &workspace_files,
906 },
907 &mut result,
908 );
909 resolve_workspace_plugin_configs(&active, input, &mut result, &mut regex_errors);
910
911 if regex_errors.is_empty() {
912 Ok(result)
913 } else {
914 Err(regex_errors)
915 }
916 }
917
918 #[must_use]
921 pub(crate) fn precompile_config_matchers(
922 &self,
923 ) -> Vec<(&dyn Plugin, Vec<globset::GlobMatcher>)> {
924 self.plugins
925 .iter()
926 .filter(|p| !p.config_patterns().is_empty())
927 .map(|p| (p.as_ref(), cached_plugin_config_matchers(p.as_ref())))
928 .collect()
929 }
930
931 pub(crate) fn resolve_script_config_files(
942 &self,
943 config_files: &[scripts::BinaryConfigFile],
944 package_root: &Path,
945 project_root: &Path,
946 ws_prefix: &str,
947 ) -> AggregatedPluginResult {
948 let mut aggregated = AggregatedPluginResult::default();
949 for config_file in config_files {
950 let Some(rel_path) =
951 scripts::normalize_script_entry_pattern(ws_prefix, &config_file.path)
952 else {
953 continue;
954 };
955 let abs_path = package_root.join(config_file.path.trim_start_matches("./"));
956 let in_package_root = abs_path.parent() == Some(package_root);
961 for plugin in &self.plugins {
962 let plugin = plugin.as_ref();
963 if !plugin
964 .script_config_binaries()
965 .contains(&config_file.binary.as_str())
966 || (in_package_root
967 && cached_plugin_config_matchers(plugin)
968 .iter()
969 .any(|m| m.is_match(rel_path.as_str())))
970 {
971 continue;
972 }
973 if !super::config_parser::is_inside_root(&abs_path, project_root) {
974 continue;
975 }
976 let Ok(source) = std::fs::read_to_string(&abs_path) else {
977 continue;
978 };
979 let mut file_result = AggregatedPluginResult::default();
980 file_result
981 .entry_point_roles
982 .insert(plugin.name().to_string(), plugin.entry_point_role());
983 let plugin_result =
984 plugin.resolve_script_config(&abs_path, &source, package_root, project_root);
985 if let Err(errors) = process_config_result(
986 plugin.name(),
987 plugin_result,
988 &mut file_result,
989 Some(&abs_path),
990 ) {
991 for error in errors {
992 tracing::warn!("{error}");
993 }
994 continue;
995 }
996 if !ws_prefix.is_empty() {
997 file_result.apply_workspace_prefix(ws_prefix);
998 }
999 file_result.used_exports.push(PluginUsedExportRule::new(
1001 plugin.name().to_string(),
1002 super::UsedExportRule::new(rel_path.clone(), ["default"]),
1003 ));
1004 aggregated.merge_into(file_result);
1005 }
1006 }
1007 aggregated
1008 }
1009}
1010
1011fn process_workspace_active_plugins(
1012 active: &[&dyn Plugin],
1013 input: &WorkspacePluginRunInput<'_>,
1014 result: &mut AggregatedPluginResult,
1015 regex_errors: &mut Vec<PluginRegexValidationError>,
1016) {
1017 for plugin in active {
1018 process_static_patterns(*plugin, input.root, result);
1019 }
1020 process_package_json_metadata(active, input.pkg, input.root, result, regex_errors);
1021}
1022
1023fn resolve_workspace_plugin_configs(
1024 active: &[&dyn Plugin],
1025 input: &WorkspacePluginRunInput<'_>,
1026 result: &mut AggregatedPluginResult,
1027 regex_errors: &mut Vec<PluginRegexValidationError>,
1028) {
1029 let workspace_matchers = select_workspace_matchers(
1030 input.precompiled_config_matchers,
1031 active,
1032 input.skip_config_plugins,
1033 );
1034
1035 let mut resolved_ws_plugins: FxHashSet<&str> = FxHashSet::default();
1036 for (plugin, matchers) in &workspace_matchers {
1037 resolve_plugin_matching_files(&mut PluginMatchingFilesInput {
1038 plugin: *plugin,
1039 matchers,
1040 relative_files: input.relative_files,
1041 root: input.root,
1042 project_root: input.project_root,
1043 result,
1044 regex_errors,
1045 resolved_plugins: &mut resolved_ws_plugins,
1046 });
1047 }
1048
1049 load_workspace_filesystem_configs(&mut WorkspaceFsConfigInput {
1050 workspace_matchers: &workspace_matchers,
1051 resolved_ws_plugins: &resolved_ws_plugins,
1052 root: input.root,
1053 project_root: input.project_root,
1054 production_mode: input.production_mode,
1055 candidate_index: input.candidate_index,
1056 result,
1057 regex_errors,
1058 });
1059}
1060
1061impl Default for PluginRegistry {
1062 fn default() -> Self {
1063 Self::new(vec![])
1064 }
1065}
1066
1067impl PluginRegistry {
1068 fn prepare_plugin_run_context<'a>(
1069 &'a self,
1070 pkg: &PackageJson,
1071 root: &Path,
1072 discovered_files: &[PathBuf],
1073 production_mode: bool,
1074 candidate_index: Option<&ConfigCandidateIndex>,
1075 ) -> PluginRunContext<'a> {
1076 let all_deps = pkg.all_dependency_names();
1077 let script_packages = script_activation_packages(pkg, root, &all_deps, production_mode);
1078 let active = self.collect_active_plugins(&PluginActivationInput {
1079 pkg,
1080 root,
1081 discovered_files,
1082 all_deps: &all_deps,
1083 script_packages: &script_packages,
1084 candidate_index,
1085 });
1086
1087 PluginRunContext { all_deps, active }
1088 }
1089
1090 fn run_plugin_preflight(
1091 &self,
1092 active: &[&dyn Plugin],
1093 all_deps: &[String],
1094 root: &Path,
1095 discovered_files: &[PathBuf],
1096 ) {
1097 log_active_plugins(active);
1098 check_meta_framework_prerequisites(active, root);
1099 self.emit_silent_fail_diagnostics(active, all_deps, root, discovered_files);
1100 }
1101
1102 fn collect_active_plugins<'a>(
1105 &'a self,
1106 activation: &PluginActivationInput<'_>,
1107 ) -> Vec<&'a dyn Plugin> {
1108 self.plugins
1109 .iter()
1110 .filter(|p| {
1111 p.is_enabled_with_files(
1112 activation.all_deps,
1113 activation.root,
1114 activation.discovered_files,
1115 activation.candidate_index,
1116 ) || p.is_enabled_with_scripts(activation.script_packages, activation.root)
1117 || p.is_enabled_with_package_json(activation.pkg, activation.root)
1118 })
1119 .map(AsRef::as_ref)
1120 .collect()
1121 }
1122
1123 fn emit_silent_fail_diagnostics(
1132 &self,
1133 active: &[&dyn Plugin],
1134 all_deps: &[String],
1135 root: &Path,
1136 discovered_files: &[PathBuf],
1137 ) {
1138 let active_external: Vec<&ExternalPluginDef> = self
1139 .external_plugins
1140 .iter()
1141 .filter(|ext| is_external_plugin_active(ext, all_deps, root, discovered_files))
1142 .collect();
1143 let mut diagnostics = detect_pattern_collisions(active, &active_external);
1144 diagnostics.extend(detect_enabler_typos(&self.external_plugins, all_deps));
1145 emit_plugin_diagnostics(&diagnostics);
1146 }
1147}
1148
1149fn plugin_warn_dedupe() -> &'static std::sync::Mutex<FxHashSet<String>> {
1156 static WARNED: std::sync::OnceLock<std::sync::Mutex<FxHashSet<String>>> =
1157 std::sync::OnceLock::new();
1158 WARNED.get_or_init(|| std::sync::Mutex::new(FxHashSet::default()))
1159}
1160
1161struct PluginConfigResolutionInput<'a> {
1162 config_matchers: &'a [(&'a dyn Plugin, Vec<globset::GlobMatcher>)],
1163 relative_files: &'a [(PathBuf, String)],
1164 config_search_roots: &'a [&'a Path],
1165 production_mode: bool,
1166 candidate_index: Option<&'a ConfigCandidateIndex>,
1167 root: &'a Path,
1168 result: &'a mut AggregatedPluginResult,
1169 regex_errors: &'a mut Vec<PluginRegexValidationError>,
1170}
1171
1172fn select_workspace_matchers<'a>(
1175 precompiled_config_matchers: &[(&'a dyn Plugin, Vec<globset::GlobMatcher>)],
1176 active: &[&dyn Plugin],
1177 skip_config_plugins: &FxHashSet<&str>,
1178) -> Vec<(&'a dyn Plugin, Vec<globset::GlobMatcher>)> {
1179 let active_names: FxHashSet<&str> = active.iter().map(|p| p.name()).collect();
1180 precompiled_config_matchers
1181 .iter()
1182 .filter(|(p, _)| {
1183 active_names.contains(p.name())
1184 && (!skip_config_plugins.contains(p.name())
1185 || must_parse_workspace_config_when_root_active(p.name()))
1186 })
1187 .map(|(plugin, matchers)| (*plugin, matchers.clone()))
1188 .collect()
1189}
1190
1191struct WorkspaceFsConfigInput<'a> {
1192 workspace_matchers: &'a [(&'a dyn Plugin, Vec<globset::GlobMatcher>)],
1193 resolved_ws_plugins: &'a FxHashSet<&'a str>,
1194 root: &'a Path,
1195 project_root: &'a Path,
1196 production_mode: bool,
1197 candidate_index: Option<&'a ConfigCandidateIndex>,
1198 result: &'a mut AggregatedPluginResult,
1199 regex_errors: &'a mut Vec<PluginRegexValidationError>,
1200}
1201
1202fn load_workspace_filesystem_configs(input: &mut WorkspaceFsConfigInput<'_>) {
1205 let search_roots: &[&Path] = if input.root == input.project_root {
1206 &[input.root]
1207 } else {
1208 &[input.root, input.project_root]
1209 };
1210 let ws_json_configs = discover_config_files(
1211 input.workspace_matchers,
1212 input.resolved_ws_plugins,
1213 search_roots,
1214 input.production_mode,
1215 input.candidate_index,
1216 );
1217 for (abs_path, plugin) in &ws_json_configs {
1218 let Ok(source) = std::fs::read_to_string(abs_path) else {
1219 continue;
1220 };
1221 let plugin_result =
1222 plugin.resolve_config_in_project(abs_path, &source, input.root, input.project_root);
1223 if plugin_result.is_empty() {
1224 continue;
1225 }
1226 let rel = abs_path
1227 .strip_prefix(input.project_root)
1228 .map(|p| p.to_string_lossy())
1229 .unwrap_or_default();
1230 tracing::debug!(
1231 plugin = plugin.name(),
1232 config = %rel,
1233 entries = plugin_result.entry_patterns.len(),
1234 deps = plugin_result.referenced_dependencies.len(),
1235 "resolved config (workspace filesystem fallback)"
1236 );
1237 if let Err(mut errors) =
1238 process_config_result(plugin.name(), plugin_result, input.result, Some(abs_path))
1239 {
1240 input.regex_errors.append(&mut errors);
1241 }
1242 }
1243}
1244
1245fn resolve_plugin_config_files(input: &mut PluginConfigResolutionInput<'_>) {
1246 if input.config_matchers.is_empty() {
1247 return;
1248 }
1249
1250 let mut resolved_plugins: FxHashSet<&str> = FxHashSet::default();
1251 for (plugin, matchers) in input.config_matchers {
1252 resolve_plugin_matching_files(&mut PluginMatchingFilesInput {
1253 plugin: *plugin,
1254 matchers,
1255 relative_files: input.relative_files,
1256 root: input.root,
1257 project_root: input.root,
1258 result: input.result,
1259 regex_errors: input.regex_errors,
1260 resolved_plugins: &mut resolved_plugins,
1261 });
1262 }
1263
1264 let json_configs = discover_config_files(
1265 input.config_matchers,
1266 &resolved_plugins,
1267 input.config_search_roots,
1268 input.production_mode,
1269 input.candidate_index,
1270 );
1271 for (abs_path, plugin) in &json_configs {
1272 resolve_plugin_filesystem_config(
1273 *plugin,
1274 abs_path,
1275 input.root,
1276 input.result,
1277 input.regex_errors,
1278 );
1279 }
1280}
1281
1282struct PluginMatchingFilesInput<'plugins, 'data, 'state> {
1283 plugin: &'plugins dyn Plugin,
1284 matchers: &'data [globset::GlobMatcher],
1285 relative_files: &'data [(PathBuf, String)],
1286 root: &'data Path,
1287 project_root: &'data Path,
1288 result: &'state mut AggregatedPluginResult,
1289 regex_errors: &'state mut Vec<PluginRegexValidationError>,
1290 resolved_plugins: &'state mut FxHashSet<&'plugins str>,
1291}
1292
1293fn resolve_plugin_matching_files(input: &mut PluginMatchingFilesInput<'_, '_, '_>) {
1294 use rayon::prelude::*;
1295
1296 let plugin_hits: Vec<&PathBuf> = input
1297 .relative_files
1298 .par_iter()
1299 .filter_map(|(abs_path, rel_path)| {
1300 input
1301 .matchers
1302 .iter()
1303 .any(|m| m.is_match(rel_path.as_str()))
1304 .then_some(abs_path)
1305 })
1306 .collect();
1307 for abs_path in plugin_hits {
1308 let Ok(source) = std::fs::read_to_string(abs_path) else {
1309 continue;
1310 };
1311 let plugin_result = input.plugin.resolve_config_in_project(
1312 abs_path,
1313 &source,
1314 input.root,
1315 input.project_root,
1316 );
1317 if plugin_result.is_empty() {
1318 continue;
1319 }
1320 input.resolved_plugins.insert(input.plugin.name());
1321 process_resolved_plugin_config(ResolvedPluginConfigInput {
1322 plugin: input.plugin,
1323 abs_path,
1324 plugin_result,
1325 result: input.result,
1326 regex_errors: input.regex_errors,
1327 message: "resolved config",
1328 config_display: abs_path.display(),
1329 });
1330 }
1331}
1332
1333fn resolve_plugin_filesystem_config(
1334 plugin: &dyn Plugin,
1335 abs_path: &Path,
1336 root: &Path,
1337 result: &mut AggregatedPluginResult,
1338 regex_errors: &mut Vec<PluginRegexValidationError>,
1339) {
1340 let Ok(source) = std::fs::read_to_string(abs_path) else {
1341 return;
1342 };
1343 let plugin_result = plugin.resolve_config(abs_path, &source, root);
1344 if plugin_result.is_empty() {
1345 return;
1346 }
1347 let rel = abs_path
1348 .strip_prefix(root)
1349 .map(|p| p.to_string_lossy())
1350 .unwrap_or_default();
1351 process_resolved_plugin_config(ResolvedPluginConfigInput {
1352 plugin,
1353 abs_path,
1354 plugin_result,
1355 result,
1356 regex_errors,
1357 message: "resolved config (filesystem fallback)",
1358 config_display: rel,
1359 });
1360}
1361
1362struct ResolvedPluginConfigInput<'a, D> {
1363 plugin: &'a dyn Plugin,
1364 abs_path: &'a Path,
1365 plugin_result: PluginResult,
1366 result: &'a mut AggregatedPluginResult,
1367 regex_errors: &'a mut Vec<PluginRegexValidationError>,
1368 message: &'static str,
1369 config_display: D,
1370}
1371
1372fn process_resolved_plugin_config(input: ResolvedPluginConfigInput<'_, impl std::fmt::Display>) {
1373 tracing::debug!(
1374 plugin = input.plugin.name(),
1375 config = %input.config_display,
1376 entries = input.plugin_result.entry_patterns.len(),
1377 deps = input.plugin_result.referenced_dependencies.len(),
1378 input.message
1379 );
1380 if let Err(mut errors) = process_config_result(
1381 input.plugin.name(),
1382 input.plugin_result,
1383 input.result,
1384 Some(input.abs_path),
1385 ) {
1386 input.regex_errors.append(&mut errors);
1387 }
1388}
1389
1390fn should_warn(key: String) -> bool {
1394 plugin_warn_dedupe()
1395 .lock()
1396 .map_or(true, |mut set| set.insert(key))
1397}
1398
1399#[derive(Debug, Clone, PartialEq, Eq)]
1406pub(crate) enum PluginDiagnostic {
1407 PatternCollision {
1409 pattern: String,
1410 owners: Vec<String>,
1411 },
1412 EnablerTypo {
1415 plugin: String,
1416 enabler: String,
1417 suggestion: String,
1418 },
1419}
1420
1421fn detect_pattern_collisions(
1447 builtin_active: &[&dyn Plugin],
1448 external_active: &[&ExternalPluginDef],
1449) -> Vec<PluginDiagnostic> {
1450 use rustc_hash::FxHashMap;
1451
1452 let mut pattern_owners: FxHashMap<String, (Vec<String>, FxHashSet<String>)> =
1453 FxHashMap::default();
1454
1455 let record = |pattern_owners: &mut FxHashMap<_, (Vec<String>, FxHashSet<String>)>,
1456 pattern: String,
1457 name: String| {
1458 let (list, seen) = pattern_owners.entry(pattern).or_default();
1459 if seen.insert(name.clone()) {
1460 list.push(name);
1461 }
1462 };
1463
1464 for plugin in builtin_active {
1465 for pat in plugin.config_patterns() {
1466 record(
1467 &mut pattern_owners,
1468 (*pat).to_string(),
1469 plugin.name().to_string(),
1470 );
1471 }
1472 }
1473 for ext in external_active {
1474 for pat in &ext.config_patterns {
1475 record(&mut pattern_owners, pat.clone(), ext.name.clone());
1476 }
1477 }
1478
1479 let builtin_names: FxHashSet<&str> = builtin_active.iter().map(|p| p.name()).collect();
1486
1487 let mut findings: Vec<PluginDiagnostic> = pattern_owners
1488 .into_iter()
1489 .filter_map(|(pattern, (owners, _seen))| {
1490 if owners.len() < 2 || owners.iter().all(|o| builtin_names.contains(o.as_str())) {
1491 None
1492 } else {
1493 Some(PluginDiagnostic::PatternCollision { pattern, owners })
1494 }
1495 })
1496 .collect();
1497 findings.sort_unstable_by(|a, b| match (a, b) {
1498 (
1499 PluginDiagnostic::PatternCollision { pattern: ap, .. },
1500 PluginDiagnostic::PatternCollision { pattern: bp, .. },
1501 ) => ap.cmp(bp),
1502 _ => std::cmp::Ordering::Equal,
1503 });
1504 findings
1505}
1506
1507fn detect_enabler_typos(
1522 external_plugins: &[ExternalPluginDef],
1523 all_deps: &[String],
1524) -> Vec<PluginDiagnostic> {
1525 let mut findings = Vec::new();
1526
1527 for ext in external_plugins {
1528 if ext.detection.is_some() || ext.enablers.is_empty() {
1529 continue;
1530 }
1531
1532 let any_match = ext.enablers.iter().any(|enabler| {
1533 if enabler.ends_with('/') {
1534 all_deps.iter().any(|d| d.starts_with(enabler))
1535 } else {
1536 all_deps.iter().any(|d| d == enabler)
1537 }
1538 });
1539 if any_match {
1540 continue;
1541 }
1542
1543 for enabler in &ext.enablers {
1544 let candidates = all_deps.iter().map(String::as_str);
1545 let Some(suggestion) = fallow_config::levenshtein::closest_match(enabler, candidates)
1546 else {
1547 continue;
1548 };
1549
1550 findings.push(PluginDiagnostic::EnablerTypo {
1551 plugin: ext.name.clone(),
1552 enabler: enabler.clone(),
1553 suggestion: suggestion.to_string(),
1554 });
1555 }
1556 }
1557
1558 findings
1559}
1560
1561fn emit_plugin_diagnostics(findings: &[PluginDiagnostic]) {
1564 for finding in findings {
1565 match finding {
1566 PluginDiagnostic::PatternCollision { pattern, owners } => {
1567 let key = format!("collision::{pattern}::{owners:?}");
1568 if !should_warn(key) {
1569 continue;
1570 }
1571 let winner = &owners[0];
1572 let others = owners[1..].join(", ");
1573 tracing::warn!(
1574 "plugin config_patterns collision: identical pattern \
1575 '{pattern}' is claimed by plugins [{joined}]; '{winner}' \
1576 runs first (registration order), others ({others}) \
1577 follow. Rename one of the patterns or remove the \
1578 duplicate plugin to make resolution explicit. A future \
1579 release may reject identical-pattern collisions.",
1580 joined = owners.join(", "),
1581 );
1582 }
1583 PluginDiagnostic::EnablerTypo {
1584 plugin,
1585 enabler,
1586 suggestion,
1587 } => {
1588 let key = format!("enabler::{plugin}::{enabler}");
1589 if !should_warn(key) {
1590 continue;
1591 }
1592 tracing::warn!(
1593 "plugin '{plugin}' enabler '{enabler}' does not match any \
1594 dependency in package.json; did you mean '{suggestion}'? \
1595 The plugin will not activate. A future release may reject \
1596 unmatched enablers.",
1597 );
1598 }
1599 }
1600 }
1601}
1602
1603fn process_package_json_inline_configs(
1608 active: &[&dyn Plugin],
1609 config_matchers: &[(&dyn Plugin, Vec<globset::GlobMatcher>)],
1610 relative_files: &[(PathBuf, String)],
1611 root: &Path,
1612 result: &mut AggregatedPluginResult,
1613 regex_errors: &mut Vec<PluginRegexValidationError>,
1614) {
1615 for plugin in active {
1616 let Some(key) = plugin.package_json_config_key() else {
1617 continue;
1618 };
1619 if check_has_config_file(*plugin, config_matchers, relative_files) {
1620 continue;
1621 }
1622 let pkg_path = root.join("package.json");
1623 let Ok(content) = std::fs::read_to_string(&pkg_path) else {
1624 continue;
1625 };
1626 let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
1627 continue;
1628 };
1629 let Some(config_value) = json.get(key) else {
1630 continue;
1631 };
1632 let config_json = serde_json::to_string(config_value).unwrap_or_default();
1633 let fake_path = root.join(format!("{key}.config.json"));
1634 let plugin_result = plugin.resolve_config(&fake_path, &config_json, root);
1635 if plugin_result.is_empty() {
1636 continue;
1637 }
1638 tracing::debug!(
1639 plugin = plugin.name(),
1640 key = key,
1641 "resolved inline package.json config"
1642 );
1643 if let Err(mut errors) =
1644 process_config_result(plugin.name(), plugin_result, result, Some(&pkg_path))
1645 {
1646 regex_errors.append(&mut errors);
1647 }
1648 }
1649}
1650
1651#[derive(Debug)]
1654struct MetaFrameworkWarning {
1655 dedupe_key: &'static str,
1656 message: &'static str,
1657}
1658
1659fn missing_meta_framework_prerequisites(
1669 active_plugins: &[&dyn Plugin],
1670 root: &Path,
1671) -> Vec<MetaFrameworkWarning> {
1672 active_plugins
1673 .iter()
1674 .filter_map(|plugin| match plugin.name() {
1675 "nuxt" if !root.join(".nuxt/tsconfig.json").exists() => Some(MetaFrameworkWarning {
1676 dedupe_key: "meta-prereq::nuxt",
1677 message: "Nuxt project missing .nuxt/tsconfig.json: run `nuxt prepare` \
1678 before fallow for accurate analysis",
1679 }),
1680 "astro" if !root.join(".astro").exists() => Some(MetaFrameworkWarning {
1681 dedupe_key: "meta-prereq::astro",
1682 message: "Astro project missing .astro/ types: run `astro sync` \
1683 before fallow for accurate analysis",
1684 }),
1685 _ => None,
1686 })
1687 .collect()
1688}
1689
1690fn check_meta_framework_prerequisites(active_plugins: &[&dyn Plugin], root: &Path) {
1700 for warning in missing_meta_framework_prerequisites(active_plugins, root) {
1701 if should_warn(warning.dedupe_key.to_owned()) {
1702 tracing::warn!("{}", warning.message);
1703 }
1704 }
1705}
1706
1707fn script_activation_packages(
1708 pkg: &PackageJson,
1709 root: &Path,
1710 all_deps: &[String],
1711 production_mode: bool,
1712) -> FxHashSet<String> {
1713 let Some(pkg_scripts) = pkg.scripts.as_ref() else {
1714 return FxHashSet::default();
1715 };
1716
1717 let scripts_to_analyze = if production_mode {
1718 scripts::filter_production_scripts(pkg_scripts)
1719 } else {
1720 pkg_scripts.clone()
1721 };
1722
1723 let mut nm_roots = Vec::new();
1724 if root.join("node_modules").is_dir() {
1725 nm_roots.push(root);
1726 }
1727 let bin_map = scripts::build_bin_to_package_map(&nm_roots, all_deps);
1728 let dep_set: FxHashSet<String> = all_deps.iter().cloned().collect();
1729 let catalog =
1730 scripts::ScriptCatalog::from_scripts_with_bodies(pkg_scripts, &scripts_to_analyze);
1731
1732 scripts::analyze_scripts_with_dependency_context(
1733 &scripts_to_analyze,
1734 root,
1735 &bin_map,
1736 &dep_set,
1737 &catalog,
1738 scripts::IgnoredCommandEntries::NONE,
1739 )
1740 .used_packages
1741}
1742
1743#[cfg(test)]
1744mod entry_pattern_tests;
1745#[cfg(test)]
1746mod tests;