From a07ab6f37b0c881f8d952587d9ded698310de1ea Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Mon, 5 Oct 2026 11:29:40 -0700 Subject: [PATCH] fix: start node once per call in the Windows npm/npx shims The Windows shims started node.exe once to print the global prefix (npm-prefix.js) and again to run npm; the bash shim, used by Git Bash, MSYS2 and Cygwin, started it a third time to turn its own directory into a Windows path. On Linux and macOS, npm is a symlink to npm-cli.js: one node start. bin/npm-shim.js makes the same choice (the npm in the global prefix if there is one, else this one) in the node process that then runs npm, and gives the chosen cli the argv it would see if run directly. npm.cmd, npx.cmd, npm.ps1 and npx.ps1 call it instead of npm-prefix.js; the .ps1 pipeline, -File and -Command branches are kept. bin/npm and bin/npx take a fast path under msys and Cygwin that calls it without subshells; elsewhere (WSL running the Windows install) they are unchanged. The config loading moves from npm-prefix.js to lib/cli/global-prefix.js, which npm-prefix.js and npm-shim.js share, so the shims' test fixture can steer the prefix while the real npm-shim.js runs. It loads config with a copy of the environment: load() exports npm_config_* variables, and the npm that runs next in the same process, possibly another version, must see the environment it would have seen on its own. --- bin/npm | 19 ++++++ bin/npm-prefix.js | 18 ++---- bin/npm-shim.js | 34 +++++++++++ bin/npm.cmd | 11 +--- bin/npm.ps1 | 23 ++------ bin/npx | 19 ++++++ bin/npx.cmd | 11 +--- bin/npx.ps1 | 23 ++------ lib/cli/global-prefix.js | 25 ++++++++ test/bin/npm-prefix.js | 34 +++++++++++ test/bin/npm-shim.js | 106 ++++++++++++++++++++++++++++++++++ test/bin/windows-shims.js | 14 ++++- test/lib/cli/global-prefix.js | 56 ++++++++++++++++++ 13 files changed, 323 insertions(+), 70 deletions(-) create mode 100755 bin/npm-shim.js create mode 100644 lib/cli/global-prefix.js create mode 100644 test/bin/npm-prefix.js create mode 100644 test/bin/npm-shim.js create mode 100644 test/lib/cli/global-prefix.js diff --git a/bin/npm b/bin/npm index 027dc9d128d22..d68e1b6d102d2 100755 --- a/bin/npm +++ b/bin/npm @@ -3,6 +3,25 @@ # This is used by the Node.js installer, which expects the cygwin/mingw # shell script to already be present in the npm dependency folder. +# Git Bash (msys) and Cygwin: start node once. npm-shim.js picks this npm or +# the one in the global prefix in the same node process, and msys converts the +# script path for node.exe, so this needs no subshells (Cygwin: one cygpath). +case "$OSTYPE" in + msys*|cygwin*) + set -o igncr 2>/dev/null # cygwin encoding fix + basedir=${0%/*} + [ "$basedir" = "$0" ] && basedir=. + case "$OSTYPE" in cygwin*) basedir=$(cygpath -w "$basedir");; esac + NODE_EXE="$basedir/node.exe" + [ -x "$NODE_EXE" ] || NODE_EXE="$basedir/node" + [ -x "$NODE_EXE" ] || NODE_EXE=node + "$NODE_EXE" "$basedir/node_modules/npm/bin/npm-shim.js" npm "$@" + exit $? + ;; +esac + +# Everything else, e.g. WSL running the Windows install: find the prefix first. + (set -o igncr) 2>/dev/null && set -o igncr; # cygwin encoding fix basedir=`dirname "$0"` diff --git a/bin/npm-prefix.js b/bin/npm-prefix.js index b0b0ace6a92ab..2c97e19debe61 100755 --- a/bin/npm-prefix.js +++ b/bin/npm-prefix.js @@ -2,25 +2,15 @@ // This is a single-use bin to help windows discover the proper prefix for npm // without having to load all of npm first // It does not accept argv params +// The Windows shims now make this decision in-process via npm-shim.js; this +// is still used by bin/npm and bin/npx outside Git Bash and Cygwin (WSL). -const path = require('node:path') -const Config = require('@npmcli/config') -const { definitions, flatten, shorthands } = require('@npmcli/config/lib/definitions') -const config = new Config({ - npmPath: path.dirname(__dirname), - // argv is explicitly not looked at since prefix is not something that can be changed via argv - argv: [], - definitions, - flatten, - shorthands, - excludeNpmCwd: false, -}) +const globalPrefix = require('../lib/cli/global-prefix.js') async function main () { try { - await config.load() // eslint-disable-next-line no-console - console.log(config.globalPrefix) + console.log(await globalPrefix()) } catch (err) { // eslint-disable-next-line no-console console.error(err) diff --git a/bin/npm-shim.js b/bin/npm-shim.js new file mode 100755 index 0000000000000..9d2310669f1d3 --- /dev/null +++ b/bin/npm-shim.js @@ -0,0 +1,34 @@ +#!/usr/bin/env node +// Entry point for the Windows shims (npm, npx, npm.cmd, npx.cmd, npm.ps1, +// npx.ps1): node npm-shim.js [args...] +// +// It runs the npm installed in the global prefix if there is one, else this +// one, like the shims did with npm-prefix.js, but decides in the node process +// that then runs npm instead of starting node a second time to print the +// prefix. + +const { join } = require('node:path') +const { existsSync } = require('node:fs') +const globalPrefix = require('../lib/cli/global-prefix.js') + +async function main () { + const [node, , bin, ...args] = process.argv + const cliName = bin === 'npx' ? 'npx-cli.js' : 'npm-cli.js' + let prefix + try { + prefix = await globalPrefix() + } catch (err) { + // eslint-disable-next-line no-console + console.error(err) + // eslint-disable-next-line no-console + console.error('Could not determine Node.js install directory') + process.exit(1) + return + } + const fromPrefix = join(prefix, 'node_modules', 'npm', 'bin', cliName) + const cli = existsSync(fromPrefix) ? fromPrefix : join(__dirname, cliName) + // the cli sees the same argv as when it is run directly + process.argv = [node, cli, ...args] + require(cli) +} +main() diff --git a/bin/npm.cmd b/bin/npm.cmd index 68af4b0fca09f..48cee72098a1a 100755 --- a/bin/npm.cmd +++ b/bin/npm.cmd @@ -8,13 +8,4 @@ IF NOT EXIST "%NODE_EXE%" ( SET "NODE_EXE=node" ) -SET "NPM_PREFIX_JS=%~dp0\node_modules\npm\bin\npm-prefix.js" -SET "NPM_CLI_JS=%~dp0\node_modules\npm\bin\npm-cli.js" -FOR /F "delims=" %%F IN ('CALL "%NODE_EXE%" "%NPM_PREFIX_JS%"') DO ( - SET "NPM_PREFIX_NPM_CLI_JS=%%F\node_modules\npm\bin\npm-cli.js" -) -IF EXIST "%NPM_PREFIX_NPM_CLI_JS%" ( - SET "NPM_CLI_JS=%NPM_PREFIX_NPM_CLI_JS%" -) - -"%NODE_EXE%" "%NPM_CLI_JS%" %* +"%NODE_EXE%" "%~dp0\node_modules\npm\bin\npm-shim.js" npm %* diff --git a/bin/npm.ps1 b/bin/npm.ps1 index efed03fe5655e..0142350a46462 100644 --- a/bin/npm.ps1 +++ b/bin/npm.ps1 @@ -10,24 +10,13 @@ if (-not (Test-Path $NODE_EXE)) { $NODE_EXE="node" } -$NPM_PREFIX_JS="$PSScriptRoot/node_modules/npm/bin/npm-prefix.js" -$NPM_CLI_JS="$PSScriptRoot/node_modules/npm/bin/npm-cli.js" -$NPM_PREFIX=(& $NODE_EXE $NPM_PREFIX_JS) - -if ($LASTEXITCODE -ne 0) { - Write-Host "Could not determine Node.js install directory" - exit 1 -} - -$NPM_PREFIX_NPM_CLI_JS="$NPM_PREFIX/node_modules/npm/bin/npm-cli.js" -if (Test-Path $NPM_PREFIX_NPM_CLI_JS) { - $NPM_CLI_JS=$NPM_PREFIX_NPM_CLI_JS -} +# npm-shim.js picks this npm or the one in the global prefix, in the same node process. +$NPM_SHIM_JS="$PSScriptRoot/node_modules/npm/bin/npm-shim.js" if ($MyInvocation.ExpectingInput) { # takes pipeline input - $input | & $NODE_EXE $NPM_CLI_JS $args + $input | & $NODE_EXE $NPM_SHIM_JS npm $args } elseif (-not $MyInvocation.Line) { # used "-File" argument - & $NODE_EXE $NPM_CLI_JS $args + & $NODE_EXE $NPM_SHIM_JS npm $args } else { # used "-Command" argument if (($MyInvocation | Get-Member -Name 'Statement') -and $MyInvocation.Statement) { $NPM_ORIGINAL_COMMAND = $MyInvocation.Statement @@ -38,13 +27,13 @@ if ($MyInvocation.ExpectingInput) { # takes pipeline input } $NODE_EXE = $NODE_EXE.Replace("``", "````") - $NPM_CLI_JS = $NPM_CLI_JS.Replace("``", "````") + $NPM_SHIM_JS = $NPM_SHIM_JS.Replace("``", "````") $NPM_COMMAND_ARRAY = [Management.Automation.Language.Parser]::ParseInput($NPM_ORIGINAL_COMMAND, [ref] $null, [ref] $null). EndBlock.Statements.PipelineElements.CommandElements.Extent.Text $NPM_ARGS = ($NPM_COMMAND_ARRAY | Select-Object -Skip 1) -join ' ' - Invoke-Expression "& `"$NODE_EXE`" `"$NPM_CLI_JS`" $NPM_ARGS" + Invoke-Expression "& `"$NODE_EXE`" `"$NPM_SHIM_JS`" npm $NPM_ARGS" } exit $LASTEXITCODE diff --git a/bin/npx b/bin/npx index b8619ee9c5e37..330749471b41c 100755 --- a/bin/npx +++ b/bin/npx @@ -3,6 +3,25 @@ # This is used by the Node.js installer, which expects the cygwin/mingw # shell script to already be present in the npm dependency folder. +# Git Bash (msys) and Cygwin: start node once. npm-shim.js picks this npm or +# the one in the global prefix in the same node process, and msys converts the +# script path for node.exe, so this needs no subshells (Cygwin: one cygpath). +case "$OSTYPE" in + msys*|cygwin*) + set -o igncr 2>/dev/null # cygwin encoding fix + basedir=${0%/*} + [ "$basedir" = "$0" ] && basedir=. + case "$OSTYPE" in cygwin*) basedir=$(cygpath -w "$basedir");; esac + NODE_EXE="$basedir/node.exe" + [ -x "$NODE_EXE" ] || NODE_EXE="$basedir/node" + [ -x "$NODE_EXE" ] || NODE_EXE=node + "$NODE_EXE" "$basedir/node_modules/npm/bin/npm-shim.js" npx "$@" + exit $? + ;; +esac + +# Everything else, e.g. WSL running the Windows install: find the prefix first. + (set -o igncr) 2>/dev/null && set -o igncr; # cygwin encoding fix basedir=`dirname "$0"` diff --git a/bin/npx.cmd b/bin/npx.cmd index ab991abfc2562..1be32b6398cab 100755 --- a/bin/npx.cmd +++ b/bin/npx.cmd @@ -8,13 +8,4 @@ IF NOT EXIST "%NODE_EXE%" ( SET "NODE_EXE=node" ) -SET "NPM_PREFIX_JS=%~dp0\node_modules\npm\bin\npm-prefix.js" -SET "NPX_CLI_JS=%~dp0\node_modules\npm\bin\npx-cli.js" -FOR /F "delims=" %%F IN ('CALL "%NODE_EXE%" "%NPM_PREFIX_JS%"') DO ( - SET "NPM_PREFIX_NPX_CLI_JS=%%F\node_modules\npm\bin\npx-cli.js" -) -IF EXIST "%NPM_PREFIX_NPX_CLI_JS%" ( - SET "NPX_CLI_JS=%NPM_PREFIX_NPX_CLI_JS%" -) - -"%NODE_EXE%" "%NPX_CLI_JS%" %* +"%NODE_EXE%" "%~dp0\node_modules\npm\bin\npm-shim.js" npx %* diff --git a/bin/npx.ps1 b/bin/npx.ps1 index 3fe7b5435763a..6c517f8381a0f 100644 --- a/bin/npx.ps1 +++ b/bin/npx.ps1 @@ -10,24 +10,13 @@ if (-not (Test-Path $NODE_EXE)) { $NODE_EXE="node" } -$NPM_PREFIX_JS="$PSScriptRoot/node_modules/npm/bin/npm-prefix.js" -$NPX_CLI_JS="$PSScriptRoot/node_modules/npm/bin/npx-cli.js" -$NPM_PREFIX=(& $NODE_EXE $NPM_PREFIX_JS) - -if ($LASTEXITCODE -ne 0) { - Write-Host "Could not determine Node.js install directory" - exit 1 -} - -$NPM_PREFIX_NPX_CLI_JS="$NPM_PREFIX/node_modules/npm/bin/npx-cli.js" -if (Test-Path $NPM_PREFIX_NPX_CLI_JS) { - $NPX_CLI_JS=$NPM_PREFIX_NPX_CLI_JS -} +# npm-shim.js picks this npx or the one in the global prefix, in the same node process. +$NPX_SHIM_JS="$PSScriptRoot/node_modules/npm/bin/npm-shim.js" if ($MyInvocation.ExpectingInput) { # takes pipeline input - $input | & $NODE_EXE $NPX_CLI_JS $args + $input | & $NODE_EXE $NPX_SHIM_JS npx $args } elseif (-not $MyInvocation.Line) { # used "-File" argument - & $NODE_EXE $NPX_CLI_JS $args + & $NODE_EXE $NPX_SHIM_JS npx $args } else { # used "-Command" argument if (($MyInvocation | Get-Member -Name 'Statement') -and $MyInvocation.Statement) { $NPX_ORIGINAL_COMMAND = $MyInvocation.Statement @@ -38,13 +27,13 @@ if ($MyInvocation.ExpectingInput) { # takes pipeline input } $NODE_EXE = $NODE_EXE.Replace("``", "````") - $NPX_CLI_JS = $NPX_CLI_JS.Replace("``", "````") + $NPX_SHIM_JS = $NPX_SHIM_JS.Replace("``", "````") $NPX_COMMAND_ARRAY = [Management.Automation.Language.Parser]::ParseInput($NPX_ORIGINAL_COMMAND, [ref] $null, [ref] $null). EndBlock.Statements.PipelineElements.CommandElements.Extent.Text $NPX_ARGS = ($NPX_COMMAND_ARRAY | Select-Object -Skip 1) -join ' ' - Invoke-Expression "& `"$NODE_EXE`" `"$NPX_CLI_JS`" $NPX_ARGS" + Invoke-Expression "& `"$NODE_EXE`" `"$NPX_SHIM_JS`" npx $NPX_ARGS" } exit $LASTEXITCODE diff --git a/lib/cli/global-prefix.js b/lib/cli/global-prefix.js new file mode 100644 index 0000000000000..5c6be0662ccee --- /dev/null +++ b/lib/cli/global-prefix.js @@ -0,0 +1,25 @@ +// Finds the global prefix the way npm does, without loading the rest of npm. +// The Windows shims use it (through bin/npm-shim.js and bin/npm-prefix.js) to +// run the npm installed in the global prefix, if there is one, instead of the +// npm that came with Node.js. + +const { resolve } = require('node:path') +const Config = require('@npmcli/config') +const { definitions, flatten, shorthands } = require('@npmcli/config/lib/definitions') + +module.exports = async () => { + const config = new Config({ + npmPath: resolve(__dirname, '../..'), + // argv is explicitly not looked at since prefix is not something that can be changed via argv + argv: [], + definitions, + flatten, + shorthands, + excludeNpmCwd: false, + // a copy: load() exports npm_config_* variables to env, and npm-shim.js + // then runs npm, maybe another version of it, in this same process + env: { ...process.env }, + }) + await config.load() + return config.globalPrefix +} diff --git a/test/bin/npm-prefix.js b/test/bin/npm-prefix.js new file mode 100644 index 0000000000000..9d8e9c1652409 --- /dev/null +++ b/test/bin/npm-prefix.js @@ -0,0 +1,34 @@ +const t = require('tap') +const mockGlobals = require('@npmcli/mock-globals') +const tmock = require('../fixtures/tmock') + +const mockPrefix = async (t, globalPrefix) => { + const logs = [] + const errors = [] + const exits = [] + mockGlobals(t, { + 'console.log': (...msg) => logs.push(msg), + 'console.error': (...msg) => errors.push(msg), + 'process.exit': (code) => exits.push(code), + }) + tmock(t, '{BIN}/npm-prefix.js', { '{LIB}/cli/global-prefix.js': globalPrefix }) + await new Promise(resolve => setImmediate(resolve)) + return { logs, errors, exits } +} + +t.test('prints the global prefix', async t => { + const { logs, errors, exits } = await mockPrefix(t, async () => '/global/prefix') + t.strictSame(logs, [['/global/prefix']]) + t.strictSame(errors, []) + t.strictSame(exits, []) +}) + +t.test('exits 1 if the global prefix cannot be determined', async t => { + const err = new Error('bad config') + const { logs, errors, exits } = await mockPrefix(t, async () => { + throw err + }) + t.strictSame(logs, []) + t.strictSame(errors, [[err]]) + t.strictSame(exits, [1]) +}) diff --git a/test/bin/npm-shim.js b/test/bin/npm-shim.js new file mode 100644 index 0000000000000..313a778163350 --- /dev/null +++ b/test/bin/npm-shim.js @@ -0,0 +1,106 @@ +const t = require('tap') +const { join } = require('node:path') +const mockGlobals = require('@npmcli/mock-globals') +const tmock = require('../fixtures/tmock') + +const BIN = join(__dirname, '../../bin') + +// the cli files record what they see when they are loaded +const cli = (name) => `global.npmShimSeen.push({ cli: ${JSON.stringify(name)}, argv: process.argv.slice() })` + +const mockShim = async (t, { argv, prefix, prefixError }) => { + const seen = [] + const errors = [] + const exits = [] + mockGlobals(t, { + npmShimSeen: seen, + 'process.argv': argv, + 'console.error': (...msg) => errors.push(msg), + 'process.exit': (code) => exits.push(code), + }) + // the npm that came with node, next to the shim; process.argv[1] tells + // which cli was loaded, since the shim sets it right before the require + tmock(t, '{BIN}/npm-shim.js', { + '{LIB}/cli/global-prefix.js': async () => { + if (prefixError) { + throw prefixError + } + return prefix + }, + '{BIN}/npm-cli.js': {}, + '{BIN}/npx-cli.js': {}, + }) + await new Promise(resolve => setImmediate(resolve)) + return { seen, errors, exits } +} + +t.test('runs the npm in the global prefix if there is one', async t => { + const prefix = t.testdir({ + node_modules: { npm: { bin: { 'npm-cli.js': cli('global npm'), 'npx-cli.js': cli('global npx') } } }, + }) + const npmCli = join(prefix, 'node_modules', 'npm', 'bin', 'npm-cli.js') + const { seen, errors, exits } = await mockShim(t, { + argv: ['node', join(BIN, 'npm-shim.js'), 'npm', 'install', '--save-dev', 'a b', ''], + prefix, + }) + t.strictSame(seen, [{ cli: 'global npm', argv: ['node', npmCli, 'install', '--save-dev', 'a b', ''] }]) + t.strictSame(errors, []) + t.strictSame(exits, []) +}) + +t.test('runs the npx in the global prefix if there is one', async t => { + const prefix = t.testdir({ + node_modules: { npm: { bin: { 'npm-cli.js': cli('global npm'), 'npx-cli.js': cli('global npx') } } }, + }) + const npxCli = join(prefix, 'node_modules', 'npm', 'bin', 'npx-cli.js') + const { seen } = await mockShim(t, { + argv: ['node', join(BIN, 'npm-shim.js'), 'npx', '--no-install', 'tsc', '--version'], + prefix, + }) + t.strictSame(seen, [{ cli: 'global npx', argv: ['node', npxCli, '--no-install', 'tsc', '--version'] }]) +}) + +t.test('runs its own npm if the global prefix has none', async t => { + const prefix = t.testdir({}) + const { seen, errors, exits } = await mockShim(t, { + argv: ['node', join(BIN, 'npm-shim.js'), 'npm', '--version'], + prefix, + }) + t.strictSame(seen, [], 'the global prefix npm is not loaded') + t.strictSame(process.argv, ['node', join(BIN, 'npm-cli.js'), '--version'], 'argv as seen by npm-cli.js') + t.strictSame(errors, []) + t.strictSame(exits, []) +}) + +t.test('runs its own npx if the global prefix has none', async t => { + const prefix = t.testdir({}) + const { seen } = await mockShim(t, { + argv: ['node', join(BIN, 'npm-shim.js'), 'npx', 'cowsay', 'hi'], + prefix, + }) + t.strictSame(seen, [], 'the global prefix npx is not loaded') + t.strictSame(process.argv, ['node', join(BIN, 'npx-cli.js'), 'cowsay', 'hi'], 'argv as seen by npx-cli.js') +}) + +t.test('a global prefix with only npx does not count for npm', async t => { + const prefix = t.testdir({ + node_modules: { npm: { bin: { 'npx-cli.js': cli('global npx') } } }, + }) + const { seen } = await mockShim(t, { + argv: ['node', join(BIN, 'npm-shim.js'), 'npm', 'ls'], + prefix, + }) + t.strictSame(seen, []) + t.strictSame(process.argv, ['node', join(BIN, 'npm-cli.js'), 'ls']) +}) + +t.test('exits 1 if the global prefix cannot be determined', async t => { + const prefixError = new Error('bad config') + const { seen, errors, exits } = await mockShim(t, { + argv: ['node', join(BIN, 'npm-shim.js'), 'npm', '--version'], + prefixError, + }) + t.strictSame(seen, [], 'no cli is loaded') + t.strictSame(errors, [[prefixError], ['Could not determine Node.js install directory']]) + t.strictSame(exits, [1]) +}) diff --git a/test/bin/windows-shims.js b/test/bin/windows-shims.js index d785fbe7b1c55..5b75ca1ce1955 100644 --- a/test/bin/windows-shims.js +++ b/test/bin/windows-shims.js @@ -88,11 +88,13 @@ t.test('run shims', t => { npm: t.fixture('symlink', ROOT), }, }, - // put in a shim that ONLY prints the intended global prefix, - // and should not be used for anything else. + // the real entry point of the shims, and a fake of the helper it uses to + // find the global prefix: it ONLY returns the intended global prefix. + // npm-prefix.js is the same, for the WSL path of the bash shims. node_modules: { npm: { bin: { + 'npm-shim.js': readFileSync(join(BIN, 'npm-shim.js'), 'utf-8'), 'npm-prefix.js': ` const { resolve } = require('path') console.log(resolve(__dirname, '../../../global-prefix')) @@ -100,6 +102,14 @@ t.test('run shims', t => { 'npx-cli.js': `throw new Error('local npx should not be called')`, 'npm-cli.js': `throw new Error('local npm should not be called')`, }, + lib: { + cli: { + 'global-prefix.js': ` + const { resolve } = require('path') + module.exports = async () => resolve(__dirname, '../../../../global-prefix') + `, + }, + }, }, }, // test script returning all command line arguments diff --git a/test/lib/cli/global-prefix.js b/test/lib/cli/global-prefix.js new file mode 100644 index 0000000000000..7f7d91c2347cc --- /dev/null +++ b/test/lib/cli/global-prefix.js @@ -0,0 +1,56 @@ +const t = require('tap') +const { resolve } = require('node:path') +const mockGlobals = require('@npmcli/mock-globals') +const tmock = require('../../fixtures/tmock') + +const ROOT = resolve(__dirname, '../../..') + +t.test('loads config like npm, without argv, and returns the global prefix', async t => { + const created = [] + class Config { + constructor (opts) { + created.push(opts) + this.globalPrefix = '/global/prefix' + } + + async load () {} + } + const globalPrefix = tmock(t, '{LIB}/cli/global-prefix.js', { '@npmcli/config': Config }) + t.equal(await globalPrefix(), '/global/prefix') + t.equal(created.length, 1) + t.match(created[0], { + npmPath: ROOT, + argv: [], + definitions: Object, + flatten: Function, + shorthands: Object, + excludeNpmCwd: false, + env: { ...process.env }, + }) + t.not(created[0].env, process.env, 'config gets a copy of the environment') +}) + +t.test('rejects if config cannot be loaded', async t => { + class Config { + async load () { + throw new Error('bad config') + } + } + const globalPrefix = tmock(t, '{LIB}/cli/global-prefix.js', { '@npmcli/config': Config }) + await t.rejects(globalPrefix(), /bad config/) +}) + +t.test('honors the prefix config, as npm does', async t => { + const prefix = t.testdir({}) + mockGlobals(t, { 'process.env.npm_config_prefix': prefix }) + const globalPrefix = require('../../../lib/cli/global-prefix.js') + t.equal(await globalPrefix(), prefix) +}) + +t.test('leaves process.env alone for the npm that runs next', async t => { + mockGlobals(t, { 'process.env': { ...process.env } }) + const before = { ...process.env } + const globalPrefix = require('../../../lib/cli/global-prefix.js') + await globalPrefix() + t.strictSame({ ...process.env }, before) +})