1use std::path::{Path, PathBuf};
13
14use fallow_config::{AutoImportRule, EntryPointRole, PackageJson, UsedClassMemberRule};
15use fallow_types::semantic::SemanticFrameworkContract;
16use regex::Regex;
17
18const TEST_ENTRY_POINT_PLUGINS: &[&str] = &[
19 "ava",
20 "bun",
21 "deno",
22 "cucumber",
23 "cypress",
24 "jest",
25 "k6",
26 "mocha",
27 "playwright",
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 "electron",
42 "ember",
43 "eve",
44 "expo",
45 "expo-router",
46 "gatsby",
47 "hardhat",
48 "kibana",
49 "module-federation",
50 "nestjs",
51 "next-intl",
52 "nextjs",
53 "nitro",
54 "nuxt",
55 "obsidian",
56 "parcel",
57 "qwik",
58 "react-native",
59 "react-router",
60 "redwoodsdk",
61 "remix",
62 "rolldown",
63 "rollup",
64 "rsbuild",
65 "rspack",
66 "sanity",
67 "supabase",
68 "sveltekit",
69 "tanstack-router",
70 "tsdown",
71 "tsup",
72 "vercel",
73 "vite",
74 "vitepress",
75 "waku",
76 "webpack",
77 "wrangler",
78 "wxt",
79];
80
81#[cfg(test)]
82const SUPPORT_ENTRY_POINT_PLUGINS: &[&str] = &[
83 "content-collections",
84 "contentlayer",
85 "danger",
86 "drizzle",
87 "fumadocs",
88 "i18next",
89 "knex",
90 "kysely",
91 "mintlify",
92 "msw",
93 "opencode",
94 "prisma",
95 "storybook",
96 "stryker",
97 "typeorm",
98 "velite",
99];
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum PluginConfigEffect {
104 Unreadable,
107 NotModeled,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct PluginConfigDiagnostic {
124 pub config_path: PathBuf,
126 pub plugin: String,
130 pub key: String,
133 pub reason: String,
135 pub effect: PluginConfigEffect,
137}
138
139impl PluginConfigDiagnostic {
140 pub(super) fn unreadable(
142 config_path: &Path,
143 plugin: &str,
144 key: &str,
145 reason: &'static str,
146 ) -> Self {
147 Self {
148 config_path: config_path.to_path_buf(),
149 plugin: plugin.to_owned(),
150 key: key.to_owned(),
151 reason: reason.to_owned(),
152 effect: PluginConfigEffect::Unreadable,
153 }
154 }
155
156 pub(crate) fn not_modeled(
158 config_path: &Path,
159 plugin: &str,
160 key: &str,
161 reason: &'static str,
162 ) -> Self {
163 Self {
164 config_path: config_path.to_path_buf(),
165 plugin: plugin.to_owned(),
166 key: key.to_owned(),
167 reason: reason.to_owned(),
168 effect: PluginConfigEffect::NotModeled,
169 }
170 }
171
172 #[must_use]
175 pub fn into_workspace_diagnostic(self, root: &Path) -> fallow_config::WorkspaceDiagnostic {
176 let Self {
177 config_path,
178 plugin,
179 key,
180 reason,
181 effect,
182 } = self;
183 let kind = match effect {
184 PluginConfigEffect::Unreadable => {
185 fallow_config::WorkspaceDiagnosticKind::PluginConfigUnreadable {
186 plugin,
187 key,
188 reason,
189 }
190 }
191 PluginConfigEffect::NotModeled => {
192 fallow_config::WorkspaceDiagnosticKind::PluginEffectNotModeled {
193 plugin,
194 key,
195 reason,
196 }
197 }
198 };
199 fallow_config::WorkspaceDiagnostic::new(root, config_path, kind)
200 }
201}
202
203#[derive(Debug, Default)]
205pub struct PluginResult {
206 entry_patterns: Vec<PathRule>,
208 replace_entry_patterns: bool,
213 accumulate_config_entry_patterns: bool,
217 replace_used_export_rules: bool,
220 used_exports: Vec<UsedExportRule>,
222 used_class_members: Vec<UsedClassMemberRule>,
227 referenced_dependencies: Vec<String>,
229 package_referenced_dependencies: Vec<(PathBuf, String)>,
232 always_used_files: Vec<String>,
234 path_aliases: Vec<(String, String)>,
236 setup_files: Vec<PathBuf>,
238 fixture_patterns: Vec<String>,
240 scss_include_paths: Vec<PathBuf>,
248 static_dir_mappings: Vec<(PathBuf, String)>,
251 framework_static_dir_mappings: Vec<(PathBuf, String)>,
252 provided_dependencies: Vec<ProvidedDependencyRule>,
255 config_diagnostics: Vec<PluginConfigDiagnostic>,
259 federation_sources: Vec<FederationSource>,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct FederationSource {
267 pub target: FederationSourceTarget,
269 pub config_path: PathBuf,
271 pub plugin: String,
273 pub key: &'static str,
275}
276
277#[derive(Debug, Clone, PartialEq, Eq)]
279pub enum FederationSourceTarget {
280 Exposed(PathRule),
282 Remote(String),
284}
285
286impl FederationSource {
287 #[must_use]
288 fn prefixed(&self, ws_prefix: &str) -> Self {
289 let target = match &self.target {
290 FederationSourceTarget::Exposed(rule) => {
291 FederationSourceTarget::Exposed(rule.prefixed(ws_prefix))
292 }
293 FederationSourceTarget::Remote(alias) => FederationSourceTarget::Remote(alias.clone()),
294 };
295 Self {
296 target,
297 config_path: self.config_path.clone(),
298 plugin: self.plugin.clone(),
299 key: self.key,
300 }
301 }
302}
303
304#[must_use]
312pub fn federation_trace_provenance(
313 root: &Path,
314 files: &[crate::discover::DiscoveredFile],
315 sources: &[FederationSource],
316 modules: &[crate::extract::ModuleInfo],
317) -> fallow_types::trace::TraceProvenance {
318 let mut provenance = fallow_types::trace::TraceProvenance::default();
319 push_runtime_remote_sources(&mut provenance, root, files, modules);
320 if sources.is_empty() {
321 return provenance;
322 }
323 let mut exposed = Vec::new();
324 for source in sources {
325 let config = source
326 .config_path
327 .strip_prefix(root)
328 .unwrap_or(&source.config_path)
329 .to_path_buf();
330 let trace_source = fallow_types::trace::TraceSource {
331 kind: "module-federation".to_owned(),
332 plugin: source.plugin.clone(),
333 config,
334 key: source.key.to_owned(),
335 };
336 match &source.target {
337 FederationSourceTarget::Exposed(rule) => {
338 if let Some(compiled) =
339 CompiledPathRule::for_entry_rule(rule, "Module Federation exposes target")
340 {
341 exposed.push((compiled, trace_source));
342 }
343 }
344 FederationSourceTarget::Remote(alias) => {
345 provenance.push_dependency(alias.clone(), trace_source);
346 }
347 }
348 }
349 if exposed.is_empty() {
350 return provenance;
351 }
352 for file in files {
353 let Ok(relative) = file.path.strip_prefix(root) else {
354 continue;
355 };
356 let relative_str = relative.to_string_lossy().replace('\\', "/");
357 for (rule, source) in &exposed {
358 if rule.matches(&relative_str) {
359 provenance.push_file(relative.to_path_buf(), source.clone());
360 }
361 }
362 }
363 provenance
364}
365
366pub fn push_tooling_trace_credits(
370 provenance: &mut fallow_types::trace::TraceProvenance,
371 root: &Path,
372 workspaces: &[fallow_config::WorkspaceInfo],
373 plugin_result: &AggregatedPluginResult,
374) {
375 for entry in &plugin_result.plugin_tooling {
376 let Some(evidence) = entry.evidence(&plugin_result.script_used_packages) else {
377 continue;
378 };
379 let credit = match evidence {
380 PluginToolingEvidence::OwnConfig(config) => fallow_types::trace::ToolingCredit {
381 reason: "plugin-config".to_owned(),
382 plugin: Some(entry.plugin.clone()),
383 config: Some(config.strip_prefix(root).unwrap_or(config).to_path_buf()),
384 reference: None,
385 },
386 PluginToolingEvidence::Reference(name) => fallow_types::trace::ToolingCredit {
387 reason: "plugin-reference".to_owned(),
388 plugin: Some(entry.plugin.clone()),
389 config: None,
390 reference: Some(name.to_owned()),
391 },
392 };
393 for dependency in &entry.dependencies {
394 provenance.push_tooling_credit(dependency.clone(), credit.clone());
395 }
396 }
397 push_types_trace_credits(provenance, plugin_result);
398 push_catalogue_trace_credits(provenance, root, workspaces, plugin_result);
399}
400
401fn push_catalogue_trace_credits(
411 provenance: &mut fallow_types::trace::TraceProvenance,
412 root: &Path,
413 workspaces: &[fallow_config::WorkspaceInfo],
414 plugin_result: &AggregatedPluginResult,
415) {
416 let manifests = read_trace_manifests(root, workspaces);
417 let mut names: Vec<&String> = plugin_result.dev_dependency_names.iter().collect();
418 names.sort_unstable();
419 for name in names {
420 let declaring: Vec<&(PathBuf, serde_json::Value)> = manifests
421 .iter()
422 .filter(|(_, manifest)| manifest_lists(manifest, "devDependencies", name))
423 .collect();
424 if let Some(credit) = catalogue_credit(name, root, &declaring) {
425 provenance.push_tooling_credit(name.clone(), credit);
426 }
427 if let Some((path, _)) = declaring
428 .iter()
429 .find(|(_, manifest)| manifest_lists(manifest, "peerDependencies", name))
430 {
431 provenance.push_tooling_credit(
432 name.clone(),
433 fallow_types::trace::ToolingCredit {
434 reason: "own-peer".to_owned(),
435 plugin: None,
436 config: Some(path.strip_prefix(root).unwrap_or(path).to_path_buf()),
437 reference: None,
438 },
439 );
440 }
441 }
442}
443
444fn read_trace_manifests(
446 root: &Path,
447 workspaces: &[fallow_config::WorkspaceInfo],
448) -> Vec<(PathBuf, serde_json::Value)> {
449 std::iter::once(root)
450 .chain(
451 workspaces
452 .iter()
453 .map(|ws| ws.root.as_path())
454 .filter(|ws_root| *ws_root != root),
455 )
456 .filter_map(|package_root| {
457 let path = package_root.join("package.json");
458 let content = std::fs::read_to_string(&path).ok()?;
459 serde_json::from_str(&content)
460 .ok()
461 .map(|value| (path, value))
462 })
463 .collect()
464}
465
466fn manifest_lists(manifest: &serde_json::Value, section: &str, name: &str) -> bool {
467 manifest
468 .get(section)
469 .and_then(serde_json::Value::as_object)
470 .is_some_and(|deps| deps.contains_key(name))
471}
472
473fn catalogue_credit(
476 name: &str,
477 root: &Path,
478 declaring: &[&(PathBuf, serde_json::Value)],
479) -> Option<fallow_types::trace::ToolingCredit> {
480 if is_ambient_types_package(name) || types_package_target(name).is_some() {
481 return None;
482 }
483 let Some(patterns) = cli_tooling_config_patterns(name) else {
484 return is_known_tooling_dependency(name).then(|| fallow_types::trace::ToolingCredit {
485 reason: "known-tooling".to_owned(),
486 plugin: None,
487 config: None,
488 reference: None,
489 });
490 };
491 let config = declaring.iter().find_map(|(path, manifest)| {
492 crate::analyze::cli_tool_own_config(name, patterns, root, path, Some(manifest))
493 })?;
494 Some(fallow_types::trace::ToolingCredit {
495 reason: "known-tooling-config".to_owned(),
496 plugin: None,
497 config: Some(config.strip_prefix(root).unwrap_or(&config).to_path_buf()),
498 reference: None,
499 })
500}
501
502fn push_types_trace_credits(
507 provenance: &mut fallow_types::trace::TraceProvenance,
508 plugin_result: &AggregatedPluginResult,
509) {
510 let declared = plugin_result.dependency_binaries.declared_packages();
511 let mut names: Vec<&String> = plugin_result.dev_dependency_names.iter().collect();
512 names.sort_unstable();
513 for name in names {
514 let credit = if is_ambient_types_package(name) {
515 fallow_types::trace::ToolingCredit {
516 reason: "ambient-types".to_owned(),
517 plugin: None,
518 config: None,
519 reference: None,
520 }
521 } else if let Some(target) = types_package_target(name) {
522 if declared.contains(&target) {
523 fallow_types::trace::ToolingCredit {
524 reason: "types-target".to_owned(),
525 plugin: None,
526 config: None,
527 reference: Some(target),
528 }
529 } else if plugin_result.referenced_dependencies.contains(name) {
530 fallow_types::trace::ToolingCredit {
531 reason: "types-config".to_owned(),
532 plugin: None,
533 config: None,
534 reference: None,
535 }
536 } else {
537 continue;
538 }
539 } else {
540 continue;
541 };
542 provenance.push_tooling_credit(name.clone(), credit);
543 }
544}
545
546fn push_runtime_remote_sources(
548 provenance: &mut fallow_types::trace::TraceProvenance,
549 root: &Path,
550 files: &[crate::discover::DiscoveredFile],
551 modules: &[crate::extract::ModuleInfo],
552) {
553 for module in modules {
554 let mut file = None;
555 for fact in module.semantic_facts.iter() {
556 let fallow_types::extract::SemanticFact::FederationRuntimeRemote(fact) = fact else {
557 continue;
558 };
559 let Some(remote) = &fact.remote else {
560 continue;
561 };
562 let Some(path) = file.get_or_insert_with(|| {
563 files.get(module.file_id.0 as usize).map(|file| {
564 file.path
565 .strip_prefix(root)
566 .unwrap_or(&file.path)
567 .to_path_buf()
568 })
569 }) else {
570 break;
571 };
572 provenance.push_dependency(
573 remote.clone(),
574 fallow_types::trace::TraceSource {
575 kind: "module-federation".to_owned(),
576 plugin: "module-federation".to_owned(),
577 config: path.clone(),
578 key: fact.call.name().to_owned(),
579 },
580 );
581 }
582 }
583}
584
585impl PluginResult {
586 fn extend_config_dir_entry_patterns(
591 &mut self,
592 values: Vec<String>,
593 config_path: &Path,
594 root: &Path,
595 ) {
596 let (negated, positive): (Vec<String>, Vec<String>) =
597 values.into_iter().partition(|value| value.starts_with('!'));
598 let excluded: Vec<String> = negated
599 .iter()
600 .filter_map(|value| value.strip_prefix('!'))
601 .filter_map(|value| config_parser::normalize_config_path(value, config_path, root))
602 .collect();
603 self.entry_patterns.extend(
604 positive
605 .iter()
606 .filter_map(|value| config_parser::normalize_config_path(value, config_path, root))
607 .map(|pattern| {
608 PathRule::new(pattern).with_excluded_globs(excluded.iter().cloned())
609 }),
610 );
611 }
612
613 fn push_parent_relative_entry_pattern(&mut self, pattern: String) {
616 let mut rule = PathRule::new(pattern);
617 rule.parent_relative = true;
618 self.entry_patterns.push(rule);
619 }
620
621 fn push_entry_pattern(&mut self, pattern: impl Into<String>) {
622 self.entry_patterns
623 .push(PathRule::new(normalize_entry_pattern(pattern.into())));
624 }
625
626 fn extend_entry_patterns<I, S>(&mut self, patterns: I)
627 where
628 I: IntoIterator<Item = S>,
629 S: Into<String>,
630 {
631 self.entry_patterns.extend(
632 patterns
633 .into_iter()
634 .map(|pat| PathRule::new(normalize_entry_pattern(pat.into()))),
635 );
636 }
637
638 fn extend_entry_patterns_or_dependencies<I, S>(
653 &mut self,
654 values: I,
655 resolve_path: impl Fn(String) -> String,
656 ) where
657 I: IntoIterator<Item = S>,
658 S: Into<String>,
659 {
660 for value in values {
661 let value = value.into();
662 if let Some(request) = module_request(&value) {
663 self.referenced_dependencies
664 .push(crate::resolve::extract_package_name(request));
665 continue;
666 }
667 self.push_entry_path(resolve_path(value));
668 }
669 }
670
671 fn extend_entry_patterns_and_dependencies<I, S>(&mut self, values: I, root: &Path)
683 where
684 I: IntoIterator<Item = S>,
685 S: Into<String>,
686 {
687 for value in values {
688 let value = value.into();
689 if let Some(request) = module_request(&value)
690 && !names_project_file(root, request)
691 {
692 self.referenced_dependencies
693 .push(crate::resolve::extract_package_name(request));
694 }
695 self.push_entry_path(value);
696 }
697 }
698
699 fn extend_entry_paths<I, S>(&mut self, values: I)
701 where
702 I: IntoIterator<Item = S>,
703 S: Into<String>,
704 {
705 for value in values {
706 self.push_entry_path(value.into());
707 }
708 }
709
710 fn push_entry_path(&mut self, value: String) {
718 if has_glob_syntax(&value) || has_source_extension(&value) {
719 self.push_entry_pattern(value);
720 return;
721 }
722 let base = value.trim_end_matches('/').to_owned();
723 self.push_entry_pattern(value);
724 self.push_entry_pattern(format!("{base}.{REQUEST_EXTENSIONS}"));
725 self.push_entry_pattern(format!("{base}/index.{REQUEST_EXTENSIONS}"));
726 }
727
728 fn push_used_export_rule(
729 &mut self,
730 pattern: impl Into<String>,
731 exports: impl IntoIterator<Item = impl Into<String>>,
732 ) {
733 self.used_exports
734 .push(UsedExportRule::new(pattern, exports));
735 }
736
737 #[must_use]
744 const fn is_empty(&self) -> bool {
745 self.config_diagnostics.is_empty()
746 && self.entry_patterns.is_empty()
747 && self.used_exports.is_empty()
748 && self.used_class_members.is_empty()
749 && self.referenced_dependencies.is_empty()
750 && self.package_referenced_dependencies.is_empty()
751 && self.always_used_files.is_empty()
752 && self.path_aliases.is_empty()
753 && self.setup_files.is_empty()
754 && self.fixture_patterns.is_empty()
755 && self.scss_include_paths.is_empty()
756 && self.static_dir_mappings.is_empty()
757 && self.framework_static_dir_mappings.is_empty()
758 && self.provided_dependencies.is_empty()
759 && self.federation_sources.is_empty()
760 }
761}
762
763fn names_project_file(root: &Path, value: &str) -> bool {
767 let base = root.join(value);
768 base.is_file()
769 || crate::discover::SOURCE_EXTENSIONS.iter().any(|extension| {
770 let mut candidate = base.clone().into_os_string();
771 candidate.push(".");
772 candidate.push(extension);
773 Path::new(&candidate).is_file() || base.join(format!("index.{extension}")).is_file()
774 })
775}
776
777const REQUEST_EXTENSIONS: &str = "{ts,tsx,mts,cts,gts,js,jsx,mjs,cjs,gjs,vue,svelte,astro,mdx}";
781
782fn normalize_entry_pattern(pattern: String) -> String {
783 pattern
784 .strip_prefix("./")
785 .map(str::to_owned)
786 .unwrap_or(pattern)
787}
788
789fn module_request(value: &str) -> Option<&str> {
800 let request = strip_resource_query(value);
801 (config_parser::is_package_specifier(request)
802 && !has_glob_syntax(request)
803 && !has_source_extension(request))
804 .then_some(request)
805}
806
807fn strip_resource_query(value: &str) -> &str {
815 match value.split_once('?') {
816 Some((request, query)) if is_resource_query(query) => request,
817 _ => value,
818 }
819}
820
821fn is_resource_query(query: &str) -> bool {
824 !query.is_empty()
825 && query.split('&').all(|pair| {
826 let key = pair.split_once('=').map_or(pair, |(key, _)| key);
827 key.starts_with(|first: char| first.is_ascii_alphanumeric() || first == '_')
828 && key
829 .chars()
830 .all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '-' | '.'))
831 })
832}
833
834fn has_glob_syntax(value: &str) -> bool {
837 value.contains('*') || value.contains('?') || value.contains('[') || value.contains('{')
838}
839
840fn has_source_extension(value: &str) -> bool {
844 Path::new(value)
845 .extension()
846 .and_then(|ext| ext.to_str())
847 .is_some_and(|ext| {
848 crate::discover::SOURCE_EXTENSIONS.contains(&ext.to_ascii_lowercase().as_str())
849 })
850}
851
852#[derive(Debug, Clone, Default, PartialEq, Eq)]
858pub struct PathRule {
859 pub pattern: String,
860 pub exclude_globs: Vec<String>,
861 pub exclude_regexes: Vec<String>,
862 pub exclude_segment_regexes: Vec<String>,
866 pub parent_relative: bool,
873}
874
875impl PathRule {
876 #[must_use]
877 pub(crate) fn new(pattern: impl Into<String>) -> Self {
878 Self {
879 pattern: pattern.into(),
880 exclude_globs: Vec::new(),
881 exclude_regexes: Vec::new(),
882 exclude_segment_regexes: Vec::new(),
883 parent_relative: false,
884 }
885 }
886
887 #[must_use]
888 fn from_static(pattern: &'static str) -> Self {
889 Self::new(pattern)
890 }
891
892 #[must_use]
893 pub(crate) fn with_excluded_globs<I, S>(mut self, patterns: I) -> Self
894 where
895 I: IntoIterator<Item = S>,
896 S: Into<String>,
897 {
898 self.exclude_globs
899 .extend(patterns.into_iter().map(Into::into));
900 self
901 }
902
903 #[must_use]
904 fn with_excluded_regexes<I, S>(mut self, patterns: I) -> Self
905 where
906 I: IntoIterator<Item = S>,
907 S: Into<String>,
908 {
909 self.exclude_regexes
910 .extend(patterns.into_iter().map(Into::into));
911 self
912 }
913
914 #[must_use]
915 fn with_excluded_segment_regexes<I, S>(mut self, patterns: I) -> Self
916 where
917 I: IntoIterator<Item = S>,
918 S: Into<String>,
919 {
920 self.exclude_segment_regexes
921 .extend(patterns.into_iter().map(Into::into));
922 self
923 }
924
925 #[must_use]
926 fn prefixed(&self, ws_prefix: &str) -> Self {
927 let pattern = if self.parent_relative && self.pattern.starts_with("../") {
928 resolve_parent_relative_pattern(&self.pattern, ws_prefix)
929 } else {
930 prefix_workspace_pattern(&self.pattern, ws_prefix)
931 };
932 Self {
933 pattern,
934 exclude_globs: self
935 .exclude_globs
936 .iter()
937 .map(|pattern| prefix_workspace_pattern(pattern, ws_prefix))
938 .collect(),
939 exclude_regexes: self
940 .exclude_regexes
941 .iter()
942 .map(|pattern| prefix_workspace_regex(pattern, ws_prefix))
943 .collect(),
944 exclude_segment_regexes: self.exclude_segment_regexes.clone(),
945 parent_relative: false,
946 }
947 }
948}
949
950#[derive(Debug, Clone, Default, PartialEq, Eq)]
952pub struct UsedExportRule {
953 pub(crate) path: PathRule,
954 pub(crate) exports: Vec<String>,
955}
956
957impl UsedExportRule {
958 #[must_use]
959 pub(crate) fn new(
960 pattern: impl Into<String>,
961 exports: impl IntoIterator<Item = impl Into<String>>,
962 ) -> Self {
963 Self {
964 path: PathRule::new(pattern),
965 exports: exports.into_iter().map(Into::into).collect(),
966 }
967 }
968
969 #[must_use]
970 fn from_static(pattern: &'static str, exports: &'static [&'static str]) -> Self {
971 Self::new(pattern, exports.iter().copied())
972 }
973
974 #[must_use]
975 fn with_excluded_globs<I, S>(mut self, patterns: I) -> Self
976 where
977 I: IntoIterator<Item = S>,
978 S: Into<String>,
979 {
980 self.path = self.path.with_excluded_globs(patterns);
981 self
982 }
983
984 #[must_use]
985 fn with_excluded_regexes<I, S>(mut self, patterns: I) -> Self
986 where
987 I: IntoIterator<Item = S>,
988 S: Into<String>,
989 {
990 self.path = self.path.with_excluded_regexes(patterns);
991 self
992 }
993
994 #[must_use]
995 fn with_excluded_segment_regexes<I, S>(mut self, patterns: I) -> Self
996 where
997 I: IntoIterator<Item = S>,
998 S: Into<String>,
999 {
1000 self.path = self.path.with_excluded_segment_regexes(patterns);
1001 self
1002 }
1003
1004 #[must_use]
1005 fn prefixed(&self, ws_prefix: &str) -> Self {
1006 Self {
1007 path: self.path.prefixed(ws_prefix),
1008 exports: self.exports.clone(),
1009 }
1010 }
1011}
1012
1013#[derive(Debug, Clone, PartialEq, Eq)]
1015pub struct PluginUsedExportRule {
1016 pub(crate) plugin_name: String,
1017 pub(crate) rule: UsedExportRule,
1018}
1019
1020impl PluginUsedExportRule {
1021 #[must_use]
1022 pub(crate) fn new(plugin_name: impl Into<String>, rule: UsedExportRule) -> Self {
1023 Self {
1024 plugin_name: plugin_name.into(),
1025 rule,
1026 }
1027 }
1028
1029 #[must_use]
1030 fn prefixed(&self, ws_prefix: &str) -> Self {
1031 Self {
1032 plugin_name: self.plugin_name.clone(),
1033 rule: self.rule.prefixed(ws_prefix),
1034 }
1035 }
1036}
1037
1038#[derive(Debug, Clone, Default, PartialEq, Eq)]
1040pub struct ProvidedDependencyRule {
1041 pub(crate) path: PathRule,
1042 exact_specifiers: Vec<String>,
1043 specifier_prefixes: Vec<String>,
1044}
1045
1046impl ProvidedDependencyRule {
1047 #[must_use]
1048 fn new(
1049 pattern: impl Into<String>,
1050 exact_specifiers: impl IntoIterator<Item = impl Into<String>>,
1051 specifier_prefixes: impl IntoIterator<Item = impl Into<String>>,
1052 ) -> Self {
1053 Self {
1054 path: PathRule::new(pattern),
1055 exact_specifiers: exact_specifiers.into_iter().map(Into::into).collect(),
1056 specifier_prefixes: specifier_prefixes.into_iter().map(Into::into).collect(),
1057 }
1058 }
1059
1060 #[must_use]
1061 fn prefixed(&self, ws_prefix: &str) -> Self {
1062 Self {
1063 path: self.path.prefixed(ws_prefix),
1064 exact_specifiers: self.exact_specifiers.clone(),
1065 specifier_prefixes: self.specifier_prefixes.clone(),
1066 }
1067 }
1068
1069 #[must_use]
1070 pub(crate) fn may_cover_package(&self, package_name: &str) -> bool {
1071 self.exact_specifiers
1072 .iter()
1073 .chain(self.specifier_prefixes.iter())
1074 .any(|specifier| crate::resolve::extract_package_name(specifier) == package_name)
1075 }
1076
1077 #[must_use]
1078 pub(crate) fn covers_specifier(&self, specifier: &str) -> bool {
1079 self.exact_specifiers
1080 .iter()
1081 .any(|allowed| allowed == specifier)
1082 || self
1083 .specifier_prefixes
1084 .iter()
1085 .any(|prefix| specifier.starts_with(prefix))
1086 }
1087}
1088
1089#[derive(Debug, Clone)]
1091pub(crate) struct CompiledPathRule {
1092 include: globset::GlobMatcher,
1093 exclude_globs: Vec<globset::GlobMatcher>,
1094 exclude_regexes: Vec<Regex>,
1095 exclude_segment_regexes: Vec<Regex>,
1096}
1097
1098impl CompiledPathRule {
1099 pub(crate) fn for_entry_rule(rule: &PathRule, rule_kind: &str) -> Option<Self> {
1100 let include = match globset::GlobBuilder::new(&rule.pattern)
1101 .literal_separator(true)
1102 .build()
1103 {
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 let include = match globset::Glob::new(&rule.pattern) {
1128 Ok(glob) => glob.compile_matcher(),
1129 Err(err) => {
1130 tracing::warn!("invalid {rule_kind} '{}': {err}", rule.pattern);
1131 return None;
1132 }
1133 };
1134 Some(Self {
1135 include,
1136 exclude_globs: compile_excluded_globs(&rule.exclude_globs, rule_kind, &rule.pattern),
1137 exclude_regexes: compile_excluded_regexes(
1138 &rule.exclude_regexes,
1139 rule_kind,
1140 &rule.pattern,
1141 ),
1142 exclude_segment_regexes: compile_excluded_segment_regexes(
1143 &rule.exclude_segment_regexes,
1144 rule_kind,
1145 &rule.pattern,
1146 ),
1147 })
1148 }
1149
1150 #[must_use]
1151 pub(crate) fn matches(&self, path: &str) -> bool {
1152 self.include.is_match(path)
1153 && !self.exclude_globs.iter().any(|glob| glob.is_match(path))
1154 && !self
1155 .exclude_regexes
1156 .iter()
1157 .any(|regex| regex.is_match(path))
1158 && !matches_segment_regex(path, &self.exclude_segment_regexes)
1159 }
1160}
1161
1162fn prefix_workspace_pattern(pattern: &str, ws_prefix: &str) -> String {
1163 if pattern.starts_with(ws_prefix) || pattern.starts_with('/') {
1164 pattern.to_string()
1165 } else {
1166 format!("{ws_prefix}/{pattern}")
1167 }
1168}
1169
1170fn resolve_parent_relative_pattern(pattern: &str, ws_prefix: &str) -> String {
1176 if ws_prefix.starts_with('/') || Path::new(ws_prefix).is_absolute() {
1177 return pattern.to_string();
1178 }
1179 let mut base: Vec<&str> = ws_prefix
1182 .split(['/', '\\'])
1183 .filter(|segment| !segment.is_empty())
1184 .collect();
1185 let mut rest = pattern;
1186 while let Some(stripped) = rest.strip_prefix("../") {
1187 if base.pop().is_none() {
1188 return pattern.to_string();
1189 }
1190 rest = stripped;
1191 }
1192 if base.is_empty() {
1193 rest.to_string()
1194 } else {
1195 format!("{}/{rest}", base.join("/"))
1196 }
1197}
1198
1199fn prefix_workspace_regex(pattern: &str, ws_prefix: &str) -> String {
1200 if let Some(pattern) = pattern.strip_prefix('^') {
1201 format!("^{}/{}", regex::escape(ws_prefix), pattern)
1202 } else {
1203 format!("^{}/(?:{})", regex::escape(ws_prefix), pattern)
1204 }
1205}
1206
1207fn compile_excluded_globs(
1208 patterns: &[String],
1209 rule_kind: &str,
1210 rule_pattern: &str,
1211) -> Vec<globset::GlobMatcher> {
1212 patterns
1213 .iter()
1214 .filter_map(|pattern| {
1215 match globset::GlobBuilder::new(pattern)
1216 .literal_separator(true)
1217 .build()
1218 {
1219 Ok(glob) => Some(glob.compile_matcher()),
1220 Err(err) => {
1221 tracing::warn!(
1222 "skipping invalid excluded glob '{}' for {} '{}': {err}",
1223 pattern,
1224 rule_kind,
1225 rule_pattern
1226 );
1227 None
1228 }
1229 }
1230 })
1231 .collect()
1232}
1233
1234fn compile_excluded_regexes(
1235 patterns: &[String],
1236 rule_kind: &str,
1237 rule_pattern: &str,
1238) -> Vec<Regex> {
1239 patterns
1240 .iter()
1241 .filter_map(|pattern| match Regex::new(pattern) {
1242 Ok(regex) => Some(regex),
1243 Err(err) => {
1244 tracing::warn!(
1245 "skipping invalid excluded regex '{}' for {} '{}': {err}",
1246 pattern,
1247 rule_kind,
1248 rule_pattern
1249 );
1250 None
1251 }
1252 })
1253 .collect()
1254}
1255
1256fn compile_excluded_segment_regexes(
1257 patterns: &[String],
1258 rule_kind: &str,
1259 rule_pattern: &str,
1260) -> Vec<Regex> {
1261 patterns
1262 .iter()
1263 .filter_map(|pattern| match Regex::new(pattern) {
1264 Ok(regex) => Some(regex),
1265 Err(err) => {
1266 tracing::warn!(
1267 "skipping invalid excluded segment regex '{}' for {} '{}': {err}",
1268 pattern,
1269 rule_kind,
1270 rule_pattern
1271 );
1272 None
1273 }
1274 })
1275 .collect()
1276}
1277
1278fn matches_segment_regex(path: &str, regexes: &[Regex]) -> bool {
1281 path.split(std::path::is_separator)
1282 .any(|segment| regexes.iter().any(|regex| regex.is_match(segment)))
1283}
1284
1285impl From<String> for PathRule {
1286 fn from(pattern: String) -> Self {
1287 Self::new(pattern)
1288 }
1289}
1290
1291impl From<&str> for PathRule {
1292 fn from(pattern: &str) -> Self {
1293 Self::new(pattern)
1294 }
1295}
1296
1297impl std::ops::Deref for PathRule {
1298 type Target = str;
1299
1300 fn deref(&self) -> &Self::Target {
1301 &self.pattern
1302 }
1303}
1304
1305impl PartialEq<&str> for PathRule {
1306 fn eq(&self, other: &&str) -> bool {
1307 self.pattern == *other
1308 }
1309}
1310
1311impl PartialEq<str> for PathRule {
1312 fn eq(&self, other: &str) -> bool {
1313 self.pattern == other
1314 }
1315}
1316
1317impl PartialEq<String> for PathRule {
1318 fn eq(&self, other: &String) -> bool {
1319 &self.pattern == other
1320 }
1321}
1322
1323pub trait Plugin: Send + Sync {
1325 fn name(&self) -> &'static str;
1327
1328 fn enablers(&self) -> &'static [&'static str] {
1331 &[]
1332 }
1333
1334 fn is_enabled(&self, pkg: &PackageJson, root: &Path) -> bool {
1337 let deps = pkg.all_dependency_names();
1338 self.is_enabled_with_deps(&deps, root)
1339 }
1340
1341 fn is_enabled_with_deps(&self, deps: &[String], _root: &Path) -> bool {
1344 let enablers = self.enablers();
1345 if enablers.is_empty() {
1346 return false;
1347 }
1348 enablers.iter().any(|enabler| {
1349 if enabler.ends_with('/') {
1350 deps.iter().any(|d| d.starts_with(enabler))
1352 } else {
1353 deps.iter().any(|d| d == enabler)
1354 }
1355 })
1356 }
1357
1358 fn is_enabled_with_files(
1371 &self,
1372 deps: &[String],
1373 root: &Path,
1374 _discovered_files: &[PathBuf],
1375 _candidate_index: Option<®istry::ConfigCandidateIndex>,
1376 ) -> bool {
1377 self.is_enabled_with_deps(deps, root)
1378 }
1379
1380 fn script_enablers(&self) -> &'static [&'static str] {
1382 &[]
1383 }
1384
1385 fn is_enabled_with_scripts(
1387 &self,
1388 script_packages: &rustc_hash::FxHashSet<String>,
1389 _root: &Path,
1390 ) -> bool {
1391 let enablers = self.script_enablers();
1392 if enablers.is_empty() {
1393 return false;
1394 }
1395 enablers.iter().any(|enabler| {
1396 if enabler.ends_with('/') {
1397 script_packages
1398 .iter()
1399 .any(|package| package.starts_with(enabler))
1400 } else {
1401 script_packages.contains(*enabler)
1402 }
1403 })
1404 }
1405
1406 fn entry_patterns(&self) -> &'static [&'static str] {
1408 &[]
1409 }
1410
1411 fn entry_pattern_rules(&self) -> Vec<PathRule> {
1413 self.entry_patterns()
1414 .iter()
1415 .map(|pattern| PathRule::from_static(pattern))
1416 .collect()
1417 }
1418
1419 fn entry_point_role(&self) -> EntryPointRole {
1424 builtin_entry_point_role(self.name())
1425 }
1426
1427 fn config_patterns(&self) -> &'static [&'static str] {
1429 &[]
1430 }
1431
1432 fn always_used(&self) -> &'static [&'static str] {
1434 &[]
1435 }
1436
1437 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
1439 vec![]
1440 }
1441
1442 fn used_export_rules(&self) -> Vec<UsedExportRule> {
1444 self.used_exports()
1445 .into_iter()
1446 .map(|(pattern, exports)| UsedExportRule::from_static(pattern, exports))
1447 .collect()
1448 }
1449
1450 fn used_class_members(&self) -> &'static [&'static str] {
1455 &[]
1456 }
1457
1458 fn used_class_member_rules(&self) -> Vec<UsedClassMemberRule> {
1466 Vec::new()
1467 }
1468
1469 fn framework_class_member_contracts(&self) -> Vec<SemanticFrameworkContract> {
1472 Vec::new()
1473 }
1474
1475 fn fixture_glob_patterns(&self) -> &'static [&'static str] {
1480 &[]
1481 }
1482
1483 fn discovery_hidden_dirs(&self) -> &'static [&'static str] {
1488 &[]
1489 }
1490
1491 fn tooling_dependencies(&self) -> &'static [&'static str] {
1494 &[]
1495 }
1496
1497 fn virtual_module_prefixes(&self) -> &'static [&'static str] {
1502 &[]
1503 }
1504
1505 fn virtual_package_suffixes(&self) -> &'static [&'static str] {
1511 &[]
1512 }
1513
1514 fn generated_import_patterns(&self) -> &'static [&'static str] {
1520 &[]
1521 }
1522
1523 fn generated_type_import_prefixes(&self) -> &'static [&'static str] {
1528 &[]
1529 }
1530
1531 fn path_aliases(&self, _root: &Path) -> Vec<(&'static str, String)> {
1541 vec![]
1542 }
1543
1544 fn static_dir_mappings(&self, _root: &Path) -> Vec<(std::path::PathBuf, String)> {
1556 vec![]
1557 }
1558
1559 fn auto_imports(&self, _root: &Path) -> Vec<AutoImportRule> {
1577 Vec::new()
1578 }
1579
1580 fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> {
1582 Vec::new()
1583 }
1584
1585 fn is_enabled_with_package_json(&self, _pkg: &PackageJson, _root: &Path) -> bool {
1587 false
1588 }
1589
1590 fn resolve_package_json(&self, _pkg: &PackageJson, _root: &Path) -> PluginResult {
1592 PluginResult::default()
1593 }
1594
1595 fn package_json_referenced_dependencies(
1600 &self,
1601 _pkg: &PackageJson,
1602 _root: &Path,
1603 ) -> Vec<String> {
1604 Vec::new()
1605 }
1606
1607 fn resolve_config(&self, _config_path: &Path, _source: &str, _root: &Path) -> PluginResult {
1612 PluginResult::default()
1613 }
1614
1615 fn script_config_binaries(&self) -> &'static [&'static str] {
1623 &[]
1624 }
1625
1626 fn package_json_config_key(&self) -> Option<&'static str> {
1631 None
1632 }
1633}
1634
1635fn builtin_entry_point_role(name: &str) -> EntryPointRole {
1636 if TEST_ENTRY_POINT_PLUGINS.contains(&name) {
1637 EntryPointRole::Test
1638 } else if RUNTIME_ENTRY_POINT_PLUGINS.contains(&name) {
1639 EntryPointRole::Runtime
1640 } else {
1641 EntryPointRole::Support
1642 }
1643}
1644
1645macro_rules! define_plugin {
1706 (
1707 struct $name:ident => $display:expr,
1708 enablers: $enablers:expr
1709 $(, entry_patterns: $entry:expr)?
1710 $(, config_patterns: $config:expr)?
1711 $(, always_used: $always:expr)?
1712 $(, tooling_dependencies: $tooling:expr)?
1713 $(, fixture_glob_patterns: $fixtures:expr)?
1714 $(, discovery_hidden_dirs: $hidden_dirs:expr)?
1715 $(, virtual_module_prefixes: $virtual:expr)?
1716 $(, virtual_package_suffixes: $virtual_suffixes:expr)?
1717 $(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
1718 $(, provided_dependencies: $provided_dependencies:expr)?
1719 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
1720 , resolve_config: imports_only
1721 $(,)?
1722 ) => {
1723 pub struct $name;
1724
1725 impl Plugin for $name {
1726 fn name(&self) -> &'static str {
1727 $display
1728 }
1729
1730 fn enablers(&self) -> &'static [&'static str] {
1731 $enablers
1732 }
1733
1734 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
1735 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
1736 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
1737 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
1738 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
1739 $( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
1740 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
1741 $( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
1742 $( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
1743 $( fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> { $provided_dependencies } )?
1744
1745 $(
1746 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
1747 vec![$( ($pat, $exports) ),*]
1748 }
1749 )?
1750
1751 fn resolve_config(
1752 &self,
1753 config_path: &std::path::Path,
1754 source: &str,
1755 _root: &std::path::Path,
1756 ) -> PluginResult {
1757 let mut result = PluginResult::default();
1758 crate::plugins::add_import_referenced_dependencies(
1759 &mut result,
1760 source,
1761 config_path,
1762 );
1763 result
1764 }
1765 }
1766 };
1767
1768 (
1769 struct $name:ident => $display:expr,
1770 enablers: $enablers:expr
1771 $(, entry_patterns: $entry:expr)?
1772 $(, config_patterns: $config:expr)?
1773 $(, always_used: $always:expr)?
1774 $(, tooling_dependencies: $tooling:expr)?
1775 $(, fixture_glob_patterns: $fixtures:expr)?
1776 $(, discovery_hidden_dirs: $hidden_dirs:expr)?
1777 $(, virtual_module_prefixes: $virtual:expr)?
1778 $(, virtual_package_suffixes: $virtual_suffixes:expr)?
1779 $(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
1780 $(, provided_dependencies: $provided_dependencies:expr)?
1781 $(, package_json_config_key: $pkg_key:expr)?
1782 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
1783 , resolve_config($cp:ident, $src:ident, $root:ident) $body:block
1784 $(,)?
1785 ) => {
1786 pub struct $name;
1787
1788 impl Plugin for $name {
1789 fn name(&self) -> &'static str {
1790 $display
1791 }
1792
1793 fn enablers(&self) -> &'static [&'static str] {
1794 $enablers
1795 }
1796
1797 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
1798 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
1799 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
1800 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
1801 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
1802 $( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
1803 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
1804 $( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
1805 $( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
1806 $( fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> { $provided_dependencies } )?
1807
1808 $(
1809 fn package_json_config_key(&self) -> Option<&'static str> {
1810 Some($pkg_key)
1811 }
1812 )?
1813
1814 $(
1815 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
1816 vec![$( ($pat, $exports) ),*]
1817 }
1818 )?
1819
1820 fn resolve_config(
1821 &self,
1822 $cp: &std::path::Path,
1823 $src: &str,
1824 $root: &std::path::Path,
1825 ) -> PluginResult
1826 $body
1827 }
1828 };
1829
1830 (
1831 struct $name:ident => $display:expr,
1832 enablers: $enablers:expr
1833 $(, entry_patterns: $entry:expr)?
1834 $(, config_patterns: $config:expr)?
1835 $(, always_used: $always:expr)?
1836 $(, tooling_dependencies: $tooling:expr)?
1837 $(, fixture_glob_patterns: $fixtures:expr)?
1838 $(, discovery_hidden_dirs: $hidden_dirs:expr)?
1839 $(, virtual_module_prefixes: $virtual:expr)?
1840 $(, virtual_package_suffixes: $virtual_suffixes:expr)?
1841 $(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
1842 $(, provided_dependencies: $provided_dependencies:expr)?
1843 $(, package_json_config_key: $pkg_key:expr)?
1844 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
1845 $(,)?
1846 ) => {
1847 pub struct $name;
1848
1849 impl Plugin for $name {
1850 fn name(&self) -> &'static str {
1851 $display
1852 }
1853
1854 fn enablers(&self) -> &'static [&'static str] {
1855 $enablers
1856 }
1857
1858 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
1859 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
1860 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
1861 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
1862 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
1863 $( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
1864 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
1865 $( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
1866 $( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
1867 $( fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> { $provided_dependencies } )?
1868
1869 $(
1870 fn package_json_config_key(&self) -> Option<&'static str> {
1871 Some($pkg_key)
1872 }
1873 )?
1874
1875 $(
1876 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
1877 vec![$( ($pat, $exports) ),*]
1878 }
1879 )?
1880 }
1881 };
1882}
1883
1884pub mod config_parser;
1885mod config_value_credits;
1886mod manifest;
1887pub mod manifest_entries;
1888pub mod registry;
1889mod tooling;
1890
1891pub(crate) use module_federation::runtime_remotes;
1892pub use registry::{
1893 AggregatedPluginResult, PluginRegistry, PluginToolingDependencies, PluginToolingEvidence,
1894};
1895pub use tooling::types_package_target;
1896pub(crate) use tooling::{
1897 cli_tooling_config_patterns, is_ambient_types_package, is_known_tooling_dependency,
1898};
1899
1900fn add_import_referenced_dependencies(result: &mut PluginResult, source: &str, config_path: &Path) {
1901 let imports = config_parser::extract_imports(source, config_path);
1902 for import in &imports {
1903 result
1904 .referenced_dependencies
1905 .push(crate::resolve::extract_package_name(import));
1906 }
1907}
1908
1909fn credit_environment_optional_peers(environment: &str, result: &mut PluginResult) {
1921 credit_config_value(
1922 config_value_credits::CreditSurface::TestEnvironmentOptionalPeer,
1923 canonical_test_environment(environment),
1924 result,
1925 );
1926}
1927
1928fn credit_config_value(
1933 surface: config_value_credits::CreditSurface,
1934 value: &str,
1935 result: &mut PluginResult,
1936) -> bool {
1937 let Some(packages) = config_value_credits::credited_packages(surface, value) else {
1938 return false;
1939 };
1940 result
1941 .referenced_dependencies
1942 .extend(packages.iter().cloned());
1943 true
1944}
1945
1946fn canonical_test_environment(environment: &str) -> &str {
1954 environment
1955 .strip_prefix("jest-environment-")
1956 .or_else(|| environment.strip_prefix("vitest-environment-"))
1957 .unwrap_or(environment)
1958}
1959
1960mod adonis;
1961mod ag_ui;
1962mod angular;
1963mod astro;
1964mod ava;
1965mod babel;
1966mod biome;
1967mod browser_extension;
1968mod bun;
1969mod c8;
1970mod capacitor;
1971mod changesets;
1972mod commit_and_tag_version;
1973mod commitizen;
1974mod commitlint;
1975mod content_collections;
1976mod contentlayer;
1977mod convex;
1978mod cspell;
1979mod cucumber;
1980mod cypress;
1981mod danger;
1982mod deno;
1983mod dependency_cruiser;
1984mod docusaurus;
1985mod drizzle;
1986mod electron;
1987mod ember;
1988mod eslint;
1989mod eve;
1990mod expo;
1991mod expo_router;
1992mod firebase;
1993mod fumadocs;
1994mod gatsby;
1995mod graphql_codegen;
1996mod hardhat;
1997mod husky;
1998mod i18next;
1999mod ionic;
2000mod jest;
2001mod k6;
2002mod karma;
2003mod kibana;
2004mod knex;
2005mod kysely;
2006mod lefthook;
2007mod lexical;
2008mod lint_staged;
2009mod lit;
2010mod markdownlint;
2011mod mintlify;
2012mod mocha;
2013mod module_federation;
2014mod msw;
2015mod napi_rs;
2016mod nestjs;
2017mod nestjs_trpc;
2018mod next_intl;
2019mod nextjs;
2020mod nitro;
2021mod nodemon;
2022pub(crate) mod nuxt;
2023mod nx;
2024mod nyc;
2025mod obsidian;
2026mod openapi_ts;
2027mod opencode;
2028mod opennext_cloudflare;
2029mod oxfmt;
2030mod oxlint;
2031mod pandacss;
2032mod parcel;
2033mod pinia;
2034mod pkg_utils;
2035mod playwright;
2036mod plop;
2037mod pm2;
2038mod pnpm;
2039mod postcss;
2040mod prettier;
2041mod prisma;
2042mod qwik;
2043mod react_compiler;
2044mod react_native;
2045mod react_router;
2046mod redwoodsdk;
2047mod relay;
2048mod release_it;
2049mod remark;
2050mod remix;
2051mod rolldown;
2052mod rollup;
2053mod rsbuild;
2054mod rspack;
2055mod rspress;
2056mod sanity;
2057mod semantic_release;
2058mod sentry;
2059mod simple_git_hooks;
2060mod size_limit;
2061mod storybook;
2062mod stryker;
2063mod stylelint;
2064mod supabase;
2065mod sveltekit;
2066mod svgo;
2067mod svgr;
2068mod swc;
2069mod syncpack;
2070mod tailwind;
2071mod tanstack_router;
2072mod tap;
2073mod test_alias;
2074mod tsd;
2075mod tsdown;
2076mod tsup;
2077mod turborepo;
2078mod typedoc;
2079mod typeorm;
2080mod typescript;
2081mod unocss;
2082mod varlock;
2083mod velite;
2084mod vercel;
2085mod vite;
2086mod vitepress;
2087mod vitest;
2088mod vscode;
2089mod waku;
2090mod webdriverio;
2091mod webpack;
2092mod wrangler;
2093mod wuchale;
2094mod wxt;
2095
2096#[cfg(test)]
2097mod tests {
2098 use super::*;
2099
2100 #[test]
2101 fn segment_regexes_split_on_the_native_separator() {
2102 let regexes = vec![Regex::new("^_(components|hooks)$").expect("valid regex")];
2103 assert!(matches_segment_regex(
2104 "src/pages/_components/a.tsx",
2105 ®exes
2106 ));
2107 assert!(!matches_segment_regex(
2108 "src/pages/components/a.tsx",
2109 ®exes
2110 ));
2111 assert_eq!(
2112 matches_segment_regex("src\\pages\\_components\\a.tsx", ®exes),
2113 cfg!(windows),
2114 "a backslash separates segments only where it is the native separator"
2115 );
2116 }
2117 use std::path::Path;
2118
2119 #[test]
2120 fn is_enabled_with_deps_exact_match() {
2121 let plugin = nextjs::NextJsPlugin;
2122 let deps = vec!["next".to_string()];
2123 assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
2124 }
2125
2126 #[test]
2127 fn is_enabled_with_deps_no_match() {
2128 let plugin = nextjs::NextJsPlugin;
2129 let deps = vec!["react".to_string()];
2130 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
2131 }
2132
2133 #[test]
2134 fn is_enabled_with_deps_empty_deps() {
2135 let plugin = nextjs::NextJsPlugin;
2136 let deps: Vec<String> = vec![];
2137 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
2138 }
2139
2140 #[test]
2141 fn environment_optional_peers_come_from_the_credit_catalogue() {
2142 for environment in [
2143 "jsdom",
2144 "jest-environment-jsdom",
2145 "vitest-environment-jsdom",
2146 ] {
2147 let mut result = PluginResult::default();
2148 credit_environment_optional_peers(environment, &mut result);
2149 assert_eq!(
2150 result.referenced_dependencies,
2151 vec!["canvas".to_string()],
2152 "expected the catalogue credit for {environment}"
2153 );
2154 }
2155 }
2156
2157 #[test]
2158 fn environment_without_a_catalogue_row_credits_nothing() {
2159 let mut result = PluginResult::default();
2160 credit_environment_optional_peers("happy-dom", &mut result);
2161 assert!(result.referenced_dependencies.is_empty());
2162 }
2163
2164 #[test]
2165 fn entry_point_role_defaults_are_centralized() {
2166 assert_eq!(vite::VitePlugin.entry_point_role(), EntryPointRole::Runtime);
2167 assert_eq!(
2168 vitest::VitestPlugin.entry_point_role(),
2169 EntryPointRole::Test
2170 );
2171 assert_eq!(
2172 storybook::StorybookPlugin.entry_point_role(),
2173 EntryPointRole::Support
2174 );
2175 assert_eq!(
2176 obsidian::ObsidianPlugin.entry_point_role(),
2177 EntryPointRole::Runtime
2178 );
2179 assert_eq!(knex::KnexPlugin.entry_point_role(), EntryPointRole::Support);
2180 }
2181
2182 #[test]
2183 fn plugins_with_entry_patterns_have_explicit_role_intent() {
2184 let runtime_or_test_or_support: rustc_hash::FxHashSet<&'static str> =
2185 TEST_ENTRY_POINT_PLUGINS
2186 .iter()
2187 .chain(RUNTIME_ENTRY_POINT_PLUGINS.iter())
2188 .chain(SUPPORT_ENTRY_POINT_PLUGINS.iter())
2189 .copied()
2190 .collect();
2191
2192 for plugin in crate::plugins::registry::builtin::create_builtin_plugins() {
2193 if plugin.entry_patterns().is_empty() {
2194 continue;
2195 }
2196 assert!(
2197 runtime_or_test_or_support.contains(plugin.name()),
2198 "plugin '{}' exposes entry patterns but is missing from the entry-point role map",
2199 plugin.name()
2200 );
2201 }
2202 }
2203
2204 #[test]
2207 fn plugin_result_is_empty_only_when_every_field_is_empty() {
2208 type Fill = fn(&mut PluginResult);
2209
2210 assert!(PluginResult::default().is_empty());
2211
2212 let rows: [(&str, Fill); 15] = [
2213 ("entry_patterns", |r| {
2214 r.entry_patterns.push(PathRule::new("src/*.ts"));
2215 }),
2216 ("used_exports", |r| {
2217 r.used_exports
2218 .push(UsedExportRule::new("src/*.ts", ["default"]));
2219 }),
2220 ("used_class_members", |r| {
2221 r.used_class_members
2222 .push(UsedClassMemberRule::from("render"));
2223 }),
2224 ("referenced_dependencies", |r| {
2225 r.referenced_dependencies.push("lodash".to_string());
2226 }),
2227 ("package_referenced_dependencies", |r| {
2228 r.package_referenced_dependencies
2229 .push((PathBuf::from("/project/pkg"), "lodash".to_string()));
2230 }),
2231 ("always_used_files", |r| {
2232 r.always_used_files.push("**/*.stories.tsx".to_string());
2233 }),
2234 ("path_aliases", |r| {
2235 r.path_aliases.push(("@".to_string(), "src".to_string()));
2236 }),
2237 ("setup_files", |r| {
2238 r.setup_files.push(PathBuf::from("/setup.ts"));
2239 }),
2240 ("fixture_patterns", |r| {
2241 r.fixture_patterns.push("**/__fixtures__/**/*".to_string());
2242 }),
2243 ("scss_include_paths", |r| {
2244 r.scss_include_paths.push(PathBuf::from("/project/styles"));
2245 }),
2246 ("static_dir_mappings", |r| {
2247 r.static_dir_mappings
2248 .push((PathBuf::from("/project/public"), "/".to_string()));
2249 }),
2250 ("framework_static_dir_mappings", |r| {
2251 r.framework_static_dir_mappings
2252 .push((PathBuf::from("/project/static"), "/".to_string()));
2253 }),
2254 ("provided_dependencies", |r| {
2255 r.provided_dependencies.push(ProvidedDependencyRule::new(
2256 "**/*.stories.tsx",
2257 ["react"],
2258 Vec::<String>::new(),
2259 ));
2260 }),
2261 ("config_diagnostics", |r| {
2262 r.config_diagnostics
2263 .push(PluginConfigDiagnostic::unreadable(
2264 Path::new("/project/webpack.config.js"),
2265 "webpack",
2266 "exposes",
2267 "dynamic-value",
2268 ));
2269 }),
2270 ("federation_sources", |r| {
2271 r.federation_sources.push(FederationSource {
2272 target: FederationSourceTarget::Remote("app".to_string()),
2273 config_path: PathBuf::from("/project/webpack.config.js"),
2274 plugin: "webpack".to_string(),
2275 key: "remotes",
2276 });
2277 }),
2278 ];
2279
2280 for (field, fill) in rows {
2281 let mut result = PluginResult::default();
2282 fill(&mut result);
2283 assert!(
2284 !result.is_empty(),
2285 "a result with only {field} set must not be empty"
2286 );
2287 }
2288 }
2289
2290 #[test]
2291 fn is_enabled_with_deps_prefix_match() {
2292 let plugin = storybook::StorybookPlugin;
2293 let deps = vec!["@storybook/react".to_string()];
2294 assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
2295 }
2296
2297 #[test]
2298 fn is_enabled_with_deps_prefix_no_match_without_slash() {
2299 let plugin = storybook::StorybookPlugin;
2300 let deps = vec!["@storybookish".to_string()];
2301 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
2302 }
2303
2304 #[test]
2305 fn is_enabled_with_deps_multiple_enablers() {
2306 let plugin = vitest::VitestPlugin;
2307 let deps_vitest = vec!["vitest".to_string()];
2308 let deps_none = vec!["mocha".to_string()];
2309 assert!(plugin.is_enabled_with_deps(&deps_vitest, Path::new("/project")));
2310 assert!(!plugin.is_enabled_with_deps(&deps_none, Path::new("/project")));
2311 }
2312
2313 #[test]
2314 fn plugin_resolve_config_default_returns_empty() {
2315 let plugin = commitizen::CommitizenPlugin;
2316 let result = plugin.resolve_config(
2317 Path::new("/project/config.js"),
2318 "const x = 1;",
2319 Path::new("/project"),
2320 );
2321 assert!(result.is_empty());
2322 }
2323
2324 #[test]
2325 fn is_enabled_with_deps_exact_and_prefix_both_work() {
2326 let plugin = storybook::StorybookPlugin;
2327 let deps_exact = vec!["storybook".to_string()];
2328 assert!(plugin.is_enabled_with_deps(&deps_exact, Path::new("/project")));
2329 let deps_prefix = vec!["@storybook/vue3".to_string()];
2330 assert!(plugin.is_enabled_with_deps(&deps_prefix, Path::new("/project")));
2331 }
2332
2333 #[test]
2334 fn is_enabled_with_deps_multiple_enablers_remix() {
2335 let plugin = remix::RemixPlugin;
2336 let deps_node = vec!["@remix-run/node".to_string()];
2337 assert!(plugin.is_enabled_with_deps(&deps_node, Path::new("/project")));
2338 let deps_react = vec!["@remix-run/react".to_string()];
2339 assert!(plugin.is_enabled_with_deps(&deps_react, Path::new("/project")));
2340 let deps_cf = vec!["@remix-run/cloudflare".to_string()];
2341 assert!(plugin.is_enabled_with_deps(&deps_cf, Path::new("/project")));
2342 }
2343
2344 struct MinimalPlugin;
2345 impl Plugin for MinimalPlugin {
2346 fn name(&self) -> &'static str {
2347 "minimal"
2348 }
2349 }
2350
2351 #[test]
2352 fn default_resolve_config_returns_empty() {
2353 let r = MinimalPlugin.resolve_config(
2354 Path::new("config.js"),
2355 "export default {}",
2356 Path::new("/"),
2357 );
2358 assert!(r.is_empty());
2359 }
2360
2361 #[test]
2362 fn default_package_json_metadata_hooks_are_empty() {
2363 let pkg = PackageJson::default();
2364 assert!(!MinimalPlugin.is_enabled_with_package_json(&pkg, Path::new("/")));
2365 assert!(
2366 MinimalPlugin
2367 .resolve_package_json(&pkg, Path::new("/"))
2368 .is_empty()
2369 );
2370 }
2371
2372 #[test]
2373 fn default_is_enabled_returns_false_when_no_enablers() {
2374 let deps = vec!["anything".to_string()];
2375 assert!(!MinimalPlugin.is_enabled_with_deps(&deps, Path::new("/")));
2376 }
2377
2378 #[test]
2379 fn all_builtin_plugin_names_are_non_empty_and_unique() {
2380 let plugins = registry::builtin::create_builtin_plugins();
2381 let mut seen = std::collections::BTreeSet::new();
2382 for p in &plugins {
2383 let name = p.name();
2384 assert!(
2385 !name.is_empty(),
2386 "builtin plugins must have a non-empty name"
2387 );
2388 assert!(seen.insert(name), "duplicate plugin name: {name}");
2389 }
2390 }
2391
2392 #[test]
2393 fn all_builtin_plugins_have_activation_signals() {
2394 const NON_DEPENDENCY_ACTIVATED_PLUGINS: &[&str] = &["napi-rs", "deno", "kibana"];
2398 let plugins = registry::builtin::create_builtin_plugins();
2399 for p in &plugins {
2400 assert!(
2401 !p.enablers().is_empty()
2402 || !p.script_enablers().is_empty()
2403 || NON_DEPENDENCY_ACTIVATED_PLUGINS.contains(&p.name()),
2404 "plugin '{}' has no activation signal",
2405 p.name()
2406 );
2407 }
2408 }
2409
2410 #[test]
2411 fn plugins_with_config_patterns_have_always_used() {
2412 let plugins = registry::builtin::create_builtin_plugins();
2413 for p in &plugins {
2414 if !p.config_patterns().is_empty() {
2415 assert!(
2416 !p.always_used().is_empty(),
2417 "plugin '{}' has config_patterns but no always_used",
2418 p.name()
2419 );
2420 }
2421 }
2422 }
2423
2424 #[test]
2425 fn framework_plugins_enablers() {
2426 let cases: Vec<(&dyn Plugin, &[&str])> = vec![
2427 (&nextjs::NextJsPlugin, &["next"]),
2428 (&nuxt::NuxtPlugin, &["nuxt"]),
2429 (&angular::AngularPlugin, &["@angular/core"]),
2430 (&ionic::IonicPlugin, &["@ionic/angular"]),
2431 (&sveltekit::SvelteKitPlugin, &["@sveltejs/kit"]),
2432 (&gatsby::GatsbyPlugin, &["gatsby"]),
2433 ];
2434 for (plugin, expected_enablers) in cases {
2435 let enablers = plugin.enablers();
2436 for expected in expected_enablers {
2437 assert!(
2438 enablers.contains(expected),
2439 "plugin '{}' should have '{}'",
2440 plugin.name(),
2441 expected
2442 );
2443 }
2444 }
2445 }
2446
2447 #[test]
2448 fn testing_plugins_enablers() {
2449 let cases: Vec<(&dyn Plugin, &str)> = vec![
2450 (&jest::JestPlugin, "jest"),
2451 (&vitest::VitestPlugin, "vitest"),
2452 (&playwright::PlaywrightPlugin, "@playwright/test"),
2453 (&cypress::CypressPlugin, "cypress"),
2454 (&mocha::MochaPlugin, "mocha"),
2455 (&stryker::StrykerPlugin, "@stryker-mutator/core"),
2456 ];
2457 for (plugin, enabler) in cases {
2458 assert!(
2459 plugin.enablers().contains(&enabler),
2460 "plugin '{}' should have '{}'",
2461 plugin.name(),
2462 enabler
2463 );
2464 }
2465 }
2466
2467 #[test]
2468 fn bundler_plugins_enablers() {
2469 let cases: Vec<(&dyn Plugin, &str)> = vec![
2470 (&vite::VitePlugin, "vite"),
2471 (&webpack::WebpackPlugin, "webpack"),
2472 (&rollup::RollupPlugin, "rollup"),
2473 ];
2474 for (plugin, enabler) in cases {
2475 assert!(
2476 plugin.enablers().contains(&enabler),
2477 "plugin '{}' should have '{}'",
2478 plugin.name(),
2479 enabler
2480 );
2481 }
2482 }
2483
2484 #[test]
2485 fn test_plugins_have_test_entry_patterns() {
2486 let test_plugins: Vec<&dyn Plugin> = vec![
2487 &bun::BunPlugin,
2488 &deno::DenoPlugin,
2489 &jest::JestPlugin,
2490 &vitest::VitestPlugin,
2491 &mocha::MochaPlugin,
2492 &tap::TapPlugin,
2493 &tsd::TsdPlugin,
2494 ];
2495 for plugin in test_plugins {
2496 let patterns = plugin.entry_patterns();
2497 assert!(
2498 !patterns.is_empty(),
2499 "test plugin '{}' should have entry patterns",
2500 plugin.name()
2501 );
2502 assert!(
2503 patterns
2504 .iter()
2505 .any(|p| p.contains("test") || p.contains("spec") || p.contains("__tests__")),
2506 "test plugin '{}' should have test/spec patterns",
2507 plugin.name()
2508 );
2509 }
2510 }
2511
2512 #[test]
2513 fn framework_plugins_have_entry_patterns() {
2514 let plugins: Vec<&dyn Plugin> = vec![
2515 &nextjs::NextJsPlugin,
2516 &nuxt::NuxtPlugin,
2517 &angular::AngularPlugin,
2518 &sveltekit::SvelteKitPlugin,
2519 ];
2520 for plugin in plugins {
2521 assert!(
2522 !plugin.entry_patterns().is_empty(),
2523 "framework plugin '{}' should have entry patterns",
2524 plugin.name()
2525 );
2526 }
2527 }
2528
2529 #[test]
2530 fn plugins_with_resolve_config_have_config_patterns() {
2531 let plugins: Vec<&dyn Plugin> = vec![
2532 &jest::JestPlugin,
2533 &vitest::VitestPlugin,
2534 &babel::BabelPlugin,
2535 &eslint::EslintPlugin,
2536 &webpack::WebpackPlugin,
2537 &storybook::StorybookPlugin,
2538 &typescript::TypeScriptPlugin,
2539 &postcss::PostCssPlugin,
2540 &nextjs::NextJsPlugin,
2541 &nuxt::NuxtPlugin,
2542 &angular::AngularPlugin,
2543 &nx::NxPlugin,
2544 &stryker::StrykerPlugin,
2545 &wuchale::WuchalePlugin,
2546 &rollup::RollupPlugin,
2547 &sveltekit::SvelteKitPlugin,
2548 &prettier::PrettierPlugin,
2549 &contentlayer::ContentlayerPlugin,
2550 ];
2551 for plugin in plugins {
2552 assert!(
2553 !plugin.config_patterns().is_empty(),
2554 "plugin '{}' with resolve_config should have config_patterns",
2555 plugin.name()
2556 );
2557 }
2558 }
2559
2560 #[test]
2561 fn plugin_tooling_deps_include_enabler_package() {
2562 let plugins: Vec<&dyn Plugin> = vec![
2563 &jest::JestPlugin,
2564 &vitest::VitestPlugin,
2565 &webpack::WebpackPlugin,
2566 &typescript::TypeScriptPlugin,
2567 &eslint::EslintPlugin,
2568 &prettier::PrettierPlugin,
2569 &danger::DangerPlugin,
2570 &stryker::StrykerPlugin,
2571 &wuchale::WuchalePlugin,
2572 &contentlayer::ContentlayerPlugin,
2573 ];
2574 for plugin in plugins {
2575 let tooling = plugin.tooling_dependencies();
2576 let enablers = plugin.enablers();
2577 assert!(
2578 enablers
2579 .iter()
2580 .any(|e| !e.ends_with('/') && tooling.contains(e)),
2581 "plugin '{}': at least one non-prefix enabler should be in tooling_dependencies",
2582 plugin.name()
2583 );
2584 }
2585 }
2586
2587 #[test]
2588 fn nextjs_has_used_exports_for_pages() {
2589 let plugin = nextjs::NextJsPlugin;
2590 let exports = plugin.used_exports();
2591 assert!(!exports.is_empty());
2592 assert!(exports.iter().any(|(_, names)| names.contains(&"default")));
2593 }
2594
2595 #[test]
2596 fn remix_has_used_exports_for_routes() {
2597 let plugin = remix::RemixPlugin;
2598 let exports = plugin.used_exports();
2599 assert!(!exports.is_empty());
2600 let route_entry = exports.iter().find(|(pat, _)| pat.contains("routes"));
2601 assert!(route_entry.is_some());
2602 let (_, names) = route_entry.unwrap();
2603 assert!(names.contains(&"loader"));
2604 assert!(names.contains(&"action"));
2605 assert!(names.contains(&"default"));
2606 }
2607
2608 #[test]
2609 fn sveltekit_has_used_exports_for_routes() {
2610 let plugin = sveltekit::SvelteKitPlugin;
2611 let exports = plugin.used_exports();
2612 assert!(!exports.is_empty());
2613 assert!(exports.iter().any(|(_, names)| names.contains(&"GET")));
2614 }
2615
2616 #[test]
2617 fn nuxt_has_hash_virtual_prefix() {
2618 assert!(nuxt::NuxtPlugin.virtual_module_prefixes().contains(&"#"));
2619 }
2620
2621 #[test]
2622 fn sveltekit_has_dollar_virtual_prefixes() {
2623 let prefixes = sveltekit::SvelteKitPlugin.virtual_module_prefixes();
2624 assert!(prefixes.contains(&"$app/"));
2625 assert!(prefixes.contains(&"$env/"));
2626 assert!(prefixes.contains(&"$lib/"));
2627 }
2628
2629 #[test]
2630 fn sveltekit_has_lib_path_alias() {
2631 let aliases = sveltekit::SvelteKitPlugin.path_aliases(Path::new("/project"));
2632 assert!(aliases.iter().any(|(prefix, _)| *prefix == "$lib/"));
2633 }
2634
2635 #[test]
2636 fn nuxt_has_tilde_path_alias() {
2637 let aliases = nuxt::NuxtPlugin.path_aliases(Path::new("/nonexistent"));
2638 assert!(aliases.iter().any(|(prefix, _)| *prefix == "~/"));
2639 assert!(aliases.iter().any(|(prefix, _)| *prefix == "~~/"));
2640 }
2641
2642 #[test]
2643 fn jest_has_package_json_config_key() {
2644 assert_eq!(jest::JestPlugin.package_json_config_key(), Some("jest"));
2645 }
2646
2647 #[test]
2648 fn tsd_has_package_json_config_key() {
2649 assert_eq!(tsd::TsdPlugin.package_json_config_key(), Some("tsd"));
2650 }
2651
2652 #[test]
2653 fn babel_has_package_json_config_key() {
2654 assert_eq!(babel::BabelPlugin.package_json_config_key(), Some("babel"));
2655 }
2656
2657 #[test]
2658 fn eslint_has_package_json_config_key() {
2659 assert_eq!(
2660 eslint::EslintPlugin.package_json_config_key(),
2661 Some("eslintConfig")
2662 );
2663 }
2664
2665 #[test]
2666 fn prettier_has_package_json_config_key() {
2667 assert_eq!(
2668 prettier::PrettierPlugin.package_json_config_key(),
2669 Some("prettier")
2670 );
2671 }
2672
2673 #[test]
2674 fn macro_generated_plugin_basic_properties() {
2675 let plugin = msw::MswPlugin;
2676 assert_eq!(plugin.name(), "msw");
2677 assert!(plugin.enablers().contains(&"msw"));
2678 assert!(!plugin.entry_patterns().is_empty());
2679 assert!(plugin.config_patterns().is_empty());
2680 assert!(!plugin.always_used().is_empty());
2681 assert!(!plugin.tooling_dependencies().is_empty());
2682 }
2683
2684 #[test]
2685 fn macro_generated_plugin_with_used_exports() {
2686 let plugin = remix::RemixPlugin;
2687 assert_eq!(plugin.name(), "remix");
2688 assert!(!plugin.used_exports().is_empty());
2689 }
2690
2691 #[test]
2692 fn macro_passes_through_virtual_package_suffixes() {
2693 define_plugin! {
2694 struct MacroSuffixSmokePlugin => "macro-suffix-smoke",
2695 enablers: &["macro-suffix-smoke"],
2696 virtual_package_suffixes: &["/__macro_smoke__"],
2697 }
2698
2699 let plugin = MacroSuffixSmokePlugin;
2700 assert_eq!(
2701 plugin.virtual_package_suffixes(),
2702 &["/__macro_smoke__"],
2703 "macro-declared virtual_package_suffixes must propagate to the trait method"
2704 );
2705 }
2706
2707 #[test]
2708 fn macro_generated_plugin_imports_only_resolve_config() {
2709 let plugin = cypress::CypressPlugin;
2710 let source = r"
2711 import { defineConfig } from 'cypress';
2712 import coveragePlugin from '@cypress/code-coverage';
2713 export default defineConfig({});
2714 ";
2715 let result = plugin.resolve_config(
2716 Path::new("cypress.config.ts"),
2717 source,
2718 Path::new("/project"),
2719 );
2720 assert!(
2721 result
2722 .referenced_dependencies
2723 .contains(&"cypress".to_string())
2724 );
2725 assert!(
2726 result
2727 .referenced_dependencies
2728 .contains(&"@cypress/code-coverage".to_string())
2729 );
2730 }
2731
2732 #[test]
2733 fn builtin_plugin_count_is_expected() {
2734 let plugins = registry::builtin::create_builtin_plugins();
2735 assert!(
2736 plugins.len() >= 110,
2737 "expected at least 110 built-in plugins, got {}",
2738 plugins.len()
2739 );
2740 }
2741
2742 #[test]
2746 fn a_parent_relative_pattern_resolves_against_the_workspace_prefix() {
2747 let parent_relative = |pattern: &str| {
2748 let mut rule = PathRule::new(pattern);
2749 rule.parent_relative = true;
2750 rule
2751 };
2752 assert_eq!(
2753 parent_relative("../shared/src/Thing.tsx")
2754 .prefixed("packages/app")
2755 .pattern,
2756 "packages/shared/src/Thing.tsx"
2757 );
2758 assert_eq!(
2759 parent_relative("../../lib/index.{ts,js}")
2760 .prefixed("apps/web/client")
2761 .pattern,
2762 "apps/lib/index.{ts,js}"
2763 );
2764 assert!(
2765 parent_relative("../../../outside/Thing.tsx")
2766 .prefixed("packages/app")
2767 .pattern
2768 .starts_with("../"),
2769 "a climb past the project root matches no project file"
2770 );
2771 assert_eq!(
2772 parent_relative("src/index.ts")
2773 .prefixed("packages/app")
2774 .pattern,
2775 "packages/app/src/index.ts"
2776 );
2777 }
2778
2779 #[test]
2782 fn a_parent_relative_pattern_resolves_against_a_backslash_prefix() {
2783 let mut rule = PathRule::new("../../packages/ui/src/**/*.mdx");
2784 rule.parent_relative = true;
2785 assert_eq!(
2786 rule.prefixed("apps\\docs").pattern,
2787 "packages/ui/src/**/*.mdx"
2788 );
2789 }
2790
2791 #[test]
2794 fn a_plain_parent_pattern_is_not_resolved() {
2795 assert_eq!(
2796 PathRule::new("../src/**/*.stories.tsx")
2797 .prefixed("packages/ui")
2798 .pattern,
2799 "packages/ui/../src/**/*.stories.tsx"
2800 );
2801 }
2802}