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