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