use std::collections::{BTreeMap, BTreeSet};
use std::path::{Component, Path, PathBuf};
use rayon::prelude::*;
use skif_config::{
Assignment, Ceiling, Config, ConfigCache, ConfigError, ConfigLoader, ConfigOverrides, ConfigSource, Origins,
SourceKind, join_key,
};
use skif_core::FileSystem;
use skif_lint::LintError;
use skif_workspace::{
DiscoverOptions, RootKind, Workspace, claim_roots, declared_member_dirs, declares_workspace, repository_root,
scan_candidates,
};
use crate::error::Error;
use crate::root::{Located, config_ceiling, is_enclosed, locate, resolve_config};
#[derive(Debug)]
#[non_exhaustive]
pub struct Target {
pub root: PathBuf,
pub config: Config,
pub config_file: Option<PathBuf>,
pub sources: Vec<ConfigSource>,
pub origins: Origins,
pub workspace: Workspace,
}
impl Target {
#[must_use]
pub fn config_path(&self) -> Option<PathBuf> {
let file = self.config_file.as_ref()?;
let base: Vec<Component<'_>> = self.root.components().collect();
let target: Vec<Component<'_>> = file.components().collect();
let common = base
.iter()
.zip(&target)
.take_while(|(left, right)| left == right)
.count();
if common == 0 {
return Some(file.clone());
}
let mut path: PathBuf = std::iter::repeat_n("..", base.len() - common).collect();
path.extend(&target[common..]);
Some(path)
}
pub fn retain_ignore(&mut self, keep: impl Fn(usize) -> bool) {
let entries = std::mem::take(&mut self.config.ignore);
self.config.ignore = entries
.into_iter()
.enumerate()
.filter(|(index, _)| keep(*index))
.map(|(_, entry)| entry)
.collect();
self.origins.retain_list("ignore", keep);
}
#[must_use]
pub fn locate(&self, error: Error) -> Error {
match error {
Error::Config(error) => Error::Config(self.locate_config(error)),
Error::Lint(LintError::Config(error)) => Error::Lint(LintError::Config(self.locate_config(error))),
other => other,
}
}
fn locate_config(&self, error: ConfigError) -> ConfigError {
let ConfigError::UnknownRule { rule, .. } = &error else {
return error;
};
let Some(assignment) = self.rule_origin(rule) else {
return error;
};
let Some(source) = self.sources.get(assignment.source) else {
return error;
};
let known = |position: usize| (position > 0 && source.kind != SourceKind::Package).then_some(position);
ConfigError::Parse {
path: Some(source.path.clone()),
message: error.to_string(),
span: None,
line: known(assignment.line),
column: known(assignment.column),
}
}
fn rule_origin(&self, rule: &str) -> Option<&Assignment> {
let names = |rules: &[String]| rules.iter().any(|id| id == rule);
let own = join_key("rules", rule);
let ignore = || {
let index = self.config.ignore.iter().position(|entry| names(&entry.rules))?;
self.origins.last(&format!("ignore[{index}]"))
};
let scoped = || {
let index = self.config.overrides.iter().position(|scoped| {
scoped.rules.contains_key(rule) || scoped.ignore.iter().any(|entry| names(&entry.rules))
})?;
self.origins.last(&format!("overrides[{index}]"))
};
self.origins
.last(&format!("{own}.level"))
.or_else(|| self.origins.last(&format!("{own}.options")))
.or_else(ignore)
.or_else(scoped)
}
}
pub fn load_target(
root: &Path,
ceiling: &Ceiling,
fs: &dyn FileSystem,
overrides: &ConfigOverrides,
cache: &ConfigCache,
) -> Result<Target, Error> {
let mut resolved = resolve_config(root, ceiling, fs, overrides, cache)?;
let workspace = Workspace::discover_with_fs(root, &resolved.config, fs)?;
if !overrides.no_config {
let members: Vec<PathBuf> = workspace.member_dirs().into_iter().collect();
ConfigLoader::new(fs, cache).add_package_files(&mut resolved, root, &members)?;
}
let file = resolved.nearest_file().map(Path::to_path_buf);
Ok(Target {
root: root.to_path_buf(),
config: resolved.config,
config_file: file,
sources: resolved.sources,
origins: resolved.origins,
workspace,
})
}
fn load_in_repository(
root: &Path,
fs: &dyn FileSystem,
overrides: &ConfigOverrides,
cache: &ConfigCache,
) -> Result<Target, Error> {
load_target(root, &Ceiling::Vcs, fs, overrides, cache)
}
pub fn find_target(start: &Path, fs: &dyn FileSystem, overrides: &ConfigOverrides) -> Result<Target, Error> {
let cached = crate::cache::per_call(fs);
let fs: &dyn FileSystem = &cached;
let Located { project, ancestors } = locate(start, fs)?;
let cache = ConfigCache::new();
if declares_workspace(fs, &project) {
return load_in_repository(&project, fs, overrides, &cache);
}
for ancestor in ancestors {
match load_in_repository(&ancestor, fs, overrides, &cache) {
Ok(target) if target.workspace.claimed_dirs().contains(&project) => return Ok(target),
Err(error) if declared_member_dirs(fs, &ancestor).contains(&project) => return Err(error),
_ => {}
}
}
let mut target = load_in_repository(&project, fs, overrides, &cache)?;
if !declares_workspace(fs, &project) {
target
.workspace
.set_nested(is_enclosed(&project, &config_ceiling(&project, fs), fs));
}
Ok(target)
}
#[derive(Debug)]
#[non_exhaustive]
pub struct RootFailure {
pub root: PathBuf,
pub error: Error,
}
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct Discovery {
pub targets: Vec<Target>,
pub failures: Vec<RootFailure>,
pub truncated: Vec<PathBuf>,
}
pub fn discover_targets(start: &Path, fs: &dyn FileSystem, overrides: &ConfigOverrides) -> Result<Discovery, Error> {
discover(start, fs, overrides, None)
}
pub fn discover_targets_below(
loaded: &Workspace,
fs: &dyn FileSystem,
overrides: &ConfigOverrides,
) -> Result<Discovery, Error> {
discover(&loaded.root, fs, overrides, Some(loaded))
}
fn discover(
start: &Path,
fs: &dyn FileSystem,
overrides: &ConfigOverrides,
known: Option<&Workspace>,
) -> Result<Discovery, Error> {
let cached = crate::cache::per_call(fs);
let fs: &dyn FileSystem = &cached;
if !start.has_root() {
return Err(Error::RelativePath(start.to_path_buf()));
}
let dir = if fs.is_dir(start) {
start
} else {
start.parent().unwrap_or(start)
};
let cache = ConfigCache::new();
let ceiling = Ceiling::VcsOr(dir.to_path_buf());
let enclosing = |root: &Path| repository_ceiling(root, dir, fs);
let candidates = scan_candidates(dir, fs, &discover_options(dir, fs, overrides, &cache));
let is_known = |root: &Path| known.is_some_and(|workspace| workspace.root.as_path() == root);
let mut workspaces: BTreeMap<PathBuf, Result<Target, Error>> = candidates
.workspaces
.par_iter()
.filter(|root| !is_known(root.as_path()))
.map(|root| (root.clone(), load_target(root, &ceiling, fs, overrides, &cache)))
.collect();
let mut members: BTreeMap<PathBuf, BTreeSet<PathBuf>> = workspaces
.iter()
.map(|(root, loaded)| {
let dirs = match loaded {
Ok(target) => target.workspace.claimed_dirs(),
Err(_) => declared_member_dirs(fs, root),
};
(root.clone(), dirs)
})
.collect();
if let Some(workspace) = known.filter(|workspace| candidates.workspaces.contains(&workspace.root)) {
members.insert(workspace.root.clone(), workspace.claimed_dirs());
}
let claimed = claim_roots(fs, &candidates, &members);
let slots: Vec<(PathBuf, Option<Result<Target, Error>>)> = claimed
.iter()
.filter(|root| !is_known(root.path()))
.map(|root| (root.path().to_path_buf(), workspaces.remove(root.path())))
.collect();
let loaded: Vec<(PathBuf, Result<Target, Error>)> = slots
.into_par_iter()
.map(|(root, found)| {
let result = found.unwrap_or_else(|| load_target(&root, &ceiling, fs, overrides, &cache));
(root, result)
})
.collect();
let mut discovery = Discovery {
truncated: candidates.truncated,
..Discovery::default()
};
let root_dirs: BTreeSet<&Path> = claimed.iter().map(RootKind::path).collect();
for (root, result) in loaded {
match result {
Ok(mut target) => {
let nested = !candidates.workspaces.contains(&root)
&& (root.ancestors().skip(1).any(|ancestor| root_dirs.contains(ancestor))
|| is_enclosed(&root, &enclosing(&root), fs));
target.workspace.set_nested(nested);
target.workspace.ignore_dirs(&candidates.workspaces);
if overrides.config_file.is_some() {
target.workspace.set_workspace_keys_anchor(start_relative(&root, dir));
}
discovery.targets.push(target);
}
Err(error) => discovery.failures.push(RootFailure { root, error }),
}
}
Ok(discovery)
}
fn start_relative(root: &Path, start: &Path) -> Option<String> {
root.strip_prefix(start)
.ok()
.filter(|rel| !rel.as_os_str().is_empty())
.map(crate::slash)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnappliedReason {
NotAMember,
NotLoaded,
}
impl UnappliedReason {
#[must_use]
pub const fn describe(self) -> &'static str {
match self {
Self::NotAMember => "not in the root or a workspace member directory",
Self::NotLoaded => "in a workspace member directory but not loaded for this run",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UnappliedConfig {
pub path: PathBuf,
pub reason: UnappliedReason,
}
#[must_use]
pub fn unapplied_config_files(target: &Target, other_roots: &[&Path], fs: &dyn FileSystem) -> Vec<UnappliedConfig> {
let candidates = scan_candidates(&target.root, fs, &unapplied_scan_options());
unapplied_among(target, other_roots, candidates.config_files)
}
#[must_use]
pub fn unapplied_config_files_all(targets: &[Target], fs: &dyn FileSystem) -> Vec<Vec<UnappliedConfig>> {
let roots: Vec<&Path> = targets.iter().map(|target| target.root.as_path()).collect();
let mut scans: BTreeMap<&Path, BTreeSet<PathBuf>> = BTreeMap::new();
targets
.iter()
.map(|target| {
let outermost = roots
.iter()
.copied()
.filter(|root| target.root.starts_with(root))
.min_by_key(|root| root.components().count())
.unwrap_or(target.root.as_path());
let files = scans
.entry(outermost)
.or_insert_with(|| scan_candidates(outermost, fs, &unapplied_scan_options()).config_files);
let below = files.iter().filter(|file| file.starts_with(&target.root)).cloned();
unapplied_among(target, &roots, below)
})
.collect()
}
fn unapplied_scan_options() -> DiscoverOptions {
DiscoverOptions::default().with_ignore(Vec::new())
}
fn unapplied_among(
target: &Target,
other_roots: &[&Path],
config_files: impl IntoIterator<Item = PathBuf>,
) -> Vec<UnappliedConfig> {
let members = target.workspace.member_dirs();
config_files
.into_iter()
.filter_map(|path| {
let dir = path.parent()?;
let owned_elsewhere = other_roots
.iter()
.any(|other| *other != target.root && other.starts_with(&target.root) && dir.starts_with(other));
if dir == target.root || owned_elsewhere || target.sources.iter().any(|source| source.path == path) {
return None;
}
let reason = if members.contains(dir) {
UnappliedReason::NotLoaded
} else {
UnappliedReason::NotAMember
};
Some(UnappliedConfig { path, reason })
})
.collect()
}
fn discover_options(
start: &Path,
fs: &dyn FileSystem,
overrides: &ConfigOverrides,
cache: &ConfigCache,
) -> DiscoverOptions {
match resolve_config(start, &Ceiling::Vcs, fs, overrides, cache) {
Ok(resolved) => DiscoverOptions::default().with_ignore(resolved.config.workspace.discover_ignore),
Err(_) => DiscoverOptions::default(),
}
}
fn repository_ceiling(root: &Path, start: &Path, fs: &dyn FileSystem) -> PathBuf {
repository_root(fs, root).unwrap_or_else(|| start.to_path_buf())
}
pub fn discover_all(start: &Path, fs: &dyn FileSystem, overrides: &ConfigOverrides) -> Result<Vec<Target>, Error> {
let discovery = discover_targets(start, fs, overrides)?;
match discovery.failures.into_iter().next() {
Some(failure) => Err(failure.error),
None => Ok(discovery.targets),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use skif_core::MemoryFs;
use skif_workspace::PackageManager;
use super::*;
fn fs() -> MemoryFs {
MemoryFs::new()
.with_file("/r/skif.toml", "[lint]\nfail-on-warnings = true\n")
.with_file("/r/a/package.json", r#"{"name": "a", "version": "1.0.0"}"#)
.with_file("/r/b/skif.toml", "")
.with_file("/r/b/package.json", r#"{"name": "b", "version": "1.0.0"}"#)
}
#[test]
fn the_roots_below_a_loaded_root_are_those_its_discovery_finds_besides_it() {
let fs = MemoryFs::new()
.with_file("/w/package.json", r#"{"name": "w", "workspaces": ["packages/*"]}"#)
.with_file("/w/packages/a/package.json", r#"{"name": "a"}"#)
.with_file("/w/packages/b/package.json", r#"{"name": "b"}"#)
.with_file("/w/tools/x/package.json", r#"{"name": "x"}"#)
.with_file("/w/apps/package.json", r#"{"name": "apps", "workspaces": ["y"]}"#)
.with_file("/w/apps/y/package.json", r#"{"name": "y"}"#)
.with_file("/w/broken/package.json", "{");
let overrides = ConfigOverrides::default();
let root = Path::new("/w");
let loaded = find_target(root, &fs, &overrides).unwrap();
let below = discover_targets_below(&loaded.workspace, &fs, &overrides).unwrap();
let all = discover_targets(root, &fs, &overrides).unwrap();
let roots = |discovery: &Discovery| -> Vec<PathBuf> {
discovery.targets.iter().map(|target| target.root.clone()).collect()
};
let failed = |discovery: &Discovery| -> Vec<PathBuf> {
discovery.failures.iter().map(|failure| failure.root.clone()).collect()
};
assert_eq!(roots(&below), [PathBuf::from("/w/apps"), PathBuf::from("/w/tools/x")]);
let mut expected = roots(&all);
expected.retain(|dir| dir.as_path() != root);
assert_eq!(roots(&below), expected);
assert_eq!(failed(&below), [PathBuf::from("/w/broken")]);
assert_eq!(failed(&below), failed(&all));
let nested: Vec<bool> = below
.targets
.iter()
.map(|target| target.workspace.is_nested())
.collect();
let nested_in_all: Vec<bool> = all
.targets
.iter()
.filter(|target| target.root.as_path() != root)
.map(|target| target.workspace.is_nested())
.collect();
assert_eq!(nested, nested_in_all);
}
#[test]
fn every_config_up_to_the_start_directory_contributes() {
let targets = discover_all(Path::new("/r"), &fs(), &ConfigOverrides::default()).unwrap();
let roots: Vec<_> = targets.iter().map(|target| target.root.clone()).collect();
assert_eq!(roots, [PathBuf::from("/r/a"), PathBuf::from("/r/b")]);
assert!(targets[0].config.lint.fail_on_warnings);
assert_eq!(targets[0].config_path(), Some(PathBuf::from("../skif.toml")));
assert_eq!(targets[0].sources.len(), 1);
assert!(targets[1].config.lint.fail_on_warnings);
assert_eq!(targets[1].config_path(), Some(PathBuf::from("skif.toml")));
assert_eq!(targets[1].sources.len(), 2);
}
#[test]
fn config_above_the_start_directory_is_not_inherited() {
let targets = discover_all(Path::new("/r/a"), &fs(), &ConfigOverrides::default()).unwrap();
assert_eq!(targets.len(), 1);
assert!(!targets[0].config.lint.fail_on_warnings);
assert_eq!(targets[0].config_path(), None);
}
#[test]
fn an_explicit_configuration_outside_the_root_is_named_relative_to_it() {
let fs = MemoryFs::new()
.with_file("/r/a/package.json", r#"{"name": "a", "version": "1.0.0"}"#)
.with_file("/etc/org/skif.toml", "[lint]\nfail-on-warnings = true\n");
let overrides = ConfigOverrides::default().with_config_file(Some(PathBuf::from("/etc/org/skif.toml")));
let target = load_target(Path::new("/r/a"), &Ceiling::Vcs, &fs, &overrides, &ConfigCache::new()).unwrap();
assert_eq!(target.config_file, Some(PathBuf::from("/etc/org/skif.toml")));
assert_eq!(target.config_path(), Some(PathBuf::from("../../etc/org/skif.toml")));
}
fn nested_fs(explicit: &str) -> MemoryFs {
MemoryFs::new()
.with_file("/r/package.json", r#"{"name": "app", "version": "1.0.0"}"#)
.with_file("/r/tools/sub/package.json", r#"{"name": "sub", "version": "1.0.0"}"#)
.with_file(
"/r/tools/other/package.json",
r#"{"name": "other", "version": "1.0.0"}"#,
)
.with_file("/etc/skif.toml", explicit)
}
fn anchors(start: &str, fs: &MemoryFs, overrides: &ConfigOverrides) -> Vec<(PathBuf, Option<String>)> {
discover_all(Path::new(start), fs, overrides)
.unwrap()
.into_iter()
.map(|target| {
let anchor = target.workspace.workspace_keys_anchor().map(str::to_owned);
(target.root, anchor)
})
.collect()
}
fn explicit() -> ConfigOverrides {
ConfigOverrides::default().with_config_file(Some(PathBuf::from("/etc/skif.toml")))
}
#[test]
fn an_explicit_configuration_anchors_the_workspace_keys_of_nested_roots_at_the_start() {
for file in [
"[js.workspaces.\".\"]\nentry = [\"src/extra.js\"]\n",
"[js]\nignore-workspaces = [\"tools/sub/legacy\"]\n",
"[js]\nentry = [\"src/extra.js\"]\n",
] {
assert_eq!(
anchors("/r", &nested_fs(file), &explicit()),
[
(PathBuf::from("/r"), None),
(PathBuf::from("/r/tools/other"), Some("tools/other".to_owned())),
(PathBuf::from("/r/tools/sub"), Some("tools/sub".to_owned())),
],
"{file}"
);
}
}
#[test]
fn the_configuration_carries_no_anchor_so_anchored_roots_share_it() {
let targets = discover_all(
Path::new("/r"),
&nested_fs("[js.workspaces.\"tools/*\"]\nentry = [\"src/extra.js\"]\n"),
&explicit(),
)
.unwrap();
assert_eq!(targets.len(), 3);
assert!(targets.iter().all(|target| target.config == targets[0].config));
}
#[test]
fn a_start_that_is_no_root_anchors_every_root_below_it() {
let fs = MemoryFs::new()
.with_file("/r/a/package.json", r#"{"name": "a", "version": "1.0.0"}"#)
.with_file("/r/b/sub/package.json", r#"{"name": "sub", "version": "1.0.0"}"#)
.with_file("/etc/skif.toml", "[js.workspaces.\"a\"]\nentry = [\"src/extra.js\"]\n");
assert_eq!(
anchors("/r", &fs, &explicit()),
[
(PathBuf::from("/r/a"), Some("a".to_owned())),
(PathBuf::from("/r/b/sub"), Some("b/sub".to_owned())),
]
);
}
#[test]
fn discovered_configurations_leave_the_roots_unanchored() {
let discovered = nested_fs("").with_file("/r/skif.toml", "[js.workspaces.\".\"]\nentry = [\"src/extra.js\"]\n");
assert_eq!(
anchors("/r", &discovered, &ConfigOverrides::default()),
[
(PathBuf::from("/r"), None),
(PathBuf::from("/r/tools/other"), None),
(PathBuf::from("/r/tools/sub"), None),
]
);
}
#[test]
fn the_root_a_run_starts_at_is_never_anchored() {
let fs = nested_fs("[js.workspaces.\".\"]\nentry = [\"src/extra.js\"]\n");
let expected: [(PathBuf, Option<String>); 1] = [(PathBuf::from("/r/tools/sub"), None)];
assert_eq!(anchors("/r/tools/sub", &fs, &explicit()), expected);
}
#[test]
fn relative_start_is_rejected() {
assert!(matches!(
discover_all(Path::new("r"), &fs(), &ConfigOverrides::default()),
Err(Error::RelativePath(_))
));
}
fn names(discovery: &Discovery) -> Vec<String> {
discovery
.targets
.iter()
.map(|target| target.root.display().to_string().replace('\\', "/"))
.collect()
}
fn discover(fs: &MemoryFs, start: &str) -> Discovery {
discover_targets(Path::new(start), fs, &ConfigOverrides::default()).unwrap()
}
fn packages_of(target: &Target) -> Vec<String> {
target
.workspace
.iter()
.filter(|(id, _)| !target.workspace.is_ignored(*id))
.filter_map(|(_, package)| package.name.clone())
.collect()
}
fn divergent_declarations() -> MemoryFs {
MemoryFs::new()
.with_file("/r/package.json", r#"{"name": "root", "workspaces": ["npmpkgs/*"]}"#)
.with_file("/r/pnpm-workspace.yaml", "packages:\n - 'yarnpkgs/*'\n")
.with_file("/r/skif.toml", "[workspace]\npackage-manager = \"npm\"\n")
.with_file("/r/npmpkgs/x/package.json", r#"{"name": "x"}"#)
.with_file("/r/yarnpkgs/y/package.json", r#"{"name": "y"}"#)
}
#[test]
fn members_are_claimed_with_the_root_s_own_configuration() {
let discovery = discover(&divergent_declarations(), "/r");
assert_eq!(names(&discovery), ["/r", "/r/yarnpkgs/y"]);
assert_eq!(packages_of(&discovery.targets[0]), ["root", "x"]);
assert_eq!(packages_of(&discovery.targets[1]), ["y"]);
}
#[test]
fn a_nested_workspace_is_a_root_and_its_members_are_linted_once() {
let fs = MemoryFs::new()
.with_file("/r/package.json", r#"{"name": "root", "workspaces": ["packages/*"]}"#)
.with_file(
"/r/packages/inner/package.json",
r#"{"name": "inner", "workspaces": ["x/*"]}"#,
)
.with_file("/r/packages/inner/x/y/package.json", r#"{"name": "y"}"#)
.with_file("/r/packages/plain/package.json", r#"{"name": "plain"}"#);
let discovery = discover(&fs, "/r");
assert_eq!(names(&discovery), ["/r", "/r/packages/inner"]);
assert_eq!(packages_of(&discovery.targets[0]), ["root", "plain"]);
assert_eq!(packages_of(&discovery.targets[1]), ["inner", "y"]);
let direct = find_target(Path::new("/r/packages/inner"), &fs, &ConfigOverrides::default()).unwrap();
assert_eq!(direct.root, Path::new("/r/packages/inner"));
assert_eq!(packages_of(&direct), ["inner", "y"]);
}
#[test]
fn a_root_that_fails_is_reported_and_the_others_still_load() {
let fs = MemoryFs::new()
.with_file("/r/bad/package.json", r#"{"name": "#)
.with_file("/r/good/package.json", r#"{"name": "good", "version": "1.0.0"}"#)
.with_file("/r/ws/pnpm-workspace.yaml", "packages:\n - 'p/*'\n")
.with_file("/r/ws/p/m/package.json", r#"{"name": "m"}"#);
let discovery = discover(&fs, "/r");
assert_eq!(names(&discovery), ["/r/good", "/r/ws"]);
let failed: Vec<_> = discovery.failures.iter().map(|failure| failure.root.clone()).collect();
assert_eq!(failed, [PathBuf::from("/r/bad")]);
assert!(discover_all(Path::new("/r"), &fs, &ConfigOverrides::default()).is_err());
}
#[test]
fn the_loaded_workspace_carries_its_own_members_and_configuration() {
let discovery = discover(&divergent_declarations(), "/r");
let root = &discovery.targets[0];
assert_eq!(root.workspace.member_dirs().len(), 1);
assert_eq!(root.config_path(), Some(PathBuf::from("skif.toml")));
}
fn nested_project_in_a_workspace_tree() -> MemoryFs {
MemoryFs::new()
.with_file("/r/.git/HEAD", "ref")
.with_file("/r/I/skif.toml", "[lint]\nfail-on-warnings = true\n")
.with_file("/r/I/package.json", r#"{"name": "i", "workspaces": ["pkgs/*"]}"#)
.with_file("/r/I/pkgs/a/package.json", r#"{"name": "a"}"#)
.with_file("/r/I/examples/e/package.json", r#"{"name": "e"}"#)
}
#[test]
fn a_project_gets_the_same_configuration_whichever_way_it_is_reached() {
let fs = nested_project_in_a_workspace_tree();
let overrides = ConfigOverrides::default();
let discovery = discover(&fs, "/r/I");
let by_discovery = discovery
.targets
.iter()
.find(|target| target.root == Path::new("/r/I/examples/e"))
.unwrap();
let direct = find_target(Path::new("/r/I/examples/e"), &fs, &overrides).unwrap();
assert!(by_discovery.config.lint.fail_on_warnings);
assert_eq!(direct.config, by_discovery.config);
assert_eq!(direct.config_path(), Some(PathBuf::from("../../skif.toml")));
}
#[test]
fn the_configuration_search_climbs_to_the_repository_root_and_no_higher() {
let fs = nested_project_in_a_workspace_tree()
.with_file("/skif.toml", "[lint]\nfail-on-warnings = true\n")
.with_file("/r/skif.toml", "")
.with_file("/r/solo/package.json", r#"{"name": "solo"}"#);
let found = find_target(Path::new("/r/solo"), &fs, &ConfigOverrides::default()).unwrap();
assert!(!found.config.lint.fail_on_warnings);
assert_eq!(found.config_file, Some(PathBuf::from("/r/skif.toml")));
assert_eq!(found.config_path(), Some(PathBuf::from("../skif.toml")));
}
#[test]
fn find_target_returns_the_ancestor_that_lists_the_project() {
let fs = divergent_declarations();
let target = find_target(Path::new("/r/npmpkgs/x"), &fs, &ConfigOverrides::default()).unwrap();
assert_eq!(target.root, Path::new("/r"));
let own = find_target(Path::new("/r/yarnpkgs/y"), &fs, &ConfigOverrides::default()).unwrap();
assert_eq!(own.root, Path::new("/r/yarnpkgs/y"));
}
#[test]
fn an_ancestor_that_lists_the_project_but_cannot_load_is_an_error() {
let fs = MemoryFs::new()
.with_file("/r/package.json", r#"{"workspaces": ["p/*"]"#)
.with_file("/r/pnpm-workspace.yaml", "packages:\n - 'p/*'\n")
.with_file("/r/p/m/package.json", r#"{"name": "m"}"#);
let error = find_target(Path::new("/r/p/m"), &fs, &ConfigOverrides::default()).unwrap_err();
assert!(matches!(error, Error::Workspace(_)), "{error}");
}
#[test]
fn both_ancestor_searches_name_an_ancestor_whose_configuration_is_broken() {
let fs = MemoryFs::new()
.with_file("/r/package.json", r#"{"workspaces": ["p/*"]}"#)
.with_file("/r/skif.toml", "[lint\n")
.with_file("/r/p/m/package.json", r#"{"name": "m"}"#);
let start = Path::new("/r/p/m");
assert_eq!(crate::find_workspace_root(start, &fs).unwrap(), Path::new("/r"));
let error = find_target(start, &fs, &ConfigOverrides::default()).unwrap_err();
assert!(matches!(error, Error::Config(_)), "{error}");
}
#[test]
fn unapplied_files_of_all_targets_match_the_per_target_answers() {
let fs = MemoryFs::new()
.with_file("/r/package.json", r#"{"name": "root", "workspaces": ["packages/*"]}"#)
.with_file("/r/packages/a/package.json", r#"{"name": "a"}"#)
.with_file("/r/packages/a/skif.toml", "")
.with_file("/r/stray/skif.toml", "")
.with_file("/r/inner/pnpm-workspace.yaml", "packages:\n - 'q/*'\n")
.with_file("/r/inner/q/n/package.json", r#"{"name": "n"}"#)
.with_file("/r/inner/q/n/skif.toml", "")
.with_file("/r/inner/loose/skif.toml", "");
let targets = discover(&fs, "/r").targets;
assert_eq!(names(&discover(&fs, "/r")), ["/r", "/r/inner"]);
let roots: Vec<&Path> = targets.iter().map(|target| target.root.as_path()).collect();
let expected: Vec<Vec<UnappliedConfig>> = targets
.iter()
.map(|target| unapplied_config_files(target, &roots, &fs))
.collect();
assert_eq!(unapplied_config_files_all(&targets, &fs), expected);
let paths =
|files: &[UnappliedConfig]| -> Vec<PathBuf> { files.iter().map(|file| file.path.clone()).collect() };
assert_eq!(paths(&expected[0]), [PathBuf::from("/r/stray/skif.toml")]);
assert_eq!(paths(&expected[1]), [PathBuf::from("/r/inner/loose/skif.toml")]);
}
#[test]
fn only_standalone_packages_below_another_root_are_nested() {
let fs = MemoryFs::new()
.with_file("/s/a/package.json", r#"{"name": "a"}"#)
.with_file("/s/b/package.json", r#"{"name": "b"}"#)
.with_file("/s/b/inner/package.json", r#"{"name": "inner"}"#)
.with_file("/s/w/package.json", r#"{"name": "w", "workspaces": ["p/*"]}"#)
.with_file("/s/w/p/m/package.json", r#"{"name": "m"}"#)
.with_file("/s/w/examples/e/package.json", r#"{"name": "e"}"#)
.with_file("/s/w/examples/e/deeper/pnpm-workspace.yaml", "packages:\n - 'q/*'\n")
.with_file("/s/w/examples/e/deeper/q/n/package.json", r#"{"name": "n"}"#);
let discovery = discover(&fs, "/s");
let nested: Vec<_> = discovery
.targets
.iter()
.map(|target| {
(
target.root.display().to_string().replace('\\', "/"),
target.workspace.is_nested(),
)
})
.collect();
assert_eq!(
nested,
[
("/s/a".to_owned(), false),
("/s/b".to_owned(), false),
("/s/b/inner".to_owned(), true),
("/s/w".to_owned(), false),
("/s/w/examples/e".to_owned(), true),
("/s/w/examples/e/deeper".to_owned(), false),
]
);
}
#[test]
fn a_single_root_is_never_nested() {
let fs = MemoryFs::new()
.with_file("/r/package.json", r#"{"name": "r"}"#)
.with_file("/r/inner/package.json", r#"{"name": "inner"}"#);
let target = find_target(Path::new("/r/inner"), &fs, &ConfigOverrides::default()).unwrap();
assert!(!target.workspace.is_nested());
}
#[test]
fn a_workspace_file_without_a_root_manifest_is_a_workspace_and_not_a_failure() {
let fs = MemoryFs::new()
.with_file("/r/ws/pnpm-workspace.yaml", "packages:\n - 'p/*'\n")
.with_file("/r/ws/p/m/package.json", r#"{"name": "m"}"#);
let discovery = discover(&fs, "/r");
assert!(discovery.failures.is_empty(), "{:?}", discovery.failures);
assert_eq!(names(&discovery), ["/r/ws"]);
assert!(!discovery.targets[0].workspace.has_root_manifest());
assert_eq!(packages_of(&discovery.targets[0]), ["m"]);
let direct = find_target(Path::new("/r/ws"), &fs, &ConfigOverrides::default()).unwrap();
assert_eq!(direct.root, Path::new("/r/ws"));
assert_eq!(packages_of(&direct), ["m"]);
}
#[test]
fn a_rush_workspace_is_one_root_with_its_projects_as_members() {
let fs = MemoryFs::new()
.with_file(
"/r/rush.json",
r#"{"npmVersion": "10.0.0", "projects": [{"packageName": "a", "projectFolder": "apps/a"}]}"#,
)
.with_file("/r/apps/a/package.json", r#"{"name": "a"}"#)
.with_file("/r/apps/a/examples/e/package.json", r#"{"name": "e"}"#);
let discovery = discover(&fs, "/r");
assert_eq!(names(&discovery), ["/r"]);
assert_eq!(packages_of(&discovery.targets[0]), ["a"]);
assert_eq!(
discovery.targets[0].workspace.package_manager,
Some(PackageManager::Npm)
);
}
#[test]
fn fixture_directories_are_skipped_unless_the_configuration_says_otherwise() {
let tree = MemoryFs::new()
.with_file("/r/real/package.json", r#"{"name": "real"}"#)
.with_file("/r/real/fixtures/f/package.json", r#"{"name": "f"}"#)
.with_file("/r/examples/e/package.json", r#"{"name": "e"}"#);
assert_eq!(names(&discover(&tree, "/r")), ["/r/examples/e", "/r/real"]);
let replaced = tree
.clone()
.with_file("/r/skif.toml", "[workspace]\ndiscover-ignore = [\"examples\"]\n");
assert_eq!(names(&discover(&replaced, "/r")), ["/r/real", "/r/real/fixtures/f"]);
let none = tree.with_file("/r/skif.toml", "[workspace]\ndiscover-ignore = []\n");
assert_eq!(
names(&discover(&none, "/r")),
["/r/examples/e", "/r/real", "/r/real/fixtures/f"]
);
}
}