use crate::error::Error;
use crate::spec::{NodeRequest, NodeSpec, RequestSource};
use std::path::Path;
pub fn find_version_file(start_dir: &Path) -> Option<NodeRequest> {
let home = aube_util::env::home_dir();
let mut dir = Some(start_dir);
while let Some(d) = dir {
for (file, source) in [
(".node-version", RequestSource::NodeVersionFile),
(".nvmrc", RequestSource::Nvmrc),
] {
let path = d.join(file);
let Ok(raw) = std::fs::read_to_string(&path) else {
continue;
};
let trimmed = first_meaningful_line(&raw);
if trimmed.is_empty() {
continue;
}
match NodeSpec::parse(trimmed) {
Ok(spec) => {
return Some(NodeRequest {
spec,
raw: trimmed.to_string(),
on_fail: aube_manifest::OnFail::Download,
source,
origin: path,
});
}
Err(_) => {
tracing::warn!(
path = %path.display(),
content = trimmed,
"ignoring unparseable node version file"
);
}
}
}
if home.as_deref() == Some(d) {
break;
}
dir = d.parent();
}
None
}
fn first_meaningful_line(raw: &str) -> &str {
raw.lines()
.map(str::trim)
.find(|l| !l.is_empty() && !l.starts_with('#'))
.unwrap_or("")
}
pub fn effective_request(
dev_engines: Option<(&str, Option<aube_manifest::OnFail>, &Path)>,
start_dir: &Path,
) -> Result<Option<NodeRequest>, Error> {
if let Some((range, on_fail, manifest_path)) = dev_engines {
let spec = NodeSpec::parse(range)?;
return Ok(Some(NodeRequest {
spec,
raw: range.to_string(),
on_fail: on_fail.unwrap_or(aube_manifest::OnFail::Error),
source: RequestSource::DevEngines,
origin: manifest_path.to_path_buf(),
}));
}
Ok(find_version_file(start_dir))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finds_nvmrc_in_parent() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join(".nvmrc"), "v22.1.0\n").unwrap();
let nested = tmp.path().join("a/b");
std::fs::create_dir_all(&nested).unwrap();
let req = find_version_file(&nested).unwrap();
assert_eq!(req.source, RequestSource::Nvmrc);
assert_eq!(req.spec, NodeSpec::Exact("22.1.0".parse().unwrap()));
assert_eq!(req.on_fail, aube_manifest::OnFail::Download);
}
#[test]
fn node_version_beats_nvmrc_in_same_dir() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join(".nvmrc"), "20").unwrap();
std::fs::write(tmp.path().join(".node-version"), "22").unwrap();
let req = find_version_file(tmp.path()).unwrap();
assert_eq!(req.source, RequestSource::NodeVersionFile);
}
#[test]
fn nearer_nvmrc_beats_farther_node_version() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join(".node-version"), "20").unwrap();
let nested = tmp.path().join("proj");
std::fs::create_dir_all(&nested).unwrap();
std::fs::write(nested.join(".nvmrc"), "22").unwrap();
let req = find_version_file(&nested).unwrap();
assert_eq!(req.source, RequestSource::Nvmrc);
}
#[test]
fn unparseable_file_is_skipped_and_walk_continues() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join(".nvmrc"), "22").unwrap();
let nested = tmp.path().join("proj");
std::fs::create_dir_all(&nested).unwrap();
std::fs::write(nested.join(".nvmrc"), "definitely not a version !!!").unwrap();
let req = find_version_file(&nested).unwrap();
assert_eq!(req.origin, tmp.path().join(".nvmrc"));
}
#[test]
fn comments_and_blank_lines_are_tolerated() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(
tmp.path().join(".nvmrc"),
"# pinned for CI\n\n lts/jod \n",
)
.unwrap();
let req = find_version_file(tmp.path()).unwrap();
assert_eq!(req.spec, NodeSpec::LtsCodename("jod".into()));
}
#[test]
fn no_file_returns_none() {
let tmp = tempfile::tempdir().unwrap();
let mut ancestor_has_file = false;
let mut d = tmp.path().parent();
while let Some(p) = d {
if p.join(".nvmrc").exists() || p.join(".node-version").exists() {
ancestor_has_file = true;
break;
}
d = p.parent();
}
if !ancestor_has_file {
assert!(find_version_file(tmp.path()).is_none());
}
}
#[test]
fn dev_engines_beats_version_files() {
let tmp = tempfile::tempdir().unwrap();
std::fs::write(tmp.path().join(".nvmrc"), "20").unwrap();
let manifest = tmp.path().join("package.json");
let req = effective_request(Some(("^22", None, manifest.as_path())), tmp.path())
.unwrap()
.unwrap();
assert_eq!(req.source, RequestSource::DevEngines);
assert_eq!(req.on_fail, aube_manifest::OnFail::Error);
}
#[test]
fn dev_engines_on_fail_is_honored() {
let tmp = tempfile::tempdir().unwrap();
let manifest = tmp.path().join("package.json");
let req = effective_request(
Some((
"^22",
Some(aube_manifest::OnFail::Download),
manifest.as_path(),
)),
tmp.path(),
)
.unwrap()
.unwrap();
assert_eq!(req.on_fail, aube_manifest::OnFail::Download);
}
}