1use std::path::{Path, PathBuf};
13
14use fallow_config::{EntryPointRole, PackageJson};
15
16const TEST_ENTRY_POINT_PLUGINS: &[&str] = &[
17 "ava",
18 "cucumber",
19 "cypress",
20 "jest",
21 "mocha",
22 "playwright",
23 "vitest",
24 "webdriverio",
25];
26
27const RUNTIME_ENTRY_POINT_PLUGINS: &[&str] = &[
28 "angular",
29 "astro",
30 "docusaurus",
31 "electron",
32 "expo",
33 "gatsby",
34 "nestjs",
35 "next-intl",
36 "nextjs",
37 "nitro",
38 "nuxt",
39 "parcel",
40 "react-native",
41 "react-router",
42 "remix",
43 "rolldown",
44 "rollup",
45 "rsbuild",
46 "rspack",
47 "sanity",
48 "sveltekit",
49 "tanstack-router",
50 "tsdown",
51 "tsup",
52 "vite",
53 "vitepress",
54 "webpack",
55 "wrangler",
56];
57
58#[cfg(test)]
59const SUPPORT_ENTRY_POINT_PLUGINS: &[&str] = &[
60 "drizzle",
61 "i18next",
62 "knex",
63 "kysely",
64 "msw",
65 "prisma",
66 "storybook",
67 "typeorm",
68];
69
70#[derive(Debug, Default)]
72pub struct PluginResult {
73 pub entry_patterns: Vec<String>,
75 pub referenced_dependencies: Vec<String>,
77 pub always_used_files: Vec<String>,
79 pub setup_files: Vec<PathBuf>,
81 pub fixture_patterns: Vec<String>,
83}
84
85impl PluginResult {
86 #[must_use]
87 pub const fn is_empty(&self) -> bool {
88 self.entry_patterns.is_empty()
89 && self.referenced_dependencies.is_empty()
90 && self.always_used_files.is_empty()
91 && self.setup_files.is_empty()
92 && self.fixture_patterns.is_empty()
93 }
94}
95
96pub trait Plugin: Send + Sync {
98 fn name(&self) -> &'static str;
100
101 fn enablers(&self) -> &'static [&'static str] {
104 &[]
105 }
106
107 fn is_enabled(&self, pkg: &PackageJson, root: &Path) -> bool {
110 let deps = pkg.all_dependency_names();
111 self.is_enabled_with_deps(&deps, root)
112 }
113
114 fn is_enabled_with_deps(&self, deps: &[String], _root: &Path) -> bool {
117 let enablers = self.enablers();
118 if enablers.is_empty() {
119 return false;
120 }
121 enablers.iter().any(|enabler| {
122 if enabler.ends_with('/') {
123 deps.iter().any(|d| d.starts_with(enabler))
125 } else {
126 deps.iter().any(|d| d == enabler)
127 }
128 })
129 }
130
131 fn entry_patterns(&self) -> &'static [&'static str] {
133 &[]
134 }
135
136 fn entry_point_role(&self) -> EntryPointRole {
141 builtin_entry_point_role(self.name())
142 }
143
144 fn config_patterns(&self) -> &'static [&'static str] {
146 &[]
147 }
148
149 fn always_used(&self) -> &'static [&'static str] {
151 &[]
152 }
153
154 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
156 vec![]
157 }
158
159 fn fixture_glob_patterns(&self) -> &'static [&'static str] {
164 &[]
165 }
166
167 fn tooling_dependencies(&self) -> &'static [&'static str] {
170 &[]
171 }
172
173 fn virtual_module_prefixes(&self) -> &'static [&'static str] {
178 &[]
179 }
180
181 fn generated_import_patterns(&self) -> &'static [&'static str] {
187 &[]
188 }
189
190 fn path_aliases(&self, _root: &Path) -> Vec<(&'static str, String)> {
200 vec![]
201 }
202
203 fn resolve_config(&self, _config_path: &Path, _source: &str, _root: &Path) -> PluginResult {
208 PluginResult::default()
209 }
210
211 fn package_json_config_key(&self) -> Option<&'static str> {
216 None
217 }
218}
219
220fn builtin_entry_point_role(name: &str) -> EntryPointRole {
221 if TEST_ENTRY_POINT_PLUGINS.contains(&name) {
222 EntryPointRole::Test
223 } else if RUNTIME_ENTRY_POINT_PLUGINS.contains(&name) {
224 EntryPointRole::Runtime
225 } else {
226 EntryPointRole::Support
227 }
228}
229
230macro_rules! define_plugin {
276 (
279 struct $name:ident => $display:expr,
280 enablers: $enablers:expr
281 $(, entry_patterns: $entry:expr)?
282 $(, config_patterns: $config:expr)?
283 $(, always_used: $always:expr)?
284 $(, tooling_dependencies: $tooling:expr)?
285 $(, fixture_glob_patterns: $fixtures:expr)?
286 $(, virtual_module_prefixes: $virtual:expr)?
287 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
288 , resolve_config: imports_only
289 $(,)?
290 ) => {
291 pub struct $name;
292
293 impl Plugin for $name {
294 fn name(&self) -> &'static str {
295 $display
296 }
297
298 fn enablers(&self) -> &'static [&'static str] {
299 $enablers
300 }
301
302 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
303 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
304 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
305 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
306 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
307 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
308
309 $(
310 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
311 vec![$( ($pat, $exports) ),*]
312 }
313 )?
314
315 fn resolve_config(
316 &self,
317 config_path: &std::path::Path,
318 source: &str,
319 _root: &std::path::Path,
320 ) -> PluginResult {
321 let mut result = PluginResult::default();
322 let imports = crate::plugins::config_parser::extract_imports(source, config_path);
323 for imp in &imports {
324 let dep = crate::resolve::extract_package_name(imp);
325 result.referenced_dependencies.push(dep);
326 }
327 result
328 }
329 }
330 };
331
332 (
334 struct $name:ident => $display:expr,
335 enablers: $enablers:expr
336 $(, entry_patterns: $entry:expr)?
337 $(, config_patterns: $config:expr)?
338 $(, always_used: $always:expr)?
339 $(, tooling_dependencies: $tooling:expr)?
340 $(, fixture_glob_patterns: $fixtures:expr)?
341 $(, virtual_module_prefixes: $virtual:expr)?
342 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
343 $(,)?
344 ) => {
345 pub struct $name;
346
347 impl Plugin for $name {
348 fn name(&self) -> &'static str {
349 $display
350 }
351
352 fn enablers(&self) -> &'static [&'static str] {
353 $enablers
354 }
355
356 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
357 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
358 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
359 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
360 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
361 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
362
363 $(
364 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
365 vec![$( ($pat, $exports) ),*]
366 }
367 )?
368 }
369 };
370}
371
372pub mod config_parser;
373pub mod registry;
374mod tooling;
375
376pub use registry::{AggregatedPluginResult, PluginRegistry};
377pub use tooling::is_known_tooling_dependency;
378
379mod angular;
380mod astro;
381mod ava;
382mod babel;
383mod biome;
384mod bun;
385mod c8;
386mod capacitor;
387mod changesets;
388mod commitizen;
389mod commitlint;
390mod cspell;
391mod cucumber;
392mod cypress;
393mod dependency_cruiser;
394mod docusaurus;
395mod drizzle;
396mod electron;
397mod eslint;
398mod expo;
399mod gatsby;
400mod graphql_codegen;
401mod husky;
402mod i18next;
403mod jest;
404mod karma;
405mod knex;
406mod kysely;
407mod lefthook;
408mod lint_staged;
409mod markdownlint;
410mod mocha;
411mod msw;
412mod nestjs;
413mod next_intl;
414mod nextjs;
415mod nitro;
416mod nodemon;
417mod nuxt;
418mod nx;
419mod nyc;
420mod openapi_ts;
421mod oxlint;
422mod parcel;
423mod playwright;
424mod plop;
425mod pm2;
426mod postcss;
427mod prettier;
428mod prisma;
429mod react_native;
430mod react_router;
431mod relay;
432mod remark;
433mod remix;
434mod rolldown;
435mod rollup;
436mod rsbuild;
437mod rspack;
438mod sanity;
439mod semantic_release;
440mod sentry;
441mod simple_git_hooks;
442mod storybook;
443mod stylelint;
444mod sveltekit;
445mod svgo;
446mod svgr;
447mod swc;
448mod syncpack;
449mod tailwind;
450mod tanstack_router;
451mod tsdown;
452mod tsup;
453mod turborepo;
454mod typedoc;
455mod typeorm;
456mod typescript;
457mod vite;
458mod vitepress;
459mod vitest;
460mod webdriverio;
461mod webpack;
462mod wrangler;
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use std::path::Path;
468
469 #[test]
472 fn is_enabled_with_deps_exact_match() {
473 let plugin = nextjs::NextJsPlugin;
474 let deps = vec!["next".to_string()];
475 assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
476 }
477
478 #[test]
479 fn is_enabled_with_deps_no_match() {
480 let plugin = nextjs::NextJsPlugin;
481 let deps = vec!["react".to_string()];
482 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
483 }
484
485 #[test]
486 fn is_enabled_with_deps_empty_deps() {
487 let plugin = nextjs::NextJsPlugin;
488 let deps: Vec<String> = vec![];
489 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
490 }
491
492 #[test]
493 fn entry_point_role_defaults_are_centralized() {
494 assert_eq!(vite::VitePlugin.entry_point_role(), EntryPointRole::Runtime);
495 assert_eq!(
496 vitest::VitestPlugin.entry_point_role(),
497 EntryPointRole::Test
498 );
499 assert_eq!(
500 storybook::StorybookPlugin.entry_point_role(),
501 EntryPointRole::Support
502 );
503 assert_eq!(knex::KnexPlugin.entry_point_role(), EntryPointRole::Support);
504 }
505
506 #[test]
507 fn plugins_with_entry_patterns_have_explicit_role_intent() {
508 let runtime_or_test_or_support: rustc_hash::FxHashSet<&'static str> =
509 TEST_ENTRY_POINT_PLUGINS
510 .iter()
511 .chain(RUNTIME_ENTRY_POINT_PLUGINS.iter())
512 .chain(SUPPORT_ENTRY_POINT_PLUGINS.iter())
513 .copied()
514 .collect();
515
516 for plugin in crate::plugins::registry::builtin::create_builtin_plugins() {
517 if plugin.entry_patterns().is_empty() {
518 continue;
519 }
520 assert!(
521 runtime_or_test_or_support.contains(plugin.name()),
522 "plugin '{}' exposes entry patterns but is missing from the entry-point role map",
523 plugin.name()
524 );
525 }
526 }
527
528 #[test]
531 fn plugin_result_is_empty_when_default() {
532 let r = PluginResult::default();
533 assert!(r.is_empty());
534 }
535
536 #[test]
537 fn plugin_result_not_empty_with_entry_patterns() {
538 let r = PluginResult {
539 entry_patterns: vec!["*.ts".to_string()],
540 ..Default::default()
541 };
542 assert!(!r.is_empty());
543 }
544
545 #[test]
546 fn plugin_result_not_empty_with_referenced_deps() {
547 let r = PluginResult {
548 referenced_dependencies: vec!["lodash".to_string()],
549 ..Default::default()
550 };
551 assert!(!r.is_empty());
552 }
553
554 #[test]
555 fn plugin_result_not_empty_with_setup_files() {
556 let r = PluginResult {
557 setup_files: vec![PathBuf::from("/setup.ts")],
558 ..Default::default()
559 };
560 assert!(!r.is_empty());
561 }
562
563 #[test]
564 fn plugin_result_not_empty_with_always_used_files() {
565 let r = PluginResult {
566 always_used_files: vec!["**/*.stories.tsx".to_string()],
567 ..Default::default()
568 };
569 assert!(!r.is_empty());
570 }
571
572 #[test]
573 fn plugin_result_not_empty_with_fixture_patterns() {
574 let r = PluginResult {
575 fixture_patterns: vec!["**/__fixtures__/**/*".to_string()],
576 ..Default::default()
577 };
578 assert!(!r.is_empty());
579 }
580
581 #[test]
584 fn is_enabled_with_deps_prefix_match() {
585 let plugin = storybook::StorybookPlugin;
587 let deps = vec!["@storybook/react".to_string()];
588 assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
589 }
590
591 #[test]
592 fn is_enabled_with_deps_prefix_no_match_without_slash() {
593 let plugin = storybook::StorybookPlugin;
595 let deps = vec!["@storybookish".to_string()];
596 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
597 }
598
599 #[test]
600 fn is_enabled_with_deps_multiple_enablers() {
601 let plugin = vitest::VitestPlugin;
603 let deps_vitest = vec!["vitest".to_string()];
604 let deps_none = vec!["mocha".to_string()];
605 assert!(plugin.is_enabled_with_deps(&deps_vitest, Path::new("/project")));
606 assert!(!plugin.is_enabled_with_deps(&deps_none, Path::new("/project")));
607 }
608
609 #[test]
612 fn plugin_default_methods_return_empty() {
613 let plugin = commitizen::CommitizenPlugin;
615 assert!(
616 plugin.tooling_dependencies().is_empty() || !plugin.tooling_dependencies().is_empty()
617 );
618 assert!(plugin.virtual_module_prefixes().is_empty());
619 assert!(plugin.path_aliases(Path::new("/project")).is_empty());
620 assert!(
621 plugin.package_json_config_key().is_none()
622 || plugin.package_json_config_key().is_some()
623 );
624 }
625
626 #[test]
627 fn plugin_resolve_config_default_returns_empty() {
628 let plugin = commitizen::CommitizenPlugin;
629 let result = plugin.resolve_config(
630 Path::new("/project/config.js"),
631 "const x = 1;",
632 Path::new("/project"),
633 );
634 assert!(result.is_empty());
635 }
636
637 #[test]
640 fn is_enabled_with_deps_exact_and_prefix_both_work() {
641 let plugin = storybook::StorybookPlugin;
642 let deps_exact = vec!["storybook".to_string()];
643 assert!(plugin.is_enabled_with_deps(&deps_exact, Path::new("/project")));
644 let deps_prefix = vec!["@storybook/vue3".to_string()];
645 assert!(plugin.is_enabled_with_deps(&deps_prefix, Path::new("/project")));
646 }
647
648 #[test]
649 fn is_enabled_with_deps_multiple_enablers_remix() {
650 let plugin = remix::RemixPlugin;
651 let deps_node = vec!["@remix-run/node".to_string()];
652 assert!(plugin.is_enabled_with_deps(&deps_node, Path::new("/project")));
653 let deps_react = vec!["@remix-run/react".to_string()];
654 assert!(plugin.is_enabled_with_deps(&deps_react, Path::new("/project")));
655 let deps_cf = vec!["@remix-run/cloudflare".to_string()];
656 assert!(plugin.is_enabled_with_deps(&deps_cf, Path::new("/project")));
657 }
658
659 struct MinimalPlugin;
662 impl Plugin for MinimalPlugin {
663 fn name(&self) -> &'static str {
664 "minimal"
665 }
666 }
667
668 #[test]
669 fn default_enablers_is_empty() {
670 assert!(MinimalPlugin.enablers().is_empty());
671 }
672
673 #[test]
674 fn default_entry_patterns_is_empty() {
675 assert!(MinimalPlugin.entry_patterns().is_empty());
676 }
677
678 #[test]
679 fn default_config_patterns_is_empty() {
680 assert!(MinimalPlugin.config_patterns().is_empty());
681 }
682
683 #[test]
684 fn default_always_used_is_empty() {
685 assert!(MinimalPlugin.always_used().is_empty());
686 }
687
688 #[test]
689 fn default_used_exports_is_empty() {
690 assert!(MinimalPlugin.used_exports().is_empty());
691 }
692
693 #[test]
694 fn default_tooling_dependencies_is_empty() {
695 assert!(MinimalPlugin.tooling_dependencies().is_empty());
696 }
697
698 #[test]
699 fn default_fixture_glob_patterns_is_empty() {
700 assert!(MinimalPlugin.fixture_glob_patterns().is_empty());
701 }
702
703 #[test]
704 fn default_virtual_module_prefixes_is_empty() {
705 assert!(MinimalPlugin.virtual_module_prefixes().is_empty());
706 }
707
708 #[test]
709 fn default_path_aliases_is_empty() {
710 assert!(MinimalPlugin.path_aliases(Path::new("/")).is_empty());
711 }
712
713 #[test]
714 fn default_resolve_config_returns_empty() {
715 let r = MinimalPlugin.resolve_config(
716 Path::new("config.js"),
717 "export default {}",
718 Path::new("/"),
719 );
720 assert!(r.is_empty());
721 }
722
723 #[test]
724 fn default_package_json_config_key_is_none() {
725 assert!(MinimalPlugin.package_json_config_key().is_none());
726 }
727
728 #[test]
729 fn default_is_enabled_returns_false_when_no_enablers() {
730 let deps = vec!["anything".to_string()];
731 assert!(!MinimalPlugin.is_enabled_with_deps(&deps, Path::new("/")));
732 }
733
734 #[test]
737 fn all_builtin_plugin_names_are_unique() {
738 let plugins = registry::builtin::create_builtin_plugins();
739 let mut seen = std::collections::BTreeSet::new();
740 for p in &plugins {
741 let name = p.name();
742 assert!(seen.insert(name), "duplicate plugin name: {name}");
743 }
744 }
745
746 #[test]
747 fn all_builtin_plugins_have_enablers() {
748 let plugins = registry::builtin::create_builtin_plugins();
749 for p in &plugins {
750 assert!(
751 !p.enablers().is_empty(),
752 "plugin '{}' has no enablers",
753 p.name()
754 );
755 }
756 }
757
758 #[test]
759 fn plugins_with_config_patterns_have_always_used() {
760 let plugins = registry::builtin::create_builtin_plugins();
761 for p in &plugins {
762 if !p.config_patterns().is_empty() {
763 assert!(
764 !p.always_used().is_empty(),
765 "plugin '{}' has config_patterns but no always_used",
766 p.name()
767 );
768 }
769 }
770 }
771
772 #[test]
775 fn framework_plugins_enablers() {
776 let cases: Vec<(&dyn Plugin, &[&str])> = vec![
777 (&nextjs::NextJsPlugin, &["next"]),
778 (&nuxt::NuxtPlugin, &["nuxt"]),
779 (&angular::AngularPlugin, &["@angular/core"]),
780 (&sveltekit::SvelteKitPlugin, &["@sveltejs/kit"]),
781 (&gatsby::GatsbyPlugin, &["gatsby"]),
782 ];
783 for (plugin, expected_enablers) in cases {
784 let enablers = plugin.enablers();
785 for expected in expected_enablers {
786 assert!(
787 enablers.contains(expected),
788 "plugin '{}' should have '{}'",
789 plugin.name(),
790 expected
791 );
792 }
793 }
794 }
795
796 #[test]
797 fn testing_plugins_enablers() {
798 let cases: Vec<(&dyn Plugin, &str)> = vec![
799 (&jest::JestPlugin, "jest"),
800 (&vitest::VitestPlugin, "vitest"),
801 (&playwright::PlaywrightPlugin, "@playwright/test"),
802 (&cypress::CypressPlugin, "cypress"),
803 (&mocha::MochaPlugin, "mocha"),
804 ];
805 for (plugin, enabler) in cases {
806 assert!(
807 plugin.enablers().contains(&enabler),
808 "plugin '{}' should have '{}'",
809 plugin.name(),
810 enabler
811 );
812 }
813 }
814
815 #[test]
816 fn bundler_plugins_enablers() {
817 let cases: Vec<(&dyn Plugin, &str)> = vec![
818 (&vite::VitePlugin, "vite"),
819 (&webpack::WebpackPlugin, "webpack"),
820 (&rollup::RollupPlugin, "rollup"),
821 ];
822 for (plugin, enabler) in cases {
823 assert!(
824 plugin.enablers().contains(&enabler),
825 "plugin '{}' should have '{}'",
826 plugin.name(),
827 enabler
828 );
829 }
830 }
831
832 #[test]
833 fn test_plugins_have_test_entry_patterns() {
834 let test_plugins: Vec<&dyn Plugin> = vec![
835 &jest::JestPlugin,
836 &vitest::VitestPlugin,
837 &mocha::MochaPlugin,
838 ];
839 for plugin in test_plugins {
840 let patterns = plugin.entry_patterns();
841 assert!(
842 !patterns.is_empty(),
843 "test plugin '{}' should have entry patterns",
844 plugin.name()
845 );
846 assert!(
847 patterns
848 .iter()
849 .any(|p| p.contains("test") || p.contains("spec") || p.contains("__tests__")),
850 "test plugin '{}' should have test/spec patterns",
851 plugin.name()
852 );
853 }
854 }
855
856 #[test]
857 fn framework_plugins_have_entry_patterns() {
858 let plugins: Vec<&dyn Plugin> = vec![
859 &nextjs::NextJsPlugin,
860 &nuxt::NuxtPlugin,
861 &angular::AngularPlugin,
862 &sveltekit::SvelteKitPlugin,
863 ];
864 for plugin in plugins {
865 assert!(
866 !plugin.entry_patterns().is_empty(),
867 "framework plugin '{}' should have entry patterns",
868 plugin.name()
869 );
870 }
871 }
872
873 #[test]
874 fn plugins_with_resolve_config_have_config_patterns() {
875 let plugins: Vec<&dyn Plugin> = vec![
876 &jest::JestPlugin,
877 &vitest::VitestPlugin,
878 &babel::BabelPlugin,
879 &eslint::EslintPlugin,
880 &webpack::WebpackPlugin,
881 &storybook::StorybookPlugin,
882 &typescript::TypeScriptPlugin,
883 &postcss::PostCssPlugin,
884 &nextjs::NextJsPlugin,
885 &nuxt::NuxtPlugin,
886 &angular::AngularPlugin,
887 &nx::NxPlugin,
888 &rollup::RollupPlugin,
889 &sveltekit::SvelteKitPlugin,
890 &prettier::PrettierPlugin,
891 ];
892 for plugin in plugins {
893 assert!(
894 !plugin.config_patterns().is_empty(),
895 "plugin '{}' with resolve_config should have config_patterns",
896 plugin.name()
897 );
898 }
899 }
900
901 #[test]
902 fn plugin_tooling_deps_include_enabler_package() {
903 let plugins: Vec<&dyn Plugin> = vec![
904 &jest::JestPlugin,
905 &vitest::VitestPlugin,
906 &webpack::WebpackPlugin,
907 &typescript::TypeScriptPlugin,
908 &eslint::EslintPlugin,
909 &prettier::PrettierPlugin,
910 ];
911 for plugin in plugins {
912 let tooling = plugin.tooling_dependencies();
913 let enablers = plugin.enablers();
914 assert!(
915 enablers
916 .iter()
917 .any(|e| !e.ends_with('/') && tooling.contains(e)),
918 "plugin '{}': at least one non-prefix enabler should be in tooling_dependencies",
919 plugin.name()
920 );
921 }
922 }
923
924 #[test]
925 fn nextjs_has_used_exports_for_pages() {
926 let plugin = nextjs::NextJsPlugin;
927 let exports = plugin.used_exports();
928 assert!(!exports.is_empty());
929 assert!(exports.iter().any(|(_, names)| names.contains(&"default")));
930 }
931
932 #[test]
933 fn remix_has_used_exports_for_routes() {
934 let plugin = remix::RemixPlugin;
935 let exports = plugin.used_exports();
936 assert!(!exports.is_empty());
937 let route_entry = exports.iter().find(|(pat, _)| pat.contains("routes"));
938 assert!(route_entry.is_some());
939 let (_, names) = route_entry.unwrap();
940 assert!(names.contains(&"loader"));
941 assert!(names.contains(&"action"));
942 assert!(names.contains(&"default"));
943 }
944
945 #[test]
946 fn sveltekit_has_used_exports_for_routes() {
947 let plugin = sveltekit::SvelteKitPlugin;
948 let exports = plugin.used_exports();
949 assert!(!exports.is_empty());
950 assert!(exports.iter().any(|(_, names)| names.contains(&"GET")));
951 }
952
953 #[test]
954 fn nuxt_has_hash_virtual_prefix() {
955 assert!(nuxt::NuxtPlugin.virtual_module_prefixes().contains(&"#"));
956 }
957
958 #[test]
959 fn sveltekit_has_dollar_virtual_prefixes() {
960 let prefixes = sveltekit::SvelteKitPlugin.virtual_module_prefixes();
961 assert!(prefixes.contains(&"$app/"));
962 assert!(prefixes.contains(&"$env/"));
963 assert!(prefixes.contains(&"$lib/"));
964 }
965
966 #[test]
967 fn sveltekit_has_lib_path_alias() {
968 let aliases = sveltekit::SvelteKitPlugin.path_aliases(Path::new("/project"));
969 assert!(aliases.iter().any(|(prefix, _)| *prefix == "$lib/"));
970 }
971
972 #[test]
973 fn nuxt_has_tilde_path_alias() {
974 let aliases = nuxt::NuxtPlugin.path_aliases(Path::new("/nonexistent"));
975 assert!(aliases.iter().any(|(prefix, _)| *prefix == "~/"));
976 assert!(aliases.iter().any(|(prefix, _)| *prefix == "~~/"));
977 }
978
979 #[test]
980 fn jest_has_package_json_config_key() {
981 assert_eq!(jest::JestPlugin.package_json_config_key(), Some("jest"));
982 }
983
984 #[test]
985 fn babel_has_package_json_config_key() {
986 assert_eq!(babel::BabelPlugin.package_json_config_key(), Some("babel"));
987 }
988
989 #[test]
990 fn eslint_has_package_json_config_key() {
991 assert_eq!(
992 eslint::EslintPlugin.package_json_config_key(),
993 Some("eslintConfig")
994 );
995 }
996
997 #[test]
998 fn prettier_has_package_json_config_key() {
999 assert_eq!(
1000 prettier::PrettierPlugin.package_json_config_key(),
1001 Some("prettier")
1002 );
1003 }
1004
1005 #[test]
1006 fn macro_generated_plugin_basic_properties() {
1007 let plugin = msw::MswPlugin;
1008 assert_eq!(plugin.name(), "msw");
1009 assert!(plugin.enablers().contains(&"msw"));
1010 assert!(!plugin.entry_patterns().is_empty());
1011 assert!(plugin.config_patterns().is_empty());
1012 assert!(!plugin.always_used().is_empty());
1013 assert!(!plugin.tooling_dependencies().is_empty());
1014 }
1015
1016 #[test]
1017 fn macro_generated_plugin_with_used_exports() {
1018 let plugin = remix::RemixPlugin;
1019 assert_eq!(plugin.name(), "remix");
1020 assert!(!plugin.used_exports().is_empty());
1021 }
1022
1023 #[test]
1024 fn macro_generated_plugin_imports_only_resolve_config() {
1025 let plugin = cypress::CypressPlugin;
1026 let source = r"
1027 import { defineConfig } from 'cypress';
1028 import coveragePlugin from '@cypress/code-coverage';
1029 export default defineConfig({});
1030 ";
1031 let result = plugin.resolve_config(
1032 Path::new("cypress.config.ts"),
1033 source,
1034 Path::new("/project"),
1035 );
1036 assert!(
1037 result
1038 .referenced_dependencies
1039 .contains(&"cypress".to_string())
1040 );
1041 assert!(
1042 result
1043 .referenced_dependencies
1044 .contains(&"@cypress/code-coverage".to_string())
1045 );
1046 }
1047
1048 #[test]
1049 fn builtin_plugin_count_is_expected() {
1050 let plugins = registry::builtin::create_builtin_plugins();
1051 assert!(
1052 plugins.len() >= 80,
1053 "expected at least 80 built-in plugins, got {}",
1054 plugins.len()
1055 );
1056 }
1057}