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