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