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