From f2c817308b5f5400a822658a54c4fb8f0a9bbd7e Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 27 Feb 2026 08:42:07 -0800 Subject: [PATCH] fix!: stop resolving node path via whichnode BREAKING CHANGE: npm will no longer attempt to resolve the path to node via whichnode. process.execPath is already set by Node to the resolved real path of the node binary, so the lookup was redundant. Scripts that expected npm to override process.execPath with a PATH-resolved (potentially symlinked) node path may be affected. --- lib/npm.js | 11 --- .../test/lib/cli/exit-handler.js.test.cjs | 2 - .../test/lib/commands/install.js.test.cjs | 12 ++-- test/lib/npm.js | 69 +------------------ 4 files changed, 7 insertions(+), 87 deletions(-) diff --git a/lib/npm.js b/lib/npm.js index b2ab377e95d71..2b4b2843e9218 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -1,6 +1,5 @@ const { resolve, dirname, join } = require('node:path') const Config = require('@npmcli/config') -const which = require('which') const fs = require('node:fs/promises') const { definitions, flatten, nerfDarts, shorthands } = require('@npmcli/config/lib/definitions') const usage = require('./utils/npm-usage.js') @@ -82,16 +81,6 @@ class Npm { } async #load () { - await time.start('npm:load:whichnode', async () => { - // TODO should we throw here? - const node = await which(process.argv[0]).catch(() => {}) - if (node && node.toUpperCase() !== process.execPath.toUpperCase()) { - log.verbose('node symlink', node) - process.execPath = node - this.config.execPath = node - } - }) - await time.start('npm:load:configload', () => this.config.load()) // npm --versions diff --git a/tap-snapshots/test/lib/cli/exit-handler.js.test.cjs b/tap-snapshots/test/lib/cli/exit-handler.js.test.cjs index fd68eea57795b..29865aadf56ed 100644 --- a/tap-snapshots/test/lib/cli/exit-handler.js.test.cjs +++ b/tap-snapshots/test/lib/cli/exit-handler.js.test.cjs @@ -6,7 +6,6 @@ */ 'use strict' exports[`test/lib/cli/exit-handler.js TAP handles unknown error with logs and debug file > debug file contents 1`] = ` -XX timing npm:load:whichnode Completed in {TIME}ms XX silly config load:file:{CWD}/npmrc XX silly config load:file:{CWD}/prefix/.npmrc XX silly config load:file:{CWD}/home/.npmrc @@ -36,7 +35,6 @@ XX error A complete log of this run can be found in: {CWD}/cache/_logs/{DATE}-de ` exports[`test/lib/cli/exit-handler.js TAP handles unknown error with logs and debug file > logs 1`] = ` -timing npm:load:whichnode Completed in {TIME}ms silly config load:file:{CWD}/npmrc silly config load:file:{CWD}/prefix/.npmrc silly config load:file:{CWD}/home/.npmrc diff --git a/tap-snapshots/test/lib/commands/install.js.test.cjs b/tap-snapshots/test/lib/commands/install.js.test.cjs index dd618ee3688f1..1d0a2733a9d2b 100644 --- a/tap-snapshots/test/lib/commands/install.js.test.cjs +++ b/tap-snapshots/test/lib/commands/install.js.test.cjs @@ -135,8 +135,8 @@ verbose stack Error: The developer of this package has specified the following t verbose stack Invalid devEngines.runtime verbose stack Invalid name "nondescript" does not match "node" for "runtime" verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:247:27) -verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:292:7) -verbose stack at MockNpm.exec ({CWD}/lib/npm.js:193:9) +verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:281:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:182:9) error code EBADDEVENGINES error EBADDEVENGINES The developer of this package has specified the following through devEngines error EBADDEVENGINES Invalid devEngines.runtime @@ -200,8 +200,8 @@ verbose stack Error: The developer of this package has specified the following t verbose stack Invalid devEngines.runtime verbose stack Invalid name "nondescript" does not match "node" for "runtime" verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:247:27) -verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:292:7) -verbose stack at MockNpm.exec ({CWD}/lib/npm.js:193:9) +verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:281:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:182:9) error code EBADDEVENGINES error EBADDEVENGINES The developer of this package has specified the following through devEngines error EBADDEVENGINES Invalid devEngines.runtime @@ -226,8 +226,8 @@ verbose stack Error: The developer of this package has specified the following t verbose stack Invalid devEngines.runtime verbose stack Invalid name "nondescript" does not match "node" for "runtime" verbose stack at Install.checkDevEngines ({CWD}/lib/base-cmd.js:247:27) -verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:292:7) -verbose stack at MockNpm.exec ({CWD}/lib/npm.js:193:9) +verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:281:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:182:9) error code EBADDEVENGINES error EBADDEVENGINES The developer of this package has specified the following through devEngines error EBADDEVENGINES Invalid devEngines.runtime diff --git a/test/lib/npm.js b/test/lib/npm.js index c4c0d720e86e8..f744d0376e9ea 100644 --- a/test/lib/npm.js +++ b/test/lib/npm.js @@ -1,5 +1,5 @@ const t = require('tap') -const { resolve, dirname, join } = require('node:path') +const { resolve, join } = require('node:path') const fs = require('node:fs/promises') const { time } = require('proc-log') const { load: loadMockNpm } = require('../fixtures/mock-npm.js') @@ -88,73 +88,6 @@ t.test('npm.load', async t => { ]) }) - await t.test('node is a symlink', async t => { - const node = process.platform === 'win32' ? 'node.exe' : 'node' - const { Npm, npm, logs, outputs, prefix } = await loadMockNpm(t, { - prefixDir: { - bin: t.fixture('symlink', dirname(process.execPath)), - }, - config: { - timing: true, - usage: '', - scope: 'foo', - }, - argv: [ - 'token', - 'revoke', - 'blergggg', - ], - globals: (dirs) => ({ - 'process.env.PATH': resolve(dirs.prefix, 'bin'), - 'process.argv': [ - node, - process.argv[1], - ], - }), - }) - - t.equal(npm.config.get('scope'), '@foo', 'added the @ sign to scope') - - t.match([ - ...logs.timing.filter((p) => p.startsWith('npm:load:whichnode')), - ...logs.verbose, - ...logs.timing.filter((p) => p.startsWith('npm:load')), - ], [ - /npm:load:whichnode Completed in [0-9.]+ms/, - `node symlink ${resolve(prefix, 'bin', node)}`, - /title npm token revoke blergggg/, - /argv "token" "revoke" "blergggg".*"--usage" "--scope" "foo"/, - /logfile logs-max:\d+ dir:.*/, - /logfile .*-debug-0.log/, - /npm:load:.* Completed in [0-9.]+ms/, - ]) - t.equal(process.execPath, resolve(prefix, 'bin', node)) - - outputs.length = 0 - logs.length = 0 - await npm.exec('ll', []) - - t.equal(npm.command, 'll', 'command set to first npm command') - t.equal(npm.flatOptions.npmCommand, 'll', 'npmCommand flatOption set') - - const ll = Npm.cmd('ll') - t.same(outputs, [ll.describeUsage], 'print usage') - npm.config.set('usage', false) - - outputs.length = 0 - logs.length = 0 - await npm.exec('get', ['scope', 'usage']) - - t.strictSame([npm.command, npm.flatOptions.npmCommand], ['ll', 'll'], - 'does not change npm.command when another command is called') - - t.match(logs, [ - /timing config:load:flatten Completed in [0-9.]+ms/, - /timing command:config Completed in [0-9.]+ms/, - ]) - t.same(outputs, ['scope=@foo\nusage=false']) - }) - await t.test('--no-workspaces with --workspace', async t => { const { npm } = await loadMockNpm(t, { prefixDir: {