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