Skip to content

Commit e05555e

Browse files
authored
fix(env): stop inherited pin lookup at nearer runtime constraints
1 parent 7df05c0 commit e05555e

4 files changed

Lines changed: 139 additions & 14 deletions

File tree

crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_pin_nvmrc/snapshots.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,20 @@ steps = [
112112
["vpt", "print-file", ".nvmrc", "package.json"],
113113
]
114114

115+
[[case]]
116+
name = "engines_node_stops_inherited_pin_lookup"
117+
vp = "global"
118+
seed-runtime = false
119+
steps = [
120+
{ argv = ["vpt", "mkdir", "-p", "middle/leaf"], snapshot = false },
121+
{ argv = ["vpt", "write-file", "middle/package.json", "{\"engines\":{\"node\":\"22.13.0\"}}\n"], snapshot = false },
122+
{ argv = ["vp", "env", "pin"], cwd = "middle/leaf", comment = "the nearer engines.node prevents inheriting the root .nvmrc" },
123+
{ argv = ["vp", "env", "current", "node", "--json"], cwd = "middle/leaf" },
124+
{ argv = ["vp", "env", "pin"], cwd = "middle", comment = "a constraint in cwd also prevents reporting an ancestor pin" },
125+
{ argv = ["vp", "env", "current", "node", "--json"], cwd = "middle" },
126+
["vpt", "print-file", ".nvmrc", "middle/package.json"],
127+
]
128+
115129
[[case]]
116130
name = "nvmrc_target_rejects_package_manager_operations"
117131
vp = "global"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# engines_node_stops_inherited_pin_lookup
2+
3+
## `vpt mkdir -p middle/leaf`
4+
5+
6+
## `vpt write-file middle/package.json '{"engines":{"node":"22.13.0"}}
7+
'`
8+
9+
10+
## `cd middle/leaf && vp env pin`
11+
12+
the nearer engines.node prevents inheriting the root .nvmrc
13+
14+
```
15+
VITE+ - The Unified Toolchain for the Web
16+
17+
No version pinned.
18+
Node.js constraint: 22.13.0 from <workspace>/middle/package.json (engines.node)
19+
20+
No package manager pinned.
21+
```
22+
23+
## `cd middle/leaf && vp env current node --json`
24+
25+
```
26+
{
27+
"node": {
28+
"version": "22.13.0",
29+
"source": "engines.node",
30+
"source_path": "<workspace>/middle/package.json",
31+
"project_root": "<workspace>/middle",
32+
"bin_path": "<home>/.vite-plus/js_runtime/node/<version>/bin/node",
33+
"installed": false,
34+
"mode": "managed"
35+
}
36+
}
37+
```
38+
39+
## `cd middle && vp env pin`
40+
41+
a constraint in cwd also prevents reporting an ancestor pin
42+
43+
```
44+
VITE+ - The Unified Toolchain for the Web
45+
46+
No version pinned.
47+
Node.js constraint: 22.13.0 from <workspace>/middle/package.json (engines.node)
48+
49+
No package manager pinned.
50+
```
51+
52+
## `cd middle && vp env current node --json`
53+
54+
```
55+
{
56+
"node": {
57+
"version": "22.13.0",
58+
"source": "engines.node",
59+
"source_path": "<workspace>/middle/package.json",
60+
"project_root": "<workspace>/middle",
61+
"bin_path": "<home>/.vite-plus/js_runtime/node/<version>/bin/node",
62+
"installed": false,
63+
"mode": "managed"
64+
}
65+
}
66+
```
67+
68+
## `vpt print-file .nvmrc middle/package.json`
69+
70+
```
71+
# Node for local tools and CI
72+
<version> # keep this comment
73+
{"engines":{"node":"22.13.0"}}
74+
```

crates/vp_global_cli/src/commands/env/pin.rs

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,24 @@ async fn show_pinned(cwd: &AbsolutePathBuf) -> Result<ExitStatus, Error> {
137137
return Ok(ExitStatus::default());
138138
}
139139

