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