Repository navigation
Conversation
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.
This was referenced Oct 8, 2026
Author
|
Friendly ping on this one and the two smaller follow-ups, #10096 and #10097. None of them has had a review yet, and since these are my first PRs here the CI workflows are still waiting for a maintainer to approve them. They're independent of each other, so they can be looked at in any order. Happy to rebase, split anything up or answer questions if that makes review easier. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follows up #3088 / #7271.
Summary
On Windows, every
npm/npxcall starts node.exe more than once:bin/npm,bin/npx(Git Bash, MSYS2, Cygwin)npm.cmd,npx.cmdnpm.ps1,npx.ps1(-File,-Command, pipeline)The extra starts:
bin/npm-prefix.js, started only to print the global prefix;node -p 'require("path").dirname(process.execPath)'.On Linux and macOS,
npmis a symlink tonpm-cli.js, which is one start.This PR makes the same decision in the node process that then runs npm:
bin/npm-shim.js(new):node npm-shim.js <npm|npx> [args...].npm-prefix.jsdid.<globalPrefix>/node_modules/npm/bin/{npm,npx}-cli.jsif that exists, else its own.process.argvto what that file sees when run directly ([node, cli, ...args]), thenrequires it.lib/cli/global-prefix.js(new): the@npmcli/configloading that was innpm-prefix.js.npm-prefix.jsandnpm-shim.jsboth use it.process.env.load()exportsnpm_config_*variables, and the npm that runs next in the same process, possibly another version from the global prefix, must see the environment it would see on its own.npm warn Unknown env config "global-ignore-file". A test covers this.bin/npm-prefix.js: a thin wrapper over the helper, with unchanged output. The bash shims still use it outside Git Bash and Cygwin (WSL), and anything else that runs it keeps working.npm.cmd,npx.cmd: one line,"%NODE_EXE%" "%~dp0\node_modules\npm\bin\npm-shim.js" npm %*.npm.ps1,npx.ps1: callnpm-shim.jsin the pipeline,-Fileand-Commandbranches. The branches themselves are unchanged.bin/npm,bin/npx: when$OSTYPEismsys*orcygwin*, a fast path that starts node once:${0%/*}instead of`dirname`;uname;-p execPathround trip, since msys converts the script path for native programs;cygpath -w.Everything else, including WSL running the Windows install, runs the existing script unchanged.
Numbers
npm packoflatest(b317f16) againstnpm packof this branch, so the "after" column is exactly this PR.node npm-cli.jsdirectlynpm --versionnpx --no-install tsc --versionnpm.cmd --versionnpx.cmd --no-install tsc --versionnpm.ps1 --versionnpx.ps1 --no-install tsc --versionnode npm-cli.jsis the shell itself: bash and cmd start-up, and about 300 ms forpwsh.Behaviour
The aim is identical behaviour. Checked by running shipped and patched side by side and comparing exit code and normalised output:
30 edge cases:
pwsh -File,pwsh -Command, a pwsh pipeline, and bash in WSL running the Windows install;$and empty strings;npm_execpath,npm_commandandnpm_config_user_agentin scripts;npm/npxfrom scripts,npm exec,npm ls, an unknown command, a cwd with spaces, invocation by relative path;npm_config_prefixpointing at a different npm version).All 30 identical.
32 real-world command lines (
npm run build/lint/test,npx tsc,npx vitest run,npx next build,npx astro build,npx eslint,npx oxlint), run in order on fresh projects through Git Bash: all identical.Node.js starts per call, counted with a
NODE_OPTIONS=--requirelogger: as in the table at the top.Two intentional differences:
npm.cmd/npx.cmdwhen the prefix cannot be determined (config fails to load) now exit 1 with "Could not determine Node.js install directory", asnpm.ps1andbin/npmalready did. Before, the cmd shims silently ran the bundled npm.Write-Host.Tests
test/bin/npm-shim.js(new), withnpm-cli.js/npx-cli.jsin a temporary global prefix that records what it was run with. Covers:npx-cli.js(no npm);process.argvthe CLI sees, including arguments with spaces and empty strings.test/bin/npm-prefix.js(new): prints the prefix; exits 1 on failure.test/lib/cli/global-prefix.js(new):npm_config_prefix;process.envuntouched (this test fails without the env copy).test/bin/windows-shims.js: the fixture now ships the realnpm-shim.jsand fakes onlylib/cli/global-prefix.js, which returns the fixture'sglobal-prefix. The localnpm-cli.js/npx-cli.jsstill throw if called, so the redirect is still asserted, now through the real shim code. The fakenpm-prefix.jsstays for the WSL path. The "shim contents" npm/npx diff check passes unchanged (only m→x).On Windows 11 (Node 26.7.0, Git for Windows 2.55, PowerShell 7 and 5.1, WSL2 Ubuntu):
node . run test -- test/bin/windows-shims.js test/bin/npm-shim.js test/bin/npm-prefix.js test/lib/cli/global-prefix.js:usr/binbash and WSL bash all pass (Cygwin not installed);node . run test(full suite): 124 of 127 files pass, coverage 100%.lateston this machine, for environmental reasons:windows-shims.js› pwsh ›npm test: the local PowerShell profile prints adotnet-suggestnotice on stdout. It passes with-NoProfile, before and after this change.publish.js(3 subtests) andstage/index.js(1): their lifecycle scripts calltouch, which cmd.exe can't find here.node . run lintpasses.Relation to #10058 (open, draft)
#10058 changes the same
-Commandbranch ofnpm.ps1/npx.ps1:ProcessStartInfoinstead ofInvoke-Expression. It also changes the same test fixture, nesting it under a$(throw ...)directory. The two compose:.ps1. Whichever lands second takes fix(powershell): avoid evaluating CLI paths as source #10058's process launch with this PR's arguments:$NPM_PROCESS.StartInfo.Arguments = ""$NPM_SHIM_JS" npm $NPM_ARGS"(andnpx), and drops the two.Replace("``", "````")lines as fix(powershell): avoid evaluating CLI paths as source #10058 does.npm-shim.js,lib/cli/global-prefix.js) inside it.windows-shims.jspasses in every shell, including fix(powershell): avoid evaluating CLI paths as source #10058's$(throw 'shim path evaluated')install path and its no-Invoke-Expressionassertions. The one exception is the same profile-caused pwsh case as above.FOR /F) is always the install's ownnpm-shim.js, never a path read from config.required inside node.@npmcli/configchange (ignoring a project-levelprefix) applies to this PR unchanged, becauselib/cli/global-prefix.jsuses the same loader.References
npmon Windows runsnodeexecutable twice (which is slow), to find the prefix #3088, fix: more lightweight npm bin discovery in windows #7271 (the earlier fix:npm prefix -g→npm-prefix.js)