From 27d73068b7dd0710eccee8dbf6d2da63d2f4c35a Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Fri, 27 Feb 2026 08:42:07 -0800 Subject: [PATCH] fix!: stop setting NODE env var and resolving node path via whichnode BREAKING CHANGE: npm will no longer attempt to resolve the path to node via whichnode, and will not set the NODE environment variable. This can affect scripts that rely on the NODE environment variable or expect npm to resolve the path to node. --- 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 +------------------ workspaces/config/lib/set-envs.js | 2 +- workspaces/config/test/set-envs.js | 5 -- 6 files changed, 8 insertions(+), 93 deletions(-) diff --git a/lib/npm.js b/lib/npm.js index d30554d400f07..65563604ecc6b 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') @@ -87,16 +86,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 6143eacb57ffa..27030da8ffedf 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:244:27) -verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:310:7) -verbose stack at MockNpm.exec ({CWD}/lib/npm.js:209:9) +verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:299:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:198: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:244:27) -verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:310:7) -verbose stack at MockNpm.exec ({CWD}/lib/npm.js:209:9) +verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:299:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:198: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:244:27) -verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:310:7) -verbose stack at MockNpm.exec ({CWD}/lib/npm.js:209:9) +verbose stack at MockNpm.execCommandClass ({CWD}/lib/npm.js:299:7) +verbose stack at MockNpm.exec ({CWD}/lib/npm.js:198: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: { diff --git a/workspaces/config/lib/set-envs.js b/workspaces/config/lib/set-envs.js index 12719b5647836..ddb2c8d4630f7 100644 --- a/workspaces/config/lib/set-envs.js +++ b/workspaces/config/lib/set-envs.js @@ -107,7 +107,7 @@ const setEnvs = (config) => { // this doesn't have a full definition so we manually export it here env.npm_config_npm_version = cliConf['npm-version'] || 'unknown' env.npm_execpath = config.npmBin - env.NODE = env.npm_node_execpath = config.execPath + env.npm_node_execpath = config.execPath } module.exports = setEnvs diff --git a/workspaces/config/test/set-envs.js b/workspaces/config/test/set-envs.js index c7af0faca33c0..51f000d3f1047 100644 --- a/workspaces/config/test/set-envs.js +++ b/workspaces/config/test/set-envs.js @@ -7,7 +7,6 @@ const { execPath } = process const cwd = process.cwd() const globalPrefix = join(cwd, 'global') const localPrefix = join(cwd, 'local') -const NODE = execPath const npmPath = '{path}' const npmBin = join(npmPath, 'bin/npm-cli.js') @@ -24,7 +23,6 @@ t.test('set envs that are not defaults and not already in env', t => { const envConf = Object.create(defaults) const cliConf = Object.create(envConf) const extras = { - NODE, INIT_CWD: cwd, EDITOR: 'vim', HOME: undefined, @@ -79,7 +77,6 @@ t.test('set envs that are not defaults and not already in env, array style', t = const envConf = Object.create(defaults) const cliConf = Object.create(envConf) const extras = { - NODE, INIT_CWD: cwd, EDITOR: 'vim', HOME: undefined, @@ -131,7 +128,6 @@ t.test('set envs that are not defaults and not already in env, boolean edition', const envConf = Object.create(defaults) const cliConf = Object.create(envConf) const extras = { - NODE, INIT_CWD: cwd, EDITOR: 'vim', HOME: undefined, @@ -210,7 +206,6 @@ t.test('dont set configs marked as envExport:false', t => { const envConf = Object.create(defaults) const cliConf = Object.create(envConf) const extras = { - NODE, INIT_CWD: cwd, EDITOR: 'vim', HOME: undefined,