use std::path::{Path, PathBuf};
use fallow_config::{
AutoImportRule, EntryPointRole, JsxImportSourceRule, PackageJson, UsedClassMemberRule,
};
use fallow_types::semantic::SemanticFrameworkContract;
use regex::Regex;
const TEST_ENTRY_POINT_PLUGINS: &[&str] = &[
"ava",
"bun",
"deno",
"cucumber",
"cypress",
"jasmine",
"jest",
"k6",
"mocha",
"playwright",
"rstest",
"tap",
"tsd",
"vitest",
"webdriverio",
];
const RUNTIME_ENTRY_POINT_PLUGINS: &[&str] = &[
"adonis",
"angular",
"astro",
"browser-extension",
"convex",
"docusaurus",
"egg",
"electron",
"ember",
"eve",
"expo",
"expo-router",
"flue",
"gatsby",
"hardhat",
"kibana",
"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",
"vercel",
"vite",
"vite-plus",
"vitepress",
"waku",
"webpack",
"wrangler",
"wxt",
];
#[cfg(test)]
pub(crate) fn entry_rules_match(result: &PluginResult, path: &str) -> bool {
result
.entry_patterns
.iter()
.filter_map(|rule| CompiledPathRule::for_entry_rule(rule, "entry pattern"))
.any(|rule| rule.matches(path))
}
#[cfg(test)]
const SUPPORT_ENTRY_POINT_PLUGINS: &[&str] = &[
"alchemy",
"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,
accumulate_config_entry_patterns: bool,
replace_used_export_rules: bool,
used_exports: Vec<UsedExportRule>,
entry_export_credits: 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>,
jsx_import_sources: Vec<JsxImportSourceRule>,
jsx_package_credits: Vec<JsxImportSourceRule>,
tsconfig_jsx_dirs: Vec<PathBuf>,
}
#[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
}
pub const CONFIG_ONLY_EXPORT_NAMES: &[&str] = &[
"dynamic",
"dynamicParams",
"revalidate",
"fetchCache",
"runtime",
"preferredRegion",
"maxDuration",
"prerender",
"config",
"metadata",
"viewport",
];
pub fn push_tooling_trace_credits(
provenance: &mut fallow_types::trace::TraceProvenance,
root: &Path,
workspaces: &[fallow_config::WorkspaceInfo],
plugin_result: &AggregatedPluginResult,
) {
for entry in &plugin_result.plugin_tooling {
let Some(evidence) = entry.evidence(&plugin_result.script_used_packages) else {
continue;
};
let credit = match evidence {
PluginToolingEvidence::OwnConfig(config) => fallow_types::trace::ToolingCredit {
reason: "plugin-config".to_owned(),
plugin: Some(entry.plugin.clone()),
config: Some(config.strip_prefix(root).unwrap_or(config).to_path_buf()),
reference: None,
},
PluginToolingEvidence::Reference(name) => fallow_types::trace::ToolingCredit {
reason: "plugin-reference".to_owned(),
plugin: Some(entry.plugin.clone()),
config: None,
reference: Some(name.to_owned()),
},
};
for dependency in &entry.dependencies {
provenance.push_tooling_credit(dependency.clone(), credit.clone());
}
}
push_types_trace_credits(provenance, plugin_result);
push_catalogue_trace_credits(provenance, root, workspaces, plugin_result);
}
fn push_catalogue_trace_credits(
provenance: &mut fallow_types::trace::TraceProvenance,
root: &Path,
workspaces: &[fallow_config::WorkspaceInfo],
plugin_result: &AggregatedPluginResult,
) {
let manifests = read_trace_manifests(root, workspaces);
let mut names: Vec<&String> = plugin_result.dev_dependency_names.iter().collect();
names.sort_unstable();
for name in names {
let declaring: Vec<&(PathBuf, serde_json::Value)> = manifests
.iter()
.filter(|(_, manifest)| manifest_lists(manifest, "devDependencies", name))
.collect();
if let Some(credit) = catalogue_credit(name, root, &declaring) {
provenance.push_tooling_credit(name.clone(), credit);
}
if let Some((path, _)) = declaring
.iter()
.find(|(_, manifest)| manifest_lists(manifest, "peerDependencies", name))
{
provenance.push_tooling_credit(
name.clone(),
fallow_types::trace::ToolingCredit {
reason: "own-peer".to_owned(),
plugin: None,
config: Some(path.strip_prefix(root).unwrap_or(path).to_path_buf()),
reference: None,
},
);
}
}
}
fn read_trace_manifests(
root: &Path,
workspaces: &[fallow_config::WorkspaceInfo],
) -> Vec<(PathBuf, serde_json::Value)> {
std::iter::once(root)
.chain(
workspaces
.iter()
.map(|ws| ws.root.as_path())
.filter(|ws_root| *ws_root != root),
)
.filter_map(|package_root| {
let path = package_root.join("package.json");
let content = std::fs::read_to_string(&path).ok()?;
serde_json::from_str(&content)
.ok()
.map(|value| (path, value))
})
.collect()
}
fn manifest_lists(manifest: &serde_json::Value, section: &str, name: &str) -> bool {
manifest
.get(section)
.and_then(serde_json::Value::as_object)
.is_some_and(|deps| deps.contains_key(name))
}
fn catalogue_credit(
name: &str,
root: &Path,
declaring: &[&(PathBuf, serde_json::Value)],
) -> Option<fallow_types::trace::ToolingCredit> {
if is_ambient_types_package(name) || types_package_target(name).is_some() {
return None;
}
let Some(patterns) = cli_tooling_config_patterns(name) else {
return is_known_tooling_dependency(name).then(|| fallow_types::trace::ToolingCredit {
reason: "known-tooling".to_owned(),
plugin: None,
config: None,
reference: None,
});
};
let config = declaring.iter().find_map(|(path, manifest)| {
crate::analyze::cli_tool_own_config(name, patterns, root, path, Some(manifest))
})?;
Some(fallow_types::trace::ToolingCredit {
reason: "known-tooling-config".to_owned(),
plugin: None,
config: Some(config.strip_prefix(root).unwrap_or(&config).to_path_buf()),
reference: None,
})
}
fn push_types_trace_credits(
provenance: &mut fallow_types::trace::TraceProvenance,
plugin_result: &AggregatedPluginResult,
) {
let declared = plugin_result.dependency_binaries.declared_packages();
let mut names: Vec<&String> = plugin_result.dev_dependency_names.iter().collect();
names.sort_unstable();
for name in names {
let credit = if is_ambient_types_package(name) {
fallow_types::trace::ToolingCredit {
reason: "ambient-types".to_owned(),
plugin: None,
config: None,
reference: None,
}
} else if let Some(target) = types_package_target(name) {
if declared.contains(&target) {
fallow_types::trace::ToolingCredit {
reason: "types-target".to_owned(),
plugin: None,
config: None,
reference: Some(target),
}
} else if plugin_result.referenced_dependencies.contains(name) {
fallow_types::trace::ToolingCredit {
reason: "types-config".to_owned(),
plugin: None,
config: None,
reference: None,
}
} else {
continue;
}
} else {
continue;
};
provenance.push_tooling_credit(name.clone(), credit);
}
}
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 extend_config_dir_entry_patterns(
&mut self,
values: Vec<String>,
config_path: &Path,
root: &Path,
) {
let (negated, positive): (Vec<String>, Vec<String>) =
values.into_iter().partition(|value| value.starts_with('!'));
let excluded: Vec<String> = negated
.iter()
.filter_map(|value| value.strip_prefix('!'))
.filter_map(|value| config_parser::normalize_config_path(value, config_path, root))
.collect();
self.entry_patterns.extend(
positive
.iter()
.filter_map(|value| config_parser::normalize_config_path(value, config_path, root))
.map(|pattern| {
PathRule::new(pattern).with_excluded_globs(excluded.iter().cloned())
}),
);
}
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}"));
}
#[must_use]
const fn is_empty(&self) -> bool {
self.config_diagnostics.is_empty()
&& self.entry_patterns.is_empty()
&& self.used_exports.is_empty()
&& self.entry_export_credits.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()
&& self.jsx_import_sources.is_empty()
&& self.jsx_package_credits.is_empty()
&& self.tsconfig_jsx_dirs.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, Hash)]
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 from_extglob(pattern: &str) -> Self {
let rewritten = fallow_config::extglob::rewrite_include(pattern);
Self::new(rewritten.include).with_excluded_globs(rewritten.excludes)
}
#[must_use]
pub(crate) fn with_excluded_extglobs<I, S>(self, patterns: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let excluded: Vec<String> = patterns
.into_iter()
.filter_map(|pattern| fallow_config::extglob::rewrite_exclude(pattern.as_ref()))
.collect();
self.with_excluded_globs(excluded)
}
#[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 prefix = |pattern: &str| {
if self.parent_relative && pattern.starts_with("../") {
resolve_parent_relative_pattern(pattern, ws_prefix)
} else {
prefix_workspace_pattern(pattern, ws_prefix)
}
};
Self {
pattern: prefix(&self.pattern),
exclude_globs: self
.exclude_globs
.iter()
.map(|pattern| prefix(pattern))
.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 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))
}
}
pub(crate) fn entry_rule_glob(pattern: &str) -> Result<globset::Glob, globset::Error> {
globset::GlobBuilder::new(pattern)
.literal_separator(true)
.empty_alternates(true)
.build()
}
#[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 entry_rule_glob(&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,
),
})
}
pub(crate) fn for_used_export_rule(rule: &PathRule, rule_kind: &str) -> Option<Self> {
Self::for_entry_rule(rule, rule_kind)
}
#[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)
.empty_alternates(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 config_default_export_opt_out(&self) -> &'static [&'static str] {
&[]
}
fn config_file_exports(&self) -> &'static [(&'static str, &'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 resolve_config_in_project(
&self,
config_path: &Path,
source: &str,
root: &Path,
_project_root: &Path,
) -> PluginResult {
self.resolve_config(config_path, source, root)
}
fn resolve_script_config(
&self,
config_path: &Path,
source: &str,
root: &Path,
project_root: &Path,
) -> PluginResult {
self.resolve_config_in_project(config_path, source, root, project_root)
}
fn script_config_binaries(&self) -> &'static [&'static str] {
&[]
}
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)?
$(, config_default_export_opt_out: $config_opt_out:expr)?
$(, config_file_exports: $config_exports: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 config_default_export_opt_out(&self) -> &'static [&'static str] { $config_opt_out } )?
$( fn config_file_exports(&self) -> &'static [(&'static str, &'static [&'static str])] { $config_exports } )?
$( 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)?
$(, config_default_export_opt_out: $config_opt_out:expr)?
$(, config_file_exports: $config_exports: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)?
$(, script_config_binaries: $script_binaries:expr)?
$(, used_exports: [$( ($pat:expr, $exports:expr) ),* $(,)?])?
, resolve_config($cp:ident, $src:ident, $root:ident) $body:block
$(, resolve_config_in_project($pcp:ident, $psrc:ident, $proot:ident, $pproj:ident) $pbody: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 config_default_export_opt_out(&self) -> &'static [&'static str] { $config_opt_out } )?
$( fn config_file_exports(&self) -> &'static [(&'static str, &'static [&'static str])] { $config_exports } )?
$( 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 script_config_binaries(&self) -> &'static [&'static str] { $script_binaries } )?
fn resolve_config(
&self,
$cp: &std::path::Path,
$src: &str,
$root: &std::path::Path,
) -> PluginResult
$body
$(
fn resolve_config_in_project(
&self,
$pcp: &std::path::Path,
$psrc: &str,
$proot: &std::path::Path,
$pproj: &std::path::Path,
) -> PluginResult
$pbody
)?
}
};
(
struct $name:ident => $display:expr,
enablers: $enablers:expr
$(, entry_patterns: $entry:expr)?
$(, config_patterns: $config:expr)?
$(, config_default_export_opt_out: $config_opt_out:expr)?
$(, config_file_exports: $config_exports: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) ),* $(,)?])?
$(,)?
) => {
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 config_default_export_opt_out(&self) -> &'static [&'static str] { $config_opt_out } )?
$( fn config_file_exports(&self) -> &'static [(&'static str, &'static [&'static str])] { $config_exports } )?
$( 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) ),*]
}
)?
}
};
}
pub mod config_parser;
mod config_value_credits;
mod flue;
mod jasmine;
mod manifest;
pub mod manifest_entries;
mod mise;
mod osls;
mod project_glob;
mod railway;
pub mod registry;
mod rstest;
mod serverless_framework;
mod textlint;
mod tooling;
mod trace_provenance;
mod used_export_names;
mod used_export_rule;
pub(crate) use used_export_rule::ConfigExportCredit;
pub use used_export_rule::{PluginUsedExportRule, UsedExportRule};
pub(crate) use module_federation::runtime_remotes;
pub use registry::{
AggregatedPluginResult, PluginRegistry, PluginToolingDependencies, PluginToolingEvidence,
};
pub use tooling::types_package_target;
pub(crate) use tooling::{
cli_tooling_config_patterns, is_ambient_types_package, is_known_tooling_dependency,
};
pub use trace_provenance::{ExportCredits, build_trace_provenance};
pub use used_export_names::UsedExportNames;
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 ag_ui;
mod alchemy;
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 egg;
mod electron;
mod ember;
mod eslint;
mod eve;
mod expo;
mod expo_router;
mod firebase;
mod fumadocs;
mod gatsby;
mod github_action;
mod graphql_codegen;
mod hardhat;
mod husky;
mod i18next;
mod ionic;
mod jest;
mod k6;
mod karma;
mod kibana;
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 nest_commander;
mod nestjs;
mod nestjs_trpc;
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 opentelemetry;
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 release_it;
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 typescript_content_mapper;
mod unocss;
mod varlock;
mod velite;
mod vercel;
mod vite;
pub(crate) mod vite_plus;
mod vitepress;
mod vitest;
mod vscode;
mod waku;
mod webdriverio;
mod webpack;
mod wrangler;
mod wuchale;
mod wxt;
#[cfg(test)]
mod tests;