use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use globset::{Glob, GlobBuilder, GlobMatcher};
use indexmap::IndexMap;
use skif_config::{Config, IgnoreExportsUsedInFile, JsWorkspace, PackageJs};
use skif_core::FileSystem;
use skif_js_syntax::{Export, ExportKind, Registration};
use skif_workspace::{Package, PackageId, Workspace, map_ordered};
use crate::ancestors;
use crate::config_check::relative_tsconfig;
use crate::deps::member_dirs;
use crate::globs::{Globs, Names};
use crate::graph::ModuleGraph;
use crate::input::{
BuildDir, CustomElementLibrary, IncompleteCause, IncompleteEntries, IncompleteScope, InputRequest, InputSource,
PathAlias, WorkspaceInputs,
};
use crate::listed::{self, Listed};
use crate::resolve::join_normalized;
use crate::scripts::{self, ScriptBinary, ScriptFacts};
use crate::tsconfig_files;
const EXTENSIONS: &str = "{js,mjs,cjs,jsx,ts,tsx,mts,cts}";
const PROJECT_EXTENSIONS: &str = "{js,mjs,cjs,jsx,ts,tsx,mts,cts,vue,svelte,astro}";
fn default_entry() -> Vec<String> {
vec![
format!("{{index,cli,main}}.{EXTENSIONS}"),
format!("src/{{index,cli,main}}.{EXTENSIONS}"),
]
}
fn ambient_declarations() -> Vec<String> {
vec!["**/*.d.{ts,mts,cts}".to_owned()]
}
fn test_directories() -> Vec<String> {
vec![
format!("{{test,tests,e2e,cypress}}/**/*.{EXTENSIONS}"),
format!("src/{{test,tests,e2e}}/**/*.{EXTENSIONS}"),
format!("**/__mocks__/**/*.{EXTENSIONS}"),
]
}
fn default_project() -> Vec<String> {
vec![format!("**/*.{PROJECT_EXTENSIONS}")]
}
fn test_files() -> Vec<String> {
vec![
format!("**/*.{{test,spec}}.{EXTENSIONS}"),
format!("**/__tests__/**/*.{EXTENSIONS}"),
]
}
#[derive(Default)]
struct Configured {
entry: Vec<String>,
project: Vec<String>,
ignore: Vec<String>,
ignore_files: Vec<String>,
ignore_dependencies: Vec<String>,
ignore_binaries: Vec<String>,
ignore_unresolved: Vec<String>,
ignore_members: Vec<String>,
include_entry_exports: Option<bool>,
ignore_exports_used_in_file: Option<IgnoreExportsUsedInFile>,
paths: IndexMap<String, Vec<String>>,
}
fn normalize_dir(dir: &str) -> &str {
let dir = dir.trim_start_matches("./").trim_end_matches('/');
if dir.is_empty() { "." } else { dir }
}
impl Configured {
fn fold(&mut self, js: &PackageJs) {
let replace = |target: &mut Vec<String>, value: &Option<Vec<String>>| {
if let Some(value) = value {
target.clone_from(value);
}
};
replace(&mut self.entry, &js.entry);
replace(&mut self.project, &js.project);
replace(&mut self.ignore, &js.ignore);
replace(&mut self.ignore_dependencies, &js.ignore_dependencies);
replace(&mut self.ignore_binaries, &js.ignore_binaries);
replace(&mut self.ignore_unresolved, &js.ignore_unresolved);
self.ignore_unresolved
.extend(js.extend_ignore_unresolved.iter().cloned());
self.ignore.extend(js.extend_ignore.iter().cloned());
self.ignore_dependencies
.extend(js.extend_ignore_dependencies.iter().cloned());
self.ignore_binaries.extend(js.extend_ignore_binaries.iter().cloned());
}
}
impl From<&JsWorkspace> for Configured {
fn from(workspace: &JsWorkspace) -> Self {
Self {
entry: workspace.entry.clone(),
project: workspace.project.clone(),
ignore: workspace.ignore.clone(),
ignore_files: workspace.ignore_files.clone(),
ignore_dependencies: workspace.ignore_dependencies.clone(),
ignore_binaries: workspace.ignore_binaries.clone(),
ignore_unresolved: workspace.ignore_unresolved.clone(),
ignore_members: workspace.ignore_members.clone(),
include_entry_exports: workspace.include_entry_exports,
ignore_exports_used_in_file: workspace.ignore_exports_used_in_file.clone(),
paths: workspace.paths.clone(),
}
}
}
impl Configured {
fn fold_root(&mut self, root: Configured) {
let replace = |target: &mut Vec<String>, value: Vec<String>| {
if !value.is_empty() {
*target = value;
}
};
replace(&mut self.entry, root.entry);
replace(&mut self.project, root.project);
self.ignore.extend(root.ignore);
self.ignore_files.extend(root.ignore_files);
self.ignore_dependencies.extend(root.ignore_dependencies);
self.ignore_binaries.extend(root.ignore_binaries);
self.ignore_unresolved.extend(root.ignore_unresolved);
self.ignore_members.extend(root.ignore_members);
self.include_entry_exports = root.include_entry_exports.or(self.include_entry_exports);
self.ignore_exports_used_in_file = root
.ignore_exports_used_in_file
.or(self.ignore_exports_used_in_file.take());
self.paths.extend(root.paths);
}
}
fn configured(
config: &Config,
keys: &WorkspaceKeys,
name: &str,
abs_dir: &Path,
dir: &str,
is_root: bool,
) -> Configured {
let js = &config.js;
let mut user = if is_root {
let mut root = Configured {
entry: js.entry.clone(),
project: js.project.clone(),
ignore: js.ignore.clone(),
..Configured::default()
};
if let Some(table) = keys.table(&js.workspaces, ".") {
root.fold_root(Configured::from(table));
}
root
} else {
keys.table(&js.workspaces, dir)
.map(Configured::from)
.unwrap_or_default()
};
for scoped in config
.overrides
.iter()
.filter(|scoped| !scoped.js.is_empty() && scoped.applies_to(Some((name, abs_dir)), None))
{
user.fold(&scoped.js);
}
user
}
#[derive(Debug, Clone, Default)]
pub(crate) struct WorkspaceKeys {
keys: Vec<WorkspaceKey>,
}
#[derive(Debug, Clone)]
struct WorkspaceKey {
dir: String,
glob: Option<GlobMatcher>,
}
impl WorkspaceKey {
fn selects(&self, dir: &str) -> bool {
self.dir == dir || (dir != "." && self.glob.as_ref().is_some_and(|glob| glob.is_match(dir)))
}
}
impl WorkspaceKeys {
pub(crate) fn new(workspaces: &IndexMap<String, JsWorkspace>) -> Self {
let keys = workspaces
.keys()
.map(|key| {
let dir = normalize_dir(key);
let glob = dir
.contains(['*', '?', '[', '{'])
.then(|| GlobBuilder::new(dir).literal_separator(true).build().ok())
.flatten()
.map(|glob| glob.compile_matcher());
WorkspaceKey {
dir: dir.to_owned(),
glob,
}
})
.collect();
Self { keys }
}
pub(crate) fn table<'a>(
&self,
workspaces: &'a IndexMap<String, JsWorkspace>,
dir: &str,
) -> Option<&'a JsWorkspace> {
self.entry(workspaces, dir).map(|(_, workspace)| workspace)
}
pub(crate) fn key<'a>(&self, workspaces: &'a IndexMap<String, JsWorkspace>, dir: &str) -> Option<&'a String> {
self.entry(workspaces, dir).map(|(key, _)| key)
}
fn entry<'a>(
&self,
workspaces: &'a IndexMap<String, JsWorkspace>,
dir: &str,
) -> Option<(&'a String, &'a JsWorkspace)> {
let dir = if dir.is_empty() { "." } else { dir };
let position = self
.keys
.iter()
.position(|key| key.dir == dir)
.or_else(|| self.keys.iter().position(|key| key.selects(dir)))?;
workspaces.get_index(position)
}
fn unmatched<'a>(&self, workspaces: &'a IndexMap<String, JsWorkspace>, dirs: &[String]) -> Vec<&'a String> {
workspaces
.keys()
.zip(&self.keys)
.filter(|(_, key)| !dirs.iter().any(|dir| key.selects(dir)))
.map(|(text, _)| text)
.collect()
}
}
fn path_aliases(paths: &IndexMap<String, Vec<String>>) -> Vec<PathAlias> {
let trim = |pattern: &str| {
let pattern = pattern
.strip_suffix("/*")
.or_else(|| pattern.strip_suffix('*'))
.unwrap_or(pattern);
pattern.trim_start_matches("./").to_owned()
};
paths
.iter()
.flat_map(|(alias, targets)| {
let find = trim(alias);
targets.iter().map(move |target| {
let target = trim(target);
PathAlias {
find: find.clone(),
target: Some(if target.is_empty() { ".".to_owned() } else { target }),
}
})
})
.collect()
}
pub(crate) fn ignores_in_file_use(setting: &IgnoreExportsUsedInFile, export: &Export) -> bool {
match setting {
IgnoreExportsUsedInFile::All(all) => *all,
IgnoreExportsUsedInFile::Kinds(kinds) => match export.kind {
ExportKind::Value => kinds.variable,
ExportKind::Function => kinds.function,
ExportKind::Class => kinds.class,
ExportKind::Enum => kinds.enumeration,
ExportKind::Namespace => kinds.namespace,
ExportKind::Type => kinds.type_alias || kinds.interface,
_ => false,
},
_ => false,
}
}
pub(crate) struct PackageSetup {
pub id: PackageId,
pub dir: PathBuf,
pub entry: Globs,
pub external_entry: Globs,
pub shared_entry: Globs,
pub plugin_entry: Globs,
pub entries_incomplete: bool,
pub incomplete_reasons: Vec<IncompleteEntries>,
pub incomplete_scope: Option<IncompleteScopeGlobs>,
pub widened_by: Option<(PathBuf, u32)>,
pub project: Globs,
pub ignore: Globs,
pub ignore_files: Globs,
pub ignore_dependencies: Names,
pub ignore_binaries: Names,
pub user_ignore_dependencies: Listed,
pub user_ignore_binaries: Listed,
pub ignore_unresolved: Listed,
pub ignore_members: Listed,
pub user_patterns: UserPatterns,
pub include_entry_exports: bool,
pub used_in_file: IgnoreExportsUsedInFile,
pub referenced_dependencies: Vec<String>,
pub referenced_binaries: Vec<String>,
pub scripts: ScriptFacts,
pub config_files: Vec<PathBuf>,
pub aliases: Vec<PathAlias>,
pub build_dirs: Vec<BuildDir>,
pub conditions: Vec<String>,
pub module_dirs: Vec<String>,
pub extensions: Vec<String>,
pub custom_elements: Vec<CustomElementLibrary>,
}
pub(crate) struct IncompleteScopeGlobs {
pub patterns: Vec<String>,
pub globs: Globs,
}
impl PackageSetup {
pub(crate) fn in_incomplete_scope(&self, rel: &str) -> bool {
self.entries_incomplete
&& self
.incomplete_scope
.as_ref()
.is_some_and(|scope| scope.globs.is_match(rel))
}
pub(crate) fn unjudged_package_wide(&self) -> bool {
self.entries_incomplete && (self.incomplete_scope.is_none() || self.widened_by.is_some())
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct UserPatterns {
pub entry: Vec<String>,
pub project: Vec<String>,
pub ignore: Vec<String>,
pub ignore_files: Vec<String>,
}
#[allow(
clippy::struct_excessive_bools,
reason = "each bool is an independent switch of the analysis"
)]
pub(crate) struct Setup {
pub packages: Vec<Option<PackageSetup>>,
pub global_ignore: Globs,
pub global_ignore_files: Globs,
pub global_ignore_dependencies: Listed,
pub global_ignore_binaries: Listed,
pub global_ignore_unresolved: Listed,
pub global_ignore_members: Listed,
pub ignore_workspaces: Listed,
pub js_ignored: HashSet<PackageId>,
pub unmatched_workspace_keys: Vec<String>,
pub production: bool,
pub root_tsconfig: Option<PathBuf>,
pub isolate_workspaces: bool,
pub include_entry_exports: bool,
pub include_fixtures: bool,
pub used_in_file: IgnoreExportsUsedInFile,
pub ignore_tags: Vec<String>,
pub include_tags: Vec<String>,
pub cycles: skif_config::JsCycles,
pub ignore_global_binaries: bool,
}
pub(crate) fn tsconfig_path(root_tsconfig: Option<&Path>, dir: &Path, is_root: bool) -> PathBuf {
match root_tsconfig {
Some(config) if is_root => config.to_path_buf(),
_ => dir.join("tsconfig.json"),
}
}
fn workspace_relative(dir: &str, pattern: &str) -> Option<String> {
let (negation, body) = pattern.strip_prefix('!').map_or(("", pattern), |rest| ("!", rest));
let mut parts: Vec<&str> = Vec::new();
for segment in dir.split('/').chain(body.split('/')) {
match segment {
"" | "." => {}
".." => {
parts.pop()?;
}
other => parts.push(other),
}
}
Some(format!("{negation}{}", parts.join("/")))
}
fn names_package(target: &str, package: &skif_workspace::Package) -> bool {
let target = target.trim_start_matches("...").trim_end_matches("...");
if target.starts_with('!') {
return false;
}
if package.name.as_deref() == Some(target) {
return true;
}
if target.starts_with('.') || (target.contains('/') && !target.starts_with('@')) {
let dir = package.dir.to_string_lossy().replace('\\', "/");
if normalize_dir(target) == dir {
return true;
}
}
package.name.as_deref().is_some_and(|name| {
target.contains(['*', '?', '[', '{'])
&& Glob::new(target).is_ok_and(|glob| glob.compile_matcher().is_match(name))
})
}
fn forward_workspace_binaries(packages: &mut [Option<PackageSetup>], workspace: &Workspace) {
let mut credits: Vec<(PackageId, ScriptBinary)> = Vec::new();
for setup in packages.iter().flatten() {
for forwarded in &setup.scripts.forwarded {
for (id, target) in workspace.iter() {
let runs_script = target
.manifest
.value()
.get("scripts")
.and_then(|scripts| scripts.get(&forwarded.binary.name))
.is_some();
if !runs_script && forwarded.targets.iter().any(|name| names_package(name, target)) {
credits.push((id, forwarded.binary.clone()));
}
}
}
}
for (id, binary) in credits {
if let Some(Some(setup)) = packages.get_mut(id.index())
&& !setup.scripts.binaries.contains(&binary)
&& !setup.scripts.ephemeral_binaries.contains(&binary)
{
setup.scripts.ephemeral_binaries.push(binary);
}
}
}
fn is_shared_pattern(pattern: &str) -> bool {
let body = pattern.trim_start_matches('!').trim_start_matches("./");
!body.starts_with("**/") && !body.starts_with("../") && !body.starts_with('/')
}
#[derive(Debug, Default, serde::Deserialize)]
#[serde(default, deny_unknown_fields, rename_all = "kebab-case")]
pub(crate) struct UnusedExportsOptions {
pub ignore_tags: Vec<String>,
pub include_tags: Vec<String>,
}
impl Setup {
pub(crate) fn new(
workspace: &Workspace,
config: &Config,
fs: &Arc<dyn FileSystem>,
sources: &[Arc<dyn InputSource>],
sequential: bool,
) -> Self {
let js = &config.js;
let production = js.production;
let ancestor_dependencies = ancestors::external_dependencies(fs.as_ref(), &workspace.root);
let UnusedExportsOptions {
ignore_tags,
include_tags,
} = config
.rule("unused-exports")
.and_then(|rule| skif_lint::parse_options::<UnusedExportsOptions>(&rule.options).ok())
.unwrap_or_default();
let ignore_workspaces = Listed::directories(&js.ignore_workspaces, production);
let workspace_keys = WorkspaceKeys::new(&js.workspaces);
let js_ignored: HashSet<PackageId> = workspace
.iter()
.filter(|(_, package)| !package.is_root)
.filter(|(_, package)| ignore_workspaces.is_match(&package.dir.to_string_lossy().replace('\\', "/")))
.map(|(id, _)| id)
.collect();
let package_dirs: Vec<String> = workspace
.iter()
.map(|(_, package)| {
if package.is_root {
".".to_owned()
} else {
package.dir.to_string_lossy().replace('\\', "/")
}
})
.collect();
let unmatched_workspace_keys: Vec<String> = workspace_keys
.unmatched(&js.workspaces, &package_dirs)
.into_iter()
.cloned()
.collect();
let root_tsconfig = js
.tsconfig
.as_deref()
.and_then(relative_tsconfig)
.map(|path| join_normalized(&workspace.root, &path));
let named_members = if js.use_tsconfig_files {
member_dirs(workspace)
} else {
Vec::new()
};
let members: Vec<(PackageId, &Package)> = workspace.iter().collect();
let mut packages: Vec<Option<PackageSetup>> = map_ordered(&members, sequential, |&(id, package)| {
if workspace.is_ignored(id) || js_ignored.contains(&id) {
return None;
}
let dir_name = package.dir.to_string_lossy().replace('\\', "/");
let abs_dir = workspace.root.join(&package.dir);
let match_dir = if package.is_root {
workspace.root.as_path()
} else {
abs_dir.as_path()
};
let user = configured(
config,
&workspace_keys,
package.display_name(),
match_dir,
&dir_name,
package.is_root,
);
let mut plugin = WorkspaceInputs::default();
let request = InputRequest {
workspace,
package,
fs: fs.as_ref(),
ancestor_dependencies: &ancestor_dependencies,
};
for source in sources {
plugin.extend(source.inputs(&request));
}
let scripts = scripts::analyze(package);
let mut entry = listed::user_globs(&user.entry, production, &default_entry());
entry.extend(ambient_declarations());
let mut shared_sources: Vec<&String> = plugin.production_entry.iter().collect();
if !production {
shared_sources.extend(plugin.entry.iter());
}
let shared_entry: Vec<String> = shared_sources
.into_iter()
.filter(|pattern| is_shared_pattern(pattern))
.cloned()
.collect();
let mut plugin_entry: Vec<String> = plugin.production_entry.clone();
if !production {
plugin_entry.extend(plugin.entry.iter().cloned());
entry.extend(plugin.entry);
}
entry.extend(plugin.production_entry);
plugin_entry.retain(|pattern| !pattern.trim_start_matches('!').starts_with("../"));
let tsconfig_project = js
.use_tsconfig_files
.then(|| {
let config = tsconfig_path(root_tsconfig.as_deref(), &abs_dir, package.is_root);
tsconfig_files::project_globs(fs.as_ref(), &named_members, &config, &abs_dir)
})
.flatten();
let from_tsconfig = tsconfig_project.is_some();
let (mut project, mut named_files) = match tsconfig_project {
Some(selected) => (selected.patterns, selected.files),
None => (
listed::user_globs(&user.project, production, &default_project()),
Vec::new(),
),
};
if production {
let mut exclusions = listed::production_exclusions(&plugin.project);
exclusions.extend(listed::non_production_entries(&user.entry));
exclusions.extend(
test_files()
.into_iter()
.chain(test_directories())
.map(|glob| format!("!{glob}")),
);
if !named_files.is_empty() {
named_files.extend(exclusions.iter().cloned());
}
project.extend(exclusions);
} else if !from_tsconfig {
project.extend(plugin.project);
}
let user_patterns = UserPatterns {
entry: user.entry.clone(),
project: user.project.clone(),
ignore: user.ignore.clone(),
ignore_files: user.ignore_files.clone(),
};
let mut ignore = user.ignore;
ignore.extend(plugin.ignore);
let mut ignore_dependencies = plugin.ignore_dependencies;
ignore_dependencies.extend(plugin.virtual_modules);
let ignore_binaries = plugin.ignore_binaries;
let mut aliases = path_aliases(&user.paths);
aliases.extend(plugin.aliases);
let mut referenced_dependencies = plugin.dependencies;
referenced_dependencies.extend(scripts.dependencies.iter().cloned());
let (outside, entry): (Vec<String>, Vec<String>) = entry
.into_iter()
.partition(|pattern| pattern.trim_start_matches('!').starts_with("../"));
let external_entry = Globs::new(
outside
.iter()
.filter_map(|pattern| workspace_relative(&dir_name, pattern)),
);
Some(PackageSetup {
id,
dir: abs_dir,
entry: Globs::new(entry),
external_entry,
shared_entry: Globs::new(shared_entry),
plugin_entry: Globs::new(plugin_entry),
entries_incomplete: plugin.entries_incomplete,
incomplete_scope: match &plugin.incomplete_scope {
IncompleteScope::Within(patterns) if plugin.entries_incomplete => Some(IncompleteScopeGlobs {
globs: Globs::new(patterns),
patterns: patterns.clone(),
}),
_ => None,
},
widened_by: None,
incomplete_reasons: if plugin.entries_incomplete && plugin.incomplete_reasons.is_empty() {
vec![IncompleteEntries {
cause: IncompleteCause::Unspecified,
plugin: None,
file: None,
}]
} else {
plugin.incomplete_reasons
},
project: Globs::new(project).or(Globs::new(named_files)),
ignore: Globs::new(ignore),
ignore_files: Globs::new(&user.ignore_files),
ignore_dependencies: Names::new(ignore_dependencies),
ignore_binaries: Names::new(ignore_binaries),
user_ignore_dependencies: Listed::new(&user.ignore_dependencies, production),
user_ignore_binaries: Listed::new(&user.ignore_binaries, production),
ignore_unresolved: Listed::new(&user.ignore_unresolved, production),
ignore_members: Listed::new(&user.ignore_members, production),
user_patterns,
include_entry_exports: user.include_entry_exports.unwrap_or(js.include_entry_exports),
used_in_file: user
.ignore_exports_used_in_file
.unwrap_or_else(|| js.ignore_exports_used_in_file.clone()),
referenced_dependencies,
referenced_binaries: plugin.binaries,
scripts,
config_files: plugin.config_files,
aliases,
build_dirs: plugin.build_dirs,
conditions: plugin.conditions,
module_dirs: plugin.module_dirs,
extensions: plugin.extensions,
custom_elements: plugin.custom_elements,
})
});
forward_workspace_binaries(&mut packages, workspace);
Self {
packages,
global_ignore: Globs::new(&js.ignore),
global_ignore_files: Globs::new(&js.ignore_files),
global_ignore_dependencies: Listed::new(&js.ignore_dependencies, production),
global_ignore_binaries: Listed::new(&js.ignore_binaries, production),
global_ignore_unresolved: Listed::new(&js.ignore_unresolved, production),
global_ignore_members: Listed::new(&js.ignore_members, production),
ignore_workspaces,
js_ignored,
unmatched_workspace_keys,
production,
root_tsconfig,
isolate_workspaces: js.isolate_workspaces,
include_entry_exports: js.include_entry_exports,
include_fixtures: js.include_fixtures,
used_in_file: js.ignore_exports_used_in_file.clone(),
ignore_tags,
include_tags,
cycles: js.cycles.clone(),
ignore_global_binaries: js.ignore_global_binaries,
}
}
pub(crate) fn tsconfig_of(&self, workspace: &Workspace, package: &Package) -> PathBuf {
tsconfig_path(
self.root_tsconfig.as_deref(),
&workspace.root.join(&package.dir),
package.is_root,
)
}
pub(crate) fn package(&self, id: PackageId) -> Option<&PackageSetup> {
self.packages.get(id.index()).and_then(Option::as_ref)
}
pub(crate) fn widen_incomplete_scopes(&mut self, graph: &ModuleGraph) {
for module in graph
.modules()
.iter()
.filter(|module| module.in_project && !module.foreign)
{
let Some(span) = module.info.unresolved_dynamic.first() else {
continue;
};
if let Some(Some(package)) = self.packages.get_mut(module.owner.index())
&& package.incomplete_scope.is_some()
&& package.widened_by.is_none()
{
package.widened_by = Some((module.path.clone(), span.start_line));
}
}
}
pub(crate) fn mark_entries_incomplete(&mut self, ids: &[PackageId]) {
for id in ids {
if let Some(Some(package)) = self.packages.get_mut(id.index()) {
package.entries_incomplete = true;
package.incomplete_scope = None;
package.incomplete_reasons.push(IncompleteEntries {
cause: IncompleteCause::UnmappedPublished,
plugin: None,
file: Some(package.dir.join("package.json")),
});
}
}
}
pub(crate) fn ignores_dependency(&self, id: PackageId, name: &str) -> bool {
let global = self.global_ignore_dependencies.is_match(name);
let package = self.package(id).is_some_and(|setup| {
let user = setup.user_ignore_dependencies.is_match(name);
setup.ignore_dependencies.is_match(name) || user
});
global || package
}
pub(crate) fn ignores_binary(&self, id: PackageId, name: &str) -> bool {
let global = self.global_ignore_binaries.is_match(name);
let package = self.package(id).is_some_and(|setup| {
let user = setup.user_ignore_binaries.is_match(name);
setup.ignore_binaries.is_match(name) || user
});
global || package
}
pub(crate) fn ignores_unresolved(&self, id: PackageId, specifier: &str) -> bool {
let global = self.global_ignore_unresolved.is_match(specifier);
let package = self
.package(id)
.is_some_and(|setup| setup.ignore_unresolved.is_match(specifier));
global || package
}
pub(crate) fn ignores_member(&self, id: PackageId, name: &str) -> bool {
self.global_ignore_members.is_match(name)
|| self
.package(id)
.is_some_and(|setup| setup.ignore_members.is_match(name))
}
pub(crate) fn includes_entry_exports(&self, id: PackageId) -> bool {
self.package(id)
.map_or(self.include_entry_exports, |setup| setup.include_entry_exports)
}
pub(crate) fn ignores_in_file_use(&self, id: PackageId, export: &Export) -> bool {
let setting = self.package(id).map_or(&self.used_in_file, |setup| &setup.used_in_file);
ignores_in_file_use(setting, export)
}
pub(crate) fn is_registered(&self, id: PackageId, export: &Export) -> bool {
let libraries = self
.package(id)
.map_or(&[][..], |setup| setup.custom_elements.as_slice());
export.registrations.iter().any(|registration| match registration {
Registration::Define => true,
Registration::Decorator { module, name } => libraries
.iter()
.any(|library| library.decorator == *name && library.is_module(module)),
Registration::BaseClassDefine { module, name } => libraries
.iter()
.any(|library| library.base_class.as_deref() == Some(name.as_str()) && library.is_module(module)),
})
}
}
#[cfg(test)]
mod tests {
use super::{is_shared_pattern, workspace_relative};
#[test]
fn only_rooted_plugin_globs_are_shared_with_nested_packages() {
assert!(is_shared_pattern("apps/web/playwright/**/*.ts"));
assert!(is_shared_pattern("!apps/web/legacy/**"));
assert!(!is_shared_pattern("**/*.test.ts"));
assert!(!is_shared_pattern("../shared/setup.ts"));
}
#[test]
fn patterns_outside_the_package_become_workspace_relative() {
assert_eq!(
workspace_relative("apps/a", "../../shared/setup.ts").as_deref(),
Some("shared/setup.ts")
);
assert_eq!(workspace_relative("a", "!../b/**").as_deref(), Some("!b/**"));
assert_eq!(workspace_relative("a", "../../escape.ts"), None);
}
}