1use std::path::{Path, PathBuf};
13
14use fallow_config::{EntryPointRole, PackageJson, UsedClassMemberRule};
15use regex::Regex;
16
17const TEST_ENTRY_POINT_PLUGINS: &[&str] = &[
18 "ava",
19 "cucumber",
20 "cypress",
21 "jest",
22 "k6",
23 "mocha",
24 "playwright",
25 "tap",
26 "tsd",
27 "vitest",
28 "webdriverio",
29];
30
31const RUNTIME_ENTRY_POINT_PLUGINS: &[&str] = &[
32 "adonis",
33 "angular",
34 "astro",
35 "convex",
36 "docusaurus",
37 "electron",
38 "ember",
39 "expo",
40 "expo-router",
41 "gatsby",
42 "hardhat",
43 "nestjs",
44 "next-intl",
45 "nextjs",
46 "nitro",
47 "nuxt",
48 "parcel",
49 "qwik",
50 "react-native",
51 "react-router",
52 "redwoodsdk",
53 "remix",
54 "rolldown",
55 "rollup",
56 "rsbuild",
57 "rspack",
58 "sanity",
59 "supabase",
60 "sveltekit",
61 "tanstack-router",
62 "tsdown",
63 "tsup",
64 "vite",
65 "vitepress",
66 "webpack",
67 "wrangler",
68];
69
70#[cfg(test)]
71const SUPPORT_ENTRY_POINT_PLUGINS: &[&str] = &[
72 "content-collections",
73 "danger",
74 "drizzle",
75 "fumadocs",
76 "i18next",
77 "knex",
78 "kysely",
79 "mintlify",
80 "msw",
81 "opencode",
82 "prisma",
83 "storybook",
84 "stryker",
85 "typeorm",
86];
87
88#[derive(Debug, Default)]
90pub struct PluginResult {
91 pub entry_patterns: Vec<PathRule>,
93 pub replace_entry_patterns: bool,
98 pub replace_used_export_rules: bool,
101 pub used_exports: Vec<UsedExportRule>,
103 pub used_class_members: Vec<UsedClassMemberRule>,
108 pub referenced_dependencies: Vec<String>,
110 pub always_used_files: Vec<String>,
112 pub path_aliases: Vec<(String, String)>,
114 pub setup_files: Vec<PathBuf>,
116 pub fixture_patterns: Vec<String>,
118 pub scss_include_paths: Vec<PathBuf>,
126}
127
128impl PluginResult {
129 pub fn push_entry_pattern(&mut self, pattern: impl Into<String>) {
130 self.entry_patterns
131 .push(PathRule::new(normalize_entry_pattern(pattern.into())));
132 }
133
134 pub fn extend_entry_patterns<I, S>(&mut self, patterns: I)
135 where
136 I: IntoIterator<Item = S>,
137 S: Into<String>,
138 {
139 self.entry_patterns.extend(
140 patterns
141 .into_iter()
142 .map(|pat| PathRule::new(normalize_entry_pattern(pat.into()))),
143 );
144 }
145
146 pub fn push_used_export_rule(
147 &mut self,
148 pattern: impl Into<String>,
149 exports: impl IntoIterator<Item = impl Into<String>>,
150 ) {
151 self.used_exports
152 .push(UsedExportRule::new(pattern, exports));
153 }
154
155 #[must_use]
156 pub const fn is_empty(&self) -> bool {
157 self.entry_patterns.is_empty()
158 && self.used_exports.is_empty()
159 && self.used_class_members.is_empty()
160 && self.referenced_dependencies.is_empty()
161 && self.always_used_files.is_empty()
162 && self.path_aliases.is_empty()
163 && self.setup_files.is_empty()
164 && self.fixture_patterns.is_empty()
165 && self.scss_include_paths.is_empty()
166 }
167}
168
169fn normalize_entry_pattern(pattern: String) -> String {
176 pattern
177 .strip_prefix("./")
178 .map(str::to_owned)
179 .unwrap_or(pattern)
180}
181
182#[derive(Debug, Clone, Default, PartialEq, Eq)]
188pub struct PathRule {
189 pub pattern: String,
190 pub exclude_globs: Vec<String>,
191 pub exclude_regexes: Vec<String>,
192 pub exclude_segment_regexes: Vec<String>,
196}
197
198impl PathRule {
199 #[must_use]
200 pub fn new(pattern: impl Into<String>) -> Self {
201 Self {
202 pattern: pattern.into(),
203 exclude_globs: Vec::new(),
204 exclude_regexes: Vec::new(),
205 exclude_segment_regexes: Vec::new(),
206 }
207 }
208
209 #[must_use]
210 pub fn from_static(pattern: &'static str) -> Self {
211 Self::new(pattern)
212 }
213
214 #[must_use]
215 pub fn with_excluded_globs<I, S>(mut self, patterns: I) -> Self
216 where
217 I: IntoIterator<Item = S>,
218 S: Into<String>,
219 {
220 self.exclude_globs
221 .extend(patterns.into_iter().map(Into::into));
222 self
223 }
224
225 #[must_use]
226 pub fn with_excluded_regexes<I, S>(mut self, patterns: I) -> Self
227 where
228 I: IntoIterator<Item = S>,
229 S: Into<String>,
230 {
231 self.exclude_regexes
232 .extend(patterns.into_iter().map(Into::into));
233 self
234 }
235
236 #[must_use]
237 pub fn with_excluded_segment_regexes<I, S>(mut self, patterns: I) -> Self
238 where
239 I: IntoIterator<Item = S>,
240 S: Into<String>,
241 {
242 self.exclude_segment_regexes
243 .extend(patterns.into_iter().map(Into::into));
244 self
245 }
246
247 #[must_use]
248 pub fn prefixed(&self, ws_prefix: &str) -> Self {
249 Self {
250 pattern: prefix_workspace_pattern(&self.pattern, ws_prefix),
251 exclude_globs: self
252 .exclude_globs
253 .iter()
254 .map(|pattern| prefix_workspace_pattern(pattern, ws_prefix))
255 .collect(),
256 exclude_regexes: self
257 .exclude_regexes
258 .iter()
259 .map(|pattern| prefix_workspace_regex(pattern, ws_prefix))
260 .collect(),
261 exclude_segment_regexes: self.exclude_segment_regexes.clone(),
262 }
263 }
264}
265
266#[derive(Debug, Clone, Default, PartialEq, Eq)]
268pub struct UsedExportRule {
269 pub path: PathRule,
270 pub exports: Vec<String>,
271}
272
273impl UsedExportRule {
274 #[must_use]
275 pub fn new(
276 pattern: impl Into<String>,
277 exports: impl IntoIterator<Item = impl Into<String>>,
278 ) -> Self {
279 Self {
280 path: PathRule::new(pattern),
281 exports: exports.into_iter().map(Into::into).collect(),
282 }
283 }
284
285 #[must_use]
286 pub fn from_static(pattern: &'static str, exports: &'static [&'static str]) -> Self {
287 Self::new(pattern, exports.iter().copied())
288 }
289
290 #[must_use]
291 pub fn with_excluded_globs<I, S>(mut self, patterns: I) -> Self
292 where
293 I: IntoIterator<Item = S>,
294 S: Into<String>,
295 {
296 self.path = self.path.with_excluded_globs(patterns);
297 self
298 }
299
300 #[must_use]
301 pub fn with_excluded_regexes<I, S>(mut self, patterns: I) -> Self
302 where
303 I: IntoIterator<Item = S>,
304 S: Into<String>,
305 {
306 self.path = self.path.with_excluded_regexes(patterns);
307 self
308 }
309
310 #[must_use]
311 pub fn with_excluded_segment_regexes<I, S>(mut self, patterns: I) -> Self
312 where
313 I: IntoIterator<Item = S>,
314 S: Into<String>,
315 {
316 self.path = self.path.with_excluded_segment_regexes(patterns);
317 self
318 }
319
320 #[must_use]
321 pub fn prefixed(&self, ws_prefix: &str) -> Self {
322 Self {
323 path: self.path.prefixed(ws_prefix),
324 exports: self.exports.clone(),
325 }
326 }
327}
328
329#[derive(Debug, Clone, PartialEq, Eq)]
331pub struct PluginUsedExportRule {
332 pub plugin_name: String,
333 pub rule: UsedExportRule,
334}
335
336impl PluginUsedExportRule {
337 #[must_use]
338 pub fn new(plugin_name: impl Into<String>, rule: UsedExportRule) -> Self {
339 Self {
340 plugin_name: plugin_name.into(),
341 rule,
342 }
343 }
344
345 #[must_use]
346 pub fn prefixed(&self, ws_prefix: &str) -> Self {
347 Self {
348 plugin_name: self.plugin_name.clone(),
349 rule: self.rule.prefixed(ws_prefix),
350 }
351 }
352}
353
354#[derive(Debug, Clone)]
356pub(crate) struct CompiledPathRule {
357 include: globset::GlobMatcher,
358 exclude_globs: Vec<globset::GlobMatcher>,
359 exclude_regexes: Vec<Regex>,
360 exclude_segment_regexes: Vec<Regex>,
361}
362
363impl CompiledPathRule {
364 pub(crate) fn for_entry_rule(rule: &PathRule, rule_kind: &str) -> Option<Self> {
365 let include = match globset::GlobBuilder::new(&rule.pattern)
366 .literal_separator(true)
367 .build()
368 {
369 Ok(glob) => glob.compile_matcher(),
370 Err(err) => {
371 tracing::warn!("invalid {rule_kind} '{}': {err}", rule.pattern);
372 return None;
373 }
374 };
375 Some(Self {
376 include,
377 exclude_globs: compile_excluded_globs(&rule.exclude_globs, rule_kind, &rule.pattern),
378 exclude_regexes: compile_excluded_regexes(
379 &rule.exclude_regexes,
380 rule_kind,
381 &rule.pattern,
382 ),
383 exclude_segment_regexes: compile_excluded_segment_regexes(
384 &rule.exclude_segment_regexes,
385 rule_kind,
386 &rule.pattern,
387 ),
388 })
389 }
390
391 pub(crate) fn for_used_export_rule(rule: &PathRule, rule_kind: &str) -> Option<Self> {
392 let include = match globset::Glob::new(&rule.pattern) {
393 Ok(glob) => glob.compile_matcher(),
394 Err(err) => {
395 tracing::warn!("invalid {rule_kind} '{}': {err}", rule.pattern);
396 return None;
397 }
398 };
399 Some(Self {
400 include,
401 exclude_globs: compile_excluded_globs(&rule.exclude_globs, rule_kind, &rule.pattern),
402 exclude_regexes: compile_excluded_regexes(
403 &rule.exclude_regexes,
404 rule_kind,
405 &rule.pattern,
406 ),
407 exclude_segment_regexes: compile_excluded_segment_regexes(
408 &rule.exclude_segment_regexes,
409 rule_kind,
410 &rule.pattern,
411 ),
412 })
413 }
414
415 #[must_use]
416 pub(crate) fn matches(&self, path: &str) -> bool {
417 self.include.is_match(path)
418 && !self.exclude_globs.iter().any(|glob| glob.is_match(path))
419 && !self
420 .exclude_regexes
421 .iter()
422 .any(|regex| regex.is_match(path))
423 && !matches_segment_regex(path, &self.exclude_segment_regexes)
424 }
425}
426
427fn prefix_workspace_pattern(pattern: &str, ws_prefix: &str) -> String {
428 if pattern.starts_with(ws_prefix) || pattern.starts_with('/') {
429 pattern.to_string()
430 } else {
431 format!("{ws_prefix}/{pattern}")
432 }
433}
434
435fn prefix_workspace_regex(pattern: &str, ws_prefix: &str) -> String {
436 if let Some(pattern) = pattern.strip_prefix('^') {
437 format!("^{}/{}", regex::escape(ws_prefix), pattern)
438 } else {
439 format!("^{}/(?:{})", regex::escape(ws_prefix), pattern)
440 }
441}
442
443fn compile_excluded_globs(
444 patterns: &[String],
445 rule_kind: &str,
446 rule_pattern: &str,
447) -> Vec<globset::GlobMatcher> {
448 patterns
449 .iter()
450 .filter_map(|pattern| {
451 match globset::GlobBuilder::new(pattern)
452 .literal_separator(true)
453 .build()
454 {
455 Ok(glob) => Some(glob.compile_matcher()),
456 Err(err) => {
457 tracing::warn!(
458 "skipping invalid excluded glob '{}' for {} '{}': {err}",
459 pattern,
460 rule_kind,
461 rule_pattern
462 );
463 None
464 }
465 }
466 })
467 .collect()
468}
469
470fn compile_excluded_regexes(
471 patterns: &[String],
472 rule_kind: &str,
473 rule_pattern: &str,
474) -> Vec<Regex> {
475 patterns
476 .iter()
477 .filter_map(|pattern| match Regex::new(pattern) {
478 Ok(regex) => Some(regex),
479 Err(err) => {
480 tracing::warn!(
481 "skipping invalid excluded regex '{}' for {} '{}': {err}",
482 pattern,
483 rule_kind,
484 rule_pattern
485 );
486 None
487 }
488 })
489 .collect()
490}
491
492fn compile_excluded_segment_regexes(
493 patterns: &[String],
494 rule_kind: &str,
495 rule_pattern: &str,
496) -> Vec<Regex> {
497 patterns
498 .iter()
499 .filter_map(|pattern| match Regex::new(pattern) {
500 Ok(regex) => Some(regex),
501 Err(err) => {
502 tracing::warn!(
503 "skipping invalid excluded segment regex '{}' for {} '{}': {err}",
504 pattern,
505 rule_kind,
506 rule_pattern
507 );
508 None
509 }
510 })
511 .collect()
512}
513
514fn matches_segment_regex(path: &str, regexes: &[Regex]) -> bool {
515 path.split('/')
516 .any(|segment| regexes.iter().any(|regex| regex.is_match(segment)))
517}
518
519impl From<String> for PathRule {
520 fn from(pattern: String) -> Self {
521 Self::new(pattern)
522 }
523}
524
525impl From<&str> for PathRule {
526 fn from(pattern: &str) -> Self {
527 Self::new(pattern)
528 }
529}
530
531impl std::ops::Deref for PathRule {
532 type Target = str;
533
534 fn deref(&self) -> &Self::Target {
535 &self.pattern
536 }
537}
538
539impl PartialEq<&str> for PathRule {
540 fn eq(&self, other: &&str) -> bool {
541 self.pattern == *other
542 }
543}
544
545impl PartialEq<str> for PathRule {
546 fn eq(&self, other: &str) -> bool {
547 self.pattern == other
548 }
549}
550
551impl PartialEq<String> for PathRule {
552 fn eq(&self, other: &String) -> bool {
553 &self.pattern == other
554 }
555}
556
557pub trait Plugin: Send + Sync {
559 fn name(&self) -> &'static str;
561
562 fn enablers(&self) -> &'static [&'static str] {
565 &[]
566 }
567
568 fn is_enabled(&self, pkg: &PackageJson, root: &Path) -> bool {
571 let deps = pkg.all_dependency_names();
572 self.is_enabled_with_deps(&deps, root)
573 }
574
575 fn is_enabled_with_deps(&self, deps: &[String], _root: &Path) -> bool {
578 let enablers = self.enablers();
579 if enablers.is_empty() {
580 return false;
581 }
582 enablers.iter().any(|enabler| {
583 if enabler.ends_with('/') {
584 deps.iter().any(|d| d.starts_with(enabler))
586 } else {
587 deps.iter().any(|d| d == enabler)
588 }
589 })
590 }
591
592 fn is_enabled_with_files(
598 &self,
599 deps: &[String],
600 root: &Path,
601 _discovered_files: &[PathBuf],
602 ) -> bool {
603 self.is_enabled_with_deps(deps, root)
604 }
605
606 fn entry_patterns(&self) -> &'static [&'static str] {
608 &[]
609 }
610
611 fn entry_pattern_rules(&self) -> Vec<PathRule> {
613 self.entry_patterns()
614 .iter()
615 .map(|pattern| PathRule::from_static(pattern))
616 .collect()
617 }
618
619 fn entry_point_role(&self) -> EntryPointRole {
624 builtin_entry_point_role(self.name())
625 }
626
627 fn config_patterns(&self) -> &'static [&'static str] {
629 &[]
630 }
631
632 fn always_used(&self) -> &'static [&'static str] {
634 &[]
635 }
636
637 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
639 vec![]
640 }
641
642 fn used_export_rules(&self) -> Vec<UsedExportRule> {
644 self.used_exports()
645 .into_iter()
646 .map(|(pattern, exports)| UsedExportRule::from_static(pattern, exports))
647 .collect()
648 }
649
650 fn used_class_members(&self) -> &'static [&'static str] {
655 &[]
656 }
657
658 fn used_class_member_rules(&self) -> Vec<UsedClassMemberRule> {
666 Vec::new()
667 }
668
669 fn fixture_glob_patterns(&self) -> &'static [&'static str] {
674 &[]
675 }
676
677 fn discovery_hidden_dirs(&self) -> &'static [&'static str] {
682 &[]
683 }
684
685 fn tooling_dependencies(&self) -> &'static [&'static str] {
688 &[]
689 }
690
691 fn virtual_module_prefixes(&self) -> &'static [&'static str] {
696 &[]
697 }
698
699 fn virtual_package_suffixes(&self) -> &'static [&'static str] {
705 &[]
706 }
707
708 fn generated_import_patterns(&self) -> &'static [&'static str] {
714 &[]
715 }
716
717 fn generated_type_import_prefixes(&self) -> &'static [&'static str] {
722 &[]
723 }
724
725 fn path_aliases(&self, _root: &Path) -> Vec<(&'static str, String)> {
735 vec![]
736 }
737
738 fn resolve_config(&self, _config_path: &Path, _source: &str, _root: &Path) -> PluginResult {
743 PluginResult::default()
744 }
745
746 fn package_json_config_key(&self) -> Option<&'static str> {
751 None
752 }
753}
754
755fn builtin_entry_point_role(name: &str) -> EntryPointRole {
756 if TEST_ENTRY_POINT_PLUGINS.contains(&name) {
757 EntryPointRole::Test
758 } else if RUNTIME_ENTRY_POINT_PLUGINS.contains(&name) {
759 EntryPointRole::Runtime
760 } else {
761 EntryPointRole::Support
762 }
763}
764
765macro_rules! define_plugin {
826 (
829 struct $name:ident => $display:expr,
830 enablers: $enablers:expr
831 $(, entry_patterns: $entry:expr)?
832 $(, config_patterns: $config:expr)?
833 $(, always_used: $always:expr)?
834 $(, tooling_dependencies: $tooling:expr)?
835 $(, fixture_glob_patterns: $fixtures:expr)?
836 $(, discovery_hidden_dirs: $hidden_dirs:expr)?
837 $(, virtual_module_prefixes: $virtual:expr)?
838 $(, virtual_package_suffixes: $virtual_suffixes:expr)?
839 $(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
840 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
841 , resolve_config: imports_only
842 $(,)?
843 ) => {
844 pub struct $name;
845
846 impl Plugin for $name {
847 fn name(&self) -> &'static str {
848 $display
849 }
850
851 fn enablers(&self) -> &'static [&'static str] {
852 $enablers
853 }
854
855 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
856 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
857 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
858 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
859 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
860 $( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
861 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
862 $( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
863 $( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
864
865 $(
866 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
867 vec![$( ($pat, $exports) ),*]
868 }
869 )?
870
871 fn resolve_config(
872 &self,
873 config_path: &std::path::Path,
874 source: &str,
875 _root: &std::path::Path,
876 ) -> PluginResult {
877 let mut result = PluginResult::default();
878 let imports = crate::plugins::config_parser::extract_imports(source, config_path);
879 for imp in &imports {
880 let dep = crate::resolve::extract_package_name(imp);
881 result.referenced_dependencies.push(dep);
882 }
883 result
884 }
885 }
886 };
887
888 (
892 struct $name:ident => $display:expr,
893 enablers: $enablers:expr
894 $(, entry_patterns: $entry:expr)?
895 $(, config_patterns: $config:expr)?
896 $(, always_used: $always:expr)?
897 $(, tooling_dependencies: $tooling:expr)?
898 $(, fixture_glob_patterns: $fixtures:expr)?
899 $(, discovery_hidden_dirs: $hidden_dirs:expr)?
900 $(, virtual_module_prefixes: $virtual:expr)?
901 $(, virtual_package_suffixes: $virtual_suffixes:expr)?
902 $(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
903 $(, package_json_config_key: $pkg_key:expr)?
904 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
905 , resolve_config($cp:ident, $src:ident, $root:ident) $body:block
906 $(,)?
907 ) => {
908 pub struct $name;
909
910 impl Plugin for $name {
911 fn name(&self) -> &'static str {
912 $display
913 }
914
915 fn enablers(&self) -> &'static [&'static str] {
916 $enablers
917 }
918
919 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
920 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
921 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
922 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
923 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
924 $( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
925 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
926 $( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
927 $( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
928
929 $(
930 fn package_json_config_key(&self) -> Option<&'static str> {
931 Some($pkg_key)
932 }
933 )?
934
935 $(
936 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
937 vec![$( ($pat, $exports) ),*]
938 }
939 )?
940
941 fn resolve_config(
942 &self,
943 $cp: &std::path::Path,
944 $src: &str,
945 $root: &std::path::Path,
946 ) -> PluginResult
947 $body
948 }
949 };
950
951 (
953 struct $name:ident => $display:expr,
954 enablers: $enablers:expr
955 $(, entry_patterns: $entry:expr)?
956 $(, config_patterns: $config:expr)?
957 $(, always_used: $always:expr)?
958 $(, tooling_dependencies: $tooling:expr)?
959 $(, fixture_glob_patterns: $fixtures:expr)?
960 $(, discovery_hidden_dirs: $hidden_dirs:expr)?
961 $(, virtual_module_prefixes: $virtual:expr)?
962 $(, virtual_package_suffixes: $virtual_suffixes:expr)?
963 $(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
964 $(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
965 $(,)?
966 ) => {
967 pub struct $name;
968
969 impl Plugin for $name {
970 fn name(&self) -> &'static str {
971 $display
972 }
973
974 fn enablers(&self) -> &'static [&'static str] {
975 $enablers
976 }
977
978 $( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
979 $( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
980 $( fn always_used(&self) -> &'static [&'static str] { $always } )?
981 $( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
982 $( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
983 $( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
984 $( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
985 $( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
986 $( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
987
988 $(
989 fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
990 vec![$( ($pat, $exports) ),*]
991 }
992 )?
993 }
994 };
995}
996
997pub mod config_parser;
998pub mod registry;
999mod tooling;
1000
1001pub use registry::{AggregatedPluginResult, PluginRegistry};
1002pub use tooling::is_known_tooling_dependency;
1003
1004mod adonis;
1005mod angular;
1006mod astro;
1007mod ava;
1008mod babel;
1009mod biome;
1010mod bun;
1011mod c8;
1012mod capacitor;
1013mod changesets;
1014mod commitizen;
1015mod commitlint;
1016mod content_collections;
1017mod convex;
1018mod cspell;
1019mod cucumber;
1020mod cypress;
1021mod danger;
1022mod dependency_cruiser;
1023mod docusaurus;
1024mod drizzle;
1025mod electron;
1026mod ember;
1027mod eslint;
1028mod expo;
1029mod expo_router;
1030mod fumadocs;
1031mod gatsby;
1032mod graphql_codegen;
1033mod hardhat;
1034mod husky;
1035mod i18next;
1036mod jest;
1037mod k6;
1038mod karma;
1039mod knex;
1040mod kysely;
1041mod lefthook;
1042mod lexical;
1043mod lint_staged;
1044mod lit;
1045mod markdownlint;
1046mod mintlify;
1047mod mocha;
1048mod msw;
1049mod nestjs;
1050mod next_intl;
1051mod nextjs;
1052mod nitro;
1053mod nodemon;
1054mod nuxt;
1055mod nx;
1056mod nyc;
1057mod openapi_ts;
1058mod opencode;
1059mod oxlint;
1060mod pandacss;
1061mod parcel;
1062mod playwright;
1063mod plop;
1064mod pm2;
1065mod pnpm;
1066mod postcss;
1067mod prettier;
1068mod prisma;
1069mod qwik;
1070mod react_native;
1071mod react_router;
1072mod redwoodsdk;
1073mod relay;
1074mod remark;
1075mod remix;
1076mod rolldown;
1077mod rollup;
1078mod rsbuild;
1079mod rspack;
1080mod sanity;
1081mod semantic_release;
1082mod sentry;
1083mod simple_git_hooks;
1084mod storybook;
1085mod stryker;
1086mod stylelint;
1087mod supabase;
1088mod sveltekit;
1089mod svgo;
1090mod svgr;
1091mod swc;
1092mod syncpack;
1093mod tailwind;
1094mod tanstack_router;
1095mod tap;
1096mod test_alias;
1097mod tsd;
1098mod tsdown;
1099mod tsup;
1100mod turborepo;
1101mod typedoc;
1102mod typeorm;
1103mod typescript;
1104mod unocss;
1105mod vite;
1106mod vitepress;
1107mod vitest;
1108mod webdriverio;
1109mod webpack;
1110mod wrangler;
1111mod wuchale;
1112
1113#[cfg(test)]
1114mod tests {
1115 use super::*;
1116 use std::path::Path;
1117
1118 #[test]
1121 fn is_enabled_with_deps_exact_match() {
1122 let plugin = nextjs::NextJsPlugin;
1123 let deps = vec!["next".to_string()];
1124 assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1125 }
1126
1127 #[test]
1128 fn is_enabled_with_deps_no_match() {
1129 let plugin = nextjs::NextJsPlugin;
1130 let deps = vec!["react".to_string()];
1131 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1132 }
1133
1134 #[test]
1135 fn is_enabled_with_deps_empty_deps() {
1136 let plugin = nextjs::NextJsPlugin;
1137 let deps: Vec<String> = vec![];
1138 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1139 }
1140
1141 #[test]
1142 fn entry_point_role_defaults_are_centralized() {
1143 assert_eq!(vite::VitePlugin.entry_point_role(), EntryPointRole::Runtime);
1144 assert_eq!(
1145 vitest::VitestPlugin.entry_point_role(),
1146 EntryPointRole::Test
1147 );
1148 assert_eq!(
1149 storybook::StorybookPlugin.entry_point_role(),
1150 EntryPointRole::Support
1151 );
1152 assert_eq!(knex::KnexPlugin.entry_point_role(), EntryPointRole::Support);
1153 }
1154
1155 #[test]
1156 fn plugins_with_entry_patterns_have_explicit_role_intent() {
1157 let runtime_or_test_or_support: rustc_hash::FxHashSet<&'static str> =
1158 TEST_ENTRY_POINT_PLUGINS
1159 .iter()
1160 .chain(RUNTIME_ENTRY_POINT_PLUGINS.iter())
1161 .chain(SUPPORT_ENTRY_POINT_PLUGINS.iter())
1162 .copied()
1163 .collect();
1164
1165 for plugin in crate::plugins::registry::builtin::create_builtin_plugins() {
1166 if plugin.entry_patterns().is_empty() {
1167 continue;
1168 }
1169 assert!(
1170 runtime_or_test_or_support.contains(plugin.name()),
1171 "plugin '{}' exposes entry patterns but is missing from the entry-point role map",
1172 plugin.name()
1173 );
1174 }
1175 }
1176
1177 #[test]
1180 fn plugin_result_is_empty_when_default() {
1181 let r = PluginResult::default();
1182 assert!(r.is_empty());
1183 }
1184
1185 #[test]
1186 fn plugin_result_not_empty_with_entry_patterns() {
1187 let r = PluginResult {
1188 entry_patterns: vec!["*.ts".into()],
1189 ..Default::default()
1190 };
1191 assert!(!r.is_empty());
1192 }
1193
1194 #[test]
1195 fn plugin_result_not_empty_with_referenced_deps() {
1196 let r = PluginResult {
1197 referenced_dependencies: vec!["lodash".to_string()],
1198 ..Default::default()
1199 };
1200 assert!(!r.is_empty());
1201 }
1202
1203 #[test]
1204 fn plugin_result_not_empty_with_setup_files() {
1205 let r = PluginResult {
1206 setup_files: vec![PathBuf::from("/setup.ts")],
1207 ..Default::default()
1208 };
1209 assert!(!r.is_empty());
1210 }
1211
1212 #[test]
1213 fn plugin_result_not_empty_with_always_used_files() {
1214 let r = PluginResult {
1215 always_used_files: vec!["**/*.stories.tsx".to_string()],
1216 ..Default::default()
1217 };
1218 assert!(!r.is_empty());
1219 }
1220
1221 #[test]
1222 fn plugin_result_not_empty_with_fixture_patterns() {
1223 let r = PluginResult {
1224 fixture_patterns: vec!["**/__fixtures__/**/*".to_string()],
1225 ..Default::default()
1226 };
1227 assert!(!r.is_empty());
1228 }
1229
1230 #[test]
1233 fn is_enabled_with_deps_prefix_match() {
1234 let plugin = storybook::StorybookPlugin;
1236 let deps = vec!["@storybook/react".to_string()];
1237 assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1238 }
1239
1240 #[test]
1241 fn is_enabled_with_deps_prefix_no_match_without_slash() {
1242 let plugin = storybook::StorybookPlugin;
1244 let deps = vec!["@storybookish".to_string()];
1245 assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
1246 }
1247
1248 #[test]
1249 fn is_enabled_with_deps_multiple_enablers() {
1250 let plugin = vitest::VitestPlugin;
1252 let deps_vitest = vec!["vitest".to_string()];
1253 let deps_none = vec!["mocha".to_string()];
1254 assert!(plugin.is_enabled_with_deps(&deps_vitest, Path::new("/project")));
1255 assert!(!plugin.is_enabled_with_deps(&deps_none, Path::new("/project")));
1256 }
1257
1258 #[test]
1261 fn plugin_default_methods_return_empty() {
1262 let plugin = commitizen::CommitizenPlugin;
1264 assert!(
1265 plugin.tooling_dependencies().is_empty() || !plugin.tooling_dependencies().is_empty()
1266 );
1267 assert!(plugin.virtual_module_prefixes().is_empty());
1268 assert!(plugin.virtual_package_suffixes().is_empty());
1269 assert!(plugin.path_aliases(Path::new("/project")).is_empty());
1270 assert!(
1271 plugin.package_json_config_key().is_none()
1272 || plugin.package_json_config_key().is_some()
1273 );
1274 }
1275
1276 #[test]
1277 fn plugin_resolve_config_default_returns_empty() {
1278 let plugin = commitizen::CommitizenPlugin;
1279 let result = plugin.resolve_config(
1280 Path::new("/project/config.js"),
1281 "const x = 1;",
1282 Path::new("/project"),
1283 );
1284 assert!(result.is_empty());
1285 }
1286
1287 #[test]
1290 fn is_enabled_with_deps_exact_and_prefix_both_work() {
1291 let plugin = storybook::StorybookPlugin;
1292 let deps_exact = vec!["storybook".to_string()];
1293 assert!(plugin.is_enabled_with_deps(&deps_exact, Path::new("/project")));
1294 let deps_prefix = vec!["@storybook/vue3".to_string()];
1295 assert!(plugin.is_enabled_with_deps(&deps_prefix, Path::new("/project")));
1296 }
1297
1298 #[test]
1299 fn is_enabled_with_deps_multiple_enablers_remix() {
1300 let plugin = remix::RemixPlugin;
1301 let deps_node = vec!["@remix-run/node".to_string()];
1302 assert!(plugin.is_enabled_with_deps(&deps_node, Path::new("/project")));
1303 let deps_react = vec!["@remix-run/react".to_string()];
1304 assert!(plugin.is_enabled_with_deps(&deps_react, Path::new("/project")));
1305 let deps_cf = vec!["@remix-run/cloudflare".to_string()];
1306 assert!(plugin.is_enabled_with_deps(&deps_cf, Path::new("/project")));
1307 }
1308
1309 struct MinimalPlugin;
1312 impl Plugin for MinimalPlugin {
1313 fn name(&self) -> &'static str {
1314 "minimal"
1315 }
1316 }
1317
1318 #[test]
1319 fn default_enablers_is_empty() {
1320 assert!(MinimalPlugin.enablers().is_empty());
1321 }
1322
1323 #[test]
1324 fn default_entry_patterns_is_empty() {
1325 assert!(MinimalPlugin.entry_patterns().is_empty());
1326 }
1327
1328 #[test]
1329 fn default_config_patterns_is_empty() {
1330 assert!(MinimalPlugin.config_patterns().is_empty());
1331 }
1332
1333 #[test]
1334 fn default_always_used_is_empty() {
1335 assert!(MinimalPlugin.always_used().is_empty());
1336 }
1337
1338 #[test]
1339 fn default_used_exports_is_empty() {
1340 assert!(MinimalPlugin.used_exports().is_empty());
1341 }
1342
1343 #[test]
1344 fn default_tooling_dependencies_is_empty() {
1345 assert!(MinimalPlugin.tooling_dependencies().is_empty());
1346 }
1347
1348 #[test]
1349 fn default_fixture_glob_patterns_is_empty() {
1350 assert!(MinimalPlugin.fixture_glob_patterns().is_empty());
1351 }
1352
1353 #[test]
1354 fn default_virtual_module_prefixes_is_empty() {
1355 assert!(MinimalPlugin.virtual_module_prefixes().is_empty());
1356 }
1357
1358 #[test]
1359 fn default_virtual_package_suffixes_is_empty() {
1360 assert!(MinimalPlugin.virtual_package_suffixes().is_empty());
1361 }
1362
1363 #[test]
1364 fn default_path_aliases_is_empty() {
1365 assert!(MinimalPlugin.path_aliases(Path::new("/")).is_empty());
1366 }
1367
1368 #[test]
1369 fn default_resolve_config_returns_empty() {
1370 let r = MinimalPlugin.resolve_config(
1371 Path::new("config.js"),
1372 "export default {}",
1373 Path::new("/"),
1374 );
1375 assert!(r.is_empty());
1376 }
1377
1378 #[test]
1379 fn default_package_json_config_key_is_none() {
1380 assert!(MinimalPlugin.package_json_config_key().is_none());
1381 }
1382
1383 #[test]
1384 fn default_is_enabled_returns_false_when_no_enablers() {
1385 let deps = vec!["anything".to_string()];
1386 assert!(!MinimalPlugin.is_enabled_with_deps(&deps, Path::new("/")));
1387 }
1388
1389 #[test]
1392 fn all_builtin_plugin_names_are_unique() {
1393 let plugins = registry::builtin::create_builtin_plugins();
1394 let mut seen = std::collections::BTreeSet::new();
1395 for p in &plugins {
1396 let name = p.name();
1397 assert!(seen.insert(name), "duplicate plugin name: {name}");
1398 }
1399 }
1400
1401 #[test]
1402 fn all_builtin_plugins_have_enablers() {
1403 let plugins = registry::builtin::create_builtin_plugins();
1404 for p in &plugins {
1405 assert!(
1406 !p.enablers().is_empty(),
1407 "plugin '{}' has no enablers",
1408 p.name()
1409 );
1410 }
1411 }
1412
1413 #[test]
1414 fn plugins_with_config_patterns_have_always_used() {
1415 let plugins = registry::builtin::create_builtin_plugins();
1416 for p in &plugins {
1417 if !p.config_patterns().is_empty() {
1418 assert!(
1419 !p.always_used().is_empty(),
1420 "plugin '{}' has config_patterns but no always_used",
1421 p.name()
1422 );
1423 }
1424 }
1425 }
1426
1427 #[test]
1430 fn framework_plugins_enablers() {
1431 let cases: Vec<(&dyn Plugin, &[&str])> = vec![
1432 (&nextjs::NextJsPlugin, &["next"]),
1433 (&nuxt::NuxtPlugin, &["nuxt"]),
1434 (&angular::AngularPlugin, &["@angular/core"]),
1435 (&sveltekit::SvelteKitPlugin, &["@sveltejs/kit"]),
1436 (&gatsby::GatsbyPlugin, &["gatsby"]),
1437 ];
1438 for (plugin, expected_enablers) in cases {
1439 let enablers = plugin.enablers();
1440 for expected in expected_enablers {
1441 assert!(
1442 enablers.contains(expected),
1443 "plugin '{}' should have '{}'",
1444 plugin.name(),
1445 expected
1446 );
1447 }
1448 }
1449 }
1450
1451 #[test]
1452 fn testing_plugins_enablers() {
1453 let cases: Vec<(&dyn Plugin, &str)> = vec![
1454 (&jest::JestPlugin, "jest"),
1455 (&vitest::VitestPlugin, "vitest"),
1456 (&playwright::PlaywrightPlugin, "@playwright/test"),
1457 (&cypress::CypressPlugin, "cypress"),
1458 (&mocha::MochaPlugin, "mocha"),
1459 (&stryker::StrykerPlugin, "@stryker-mutator/core"),
1460 ];
1461 for (plugin, enabler) in cases {
1462 assert!(
1463 plugin.enablers().contains(&enabler),
1464 "plugin '{}' should have '{}'",
1465 plugin.name(),
1466 enabler
1467 );
1468 }
1469 }
1470
1471 #[test]
1472 fn bundler_plugins_enablers() {
1473 let cases: Vec<(&dyn Plugin, &str)> = vec![
1474 (&vite::VitePlugin, "vite"),
1475 (&webpack::WebpackPlugin, "webpack"),
1476 (&rollup::RollupPlugin, "rollup"),
1477 ];
1478 for (plugin, enabler) in cases {
1479 assert!(
1480 plugin.enablers().contains(&enabler),
1481 "plugin '{}' should have '{}'",
1482 plugin.name(),
1483 enabler
1484 );
1485 }
1486 }
1487
1488 #[test]
1489 fn test_plugins_have_test_entry_patterns() {
1490 let test_plugins: Vec<&dyn Plugin> = vec![
1491 &jest::JestPlugin,
1492 &vitest::VitestPlugin,
1493 &mocha::MochaPlugin,
1494 &tap::TapPlugin,
1495 &tsd::TsdPlugin,
1496 ];
1497 for plugin in test_plugins {
1498 let patterns = plugin.entry_patterns();
1499 assert!(
1500 !patterns.is_empty(),
1501 "test plugin '{}' should have entry patterns",
1502 plugin.name()
1503 );
1504 assert!(
1505 patterns
1506 .iter()
1507 .any(|p| p.contains("test") || p.contains("spec") || p.contains("__tests__")),
1508 "test plugin '{}' should have test/spec patterns",
1509 plugin.name()
1510 );
1511 }
1512 }
1513
1514 #[test]
1515 fn framework_plugins_have_entry_patterns() {
1516 let plugins: Vec<&dyn Plugin> = vec![
1517 &nextjs::NextJsPlugin,
1518 &nuxt::NuxtPlugin,
1519 &angular::AngularPlugin,
1520 &sveltekit::SvelteKitPlugin,
1521 ];
1522 for plugin in plugins {
1523 assert!(
1524 !plugin.entry_patterns().is_empty(),
1525 "framework plugin '{}' should have entry patterns",
1526 plugin.name()
1527 );
1528 }
1529 }
1530
1531 #[test]
1532 fn plugins_with_resolve_config_have_config_patterns() {
1533 let plugins: Vec<&dyn Plugin> = vec![
1534 &jest::JestPlugin,
1535 &vitest::VitestPlugin,
1536 &babel::BabelPlugin,
1537 &eslint::EslintPlugin,
1538 &webpack::WebpackPlugin,
1539 &storybook::StorybookPlugin,
1540 &typescript::TypeScriptPlugin,
1541 &postcss::PostCssPlugin,
1542 &nextjs::NextJsPlugin,
1543 &nuxt::NuxtPlugin,
1544 &angular::AngularPlugin,
1545 &nx::NxPlugin,
1546 &stryker::StrykerPlugin,
1547 &wuchale::WuchalePlugin,
1548 &rollup::RollupPlugin,
1549 &sveltekit::SvelteKitPlugin,
1550 &prettier::PrettierPlugin,
1551 ];
1552 for plugin in plugins {
1553 assert!(
1554 !plugin.config_patterns().is_empty(),
1555 "plugin '{}' with resolve_config should have config_patterns",
1556 plugin.name()
1557 );
1558 }
1559 }
1560
1561 #[test]
1562 fn plugin_tooling_deps_include_enabler_package() {
1563 let plugins: Vec<&dyn Plugin> = vec![
1564 &jest::JestPlugin,
1565 &vitest::VitestPlugin,
1566 &webpack::WebpackPlugin,
1567 &typescript::TypeScriptPlugin,
1568 &eslint::EslintPlugin,
1569 &prettier::PrettierPlugin,
1570 &danger::DangerPlugin,
1571 &stryker::StrykerPlugin,
1572 &wuchale::WuchalePlugin,
1573 ];
1574 for plugin in plugins {
1575 let tooling = plugin.tooling_dependencies();
1576 let enablers = plugin.enablers();
1577 assert!(
1578 enablers
1579 .iter()
1580 .any(|e| !e.ends_with('/') && tooling.contains(e)),
1581 "plugin '{}': at least one non-prefix enabler should be in tooling_dependencies",
1582 plugin.name()
1583 );
1584 }
1585 }
1586
1587 #[test]
1588 fn nextjs_has_used_exports_for_pages() {
1589 let plugin = nextjs::NextJsPlugin;
1590 let exports = plugin.used_exports();
1591 assert!(!exports.is_empty());
1592 assert!(exports.iter().any(|(_, names)| names.contains(&"default")));
1593 }
1594
1595 #[test]
1596 fn remix_has_used_exports_for_routes() {
1597 let plugin = remix::RemixPlugin;
1598 let exports = plugin.used_exports();
1599 assert!(!exports.is_empty());
1600 let route_entry = exports.iter().find(|(pat, _)| pat.contains("routes"));
1601 assert!(route_entry.is_some());
1602 let (_, names) = route_entry.unwrap();
1603 assert!(names.contains(&"loader"));
1604 assert!(names.contains(&"action"));
1605 assert!(names.contains(&"default"));
1606 }
1607
1608 #[test]
1609 fn sveltekit_has_used_exports_for_routes() {
1610 let plugin = sveltekit::SvelteKitPlugin;
1611 let exports = plugin.used_exports();
1612 assert!(!exports.is_empty());
1613 assert!(exports.iter().any(|(_, names)| names.contains(&"GET")));
1614 }
1615
1616 #[test]
1617 fn nuxt_has_hash_virtual_prefix() {
1618 assert!(nuxt::NuxtPlugin.virtual_module_prefixes().contains(&"#"));
1619 }
1620
1621 #[test]
1622 fn sveltekit_has_dollar_virtual_prefixes() {
1623 let prefixes = sveltekit::SvelteKitPlugin.virtual_module_prefixes();
1624 assert!(prefixes.contains(&"$app/"));
1625 assert!(prefixes.contains(&"$env/"));
1626 assert!(prefixes.contains(&"$lib/"));
1627 }
1628
1629 #[test]
1630 fn sveltekit_has_lib_path_alias() {
1631 let aliases = sveltekit::SvelteKitPlugin.path_aliases(Path::new("/project"));
1632 assert!(aliases.iter().any(|(prefix, _)| *prefix == "$lib/"));
1633 }
1634
1635 #[test]
1636 fn nuxt_has_tilde_path_alias() {
1637 let aliases = nuxt::NuxtPlugin.path_aliases(Path::new("/nonexistent"));
1638 assert!(aliases.iter().any(|(prefix, _)| *prefix == "~/"));
1639 assert!(aliases.iter().any(|(prefix, _)| *prefix == "~~/"));
1640 }
1641
1642 #[test]
1643 fn jest_has_package_json_config_key() {
1644 assert_eq!(jest::JestPlugin.package_json_config_key(), Some("jest"));
1645 }
1646
1647 #[test]
1648 fn tsd_has_package_json_config_key() {
1649 assert_eq!(tsd::TsdPlugin.package_json_config_key(), Some("tsd"));
1650 }
1651
1652 #[test]
1653 fn babel_has_package_json_config_key() {
1654 assert_eq!(babel::BabelPlugin.package_json_config_key(), Some("babel"));
1655 }
1656
1657 #[test]
1658 fn eslint_has_package_json_config_key() {
1659 assert_eq!(
1660 eslint::EslintPlugin.package_json_config_key(),
1661 Some("eslintConfig")
1662 );
1663 }
1664
1665 #[test]
1666 fn prettier_has_package_json_config_key() {
1667 assert_eq!(
1668 prettier::PrettierPlugin.package_json_config_key(),
1669 Some("prettier")
1670 );
1671 }
1672
1673 #[test]
1674 fn macro_generated_plugin_basic_properties() {
1675 let plugin = msw::MswPlugin;
1676 assert_eq!(plugin.name(), "msw");
1677 assert!(plugin.enablers().contains(&"msw"));
1678 assert!(!plugin.entry_patterns().is_empty());
1679 assert!(plugin.config_patterns().is_empty());
1680 assert!(!plugin.always_used().is_empty());
1681 assert!(!plugin.tooling_dependencies().is_empty());
1682 }
1683
1684 #[test]
1685 fn macro_generated_plugin_with_used_exports() {
1686 let plugin = remix::RemixPlugin;
1687 assert_eq!(plugin.name(), "remix");
1688 assert!(!plugin.used_exports().is_empty());
1689 }
1690
1691 #[test]
1692 fn macro_passes_through_virtual_package_suffixes() {
1693 define_plugin! {
1698 struct MacroSuffixSmokePlugin => "macro-suffix-smoke",
1699 enablers: &["macro-suffix-smoke"],
1700 virtual_package_suffixes: &["/__macro_smoke__"],
1701 }
1702
1703 let plugin = MacroSuffixSmokePlugin;
1704 assert_eq!(
1705 plugin.virtual_package_suffixes(),
1706 &["/__macro_smoke__"],
1707 "macro-declared virtual_package_suffixes must propagate to the trait method"
1708 );
1709 }
1710
1711 #[test]
1712 fn macro_generated_plugin_imports_only_resolve_config() {
1713 let plugin = cypress::CypressPlugin;
1714 let source = r"
1715 import { defineConfig } from 'cypress';
1716 import coveragePlugin from '@cypress/code-coverage';
1717 export default defineConfig({});
1718 ";
1719 let result = plugin.resolve_config(
1720 Path::new("cypress.config.ts"),
1721 source,
1722 Path::new("/project"),
1723 );
1724 assert!(
1725 result
1726 .referenced_dependencies
1727 .contains(&"cypress".to_string())
1728 );
1729 assert!(
1730 result
1731 .referenced_dependencies
1732 .contains(&"@cypress/code-coverage".to_string())
1733 );
1734 }
1735
1736 #[test]
1737 fn builtin_plugin_count_is_expected() {
1738 let plugins = registry::builtin::create_builtin_plugins();
1739 assert!(
1740 plugins.len() >= 80,
1741 "expected at least 80 built-in plugins, got {}",
1742 plugins.len()
1743 );
1744 }
1745}