1use rustc_hash::FxHashSet;
4use std::path::{Path, PathBuf};
5
6use fallow_config::{
7 AutoImportRule, EntryPointRole, ExternalPluginDef, PackageJson, UsedClassMemberRule,
8};
9
10use crate::scripts;
11
12use super::{PathRule, Plugin, PluginUsedExportRule, ProvidedDependencyRule};
13
14pub(crate) mod builtin;
15mod helpers;
16
17use helpers::{
18 check_has_config_file, discover_config_files, is_external_plugin_active,
19 prepare_config_pattern, process_config_result, process_external_plugins,
20 process_static_patterns,
21};
22
23fn must_parse_workspace_config_when_root_active(plugin_name: &str) -> bool {
24 matches!(
25 plugin_name,
26 "eslint" | "docusaurus" | "jest" | "tanstack-router" | "vitest"
27 )
28}
29
30pub struct PluginRegistry {
32 plugins: Vec<Box<dyn Plugin>>,
33 external_plugins: Vec<ExternalPluginDef>,
34}
35
36#[derive(Debug, Clone, Default)]
38pub struct AggregatedPluginResult {
39 pub entry_patterns: Vec<(PathRule, String)>,
41 pub entry_point_roles: rustc_hash::FxHashMap<String, EntryPointRole>,
43 pub config_patterns: Vec<String>,
45 pub always_used: Vec<(String, String)>,
47 pub used_exports: Vec<PluginUsedExportRule>,
49 pub used_class_members: Vec<UsedClassMemberRule>,
53 pub referenced_dependencies: Vec<String>,
55 pub discovered_always_used: Vec<(String, String)>,
57 pub setup_files: Vec<(PathBuf, String)>,
59 pub tooling_dependencies: Vec<String>,
61 pub script_used_packages: FxHashSet<String>,
63 pub virtual_module_prefixes: Vec<String>,
66 pub virtual_package_suffixes: Vec<String>,
69 pub generated_import_patterns: Vec<String>,
72 pub generated_type_import_prefixes: Vec<String>,
75 pub path_aliases: Vec<(String, String)>,
78 pub auto_imports: Vec<AutoImportRule>,
82 pub active_plugins: Vec<String>,
84 pub fixture_patterns: Vec<(String, String)>,
86 pub scss_include_paths: Vec<PathBuf>,
91 pub static_dir_mappings: Vec<(PathBuf, String)>,
93 pub provided_dependencies: Vec<ProvidedDependencyRule>,
95}
96
97fn extend_unique(target: &mut Vec<String>, incoming: Vec<String>) {
102 let mut seen: FxHashSet<String> = target.iter().cloned().collect();
103 for item in incoming {
104 if seen.insert(item.clone()) {
105 target.push(item);
106 }
107 }
108}
109
110fn prefix_if_needed(pat: &str, ws_prefix: &str) -> String {
114 if pat.starts_with(ws_prefix) || pat.starts_with('/') {
115 pat.to_string()
116 } else {
117 format!("{ws_prefix}/{pat}")
118 }
119}
120
121impl AggregatedPluginResult {
122 pub fn apply_workspace_prefix(&mut self, ws_prefix: &str) {
136 for (rule, _) in &mut self.entry_patterns {
137 *rule = rule.prefixed(ws_prefix);
138 }
139 for (pat, _) in &mut self.always_used {
140 *pat = prefix_if_needed(pat, ws_prefix);
141 }
142 for (pat, _) in &mut self.discovered_always_used {
143 *pat = prefix_if_needed(pat, ws_prefix);
144 }
145 for (pat, _) in &mut self.fixture_patterns {
146 *pat = prefix_if_needed(pat, ws_prefix);
147 }
148 for rule in &mut self.used_exports {
149 *rule = rule.prefixed(ws_prefix);
150 }
151 for rule in &mut self.provided_dependencies {
152 *rule = rule.prefixed(ws_prefix);
153 }
154 for (_, replacement) in &mut self.path_aliases {
155 *replacement = format!("{ws_prefix}/{replacement}");
156 }
157 }
158
159 pub fn merge_into(&mut self, other: Self) {
172 let Self {
173 entry_patterns,
174 entry_point_roles,
175 config_patterns,
176 always_used,
177 used_exports,
178 used_class_members,
179 referenced_dependencies,
180 discovered_always_used,
181 setup_files,
182 tooling_dependencies,
183 script_used_packages,
184 virtual_module_prefixes,
185 virtual_package_suffixes,
186 generated_import_patterns,
187 generated_type_import_prefixes,
188 path_aliases,
189 auto_imports,
190 active_plugins,
191 fixture_patterns,
192 scss_include_paths,
193 static_dir_mappings,
194 provided_dependencies,
195 } = other;
196
197 self.entry_patterns.extend(entry_patterns);
198 for (plugin_name, role) in entry_point_roles {
199 self.entry_point_roles.entry(plugin_name).or_insert(role);
200 }
201 self.config_patterns.extend(config_patterns);
202 self.always_used.extend(always_used);
203 self.used_exports.extend(used_exports);
204 self.used_class_members.extend(used_class_members);
205 self.referenced_dependencies.extend(referenced_dependencies);
206 self.discovered_always_used.extend(discovered_always_used);
207 self.setup_files.extend(setup_files);
208 self.tooling_dependencies.extend(tooling_dependencies);
209 self.script_used_packages.extend(script_used_packages);
210 extend_unique(&mut self.virtual_module_prefixes, virtual_module_prefixes);
211 extend_unique(&mut self.virtual_package_suffixes, virtual_package_suffixes);
212 extend_unique(
213 &mut self.generated_import_patterns,
214 generated_import_patterns,
215 );
216 extend_unique(
217 &mut self.generated_type_import_prefixes,
218 generated_type_import_prefixes,
219 );
220 self.path_aliases.extend(path_aliases);
221 self.auto_imports.extend(auto_imports);
222 extend_unique(&mut self.active_plugins, active_plugins);
223 self.fixture_patterns.extend(fixture_patterns);
224 self.scss_include_paths.extend(scss_include_paths);
225 self.static_dir_mappings.extend(static_dir_mappings);
226 self.provided_dependencies.extend(provided_dependencies);
227 }
228}
229
230impl PluginRegistry {
231 #[must_use]
233 pub fn new(external: Vec<ExternalPluginDef>) -> Self {
234 Self {
235 plugins: builtin::create_builtin_plugins(),
236 external_plugins: external,
237 }
238 }
239
240 #[must_use]
245 pub fn discovery_hidden_dirs(&self, pkg: &PackageJson, root: &Path) -> Vec<String> {
246 let all_deps = pkg.all_dependency_names();
247 let mut seen = FxHashSet::default();
248 let mut dirs = Vec::new();
249
250 for plugin in &self.plugins {
251 if !plugin.is_enabled_with_deps(&all_deps, root) {
252 continue;
253 }
254 for dir in plugin.discovery_hidden_dirs() {
255 if seen.insert(*dir) {
256 dirs.push((*dir).to_string());
257 }
258 }
259 }
260
261 dirs
262 }
263
264 pub fn run(
269 &self,
270 pkg: &PackageJson,
271 root: &Path,
272 discovered_files: &[PathBuf],
273 ) -> AggregatedPluginResult {
274 self.run_with_search_roots(pkg, root, discovered_files, &[root], false)
275 }
276
277 pub fn run_with_search_roots(
289 &self,
290 pkg: &PackageJson,
291 root: &Path,
292 discovered_files: &[PathBuf],
293 config_search_roots: &[&Path],
294 production_mode: bool,
295 ) -> AggregatedPluginResult {
296 let _span = tracing::info_span!("run_plugins").entered();
297 let mut result = AggregatedPluginResult::default();
298
299 let all_deps = pkg.all_dependency_names();
300 let script_packages = script_activation_packages(pkg, root, &all_deps, production_mode);
301 let active: Vec<&dyn Plugin> = self
302 .plugins
303 .iter()
304 .filter(|p| {
305 p.is_enabled_with_files(&all_deps, root, discovered_files)
306 || p.is_enabled_with_scripts(&script_packages, root)
307 })
308 .map(AsRef::as_ref)
309 .collect();
310
311 tracing::info!(
312 plugins = active
313 .iter()
314 .map(|p| p.name())
315 .collect::<Vec<_>>()
316 .join(", "),
317 "active plugins"
318 );
319
320 check_meta_framework_prerequisites(&active, root);
321
322 self.emit_silent_fail_diagnostics(&active, &all_deps, root, discovered_files);
323
324 for plugin in &active {
325 process_static_patterns(*plugin, root, &mut result);
326 }
327
328 process_external_plugins(
329 &self.external_plugins,
330 &all_deps,
331 root,
332 discovered_files,
333 &mut result,
334 );
335
336 let config_matchers: Vec<(&dyn Plugin, Vec<globset::GlobMatcher>)> = active
337 .iter()
338 .filter(|p| !p.config_patterns().is_empty())
339 .map(|p| {
340 let matchers: Vec<globset::GlobMatcher> = p
341 .config_patterns()
342 .iter()
343 .filter_map(|pat| {
344 let prepared = prepare_config_pattern(pat);
345 globset::Glob::new(&prepared)
346 .ok()
347 .map(|g| g.compile_matcher())
348 })
349 .collect();
350 (*p, matchers)
351 })
352 .collect();
353
354 use rayon::prelude::*;
355 let needs_relative_files = !config_matchers.is_empty()
356 || active.iter().any(|p| p.package_json_config_key().is_some());
357 let relative_files: Vec<(PathBuf, String)> = if needs_relative_files {
358 discovered_files
359 .par_iter()
360 .map(|f| {
361 let rel = f
362 .strip_prefix(root)
363 .unwrap_or(f)
364 .to_string_lossy()
365 .into_owned();
366 (f.clone(), rel)
367 })
368 .collect()
369 } else {
370 Vec::new()
371 };
372
373 if !config_matchers.is_empty() {
374 let mut resolved_plugins: FxHashSet<&str> = FxHashSet::default();
375
376 for (plugin, matchers) in &config_matchers {
377 let plugin_hits: Vec<&PathBuf> = relative_files
378 .par_iter()
379 .filter_map(|(abs_path, rel_path)| {
380 matchers
381 .iter()
382 .any(|m| m.is_match(rel_path.as_str()))
383 .then_some(abs_path)
384 })
385 .collect();
386 for abs_path in plugin_hits {
387 if let Ok(source) = std::fs::read_to_string(abs_path) {
388 let plugin_result = plugin.resolve_config(abs_path, &source, root);
389 if !plugin_result.is_empty() {
390 resolved_plugins.insert(plugin.name());
391 tracing::debug!(
392 plugin = plugin.name(),
393 config = %abs_path.display(),
394 entries = plugin_result.entry_patterns.len(),
395 deps = plugin_result.referenced_dependencies.len(),
396 "resolved config"
397 );
398 process_config_result(
399 plugin.name(),
400 plugin_result,
401 &mut result,
402 Some(abs_path),
403 );
404 }
405 }
406 }
407 }
408
409 let json_configs = discover_config_files(
410 &config_matchers,
411 &resolved_plugins,
412 config_search_roots,
413 production_mode,
414 );
415 for (abs_path, plugin) in &json_configs {
416 if let Ok(source) = std::fs::read_to_string(abs_path) {
417 let plugin_result = plugin.resolve_config(abs_path, &source, root);
418 if !plugin_result.is_empty() {
419 let rel = abs_path
420 .strip_prefix(root)
421 .map(|p| p.to_string_lossy())
422 .unwrap_or_default();
423 tracing::debug!(
424 plugin = plugin.name(),
425 config = %rel,
426 entries = plugin_result.entry_patterns.len(),
427 deps = plugin_result.referenced_dependencies.len(),
428 "resolved config (filesystem fallback)"
429 );
430 process_config_result(
431 plugin.name(),
432 plugin_result,
433 &mut result,
434 Some(abs_path),
435 );
436 }
437 }
438 }
439 }
440
441 process_package_json_inline_configs(
442 &active,
443 &config_matchers,
444 &relative_files,
445 root,
446 &mut result,
447 );
448
449 result
450 }
451
452 #[expect(
458 clippy::too_many_arguments,
459 reason = "Each parameter is a distinct, small value with no natural grouping; \
460 bundling them into a struct hurts call-site readability."
461 )]
462 pub fn run_workspace_fast(
463 &self,
464 pkg: &PackageJson,
465 root: &Path,
466 project_root: &Path,
467 precompiled_config_matchers: &[(&dyn Plugin, Vec<globset::GlobMatcher>)],
468 relative_files: &[(PathBuf, String)],
469 skip_config_plugins: &FxHashSet<&str>,
470 production_mode: bool,
471 ) -> AggregatedPluginResult {
472 let _span = tracing::info_span!("run_plugins").entered();
473 let mut result = AggregatedPluginResult::default();
474
475 let all_deps = pkg.all_dependency_names();
476 let script_packages = script_activation_packages(pkg, root, &all_deps, production_mode);
477 let workspace_files: Vec<PathBuf> = relative_files
478 .iter()
479 .map(|(abs_path, _)| abs_path.clone())
480 .collect();
481
482 let active: Vec<&dyn Plugin> = self
483 .plugins
484 .iter()
485 .filter(|p| {
486 p.is_enabled_with_files(&all_deps, root, &workspace_files)
487 || p.is_enabled_with_scripts(&script_packages, root)
488 })
489 .map(AsRef::as_ref)
490 .collect();
491
492 tracing::info!(
493 plugins = active
494 .iter()
495 .map(|p| p.name())
496 .collect::<Vec<_>>()
497 .join(", "),
498 "active plugins"
499 );
500
501 self.emit_silent_fail_diagnostics(&active, &all_deps, root, &workspace_files);
502
503 process_external_plugins(
504 &self.external_plugins,
505 &all_deps,
506 root,
507 &workspace_files,
508 &mut result,
509 );
510
511 if active.is_empty() && result.active_plugins.is_empty() {
512 return result;
513 }
514
515 for plugin in &active {
516 process_static_patterns(*plugin, root, &mut result);
517 }
518
519 let active_names: FxHashSet<&str> = active.iter().map(|p| p.name()).collect();
520 let workspace_matchers: Vec<_> = precompiled_config_matchers
521 .iter()
522 .filter(|(p, _)| {
523 active_names.contains(p.name())
524 && (!skip_config_plugins.contains(p.name())
525 || must_parse_workspace_config_when_root_active(p.name()))
526 })
527 .map(|(plugin, matchers)| (*plugin, matchers.clone()))
528 .collect();
529
530 let mut resolved_ws_plugins: FxHashSet<&str> = FxHashSet::default();
531 if !workspace_matchers.is_empty() {
532 use rayon::prelude::*;
533 for (plugin, matchers) in &workspace_matchers {
534 let plugin_hits: Vec<&PathBuf> = relative_files
535 .par_iter()
536 .filter_map(|(abs_path, rel_path)| {
537 matchers
538 .iter()
539 .any(|m| m.is_match(rel_path.as_str()))
540 .then_some(abs_path)
541 })
542 .collect();
543 for abs_path in plugin_hits {
544 if let Ok(source) = std::fs::read_to_string(abs_path) {
545 let plugin_result = plugin.resolve_config(abs_path, &source, root);
546 if !plugin_result.is_empty() {
547 resolved_ws_plugins.insert(plugin.name());
548 tracing::debug!(
549 plugin = plugin.name(),
550 config = %abs_path.display(),
551 entries = plugin_result.entry_patterns.len(),
552 deps = plugin_result.referenced_dependencies.len(),
553 "resolved config"
554 );
555 process_config_result(
556 plugin.name(),
557 plugin_result,
558 &mut result,
559 Some(abs_path),
560 );
561 }
562 }
563 }
564 }
565 }
566
567 let ws_json_configs = if root == project_root {
568 discover_config_files(
569 &workspace_matchers,
570 &resolved_ws_plugins,
571 &[root],
572 production_mode,
573 )
574 } else {
575 discover_config_files(
576 &workspace_matchers,
577 &resolved_ws_plugins,
578 &[root, project_root],
579 production_mode,
580 )
581 };
582 for (abs_path, plugin) in &ws_json_configs {
583 if let Ok(source) = std::fs::read_to_string(abs_path) {
584 let plugin_result = plugin.resolve_config(abs_path, &source, root);
585 if !plugin_result.is_empty() {
586 let rel = abs_path
587 .strip_prefix(project_root)
588 .map(|p| p.to_string_lossy())
589 .unwrap_or_default();
590 tracing::debug!(
591 plugin = plugin.name(),
592 config = %rel,
593 entries = plugin_result.entry_patterns.len(),
594 deps = plugin_result.referenced_dependencies.len(),
595 "resolved config (workspace filesystem fallback)"
596 );
597 process_config_result(
598 plugin.name(),
599 plugin_result,
600 &mut result,
601 Some(abs_path),
602 );
603 }
604 }
605 }
606
607 result
608 }
609
610 #[must_use]
613 pub fn precompile_config_matchers(&self) -> Vec<(&dyn Plugin, Vec<globset::GlobMatcher>)> {
614 self.plugins
615 .iter()
616 .filter(|p| !p.config_patterns().is_empty())
617 .map(|p| {
618 let matchers: Vec<globset::GlobMatcher> = p
619 .config_patterns()
620 .iter()
621 .filter_map(|pat| {
622 let prepared = prepare_config_pattern(pat);
623 globset::Glob::new(&prepared)
624 .ok()
625 .map(|g| g.compile_matcher())
626 })
627 .collect();
628 (p.as_ref(), matchers)
629 })
630 .collect()
631 }
632}
633
634impl Default for PluginRegistry {
635 fn default() -> Self {
636 Self::new(vec![])
637 }
638}
639
640impl PluginRegistry {
641 fn emit_silent_fail_diagnostics(
650 &self,
651 active: &[&dyn Plugin],
652 all_deps: &[String],
653 root: &Path,
654 discovered_files: &[PathBuf],
655 ) {
656 let active_external: Vec<&ExternalPluginDef> = self
657 .external_plugins
658 .iter()
659 .filter(|ext| is_external_plugin_active(ext, all_deps, root, discovered_files))
660 .collect();
661 let mut diagnostics = detect_pattern_collisions(active, &active_external);
662 diagnostics.extend(detect_enabler_typos(&self.external_plugins, all_deps));
663 emit_plugin_diagnostics(&diagnostics);
664 }
665}
666
667fn plugin_warn_dedupe() -> &'static std::sync::Mutex<FxHashSet<String>> {
674 static WARNED: std::sync::OnceLock<std::sync::Mutex<FxHashSet<String>>> =
675 std::sync::OnceLock::new();
676 WARNED.get_or_init(|| std::sync::Mutex::new(FxHashSet::default()))
677}
678
679fn should_warn(key: String) -> bool {
683 plugin_warn_dedupe()
684 .lock()
685 .map_or(true, |mut set| set.insert(key))
686}
687
688#[derive(Debug, Clone, PartialEq, Eq)]
695pub(crate) enum PluginDiagnostic {
696 PatternCollision {
698 pattern: String,
699 owners: Vec<String>,
700 },
701 EnablerTypo {
704 plugin: String,
705 enabler: String,
706 suggestion: String,
707 },
708}
709
710pub(crate) fn detect_pattern_collisions(
736 builtin_active: &[&dyn Plugin],
737 external_active: &[&ExternalPluginDef],
738) -> Vec<PluginDiagnostic> {
739 use rustc_hash::FxHashMap;
740
741 let mut pattern_owners: FxHashMap<String, (Vec<String>, FxHashSet<String>)> =
742 FxHashMap::default();
743
744 let record = |pattern_owners: &mut FxHashMap<_, (Vec<String>, FxHashSet<String>)>,
745 pattern: String,
746 name: String| {
747 let (list, seen) = pattern_owners.entry(pattern).or_default();
748 if seen.insert(name.clone()) {
749 list.push(name);
750 }
751 };
752
753 for plugin in builtin_active {
754 for pat in plugin.config_patterns() {
755 record(
756 &mut pattern_owners,
757 (*pat).to_string(),
758 plugin.name().to_string(),
759 );
760 }
761 }
762 for ext in external_active {
763 for pat in &ext.config_patterns {
764 record(&mut pattern_owners, pat.clone(), ext.name.clone());
765 }
766 }
767
768 let builtin_names: FxHashSet<&str> = builtin_active.iter().map(|p| p.name()).collect();
775
776 let mut findings: Vec<PluginDiagnostic> = pattern_owners
777 .into_iter()
778 .filter_map(|(pattern, (owners, _seen))| {
779 if owners.len() < 2 || owners.iter().all(|o| builtin_names.contains(o.as_str())) {
780 None
781 } else {
782 Some(PluginDiagnostic::PatternCollision { pattern, owners })
783 }
784 })
785 .collect();
786 findings.sort_unstable_by(|a, b| match (a, b) {
787 (
788 PluginDiagnostic::PatternCollision { pattern: ap, .. },
789 PluginDiagnostic::PatternCollision { pattern: bp, .. },
790 ) => ap.cmp(bp),
791 _ => std::cmp::Ordering::Equal,
792 });
793 findings
794}
795
796pub(crate) fn detect_enabler_typos(
811 external_plugins: &[ExternalPluginDef],
812 all_deps: &[String],
813) -> Vec<PluginDiagnostic> {
814 let mut findings = Vec::new();
815
816 for ext in external_plugins {
817 if ext.detection.is_some() || ext.enablers.is_empty() {
818 continue;
819 }
820
821 let any_match = ext.enablers.iter().any(|enabler| {
822 if enabler.ends_with('/') {
823 all_deps.iter().any(|d| d.starts_with(enabler))
824 } else {
825 all_deps.iter().any(|d| d == enabler)
826 }
827 });
828 if any_match {
829 continue;
830 }
831
832 for enabler in &ext.enablers {
833 let candidates = all_deps.iter().map(String::as_str);
834 let Some(suggestion) = fallow_config::levenshtein::closest_match(enabler, candidates)
835 else {
836 continue;
837 };
838
839 findings.push(PluginDiagnostic::EnablerTypo {
840 plugin: ext.name.clone(),
841 enabler: enabler.clone(),
842 suggestion: suggestion.to_string(),
843 });
844 }
845 }
846
847 findings
848}
849
850fn emit_plugin_diagnostics(findings: &[PluginDiagnostic]) {
853 for finding in findings {
854 match finding {
855 PluginDiagnostic::PatternCollision { pattern, owners } => {
856 let key = format!("collision::{pattern}::{owners:?}");
857 if !should_warn(key) {
858 continue;
859 }
860 let winner = &owners[0];
861 let others = owners[1..].join(", ");
862 tracing::warn!(
863 "plugin config_patterns collision: identical pattern \
864 '{pattern}' is claimed by plugins [{joined}]; '{winner}' \
865 runs first (registration order), others ({others}) \
866 follow. Rename one of the patterns or remove the \
867 duplicate plugin to make resolution explicit. A future \
868 release may reject identical-pattern collisions.",
869 joined = owners.join(", "),
870 );
871 }
872 PluginDiagnostic::EnablerTypo {
873 plugin,
874 enabler,
875 suggestion,
876 } => {
877 let key = format!("enabler::{plugin}::{enabler}");
878 if !should_warn(key) {
879 continue;
880 }
881 tracing::warn!(
882 "plugin '{plugin}' enabler '{enabler}' does not match any \
883 dependency in package.json; did you mean '{suggestion}'? \
884 The plugin will not activate. A future release may reject \
885 unmatched enablers.",
886 );
887 }
888 }
889 }
890}
891
892fn process_package_json_inline_configs(
897 active: &[&dyn Plugin],
898 config_matchers: &[(&dyn Plugin, Vec<globset::GlobMatcher>)],
899 relative_files: &[(PathBuf, String)],
900 root: &Path,
901 result: &mut AggregatedPluginResult,
902) {
903 for plugin in active {
904 let Some(key) = plugin.package_json_config_key() else {
905 continue;
906 };
907 if check_has_config_file(*plugin, config_matchers, relative_files) {
908 continue;
909 }
910 let pkg_path = root.join("package.json");
911 let Ok(content) = std::fs::read_to_string(&pkg_path) else {
912 continue;
913 };
914 let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
915 continue;
916 };
917 let Some(config_value) = json.get(key) else {
918 continue;
919 };
920 let config_json = serde_json::to_string(config_value).unwrap_or_default();
921 let fake_path = root.join(format!("{key}.config.json"));
922 let plugin_result = plugin.resolve_config(&fake_path, &config_json, root);
923 if plugin_result.is_empty() {
924 continue;
925 }
926 tracing::debug!(
927 plugin = plugin.name(),
928 key = key,
929 "resolved inline package.json config"
930 );
931 process_config_result(plugin.name(), plugin_result, result, Some(&pkg_path));
932 }
933}
934
935#[derive(Debug)]
938struct MetaFrameworkWarning {
939 dedupe_key: &'static str,
940 message: &'static str,
941}
942
943fn missing_meta_framework_prerequisites(
953 active_plugins: &[&dyn Plugin],
954 root: &Path,
955) -> Vec<MetaFrameworkWarning> {
956 active_plugins
957 .iter()
958 .filter_map(|plugin| match plugin.name() {
959 "nuxt" if !root.join(".nuxt/tsconfig.json").exists() => Some(MetaFrameworkWarning {
960 dedupe_key: "meta-prereq::nuxt",
961 message: "Nuxt project missing .nuxt/tsconfig.json: run `nuxt prepare` \
962 before fallow for accurate analysis",
963 }),
964 "astro" if !root.join(".astro").exists() => Some(MetaFrameworkWarning {
965 dedupe_key: "meta-prereq::astro",
966 message: "Astro project missing .astro/ types: run `astro sync` \
967 before fallow for accurate analysis",
968 }),
969 _ => None,
970 })
971 .collect()
972}
973
974fn check_meta_framework_prerequisites(active_plugins: &[&dyn Plugin], root: &Path) {
984 for warning in missing_meta_framework_prerequisites(active_plugins, root) {
985 if should_warn(warning.dedupe_key.to_owned()) {
986 tracing::warn!("{}", warning.message);
987 }
988 }
989}
990
991fn script_activation_packages(
992 pkg: &PackageJson,
993 root: &Path,
994 all_deps: &[String],
995 production_mode: bool,
996) -> FxHashSet<String> {
997 let Some(pkg_scripts) = pkg.scripts.as_ref() else {
998 return FxHashSet::default();
999 };
1000
1001 let scripts_to_analyze = if production_mode {
1002 scripts::filter_production_scripts(pkg_scripts)
1003 } else {
1004 pkg_scripts.clone()
1005 };
1006
1007 let mut nm_roots = Vec::new();
1008 if root.join("node_modules").is_dir() {
1009 nm_roots.push(root);
1010 }
1011 let bin_map = scripts::build_bin_to_package_map(&nm_roots, all_deps);
1012
1013 scripts::analyze_scripts(&scripts_to_analyze, root, &bin_map).used_packages
1014}
1015
1016#[cfg(test)]
1017mod tests;