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