1use std::path::{Path, PathBuf};
9
10use fallow_config::{
11 AutoImportRule, EntryPointRole, JsxImportSourceRule, PackageJson, UsedClassMemberRule,
12};
13use fallow_types::semantic::SemanticFrameworkContract;
14use regex::Regex;
15
16const TEST_ENTRY_POINT_PLUGINS: &[&str] = &[
17 "ava",
18 "bun",
19 "deno",
20 "cucumber",
21 "cypress",
22 "jasmine",
23 "jest",
24 "k6",
25 "mocha",
26 "playwright",
27 "rstest",
28 "tap",
29 "tsd",
30 "vitest",
31 "webdriverio",
32];
33
34const RUNTIME_ENTRY_POINT_PLUGINS: &[&str] = &[
35 "adonis",
36 "angular",
37 "astro",
38 "browser-extension",
39 "convex",
40 "docusaurus",
41 "egg",
42 "electron",
43 "ember",
44 "eve",
45 "expo",
46 "expo-router",
47 "flue",
48 "gatsby",
49 "hardhat",
50 "kibana",
51 "module-federation",
52 "nestjs",
53 "next-intl",
54 "nextjs",
55 "nitro",
56 "nuxt",
57 "obsidian",
58 "parcel",
59 "qwik",
60 "react-native",
61 "react-router",
62 "redwoodsdk",
63 "remix",
64 "rolldown",
65 "rollup",
66 "rsbuild",
67 "rspack",
68 "sanity",
69 "supabase",
70 "sveltekit",
71 "tanstack-router",
72 "tsdown",
73 "tsup",
74 "vercel",
75 "vite",
76 "vite-plus",
77 "vitepress",
78 "waku",
79 "webpack",
80 "wrangler",
81 "wxt",
82];
83
84#[cfg(test)]
87pub(crate) fn entry_rules_match(result: &PluginResult, path: &str) -> bool {
88 result
89 .entry_patterns
90 .iter()
91 .filter_map(|rule| CompiledPathRule::for_entry_rule(rule, "entry pattern"))
92 .any(|rule| rule.matches(path))
93}
94
95#[cfg(test)]
96const SUPPORT_ENTRY_POINT_PLUGINS: &[&str] = &[
97 "alchemy",
98 "content-collections",
99 "contentlayer",
100 "danger",
101 "drizzle",
102 "fumadocs",
103 "i18next",
104 "knex",
105 "kysely",
106 "mintlify",
107 "msw",
108 "opencode",
109 "prisma",
110 "storybook",
111 "stryker",
112 "typeorm",
113 "velite",
114];
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum PluginConfigEffect {
119 Unreadable,
122 NotModeled,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct PluginConfigDiagnostic {
139 pub config_path: PathBuf,
141 pub plugin: String,
145 pub key: String,
148 pub reason: String,
150 pub effect: PluginConfigEffect,
152}
153
154impl PluginConfigDiagnostic {
155 pub(super) fn unreadable(
157 config_path: &Path,
158 plugin: &str,
159 key: &str,
160 reason: &'static str,
161 ) -> Self {
162 Self {
163 config_path: config_path.to_path_buf(),
164 plugin: plugin.to_owned(),
165 key: key.to_owned(),
166 reason: reason.to_owned(),
167 effect: PluginConfigEffect::Unreadable,
168 }
169 }
170
171 pub(crate) fn not_modeled(
173 config_path: &Path,
174 plugin: &str,
175 key: &str,
176 reason: &'static str,
177 ) -> Self {
178 Self {
179 config_path: config_path.to_path_buf(),
180 plugin: plugin.to_owned(),
181 key: key.to_owned(),
182 reason: reason.to_owned(),
183 effect: PluginConfigEffect::NotModeled,
184 }
185 }
186
187 #[must_use]
190 pub fn into_workspace_diagnostic(self, root: &Path) -> fallow_config::WorkspaceDiagnostic {
191 let Self {
192 config_path,
193 plugin,
194 key,
195 reason,
196 effect,
197 } = self;
198 let kind = match effect {
199 PluginConfigEffect::Unreadable => {
200 fallow_config::WorkspaceDiagnosticKind::PluginConfigUnreadable {
201 plugin,
202 key,
203 reason,
204 }
205 }
206 PluginConfigEffect::NotModeled => {
207 fallow_config::WorkspaceDiagnosticKind::PluginEffectNotModeled {
208 plugin,
209 key,
210 reason,
211 }
212 }
213 };
214 fallow_config::WorkspaceDiagnostic::new(root, config_path, kind)
215 }
216}
217
218#[derive(Debug, Default)]
220pub struct PluginResult {
221 entry_patterns: Vec<PathRule>,
223 replace_entry_patterns: bool,
228 accumulate_config_entry_patterns: bool,
232 replace_used_export_rules: bool,
235 used_exports: Vec<UsedExportRule>,
237 entry_export_credits: Vec<UsedExportRule>,
242 used_class_members: Vec<UsedClassMemberRule>,
247 referenced_dependencies: Vec<String>,
249 package_referenced_dependencies: Vec<(PathBuf, String)>,
252 always_used_files: Vec<String>,
254 path_aliases: Vec<(String, String)>,
256 setup_files: Vec<PathBuf>,
258 fixture_patterns: Vec<String>,
260 scss_include_paths: Vec<PathBuf>,
268 static_dir_mappings: Vec<(PathBuf, String)>,
271 framework_static_dir_mappings: Vec<(PathBuf, String)>,
272 provided_dependencies: Vec<ProvidedDependencyRule>,
275 config_diagnostics: Vec<PluginConfigDiagnostic>,
279 federation_sources: Vec<FederationSource>,
282 jsx_import_sources: Vec<JsxImportSourceRule>,
286 jsx_package_credits: Vec<JsxImportSourceRule>,
291 tsconfig_jsx_dirs: Vec<PathBuf>,
296}
297
298#[derive(Debug, Clone, PartialEq, Eq)]
300pub struct FederationSource {
301 pub target: FederationSourceTarget,
303 pub config_path: PathBuf,
305 pub plugin: String,
307 pub key: &'static str,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq)]
313pub enum FederationSourceTarget {
314 Exposed(PathRule),
316 Remote(String),
318}
319
320impl FederationSource {
321 #[must_use]
322 fn prefixed(&self, ws_prefix: &str) -> Self {
323 let target = match &self.target {
324 FederationSourceTarget::Exposed(rule) => {
325 FederationSourceTarget::Exposed(rule.prefixed(ws_prefix))
326 }
327 FederationSourceTarget::Remote(alias) => FederationSourceTarget::Remote(alias.clone()),
328 };
329 Self {
330 target,
331 config_path: self.config_path.clone(),
332 plugin: self.plugin.clone(),
333 key: self.key,
334 }
335 }
336}
337
338#[must_use]
346pub fn federation_trace_provenance(
347 root: &Path,
348 files: &[crate::discover::DiscoveredFile],
349 sources: &[FederationSource],
350 modules: &[crate::extract::ModuleInfo],
351) -> fallow_types::trace::TraceProvenance {
352 let mut provenance = fallow_types::trace::TraceProvenance::default();
353 push_runtime_remote_sources(&mut provenance, root, files, modules);
354 if sources.is_empty() {
355 return provenance;
356 }
357 let mut exposed = Vec::new();
358 for source in sources {
359 let config = source
360 .config_path
361 .strip_prefix(root)
362 .unwrap_or(&source.config_path)
363 .to_path_buf();
364 let trace_source = fallow_types::trace::TraceSource {
365 kind: "module-federation".to_owned(),
366 plugin: source.plugin.clone(),
367 config,
368 key: source.key.to_owned(),
369 };
370 match &source.target {
371 FederationSourceTarget::Exposed(rule) => {
372 if let Some(compiled) =
373 CompiledPathRule::for_entry_rule(rule, "Module Federation exposes target")
374 {
375 exposed.push((compiled, trace_source));
376 }
377 }
378 FederationSourceTarget::Remote(alias) => {
379 provenance.push_dependency(alias.clone(), trace_source);
380 }
381 }
382 }
383 if exposed.is_empty() {
384 return provenance;
385 }
386 for file in files {
387 let Ok(relative) = file.path.strip_prefix(root) else {
388 continue;
389 };
390 let relative_str = relative.to_string_lossy().replace('\\', "/");
391 for (rule, source) in &exposed {
392 if rule.matches(&relative_str) {
393 provenance.push_file(relative.to_path_buf(), source.clone());
394 }
395 }
396 }
397 provenance
398}
399
400pub const CONFIG_ONLY_EXPORT_NAMES: &[&str] = &[
409 "dynamic",
410 "dynamicParams",
411 "revalidate",
412 "fetchCache",
413 "runtime",
414 "preferredRegion",
415 "maxDuration",
416 "prerender",
417 "config",
418 "metadata",
419 "viewport",
420];
421
422pub fn push_tooling_trace_credits(
426 provenance: &mut fallow_types::trace::TraceProvenance,
427 root: &Path,
428 workspaces: &[fallow_config::WorkspaceInfo],
429 plugin_result: &AggregatedPluginResult,
430) {
431 for entry in &plugin_result.plugin_tooling {
432 let Some(evidence) = entry.evidence(&plugin_result.script_used_packages) else {
433 continue;
434 };
435 let credit = match evidence {
436 PluginToolingEvidence::OwnConfig(config) => fallow_types::trace::ToolingCredit {
437 reason: "plugin-config".to_owned(),
438 plugin: Some(entry.plugin.clone()),
439 config: Some(config.strip_prefix(root).unwrap_or(config).to_path_buf()),
440 reference: None,
441 },
442 PluginToolingEvidence::Reference(name) => fallow_types::trace::ToolingCredit {
443 reason: "plugin-reference".to_owned(),
444 plugin: Some(entry.plugin.clone()),
445 config: None,
446 reference: Some(name.to_owned()),
447 },
448 };
449 for dependency in &entry.dependencies {
450 provenance.push_tooling_credit(dependency.clone(), credit.clone());
451 }
452 }
453 push_types_trace_credits(provenance, plugin_result);
454 push_catalogue_trace_credits(provenance, root, workspaces, plugin_result);
455}
456
457fn push_catalogue_trace_credits(
467 provenance: &mut fallow_types::trace::TraceProvenance,
468 root: &Path,
469 workspaces: &[fallow_config::WorkspaceInfo],
470 plugin_result: &AggregatedPluginResult,
471) {
472 let manifests = read_trace_manifests(root, workspaces);
473 let mut names: Vec<&String> = plugin_result.dev_dependency_names.iter().collect();
474 names.sort_unstable();
475 for name in names {
476 let declaring: Vec<&(PathBuf, serde_json::Value)> = manifests
477 .iter()
478 .filter(|(_, manifest)| manifest_lists(manifest, "devDependencies", name))
479 .collect();
480 if let Some(credit) = catalogue_credit(name, root, &declaring) {
481 provenance.push_tooling_credit(name.clone(), credit);
482 }
483 if let Some((path, _)) = declaring
484 .iter()
485 .find(|(_, manifest)| manifest_lists(manifest, "peerDependencies", name))
486 {
487 provenance.push_tooling_credit(
488 name.clone(),
489 fallow_types::trace::ToolingCredit {
490 reason: "own-peer".to_owned(),
491 plugin: None,
492 config: Some(path.strip_prefix(root).unwrap_or(path).to_path_buf()),
493 reference: None,
494 },
495 );
496 }
497 }
498}
499
500fn read_trace_manifests(
502 root: &Path,
503 workspaces: &[fallow_config::WorkspaceInfo],
504) -> Vec<(PathBuf, serde_json::Value)> {
505 std::iter::once(root)
506 .chain(
507 workspaces
508 .iter()
509 .map(|ws| ws.root.as_path())
510 .filter(|ws_root| *ws_root != root),
511 )
512 .filter_map(|package_root| {
513 let path = package_root.join("package.json");
514 let content = std::fs::read_to_string(&path).ok()?;
515 serde_json::from_str(&content)
516 .ok()
517 .map(|value| (path, value))
518 })
519 .collect()
520}
521
522fn manifest_lists(manifest: &serde_json::Value, section: &str, name: &str) -> bool {
523 manifest
524 .get(section)
525 .and_then(serde_json::Value::as_object)
526 .is_some_and(|deps| deps.contains_key(name))
527}
528
529fn catalogue_credit(
532 name: &str,
533 root: &Path,
534 declaring: &[&(PathBuf, serde_json::Value)],
535) -> Option<fallow_types::trace::ToolingCredit> {
536 if is_ambient_types_package(name) || types_package_target(name).is_some() {
537 return None;
538 }
539 let Some(patterns) = cli_tooling_config_patterns(name) else {
540 return is_known_tooling_dependency(name).then(|| fallow_types::trace::ToolingCredit {
541 reason: "known-tooling".to_owned(),
542 plugin: None,
543 config: None,
544 reference: None,
545 });
546 };
547 let config = declaring.iter().find_map(|(path, manifest)| {
548 crate::analyze::cli_tool_own_config(name, patterns, root, path, Some(manifest))
549 })?;
550 Some(fallow_types::trace::ToolingCredit {
551 reason: "known-tooling-config".to_owned(),
552 plugin: None,
553 config: Some(config.strip_prefix(root).unwrap_or(&config).to_path_buf()),
554 reference: None,
555 })
556}
557
558fn push_types_trace_credits(
563 provenance: &mut fallow_types::trace::TraceProvenance,
564 plugin_result: &AggregatedPluginResult,
565) {
566 let declared = plugin_result.dependency_binaries.declared_packages();
567 let mut names: Vec<&String> = plugin_result.dev_dependency_names.iter().collect();
568 names.sort_unstable();
569 for name in names {
570 let credit = if is_ambient_types_package(name) {
571 fallow_types::trace::ToolingCredit {
572 reason: "ambient-types".to_owned(),
573 plugin: None,
574 config: None,
575 reference: None,
576 }
577 } else if let Some(target) = types_package_target(name) {
578 if declared.contains(&target) {
579 fallow_types::trace::ToolingCredit {
580 reason: "types-target".to_owned(),
581 plugin: None,
582 config: None,
583 reference: Some(target),
584 }
585 } else if plugin_result.referenced_dependencies.contains(name) {
586 fallow_types::trace::ToolingCredit {
587 reason: "types-config".to_owned(),
588 plugin: None,
589 config: None,
590 reference: None,
591 }
592 } else {
593 continue;
594 }
595 } else {
596 continue;
597 };
598 provenance.push_tooling_credit(name.clone(), credit);
599 }
600}
601
602fn push_runtime_remote_sources(
604 provenance: &mut fallow_types::trace::TraceProvenance,
605 root: &Path,
606 files: &[crate::discover::DiscoveredFile],
607 modules: &[crate::extract::ModuleInfo],
608) {
609 for module in modules {
610 let mut file = None;
611 for fact in module.semantic_facts.iter() {
612 let fallow_types::extract::SemanticFact::FederationRuntimeRemote(fact) = fact else {
613 continue;
614 };
615 let Some(remote) = &fact.remote else {
616 continue;
617 };
618 let Some(path) = file.get_or_insert_with(|| {
619 files.get(module.file_id.0 as usize).map(|file| {
620 file.path
621 .strip_prefix(root)
622 .unwrap_or(&file.path)
623 .to_path_buf()
624 })
625 }) else {
626 break;
627 };
628 provenance.push_dependency(
629 remote.clone(),
630 fallow_types::trace::TraceSource {
631 kind: "module-federation".to_owned(),
632 plugin: "module-federation".to_owned(),
633 config: path.clone(),
634 key: fact.call.name().to_owned(),
635 },
636 );
637 }
638 }
639}
640
641impl PluginResult {
642 fn extend_config_dir_entry_patterns(
647 &mut self,
648 values: Vec<String>,
649 config_path: &Path,
650 root: &Path,
651 ) {
652 let (negated, positive): (Vec<String>, Vec<String>) =
653 values.into_iter().partition(|value| value.starts_with('!'));
654 let excluded: Vec<String> = negated
655 .iter()
656 .filter_map(|value| value.strip_prefix('!'))
657 .filter_map(|value| config_parser::normalize_config_path(value, config_path, root))
658 .collect();
659 self.entry_patterns.extend(
660 positive
661 .iter()
662 .filter_map(|value| config_parser::normalize_config_path(value, config_path, root))
663 .map(|pattern| {
664 PathRule::new(pattern).with_excluded_globs(excluded.iter().cloned())
665 }),
666 );
667 }
668
669 fn push_parent_relative_entry_pattern(&mut self, pattern: String) {
672 let mut rule = PathRule::new(pattern);
673 rule.parent_relative = true;
674 self.entry_patterns.push(rule);
675 }
676
677 fn push_entry_pattern(&mut self, pattern: impl Into<String>) {
678 self.entry_patterns
679 .push(PathRule::new(normalize_entry_pattern(pattern.into())));
680 }
681
682 fn extend_entry_patterns<I, S>(&mut self, patterns: I)
683 where
684 I: IntoIterator<Item = S>,
685 S: Into<String>,
686 {
687 self.entry_patterns.extend(
688 patterns
689 .into_iter()
690 .map(|pat| PathRule::new(normalize_entry_pattern(pat.into()))),
691 );
692 }
693
694 fn extend_entry_patterns_or_dependencies<I, S>(
709 &mut self,
710 values: I,
711 resolve_path: impl Fn(String) -> String,
712 ) where
713 I: IntoIterator<Item = S>,
714 S: Into<String>,
715 {
716 for value in values {
717 let value = value.into();
718 if let Some(request) = module_request(&value) {
719 self.referenced_dependencies
720 .push(crate::resolve::extract_package_name(request));
721 continue;
722 }
723 self.push_entry_path(resolve_path(value));
724 }
725 }
726
727 fn extend_entry_patterns_and_dependencies<I, S>(&mut self, values: I, root: &Path)
739 where
740 I: IntoIterator<Item = S>,
741 S: Into<String>,
742 {
743 for value in values {
744 let value = value.into();
745 if let Some(request) = module_request(&value)
746 && !names_project_file(root, request)
747 {
748 self.referenced_dependencies
749 .push(crate::resolve::extract_package_name(request));
750 }
751 self.push_entry_path(value);
752 }
753 }
754
755 fn extend_entry_paths<I, S>(&mut self, values: I)
757 where
758 I: IntoIterator<Item = S>,
759 S: Into<String>,
760 {
761 for value in values {
762 self.push_entry_path(value.into());
763 }
764 }
765
766 fn push_entry_path(&mut self, value: String) {
774 if has_glob_syntax(&value) || has_source_extension(&value) {
775 self.push_entry_pattern(value);
776 return;
777 }
778 let base = value.trim_end_matches('/').to_owned();
779 self.push_entry_pattern(value);
780 self.push_entry_pattern(format!("{base}.{REQUEST_EXTENSIONS}"));
781 self.push_entry_pattern(format!("{base}/index.{REQUEST_EXTENSIONS}"));
782 }
783
784 #[must_use]
791 const fn is_empty(&self) -> bool {
792 self.config_diagnostics.is_empty()
793 && self.entry_patterns.is_empty()
794 && self.used_exports.is_empty()
795 && self.entry_export_credits.is_empty()
796 && self.used_class_members.is_empty()
797 && self.referenced_dependencies.is_empty()
798 && self.package_referenced_dependencies.is_empty()
799 && self.always_used_files.is_empty()
800 && self.path_aliases.is_empty()
801 && self.setup_files.is_empty()
802 && self.fixture_patterns.is_empty()
803 && self.scss_include_paths.is_empty()
804 && self.static_dir_mappings.is_empty()
805 && self.framework_static_dir_mappings.is_empty()
806 && self.provided_dependencies.is_empty()
807 && self.federation_sources.is_empty()
808 && self.jsx_import_sources.is_empty()
809 && self.jsx_package_credits.is_empty()
810 && self.tsconfig_jsx_dirs.is_empty()
811 }
812}
813
814fn names_project_file(root: &Path, value: &str) -> bool {
818 let base = root.join(value);
819 base.is_file()
820 || crate::discover::SOURCE_EXTENSIONS.iter().any(|extension| {
821 let mut candidate = base.clone().into_os_string();
822 candidate.push(".");
823 candidate.push(extension);
824 Path::new(&candidate).is_file() || base.join(format!("index.{extension}")).is_file()
825 })
826}
827
828const REQUEST_EXTENSIONS: &str = "{ts,tsx,mts,cts,gts,js,jsx,mjs,cjs,gjs,vue,svelte,astro,mdx}";
832
833fn normalize_entry_pattern(pattern: String) -> String {
834 pattern
835 .strip_prefix("./")
836 .map(str::to_owned)
837 .unwrap_or(pattern)
838}
839
840fn module_request(value: &str) -> Option<&str> {
851 let request = strip_resource_query(value);
852 (config_parser::is_package_specifier(request)
853 && !has_glob_syntax(request)
854 && !has_source_extension(request))
855 .then_some(request)
856}
857
858fn strip_resource_query(value: &str) -> &str {
866 match value.split_once('?') {
867 Some((request, query)) if is_resource_query(query) => request,
868 _ => value,
869 }
870}
871
872fn is_resource_query(query: &str) -> bool {
875 !query.is_empty()
876 && query.split('&').all(|pair| {
877 let key = pair.split_once('=').map_or(pair, |(key, _)| key);
878 key.starts_with(|first: char| first.is_ascii_alphanumeric() || first == '_')
879 && key
880 .chars()
881 .all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '-' | '.'))
882 })
883}
884
885fn has_glob_syntax(value: &str) -> bool {
888 value.contains('*') || value.contains('?') || value.contains('[') || value.contains('{')
889}
890
891fn has_source_extension(value: &str) -> bool {
895 Path::new(value)
896 .extension()
897 .and_then(|ext| ext.to_str())
898 .is_some_and(|ext| {
899 crate::discover::SOURCE_EXTENSIONS.contains(&ext.to_ascii_lowercase().as_str())
900 })
901}
902
903#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
909pub struct PathRule {
910 pub pattern: String,
911 pub exclude_globs: Vec<String>,
912 pub exclude_regexes: Vec<String>,
913 pub exclude_segment_regexes: Vec<String>,
917 pub parent_relative: bool,
924}
925
926impl PathRule {
927 #[must_use]
928 pub(crate) fn new(pattern: impl Into<String>) -> Self {
929 Self {
930 pattern: pattern.into(),
931 exclude_globs: Vec::new(),
932 exclude_regexes: Vec::new(),
933 exclude_segment_regexes: Vec::new(),
934 parent_relative: false,
935 }
936 }
937
938 #[must_use]
939 fn from_static(pattern: &'static str) -> Self {
940 Self::new(pattern)
941 }
942
943 #[must_use]
947 pub(crate) fn from_extglob(pattern: &str) -> Self {
948 let rewritten = fallow_config::extglob::rewrite_include(pattern);
949 Self::new(rewritten.include).with_excluded_globs(rewritten.excludes)
950 }
951
952 #[must_use]
956 pub(crate) fn with_excluded_extglobs<I, S>(self, patterns: I) -> Self
957 where
958 I: IntoIterator<Item = S>,
959 S: AsRef<str>,
960 {
961 let excluded: Vec<String> = patterns
962 .into_iter()
963 .filter_map(|pattern| fallow_config::extglob::rewrite_exclude(pattern.as_ref()))
964 .collect();
965 self.with_excluded_globs(excluded)
966 }
967
968 #[must_use]
969 pub(crate) fn with_excluded_globs<I, S>(mut self, patterns: I) -> Self
970 where
971 I: IntoIterator<Item = S>,
972 S: Into<String>,
973 {
974 self.exclude_globs
975 .extend(patterns.into_iter().map(Into::into));
976 self
977 }
978
979 #[must_use]
980 fn with_excluded_regexes<I, S>(mut self, patterns: I) -> Self
981 where
982 I: IntoIterator<Item = S>,
983 S: Into<String>,
984 {
985 self.exclude_regexes
986 .extend(patterns.into_iter().map(Into::into));
987 self
988 }
989
990 #[must_use]
991 fn with_excluded_segment_regexes<I, S>(mut self, patterns: I) -> Self
992 where
993 I: IntoIterator<Item = S>,
994 S: Into<String>,
995 {
996 self.exclude_segment_regexes
997 .extend(patterns.into_iter().map(Into::into));
998 self
999 }
1000
1001 #[must_use]
1002 fn prefixed(&self, ws_prefix: &str) -> Self {
1003 let prefix = |pattern: &str| {
1004 if self.parent_relative && pattern.starts_with("../") {
1005 resolve_parent_relative_pattern(pattern, ws_prefix)
1006 } else {
1007 prefix_workspace_pattern(pattern, ws_prefix)
1008 }
1009 };
1010 Self {
1011 pattern: prefix(&self.pattern),
1012 exclude_globs: self
1013 .exclude_globs
1014 .iter()
1015 .map(|pattern| prefix(pattern))
1016 .collect(),
1017 exclude_regexes: self
1018 .exclude_regexes
1019 .iter()
1020 .map(|pattern| prefix_workspace_regex(pattern, ws_prefix))
1021 .collect(),
1022 exclude_segment_regexes: self.exclude_segment_regexes.clone(),
1023 parent_relative: false,
1024 }
1025 }
1026}
1027
1028#[derive(Debug, Clone, Default, PartialEq, Eq)]
1030pub struct ProvidedDependencyRule {
1031 pub(crate) path: PathRule,
1032 exact_specifiers: Vec<String>,
1033 specifier_prefixes: Vec<String>,
1034}
1035
1036impl ProvidedDependencyRule {
1037 #[must_use]
1038 fn new(
1039 pattern: impl Into<String>,
1040 exact_specifiers: impl IntoIterator<Item = impl Into<String>>,
1041 specifier_prefixes: impl IntoIterator<Item = impl Into<String>>,
1042 ) -> Self {
1043 Self {
1044 path: PathRule::new(pattern),
1045 exact_specifiers: exact_specifiers.into_iter().map(Into::into).collect(),
1046 specifier_prefixes: specifier_prefixes.into_iter().map(Into::into).collect(),
1047 }
1048 }
1049
1050 #[must_use]
1051 fn prefixed(&self, ws_prefix: &str) -> Self {
1052 Self {
1053 path: self.path.prefixed(ws_prefix),
1054 exact_specifiers: self.exact_specifiers.clone(),
1055 specifier_prefixes: self.specifier_prefixes.clone(),
1056 }
1057 }
1058
1059 #[must_use]
1060 pub(crate) fn may_cover_package(&self, package_name: &str) -> bool {
1061 self.exact_specifiers
1062 .iter()
1063 .chain(self.specifier_prefixes.iter())
1064 .any(|specifier| crate::resolve::extract_package_name(specifier) == package_name)
1065 }
1066
1067 #[must_use]
1068 pub(crate) fn covers_specifier(&self, specifier: &str) -> bool {
1069 self.exact_specifiers
1070 .iter()
1071 .any(|allowed| allowed == specifier)
1072 || self
1073 .specifier_prefixes
1074 .iter()
1075 .any(|prefix| specifier.starts_with(prefix))
1076 }
1077}
1078
1079pub(crate) fn entry_rule_glob(pattern: &str) -> Result<globset::Glob, globset::Error> {
1083 globset::GlobBuilder::new(pattern)
1084 .literal_separator(true)
1085 .empty_alternates(true)
1086 .build()
1087}
1088
1089#[derive(Debug, Clone)]
1094pub(crate) struct CompiledPathRule {
1095 include: globset::GlobMatcher,
1096 exclude_globs: Vec<globset::GlobMatcher>,
1097 exclude_regexes: Vec<Regex>,
1098 exclude_segment_regexes: Vec<Regex>,
1099}
1100
1101impl CompiledPathRule {
1102 pub(crate) fn for_entry_rule(rule: &PathRule, rule_kind: &str) -> Option<Self> {
1103 let include = match entry_rule_glob(&rule.pattern) {
1104 Ok(glob) => glob.compile_matcher(),
1105 Err(err) => {
1106 tracing::warn!("invalid {rule_kind} '{}': {err}", rule.pattern);
1107 return None;
1108 }
1109 };
1110 Some(Self {
1111 include,
1112 exclude_globs: compile_excluded_globs(&rule.exclude_globs, rule_kind, &rule.pattern),
1113 exclude_regexes: compile_excluded_regexes(
1114 &rule.exclude_regexes,
1115 rule_kind,
1116 &rule.pattern,
1117 ),
1118 exclude_segment_regexes: compile_excluded_segment_regexes(
1119 &rule.exclude_segment_regexes,
1120 rule_kind,
1121 &rule.pattern,
1122 ),
1123 })
1124 }
1125
1126 pub(crate) fn for_used_export_rule(rule: &PathRule, rule_kind: &str) -> Option<Self> {
1127 Self::for_entry_rule(rule, rule_kind)
1128 }
1129
1130 #[must_use]
1131 pub(crate) fn matches(&self, path: &str) -> bool {
1132 self.include.is_match(path)
1133 && !self.exclude_globs.iter().any(|glob| glob.is_match(path))
1134 && !self
1135 .exclude_regexes
1136 .iter()
1137 .any(|regex| regex.is_match(path))
1138 && !matches_segment_regex(path, &self.exclude_segment_regexes)
1139 }
1140}
1141
1142fn prefix_workspace_pattern(pattern: &str, ws_prefix: &str) -> String {
1143 if pattern.starts_with(ws_prefix) || pattern.starts_with('/') {
1144 pattern.to_string()
1145 } else {
1146 format!("{ws_prefix}/{pattern}")
1147 }
1148}
1149
1150fn resolve_parent_relative_pattern(pattern: &str, ws_prefix: &str) -> String {
1156 if ws_prefix.starts_with('/') || Path::new(ws_prefix).is_absolute() {
1157 return pattern.to_string();
1158 }
1159 let mut base: Vec<&str> = ws_prefix
1162 .split(['/', '\\'])
1163 .filter(|segment| !segment.is_empty())
1164 .collect();
1165 let mut rest = pattern;
1166 while let Some(stripped) = rest.strip_prefix("../") {
1167 if base.pop().is_none() {
1168 return pattern.to_string();
1169 }
1170 rest = stripped;
1171 }
1172 if base.is_empty() {
1173 rest.to_string()
1174 } else {
1175 format!("{}/{rest}", base.join("/"))
1176 }
1177}
1178
1179fn prefix_workspace_regex(pattern: &str, ws_prefix: &str) -> String {
1180 if let Some(pattern) = pattern.strip_prefix('^') {
1181 format!("^{}/{}", regex::escape(ws_prefix), pattern)
1182 } else {
1183 format!("^{}/(?:{})", regex::escape(ws_prefix), pattern)
1184 }
1185}
1186
1187fn compile_excluded_globs(
1188 patterns: &[String],
1189 rule_kind: &str,
1190 rule_pattern: &str,
1191) -> Vec<globset::GlobMatcher> {
1192 patterns
1193 .iter()
1194 .filter_map(|pattern| {
1195 match globset::GlobBuilder::new(pattern)
1196 .literal_separator(true)
1197 .empty_alternates(true)
1198 .build()
1199 {
1200 Ok(glob) => Some(glob.compile_matcher()),
1201 Err(err) => {
1202 tracing::warn!(
1203 "skipping invalid excluded glob '{}' for {} '{}': {err}",
1204 pattern,
1205 rule_kind,
1206 rule_pattern
1207 );
1208 None
1209 }
1210 }
1211 })
1212 .collect()
1213}
1214
1215fn compile_excluded_regexes(
1216 patterns: &[String],
1217 rule_kind: &str,
1218 rule_pattern: &str,
1219) -> Vec<Regex> {
1220 patterns
1221 .iter()
1222 .filter_map(|pattern| match Regex::new(pattern) {
1223 Ok(regex) => Some(regex),
1224 Err(err) => {
1225 tracing::warn!(
1226 "skipping invalid excluded regex '{}' for {} '{}': {err}",
1227 pattern,
1228 rule_kind,
1229 rule_pattern
1230 );
1231 None
1232 }
1233 })
1234 .collect()
1235}
1236
1237fn compile_excluded_segment_regexes(
1238 patterns: &[String],
1239 rule_kind: &str,
1240 rule_pattern: &str,
1241) -> Vec<Regex> {
1242 patterns
1243 .iter()
1244 .filter_map(|pattern| match Regex::new(pattern) {
1245 Ok(regex) => Some(regex),
1246 Err(err) => {
1247 tracing::warn!(
1248 "skipping invalid excluded segment regex '{}' for {} '{}': {err}",
1249 pattern,
1250 rule_kind,
1251 rule_pattern
1252 );
1253 None
1254 }
1255 })
1256 .collect()
1257}
1258
1259fn matches_segment_regex(path: &str, regexes: &[Regex]) -> bool {
1262 path.split(std::path::is_separator)
1263 .any(|segment| regexes.iter().any(|regex| regex.is_match(segment)))
1264}
1265
1266impl From<String> for PathRule {
1267 fn from(pattern: String) -> Self {
1268 Self::new(pattern)
1269 }
1270}
1271
1272impl From<&str> for PathRule {
1273 fn from(pattern: &str) -> Self {
1274 Self::new(pattern)
1275 }
1276}
1277
1278impl std::ops::Deref for PathRule {
1279 type Target = str;
1280
1281 fn deref(&self) -> &Self::Target {
1282 &self.pattern
1283 }
1284}
1285
1286impl PartialEq<&str> for PathRule {
1287 fn eq(&self, other: &&str) -> bool {
1288 self.pattern == *other
1289 }
1290}
1291
1292impl PartialEq<str> for PathRule {
1293 fn eq(&self, other: &str) -> bool {
1294 self.pattern == other
1295 }
1296}
1297
1298impl PartialEq<String> for PathRule {
1299 fn eq(&self, other: &String) -> bool {
1300 &self.pattern == other
1301 }
1302}
1303
1304pub trait Plugin: Send + Sync {
1306 fn name(&self) -> &'static str;
1308
1309 fn enablers(&self) -> &'static [&'static str] {
1312 &[]
1313 }
1314
1315 fn is_enabled(&self, pkg: &PackageJson, root: &Path) -> bool {
1318 let deps = pkg.all_dependency_names();
1319 self.is_enabled_with_deps(&deps, root)
1320 }
1321
1322 fn is_enabled_with_deps(&self, deps: &[String], _root: &Path) -> bool {
1325 let enablers = self.enablers();
1326 if enablers.is_empty() {
1327 return false;
1328 }
1329 enablers.iter().any(|enabler| {
1330 if enabler.ends_with('/') {
1331 deps.iter().any(|d| d.starts_with(enabler))
1333 } else {
1334 deps.iter().any(|d| d == enabler)
1335 }
1336 })
1337 }
1338
1339 fn is_enabled_with_files(
1352 &self,
1353 deps: &[String],
1354 root: &Path,
1355 _discovered_files: &[PathBuf],
1356 _candidate_index: Option<®istry::ConfigCandidateIndex>,
1357 ) -> bool {
1358 self.is_enabled_with_deps(deps, root)
1359 }
1360
1361 fn script_enablers(&self) -> &'static [&'static str] {
1363 &[]
1364 }
1365
1366 fn is_enabled_with_scripts(
1368 &self,
1369 script_packages: &rustc_hash::FxHashSet<String>,
1370 _root: &Path,
1371 ) -> bool {
1372 let enablers = self.script_enablers();
1373 if enablers.is_empty() {
1374 return false;
1375 }
1376 enablers.iter().any(|enabler| {
1377 if enabler.ends_with('/') {
1378 script_packages
1379 .iter()
1380 .any(|package| package.starts_with(enabler))
1381 } else {
1382 script_packages.contains(*enabler)
1383 }
1384 })
1385 }
1386
1387 fn entry_patterns(&self) -> &'static [&'static str] {
1389 &[]
1390 }
1391
1392 fn entry_pattern_rules(&self) -> Vec<PathRule> {
1394 self.entry_patterns()
1395 .iter()
1396 .map(|pattern| PathRule::from_static(pattern))
1397 .collect()
1398 }
1399
1400 fn entry_point_role(&self) -> EntryPointRole {
1405 builtin_entry_point_role(self.name())
1406 }
1407
1408 fn config_patterns(&self) -> &'static [&'static str] {
1410 &[]
1411 }
1412
1413 fn config_default_export_opt_out(&self) -> &'static [&'static str] {
1422 &[]
1423 }
1424
1425 fn config_file_exports(&self) -> &'static [(&'static str, &'static [&'static str])] {
1434 &[]
1435 }
1436
1437 fn always_used(&self) -> &'static [&'static str] {
1439 &[]
1440 }
1441
1442 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
1444 vec![]
1445 }
1446
1447 fn used_export_rules(&self) -> Vec<UsedExportRule> {
1449 self.used_exports()
1450 .into_iter()
1451 .map(|(pattern, exports)| UsedExportRule::from_static(pattern, exports))
1452 .collect()
1453 }
1454
1455 fn used_class_members(&self) -> &'static [&'static str] {
1460 &[]
1461 }
1462
1463 fn used_class_member_rules(&self) -> Vec<UsedClassMemberRule> {
1471 Vec::new()
1472 }
1473
1474 fn framework_class_member_contracts(&self) -> Vec<SemanticFrameworkContract> {
1477 Vec::new()
1478 }
1479
1480 fn fixture_glob_patterns(&self) -> &'static [&'static str] {
1485 &[]
1486 }
1487
1488 fn discovery_hidden_dirs(&self) -> &'static [&'static str] {
1493 &[]
1494 }
1495
1496 fn tooling_dependencies(&self) -> &'static [&'static str] {
1499 &[]
1500 }
1501
1502 fn virtual_module_prefixes(&self) -> &'static [&'static str] {
1507 &[]
1508 }
1509
1510 fn virtual_package_suffixes(&self) -> &'static [&'static str] {
1516 &[]
1517 }
1518
1519 fn generated_import_patterns(&self) -> &'static [&'static str] {
1525 &[]
1526 }
1527
1528 fn generated_type_import_prefixes(&self) -> &'static [&'static str] {
1533 &[]
1534 }
1535
1536 fn path_aliases(&self, _root: &Path) -> Vec<(&'static str, String)> {
1546 vec![]
1547 }
1548
1549 fn static_dir_mappings(&self, _root: &Path) -> Vec<(std::path::PathBuf, String)> {
1561 vec![]
1562 }
1563
1564 fn auto_imports(&self, _root: &Path) -> Vec<AutoImportRule> {
1582 Vec::new()
1583 }
1584
1585 fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> {
1587 Vec::new()
1588 }
1589
1590 fn is_enabled_with_package_json(&self, _pkg: &PackageJson, _root: &Path) -> bool {
1592 false
1593 }
1594
1595 fn resolve_package_json(&self, _pkg: &PackageJson, _root: &Path) -> PluginResult {
1597 PluginResult::default()
1598 }
1599
1600 fn package_json_referenced_dependencies(
1605 &self,
1606 _pkg: &PackageJson,
1607 _root: &Path,
1608 ) -> Vec<String> {
1609 Vec::new()
1610 }
1611
1612 fn resolve_config(&self, _config_path: &Path, _source: &str, _root: &Path) -> PluginResult {
1617 PluginResult::default()
1618 }
1619
1620 fn resolve_config_in_project(
1629 &self,
1630 config_path: &Path,
1631 source: &str,
1632 root: &Path,
1633 _project_root: &Path,
1634 ) -> PluginResult {
1635 self.resolve_config(config_path, source, root)
1636 }
1637
1638 fn resolve_script_config(
1642 &self,
1643 config_path: &Path,
1644 source: &str,
1645 root: &Path,
1646 project_root: &Path,
1647 ) -> PluginResult {
1648 self.resolve_config_in_project(config_path, source, root, project_root)
1649 }
1650
1651 fn script_config_binaries(&self) -> &'static [&'static str] {
1659 &[]
1660 }
1661
1662 fn package_json_config_key(&self) -> Option<&'static str> {
1667 None
1668 }
1669}
1670
1671fn builtin_entry_point_role(name: &str) -> EntryPointRole {
1672 if TEST_ENTRY_POINT_PLUGINS.contains(&name) {
1673 EntryPointRole::Test
1674 } else if RUNTIME_ENTRY_POINT_PLUGINS.contains(&name) {
1675 EntryPointRole::Runtime
1676 } else {
1677 EntryPointRole::Support
1678 }
1679}
1680
1681macro_rules! define_plugin {
1742 (
1743 struct $name:ident => $display:expr,
1744 enablers: $enablers:expr
1745 $(, entry_patterns: $entry:expr)?
1746 $(, config_patterns: $config:expr)?
1747 $(, config_default_export_opt_out: $config_opt_out:expr)?
1748 $(, config_file_exports: $config_exports:expr)?
1749 $(, always_used: $always:expr)?
1750 $(, tooling_dependencies: $tooling:expr)?
1751 $(, fixture_glob_patterns: $fixtures:expr)?
1752 $(, discovery_hidden_dirs: $hidden_dirs:expr)?
1753 $(, virtual_module_prefixes: $virtual:expr)?
1754 $(, virtual_package_suffixes: $virtual_suffixes:expr)?
1755 $(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
1756 $(, provided_dependencies: $provided_dependencies:expr)?
1757 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
1758 , resolve_config: imports_only
1759 $(,)?
1760 ) => {
1761 pub struct $name;
1762
1763 impl Plugin for $name {
1764 fn name(&self) -> &'static str {
1765 $display
1766 }
1767
1768 fn enablers(&self) -> &'static [&'static str] {
1769 $enablers
1770 }
1771
1772 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
1773 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
1774 $( fn config_default_export_opt_out(&self) -> &'static [&'static str] { $config_opt_out } )?
1775 $( fn config_file_exports(&self) -> &'static [(&'static str, &'static [&'static str])] { $config_exports } )?
1776 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
1777 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
1778 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
1779 $( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
1780 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
1781 $( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
1782 $( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
1783 $( fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> { $provided_dependencies } )?
1784
1785 $(
1786 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
1787 vec![$( ($pat, $exports) ),*]
1788 }
1789 )?
1790
1791 fn resolve_config(
1792 &self,
1793 config_path: &std::path::Path,
1794 source: &str,
1795 _root: &std::path::Path,
1796 ) -> PluginResult {
1797 let mut result = PluginResult::default();
1798 crate::plugins::add_import_referenced_dependencies(
1799 &mut result,
1800 source,
1801 config_path,
1802 );
1803 result
1804 }
1805 }
1806 };
1807
1808 (
1809 struct $name:ident => $display:expr,
1810 enablers: $enablers:expr
1811 $(, entry_patterns: $entry:expr)?
1812 $(, config_patterns: $config:expr)?
1813 $(, config_default_export_opt_out: $config_opt_out:expr)?
1814 $(, config_file_exports: $config_exports:expr)?
1815 $(, always_used: $always:expr)?
1816 $(, tooling_dependencies: $tooling:expr)?
1817 $(, fixture_glob_patterns: $fixtures:expr)?
1818 $(, discovery_hidden_dirs: $hidden_dirs:expr)?
1819 $(, virtual_module_prefixes: $virtual:expr)?
1820 $(, virtual_package_suffixes: $virtual_suffixes:expr)?
1821 $(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
1822 $(, provided_dependencies: $provided_dependencies:expr)?
1823 $(, package_json_config_key: $pkg_key:expr)?
1824 $(, script_config_binaries: $script_binaries:expr)?
1825 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
1826 , resolve_config($cp:ident, $src:ident, $root:ident) $body:block
1827 $(, resolve_config_in_project($pcp:ident, $psrc:ident, $proot:ident, $pproj:ident) $pbody:block)?
1828 $(,)?
1829 ) => {
1830 pub struct $name;
1831
1832 impl Plugin for $name {
1833 fn name(&self) -> &'static str {
1834 $display
1835 }
1836
1837 fn enablers(&self) -> &'static [&'static str] {
1838 $enablers
1839 }
1840
1841 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
1842 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
1843 $( fn config_default_export_opt_out(&self) -> &'static [&'static str] { $config_opt_out } )?
1844 $( fn config_file_exports(&self) -> &'static [(&'static str, &'static [&'static str])] { $config_exports } )?
1845 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
1846 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
1847 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
1848 $( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
1849 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
1850 $( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
1851 $( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
1852 $( fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> { $provided_dependencies } )?
1853
1854 $(
1855 fn package_json_config_key(&self) -> Option<&'static str> {
1856 Some($pkg_key)
1857 }
1858 )?
1859
1860 $(
1861 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
1862 vec![$( ($pat, $exports) ),*]
1863 }
1864 )?
1865
1866 $( fn script_config_binaries(&self) -> &'static [&'static str] { $script_binaries } )?
1867
1868 fn resolve_config(
1869 &self,
1870 $cp: &std::path::Path,
1871 $src: &str,
1872 $root: &std::path::Path,
1873 ) -> PluginResult
1874 $body
1875
1876 $(
1877 fn resolve_config_in_project(
1878 &self,
1879 $pcp: &std::path::Path,
1880 $psrc: &str,
1881 $proot: &std::path::Path,
1882 $pproj: &std::path::Path,
1883 ) -> PluginResult
1884 $pbody
1885 )?
1886 }
1887 };
1888
1889 (
1890 struct $name:ident => $display:expr,
1891 enablers: $enablers:expr
1892 $(, entry_patterns: $entry:expr)?
1893 $(, config_patterns: $config:expr)?
1894 $(, config_default_export_opt_out: $config_opt_out:expr)?
1895 $(, config_file_exports: $config_exports:expr)?
1896 $(, always_used: $always:expr)?
1897 $(, tooling_dependencies: $tooling:expr)?
1898 $(, fixture_glob_patterns: $fixtures:expr)?
1899 $(, discovery_hidden_dirs: $hidden_dirs:expr)?
1900 $(, virtual_module_prefixes: $virtual:expr)?
1901 $(, virtual_package_suffixes: $virtual_suffixes:expr)?
1902 $(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
1903 $(, provided_dependencies: $provided_dependencies:expr)?
1904 $(, package_json_config_key: $pkg_key:expr)?
1905 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
1906 $(,)?
1907 ) => {
1908 pub struct $name;
1909
1910 impl Plugin for $name {
1911 fn name(&self) -> &'static str {
1912 $display
1913 }
1914
1915 fn enablers(&self) -> &'static [&'static str] {
1916 $enablers
1917 }
1918
1919 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
1920 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
1921 $( fn config_default_export_opt_out(&self) -> &'static [&'static str] { $config_opt_out } )?
1922 $( fn config_file_exports(&self) -> &'static [(&'static str, &'static [&'static str])] { $config_exports } )?
1923 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
1924 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
1925 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
1926 $( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
1927 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
1928 $( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
1929 $( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
1930 $( fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> { $provided_dependencies } )?
1931
1932 $(
1933 fn package_json_config_key(&self) -> Option<&'static str> {
1934 Some($pkg_key)
1935 }
1936 )?
1937
1938 $(
1939 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
1940 vec![$( ($pat, $exports) ),*]
1941 }
1942 )?
1943 }
1944 };
1945}
1946
1947pub mod config_parser;
1948mod config_value_credits;
1949mod flue;
1950mod jasmine;
1951mod manifest;
1952pub mod manifest_entries;
1953mod mise;
1954mod osls;
1955mod project_glob;
1956mod railway;
1957pub mod registry;
1958mod rstest;
1959mod serverless_framework;
1960mod textlint;
1961mod tooling;
1962mod trace_provenance;
1963mod used_export_names;
1964mod used_export_rule;
1965
1966pub(crate) use used_export_rule::ConfigExportCredit;
1967pub use used_export_rule::{PluginUsedExportRule, UsedExportRule};
1968
1969pub(crate) use module_federation::runtime_remotes;
1970pub use registry::{
1971 AggregatedPluginResult, PluginRegistry, PluginToolingDependencies, PluginToolingEvidence,
1972};
1973pub use tooling::types_package_target;
1974pub(crate) use tooling::{
1975 cli_tooling_config_patterns, is_ambient_types_package, is_known_tooling_dependency,
1976};
1977pub use trace_provenance::{ExportCredits, build_trace_provenance};
1978pub use used_export_names::UsedExportNames;
1979
1980fn add_import_referenced_dependencies(result: &mut PluginResult, source: &str, config_path: &Path) {
1981 let imports = config_parser::extract_imports(source, config_path);
1982 for import in &imports {
1983 result
1984 .referenced_dependencies
1985 .push(crate::resolve::extract_package_name(import));
1986 }
1987}
1988
1989fn credit_environment_optional_peers(environment: &str, result: &mut PluginResult) {
2001 credit_config_value(
2002 config_value_credits::CreditSurface::TestEnvironmentOptionalPeer,
2003 canonical_test_environment(environment),
2004 result,
2005 );
2006}
2007
2008fn credit_config_value(
2013 surface: config_value_credits::CreditSurface,
2014 value: &str,
2015 result: &mut PluginResult,
2016) -> bool {
2017 let Some(packages) = config_value_credits::credited_packages(surface, value) else {
2018 return false;
2019 };
2020 result
2021 .referenced_dependencies
2022 .extend(packages.iter().cloned());
2023 true
2024}
2025
2026fn canonical_test_environment(environment: &str) -> &str {
2033 environment
2034 .strip_prefix("jest-environment-")
2035 .or_else(|| environment.strip_prefix("vitest-environment-"))
2036 .unwrap_or(environment)
2037}
2038
2039mod adonis;
2040mod ag_ui;
2041mod alchemy;
2042mod angular;
2043mod astro;
2044mod ava;
2045mod babel;
2046mod biome;
2047mod browser_extension;
2048mod bun;
2049mod c8;
2050mod capacitor;
2051mod changesets;
2052mod commit_and_tag_version;
2053mod commitizen;
2054mod commitlint;
2055mod content_collections;
2056mod contentlayer;
2057mod convex;
2058mod cspell;
2059mod cucumber;
2060mod cypress;
2061mod danger;
2062mod deno;
2063mod dependency_cruiser;
2064mod docusaurus;
2065mod drizzle;
2066mod egg;
2067mod electron;
2068mod ember;
2069mod eslint;
2070mod eve;
2071mod expo;
2072mod expo_router;
2073mod firebase;
2074mod fumadocs;
2075mod gatsby;
2076mod github_action;
2077mod graphql_codegen;
2078mod hardhat;
2079mod husky;
2080mod i18next;
2081mod ionic;
2082mod jest;
2083mod k6;
2084mod karma;
2085mod kibana;
2086mod knex;
2087mod kysely;
2088mod lefthook;
2089mod lexical;
2090mod lint_staged;
2091mod lit;
2092mod markdownlint;
2093mod mintlify;
2094mod mocha;
2095mod module_federation;
2096mod msw;
2097mod napi_rs;
2098mod nest_commander;
2099mod nestjs;
2100mod nestjs_trpc;
2101mod next_intl;
2102mod nextjs;
2103mod nitro;
2104mod nodemon;
2105pub(crate) mod nuxt;
2106mod nx;
2107mod nyc;
2108mod obsidian;
2109mod openapi_ts;
2110mod opencode;
2111mod opennext_cloudflare;
2112mod opentelemetry;
2113mod oxfmt;
2114mod oxlint;
2115mod pandacss;
2116mod parcel;
2117mod pinia;
2118mod pkg_utils;
2119mod playwright;
2120mod plop;
2121mod pm2;
2122mod pnpm;
2123mod postcss;
2124mod prettier;
2125mod prisma;
2126mod qwik;
2127mod react_compiler;
2128mod react_native;
2129mod react_router;
2130mod redwoodsdk;
2131mod relay;
2132mod release_it;
2133mod remark;
2134mod remix;
2135mod rolldown;
2136mod rollup;
2137mod rsbuild;
2138mod rspack;
2139mod rspress;
2140mod sanity;
2141mod semantic_release;
2142mod sentry;
2143mod simple_git_hooks;
2144mod size_limit;
2145mod storybook;
2146mod stryker;
2147mod stylelint;
2148mod supabase;
2149mod sveltekit;
2150mod svgo;
2151mod svgr;
2152mod swc;
2153mod syncpack;
2154mod tailwind;
2155mod tanstack_router;
2156mod tap;
2157mod test_alias;
2158mod tsd;
2159mod tsdown;
2160mod tsup;
2161mod turborepo;
2162mod typedoc;
2163mod typeorm;
2164mod typescript;
2165mod typescript_content_mapper;
2166mod unocss;
2167mod varlock;
2168mod velite;
2169mod vercel;
2170mod vite;
2171pub(crate) mod vite_plus;
2172mod vitepress;
2173mod vitest;
2174mod vscode;
2175mod waku;
2176mod webdriverio;
2177mod webpack;
2178mod wrangler;
2179mod wuchale;
2180mod wxt;
2181
2182#[cfg(test)]
2183mod tests;