140-
if let Some(resolution) = resolve_node_version(cwd, false).await?
141-
&& resolution.source == VersionSource::NvmrcFile
142-
{
143-
println!("Pinned version: {}", resolution.version);
144-
println!(" Source: {}", cwd.join(NVMRC_FILE).as_path().display());
145-
return Ok(ExitStatus::default());
140+
if let Some(resolution) = resolve_node_version(cwd, true).await? {
141+
if resolution.source == VersionSource::NvmrcFile
142+
&& resolution.project_root.as_ref() == Some(cwd)
143+
{
144+
println!("Pinned version: {}", resolution.version);
145+
println!(" Source: {}", cwd.join(NVMRC_FILE).as_path().display());
146+
return Ok(ExitStatus::default());
147+
}
148+
if resolution.source == VersionSource::EnginesNode {
149+
let path = resolution.source_path.unwrap_or_else(|| cwd.join(PACKAGE_JSON_FILE));
150+
println!("No version pinned.");
151+
println!(
152+
" Node.js constraint: {} from {} (engines.node)",
153+
resolution.version,
154+
path.as_path().display()
155+
);
156+
return Ok(ExitStatus::default());
157+
}
146158
}
147159

148160
// Check for inherited version from parent directories
@@ -194,13 +206,16 @@ async fn find_inherited_version(cwd: &AbsolutePathBuf) -> Result<Option<(String,
194206
format!("{} (devEngines.runtime)", dir.join(PACKAGE_JSON_FILE).as_path().display()),
195207
)));
196208
}
197-
if let Some(resolution) = resolve_node_version(&dir, false).await?
198-
&& resolution.source == VersionSource::NvmrcFile
199-
{
200-
return Ok(Some((
201-
resolution.version.to_string(),
202-
dir.join(NVMRC_FILE).as_path().display().to_string(),
203-
)));
209+
if let Some(resolution) = resolve_node_version(&dir, false).await? {
210+
if resolution.source == VersionSource::NvmrcFile {
211+
return Ok(Some((
212+
resolution.version.to_string(),
213+
dir.join(NVMRC_FILE).as_path().display().to_string(),
214+
)));
215+
}
216+
// A nearer runtime constraint blocks more distant pins, even if
217+
// this command does not treat that source as a writable pin.
218+
return Ok(None);
204219
}
205220
current = dir.parent().map(|p| p.to_absolute_path_buf());
206221
}
@@ -1254,6 +1269,28 @@ mod tests {
12541269
assert!(source.ends_with("package.json (devEngines.runtime)"), "got: {source}");
12551270
}
12561271

1272+
#[tokio::test]
1273+
async fn test_find_inherited_version_stops_at_nearer_engines_node() {
1274+
let temp_dir = TempDir::new().unwrap();
1275+
let root = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
1276+
let middle = root.join("middle");
1277+
let leaf = middle.join("leaf");
1278+
tokio::fs::create_dir_all(&leaf).await.unwrap();
1279+
tokio::fs::write(root.join(".nvmrc"), "20.18.0\n").await.unwrap();
1280+
tokio::fs::write(middle.join("package.json"), r#"{"engines":{"node":"22.13.0"}}"#)
1281+
.await
1282+
.unwrap();
1283+
1284+
let runtime = resolve_node_version(&leaf, true).await.unwrap().unwrap();
1285+
assert_eq!(runtime.source, VersionSource::EnginesNode);
1286+
assert!(find_inherited_version(&leaf).await.unwrap().is_none());
1287+
1288+
tokio::fs::remove_file(middle.join("package.json")).await.unwrap();
1289+
let (version, source) = find_inherited_version(&leaf).await.unwrap().unwrap();
1290+
assert_eq!(version, "20.18.0");
1291+
assert!(source.ends_with(".nvmrc"));
1292+
}
1293+
12571294
#[tokio::test]
12581295
async fn test_find_inherited_version_node_version_wins_over_dev_engines() {
12591296
let temp_dir = TempDir::new().unwrap();

rfcs/dev-engines.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ When `.node-version` is the write target and a `devEngines.runtime` node entry a
207207

208208
#### 2.5 `vp env pin` (show) and `vp env unpin`
209209

210-
- `vp env pin` with no argument reports the active pin and its source, including an effective `.nvmrc`. Inherited pins from parent directories are reported in the same order: `.node-version`, the `devEngines.runtime` node entry, then an effective `.nvmrc`.
210+
- `vp env pin` with no argument reports the active pin and its source, including an effective `.nvmrc`. Inherited pins from parent directories are reported in the same order: `.node-version`, the `devEngines.runtime` node entry, then an effective `.nvmrc`. A nearer `engines.node` stops the search and is displayed as a runtime constraint rather than inheriting a more distant pin.
211211
- `vp env unpin` / `vp env pin --unpin` removes the pin from the same target that `vp env pin` would write: delete `.node-version` if present, delete an effective `.nvmrc` in cwd, or remove the node entry from `devEngines.runtime` (removing the `devEngines.runtime` key entirely if it becomes empty, and `devEngines` if it becomes empty). An explicit `--target nvmrc` removes that file even if another source shadows it. Parent-directory files are never removed.
212212

213213
### 3. Package manager

0 commit comments

Comments
 (0)