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 "expo",
44 "expo-router",
45 "gatsby",
46 "hardhat",
47 "nestjs",
48 "next-intl",
49 "nextjs",
50 "nitro",
51 "nuxt",
52 "obsidian",
53 "parcel",
54 "qwik",
55 "react-native",
56 "react-router",
57 "redwoodsdk",
58 "remix",
59 "rolldown",
60 "rollup",
61 "rsbuild",
62 "rspack",
63 "sanity",
64 "supabase",
65 "sveltekit",
66 "tanstack-router",
67 "tsdown",
68 "tsup",
69 "vite",
70 "vitepress",
71 "webpack",
72 "wrangler",
73 "wxt",
74];
75
76#[cfg(test)]
77const SUPPORT_ENTRY_POINT_PLUGINS: &[&str] = &[
78 "content-collections",
79 "contentlayer",
80 "danger",
81 "drizzle",
82 "fumadocs",
83 "i18next",
84 "knex",
85 "kysely",
86 "mintlify",
87 "msw",
88 "opencode",
89 "prisma",
90 "storybook",
91 "stryker",
92 "typeorm",
93 "velite",
94];
95
96#[derive(Debug, Default)]
98pub struct PluginResult {
99 entry_patterns: Vec<PathRule>,
101 replace_entry_patterns: bool,
106 replace_used_export_rules: bool,
109 used_exports: Vec<UsedExportRule>,
111 used_class_members: Vec<UsedClassMemberRule>,
116 referenced_dependencies: Vec<String>,
118 always_used_files: Vec<String>,
120 path_aliases: Vec<(String, String)>,
122 setup_files: Vec<PathBuf>,
124 fixture_patterns: Vec<String>,
126 scss_include_paths: Vec<PathBuf>,
134 static_dir_mappings: Vec<(PathBuf, String)>,
137 framework_static_dir_mappings: Vec<(PathBuf, String)>,
138 provided_dependencies: Vec<ProvidedDependencyRule>,
141}
142
143impl PluginResult {
144 fn push_entry_pattern(&mut self, pattern: impl Into<String>) {
145 self.entry_patterns
146 .push(PathRule::new(normalize_entry_pattern(pattern.into())));
147 }
148
149 fn extend_entry_patterns<I, S>(&mut self, patterns: I)
150 where
151 I: IntoIterator<Item = S>,
152 S: Into<String>,
153 {
154 self.entry_patterns.extend(
155 patterns
156 .into_iter()
157 .map(|pat| PathRule::new(normalize_entry_pattern(pat.into()))),
158 );
159 }
160
161 fn push_used_export_rule(
162 &mut self,
163 pattern: impl Into<String>,
164 exports: impl IntoIterator<Item = impl Into<String>>,
165 ) {
166 self.used_exports
167 .push(UsedExportRule::new(pattern, exports));
168 }
169
170 #[must_use]
171 const fn is_empty(&self) -> bool {
172 self.entry_patterns.is_empty()
173 && self.used_exports.is_empty()
174 && self.used_class_members.is_empty()
175 && self.referenced_dependencies.is_empty()
176 && self.always_used_files.is_empty()
177 && self.path_aliases.is_empty()
178 && self.setup_files.is_empty()
179 && self.fixture_patterns.is_empty()
180 && self.scss_include_paths.is_empty()
181 && self.static_dir_mappings.is_empty()
182 && self.framework_static_dir_mappings.is_empty()
183 && self.provided_dependencies.is_empty()
184 }
185}
186
187fn normalize_entry_pattern(pattern: String) -> String {
188 pattern
189 .strip_prefix("./")
190 .map(str::to_owned)
191 .unwrap_or(pattern)
192}
193
194#[derive(Debug, Clone, Default, PartialEq, Eq)]
200pub struct PathRule {
201 pub pattern: String,
202 pub exclude_globs: Vec<String>,
203 pub exclude_regexes: Vec<String>,
204 pub exclude_segment_regexes: Vec<String>,
208}
209
210impl PathRule {
211 #[must_use]
212 pub(crate) fn new(pattern: impl Into<String>) -> Self {
213 Self {
214 pattern: pattern.into(),
215 exclude_globs: Vec::new(),
216 exclude_regexes: Vec::new(),
217 exclude_segment_regexes: Vec::new(),
218 }
219 }
220
221 #[must_use]
222 fn from_static(pattern: &'static str) -> Self {
223 Self::new(pattern)
224 }
225
226 #[must_use]
227 pub(crate) fn with_excluded_globs<I, S>(mut self, patterns: I) -> Self
228 where
229 I: IntoIterator<Item = S>,
230 S: Into<String>,
231 {
232 self.exclude_globs
233 .extend(patterns.into_iter().map(Into::into));
234 self
235 }
236
237 #[must_use]
238 fn with_excluded_regexes<I, S>(mut self, patterns: I) -> Self
239 where
240 I: IntoIterator<Item = S>,
241 S: Into<String>,
242 {
243 self.exclude_regexes
244 .extend(patterns.into_iter().map(Into::into));
245 self
246 }
247
248 #[must_use]
249 fn with_excluded_segment_regexes<I, S>(mut self, patterns: I) -> Self
250 where
251 I: IntoIterator<Item = S>,
252 S: Into<String>,
253 {
254 self.exclude_segment_regexes
255 .extend(patterns.into_iter().map(Into::into));
256 self
257 }
258
259 #[must_use]
260 fn prefixed(&self, ws_prefix: &str) -> Self {
261 Self {
262 pattern: prefix_workspace_pattern(&self.pattern, ws_prefix),
263 exclude_globs: self
264 .exclude_globs
265 .iter()
266 .map(|pattern| prefix_workspace_pattern(pattern, ws_prefix))
267 .collect(),
268 exclude_regexes: self
269 .exclude_regexes
270 .iter()
271 .map(|pattern| prefix_workspace_regex(pattern, ws_prefix))
272 .collect(),
273 exclude_segment_regexes: self.exclude_segment_regexes.clone(),
274 }
275 }
276}
277
278#[derive(Debug, Clone, Default, PartialEq, Eq)]
280pub struct UsedExportRule {
281 pub(crate) path: PathRule,
282 pub(crate) exports: Vec<String>,
283}
284
285impl UsedExportRule {
286 #[must_use]
287 pub(crate) fn new(
288 pattern: impl Into<String>,
289 exports: impl IntoIterator<Item = impl Into<String>>,
290 ) -> Self {
291 Self {
292 path: PathRule::new(pattern),
293 exports: exports.into_iter().map(Into::into).collect(),
294 }
295 }
296
297 #[must_use]
298 fn from_static(pattern: &'static str, exports: &'static [&'static str]) -> Self {
299 Self::new(pattern, exports.iter().copied())
300 }
301
302 #[must_use]
303 fn with_excluded_globs<I, S>(mut self, patterns: I) -> Self
304 where
305 I: IntoIterator<Item = S>,
306 S: Into<String>,
307 {
308 self.path = self.path.with_excluded_globs(patterns);
309 self
310 }
311
312 #[must_use]
313 fn with_excluded_regexes<I, S>(mut self, patterns: I) -> Self
314 where
315 I: IntoIterator<Item = S>,
316 S: Into<String>,
317 {
318 self.path = self.path.with_excluded_regexes(patterns);
319 self
320 }
321
322 #[must_use]
323 fn with_excluded_segment_regexes<I, S>(mut self, patterns: I) -> Self
324 where
325 I: IntoIterator<Item = S>,
326 S: Into<String>,
327 {
328 self.path = self.path.with_excluded_segment_regexes(patterns);
329 self
330 }
331
332 #[must_use]
333 fn prefixed(&self, ws_prefix: &str) -> Self {
334 Self {
335 path: self.path.prefixed(ws_prefix),
336 exports: self.exports.clone(),
337 }
338 }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq)]
343pub struct PluginUsedExportRule {
344 pub(crate) plugin_name: String,
345 pub(crate) rule: UsedExportRule,
346}
347
348impl PluginUsedExportRule {
349 #[must_use]
350 pub(crate) fn new(plugin_name: impl Into<String>, rule: UsedExportRule) -> Self {
351 Self {
352 plugin_name: plugin_name.into(),
353 rule,
354 }
355 }
356
357 #[must_use]
358 fn prefixed(&self, ws_prefix: &str) -> Self {
359 Self {
360 plugin_name: self.plugin_name.clone(),
361 rule: self.rule.prefixed(ws_prefix),
362 }
363 }
364}
365
366#[derive(Debug, Clone, Default, PartialEq, Eq)]
368pub struct ProvidedDependencyRule {
369 pub(crate) path: PathRule,
370 exact_specifiers: Vec<String>,
371 specifier_prefixes: Vec<String>,
372}
373
374impl ProvidedDependencyRule {
375 #[must_use]
376 fn new(
377 pattern: impl Into<String>,
378 exact_specifiers: impl IntoIterator<Item = impl Into<String>>,
379 specifier_prefixes: impl IntoIterator<Item = impl Into<String>>,
380 ) -> Self {
381 Self {
382 path: PathRule::new(pattern),
383 exact_specifiers: exact_specifiers.into_iter().map(Into::into).collect(),
384 specifier_prefixes: specifier_prefixes.into_iter().map(Into::into).collect(),
385 }
386 }
387
388 #[must_use]
389 fn prefixed(&self, ws_prefix: &str) -> Self {
390 Self {
391 path: self.path.prefixed(ws_prefix),
392 exact_specifiers: self.exact_specifiers.clone(),
393 specifier_prefixes: self.specifier_prefixes.clone(),
394 }
395 }
396
397 #[must_use]
398 pub(crate) fn may_cover_package(&self, package_name: &str) -> bool {
399 self.exact_specifiers
400 .iter()
401 .chain(self.specifier_prefixes.iter())
402 .any(|specifier| crate::resolve::extract_package_name(specifier) == package_name)
403 }
404
405 #[must_use]
406 pub(crate) fn covers_specifier(&self, specifier: &str) -> bool {
407 self.exact_specifiers
408 .iter()
409 .any(|allowed| allowed == specifier)
410 || self
411 .specifier_prefixes
412 .iter()
413 .any(|prefix| specifier.starts_with(prefix))
414 }
415}
416
417#[derive(Debug, Clone)]
419pub(crate) struct CompiledPathRule {
420 include: globset::GlobMatcher,
421 exclude_globs: Vec<globset::GlobMatcher>,
422 exclude_regexes: Vec<Regex>,
423 exclude_segment_regexes: Vec<Regex>,
424}
425
426impl CompiledPathRule {
427 pub(crate) fn for_entry_rule(rule: &PathRule, rule_kind: &str) -> Option<Self> {
428 let include = match globset::GlobBuilder::new(&rule.pattern)
429 .literal_separator(true)
430 .build()
431 {
432 Ok(glob) => glob.compile_matcher(),
433 Err(err) => {
434 tracing::warn!("invalid {rule_kind} '{}': {err}", rule.pattern);
435 return None;
436 }
437 };
438 Some(Self {
439 include,
440 exclude_globs: compile_excluded_globs(&rule.exclude_globs, rule_kind, &rule.pattern),
441 exclude_regexes: compile_excluded_regexes(
442 &rule.exclude_regexes,
443 rule_kind,
444 &rule.pattern,
445 ),
446 exclude_segment_regexes: compile_excluded_segment_regexes(
447 &rule.exclude_segment_regexes,
448 rule_kind,
449 &rule.pattern,
450 ),
451 })
452 }
453
454 pub(crate) fn for_used_export_rule(rule: &PathRule, rule_kind: &str) -> Option<Self> {
455 let include = match globset::Glob::new(&rule.pattern) {
456 Ok(glob) => glob.compile_matcher(),
457 Err(err) => {
458 tracing::warn!("invalid {rule_kind} '{}': {err}", rule.pattern);
459 return None;
460 }
461 };
462 Some(Self {
463 include,
464 exclude_globs: compile_excluded_globs(&rule.exclude_globs, rule_kind, &rule.pattern),
465 exclude_regexes: compile_excluded_regexes(
466 &rule.exclude_regexes,
467 rule_kind,
468 &rule.pattern,
469 ),
470 exclude_segment_regexes: compile_excluded_segment_regexes(
471 &rule.exclude_segment_regexes,
472 rule_kind,
473 &rule.pattern,
474 ),
475 })
476 }
477
478 #[must_use]
479 pub(crate) fn matches(&self, path: &str) -> bool {
480 self.include.is_match(path)
481 && !self.exclude_globs.iter().any(|glob| glob.is_match(path))
482 && !self
483 .exclude_regexes
484 .iter()
485 .any(|regex| regex.is_match(path))
486 && !matches_segment_regex(path, &self.exclude_segment_regexes)
487 }
488}
489
490fn prefix_workspace_pattern(pattern: &str, ws_prefix: &str) -> String {
491 if pattern.starts_with(ws_prefix) || pattern.starts_with('/') {
492 pattern.to_string()
493 } else {
494 format!("{ws_prefix}/{pattern}")
495 }
496}
497
498fn prefix_workspace_regex(pattern: &str, ws_prefix: &str) -> String {
499 if let Some(pattern) = pattern.strip_prefix('^') {
500 format!("^{}/{}", regex::escape(ws_prefix), pattern)
501 } else {
502 format!("^{}/(?:{})", regex::escape(ws_prefix), pattern)
503 }
504}
505
506fn compile_excluded_globs(
507 patterns: &[String],
508 rule_kind: &str,
509 rule_pattern: &str,
510) -> Vec<globset::GlobMatcher> {
511 patterns
512 .iter()
513 .filter_map(|pattern| {
514 match globset::GlobBuilder::new(pattern)
515 .literal_separator(true)
516 .build()
517 {
518 Ok(glob) => Some(glob.compile_matcher()),
519 Err(err) => {
520 tracing::warn!(
521 "skipping invalid excluded glob '{}' for {} '{}': {err}",
522 pattern,
523 rule_kind,
524 rule_pattern
525 );
526 None
527 }
528 }
529 })
530 .collect()
531}
532
533fn compile_excluded_regexes(
534 patterns: &[String],
535 rule_kind: &str,
536 rule_pattern: &str,
537) -> Vec<Regex> {
538 patterns
539 .iter()
540 .filter_map(|pattern| match Regex::new(pattern) {
541 Ok(regex) => Some(regex),
542 Err(err) => {
543 tracing::warn!(
544 "skipping invalid excluded regex '{}' for {} '{}': {err}",
545 pattern,
546 rule_kind,
547 rule_pattern
548 );
549 None
550 }
551 })
552 .collect()
553}
554
555fn compile_excluded_segment_regexes(
556 patterns: &[String],
557 rule_kind: &str,
558 rule_pattern: &str,
559) -> Vec<Regex> {
560 patterns
561 .iter()
562 .filter_map(|pattern| match Regex::new(pattern) {
563 Ok(regex) => Some(regex),
564 Err(err) => {
565 tracing::warn!(
566 "skipping invalid excluded segment regex '{}' for {} '{}': {err}",
567 pattern,
568 rule_kind,
569 rule_pattern
570 );
571 None
572 }
573 })
574 .collect()
575}
576
577fn matches_segment_regex(path: &str, regexes: &[Regex]) -> bool {
578 path.split('/')
579 .any(|segment| regexes.iter().any(|regex| regex.is_match(segment)))
580}
581
582impl From<String> for PathRule {
583 fn from(pattern: String) -> Self {
584 Self::new(pattern)
585 }
586}
587
588impl From<&str> for PathRule {
589 fn from(pattern: &str) -> Self {
590 Self::new(pattern)
591 }
592}
593
594impl std::ops::Deref for PathRule {
595 type Target = str;
596
597 fn deref(&self) -> &Self::Target {
598 &self.pattern
599 }
600}
601
602impl PartialEq<&str> for PathRule {
603 fn eq(&self, other: &&str) -> bool {
604 self.pattern == *other
605 }
606}
607
608impl PartialEq<str> for PathRule {
609 fn eq(&self, other: &str) -> bool {
610 self.pattern == other
611 }
612}
613
614impl PartialEq<String> for PathRule {
615 fn eq(&self, other: &String) -> bool {
616 &self.pattern == other
617 }
618}
619
620pub trait Plugin: Send + Sync {
622 fn name(&self) -> &'static str;
624
625 fn enablers(&self) -> &'static [&'static str] {
628 &[]
629 }
630
631 fn is_enabled(&self, pkg: &PackageJson, root: &Path) -> bool {
634 let deps = pkg.all_dependency_names();
635 self.is_enabled_with_deps(&deps, root)
636 }
637
638 fn is_enabled_with_deps(&self, deps: &[String], _root: &Path) -> bool {
641 let enablers = self.enablers();
642 if enablers.is_empty() {
643 return false;
644 }
645 enablers.iter().any(|enabler| {
646 if enabler.ends_with('/') {
647 deps.iter().any(|d| d.starts_with(enabler))
649 } else {
650 deps.iter().any(|d| d == enabler)
651 }
652 })
653 }
654
655 fn is_enabled_with_files(
668 &self,
669 deps: &[String],
670 root: &Path,
671 _discovered_files: &[PathBuf],
672 _candidate_index: Option<®istry::ConfigCandidateIndex>,
673 ) -> bool {
674 self.is_enabled_with_deps(deps, root)
675 }
676
677 fn script_enablers(&self) -> &'static [&'static str] {
679 &[]
680 }
681
682 fn is_enabled_with_scripts(
684 &self,
685 script_packages: &rustc_hash::FxHashSet<String>,
686 _root: &Path,
687 ) -> bool {
688 let enablers = self.script_enablers();
689 if enablers.is_empty() {
690 return false;
691 }
692 enablers.iter().any(|enabler| {
693 if enabler.ends_with('/') {
694 script_packages
695 .iter()
696 .any(|package| package.starts_with(enabler))
697 } else {
698 script_packages.contains(*enabler)
699 }
700 })
701 }
702
703 fn entry_patterns(&self) -> &'static [&'static str] {
705 &[]
706 }
707
708 fn entry_pattern_rules(&self) -> Vec<PathRule> {
710 self.entry_patterns()
711 .iter()
712 .map(|pattern| PathRule::from_static(pattern))
713 .collect()
714 }
715
716 fn entry_point_role(&self) -> EntryPointRole {
721 builtin_entry_point_role(self.name())
722 }
723
724 fn config_patterns(&self) -> &'static [&'static str] {
726 &[]
727 }
728
729 fn always_used(&self) -> &'static [&'static str] {
731 &[]
732 }
733
734 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
736 vec![]
737 }
738
739 fn used_export_rules(&self) -> Vec<UsedExportRule> {
741 self.used_exports()
742 .into_iter()
743 .map(|(pattern, exports)| UsedExportRule::from_static(pattern, exports))
744 .collect()
745 }
746
747 fn used_class_members(&self) -> &'static [&'static str] {
752 &[]
753 }
754
755 fn used_class_member_rules(&self) -> Vec<UsedClassMemberRule> {
763 Vec::new()
764 }
765
766 fn framework_class_member_contracts(&self) -> Vec<SemanticFrameworkContract> {
769 Vec::new()
770 }
771
772 fn fixture_glob_patterns(&self) -> &'static [&'static str] {
777 &[]
778 }
779
780 fn discovery_hidden_dirs(&self) -> &'static [&'static str] {
785 &[]
786 }
787
788 fn tooling_dependencies(&self) -> &'static [&'static str] {
791 &[]
792 }
793
794 fn virtual_module_prefixes(&self) -> &'static [&'static str] {
799 &[]
800 }
801
802 fn virtual_package_suffixes(&self) -> &'static [&'static str] {
808 &[]
809 }
810
811 fn generated_import_patterns(&self) -> &'static [&'static str] {
817 &[]
818 }
819
820 fn generated_type_import_prefixes(&self) -> &'static [&'static str] {
825 &[]
826 }
827
828 fn path_aliases(&self, _root: &Path) -> Vec<(&'static str, String)> {
838 vec![]
839 }
840
841 fn static_dir_mappings(&self, _root: &Path) -> Vec<(std::path::PathBuf, String)> {
853 vec![]
854 }
855
856 fn auto_imports(&self, _root: &Path) -> Vec<AutoImportRule> {
869 Vec::new()
870 }
871
872 fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> {
874 Vec::new()
875 }
876
877 fn is_enabled_with_package_json(&self, _pkg: &PackageJson, _root: &Path) -> bool {
879 false
880 }
881
882 fn resolve_package_json(&self, _pkg: &PackageJson, _root: &Path) -> PluginResult {
884 PluginResult::default()
885 }
886
887 fn package_json_referenced_dependencies(
892 &self,
893 _pkg: &PackageJson,
894 _root: &Path,
895 ) -> Vec<String> {
896 Vec::new()
897 }
898
899 fn resolve_config(&self, _config_path: &Path, _source: &str, _root: &Path) -> PluginResult {
904 PluginResult::default()
905 }
906
907 fn package_json_config_key(&self) -> Option<&'static str> {
912 None
913 }
914}
915
916fn builtin_entry_point_role(name: &str) -> EntryPointRole {
917 if TEST_ENTRY_POINT_PLUGINS.contains(&name) {
918 EntryPointRole::Test
919 } else if RUNTIME_ENTRY_POINT_PLUGINS.contains(&name) {
920 EntryPointRole::Runtime
921 } else {
922 EntryPointRole::Support
923 }
924}
925
926macro_rules! define_plugin {
987 (
988 struct $name:ident => $display:expr,
989 enablers: $enablers:expr
990 $(, entry_patterns: $entry:expr)?
991 $(, config_patterns: $config:expr)?
992 $(, always_used: $always:expr)?
993 $(, tooling_dependencies: $tooling:expr)?
994 $(, fixture_glob_patterns: $fixtures:expr)?
995 $(, discovery_hidden_dirs: $hidden_dirs:expr)?
996 $(, virtual_module_prefixes: $virtual:expr)?
997 $(, virtual_package_suffixes: $virtual_suffixes:expr)?
998 $(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
999 $(, provided_dependencies: $provided_dependencies:expr)?
1000 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
1001 , resolve_config: imports_only
1002 $(,)?
1003 ) => {
1004 pub struct $name;
1005
1006 impl Plugin for $name {
1007 fn name(&self) -> &'static str {
1008 $display
1009 }
1010
1011 fn enablers(&self) -> &'static [&'static str] {
1012 $enablers
1013 }
1014
1015 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
1016 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
1017 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
1018 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
1019 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
1020 $( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
1021 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
1022 $( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
1023 $( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
1024 $( fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> { $provided_dependencies } )?
1025
1026 $(
1027 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
1028 vec![$( ($pat, $exports) ),*]
1029 }
1030 )?
1031
1032 fn resolve_config(
1033 &self,
1034 config_path: &std::path::Path,
1035 source: &str,
1036 _root: &std::path::Path,
1037 ) -> PluginResult {
1038 let mut result = PluginResult::default();
1039 crate::plugins::add_import_referenced_dependencies(
1040 &mut result,
1041 source,
1042 config_path,
1043 );
1044 result
1045 }
1046 }
1047 };
1048
1049 (
1050 struct $name:ident => $display:expr,
1051 enablers: $enablers:expr
1052 $(, entry_patterns: $entry:expr)?
1053 $(, config_patterns: $config:expr)?
1054 $(, always_used: $always:expr)?
1055 $(, tooling_dependencies: $tooling:expr)?
1056 $(, fixture_glob_patterns: $fixtures:expr)?
1057 $(, discovery_hidden_dirs: $hidden_dirs:expr)?
1058 $(, virtual_module_prefixes: $virtual:expr)?
1059 $(, virtual_package_suffixes: $virtual_suffixes:expr)?
1060 $(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
1061 $(, provided_dependencies: $provided_dependencies:expr)?
1062 $(, package_json_config_key: $pkg_key:expr)?
1063 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
1064 , resolve_config($cp:ident, $src:ident, $root:ident) $body:block
1065 $(,)?
1066 ) => {
1067 pub struct $name;
1068
1069 impl Plugin for $name {
1070 fn name(&self) -> &'static str {
1071 $display
1072 }
1073
1074 fn enablers(&self) -> &'static [&'static str] {
1075 $enablers
1076 }
1077
1078 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
1079 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
1080 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
1081 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
1082 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
1083 $( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
1084 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
1085 $( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
1086 $( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
1087 $( fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> { $provided_dependencies } )?
1088
1089 $(
1090 fn package_json_config_key(&self) -> Option<&'static str> {
1091 Some($pkg_key)
1092 }
1093 )?
1094
1095 $(
1096 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
1097 vec![$( ($pat, $exports) ),*]
1098 }
1099 )?
1100
1101 fn resolve_config(
1102 &self,
1103 $cp: &std::path::Path,
1104 $src: &str,
1105 $root: &std::path::Path,
1106 ) -> PluginResult
1107 $body
1108 }
1109 };
1110
1111 (
1112 struct $name:ident => $display:expr,
1113 enablers: $enablers:expr
1114 $(, entry_patterns: $entry:expr)?
1115 $(, config_patterns: $config:expr)?
1116 $(, always_used: $always:expr)?
1117 $(, tooling_dependencies: $tooling:expr)?
1118 $(, fixture_glob_patterns: $fixtures:expr)?
1119 $(, discovery_hidden_dirs: $hidden_dirs:expr)?
1120 $(, virtual_module_prefixes: $virtual:expr)?
1121 $(, virtual_package_suffixes: $virtual_suffixes:expr)?
1122 $(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
1123 $(, provided_dependencies: $provided_dependencies:expr)?
1124 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
1125 $(,)?
1126 ) => {
1127 pub struct $name;
1128
1129 impl Plugin for $name {
1130 fn name(&self) -> &'static str {
1131 $display
1132 }
1133
1134 fn enablers(&self) -> &'static [&'static str] {
1135 $enablers
1136 }
1137
1138 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
1139 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
1140 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
1141 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
1142 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
1143 $( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
1144 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
1145 $( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
1146 $( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
1147 $( fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> { $provided_dependencies } )?
1148
1149 $(
1150 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
1151 vec![$( ($pat, $exports) ),*]
1152 }
1153 )?
1154 }
1155 };
1156}
1157
1158pub mod config_parser;
1159mod config_value_credits;
1160mod manifest;
1161pub mod manifest_entries;
1162pub mod registry;
1163mod tooling;
1164
1165pub use registry::{AggregatedPluginResult, PluginRegistry};
1166pub(crate) use tooling::is_known_tooling_dependency;
1167
1168fn add_import_referenced_dependencies(result: &mut PluginResult, source: &str, config_path: &Path) {
1169 let imports = config_parser::extract_imports(source, config_path);
1170 for import in &imports {
1171 result
1172 .referenced_dependencies
1173 .push(crate::resolve::extract_package_name(import));
1174 }
1175}
1176
1177fn credit_environment_optional_peers(environment: &str, result: &mut PluginResult) {
1189 credit_config_value(
1190 config_value_credits::CreditSurface::TestEnvironmentOptionalPeer,
1191 canonical_test_environment(environment),
1192 result,
1193 );
1194}
1195
1196fn credit_config_value(
1201 surface: config_value_credits::CreditSurface,
1202 value: &str,
1203 result: &mut PluginResult,
1204) -> bool {
1205 let Some(packages) = config_value_credits::credited_packages(surface, value) else {
1206 return false;
1207 };
1208 result
1209 .referenced_dependencies
1210 .extend(packages.iter().cloned());
1211 true
1212}
1213
1214fn canonical_test_environment(environment: &str) -> &str {
1222 environment
1223 .strip_prefix("jest-environment-")
1224 .or_else(|| environment.strip_prefix("vitest-environment-"))
1225 .unwrap_or(environment)
1226}
1227
1228mod adonis;
1229mod angular;
1230mod astro;
1231mod ava;
1232mod babel;
1233mod biome;
1234mod browser_extension;
1235mod bun;
1236mod c8;
1237mod capacitor;
1238mod changesets;
1239mod commit_and_tag_version;
1240mod commitizen;
1241mod commitlint;
1242mod content_collections;
1243mod contentlayer;
1244mod convex;
1245mod cspell;
1246mod cucumber;
1247mod cypress;
1248mod danger;
1249mod deno;
1250mod dependency_cruiser;
1251mod docusaurus;
1252mod drizzle;
1253mod electron;
1254mod ember;
1255mod eslint;
1256mod expo;
1257mod expo_router;
1258mod firebase;
1259mod fumadocs;
1260mod gatsby;
1261mod graphql_codegen;
1262mod hardhat;
1263mod husky;
1264mod i18next;
1265mod ionic;
1266mod jest;
1267mod k6;
1268mod karma;
1269mod knex;
1270mod kysely;
1271mod lefthook;
1272mod lexical;
1273mod lint_staged;
1274mod lit;
1275mod markdownlint;
1276mod mintlify;
1277mod mocha;
1278mod msw;
1279mod napi_rs;
1280mod nestjs;
1281mod next_intl;
1282mod nextjs;
1283mod nitro;
1284mod nodemon;
1285pub(crate) mod nuxt;
1286mod nx;
1287mod nyc;
1288mod obsidian;
1289mod openapi_ts;
1290mod opencode;
1291mod opennext_cloudflare;
1292mod oxlint;
1293mod pandacss;
1294mod parcel;
1295mod pinia;
1296mod pkg_utils;
1297mod playwright;
1298mod plop;
1299mod pm2;
1300mod pnpm;
1301mod postcss;
1302mod prettier;
1303mod prisma;
1304mod qwik;
1305mod react_compiler;
1306mod react_native;
1307mod react_router;
1308mod redwoodsdk;
1309mod relay;
1310mod remark;
1311mod remix;
1312mod rolldown;
1313mod rollup;
1314mod rsbuild;
1315mod rspack;
1316mod rspress;
1317mod sanity;
1318mod semantic_release;
1319mod sentry;
1320mod simple_git_hooks;
1321mod size_limit;
1322mod storybook;
1323mod stryker;
1324mod stylelint;
1325mod supabase;
1326mod sveltekit;
1327mod svgo;
1328mod svgr;
1329mod swc;
1330mod syncpack;
1331mod tailwind;
1332mod tanstack_router;
1333mod tap;
1334mod test_alias;
1335mod tsd;
1336mod tsdown;
1337mod tsup;
1338mod turborepo;
1339mod typedoc;
1340mod typeorm;
1341mod typescript;
1342mod unocss;
1343mod varlock;
1344mod velite;
1345mod vercel;
1346mod vite;
1347mod vitepress;
1348mod vitest;
1349mod vscode;
1350mod webdriverio;
1351mod webpack;
1352mod wrangler;
1353mod wuchale;
1354mod wxt;
1355
1356#[cfg(test)]
1357mod tests {
1358 use super::*;
1359 use std::path::Path;
1360
1361 #[test]
1362 fn is_enabled_with_deps_exact_match() {
1363 let plugin = nextjs::NextJsPlugin;
1364 let deps = vec!["next".to_string()];
1365 assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1366 }
1367
1368 #[test]
1369 fn is_enabled_with_deps_no_match() {
1370 let plugin = nextjs::NextJsPlugin;
1371 let deps = vec!["react".to_string()];
1372 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1373 }
1374
1375 #[test]
1376 fn is_enabled_with_deps_empty_deps() {
1377 let plugin = nextjs::NextJsPlugin;
1378 let deps: Vec<String> = vec![];
1379 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1380 }
1381
1382 #[test]
1383 fn environment_optional_peers_come_from_the_credit_catalogue() {
1384 for environment in [
1385 "jsdom",
1386 "jest-environment-jsdom",
1387 "vitest-environment-jsdom",
1388 ] {
1389 let mut result = PluginResult::default();
1390 credit_environment_optional_peers(environment, &mut result);
1391 assert_eq!(
1392 result.referenced_dependencies,
1393 vec!["canvas".to_string()],
1394 "expected the catalogue credit for {environment}"
1395 );
1396 }
1397 }
1398
1399 #[test]
1400 fn environment_without_a_catalogue_row_credits_nothing() {
1401 let mut result = PluginResult::default();
1402 credit_environment_optional_peers("happy-dom", &mut result);
1403 assert!(result.referenced_dependencies.is_empty());
1404 }
1405
1406 #[test]
1407 fn entry_point_role_defaults_are_centralized() {
1408 assert_eq!(vite::VitePlugin.entry_point_role(), EntryPointRole::Runtime);
1409 assert_eq!(
1410 vitest::VitestPlugin.entry_point_role(),
1411 EntryPointRole::Test
1412 );
1413 assert_eq!(
1414 storybook::StorybookPlugin.entry_point_role(),
1415 EntryPointRole::Support
1416 );
1417 assert_eq!(
1418 obsidian::ObsidianPlugin.entry_point_role(),
1419 EntryPointRole::Runtime
1420 );
1421 assert_eq!(knex::KnexPlugin.entry_point_role(), EntryPointRole::Support);
1422 }
1423
1424 #[test]
1425 fn plugins_with_entry_patterns_have_explicit_role_intent() {
1426 let runtime_or_test_or_support: rustc_hash::FxHashSet<&'static str> =
1427 TEST_ENTRY_POINT_PLUGINS
1428 .iter()
1429 .chain(RUNTIME_ENTRY_POINT_PLUGINS.iter())
1430 .chain(SUPPORT_ENTRY_POINT_PLUGINS.iter())
1431 .copied()
1432 .collect();
1433
1434 for plugin in crate::plugins::registry::builtin::create_builtin_plugins() {
1435 if plugin.entry_patterns().is_empty() {
1436 continue;
1437 }
1438 assert!(
1439 runtime_or_test_or_support.contains(plugin.name()),
1440 "plugin '{}' exposes entry patterns but is missing from the entry-point role map",
1441 plugin.name()
1442 );
1443 }
1444 }
1445
1446 #[test]
1447 fn plugin_result_is_empty_when_default() {
1448 let r = PluginResult::default();
1449 assert!(r.is_empty());
1450 }
1451
1452 #[test]
1453 fn plugin_result_not_empty_with_entry_patterns() {
1454 let r = PluginResult {
1455 entry_patterns: vec!["*.ts".into()],
1456 ..Default::default()
1457 };
1458 assert!(!r.is_empty());
1459 }
1460
1461 #[test]
1462 fn plugin_result_not_empty_with_referenced_deps() {
1463 let r = PluginResult {
1464 referenced_dependencies: vec!["lodash".to_string()],
1465 ..Default::default()
1466 };
1467 assert!(!r.is_empty());
1468 }
1469
1470 #[test]
1471 fn plugin_result_not_empty_with_setup_files() {
1472 let r = PluginResult {
1473 setup_files: vec![PathBuf::from("/setup.ts")],
1474 ..Default::default()
1475 };
1476 assert!(!r.is_empty());
1477 }
1478
1479 #[test]
1480 fn plugin_result_not_empty_with_always_used_files() {
1481 let r = PluginResult {
1482 always_used_files: vec!["**/*.stories.tsx".to_string()],
1483 ..Default::default()
1484 };
1485 assert!(!r.is_empty());
1486 }
1487
1488 #[test]
1489 fn plugin_result_not_empty_with_fixture_patterns() {
1490 let r = PluginResult {
1491 fixture_patterns: vec!["**/__fixtures__/**/*".to_string()],
1492 ..Default::default()
1493 };
1494 assert!(!r.is_empty());
1495 }
1496
1497 #[test]
1498 fn is_enabled_with_deps_prefix_match() {
1499 let plugin = storybook::StorybookPlugin;
1500 let deps = vec!["@storybook/react".to_string()];
1501 assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1502 }
1503
1504 #[test]
1505 fn is_enabled_with_deps_prefix_no_match_without_slash() {
1506 let plugin = storybook::StorybookPlugin;
1507 let deps = vec!["@storybookish".to_string()];
1508 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1509 }
1510
1511 #[test]
1512 fn is_enabled_with_deps_multiple_enablers() {
1513 let plugin = vitest::VitestPlugin;
1514 let deps_vitest = vec!["vitest".to_string()];
1515 let deps_none = vec!["mocha".to_string()];
1516 assert!(plugin.is_enabled_with_deps(&deps_vitest, Path::new("/project")));
1517 assert!(!plugin.is_enabled_with_deps(&deps_none, Path::new("/project")));
1518 }
1519
1520 #[test]
1521 fn plugin_default_methods_return_empty() {
1522 let plugin = commitizen::CommitizenPlugin;
1523 assert!(
1524 plugin.tooling_dependencies().is_empty() || !plugin.tooling_dependencies().is_empty()
1525 );
1526 assert!(plugin.virtual_module_prefixes().is_empty());
1527 assert!(plugin.virtual_package_suffixes().is_empty());
1528 assert!(plugin.path_aliases(Path::new("/project")).is_empty());
1529 assert!(
1530 plugin.package_json_config_key().is_none()
1531 || plugin.package_json_config_key().is_some()
1532 );
1533 }
1534
1535 #[test]
1536 fn plugin_resolve_config_default_returns_empty() {
1537 let plugin = commitizen::CommitizenPlugin;
1538 let result = plugin.resolve_config(
1539 Path::new("/project/config.js"),
1540 "const x = 1;",
1541 Path::new("/project"),
1542 );
1543 assert!(result.is_empty());
1544 }
1545
1546 #[test]
1547 fn is_enabled_with_deps_exact_and_prefix_both_work() {
1548 let plugin = storybook::StorybookPlugin;
1549 let deps_exact = vec!["storybook".to_string()];
1550 assert!(plugin.is_enabled_with_deps(&deps_exact, Path::new("/project")));
1551 let deps_prefix = vec!["@storybook/vue3".to_string()];
1552 assert!(plugin.is_enabled_with_deps(&deps_prefix, Path::new("/project")));
1553 }
1554
1555 #[test]
1556 fn is_enabled_with_deps_multiple_enablers_remix() {
1557 let plugin = remix::RemixPlugin;
1558 let deps_node = vec!["@remix-run/node".to_string()];
1559 assert!(plugin.is_enabled_with_deps(&deps_node, Path::new("/project")));
1560 let deps_react = vec!["@remix-run/react".to_string()];
1561 assert!(plugin.is_enabled_with_deps(&deps_react, Path::new("/project")));
1562 let deps_cf = vec!["@remix-run/cloudflare".to_string()];
1563 assert!(plugin.is_enabled_with_deps(&deps_cf, Path::new("/project")));
1564 }
1565
1566 struct MinimalPlugin;
1567 impl Plugin for MinimalPlugin {
1568 fn name(&self) -> &'static str {
1569 "minimal"
1570 }
1571 }
1572
1573 #[test]
1574 fn default_enablers_is_empty() {
1575 assert!(MinimalPlugin.enablers().is_empty());
1576 }
1577
1578 #[test]
1579 fn default_entry_patterns_is_empty() {
1580 assert!(MinimalPlugin.entry_patterns().is_empty());
1581 }
1582
1583 #[test]
1584 fn default_config_patterns_is_empty() {
1585 assert!(MinimalPlugin.config_patterns().is_empty());
1586 }
1587
1588 #[test]
1589 fn default_always_used_is_empty() {
1590 assert!(MinimalPlugin.always_used().is_empty());
1591 }
1592
1593 #[test]
1594 fn default_used_exports_is_empty() {
1595 assert!(MinimalPlugin.used_exports().is_empty());
1596 }
1597
1598 #[test]
1599 fn default_tooling_dependencies_is_empty() {
1600 assert!(MinimalPlugin.tooling_dependencies().is_empty());
1601 }
1602
1603 #[test]
1604 fn default_fixture_glob_patterns_is_empty() {
1605 assert!(MinimalPlugin.fixture_glob_patterns().is_empty());
1606 }
1607
1608 #[test]
1609 fn default_virtual_module_prefixes_is_empty() {
1610 assert!(MinimalPlugin.virtual_module_prefixes().is_empty());
1611 }
1612
1613 #[test]
1614 fn default_virtual_package_suffixes_is_empty() {
1615 assert!(MinimalPlugin.virtual_package_suffixes().is_empty());
1616 }
1617
1618 #[test]
1619 fn default_path_aliases_is_empty() {
1620 assert!(MinimalPlugin.path_aliases(Path::new("/")).is_empty());
1621 }
1622
1623 #[test]
1624 fn default_resolve_config_returns_empty() {
1625 let r = MinimalPlugin.resolve_config(
1626 Path::new("config.js"),
1627 "export default {}",
1628 Path::new("/"),
1629 );
1630 assert!(r.is_empty());
1631 }
1632
1633 #[test]
1634 fn default_package_json_metadata_hooks_are_empty() {
1635 let pkg = PackageJson::default();
1636 assert!(!MinimalPlugin.is_enabled_with_package_json(&pkg, Path::new("/")));
1637 assert!(
1638 MinimalPlugin
1639 .resolve_package_json(&pkg, Path::new("/"))
1640 .is_empty()
1641 );
1642 }
1643
1644 #[test]
1645 fn default_package_json_config_key_is_none() {
1646 assert!(MinimalPlugin.package_json_config_key().is_none());
1647 }
1648
1649 #[test]
1650 fn default_is_enabled_returns_false_when_no_enablers() {
1651 let deps = vec!["anything".to_string()];
1652 assert!(!MinimalPlugin.is_enabled_with_deps(&deps, Path::new("/")));
1653 }
1654
1655 #[test]
1656 fn all_builtin_plugin_names_are_unique() {
1657 let plugins = registry::builtin::create_builtin_plugins();
1658 let mut seen = std::collections::BTreeSet::new();
1659 for p in &plugins {
1660 let name = p.name();
1661 assert!(seen.insert(name), "duplicate plugin name: {name}");
1662 }
1663 }
1664
1665 #[test]
1666 fn all_builtin_plugins_have_activation_signals() {
1667 const NON_DEPENDENCY_ACTIVATED_PLUGINS: &[&str] = &["napi-rs", "deno"];
1670 let plugins = registry::builtin::create_builtin_plugins();
1671 for p in &plugins {
1672 assert!(
1673 !p.enablers().is_empty()
1674 || !p.script_enablers().is_empty()
1675 || NON_DEPENDENCY_ACTIVATED_PLUGINS.contains(&p.name()),
1676 "plugin '{}' has no activation signal",
1677 p.name()
1678 );
1679 }
1680 }
1681
1682 #[test]
1683 fn plugins_with_config_patterns_have_always_used() {
1684 let plugins = registry::builtin::create_builtin_plugins();
1685 for p in &plugins {
1686 if !p.config_patterns().is_empty() {
1687 assert!(
1688 !p.always_used().is_empty(),
1689 "plugin '{}' has config_patterns but no always_used",
1690 p.name()
1691 );
1692 }
1693 }
1694 }
1695
1696 #[test]
1697 fn framework_plugins_enablers() {
1698 let cases: Vec<(&dyn Plugin, &[&str])> = vec![
1699 (&nextjs::NextJsPlugin, &["next"]),
1700 (&nuxt::NuxtPlugin, &["nuxt"]),
1701 (&angular::AngularPlugin, &["@angular/core"]),
1702 (&ionic::IonicPlugin, &["@ionic/angular"]),
1703 (&sveltekit::SvelteKitPlugin, &["@sveltejs/kit"]),
1704 (&gatsby::GatsbyPlugin, &["gatsby"]),
1705 ];
1706 for (plugin, expected_enablers) in cases {
1707 let enablers = plugin.enablers();
1708 for expected in expected_enablers {
1709 assert!(
1710 enablers.contains(expected),
1711 "plugin '{}' should have '{}'",
1712 plugin.name(),
1713 expected
1714 );
1715 }
1716 }
1717 }
1718
1719 #[test]
1720 fn testing_plugins_enablers() {
1721 let cases: Vec<(&dyn Plugin, &str)> = vec![
1722 (&jest::JestPlugin, "jest"),
1723 (&vitest::VitestPlugin, "vitest"),
1724 (&playwright::PlaywrightPlugin, "@playwright/test"),
1725 (&cypress::CypressPlugin, "cypress"),
1726 (&mocha::MochaPlugin, "mocha"),
1727 (&stryker::StrykerPlugin, "@stryker-mutator/core"),
1728 ];
1729 for (plugin, enabler) in cases {
1730 assert!(
1731 plugin.enablers().contains(&enabler),
1732 "plugin '{}' should have '{}'",
1733 plugin.name(),
1734 enabler
1735 );
1736 }
1737 }
1738
1739 #[test]
1740 fn bundler_plugins_enablers() {
1741 let cases: Vec<(&dyn Plugin, &str)> = vec![
1742 (&vite::VitePlugin, "vite"),
1743 (&webpack::WebpackPlugin, "webpack"),
1744 (&rollup::RollupPlugin, "rollup"),
1745 ];
1746 for (plugin, enabler) in cases {
1747 assert!(
1748 plugin.enablers().contains(&enabler),
1749 "plugin '{}' should have '{}'",
1750 plugin.name(),
1751 enabler
1752 );
1753 }
1754 }
1755
1756 #[test]
1757 fn test_plugins_have_test_entry_patterns() {
1758 let test_plugins: Vec<&dyn Plugin> = vec![
1759 &bun::BunPlugin,
1760 &deno::DenoPlugin,
1761 &jest::JestPlugin,
1762 &vitest::VitestPlugin,
1763 &mocha::MochaPlugin,
1764 &tap::TapPlugin,
1765 &tsd::TsdPlugin,
1766 ];
1767 for plugin in test_plugins {
1768 let patterns = plugin.entry_patterns();
1769 assert!(
1770 !patterns.is_empty(),
1771 "test plugin '{}' should have entry patterns",
1772 plugin.name()
1773 );
1774 assert!(
1775 patterns
1776 .iter()
1777 .any(|p| p.contains("test") || p.contains("spec") || p.contains("__tests__")),
1778 "test plugin '{}' should have test/spec patterns",
1779 plugin.name()
1780 );
1781 }
1782 }
1783
1784 #[test]
1785 fn framework_plugins_have_entry_patterns() {
1786 let plugins: Vec<&dyn Plugin> = vec![
1787 &nextjs::NextJsPlugin,
1788 &nuxt::NuxtPlugin,
1789 &angular::AngularPlugin,
1790 &sveltekit::SvelteKitPlugin,
1791 ];
1792 for plugin in plugins {
1793 assert!(
1794 !plugin.entry_patterns().is_empty(),
1795 "framework plugin '{}' should have entry patterns",
1796 plugin.name()
1797 );
1798 }
1799 }
1800
1801 #[test]
1802 fn plugins_with_resolve_config_have_config_patterns() {
1803 let plugins: Vec<&dyn Plugin> = vec![
1804 &jest::JestPlugin,
1805 &vitest::VitestPlugin,
1806 &babel::BabelPlugin,
1807 &eslint::EslintPlugin,
1808 &webpack::WebpackPlugin,
1809 &storybook::StorybookPlugin,
1810 &typescript::TypeScriptPlugin,
1811 &postcss::PostCssPlugin,
1812 &nextjs::NextJsPlugin,
1813 &nuxt::NuxtPlugin,
1814 &angular::AngularPlugin,
1815 &nx::NxPlugin,
1816 &stryker::StrykerPlugin,
1817 &wuchale::WuchalePlugin,
1818 &rollup::RollupPlugin,
1819 &sveltekit::SvelteKitPlugin,
1820 &prettier::PrettierPlugin,
1821 &contentlayer::ContentlayerPlugin,
1822 ];
1823 for plugin in plugins {
1824 assert!(
1825 !plugin.config_patterns().is_empty(),
1826 "plugin '{}' with resolve_config should have config_patterns",
1827 plugin.name()
1828 );
1829 }
1830 }
1831
1832 #[test]
1833 fn plugin_tooling_deps_include_enabler_package() {
1834 let plugins: Vec<&dyn Plugin> = vec![
1835 &jest::JestPlugin,
1836 &vitest::VitestPlugin,
1837 &webpack::WebpackPlugin,
1838 &typescript::TypeScriptPlugin,
1839 &eslint::EslintPlugin,
1840 &prettier::PrettierPlugin,
1841 &danger::DangerPlugin,
1842 &stryker::StrykerPlugin,
1843 &wuchale::WuchalePlugin,
1844 &contentlayer::ContentlayerPlugin,
1845 ];
1846 for plugin in plugins {
1847 let tooling = plugin.tooling_dependencies();
1848 let enablers = plugin.enablers();
1849 assert!(
1850 enablers
1851 .iter()
1852 .any(|e| !e.ends_with('/') && tooling.contains(e)),
1853 "plugin '{}': at least one non-prefix enabler should be in tooling_dependencies",
1854 plugin.name()
1855 );
1856 }
1857 }
1858
1859 #[test]
1860 fn nextjs_has_used_exports_for_pages() {
1861 let plugin = nextjs::NextJsPlugin;
1862 let exports = plugin.used_exports();
1863 assert!(!exports.is_empty());
1864 assert!(exports.iter().any(|(_, names)| names.contains(&"default")));
1865 }
1866
1867 #[test]
1868 fn remix_has_used_exports_for_routes() {
1869 let plugin = remix::RemixPlugin;
1870 let exports = plugin.used_exports();
1871 assert!(!exports.is_empty());
1872 let route_entry = exports.iter().find(|(pat, _)| pat.contains("routes"));
1873 assert!(route_entry.is_some());
1874 let (_, names) = route_entry.unwrap();
1875 assert!(names.contains(&"loader"));
1876 assert!(names.contains(&"action"));
1877 assert!(names.contains(&"default"));
1878 }
1879
1880 #[test]
1881 fn sveltekit_has_used_exports_for_routes() {
1882 let plugin = sveltekit::SvelteKitPlugin;
1883 let exports = plugin.used_exports();
1884 assert!(!exports.is_empty());
1885 assert!(exports.iter().any(|(_, names)| names.contains(&"GET")));
1886 }
1887
1888 #[test]
1889 fn nuxt_has_hash_virtual_prefix() {
1890 assert!(nuxt::NuxtPlugin.virtual_module_prefixes().contains(&"#"));
1891 }
1892
1893 #[test]
1894 fn sveltekit_has_dollar_virtual_prefixes() {
1895 let prefixes = sveltekit::SvelteKitPlugin.virtual_module_prefixes();
1896 assert!(prefixes.contains(&"$app/"));
1897 assert!(prefixes.contains(&"$env/"));
1898 assert!(prefixes.contains(&"$lib/"));
1899 }
1900
1901 #[test]
1902 fn sveltekit_has_lib_path_alias() {
1903 let aliases = sveltekit::SvelteKitPlugin.path_aliases(Path::new("/project"));
1904 assert!(aliases.iter().any(|(prefix, _)| *prefix == "$lib/"));
1905 }
1906
1907 #[test]
1908 fn nuxt_has_tilde_path_alias() {
1909 let aliases = nuxt::NuxtPlugin.path_aliases(Path::new("/nonexistent"));
1910 assert!(aliases.iter().any(|(prefix, _)| *prefix == "~/"));
1911 assert!(aliases.iter().any(|(prefix, _)| *prefix == "~~/"));
1912 }
1913
1914 #[test]
1915 fn jest_has_package_json_config_key() {
1916 assert_eq!(jest::JestPlugin.package_json_config_key(), Some("jest"));
1917 }
1918
1919 #[test]
1920 fn tsd_has_package_json_config_key() {
1921 assert_eq!(tsd::TsdPlugin.package_json_config_key(), Some("tsd"));
1922 }
1923
1924 #[test]
1925 fn babel_has_package_json_config_key() {
1926 assert_eq!(babel::BabelPlugin.package_json_config_key(), Some("babel"));
1927 }
1928
1929 #[test]
1930 fn eslint_has_package_json_config_key() {
1931 assert_eq!(
1932 eslint::EslintPlugin.package_json_config_key(),
1933 Some("eslintConfig")
1934 );
1935 }
1936
1937 #[test]
1938 fn prettier_has_package_json_config_key() {
1939 assert_eq!(
1940 prettier::PrettierPlugin.package_json_config_key(),
1941 Some("prettier")
1942 );
1943 }
1944
1945 #[test]
1946 fn macro_generated_plugin_basic_properties() {
1947 let plugin = msw::MswPlugin;
1948 assert_eq!(plugin.name(), "msw");
1949 assert!(plugin.enablers().contains(&"msw"));
1950 assert!(!plugin.entry_patterns().is_empty());
1951 assert!(plugin.config_patterns().is_empty());
1952 assert!(!plugin.always_used().is_empty());
1953 assert!(!plugin.tooling_dependencies().is_empty());
1954 }
1955
1956 #[test]
1957 fn macro_generated_plugin_with_used_exports() {
1958 let plugin = remix::RemixPlugin;
1959 assert_eq!(plugin.name(), "remix");
1960 assert!(!plugin.used_exports().is_empty());
1961 }
1962
1963 #[test]
1964 fn macro_passes_through_virtual_package_suffixes() {
1965 define_plugin! {
1966 struct MacroSuffixSmokePlugin => "macro-suffix-smoke",
1967 enablers: &["macro-suffix-smoke"],
1968 virtual_package_suffixes: &["/__macro_smoke__"],
1969 }
1970
1971 let plugin = MacroSuffixSmokePlugin;
1972 assert_eq!(
1973 plugin.virtual_package_suffixes(),
1974 &["/__macro_smoke__"],
1975 "macro-declared virtual_package_suffixes must propagate to the trait method"
1976 );
1977 }
1978
1979 #[test]
1980 fn macro_generated_plugin_imports_only_resolve_config() {
1981 let plugin = cypress::CypressPlugin;
1982 let source = r"
1983 import { defineConfig } from 'cypress';
1984 import coveragePlugin from '@cypress/code-coverage';
1985 export default defineConfig({});
1986 ";
1987 let result = plugin.resolve_config(
1988 Path::new("cypress.config.ts"),
1989 source,
1990 Path::new("/project"),
1991 );
1992 assert!(
1993 result
1994 .referenced_dependencies
1995 .contains(&"cypress".to_string())
1996 );
1997 assert!(
1998 result
1999 .referenced_dependencies
2000 .contains(&"@cypress/code-coverage".to_string())
2001 );
2002 }
2003
2004 #[test]
2005 fn builtin_plugin_count_is_expected() {
2006 let plugins = registry::builtin::create_builtin_plugins();
2007 assert!(
2008 plugins.len() >= 110,
2009 "expected at least 110 built-in plugins, got {}",
2010 plugins.len()
2011 );
2012 }
2013}