1use std::path::{Path, PathBuf};
13
14use fallow_config::PackageJson;
15
16#[derive(Debug, Default)]
18pub struct PluginResult {
19 pub entry_patterns: Vec<String>,
21 pub referenced_dependencies: Vec<String>,
23 pub always_used_files: Vec<String>,
25 pub setup_files: Vec<PathBuf>,
27 pub fixture_patterns: Vec<String>,
29}
30
31impl PluginResult {
32 #[must_use]
33 pub const fn is_empty(&self) -> bool {
34 self.entry_patterns.is_empty()
35 && self.referenced_dependencies.is_empty()
36 && self.always_used_files.is_empty()
37 && self.setup_files.is_empty()
38 && self.fixture_patterns.is_empty()
39 }
40}
41
42pub trait Plugin: Send + Sync {
44 fn name(&self) -> &'static str;
46
47 fn enablers(&self) -> &'static [&'static str] {
50 &[]
51 }
52
53 fn is_enabled(&self, pkg: &PackageJson, root: &Path) -> bool {
56 let deps = pkg.all_dependency_names();
57 self.is_enabled_with_deps(&deps, root)
58 }
59
60 fn is_enabled_with_deps(&self, deps: &[String], _root: &Path) -> bool {
63 let enablers = self.enablers();
64 if enablers.is_empty() {
65 return false;
66 }
67 enablers.iter().any(|enabler| {
68 if enabler.ends_with('/') {
69 deps.iter().any(|d| d.starts_with(enabler))
71 } else {
72 deps.iter().any(|d| d == enabler)
73 }
74 })
75 }
76
77 fn entry_patterns(&self) -> &'static [&'static str] {
79 &[]
80 }
81
82 fn config_patterns(&self) -> &'static [&'static str] {
84 &[]
85 }
86
87 fn always_used(&self) -> &'static [&'static str] {
89 &[]
90 }
91
92 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
94 vec![]
95 }
96
97 fn fixture_glob_patterns(&self) -> &'static [&'static str] {
102 &[]
103 }
104
105 fn tooling_dependencies(&self) -> &'static [&'static str] {
108 &[]
109 }
110
111 fn virtual_module_prefixes(&self) -> &'static [&'static str] {
116 &[]
117 }
118
119 fn generated_import_patterns(&self) -> &'static [&'static str] {
125 &[]
126 }
127
128 fn path_aliases(&self, _root: &Path) -> Vec<(&'static str, String)> {
138 vec![]
139 }
140
141 fn resolve_config(&self, _config_path: &Path, _source: &str, _root: &Path) -> PluginResult {
146 PluginResult::default()
147 }
148
149 fn package_json_config_key(&self) -> Option<&'static str> {
154 None
155 }
156}
157
158macro_rules! define_plugin {
204 (
207 struct $name:ident => $display:expr,
208 enablers: $enablers:expr
209 $(, entry_patterns: $entry:expr)?
210 $(, config_patterns: $config:expr)?
211 $(, always_used: $always:expr)?
212 $(, tooling_dependencies: $tooling:expr)?
213 $(, fixture_glob_patterns: $fixtures:expr)?
214 $(, virtual_module_prefixes: $virtual:expr)?
215 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
216 , resolve_config: imports_only
217 $(,)?
218 ) => {
219 pub struct $name;
220
221 impl Plugin for $name {
222 fn name(&self) -> &'static str {
223 $display
224 }
225
226 fn enablers(&self) -> &'static [&'static str] {
227 $enablers
228 }
229
230 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
231 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
232 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
233 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
234 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
235 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
236
237 $(
238 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
239 vec![$( ($pat, $exports) ),*]
240 }
241 )?
242
243 fn resolve_config(
244 &self,
245 config_path: &std::path::Path,
246 source: &str,
247 _root: &std::path::Path,
248 ) -> PluginResult {
249 let mut result = PluginResult::default();
250 let imports = crate::plugins::config_parser::extract_imports(source, config_path);
251 for imp in &imports {
252 let dep = crate::resolve::extract_package_name(imp);
253 result.referenced_dependencies.push(dep);
254 }
255 result
256 }
257 }
258 };
259
260 (
262 struct $name:ident => $display:expr,
263 enablers: $enablers:expr
264 $(, entry_patterns: $entry:expr)?
265 $(, config_patterns: $config:expr)?
266 $(, always_used: $always:expr)?
267 $(, tooling_dependencies: $tooling:expr)?
268 $(, fixture_glob_patterns: $fixtures:expr)?
269 $(, virtual_module_prefixes: $virtual:expr)?
270 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
271 $(,)?
272 ) => {
273 pub struct $name;
274
275 impl Plugin for $name {
276 fn name(&self) -> &'static str {
277 $display
278 }
279
280 fn enablers(&self) -> &'static [&'static str] {
281 $enablers
282 }
283
284 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
285 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
286 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
287 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
288 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
289 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
290
291 $(
292 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
293 vec![$( ($pat, $exports) ),*]
294 }
295 )?
296 }
297 };
298}
299
300pub mod config_parser;
301pub mod registry;
302mod tooling;
303
304pub use registry::{AggregatedPluginResult, PluginRegistry};
305pub use tooling::is_known_tooling_dependency;
306
307mod angular;
308mod astro;
309mod ava;
310mod babel;
311mod biome;
312mod bun;
313mod c8;
314mod capacitor;
315mod changesets;
316mod commitizen;
317mod commitlint;
318mod cspell;
319mod cucumber;
320mod cypress;
321mod dependency_cruiser;
322mod docusaurus;
323mod drizzle;
324mod electron;
325mod eslint;
326mod expo;
327mod gatsby;
328mod graphql_codegen;
329mod husky;
330mod i18next;
331mod jest;
332mod karma;
333mod knex;
334mod kysely;
335mod lefthook;
336mod lint_staged;
337mod markdownlint;
338mod mocha;
339mod msw;
340mod nestjs;
341mod next_intl;
342mod nextjs;
343mod nitro;
344mod nodemon;
345mod nuxt;
346mod nx;
347mod nyc;
348mod openapi_ts;
349mod oxlint;
350mod parcel;
351mod playwright;
352mod plop;
353mod pm2;
354mod postcss;
355mod prettier;
356mod prisma;
357mod react_native;
358mod react_router;
359mod relay;
360mod remark;
361mod remix;
362mod rolldown;
363mod rollup;
364mod rsbuild;
365mod rspack;
366mod sanity;
367mod semantic_release;
368mod sentry;
369mod simple_git_hooks;
370mod storybook;
371mod stylelint;
372mod sveltekit;
373mod svgo;
374mod svgr;
375mod swc;
376mod syncpack;
377mod tailwind;
378mod tanstack_router;
379mod tsdown;
380mod tsup;
381mod turborepo;
382mod typedoc;
383mod typeorm;
384mod typescript;
385mod vite;
386mod vitepress;
387mod vitest;
388mod webdriverio;
389mod webpack;
390mod wrangler;
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395 use std::path::Path;
396
397 #[test]
400 fn is_enabled_with_deps_exact_match() {
401 let plugin = nextjs::NextJsPlugin;
402 let deps = vec!["next".to_string()];
403 assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
404 }
405
406 #[test]
407 fn is_enabled_with_deps_no_match() {
408 let plugin = nextjs::NextJsPlugin;
409 let deps = vec!["react".to_string()];
410 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
411 }
412
413 #[test]
414 fn is_enabled_with_deps_empty_deps() {
415 let plugin = nextjs::NextJsPlugin;
416 let deps: Vec<String> = vec![];
417 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
418 }
419
420 #[test]
423 fn plugin_result_is_empty_when_default() {
424 let r = PluginResult::default();
425 assert!(r.is_empty());
426 }
427
428 #[test]
429 fn plugin_result_not_empty_with_entry_patterns() {
430 let r = PluginResult {
431 entry_patterns: vec!["*.ts".to_string()],
432 ..Default::default()
433 };
434 assert!(!r.is_empty());
435 }
436
437 #[test]
438 fn plugin_result_not_empty_with_referenced_deps() {
439 let r = PluginResult {
440 referenced_dependencies: vec!["lodash".to_string()],
441 ..Default::default()
442 };
443 assert!(!r.is_empty());
444 }
445
446 #[test]
447 fn plugin_result_not_empty_with_setup_files() {
448 let r = PluginResult {
449 setup_files: vec![PathBuf::from("/setup.ts")],
450 ..Default::default()
451 };
452 assert!(!r.is_empty());
453 }
454
455 #[test]
456 fn plugin_result_not_empty_with_always_used_files() {
457 let r = PluginResult {
458 always_used_files: vec!["**/*.stories.tsx".to_string()],
459 ..Default::default()
460 };
461 assert!(!r.is_empty());
462 }
463
464 #[test]
465 fn plugin_result_not_empty_with_fixture_patterns() {
466 let r = PluginResult {
467 fixture_patterns: vec!["**/__fixtures__/**/*".to_string()],
468 ..Default::default()
469 };
470 assert!(!r.is_empty());
471 }
472
473 #[test]
476 fn is_enabled_with_deps_prefix_match() {
477 let plugin = storybook::StorybookPlugin;
479 let deps = vec!["@storybook/react".to_string()];
480 assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
481 }
482
483 #[test]
484 fn is_enabled_with_deps_prefix_no_match_without_slash() {
485 let plugin = storybook::StorybookPlugin;
487 let deps = vec!["@storybookish".to_string()];
488 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
489 }
490
491 #[test]
492 fn is_enabled_with_deps_multiple_enablers() {
493 let plugin = vitest::VitestPlugin;
495 let deps_vitest = vec!["vitest".to_string()];
496 let deps_none = vec!["mocha".to_string()];
497 assert!(plugin.is_enabled_with_deps(&deps_vitest, Path::new("/project")));
498 assert!(!plugin.is_enabled_with_deps(&deps_none, Path::new("/project")));
499 }
500
501 #[test]
504 fn plugin_default_methods_return_empty() {
505 let plugin = commitizen::CommitizenPlugin;
507 assert!(
508 plugin.tooling_dependencies().is_empty() || !plugin.tooling_dependencies().is_empty()
509 );
510 assert!(plugin.virtual_module_prefixes().is_empty());
511 assert!(plugin.path_aliases(Path::new("/project")).is_empty());
512 assert!(
513 plugin.package_json_config_key().is_none()
514 || plugin.package_json_config_key().is_some()
515 );
516 }
517
518 #[test]
519 fn plugin_resolve_config_default_returns_empty() {
520 let plugin = commitizen::CommitizenPlugin;
521 let result = plugin.resolve_config(
522 Path::new("/project/config.js"),
523 "const x = 1;",
524 Path::new("/project"),
525 );
526 assert!(result.is_empty());
527 }
528
529 #[test]
532 fn is_enabled_with_deps_exact_and_prefix_both_work() {
533 let plugin = storybook::StorybookPlugin;
534 let deps_exact = vec!["storybook".to_string()];
535 assert!(plugin.is_enabled_with_deps(&deps_exact, Path::new("/project")));
536 let deps_prefix = vec!["@storybook/vue3".to_string()];
537 assert!(plugin.is_enabled_with_deps(&deps_prefix, Path::new("/project")));
538 }
539
540 #[test]
541 fn is_enabled_with_deps_multiple_enablers_remix() {
542 let plugin = remix::RemixPlugin;
543 let deps_node = vec!["@remix-run/node".to_string()];
544 assert!(plugin.is_enabled_with_deps(&deps_node, Path::new("/project")));
545 let deps_react = vec!["@remix-run/react".to_string()];
546 assert!(plugin.is_enabled_with_deps(&deps_react, Path::new("/project")));
547 let deps_cf = vec!["@remix-run/cloudflare".to_string()];
548 assert!(plugin.is_enabled_with_deps(&deps_cf, Path::new("/project")));
549 }
550
551 struct MinimalPlugin;
554 impl Plugin for MinimalPlugin {
555 fn name(&self) -> &'static str {
556 "minimal"
557 }
558 }
559
560 #[test]
561 fn default_enablers_is_empty() {
562 assert!(MinimalPlugin.enablers().is_empty());
563 }
564
565 #[test]
566 fn default_entry_patterns_is_empty() {
567 assert!(MinimalPlugin.entry_patterns().is_empty());
568 }
569
570 #[test]
571 fn default_config_patterns_is_empty() {
572 assert!(MinimalPlugin.config_patterns().is_empty());
573 }
574
575 #[test]
576 fn default_always_used_is_empty() {
577 assert!(MinimalPlugin.always_used().is_empty());
578 }
579
580 #[test]
581 fn default_used_exports_is_empty() {
582 assert!(MinimalPlugin.used_exports().is_empty());
583 }
584
585 #[test]
586 fn default_tooling_dependencies_is_empty() {
587 assert!(MinimalPlugin.tooling_dependencies().is_empty());
588 }
589
590 #[test]
591 fn default_fixture_glob_patterns_is_empty() {
592 assert!(MinimalPlugin.fixture_glob_patterns().is_empty());
593 }
594
595 #[test]
596 fn default_virtual_module_prefixes_is_empty() {
597 assert!(MinimalPlugin.virtual_module_prefixes().is_empty());
598 }
599
600 #[test]
601 fn default_path_aliases_is_empty() {
602 assert!(MinimalPlugin.path_aliases(Path::new("/")).is_empty());
603 }
604
605 #[test]
606 fn default_resolve_config_returns_empty() {
607 let r = MinimalPlugin.resolve_config(
608 Path::new("config.js"),
609 "export default {}",
610 Path::new("/"),
611 );
612 assert!(r.is_empty());
613 }
614
615 #[test]
616 fn default_package_json_config_key_is_none() {
617 assert!(MinimalPlugin.package_json_config_key().is_none());
618 }
619
620 #[test]
621 fn default_is_enabled_returns_false_when_no_enablers() {
622 let deps = vec!["anything".to_string()];
623 assert!(!MinimalPlugin.is_enabled_with_deps(&deps, Path::new("/")));
624 }
625
626 #[test]
629 fn all_builtin_plugin_names_are_unique() {
630 let plugins = registry::builtin::create_builtin_plugins();
631 let mut seen = std::collections::BTreeSet::new();
632 for p in &plugins {
633 let name = p.name();
634 assert!(seen.insert(name), "duplicate plugin name: {name}");
635 }
636 }
637
638 #[test]
639 fn all_builtin_plugins_have_enablers() {
640 let plugins = registry::builtin::create_builtin_plugins();
641 for p in &plugins {
642 assert!(
643 !p.enablers().is_empty(),
644 "plugin '{}' has no enablers",
645 p.name()
646 );
647 }
648 }
649
650 #[test]
651 fn plugins_with_config_patterns_have_always_used() {
652 let plugins = registry::builtin::create_builtin_plugins();
653 for p in &plugins {
654 if !p.config_patterns().is_empty() {
655 assert!(
656 !p.always_used().is_empty(),
657 "plugin '{}' has config_patterns but no always_used",
658 p.name()
659 );
660 }
661 }
662 }
663
664 #[test]
667 fn framework_plugins_enablers() {
668 let cases: Vec<(&dyn Plugin, &[&str])> = vec![
669 (&nextjs::NextJsPlugin, &["next"]),
670 (&nuxt::NuxtPlugin, &["nuxt"]),
671 (&angular::AngularPlugin, &["@angular/core"]),
672 (&sveltekit::SvelteKitPlugin, &["@sveltejs/kit"]),
673 (&gatsby::GatsbyPlugin, &["gatsby"]),
674 ];
675 for (plugin, expected_enablers) in cases {
676 let enablers = plugin.enablers();
677 for expected in expected_enablers {
678 assert!(
679 enablers.contains(expected),
680 "plugin '{}' should have '{}'",
681 plugin.name(),
682 expected
683 );
684 }
685 }
686 }
687
688 #[test]
689 fn testing_plugins_enablers() {
690 let cases: Vec<(&dyn Plugin, &str)> = vec![
691 (&jest::JestPlugin, "jest"),
692 (&vitest::VitestPlugin, "vitest"),
693 (&playwright::PlaywrightPlugin, "@playwright/test"),
694 (&cypress::CypressPlugin, "cypress"),
695 (&mocha::MochaPlugin, "mocha"),
696 ];
697 for (plugin, enabler) in cases {
698 assert!(
699 plugin.enablers().contains(&enabler),
700 "plugin '{}' should have '{}'",
701 plugin.name(),
702 enabler
703 );
704 }
705 }
706
707 #[test]
708 fn bundler_plugins_enablers() {
709 let cases: Vec<(&dyn Plugin, &str)> = vec![
710 (&vite::VitePlugin, "vite"),
711 (&webpack::WebpackPlugin, "webpack"),
712 (&rollup::RollupPlugin, "rollup"),
713 ];
714 for (plugin, enabler) in cases {
715 assert!(
716 plugin.enablers().contains(&enabler),
717 "plugin '{}' should have '{}'",
718 plugin.name(),
719 enabler
720 );
721 }
722 }
723
724 #[test]
725 fn test_plugins_have_test_entry_patterns() {
726 let test_plugins: Vec<&dyn Plugin> = vec![
727 &jest::JestPlugin,
728 &vitest::VitestPlugin,
729 &mocha::MochaPlugin,
730 ];
731 for plugin in test_plugins {
732 let patterns = plugin.entry_patterns();
733 assert!(
734 !patterns.is_empty(),
735 "test plugin '{}' should have entry patterns",
736 plugin.name()
737 );
738 assert!(
739 patterns
740 .iter()
741 .any(|p| p.contains("test") || p.contains("spec") || p.contains("__tests__")),
742 "test plugin '{}' should have test/spec patterns",
743 plugin.name()
744 );
745 }
746 }
747
748 #[test]
749 fn framework_plugins_have_entry_patterns() {
750 let plugins: Vec<&dyn Plugin> = vec![
751 &nextjs::NextJsPlugin,
752 &nuxt::NuxtPlugin,
753 &angular::AngularPlugin,
754 &sveltekit::SvelteKitPlugin,
755 ];
756 for plugin in plugins {
757 assert!(
758 !plugin.entry_patterns().is_empty(),
759 "framework plugin '{}' should have entry patterns",
760 plugin.name()
761 );
762 }
763 }
764
765 #[test]
766 fn plugins_with_resolve_config_have_config_patterns() {
767 let plugins: Vec<&dyn Plugin> = vec![
768 &jest::JestPlugin,
769 &vitest::VitestPlugin,
770 &babel::BabelPlugin,
771 &eslint::EslintPlugin,
772 &webpack::WebpackPlugin,
773 &storybook::StorybookPlugin,
774 &typescript::TypeScriptPlugin,
775 &postcss::PostCssPlugin,
776 &nextjs::NextJsPlugin,
777 &nuxt::NuxtPlugin,
778 &angular::AngularPlugin,
779 &nx::NxPlugin,
780 &rollup::RollupPlugin,
781 &sveltekit::SvelteKitPlugin,
782 &prettier::PrettierPlugin,
783 ];
784 for plugin in plugins {
785 assert!(
786 !plugin.config_patterns().is_empty(),
787 "plugin '{}' with resolve_config should have config_patterns",
788 plugin.name()
789 );
790 }
791 }
792
793 #[test]
794 fn plugin_tooling_deps_include_enabler_package() {
795 let plugins: Vec<&dyn Plugin> = vec![
796 &jest::JestPlugin,
797 &vitest::VitestPlugin,
798 &webpack::WebpackPlugin,
799 &typescript::TypeScriptPlugin,
800 &eslint::EslintPlugin,
801 &prettier::PrettierPlugin,
802 ];
803 for plugin in plugins {
804 let tooling = plugin.tooling_dependencies();
805 let enablers = plugin.enablers();
806 assert!(
807 enablers
808 .iter()
809 .any(|e| !e.ends_with('/') && tooling.contains(e)),
810 "plugin '{}': at least one non-prefix enabler should be in tooling_dependencies",
811 plugin.name()
812 );
813 }
814 }
815
816 #[test]
817 fn nextjs_has_used_exports_for_pages() {
818 let plugin = nextjs::NextJsPlugin;
819 let exports = plugin.used_exports();
820 assert!(!exports.is_empty());
821 assert!(exports.iter().any(|(_, names)| names.contains(&"default")));
822 }
823
824 #[test]
825 fn remix_has_used_exports_for_routes() {
826 let plugin = remix::RemixPlugin;
827 let exports = plugin.used_exports();
828 assert!(!exports.is_empty());
829 let route_entry = exports.iter().find(|(pat, _)| pat.contains("routes"));
830 assert!(route_entry.is_some());
831 let (_, names) = route_entry.unwrap();
832 assert!(names.contains(&"loader"));
833 assert!(names.contains(&"action"));
834 assert!(names.contains(&"default"));
835 }
836
837 #[test]
838 fn sveltekit_has_used_exports_for_routes() {
839 let plugin = sveltekit::SvelteKitPlugin;
840 let exports = plugin.used_exports();
841 assert!(!exports.is_empty());
842 assert!(exports.iter().any(|(_, names)| names.contains(&"GET")));
843 }
844
845 #[test]
846 fn nuxt_has_hash_virtual_prefix() {
847 assert!(nuxt::NuxtPlugin.virtual_module_prefixes().contains(&"#"));
848 }
849
850 #[test]
851 fn sveltekit_has_dollar_virtual_prefixes() {
852 let prefixes = sveltekit::SvelteKitPlugin.virtual_module_prefixes();
853 assert!(prefixes.contains(&"$app/"));
854 assert!(prefixes.contains(&"$env/"));
855 assert!(prefixes.contains(&"$lib/"));
856 }
857
858 #[test]
859 fn sveltekit_has_lib_path_alias() {
860 let aliases = sveltekit::SvelteKitPlugin.path_aliases(Path::new("/project"));
861 assert!(aliases.iter().any(|(prefix, _)| *prefix == "$lib/"));
862 }
863
864 #[test]
865 fn nuxt_has_tilde_path_alias() {
866 let aliases = nuxt::NuxtPlugin.path_aliases(Path::new("/nonexistent"));
867 assert!(aliases.iter().any(|(prefix, _)| *prefix == "~/"));
868 assert!(aliases.iter().any(|(prefix, _)| *prefix == "~~/"));
869 }
870
871 #[test]
872 fn jest_has_package_json_config_key() {
873 assert_eq!(jest::JestPlugin.package_json_config_key(), Some("jest"));
874 }
875
876 #[test]
877 fn babel_has_package_json_config_key() {
878 assert_eq!(babel::BabelPlugin.package_json_config_key(), Some("babel"));
879 }
880
881 #[test]
882 fn eslint_has_package_json_config_key() {
883 assert_eq!(
884 eslint::EslintPlugin.package_json_config_key(),
885 Some("eslintConfig")
886 );
887 }
888
889 #[test]
890 fn prettier_has_package_json_config_key() {
891 assert_eq!(
892 prettier::PrettierPlugin.package_json_config_key(),
893 Some("prettier")
894 );
895 }
896
897 #[test]
898 fn macro_generated_plugin_basic_properties() {
899 let plugin = msw::MswPlugin;
900 assert_eq!(plugin.name(), "msw");
901 assert!(plugin.enablers().contains(&"msw"));
902 assert!(!plugin.entry_patterns().is_empty());
903 assert!(plugin.config_patterns().is_empty());
904 assert!(!plugin.always_used().is_empty());
905 assert!(!plugin.tooling_dependencies().is_empty());
906 }
907
908 #[test]
909 fn macro_generated_plugin_with_used_exports() {
910 let plugin = remix::RemixPlugin;
911 assert_eq!(plugin.name(), "remix");
912 assert!(!plugin.used_exports().is_empty());
913 }
914
915 #[test]
916 fn macro_generated_plugin_imports_only_resolve_config() {
917 let plugin = cypress::CypressPlugin;
918 let source = r"
919 import { defineConfig } from 'cypress';
920 import coveragePlugin from '@cypress/code-coverage';
921 export default defineConfig({});
922 ";
923 let result = plugin.resolve_config(
924 Path::new("cypress.config.ts"),
925 source,
926 Path::new("/project"),
927 );
928 assert!(
929 result
930 .referenced_dependencies
931 .contains(&"cypress".to_string())
932 );
933 assert!(
934 result
935 .referenced_dependencies
936 .contains(&"@cypress/code-coverage".to_string())
937 );
938 }
939
940 #[test]
941 fn builtin_plugin_count_is_expected() {
942 let plugins = registry::builtin::create_builtin_plugins();
943 assert!(
944 plugins.len() >= 80,
945 "expected at least 80 built-in plugins, got {}",
946 plugins.len()
947 );
948 }
949}