use crate::snippets::error::Result;
use crate::snippets::scratch::ScratchDir;
use std::path::{Path, PathBuf};
const TYPES_NODE_MARKER: &str = "node_modules/@types/node/package.json";
fn nearest_ancestor_with_types_node(directory: &Path) -> Option<PathBuf> {
directory
.ancestors()
.find(|candidate| candidate.join(TYPES_NODE_MARKER).is_file())
.map(Path::to_path_buf)
}
fn real_snippet_directory(snippet_path: &Path) -> Option<PathBuf> {
snippet_path
.canonicalize()
.ok()
.and_then(|canonical| canonical.parent().map(Path::to_path_buf))
}
pub(super) fn resolve_isolated_scratch(anchor: Option<&Path>, timeout_secs: u64) -> Result<(ScratchDir, bool)> {
let project_root = anchor
.and_then(real_snippet_directory)
.and_then(|directory| nearest_ancestor_with_types_node(&directory));
match project_root {
Some(root) => Ok((ScratchDir::rooted(&root, timeout_secs)?, true)),
None => Ok((ScratchDir::isolated()?, false)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_types_node_at_the_directory_itself() {
let root = tempfile::tempdir().expect("project root");
std::fs::create_dir_all(root.path().join("node_modules/@types/node")).expect("types/node directory");
std::fs::write(root.path().join("node_modules/@types/node/package.json"), "{}").expect("package.json");
assert_eq!(
nearest_ancestor_with_types_node(root.path()),
Some(root.path().to_path_buf())
);
}
#[test]
fn finds_types_node_several_levels_up() {
let root = tempfile::tempdir().expect("project root");
std::fs::create_dir_all(root.path().join("node_modules/@types/node")).expect("types/node directory");
std::fs::write(root.path().join("node_modules/@types/node/package.json"), "{}").expect("package.json");
let nested = root.path().join("docs-site/src/snippets/typescript/api");
std::fs::create_dir_all(&nested).expect("nested snippet directory");
assert_eq!(
nearest_ancestor_with_types_node(&nested),
Some(root.path().to_path_buf())
);
}
#[test]
fn the_nearest_ancestor_wins_over_a_more_distant_one() {
let root = tempfile::tempdir().expect("workspace root");
for level in ["", "packages/docs-site"] {
let types_dir = root.path().join(level).join("node_modules/@types/node");
std::fs::create_dir_all(&types_dir).expect("types/node directory");
std::fs::write(types_dir.join("package.json"), "{}").expect("package.json");
}
let nested = root.path().join("packages/docs-site/src/snippets");
std::fs::create_dir_all(&nested).expect("nested snippet directory");
assert_eq!(
nearest_ancestor_with_types_node(&nested),
Some(root.path().join("packages/docs-site"))
);
}
#[test]
fn no_ancestor_with_types_node_resolves_to_none() {
let root = tempfile::tempdir().expect("project root");
let nested = root.path().join("src/snippets");
std::fs::create_dir_all(&nested).expect("nested snippet directory");
assert_eq!(nearest_ancestor_with_types_node(&nested), None);
}
#[test]
fn an_empty_types_node_directory_without_a_package_json_does_not_count() {
let root = tempfile::tempdir().expect("project root");
std::fs::create_dir_all(root.path().join("node_modules/@types/node")).expect("types/node directory");
assert_eq!(nearest_ancestor_with_types_node(root.path()), None);
}
#[test]
fn a_synthetic_snippet_path_never_resolves_to_a_real_project_root() {
let synthetic = Path::new("snippet.ts");
assert_eq!(real_snippet_directory(synthetic), None);
}
#[test]
fn resolve_isolated_scratch_falls_back_to_the_os_temp_directory_with_no_anchor() {
let (scratch, types_node_resolved) = resolve_isolated_scratch(None, 5).expect("isolated scratch directory");
assert!(
scratch.path().starts_with(std::env::temp_dir()),
"no anchor must fall back to the OS temp directory: {}",
scratch.path().display()
);
assert!(
!types_node_resolved,
"no anchor must never report a resolved @types/node -- the caller uses this to decide \
whether `\"types\": [\"node\"]` is safe to name"
);
}
#[test]
fn resolve_isolated_scratch_nests_inside_a_resolved_real_project_root() {
let root = tempfile::tempdir().expect("project root");
let canonical_root = root.path().canonicalize().expect("canonical project root");
std::fs::create_dir_all(canonical_root.join("node_modules/@types/node")).expect("types/node directory");
std::fs::write(canonical_root.join("node_modules/@types/node/package.json"), "{}").expect("package.json");
let snippet_file = canonical_root.join("docs/example.md");
std::fs::create_dir_all(snippet_file.parent().unwrap()).expect("docs directory");
std::fs::write(&snippet_file, "example").expect("snippet source file");
let (scratch, types_node_resolved) =
resolve_isolated_scratch(Some(snippet_file.as_path()), 5).expect("rooted scratch directory");
assert!(
scratch
.path()
.starts_with(canonical_root.join(crate::snippets::scratch::SNIPPET_SCRATCH_ROOT)),
"a real project root with @types/node must nest scratch under its own {} convention, got {}",
crate::snippets::scratch::SNIPPET_SCRATCH_ROOT,
scratch.path().display()
);
assert!(
types_node_resolved,
"a resolved ancestor @types/node must be reported so the caller can safely name \
`\"types\": [\"node\"]`"
);
}
}