Skip to content

Commit d239360

Browse files
authored
fix(cli): default unpinned npm to Node's bundled version (#2742)
Close #2726 An npm lockfile now selects npm without requesting its latest release. When no npm version is pinned or configured as the default, vp commands and npm/npx shims use the npm bundled with the selected Node.js runtime. Explicit npm versions and configured defaults keep their precedence. Direct shims and vp commands share default-version resolution after selecting the package-manager family, while keeping their independent environment overrides. `vp env current` reports the bundled npm version and source. Explicit standalone installation with `vp env install npm` keeps its registry fallback. 🤖 Generated with Codex
1 parent 4adaef5 commit d239360

22 files changed

Lines changed: 588 additions & 203 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const assert = require('node:assert/strict');
2+
const { execFileSync } = require('node:child_process');
3+
4+
const installed = JSON.parse(
5+
execFileSync('vp', ['env', 'list', 'npm', '--json'], { encoding: 'utf8' }),
6+
).package_managers.npm;
7+
assert.equal(installed.length, 1);
8+
assert.match(installed[0].version, /^\d+\.\d+\.\d+/);
9+
assert.equal(installed[0].current, false);
10+
assert.equal(installed[0].default, false);
11+
console.log('Standalone npm is installed but is not current');

‎crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_no_arg/snapshots.toml‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ steps = [
1313
[[case]]
1414
name = "command_env_install_standalone_npm_fallback"
1515
vp = "global"
16-
comment = "Explicit npm family scopes use standalone registry npm; only the directly invoked npm shim keeps Node.js' bundled npm fallback."
16+
comment = "Explicit npm installation uses standalone registry npm without installing Node.js."
1717
local-registry = true
1818
skip-platforms = ["windows"]
1919
seed-runtime = false
2020
env = { VP_ENV_USE_EVAL_ENABLE = "1", VP_SHELL = "bash" }
2121
steps = [
2222
{ argv = ["vp", "env", "use", "npm", "--no-install"], comment = "an explicit npm scope exports the standalone npm fallback" },
2323
{ argv = ["vp", "env", "install", "npm"], comment = "an explicit npm scope installs the standalone registry fallback" },
24-
{ argv = ["vp", "env", "current", "npm", "--json"], comment = "the standalone npm fallback is installed" },
2524
{ argv = ["vpt", "stat-file", "$VP_HOME/js_runtime/node", "--assert", "missing"], comment = "installing standalone npm does not install Node.js" },
25+
{ argv = ["vp", "env", "install", "22.18.0"], snapshot = false, timeout = 120000 },
26+
{ argv = ["node", "assert-installed-npm.cjs"], envs = [["VP_NODE_VERSION", "22.18.0"]], comment = "the standalone npm fallback is installed but is not selected" },
27+
{ argv = ["vp", "env", "current", "npm", "--json"], envs = [["VP_NODE_VERSION", "22.18.0"]], comment = "installing standalone npm does not select it: current reports Node's bundled npm until a version is configured" },
2628
]

‎crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/command_env_install_no_arg/snapshots/command_env_install_standalone_npm_fallback.md‎

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# command_env_install_standalone_npm_fallback
22

3-
Explicit npm family scopes use standalone registry npm; only the directly invoked npm shim keeps Node.js' bundled npm fallback.
3+
Explicit npm installation uses standalone registry npm without installing Node.js.
44

55
## `vp env use npm --no-install`
66

@@ -22,30 +22,41 @@ Installing npm <version>...
2222
Installed npm <version>
2323
```
2424

25-
## `vp env current npm --json`
25+
## `vpt stat-file $VP_HOME/js_runtime/node --assert missing`
26+
27+
installing standalone npm does not install Node.js
28+
29+
```
30+
<home>/.vite-plus/js_runtime/node: missing
31+
```
32+
33+
## `vp env install 22.18.0`
34+
2635

27-
the standalone npm fallback is installed
36+
## `VP_NODE_VERSION=22.18.0 node assert-installed-npm.cjs`
37+
38+
the standalone npm fallback is installed but is not selected
39+
40+
```
41+
Standalone npm is installed but is not current
42+
```
43+
44+
## `VP_NODE_VERSION=22.18.0 vp env current npm --json`
45+
46+
installing standalone npm does not select it: current reports Node's bundled npm until a version is configured
2847

2948
```
3049
{
3150
"package_manager": {
3251
"name": "npm",
3352
"version": "<version>",
34-
"source": "registry fallback",
53+
"source": "Node.js bundled npm",
3554
"bin_paths": {
36-
"npm": "<home>/.vite-plus/package_manager/npm/<version>/npm/bin/npm",
37-
"npx": "<home>/.vite-plus/package_manager/npm/<version>/npm/bin/npx"
55+
"npm": "<home>/.vite-plus/js_runtime/node/<version>/bin/npm",
56+
"npx": "<home>/.vite-plus/js_runtime/node/<version>/bin/npx"
3857
},
3958
"installed": true,
4059
"mode": "managed"
4160
}
4261
}
4362
```
44-
45-
## `vpt stat-file $VP_HOME/js_runtime/node --assert missing`
46-
47-
installing standalone npm does not install Node.js
48-
49-
```
50-
<home>/.vite-plus/js_runtime/node: missing
51-
```
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const assert = require('node:assert/strict');
2+
const { execSync } = require('node:child_process');
3+
const { dirname, resolve } = require('node:path');
4+
5+
const bundled = require(resolve(
6+
dirname(process.execPath),
7+
process.platform === 'win32' ? 'node_modules/npm/package.json' : '../lib/node_modules/npm/package.json',
8+
)).version;
9+
const expected = process.argv[2] || bundled;
10+
const run = (command) => execSync(command, { encoding: 'utf8' }).trim();
11+
12+
// Clear inherited tool markers so each shim resolves this project's configuration itself.
13+
delete process.env.VP_PATH_INJECTED_TOOLS;
14+
assert.equal(run('npm --version'), expected);
15+
assert.equal(run('npx --version'), expected);
16+
assert.equal(JSON.parse(run('vp pm version --json')).npm, expected);
17+
for (const scope of ['pm', 'npm']) {
18+
const current = JSON.parse(run(`vp env current ${scope} --json`)).package_manager;
19+
assert.equal(current.version, expected);
20+
if (!process.argv[2]) {
21+
assert.equal(current.source, 'Node.js bundled npm');
22+
}
23+
}
24+
console.log(process.argv[2] ? `npm/npx and vp use configured npm ${expected}` : 'npm/npx and vp use Node bundled npm');

‎crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/npm_bundled_default/package-lock.json‎

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "npm-bundled-default",
3+
"private": true,
4+
"devEngines": {
5+
"runtime": { "name": "node", "version": "22.18.0" }
6+
}
7+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
[[case]]
2+
name = "npm_lockfile_uses_bundled_version"
3+
vp = "global"
4+
steps = [
5+
{ argv = ["node", "assert-npm.cjs"], comment = "A Node-only devEngines declaration and npm lockfile use bundled npm without querying the registry", envs = [["NPM_CONFIG_REGISTRY", "http://127.0.0.1:9"]] },
6+
{ argv = ["vp", "pm", "patch", "example"], comment = "Bundled npm retains its version gates for unsupported commands" },
7+
{ argv = ["vp", "pm", "approve-builds"], comment = "Bundled npm does not invoke approval commands added in later npm releases" },
8+
{ argv = ["vp", "env", "default", "npm@10.5.0"], snapshot = false },
9+
{ argv = ["node", "assert-npm.cjs", "10.5.0"], comment = "An npm default overrides the bundled version even when a lockfile exists" },
10+
{ argv = ["vpt", "json-edit", "package.json", "devEngines.packageManager", "{\"name\":\"npm\",\"version\":\"10.9.4\"}"], snapshot = false },
11+
{ argv = ["node", "assert-npm.cjs", "10.9.4"], comment = "A devEngines npm version overrides the default" },
12+
{ argv = ["vpt", "json-edit", "package.json", "packageManager", "npm@10.8.2"], snapshot = false },
13+
{ argv = ["node", "assert-npm.cjs", "10.8.2"], comment = "A top-level npm pin still has priority" },
14+
]
15+
16+
[[case]]
17+
name = "npm_current_does_not_install_node"
18+
vp = "global"
19+
seed-runtime = false
20+
env = { VP_NODE_VERSION = "22.18.0", VP_NODE_DIST_MIRROR = "http://127.0.0.1:9" }
21+
steps = [
22+
{ argv = ["vp", "env", "current", "--json"], comment = "An absent Node runtime and bundled npm are reported without downloading" },
23+
{ argv = ["vp", "env", "current", "npm", "--json"], comment = "The explicit npm scope also reports an unknown, uninstalled version" },
24+
{ argv = ["vpt", "stat-file", "$VP_HOME/js_runtime/node/22.18.0", "--assert", "missing"], comment = "Reporting the environment leaves the runtime uninstalled" },
25+
]
26+
27+
[[case]]
28+
name = "npm_bundled_ignores_project_bin"
29+
vp = "global"
30+
skip-platforms = ["windows"]
31+
steps = [
32+
{ argv = ["vpt", "write-file", "bin/npm", "#!/bin/sh\necho project npm must not run\nexit 1\n"], snapshot = false },
33+
{ argv = ["vpt", "chmod", "+x", "bin/npm"], snapshot = false },
34+
{ argv = ["node", "assert-npm.cjs"], comment = "The version probe and actual command both use bundled npm despite an executable project bin/npm" },
35+
]
36+
37+
[[case]]
38+
name = "npm_bundled_env_print"
39+
vp = "global"
40+
env = { VP_SHELL = "bash", NPM_CONFIG_REGISTRY = "http://127.0.0.1:9" }
41+
steps = [
42+
{ argv = ["vp", "env", "print", "pm"], comment = "The selected package manager uses Node's bundled npm directory" },
43+
{ argv = ["vp", "env", "print", "npm"], comment = "The explicit npm scope uses the same bundled directory" },
44+
{ argv = ["vp", "env", "print"], comment = "The combined scope includes the shared directory only once" },
45+
]
46+
47+
[[case]]
48+
name = "npm_bundled_version_probe_ignores_preloads"
49+
vp = "global"
50+
env = { NODE_OPTIONS = "--require ./preload.cjs" }
51+
steps = [
52+
{ argv = ["vpt", "write-file", "preload.cjs", "console.log('preload start');\n"], snapshot = false },
53+
{ argv = ["vp", "pm", "patch", "example"], comment = "A noisy preload cannot bypass npm's patch version gate" },
54+
{ argv = ["vp", "pm", "approve-builds"], comment = "A noisy preload cannot bypass npm's approval version gate" },
55+
{ argv = ["vp", "pm", "version", "--json"], comment = "The actual npm command still executes the user's preload" },
56+
]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# npm_bundled_env_print
2+
3+
## `vp env print pm`
4+
5+
The selected package manager uses Node's bundled npm directory
6+
7+
```
8+
VITE+ - The Unified Toolchain for the Web
9+
10+
# Add to your shell to use this environment for this session:
11+
export PATH="<home>/.vite-plus/js_runtime/node/<version>/bin:$PATH"
12+
```
13+
14+
## `vp env print npm`
15+
16+
The explicit npm scope uses the same bundled directory
17+
18+
```
19+
VITE+ - The Unified Toolchain for the Web
20+
21+
# Add to your shell to use this environment for this session:
22+
export PATH="<home>/.vite-plus/js_runtime/node/<version>/bin:$PATH"
23+
```
24+
25+
## `vp env print`
26+
27+
The combined scope includes the shared directory only once
28+
29+
```
30+
VITE+ - The Unified Toolchain for the Web
31+
32+
# Add to your shell to use this environment for this session:
33+
export PATH="<home>/.vite-plus/js_runtime/node/<version>/bin:$PATH"
34+
```
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# npm_bundled_ignores_project_bin
2+
3+
## `vpt write-file bin/npm '#'\!'/bin/sh
4+
echo project npm must not run
5+
exit 1
6+
'`
7+
8+
9+
## `vpt chmod +x bin/npm`
10+
11+
12+
## `node assert-npm.cjs`
13+
14+
The version probe and actual command both use bundled npm despite an executable project bin/npm
15+
16+
```
17+
npm/npx and vp use Node bundled npm
18+
```
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# npm_bundled_version_probe_ignores_preloads
2+
3+
## `vpt write-file preload.cjs 'console.log('\''preload start'\'');
4+
'`
5+
6+
7+
## `vp pm patch example`
8+
9+
A noisy preload cannot bypass npm's patch version gate
10+
11+
```
12+
warn: npm does not have a 'patch' command.
13+
```
14+
15+
## `vp pm approve-builds`
16+
17+
A noisy preload cannot bypass npm's approval version gate
18+
19+
```
20+
warn: npm runs lifecycle scripts by default. Upgrade to npm >= 11.16.0 for `npm approve-scripts`/`deny-scripts`, or set `ignore-scripts=true` in .npmrc and rebuild approved packages with `vp pm rebuild <package>`.
21+
```
22+
23+
## `vp pm version --json`
24+
25+
The actual npm command still executes the user's preload
26+
27+
```
28+
preload start
29+
{
30+
"npm": "10.9.3",
31+
"node": "22.18.0",
32+
"acorn": "8.15.0",
33+
"ada": "2.9.2",
34+
"amaro": "1.1.0",
35+
"ares": "1.34.5",
36+
"brotli": "1.1.0",
37+
"cjs_module_lexer": "2.1.0",
38+
"cldr": "47.0",
39+
"icu": "77.1",
40+
"llhttp": "9.3.0",
41+
"modules": "127",
42+
"napi": "10",
43+
"nbytes": "0.1.1",
44+
"ncrypto": "0.0.1",
45+
"nghttp2": "1.64.0",
46+
"openssl": "3.0.16",
47+
"simdjson": "3.13.0",
48+
"simdutf": "6.4.2",
49+
"sqlite": "3.50.2",
50+
"tz": "2025b",
51+
"undici": "6.21.2",
52+
"unicode": "16.0",
53+
"uv": "1.51.0",
54+
"uvwasi": "0.0.21",
55+
"v8": "12.4.254.21-node.27",
56+
"zlib": "1.3.1-470d3a2",
57+
"zstd": "1.5.7"
58+
}
59+
```

0 commit comments

Comments
 (0)