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