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 fumadocs;
1138mod gatsby;
1139mod graphql_codegen;
1140mod hardhat;
1141mod husky;
1142mod i18next;
1143mod jest;
1144mod k6;
1145mod karma;
1146mod knex;
1147mod kysely;
1148mod lefthook;
1149mod lexical;
1150mod lint_staged;
1151mod lit;
1152mod markdownlint;
1153mod mintlify;
1154mod mocha;
1155mod msw;
1156mod nestjs;
1157mod next_intl;
1158mod nextjs;
1159mod nitro;
1160mod nodemon;
1161pub(crate) mod nuxt;
1162mod nx;
1163mod nyc;
1164mod obsidian;
1165mod openapi_ts;
1166mod opencode;
1167mod opennext_cloudflare;
1168mod oxlint;
1169mod pandacss;
1170mod parcel;
1171mod pinia;
1172mod pkg_utils;
1173mod playwright;
1174mod plop;
1175mod pm2;
1176mod pnpm;
1177mod postcss;
1178mod prettier;
1179mod prisma;
1180mod qwik;
1181mod react_compiler;
1182mod react_native;
1183mod react_router;
1184mod redwoodsdk;
1185mod relay;
1186mod remark;
1187mod remix;
1188mod rolldown;
1189mod rollup;
1190mod rsbuild;
1191mod rspack;
1192mod rspress;
1193mod sanity;
1194mod semantic_release;
1195mod sentry;
1196mod simple_git_hooks;
1197mod storybook;
1198mod stryker;
1199mod stylelint;
1200mod supabase;
1201mod sveltekit;
1202mod svgo;
1203mod svgr;
1204mod swc;
1205mod syncpack;
1206mod tailwind;
1207mod tanstack_router;
1208mod tap;
1209mod test_alias;
1210mod tsd;
1211mod tsdown;
1212mod tsup;
1213mod turborepo;
1214mod typedoc;
1215mod typeorm;
1216mod typescript;
1217mod unocss;
1218mod varlock;
1219mod velite;
1220mod vercel;
1221mod vite;
1222mod vitepress;
1223mod vitest;
1224mod webdriverio;
1225mod webpack;
1226mod wrangler;
1227mod wuchale;
1228mod wxt;
1229
1230#[cfg(test)]
1231mod tests {
1232 use super::*;
1233 use std::path::Path;
1234
1235 #[test]
1236 fn is_enabled_with_deps_exact_match() {
1237 let plugin = nextjs::NextJsPlugin;
1238 let deps = vec!["next".to_string()];
1239 assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1240 }
1241
1242 #[test]
1243 fn is_enabled_with_deps_no_match() {
1244 let plugin = nextjs::NextJsPlugin;
1245 let deps = vec!["react".to_string()];
1246 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1247 }
1248
1249 #[test]
1250 fn is_enabled_with_deps_empty_deps() {
1251 let plugin = nextjs::NextJsPlugin;
1252 let deps: Vec<String> = vec![];
1253 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1254 }
1255
1256 #[test]
1257 fn entry_point_role_defaults_are_centralized() {
1258 assert_eq!(vite::VitePlugin.entry_point_role(), EntryPointRole::Runtime);
1259 assert_eq!(
1260 vitest::VitestPlugin.entry_point_role(),
1261 EntryPointRole::Test
1262 );
1263 assert_eq!(
1264 storybook::StorybookPlugin.entry_point_role(),
1265 EntryPointRole::Support
1266 );
1267 assert_eq!(
1268 obsidian::ObsidianPlugin.entry_point_role(),
1269 EntryPointRole::Runtime
1270 );
1271 assert_eq!(knex::KnexPlugin.entry_point_role(), EntryPointRole::Support);
1272 }
1273
1274 #[test]
1275 fn plugins_with_entry_patterns_have_explicit_role_intent() {
1276 let runtime_or_test_or_support: rustc_hash::FxHashSet<&'static str> =
1277 TEST_ENTRY_POINT_PLUGINS
1278 .iter()
1279 .chain(RUNTIME_ENTRY_POINT_PLUGINS.iter())
1280 .chain(SUPPORT_ENTRY_POINT_PLUGINS.iter())
1281 .copied()
1282 .collect();
1283
1284 for plugin in crate::plugins::registry::builtin::create_builtin_plugins() {
1285 if plugin.entry_patterns().is_empty() {
1286 continue;
1287 }
1288 assert!(
1289 runtime_or_test_or_support.contains(plugin.name()),
1290 "plugin '{}' exposes entry patterns but is missing from the entry-point role map",
1291 plugin.name()
1292 );
1293 }
1294 }
1295
1296 #[test]
1297 fn plugin_result_is_empty_when_default() {
1298 let r = PluginResult::default();
1299 assert!(r.is_empty());
1300 }
1301
1302 #[test]
1303 fn plugin_result_not_empty_with_entry_patterns() {
1304 let r = PluginResult {
1305 entry_patterns: vec!["*.ts".into()],
1306 ..Default::default()
1307 };
1308 assert!(!r.is_empty());
1309 }
1310
1311 #[test]
1312 fn plugin_result_not_empty_with_referenced_deps() {
1313 let r = PluginResult {
1314 referenced_dependencies: vec!["lodash".to_string()],
1315 ..Default::default()
1316 };
1317 assert!(!r.is_empty());
1318 }
1319
1320 #[test]
1321 fn plugin_result_not_empty_with_setup_files() {
1322 let r = PluginResult {
1323 setup_files: vec![PathBuf::from("/setup.ts")],
1324 ..Default::default()
1325 };
1326 assert!(!r.is_empty());
1327 }
1328
1329 #[test]
1330 fn plugin_result_not_empty_with_always_used_files() {
1331 let r = PluginResult {
1332 always_used_files: vec!["**/*.stories.tsx".to_string()],
1333 ..Default::default()
1334 };
1335 assert!(!r.is_empty());
1336 }
1337
1338 #[test]
1339 fn plugin_result_not_empty_with_fixture_patterns() {
1340 let r = PluginResult {
1341 fixture_patterns: vec!["**/__fixtures__/**/*".to_string()],
1342 ..Default::default()
1343 };
1344 assert!(!r.is_empty());
1345 }
1346
1347 #[test]
1348 fn is_enabled_with_deps_prefix_match() {
1349 let plugin = storybook::StorybookPlugin;
1350 let deps = vec!["@storybook/react".to_string()];
1351 assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1352 }
1353
1354 #[test]
1355 fn is_enabled_with_deps_prefix_no_match_without_slash() {
1356 let plugin = storybook::StorybookPlugin;
1357 let deps = vec!["@storybookish".to_string()];
1358 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1359 }
1360
1361 #[test]
1362 fn is_enabled_with_deps_multiple_enablers() {
1363 let plugin = vitest::VitestPlugin;
1364 let deps_vitest = vec!["vitest".to_string()];
1365 let deps_none = vec!["mocha".to_string()];
1366 assert!(plugin.is_enabled_with_deps(&deps_vitest, Path::new("/project")));
1367 assert!(!plugin.is_enabled_with_deps(&deps_none, Path::new("/project")));
1368 }
1369
1370 #[test]
1371 fn plugin_default_methods_return_empty() {
1372 let plugin = commitizen::CommitizenPlugin;
1373 assert!(
1374 plugin.tooling_dependencies().is_empty() || !plugin.tooling_dependencies().is_empty()
1375 );
1376 assert!(plugin.virtual_module_prefixes().is_empty());
1377 assert!(plugin.virtual_package_suffixes().is_empty());
1378 assert!(plugin.path_aliases(Path::new("/project")).is_empty());
1379 assert!(
1380 plugin.package_json_config_key().is_none()
1381 || plugin.package_json_config_key().is_some()
1382 );
1383 }
1384
1385 #[test]
1386 fn plugin_resolve_config_default_returns_empty() {
1387 let plugin = commitizen::CommitizenPlugin;
1388 let result = plugin.resolve_config(
1389 Path::new("/project/config.js"),
1390 "const x = 1;",
1391 Path::new("/project"),
1392 );
1393 assert!(result.is_empty());
1394 }
1395
1396 #[test]
1397 fn is_enabled_with_deps_exact_and_prefix_both_work() {
1398 let plugin = storybook::StorybookPlugin;
1399 let deps_exact = vec!["storybook".to_string()];
1400 assert!(plugin.is_enabled_with_deps(&deps_exact, Path::new("/project")));
1401 let deps_prefix = vec!["@storybook/vue3".to_string()];
1402 assert!(plugin.is_enabled_with_deps(&deps_prefix, Path::new("/project")));
1403 }
1404
1405 #[test]
1406 fn is_enabled_with_deps_multiple_enablers_remix() {
1407 let plugin = remix::RemixPlugin;
1408 let deps_node = vec!["@remix-run/node".to_string()];
1409 assert!(plugin.is_enabled_with_deps(&deps_node, Path::new("/project")));
1410 let deps_react = vec!["@remix-run/react".to_string()];
1411 assert!(plugin.is_enabled_with_deps(&deps_react, Path::new("/project")));
1412 let deps_cf = vec!["@remix-run/cloudflare".to_string()];
1413 assert!(plugin.is_enabled_with_deps(&deps_cf, Path::new("/project")));
1414 }
1415
1416 struct MinimalPlugin;
1417 impl Plugin for MinimalPlugin {
1418 fn name(&self) -> &'static str {
1419 "minimal"
1420 }
1421 }
1422
1423 #[test]
1424 fn default_enablers_is_empty() {
1425 assert!(MinimalPlugin.enablers().is_empty());
1426 }
1427
1428 #[test]
1429 fn default_entry_patterns_is_empty() {
1430 assert!(MinimalPlugin.entry_patterns().is_empty());
1431 }
1432
1433 #[test]
1434 fn default_config_patterns_is_empty() {
1435 assert!(MinimalPlugin.config_patterns().is_empty());
1436 }
1437
1438 #[test]
1439 fn default_always_used_is_empty() {
1440 assert!(MinimalPlugin.always_used().is_empty());
1441 }
1442
1443 #[test]
1444 fn default_used_exports_is_empty() {
1445 assert!(MinimalPlugin.used_exports().is_empty());
1446 }
1447
1448 #[test]
1449 fn default_tooling_dependencies_is_empty() {
1450 assert!(MinimalPlugin.tooling_dependencies().is_empty());
1451 }
1452
1453 #[test]
1454 fn default_fixture_glob_patterns_is_empty() {
1455 assert!(MinimalPlugin.fixture_glob_patterns().is_empty());
1456 }
1457
1458 #[test]
1459 fn default_virtual_module_prefixes_is_empty() {
1460 assert!(MinimalPlugin.virtual_module_prefixes().is_empty());
1461 }
1462
1463 #[test]
1464 fn default_virtual_package_suffixes_is_empty() {
1465 assert!(MinimalPlugin.virtual_package_suffixes().is_empty());
1466 }
1467
1468 #[test]
1469 fn default_path_aliases_is_empty() {
1470 assert!(MinimalPlugin.path_aliases(Path::new("/")).is_empty());
1471 }
1472
1473 #[test]
1474 fn default_resolve_config_returns_empty() {
1475 let r = MinimalPlugin.resolve_config(
1476 Path::new("config.js"),
1477 "export default {}",
1478 Path::new("/"),
1479 );
1480 assert!(r.is_empty());
1481 }
1482
1483 #[test]
1484 fn default_package_json_config_key_is_none() {
1485 assert!(MinimalPlugin.package_json_config_key().is_none());
1486 }
1487
1488 #[test]
1489 fn default_is_enabled_returns_false_when_no_enablers() {
1490 let deps = vec!["anything".to_string()];
1491 assert!(!MinimalPlugin.is_enabled_with_deps(&deps, Path::new("/")));
1492 }
1493
1494 #[test]
1495 fn all_builtin_plugin_names_are_unique() {
1496 let plugins = registry::builtin::create_builtin_plugins();
1497 let mut seen = std::collections::BTreeSet::new();
1498 for p in &plugins {
1499 let name = p.name();
1500 assert!(seen.insert(name), "duplicate plugin name: {name}");
1501 }
1502 }
1503
1504 #[test]
1505 fn all_builtin_plugins_have_enablers() {
1506 let plugins = registry::builtin::create_builtin_plugins();
1507 for p in &plugins {
1508 assert!(
1509 !p.enablers().is_empty(),
1510 "plugin '{}' has no enablers",
1511 p.name()
1512 );
1513 }
1514 }
1515
1516 #[test]
1517 fn plugins_with_config_patterns_have_always_used() {
1518 let plugins = registry::builtin::create_builtin_plugins();
1519 for p in &plugins {
1520 if !p.config_patterns().is_empty() {
1521 assert!(
1522 !p.always_used().is_empty(),
1523 "plugin '{}' has config_patterns but no always_used",
1524 p.name()
1525 );
1526 }
1527 }
1528 }
1529
1530 #[test]
1531 fn framework_plugins_enablers() {
1532 let cases: Vec<(&dyn Plugin, &[&str])> = vec![
1533 (&nextjs::NextJsPlugin, &["next"]),
1534 (&nuxt::NuxtPlugin, &["nuxt"]),
1535 (&angular::AngularPlugin, &["@angular/core"]),
1536 (&sveltekit::SvelteKitPlugin, &["@sveltejs/kit"]),
1537 (&gatsby::GatsbyPlugin, &["gatsby"]),
1538 ];
1539 for (plugin, expected_enablers) in cases {
1540 let enablers = plugin.enablers();
1541 for expected in expected_enablers {
1542 assert!(
1543 enablers.contains(expected),
1544 "plugin '{}' should have '{}'",
1545 plugin.name(),
1546 expected
1547 );
1548 }
1549 }
1550 }
1551
1552 #[test]
1553 fn testing_plugins_enablers() {
1554 let cases: Vec<(&dyn Plugin, &str)> = vec![
1555 (&jest::JestPlugin, "jest"),
1556 (&vitest::VitestPlugin, "vitest"),
1557 (&playwright::PlaywrightPlugin, "@playwright/test"),
1558 (&cypress::CypressPlugin, "cypress"),
1559 (&mocha::MochaPlugin, "mocha"),
1560 (&stryker::StrykerPlugin, "@stryker-mutator/core"),
1561 ];
1562 for (plugin, enabler) in cases {
1563 assert!(
1564 plugin.enablers().contains(&enabler),
1565 "plugin '{}' should have '{}'",
1566 plugin.name(),
1567 enabler
1568 );
1569 }
1570 }
1571
1572 #[test]
1573 fn bundler_plugins_enablers() {
1574 let cases: Vec<(&dyn Plugin, &str)> = vec![
1575 (&vite::VitePlugin, "vite"),
1576 (&webpack::WebpackPlugin, "webpack"),
1577 (&rollup::RollupPlugin, "rollup"),
1578 ];
1579 for (plugin, enabler) in cases {
1580 assert!(
1581 plugin.enablers().contains(&enabler),
1582 "plugin '{}' should have '{}'",
1583 plugin.name(),
1584 enabler
1585 );
1586 }
1587 }
1588
1589 #[test]
1590 fn test_plugins_have_test_entry_patterns() {
1591 let test_plugins: Vec<&dyn Plugin> = vec![
1592 &jest::JestPlugin,
1593 &vitest::VitestPlugin,
1594 &mocha::MochaPlugin,
1595 &tap::TapPlugin,
1596 &tsd::TsdPlugin,
1597 ];
1598 for plugin in test_plugins {
1599 let patterns = plugin.entry_patterns();
1600 assert!(
1601 !patterns.is_empty(),
1602 "test plugin '{}' should have entry patterns",
1603 plugin.name()
1604 );
1605 assert!(
1606 patterns
1607 .iter()
1608 .any(|p| p.contains("test") || p.contains("spec") || p.contains("__tests__")),
1609 "test plugin '{}' should have test/spec patterns",
1610 plugin.name()
1611 );
1612 }
1613 }
1614
1615 #[test]
1616 fn framework_plugins_have_entry_patterns() {
1617 let plugins: Vec<&dyn Plugin> = vec![
1618 &nextjs::NextJsPlugin,
1619 &nuxt::NuxtPlugin,
1620 &angular::AngularPlugin,
1621 &sveltekit::SvelteKitPlugin,
1622 ];
1623 for plugin in plugins {
1624 assert!(
1625 !plugin.entry_patterns().is_empty(),
1626 "framework plugin '{}' should have entry patterns",
1627 plugin.name()
1628 );
1629 }
1630 }
1631
1632 #[test]
1633 fn plugins_with_resolve_config_have_config_patterns() {
1634 let plugins: Vec<&dyn Plugin> = vec![
1635 &jest::JestPlugin,
1636 &vitest::VitestPlugin,
1637 &babel::BabelPlugin,
1638 &eslint::EslintPlugin,
1639 &webpack::WebpackPlugin,
1640 &storybook::StorybookPlugin,
1641 &typescript::TypeScriptPlugin,
1642 &postcss::PostCssPlugin,
1643 &nextjs::NextJsPlugin,
1644 &nuxt::NuxtPlugin,
1645 &angular::AngularPlugin,
1646 &nx::NxPlugin,
1647 &stryker::StrykerPlugin,
1648 &wuchale::WuchalePlugin,
1649 &rollup::RollupPlugin,
1650 &sveltekit::SvelteKitPlugin,
1651 &prettier::PrettierPlugin,
1652 &contentlayer::ContentlayerPlugin,
1653 ];
1654 for plugin in plugins {
1655 assert!(
1656 !plugin.config_patterns().is_empty(),
1657 "plugin '{}' with resolve_config should have config_patterns",
1658 plugin.name()
1659 );
1660 }
1661 }
1662
1663 #[test]
1664 fn plugin_tooling_deps_include_enabler_package() {
1665 let plugins: Vec<&dyn Plugin> = vec![
1666 &jest::JestPlugin,
1667 &vitest::VitestPlugin,
1668 &webpack::WebpackPlugin,
1669 &typescript::TypeScriptPlugin,
1670 &eslint::EslintPlugin,
1671 &prettier::PrettierPlugin,
1672 &danger::DangerPlugin,
1673 &stryker::StrykerPlugin,
1674 &wuchale::WuchalePlugin,
1675 &contentlayer::ContentlayerPlugin,
1676 ];
1677 for plugin in plugins {
1678 let tooling = plugin.tooling_dependencies();
1679 let enablers = plugin.enablers();
1680 assert!(
1681 enablers
1682 .iter()
1683 .any(|e| !e.ends_with('/') && tooling.contains(e)),
1684 "plugin '{}': at least one non-prefix enabler should be in tooling_dependencies",
1685 plugin.name()
1686 );
1687 }
1688 }
1689
1690 #[test]
1691 fn nextjs_has_used_exports_for_pages() {
1692 let plugin = nextjs::NextJsPlugin;
1693 let exports = plugin.used_exports();
1694 assert!(!exports.is_empty());
1695 assert!(exports.iter().any(|(_, names)| names.contains(&"default")));
1696 }
1697
1698 #[test]
1699 fn remix_has_used_exports_for_routes() {
1700 let plugin = remix::RemixPlugin;
1701 let exports = plugin.used_exports();
1702 assert!(!exports.is_empty());
1703 let route_entry = exports.iter().find(|(pat, _)| pat.contains("routes"));
1704 assert!(route_entry.is_some());
1705 let (_, names) = route_entry.unwrap();
1706 assert!(names.contains(&"loader"));
1707 assert!(names.contains(&"action"));
1708 assert!(names.contains(&"default"));
1709 }
1710
1711 #[test]
1712 fn sveltekit_has_used_exports_for_routes() {
1713 let plugin = sveltekit::SvelteKitPlugin;
1714 let exports = plugin.used_exports();
1715 assert!(!exports.is_empty());
1716 assert!(exports.iter().any(|(_, names)| names.contains(&"GET")));
1717 }
1718
1719 #[test]
1720 fn nuxt_has_hash_virtual_prefix() {
1721 assert!(nuxt::NuxtPlugin.virtual_module_prefixes().contains(&"#"));
1722 }
1723
1724 #[test]
1725 fn sveltekit_has_dollar_virtual_prefixes() {
1726 let prefixes = sveltekit::SvelteKitPlugin.virtual_module_prefixes();
1727 assert!(prefixes.contains(&"$app/"));
1728 assert!(prefixes.contains(&"$env/"));
1729 assert!(prefixes.contains(&"$lib/"));
1730 }
1731
1732 #[test]
1733 fn sveltekit_has_lib_path_alias() {
1734 let aliases = sveltekit::SvelteKitPlugin.path_aliases(Path::new("/project"));
1735 assert!(aliases.iter().any(|(prefix, _)| *prefix == "$lib/"));
1736 }
1737
1738 #[test]
1739 fn nuxt_has_tilde_path_alias() {
1740 let aliases = nuxt::NuxtPlugin.path_aliases(Path::new("/nonexistent"));
1741 assert!(aliases.iter().any(|(prefix, _)| *prefix == "~/"));
1742 assert!(aliases.iter().any(|(prefix, _)| *prefix == "~~/"));
1743 }
1744
1745 #[test]
1746 fn jest_has_package_json_config_key() {
1747 assert_eq!(jest::JestPlugin.package_json_config_key(), Some("jest"));
1748 }
1749
1750 #[test]
1751 fn tsd_has_package_json_config_key() {
1752 assert_eq!(tsd::TsdPlugin.package_json_config_key(), Some("tsd"));
1753 }
1754
1755 #[test]
1756 fn babel_has_package_json_config_key() {
1757 assert_eq!(babel::BabelPlugin.package_json_config_key(), Some("babel"));
1758 }
1759
1760 #[test]
1761 fn eslint_has_package_json_config_key() {
1762 assert_eq!(
1763 eslint::EslintPlugin.package_json_config_key(),
1764 Some("eslintConfig")
1765 );
1766 }
1767
1768 #[test]
1769 fn prettier_has_package_json_config_key() {
1770 assert_eq!(
1771 prettier::PrettierPlugin.package_json_config_key(),
1772 Some("prettier")
1773 );
1774 }
1775
1776 #[test]
1777 fn macro_generated_plugin_basic_properties() {
1778 let plugin = msw::MswPlugin;
1779 assert_eq!(plugin.name(), "msw");
1780 assert!(plugin.enablers().contains(&"msw"));
1781 assert!(!plugin.entry_patterns().is_empty());
1782 assert!(plugin.config_patterns().is_empty());
1783 assert!(!plugin.always_used().is_empty());
1784 assert!(!plugin.tooling_dependencies().is_empty());
1785 }
1786
1787 #[test]
1788 fn macro_generated_plugin_with_used_exports() {
1789 let plugin = remix::RemixPlugin;
1790 assert_eq!(plugin.name(), "remix");
1791 assert!(!plugin.used_exports().is_empty());
1792 }
1793
1794 #[test]
1795 fn macro_passes_through_virtual_package_suffixes() {
1796 define_plugin! {
1797 struct MacroSuffixSmokePlugin => "macro-suffix-smoke",
1798 enablers: &["macro-suffix-smoke"],
1799 virtual_package_suffixes: &["/__macro_smoke__"],
1800 }
1801
1802 let plugin = MacroSuffixSmokePlugin;
1803 assert_eq!(
1804 plugin.virtual_package_suffixes(),
1805 &["/__macro_smoke__"],
1806 "macro-declared virtual_package_suffixes must propagate to the trait method"
1807 );
1808 }
1809
1810 #[test]
1811 fn macro_generated_plugin_imports_only_resolve_config() {
1812 let plugin = cypress::CypressPlugin;
1813 let source = r"
1814 import { defineConfig } from 'cypress';
1815 import coveragePlugin from '@cypress/code-coverage';
1816 export default defineConfig({});
1817 ";
1818 let result = plugin.resolve_config(
1819 Path::new("cypress.config.ts"),
1820 source,
1821 Path::new("/project"),
1822 );
1823 assert!(
1824 result
1825 .referenced_dependencies
1826 .contains(&"cypress".to_string())
1827 );
1828 assert!(
1829 result
1830 .referenced_dependencies
1831 .contains(&"@cypress/code-coverage".to_string())
1832 );
1833 }
1834
1835 #[test]
1836 fn builtin_plugin_count_is_expected() {
1837 let plugins = registry::builtin::create_builtin_plugins();
1838 assert!(
1839 plugins.len() >= 80,
1840 "expected at least 80 built-in plugins, got {}",
1841 plugins.len()
1842 );
1843 }
1844}