use std::path::{Path, PathBuf};
use fallow_config::{AutoImportRule, EntryPointRole, PackageJson, UsedClassMemberRule};
use fallow_types::semantic::SemanticFrameworkContract;
use regex::Regex;
const TEST_ENTRY_POINT_PLUGINS: &[&str] = &[
"ava",
"bun",
"deno",
"cucumber",
"cypress",
"jest",
"k6",
"mocha",
"playwright",
"tap",
"tsd",
"vitest",
"webdriverio",
];
const RUNTIME_ENTRY_POINT_PLUGINS: &[&str] = &[
"adonis",
"angular",
"astro",
"browser-extension",
"convex",
"docusaurus",
"electron",
"ember",
"expo",
"expo-router",
"gatsby",
"hardhat",
"module-federation",
"nestjs",
"next-intl",
"nextjs",
"nitro",
"nuxt",
"obsidian",
"parcel",
"qwik",
"react-native",
"react-router",
"redwoodsdk",
"remix",
"rolldown",
"rollup",
"rsbuild",
"rspack",
"sanity",
"supabase",
"sveltekit",
"tanstack-router",
"tsdown",
"tsup",
"vite",
"vitepress",
"waku",
"webpack",
"wrangler",
"wxt",
];
#[cfg(test)]
const SUPPORT_ENTRY_POINT_PLUGINS: &[&str] = &[
"content-collections",
"contentlayer",
"danger",
"drizzle",
"fumadocs",
"i18next",
"knex",
"kysely",
"mintlify",
"msw",
"opencode",
"prisma",
"storybook",
"stryker",
"typeorm",
"velite",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginConfigEffect {
Unreadable,
NotModeled,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginConfigDiagnostic {
pub config_path: PathBuf,
pub plugin: String,
pub key: String,
pub reason: String,
pub effect: PluginConfigEffect,
}
impl PluginConfigDiagnostic {
pub(super) fn unreadable(
config_path: &Path,
plugin: &str,
key: &str,
reason: &'static str,
) -> Self {
Self {
config_path: config_path.to_path_buf(),
plugin: plugin.to_owned(),
key: key.to_owned(),
reason: reason.to_owned(),
effect: PluginConfigEffect::Unreadable,
}
}
pub(crate) fn not_modeled(
config_path: &Path,
plugin: &str,
key: &str,
reason: &'static str,
) -> Self {
Self {
config_path: config_path.to_path_buf(),
plugin: plugin.to_owned(),
key: key.to_owned(),
reason: reason.to_owned(),
effect: PluginConfigEffect::NotModeled,
}
}
#[must_use]
pub fn into_workspace_diagnostic(self, root: &Path) -> fallow_config::WorkspaceDiagnostic {
let Self {
config_path,
plugin,
key,
reason,
effect,
} = self;
let kind = match effect {
PluginConfigEffect::Unreadable => {
fallow_config::WorkspaceDiagnosticKind::PluginConfigUnreadable {
plugin,
key,
reason,
}
}
PluginConfigEffect::NotModeled => {
fallow_config::WorkspaceDiagnosticKind::PluginEffectNotModeled {
plugin,
key,
reason,
}
}
};
fallow_config::WorkspaceDiagnostic::new(root, config_path, kind)
}
}
#[derive(Debug, Default)]
pub struct PluginResult {
entry_patterns: Vec<PathRule>,
replace_entry_patterns: bool,
replace_used_export_rules: bool,
used_exports: Vec<UsedExportRule>,
used_class_members: Vec<UsedClassMemberRule>,
referenced_dependencies: Vec<String>,
package_referenced_dependencies: Vec<(PathBuf, String)>,
always_used_files: Vec<String>,
path_aliases: Vec<(String, String)>,
setup_files: Vec<PathBuf>,
fixture_patterns: Vec<String>,
scss_include_paths: Vec<PathBuf>,
static_dir_mappings: Vec<(PathBuf, String)>,
framework_static_dir_mappings: Vec<(PathBuf, String)>,
provided_dependencies: Vec<ProvidedDependencyRule>,
config_diagnostics: Vec<PluginConfigDiagnostic>,
federation_sources: Vec<FederationSource>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FederationSource {
pub target: FederationSourceTarget,
pub config_path: PathBuf,
pub plugin: String,
pub key: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FederationSourceTarget {
Exposed(PathRule),
Remote(String),
}
impl FederationSource {
#[must_use]
fn prefixed(&self, ws_prefix: &str) -> Self {
let target = match &self.target {
FederationSourceTarget::Exposed(rule) => {
FederationSourceTarget::Exposed(rule.prefixed(ws_prefix))
}
FederationSourceTarget::Remote(alias) => FederationSourceTarget::Remote(alias.clone()),
};
Self {
target,
config_path: self.config_path.clone(),
plugin: self.plugin.clone(),
key: self.key,
}
}
}
#[must_use]
pub fn federation_trace_provenance(
root: &Path,
files: &[crate::discover::DiscoveredFile],
sources: &[FederationSource],
modules: &[crate::extract::ModuleInfo],
) -> fallow_types::trace::TraceProvenance {
let mut provenance = fallow_types::trace::TraceProvenance::default();
push_runtime_remote_sources(&mut provenance, root, files, modules);
if sources.is_empty() {
return provenance;
}
let mut exposed = Vec::new();
for source in sources {
let config = source
.config_path
.strip_prefix(root)
.unwrap_or(&source.config_path)
.to_path_buf();
let trace_source = fallow_types::trace::TraceSource {
kind: "module-federation".to_owned(),
plugin: source.plugin.clone(),
config,
key: source.key.to_owned(),
};
match &source.target {
FederationSourceTarget::Exposed(rule) => {
if let Some(compiled) =
CompiledPathRule::for_entry_rule(rule, "Module Federation exposes target")
{
exposed.push((compiled, trace_source));
}
}
FederationSourceTarget::Remote(alias) => {
provenance.push_dependency(alias.clone(), trace_source);
}
}
}
if exposed.is_empty() {
return provenance;
}
for file in files {
let Ok(relative) = file.path.strip_prefix(root) else {
continue;
};
let relative_str = relative.to_string_lossy().replace('\\', "/");
for (rule, source) in &exposed {
if rule.matches(&relative_str) {
provenance.push_file(relative.to_path_buf(), source.clone());
}
}
}
provenance
}
fn push_runtime_remote_sources(
provenance: &mut fallow_types::trace::TraceProvenance,
root: &Path,
files: &[crate::discover::DiscoveredFile],
modules: &[crate::extract::ModuleInfo],
) {
for module in modules {
let mut file = None;
for fact in module.semantic_facts.iter() {
let fallow_types::extract::SemanticFact::FederationRuntimeRemote(fact) = fact else {
continue;
};
let Some(remote) = &fact.remote else {
continue;
};
let Some(path) = file.get_or_insert_with(|| {
files.get(module.file_id.0 as usize).map(|file| {
file.path
.strip_prefix(root)
.unwrap_or(&file.path)
.to_path_buf()
})
}) else {
break;
};
provenance.push_dependency(
remote.clone(),
fallow_types::trace::TraceSource {
kind: "module-federation".to_owned(),
plugin: "module-federation".to_owned(),
config: path.clone(),
key: fact.call.name().to_owned(),
},
);
}
}
}
impl PluginResult {
fn push_parent_relative_entry_pattern(&mut self, pattern: String) {
let mut rule = PathRule::new(pattern);
rule.parent_relative = true;
self.entry_patterns.push(rule);
}
fn push_entry_pattern(&mut self, pattern: impl Into<String>) {
self.entry_patterns
.push(PathRule::new(normalize_entry_pattern(pattern.into())));
}
fn extend_entry_patterns<I, S>(&mut self, patterns: I)
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.entry_patterns.extend(
patterns
.into_iter()
.map(|pat| PathRule::new(normalize_entry_pattern(pat.into()))),
);
}
fn extend_entry_patterns_or_dependencies<I, S>(
&mut self,
values: I,
resolve_path: impl Fn(String) -> String,
) where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for value in values {
let value = value.into();
if let Some(request) = module_request(&value) {
self.referenced_dependencies
.push(crate::resolve::extract_package_name(request));
continue;
}
self.push_entry_path(resolve_path(value));
}
}
fn extend_entry_patterns_and_dependencies<I, S>(&mut self, values: I, root: &Path)
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for value in values {
let value = value.into();
if let Some(request) = module_request(&value)
&& !names_project_file(root, request)
{
self.referenced_dependencies
.push(crate::resolve::extract_package_name(request));
}
self.push_entry_path(value);
}
}
fn extend_entry_paths<I, S>(&mut self, values: I)
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for value in values {
self.push_entry_path(value.into());
}
}
fn push_entry_path(&mut self, value: String) {
if has_glob_syntax(&value) || has_source_extension(&value) {
self.push_entry_pattern(value);
return;
}
let base = value.trim_end_matches('/').to_owned();
self.push_entry_pattern(value);
self.push_entry_pattern(format!("{base}.{REQUEST_EXTENSIONS}"));
self.push_entry_pattern(format!("{base}/index.{REQUEST_EXTENSIONS}"));
}
fn push_used_export_rule(
&mut self,
pattern: impl Into<String>,
exports: impl IntoIterator<Item = impl Into<String>>,
) {
self.used_exports
.push(UsedExportRule::new(pattern, exports));
}
#[must_use]
const fn is_empty(&self) -> bool {
self.config_diagnostics.is_empty()
&& self.entry_patterns.is_empty()
&& self.used_exports.is_empty()
&& self.used_class_members.is_empty()
&& self.referenced_dependencies.is_empty()
&& self.package_referenced_dependencies.is_empty()
&& self.always_used_files.is_empty()
&& self.path_aliases.is_empty()
&& self.setup_files.is_empty()
&& self.fixture_patterns.is_empty()
&& self.scss_include_paths.is_empty()
&& self.static_dir_mappings.is_empty()
&& self.framework_static_dir_mappings.is_empty()
&& self.provided_dependencies.is_empty()
&& self.federation_sources.is_empty()
}
}
fn names_project_file(root: &Path, value: &str) -> bool {
let base = root.join(value);
base.is_file()
|| crate::discover::SOURCE_EXTENSIONS.iter().any(|extension| {
let mut candidate = base.clone().into_os_string();
candidate.push(".");
candidate.push(extension);
Path::new(&candidate).is_file() || base.join(format!("index.{extension}")).is_file()
})
}
const REQUEST_EXTENSIONS: &str = "{ts,tsx,mts,cts,gts,js,jsx,mjs,cjs,gjs,vue,svelte,astro,mdx}";
fn normalize_entry_pattern(pattern: String) -> String {
pattern
.strip_prefix("./")
.map(str::to_owned)
.unwrap_or(pattern)
}
fn module_request(value: &str) -> Option<&str> {
let request = strip_resource_query(value);
(config_parser::is_package_specifier(request)
&& !has_glob_syntax(request)
&& !has_source_extension(request))
.then_some(request)
}
fn strip_resource_query(value: &str) -> &str {
match value.split_once('?') {
Some((request, query)) if is_resource_query(query) => request,
_ => value,
}
}
fn is_resource_query(query: &str) -> bool {
!query.is_empty()
&& query.split('&').all(|pair| {
let key = pair.split_once('=').map_or(pair, |(key, _)| key);
key.starts_with(|first: char| first.is_ascii_alphanumeric() || first == '_')
&& key
.chars()
.all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '-' | '.'))
})
}
fn has_glob_syntax(value: &str) -> bool {
value.contains('*') || value.contains('?') || value.contains('[') || value.contains('{')
}
fn has_source_extension(value: &str) -> bool {
Path::new(value)
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| {
crate::discover::SOURCE_EXTENSIONS.contains(&ext.to_ascii_lowercase().as_str())
})
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PathRule {
pub pattern: String,
pub exclude_globs: Vec<String>,
pub exclude_regexes: Vec<String>,
pub exclude_segment_regexes: Vec<String>,
pub parent_relative: bool,
}
impl PathRule {
#[must_use]
pub(crate) fn new(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
exclude_globs: Vec::new(),
exclude_regexes: Vec::new(),
exclude_segment_regexes: Vec::new(),
parent_relative: false,
}
}
#[must_use]
fn from_static(pattern: &'static str) -> Self {
Self::new(pattern)
}
#[must_use]
pub(crate) fn with_excluded_globs<I, S>(mut self, patterns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.exclude_globs
.extend(patterns.into_iter().map(Into::into));
self
}
#[must_use]
fn with_excluded_regexes<I, S>(mut self, patterns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.exclude_regexes
.extend(patterns.into_iter().map(Into::into));
self
}
#[must_use]
fn with_excluded_segment_regexes<I, S>(mut self, patterns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.exclude_segment_regexes
.extend(patterns.into_iter().map(Into::into));
self
}
#[must_use]
fn prefixed(&self, ws_prefix: &str) -> Self {
let pattern = if self.parent_relative && self.pattern.starts_with("../") {
resolve_parent_relative_pattern(&self.pattern, ws_prefix)
} else {
prefix_workspace_pattern(&self.pattern, ws_prefix)
};
Self {
pattern,
exclude_globs: self
.exclude_globs
.iter()
.map(|pattern| prefix_workspace_pattern(pattern, ws_prefix))
.collect(),
exclude_regexes: self
.exclude_regexes
.iter()
.map(|pattern| prefix_workspace_regex(pattern, ws_prefix))
.collect(),
exclude_segment_regexes: self.exclude_segment_regexes.clone(),
parent_relative: false,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UsedExportRule {
pub(crate) path: PathRule,
pub(crate) exports: Vec<String>,
}
impl UsedExportRule {
#[must_use]
pub(crate) fn new(
pattern: impl Into<String>,
exports: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
path: PathRule::new(pattern),
exports: exports.into_iter().map(Into::into).collect(),
}
}
#[must_use]
fn from_static(pattern: &'static str, exports: &'static [&'static str]) -> Self {
Self::new(pattern, exports.iter().copied())
}
#[must_use]
fn with_excluded_globs<I, S>(mut self, patterns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.path = self.path.with_excluded_globs(patterns);
self
}
#[must_use]
fn with_excluded_regexes<I, S>(mut self, patterns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.path = self.path.with_excluded_regexes(patterns);
self
}
#[must_use]
fn with_excluded_segment_regexes<I, S>(mut self, patterns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.path = self.path.with_excluded_segment_regexes(patterns);
self
}
#[must_use]
fn prefixed(&self, ws_prefix: &str) -> Self {
Self {
path: self.path.prefixed(ws_prefix),
exports: self.exports.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginUsedExportRule {
pub(crate) plugin_name: String,
pub(crate) rule: UsedExportRule,
}
impl PluginUsedExportRule {
#[must_use]
pub(crate) fn new(plugin_name: impl Into<String>, rule: UsedExportRule) -> Self {
Self {
plugin_name: plugin_name.into(),
rule,
}
}
#[must_use]
fn prefixed(&self, ws_prefix: &str) -> Self {
Self {
plugin_name: self.plugin_name.clone(),
rule: self.rule.prefixed(ws_prefix),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ProvidedDependencyRule {
pub(crate) path: PathRule,
exact_specifiers: Vec<String>,
specifier_prefixes: Vec<String>,
}
impl ProvidedDependencyRule {
#[must_use]
fn new(
pattern: impl Into<String>,
exact_specifiers: impl IntoIterator<Item = impl Into<String>>,
specifier_prefixes: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self {
path: PathRule::new(pattern),
exact_specifiers: exact_specifiers.into_iter().map(Into::into).collect(),
specifier_prefixes: specifier_prefixes.into_iter().map(Into::into).collect(),
}
}
#[must_use]
fn prefixed(&self, ws_prefix: &str) -> Self {
Self {
path: self.path.prefixed(ws_prefix),
exact_specifiers: self.exact_specifiers.clone(),
specifier_prefixes: self.specifier_prefixes.clone(),
}
}
#[must_use]
pub(crate) fn may_cover_package(&self, package_name: &str) -> bool {
self.exact_specifiers
.iter()
.chain(self.specifier_prefixes.iter())
.any(|specifier| crate::resolve::extract_package_name(specifier) == package_name)
}
#[must_use]
pub(crate) fn covers_specifier(&self, specifier: &str) -> bool {
self.exact_specifiers
.iter()
.any(|allowed| allowed == specifier)
|| self
.specifier_prefixes
.iter()
.any(|prefix| specifier.starts_with(prefix))
}
}
#[derive(Debug, Clone)]
pub(crate) struct CompiledPathRule {
include: globset::GlobMatcher,
exclude_globs: Vec<globset::GlobMatcher>,
exclude_regexes: Vec<Regex>,
exclude_segment_regexes: Vec<Regex>,
}
impl CompiledPathRule {
pub(crate) fn for_entry_rule(rule: &PathRule, rule_kind: &str) -> Option<Self> {
let include = match globset::GlobBuilder::new(&rule.pattern)
.literal_separator(true)
.build()
{
Ok(glob) => glob.compile_matcher(),
Err(err) => {
tracing::warn!("invalid {rule_kind} '{}': {err}", rule.pattern);
return None;
}
};
Some(Self {
include,
exclude_globs: compile_excluded_globs(&rule.exclude_globs, rule_kind, &rule.pattern),
exclude_regexes: compile_excluded_regexes(
&rule.exclude_regexes,
rule_kind,
&rule.pattern,
),
exclude_segment_regexes: compile_excluded_segment_regexes(
&rule.exclude_segment_regexes,
rule_kind,
&rule.pattern,
),
})
}
pub(crate) fn for_used_export_rule(rule: &PathRule, rule_kind: &str) -> Option<Self> {
let include = match globset::Glob::new(&rule.pattern) {
Ok(glob) => glob.compile_matcher(),
Err(err) => {
tracing::warn!("invalid {rule_kind} '{}': {err}", rule.pattern);
return None;
}
};
Some(Self {
include,
exclude_globs: compile_excluded_globs(&rule.exclude_globs, rule_kind, &rule.pattern),
exclude_regexes: compile_excluded_regexes(
&rule.exclude_regexes,
rule_kind,
&rule.pattern,
),
exclude_segment_regexes: compile_excluded_segment_regexes(
&rule.exclude_segment_regexes,
rule_kind,
&rule.pattern,
),
})
}
#[must_use]
pub(crate) fn matches(&self, path: &str) -> bool {
self.include.is_match(path)
&& !self.exclude_globs.iter().any(|glob| glob.is_match(path))
&& !self
.exclude_regexes
.iter()
.any(|regex| regex.is_match(path))
&& !matches_segment_regex(path, &self.exclude_segment_regexes)
}
}
fn prefix_workspace_pattern(pattern: &str, ws_prefix: &str) -> String {
if pattern.starts_with(ws_prefix) || pattern.starts_with('/') {
pattern.to_string()
} else {
format!("{ws_prefix}/{pattern}")
}
}
fn resolve_parent_relative_pattern(pattern: &str, ws_prefix: &str) -> String {
if ws_prefix.starts_with('/') || Path::new(ws_prefix).is_absolute() {
return pattern.to_string();
}
let mut base: Vec<&str> = ws_prefix
.split(['/', '\\'])
.filter(|segment| !segment.is_empty())
.collect();
let mut rest = pattern;
while let Some(stripped) = rest.strip_prefix("../") {
if base.pop().is_none() {
return pattern.to_string();
}
rest = stripped;
}
if base.is_empty() {
rest.to_string()
} else {
format!("{}/{rest}", base.join("/"))
}
}
fn prefix_workspace_regex(pattern: &str, ws_prefix: &str) -> String {
if let Some(pattern) = pattern.strip_prefix('^') {
format!("^{}/{}", regex::escape(ws_prefix), pattern)
} else {
format!("^{}/(?:{})", regex::escape(ws_prefix), pattern)
}
}
fn compile_excluded_globs(
patterns: &[String],
rule_kind: &str,
rule_pattern: &str,
) -> Vec<globset::GlobMatcher> {
patterns
.iter()
.filter_map(|pattern| {
match globset::GlobBuilder::new(pattern)
.literal_separator(true)
.build()
{
Ok(glob) => Some(glob.compile_matcher()),
Err(err) => {
tracing::warn!(
"skipping invalid excluded glob '{}' for {} '{}': {err}",
pattern,
rule_kind,
rule_pattern
);
None
}
}
})
.collect()
}
fn compile_excluded_regexes(
patterns: &[String],
rule_kind: &str,
rule_pattern: &str,
) -> Vec<Regex> {
patterns
.iter()
.filter_map(|pattern| match Regex::new(pattern) {
Ok(regex) => Some(regex),
Err(err) => {
tracing::warn!(
"skipping invalid excluded regex '{}' for {} '{}': {err}",
pattern,
rule_kind,
rule_pattern
);
None
}
})
.collect()
}
fn compile_excluded_segment_regexes(
patterns: &[String],
rule_kind: &str,
rule_pattern: &str,
) -> Vec<Regex> {
patterns
.iter()
.filter_map(|pattern| match Regex::new(pattern) {
Ok(regex) => Some(regex),
Err(err) => {
tracing::warn!(
"skipping invalid excluded segment regex '{}' for {} '{}': {err}",
pattern,
rule_kind,
rule_pattern
);
None
}
})
.collect()
}
fn matches_segment_regex(path: &str, regexes: &[Regex]) -> bool {
path.split(std::path::is_separator)
.any(|segment| regexes.iter().any(|regex| regex.is_match(segment)))
}
impl From<String> for PathRule {
fn from(pattern: String) -> Self {
Self::new(pattern)
}
}
impl From<&str> for PathRule {
fn from(pattern: &str) -> Self {
Self::new(pattern)
}
}
impl std::ops::Deref for PathRule {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.pattern
}
}
impl PartialEq<&str> for PathRule {
fn eq(&self, other: &&str) -> bool {
self.pattern == *other
}
}
impl PartialEq<str> for PathRule {
fn eq(&self, other: &str) -> bool {
self.pattern == other
}
}
impl PartialEq<String> for PathRule {
fn eq(&self, other: &String) -> bool {
&self.pattern == other
}
}
pub trait Plugin: Send + Sync {
fn name(&self) -> &'static str;
fn enablers(&self) -> &'static [&'static str] {
&[]
}
fn is_enabled(&self, pkg: &PackageJson, root: &Path) -> bool {
let deps = pkg.all_dependency_names();
self.is_enabled_with_deps(&deps, root)
}
fn is_enabled_with_deps(&self, deps: &[String], _root: &Path) -> bool {
let enablers = self.enablers();
if enablers.is_empty() {
return false;
}
enablers.iter().any(|enabler| {
if enabler.ends_with('/') {
deps.iter().any(|d| d.starts_with(enabler))
} else {
deps.iter().any(|d| d == enabler)
}
})
}
fn is_enabled_with_files(
&self,
deps: &[String],
root: &Path,
_discovered_files: &[PathBuf],
_candidate_index: Option<®istry::ConfigCandidateIndex>,
) -> bool {
self.is_enabled_with_deps(deps, root)
}
fn script_enablers(&self) -> &'static [&'static str] {
&[]
}
fn is_enabled_with_scripts(
&self,
script_packages: &rustc_hash::FxHashSet<String>,
_root: &Path,
) -> bool {
let enablers = self.script_enablers();
if enablers.is_empty() {
return false;
}
enablers.iter().any(|enabler| {
if enabler.ends_with('/') {
script_packages
.iter()
.any(|package| package.starts_with(enabler))
} else {
script_packages.contains(*enabler)
}
})
}
fn entry_patterns(&self) -> &'static [&'static str] {
&[]
}
fn entry_pattern_rules(&self) -> Vec<PathRule> {
self.entry_patterns()
.iter()
.map(|pattern| PathRule::from_static(pattern))
.collect()
}
fn entry_point_role(&self) -> EntryPointRole {
builtin_entry_point_role(self.name())
}
fn config_patterns(&self) -> &'static [&'static str] {
&[]
}
fn always_used(&self) -> &'static [&'static str] {
&[]
}
fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
vec![]
}
fn used_export_rules(&self) -> Vec<UsedExportRule> {
self.used_exports()
.into_iter()
.map(|(pattern, exports)| UsedExportRule::from_static(pattern, exports))
.collect()
}
fn used_class_members(&self) -> &'static [&'static str] {
&[]
}
fn used_class_member_rules(&self) -> Vec<UsedClassMemberRule> {
Vec::new()
}
fn framework_class_member_contracts(&self) -> Vec<SemanticFrameworkContract> {
Vec::new()
}
fn fixture_glob_patterns(&self) -> &'static [&'static str] {
&[]
}
fn discovery_hidden_dirs(&self) -> &'static [&'static str] {
&[]
}
fn tooling_dependencies(&self) -> &'static [&'static str] {
&[]
}
fn virtual_module_prefixes(&self) -> &'static [&'static str] {
&[]
}
fn virtual_package_suffixes(&self) -> &'static [&'static str] {
&[]
}
fn generated_import_patterns(&self) -> &'static [&'static str] {
&[]
}
fn generated_type_import_prefixes(&self) -> &'static [&'static str] {
&[]
}
fn path_aliases(&self, _root: &Path) -> Vec<(&'static str, String)> {
vec![]
}
fn static_dir_mappings(&self, _root: &Path) -> Vec<(std::path::PathBuf, String)> {
vec![]
}
fn auto_imports(&self, _root: &Path) -> Vec<AutoImportRule> {
Vec::new()
}
fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> {
Vec::new()
}
fn is_enabled_with_package_json(&self, _pkg: &PackageJson, _root: &Path) -> bool {
false
}
fn resolve_package_json(&self, _pkg: &PackageJson, _root: &Path) -> PluginResult {
PluginResult::default()
}
fn package_json_referenced_dependencies(
&self,
_pkg: &PackageJson,
_root: &Path,
) -> Vec<String> {
Vec::new()
}
fn resolve_config(&self, _config_path: &Path, _source: &str, _root: &Path) -> PluginResult {
PluginResult::default()
}
fn package_json_config_key(&self) -> Option<&'static str> {
None
}
}
fn builtin_entry_point_role(name: &str) -> EntryPointRole {
if TEST_ENTRY_POINT_PLUGINS.contains(&name) {
EntryPointRole::Test
} else if RUNTIME_ENTRY_POINT_PLUGINS.contains(&name) {
EntryPointRole::Runtime
} else {
EntryPointRole::Support
}
}
macro_rules! define_plugin {
(
struct $name:ident => $display:expr,
enablers: $enablers:expr
$(, entry_patterns: $entry:expr)?
$(, config_patterns: $config:expr)?
$(, always_used: $always:expr)?
$(, tooling_dependencies: $tooling:expr)?
$(, fixture_glob_patterns: $fixtures:expr)?
$(, discovery_hidden_dirs: $hidden_dirs:expr)?
$(, virtual_module_prefixes: $virtual:expr)?
$(, virtual_package_suffixes: $virtual_suffixes:expr)?
$(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
$(, provided_dependencies: $provided_dependencies:expr)?
$(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
, resolve_config: imports_only
$(,)?
) => {
pub struct $name;
impl Plugin for $name {
fn name(&self) -> &'static str {
$display
}
fn enablers(&self) -> &'static [&'static str] {
$enablers
}
$( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
$( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
$( fn always_used(&self) -> &'static [&'static str] { $always } )?
$( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
$( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
$( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
$( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
$( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
$( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
$( fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> { $provided_dependencies } )?
$(
fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
vec![$( ($pat, $exports) ),*]
}
)?
fn resolve_config(
&self,
config_path: &std::path::Path,
source: &str,
_root: &std::path::Path,
) -> PluginResult {
let mut result = PluginResult::default();
crate::plugins::add_import_referenced_dependencies(
&mut result,
source,
config_path,
);
result
}
}
};
(
struct $name:ident => $display:expr,
enablers: $enablers:expr
$(, entry_patterns: $entry:expr)?
$(, config_patterns: $config:expr)?
$(, always_used: $always:expr)?
$(, tooling_dependencies: $tooling:expr)?
$(, fixture_glob_patterns: $fixtures:expr)?
$(, discovery_hidden_dirs: $hidden_dirs:expr)?
$(, virtual_module_prefixes: $virtual:expr)?
$(, virtual_package_suffixes: $virtual_suffixes:expr)?
$(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
$(, provided_dependencies: $provided_dependencies:expr)?
$(, package_json_config_key: $pkg_key:expr)?
$(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
, resolve_config($cp:ident, $src:ident, $root:ident) $body:block
$(,)?
) => {
pub struct $name;
impl Plugin for $name {
fn name(&self) -> &'static str {
$display
}
fn enablers(&self) -> &'static [&'static str] {
$enablers
}
$( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
$( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
$( fn always_used(&self) -> &'static [&'static str] { $always } )?
$( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
$( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
$( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
$( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
$( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
$( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
$( fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> { $provided_dependencies } )?
$(
fn package_json_config_key(&self) -> Option<&'static str> {
Some($pkg_key)
}
)?
$(
fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
vec![$( ($pat, $exports) ),*]
}
)?
fn resolve_config(
&self,
$cp: &std::path::Path,
$src: &str,
$root: &std::path::Path,
) -> PluginResult
$body
}
};
(
struct $name:ident => $display:expr,
enablers: $enablers:expr
$(, entry_patterns: $entry:expr)?
$(, config_patterns: $config:expr)?
$(, always_used: $always:expr)?
$(, tooling_dependencies: $tooling:expr)?
$(, fixture_glob_patterns: $fixtures:expr)?
$(, discovery_hidden_dirs: $hidden_dirs:expr)?
$(, virtual_module_prefixes: $virtual:expr)?
$(, virtual_package_suffixes: $virtual_suffixes:expr)?
$(, generated_type_import_prefixes: $generated_type_prefixes:expr)?
$(, provided_dependencies: $provided_dependencies:expr)?
$(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
$(,)?
) => {
pub struct $name;
impl Plugin for $name {
fn name(&self) -> &'static str {
$display
}
fn enablers(&self) -> &'static [&'static str] {
$enablers
}
$( fn entry_patterns(&self) -> &'static [&'static str] { $entry } )?
$( fn config_patterns(&self) -> &'static [&'static str] { $config } )?
$( fn always_used(&self) -> &'static [&'static str] { $always } )?
$( fn tooling_dependencies(&self) -> &'static [&'static str] { $tooling } )?
$( fn fixture_glob_patterns(&self) -> &'static [&'static str] { $fixtures } )?
$( fn discovery_hidden_dirs(&self) -> &'static [&'static str] { $hidden_dirs } )?
$( fn virtual_module_prefixes(&self) -> &'static [&'static str] { $virtual } )?
$( fn virtual_package_suffixes(&self) -> &'static [&'static str] { $virtual_suffixes } )?
$( fn generated_type_import_prefixes(&self) -> &'static [&'static str] { $generated_type_prefixes } )?
$( fn provided_dependencies(&self) -> Vec<ProvidedDependencyRule> { $provided_dependencies } )?
$(
fn used_exports(&self) -> Vec<(&'static str, &'static [&'static str])> {
vec![$( ($pat, $exports) ),*]
}
)?
}
};
}
pub mod config_parser;
mod config_value_credits;
mod manifest;
pub mod manifest_entries;
pub mod registry;
mod tooling;
pub(crate) use module_federation::runtime_remotes;
pub use registry::{AggregatedPluginResult, PluginRegistry};
pub(crate) use tooling::is_known_tooling_dependency;
fn add_import_referenced_dependencies(result: &mut PluginResult, source: &str, config_path: &Path) {
let imports = config_parser::extract_imports(source, config_path);
for import in &imports {
result
.referenced_dependencies
.push(crate::resolve::extract_package_name(import));
}
}
fn credit_environment_optional_peers(environment: &str, result: &mut PluginResult) {
credit_config_value(
config_value_credits::CreditSurface::TestEnvironmentOptionalPeer,
canonical_test_environment(environment),
result,
);
}
fn credit_config_value(
surface: config_value_credits::CreditSurface,
value: &str,
result: &mut PluginResult,
) -> bool {
let Some(packages) = config_value_credits::credited_packages(surface, value) else {
return false;
};
result
.referenced_dependencies
.extend(packages.iter().cloned());
true
}
fn canonical_test_environment(environment: &str) -> &str {
environment
.strip_prefix("jest-environment-")
.or_else(|| environment.strip_prefix("vitest-environment-"))
.unwrap_or(environment)
}
mod adonis;
mod angular;
mod astro;
mod ava;
mod babel;
mod biome;
mod browser_extension;
mod bun;
mod c8;
mod capacitor;
mod changesets;
mod commit_and_tag_version;
mod commitizen;
mod commitlint;
mod content_collections;
mod contentlayer;
mod convex;
mod cspell;
mod cucumber;
mod cypress;
mod danger;
mod deno;
mod dependency_cruiser;
mod docusaurus;
mod drizzle;
mod electron;
mod ember;
mod eslint;
mod expo;
mod expo_router;
mod firebase;
mod fumadocs;
mod gatsby;
mod graphql_codegen;
mod hardhat;
mod husky;
mod i18next;
mod ionic;
mod jest;
mod k6;
mod karma;
mod knex;
mod kysely;
mod lefthook;
mod lexical;
mod lint_staged;
mod lit;
mod markdownlint;
mod mintlify;
mod mocha;
mod module_federation;
mod msw;
mod napi_rs;
mod nestjs;
mod next_intl;
mod nextjs;
mod nitro;
mod nodemon;
pub(crate) mod nuxt;
mod nx;
mod nyc;
mod obsidian;
mod openapi_ts;
mod opencode;
mod opennext_cloudflare;
mod oxfmt;
mod oxlint;
mod pandacss;
mod parcel;
mod pinia;
mod pkg_utils;
mod playwright;
mod plop;
mod pm2;
mod pnpm;
mod postcss;
mod prettier;
mod prisma;
mod qwik;
mod react_compiler;
mod react_native;
mod react_router;
mod redwoodsdk;
mod relay;
mod remark;
mod remix;
mod rolldown;
mod rollup;
mod rsbuild;
mod rspack;
mod rspress;
mod sanity;
mod semantic_release;
mod sentry;
mod simple_git_hooks;
mod size_limit;
mod storybook;
mod stryker;
mod stylelint;
mod supabase;
mod sveltekit;
mod svgo;
mod svgr;
mod swc;
mod syncpack;
mod tailwind;
mod tanstack_router;
mod tap;
mod test_alias;
mod tsd;
mod tsdown;
mod tsup;
mod turborepo;
mod typedoc;
mod typeorm;
mod typescript;
mod unocss;
mod varlock;
mod velite;
mod vercel;
mod vite;
mod vitepress;
mod vitest;
mod vscode;
mod waku;
mod webdriverio;
mod webpack;
mod wrangler;
mod wuchale;
mod wxt;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn segment_regexes_split_on_the_native_separator() {
let regexes = vec![Regex::new("^_(components|hooks)$").expect("valid regex")];
assert!(matches_segment_regex(
"src/pages/_components/a.tsx",
®exes
));
assert!(!matches_segment_regex(
"src/pages/components/a.tsx",
®exes
));
assert_eq!(
matches_segment_regex("src\\pages\\_components\\a.tsx", ®exes),
cfg!(windows),
"a backslash separates segments only where it is the native separator"
);
}
use std::path::Path;
#[test]
fn is_enabled_with_deps_exact_match() {
let plugin = nextjs::NextJsPlugin;
let deps = vec!["next".to_string()];
assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
}
#[test]
fn is_enabled_with_deps_no_match() {
let plugin = nextjs::NextJsPlugin;
let deps = vec!["react".to_string()];
assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
}
#[test]
fn is_enabled_with_deps_empty_deps() {
let plugin = nextjs::NextJsPlugin;
let deps: Vec<String> = vec![];
assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
}
#[test]
fn environment_optional_peers_come_from_the_credit_catalogue() {
for environment in [
"jsdom",
"jest-environment-jsdom",
"vitest-environment-jsdom",
] {
let mut result = PluginResult::default();
credit_environment_optional_peers(environment, &mut result);
assert_eq!(
result.referenced_dependencies,
vec!["canvas".to_string()],
"expected the catalogue credit for {environment}"
);
}
}
#[test]
fn environment_without_a_catalogue_row_credits_nothing() {
let mut result = PluginResult::default();
credit_environment_optional_peers("happy-dom", &mut result);
assert!(result.referenced_dependencies.is_empty());
}
#[test]
fn entry_point_role_defaults_are_centralized() {
assert_eq!(vite::VitePlugin.entry_point_role(), EntryPointRole::Runtime);
assert_eq!(
vitest::VitestPlugin.entry_point_role(),
EntryPointRole::Test
);
assert_eq!(
storybook::StorybookPlugin.entry_point_role(),
EntryPointRole::Support
);
assert_eq!(
obsidian::ObsidianPlugin.entry_point_role(),
EntryPointRole::Runtime
);
assert_eq!(knex::KnexPlugin.entry_point_role(), EntryPointRole::Support);
}
#[test]
fn plugins_with_entry_patterns_have_explicit_role_intent() {
let runtime_or_test_or_support: rustc_hash::FxHashSet<&'static str> =
TEST_ENTRY_POINT_PLUGINS
.iter()
.chain(RUNTIME_ENTRY_POINT_PLUGINS.iter())
.chain(SUPPORT_ENTRY_POINT_PLUGINS.iter())
.copied()
.collect();
for plugin in crate::plugins::registry::builtin::create_builtin_plugins() {
if plugin.entry_patterns().is_empty() {
continue;
}
assert!(
runtime_or_test_or_support.contains(plugin.name()),
"plugin '{}' exposes entry patterns but is missing from the entry-point role map",
plugin.name()
);
}
}
#[test]
fn plugin_result_is_empty_only_when_every_field_is_empty() {
type Fill = fn(&mut PluginResult);
assert!(PluginResult::default().is_empty());
let rows: [(&str, Fill); 15] = [
("entry_patterns", |r| {
r.entry_patterns.push(PathRule::new("src/*.ts"));
}),
("used_exports", |r| {
r.used_exports
.push(UsedExportRule::new("src/*.ts", ["default"]));
}),
("used_class_members", |r| {
r.used_class_members
.push(UsedClassMemberRule::from("render"));
}),
("referenced_dependencies", |r| {
r.referenced_dependencies.push("lodash".to_string());
}),
("package_referenced_dependencies", |r| {
r.package_referenced_dependencies
.push((PathBuf::from("/project/pkg"), "lodash".to_string()));
}),
("always_used_files", |r| {
r.always_used_files.push("**/*.stories.tsx".to_string());
}),
("path_aliases", |r| {
r.path_aliases.push(("@".to_string(), "src".to_string()));
}),
("setup_files", |r| {
r.setup_files.push(PathBuf::from("/setup.ts"));
}),
("fixture_patterns", |r| {
r.fixture_patterns.push("**/__fixtures__/**/*".to_string());
}),
("scss_include_paths", |r| {
r.scss_include_paths.push(PathBuf::from("/project/styles"));
}),
("static_dir_mappings", |r| {
r.static_dir_mappings
.push((PathBuf::from("/project/public"), "/".to_string()));
}),
("framework_static_dir_mappings", |r| {
r.framework_static_dir_mappings
.push((PathBuf::from("/project/static"), "/".to_string()));
}),
("provided_dependencies", |r| {
r.provided_dependencies.push(ProvidedDependencyRule::new(
"**/*.stories.tsx",
["react"],
Vec::<String>::new(),
));
}),
("config_diagnostics", |r| {
r.config_diagnostics
.push(PluginConfigDiagnostic::unreadable(
Path::new("/project/webpack.config.js"),
"webpack",
"exposes",
"dynamic-value",
));
}),
("federation_sources", |r| {
r.federation_sources.push(FederationSource {
target: FederationSourceTarget::Remote("app".to_string()),
config_path: PathBuf::from("/project/webpack.config.js"),
plugin: "webpack".to_string(),
key: "remotes",
});
}),
];
for (field, fill) in rows {
let mut result = PluginResult::default();
fill(&mut result);
assert!(
!result.is_empty(),
"a result with only {field} set must not be empty"
);
}
}
#[test]
fn is_enabled_with_deps_prefix_match() {
let plugin = storybook::StorybookPlugin;
let deps = vec!["@storybook/react".to_string()];
assert!(plugin.is_enabled_with_deps(&deps, Path::new("/project")));
}
#[test]
fn is_enabled_with_deps_prefix_no_match_without_slash() {
let plugin = storybook::StorybookPlugin;
let deps = vec!["@storybookish".to_string()];
assert!(!plugin.is_enabled_with_deps(&deps, Path::new("/project")));
}
#[test]
fn is_enabled_with_deps_multiple_enablers() {
let plugin = vitest::VitestPlugin;
let deps_vitest = vec!["vitest".to_string()];
let deps_none = vec!["mocha".to_string()];
assert!(plugin.is_enabled_with_deps(&deps_vitest, Path::new("/project")));
assert!(!plugin.is_enabled_with_deps(&deps_none, Path::new("/project")));
}
#[test]
fn plugin_resolve_config_default_returns_empty() {
let plugin = commitizen::CommitizenPlugin;
let result = plugin.resolve_config(
Path::new("/project/config.js"),
"const x = 1;",
Path::new("/project"),
);
assert!(result.is_empty());
}
#[test]
fn is_enabled_with_deps_exact_and_prefix_both_work() {
let plugin = storybook::StorybookPlugin;
let deps_exact = vec!["storybook".to_string()];
assert!(plugin.is_enabled_with_deps(&deps_exact, Path::new("/project")));
let deps_prefix = vec!["@storybook/vue3".to_string()];
assert!(plugin.is_enabled_with_deps(&deps_prefix, Path::new("/project")));
}
#[test]
fn is_enabled_with_deps_multiple_enablers_remix() {
let plugin = remix::RemixPlugin;
let deps_node = vec!["@remix-run/node".to_string()];
assert!(plugin.is_enabled_with_deps(&deps_node, Path::new("/project")));
let deps_react = vec!["@remix-run/react".to_string()];
assert!(plugin.is_enabled_with_deps(&deps_react, Path::new("/project")));
let deps_cf = vec!["@remix-run/cloudflare".to_string()];
assert!(plugin.is_enabled_with_deps(&deps_cf, Path::new("/project")));
}
struct MinimalPlugin;
impl Plugin for MinimalPlugin {
fn name(&self) -> &'static str {
"minimal"
}
}
#[test]
fn default_resolve_config_returns_empty() {
let r = MinimalPlugin.resolve_config(
Path::new("config.js"),
"export default {}",
Path::new("/"),
);
assert!(r.is_empty());
}
#[test]
fn default_package_json_metadata_hooks_are_empty() {
let pkg = PackageJson::default();
assert!(!MinimalPlugin.is_enabled_with_package_json(&pkg, Path::new("/")));
assert!(
MinimalPlugin
.resolve_package_json(&pkg, Path::new("/"))
.is_empty()
);
}
#[test]
fn default_is_enabled_returns_false_when_no_enablers() {
let deps = vec!["anything".to_string()];
assert!(!MinimalPlugin.is_enabled_with_deps(&deps, Path::new("/")));
}
#[test]
fn all_builtin_plugin_names_are_non_empty_and_unique() {
let plugins = registry::builtin::create_builtin_plugins();
let mut seen = std::collections::BTreeSet::new();
for p in &plugins {
let name = p.name();
assert!(
!name.is_empty(),
"builtin plugins must have a non-empty name"
);
assert!(seen.insert(name), "duplicate plugin name: {name}");
}
}
#[test]
fn all_builtin_plugins_have_activation_signals() {
const NON_DEPENDENCY_ACTIVATED_PLUGINS: &[&str] = &["napi-rs", "deno"];
let plugins = registry::builtin::create_builtin_plugins();
for p in &plugins {
assert!(
!p.enablers().is_empty()
|| !p.script_enablers().is_empty()
|| NON_DEPENDENCY_ACTIVATED_PLUGINS.contains(&p.name()),
"plugin '{}' has no activation signal",
p.name()
);
}
}
#[test]
fn plugins_with_config_patterns_have_always_used() {
let plugins = registry::builtin::create_builtin_plugins();
for p in &plugins {
if !p.config_patterns().is_empty() {
assert!(
!p.always_used().is_empty(),
"plugin '{}' has config_patterns but no always_used",
p.name()
);
}
}
}
#[test]
fn framework_plugins_enablers() {
let cases: Vec<(&dyn Plugin, &[&str])> = vec![
(&nextjs::NextJsPlugin, &["next"]),
(&nuxt::NuxtPlugin, &["nuxt"]),
(&angular::AngularPlugin, &["@angular/core"]),
(&ionic::IonicPlugin, &["@ionic/angular"]),
(&sveltekit::SvelteKitPlugin, &["@sveltejs/kit"]),
(&gatsby::GatsbyPlugin, &["gatsby"]),
];
for (plugin, expected_enablers) in cases {
let enablers = plugin.enablers();
for expected in expected_enablers {
assert!(
enablers.contains(expected),
"plugin '{}' should have '{}'",
plugin.name(),
expected
);
}
}
}
#[test]
fn testing_plugins_enablers() {
let cases: Vec<(&dyn Plugin, &str)> = vec![
(&jest::JestPlugin, "jest"),
(&vitest::VitestPlugin, "vitest"),
(&playwright::PlaywrightPlugin, "@playwright/test"),
(&cypress::CypressPlugin, "cypress"),
(&mocha::MochaPlugin, "mocha"),
(&stryker::StrykerPlugin, "@stryker-mutator/core"),
];
for (plugin, enabler) in cases {
assert!(
plugin.enablers().contains(&enabler),
"plugin '{}' should have '{}'",
plugin.name(),
enabler
);
}
}
#[test]
fn bundler_plugins_enablers() {
let cases: Vec<(&dyn Plugin, &str)> = vec![
(&vite::VitePlugin, "vite"),
(&webpack::WebpackPlugin, "webpack"),
(&rollup::RollupPlugin, "rollup"),
];
for (plugin, enabler) in cases {
assert!(
plugin.enablers().contains(&enabler),
"plugin '{}' should have '{}'",
plugin.name(),
enabler
);
}
}
#[test]
fn test_plugins_have_test_entry_patterns() {
let test_plugins: Vec<&dyn Plugin> = vec![
&bun::BunPlugin,
&deno::DenoPlugin,
&jest::JestPlugin,
&vitest::VitestPlugin,
&mocha::MochaPlugin,
&tap::TapPlugin,
&tsd::TsdPlugin,
];
for plugin in test_plugins {
let patterns = plugin.entry_patterns();
assert!(
!patterns.is_empty(),
"test plugin '{}' should have entry patterns",
plugin.name()
);
assert!(
patterns
.iter()
.any(|p| p.contains("test") || p.contains("spec") || p.contains("__tests__")),
"test plugin '{}' should have test/spec patterns",
plugin.name()
);
}
}
#[test]
fn framework_plugins_have_entry_patterns() {
let plugins: Vec<&dyn Plugin> = vec![
&nextjs::NextJsPlugin,
&nuxt::NuxtPlugin,
&angular::AngularPlugin,
&sveltekit::SvelteKitPlugin,
];
for plugin in plugins {
assert!(
!plugin.entry_patterns().is_empty(),
"framework plugin '{}' should have entry patterns",
plugin.name()
);
}
}
#[test]
fn plugins_with_resolve_config_have_config_patterns() {
let plugins: Vec<&dyn Plugin> = vec![
&jest::JestPlugin,
&vitest::VitestPlugin,
&babel::BabelPlugin,
&eslint::EslintPlugin,
&webpack::WebpackPlugin,
&storybook::StorybookPlugin,
&typescript::TypeScriptPlugin,
&postcss::PostCssPlugin,
&nextjs::NextJsPlugin,
&nuxt::NuxtPlugin,
&angular::AngularPlugin,
&nx::NxPlugin,
&stryker::StrykerPlugin,
&wuchale::WuchalePlugin,
&rollup::RollupPlugin,
&sveltekit::SvelteKitPlugin,
&prettier::PrettierPlugin,
&contentlayer::ContentlayerPlugin,
];
for plugin in plugins {
assert!(
!plugin.config_patterns().is_empty(),
"plugin '{}' with resolve_config should have config_patterns",
plugin.name()
);
}
}
#[test]
fn plugin_tooling_deps_include_enabler_package() {
let plugins: Vec<&dyn Plugin> = vec![
&jest::JestPlugin,
&vitest::VitestPlugin,
&webpack::WebpackPlugin,
&typescript::TypeScriptPlugin,
&eslint::EslintPlugin,
&prettier::PrettierPlugin,
&danger::DangerPlugin,
&stryker::StrykerPlugin,
&wuchale::WuchalePlugin,
&contentlayer::ContentlayerPlugin,
];
for plugin in plugins {
let tooling = plugin.tooling_dependencies();
let enablers = plugin.enablers();
assert!(
enablers
.iter()
.any(|e| !e.ends_with('/') && tooling.contains(e)),
"plugin '{}': at least one non-prefix enabler should be in tooling_dependencies",
plugin.name()
);
}
}
#[test]
fn nextjs_has_used_exports_for_pages() {
let plugin = nextjs::NextJsPlugin;
let exports = plugin.used_exports();
assert!(!exports.is_empty());
assert!(exports.iter().any(|(_, names)| names.contains(&"default")));
}
#[test]
fn remix_has_used_exports_for_routes() {
let plugin = remix::RemixPlugin;
let exports = plugin.used_exports();
assert!(!exports.is_empty());
let route_entry = exports.iter().find(|(pat, _)| pat.contains("routes"));
assert!(route_entry.is_some());
let (_, names) = route_entry.unwrap();
assert!(names.contains(&"loader"));
assert!(names.contains(&"action"));
assert!(names.contains(&"default"));
}
#[test]
fn sveltekit_has_used_exports_for_routes() {
let plugin = sveltekit::SvelteKitPlugin;
let exports = plugin.used_exports();
assert!(!exports.is_empty());
assert!(exports.iter().any(|(_, names)| names.contains(&"GET")));
}
#[test]
fn nuxt_has_hash_virtual_prefix() {
assert!(nuxt::NuxtPlugin.virtual_module_prefixes().contains(&"#"));
}
#[test]
fn sveltekit_has_dollar_virtual_prefixes() {
let prefixes = sveltekit::SvelteKitPlugin.virtual_module_prefixes();
assert!(prefixes.contains(&"$app/"));
assert!(prefixes.contains(&"$env/"));
assert!(prefixes.contains(&"$lib/"));
}
#[test]
fn sveltekit_has_lib_path_alias() {
let aliases = sveltekit::SvelteKitPlugin.path_aliases(Path::new("/project"));
assert!(aliases.iter().any(|(prefix, _)| *prefix == "$lib/"));
}
#[test]
fn nuxt_has_tilde_path_alias() {
let aliases = nuxt::NuxtPlugin.path_aliases(Path::new("/nonexistent"));
assert!(aliases.iter().any(|(prefix, _)| *prefix == "~/"));
assert!(aliases.iter().any(|(prefix, _)| *prefix == "~~/"));
}
#[test]
fn jest_has_package_json_config_key() {
assert_eq!(jest::JestPlugin.package_json_config_key(), Some("jest"));
}
#[test]
fn tsd_has_package_json_config_key() {
assert_eq!(tsd::TsdPlugin.package_json_config_key(), Some("tsd"));
}
#[test]
fn babel_has_package_json_config_key() {
assert_eq!(babel::BabelPlugin.package_json_config_key(), Some("babel"));
}
#[test]
fn eslint_has_package_json_config_key() {
assert_eq!(
eslint::EslintPlugin.package_json_config_key(),
Some("eslintConfig")
);
}
#[test]
fn prettier_has_package_json_config_key() {
assert_eq!(
prettier::PrettierPlugin.package_json_config_key(),
Some("prettier")
);
}
#[test]
fn macro_generated_plugin_basic_properties() {
let plugin = msw::MswPlugin;
assert_eq!(plugin.name(), "msw");
assert!(plugin.enablers().contains(&"msw"));
assert!(!plugin.entry_patterns().is_empty());
assert!(plugin.config_patterns().is_empty());
assert!(!plugin.always_used().is_empty());
assert!(!plugin.tooling_dependencies().is_empty());
}
#[test]
fn macro_generated_plugin_with_used_exports() {
let plugin = remix::RemixPlugin;
assert_eq!(plugin.name(), "remix");
assert!(!plugin.used_exports().is_empty());
}
#[test]
fn macro_passes_through_virtual_package_suffixes() {
define_plugin! {
struct MacroSuffixSmokePlugin => "macro-suffix-smoke",
enablers: &["macro-suffix-smoke"],
virtual_package_suffixes: &["/__macro_smoke__"],
}
let plugin = MacroSuffixSmokePlugin;
assert_eq!(
plugin.virtual_package_suffixes(),
&["/__macro_smoke__"],
"macro-declared virtual_package_suffixes must propagate to the trait method"
);
}
#[test]
fn macro_generated_plugin_imports_only_resolve_config() {
let plugin = cypress::CypressPlugin;
let source = r"
import { defineConfig } from 'cypress';
import coveragePlugin from '@cypress/code-coverage';
export default defineConfig({});
";
let result = plugin.resolve_config(
Path::new("cypress.config.ts"),
source,
Path::new("/project"),
);
assert!(
result
.referenced_dependencies
.contains(&"cypress".to_string())
);
assert!(
result
.referenced_dependencies
.contains(&"@cypress/code-coverage".to_string())
);
}
#[test]
fn builtin_plugin_count_is_expected() {
let plugins = registry::builtin::create_builtin_plugins();
assert!(
plugins.len() >= 110,
"expected at least 110 built-in plugins, got {}",
plugins.len()
);
}
#[test]
fn a_parent_relative_pattern_resolves_against_the_workspace_prefix() {
let parent_relative = |pattern: &str| {
let mut rule = PathRule::new(pattern);
rule.parent_relative = true;
rule
};
assert_eq!(
parent_relative("../shared/src/Thing.tsx")
.prefixed("packages/app")
.pattern,
"packages/shared/src/Thing.tsx"
);
assert_eq!(
parent_relative("../../lib/index.{ts,js}")
.prefixed("apps/web/client")
.pattern,
"apps/lib/index.{ts,js}"
);
assert!(
parent_relative("../../../outside/Thing.tsx")
.prefixed("packages/app")
.pattern
.starts_with("../"),
"a climb past the project root matches no project file"
);
assert_eq!(
parent_relative("src/index.ts")
.prefixed("packages/app")
.pattern,
"packages/app/src/index.ts"
);
}
#[test]
fn a_parent_relative_pattern_resolves_against_a_backslash_prefix() {
let mut rule = PathRule::new("../../packages/ui/src/**/*.mdx");
rule.parent_relative = true;
assert_eq!(
rule.prefixed("apps\\docs").pattern,
"packages/ui/src/**/*.mdx"
);
}
#[test]
fn a_plain_parent_pattern_is_not_resolved() {
assert_eq!(
PathRule::new("../src/**/*.stories.tsx")
.prefixed("packages/ui")
.pattern,
"packages/ui/../src/**/*.stories.tsx"
);
}
}