#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::path::Path;
use std::sync::Arc;
use skif_config::Config;
use skif_core::{Diagnostic, FileSystem, MemoryFs};
use skif_js::{JsProvider, PluginSource};
use skif_lint::{Registry, RunOptions, run_unsorted};
use skif_workspace::Workspace;
const ROOT: &str = "/ws";
const WORKSPACE: &[(&str, &str)] = &[
(
"package.json",
r#"{"name":"root","private":true,"workspaces":["packages/*"],"devDependencies":{"typescript":"5.0.0","test-kit":"1.0.0","shared-types":"1.0.0","@tsconfig/strictest":"2.0.0"}}"#,
),
(
"packages/app/package.json",
r#"{"name":"app","version":"1.0.0","private":true,"main":"src/index.ts","dependencies":{"own-dep":"1.0.0"},"devDependencies":{"own-dev":"1.0.0"}}"#,
),
(
"packages/app/tsconfig.json",
r#"{"extends":"@tsconfig/strictest/tsconfig.json"}"#,
),
(
"packages/app/src/index.ts",
"import own from 'own-dep';\nimport dev from 'own-dev';\nimport kit from 'test-kit';\nimport type { Shape } from 'shared-types';\nimport lib from 'lib';\nexport const all: Shape = [own, dev, kit, lib];\n",
),
(
"packages/lib/package.json",
r#"{"name":"lib","version":"1.0.0","main":"src/index.ts"}"#,
),
("packages/lib/src/index.ts", "export default 1;\n"),
];
fn lint(toml: &str) -> Vec<Diagnostic> {
lint_files(WORKSPACE, toml)
}
fn lint_files(files: &[(&str, &str)], toml: &str) -> Vec<Diagnostic> {
let memory = files.iter().fold(MemoryFs::new(), |memory, (path, text)| {
memory.with_file(Path::new(ROOT).join(path), text)
});
let config = Config::from_toml_str(toml).unwrap();
let workspace = Workspace::discover_with_fs(Path::new(ROOT), &config, &memory).unwrap();
let fs: Arc<dyn FileSystem> = Arc::new(memory);
let provider = JsProvider::new(fs).with_input_source(Arc::new(PluginSource::new(config.js.plugins.clone())));
let registry = Registry::new(vec![Box::new(provider)]).unwrap();
run_unsorted(®istry, &workspace, &config, &RunOptions::default()).unwrap()
}
fn found(diagnostics: &[Diagnostic], rule: &str) -> Vec<String> {
let mut out: Vec<String> = diagnostics
.iter()
.filter(|d| d.rule.as_str() == rule)
.map(|d| {
format!(
"{}: {}",
d.location.path.display().to_string().replace('\\', "/"),
d.dependency.clone().unwrap_or_default()
)
})
.collect();
out.sort();
out
}
#[test]
fn isolation_checks_every_package_against_its_own_manifest_in_the_default_mode() {
let cases: [(&str, &[&str]); 4] = [
("", &[]),
(
"[js]\nisolate-workspaces = true\n",
&[
"packages/app/src/index.ts: lib",
"packages/app/src/index.ts: shared-types",
"packages/app/src/index.ts: test-kit",
"packages/app/tsconfig.json: @tsconfig/strictest",
],
),
(
"[js]\nstrict = true\n",
&[
"packages/app/src/index.ts: lib",
"packages/app/src/index.ts: own-dev",
"packages/app/src/index.ts: test-kit",
],
),
(
"[js]\nproduction = true\nisolate-workspaces = true\n",
&[
"packages/app/src/index.ts: lib",
"packages/app/src/index.ts: shared-types",
"packages/app/src/index.ts: test-kit",
],
),
];
for (toml, expected) in cases {
assert_eq!(found(&lint(toml), "unlisted-dependencies"), expected, "{toml:?}");
}
}
#[test]
fn isolation_without_production_mode_reports_an_ancestor_development_dependency_only_members_use() {
let unused = "package.json: test-kit".to_owned();
let default = found(&lint(""), "unused-dev-dependencies");
assert!(!default.contains(&unused), "{default:?}");
let isolated = found(&lint("[js]\nisolate-workspaces = true\n"), "unused-dev-dependencies");
assert!(isolated.contains(&unused), "{isolated:?}");
assert_eq!(
found(&lint("[js]\nstrict = true\n"), "unused-dev-dependencies"),
Vec::<String>::new()
);
}
#[test]
fn isolation_lets_tests_and_configs_use_the_packages_own_development_dependencies() {
let files: &[(&str, &str)] = &[
(
"package.json",
r#"{"name":"root","private":true,"workspaces":["packages/*"],"devDependencies":{"typescript":"5.0.0","root-kit":"1.0.0"}}"#,
),
(
"packages/app/package.json",
r#"{"name":"app","version":"1.0.0","private":true,"main":"src/index.ts","devDependencies":{"vitest":"3.0.0","own-plugin":"1.0.0","own-dev":"1.0.0","@tsconfig/node22":"22.0.0"}}"#,
),
(
"packages/app/tsconfig.json",
r#"{"extends":"@tsconfig/node22/tsconfig.json"}"#,
),
(
"packages/app/vitest.config.ts",
"import plugin from 'own-plugin';\nexport default { plugins: [plugin()] };\n",
),
("packages/app/src/index.ts", "export const one = 1;\n"),
(
"packages/app/src/index.test.ts",
"import dev from 'own-dev';\nimport kit from 'root-kit';\nimport { one } from './index';\nexport const all = [dev, kit, one];\n",
),
];
assert_eq!(
found(&lint_files(files, ""), "unlisted-dependencies"),
Vec::<String>::new()
);
assert_eq!(
found(
&lint_files(files, "[js]\nisolate-workspaces = true\n"),
"unlisted-dependencies"
),
["packages/app/src/index.test.ts: root-kit"]
);
}