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