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