pnpm version
12.3.0 and 12.3.1 (regression from 12.2.1)
Link to the code that reproduces this issue
https://github.com/ruchernchong/pnpm-12-vercel
It is currently pinned to pnpm@12.2.1 — the last version that works. Change packageManager to pnpm@12.3.0 or pnpm@12.3.1 and run any pnpm command with an older pnpm on PATH to reproduce. The self-contained shell repro below needs no clone.
Reproduction steps
With any pnpm 11.x on PATH, pinning packageManager to pnpm 12.3.0 or later makes every pnpm command in that project crash. 12.2.1 is fine.
mkdir -p /tmp/repro && cd /tmp/repro
curl -sL https://registry.npmjs.org/pnpm/-/pnpm-11.25.0.tgz | tar xz
for v in 12.2.1 12.3.0 12.3.1; do
rm -rf p && mkdir p && cd p
echo "{\"name\":\"s\",\"private\":true,\"packageManager\":\"pnpm@$v\"}" > package.json
echo "== $v"; node /tmp/repro/package/bin/pnpm.mjs --version 2>&1 | tail -2
cd ..
done
| pinned |
result |
pnpm@12.2.1 |
exit 0 — prints 12.2.1 |
pnpm@12.3.0 |
exit 1 — SyntaxError: Invalid or unexpected token |
pnpm@12.3.1 |
exit 1 — SyntaxError: Invalid or unexpected token |
The failure:
file:///Users/me/Library/pnpm/store/v11/links/@/pnpm/12.3.1/<hash>/node_modules/pnpm/pnpm:1
<Mach-O header bytes> __PAGEZERO __TEXT __text __TEXT
SyntaxError: Invalid or unexpected token
at compileSourceTextModule (node:internal/modules/esm/utils:354:16)
at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:91:18)
at #translate (node:internal/modules/esm/loader:438:20)
at afterLoad (node:internal/modules/esm/loader:494:29)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:499:12)
at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:552:36)
at afterResolve (node:internal/modules/esm/loader:600:52)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:606:12)
at node:internal/modules/esm/loader:625:32
at TracingChannel.tracePromise (node:diagnostics_channel:539:22)
Not Corepack — it is not installed on this machine:
| check |
result |
command -v corepack |
not found |
~/Library/Caches/node/corepack |
does not exist |
corepack shim next to node |
none |
COREPACK_* env |
COREPACK_ENABLE_AUTO_PIN=0, FNM_COREPACK_ENABLED=false |
The tarball is fetched into pnpm's own store ($PNPM_HOME/store/v11/links/@/pnpm/<version>/...), so this is pnpm's built-in packageManager version manager doing the install and the launch.
Describe the Bug
The generated bin shim is baked with exec node <target> while the target is still the JS placeholder, and the native binary replaces the target afterwards.
switchCliVersion spawns rather than imports, so the earlier "import vs spawn" framing was wrong — it does:
const pnpmBinPath = path.join(wantedPnpmBinDir, "pnpm")
crossSpawn.sync(pnpmBinPath, process.argv.slice(2), { stdio: "inherit" })
pnpmBinPath is the generated shim, not the package file. Comparing the two slots on the same machine — both targets are byte-identical in kind (Mach-O 64-bit executable arm64):
# $PNPM_HOME/store/v11/links/@/pnpm/12.2.1/<hash>/bin/pnpm
exec "$basedir/../node_modules/pnpm/pnpm" "$@" # direct exec — works
# $PNPM_HOME/store/v11/links/@/pnpm/12.3.1/<hash>/bin/pnpm
exec node "$basedir/../node_modules/pnpm/pnpm" "$@" # via node — SyntaxError
@zkochan/cmd-shim decides exec node <target> vs. a direct exec by reading the target's shebang when the shim is generated. The published tarball's bin.pnpm is a JS placeholder carrying a real shebang:
$ curl -sL https://registry.npmjs.org/pnpm/-/pnpm-12.3.0.tgz | tar xz
$ file -b package/pnpm
a /usr/bin/env node script text executable, Unicode text, UTF-8 text
$ head -3 package/pnpm
#!/usr/bin/env node
// pnpm's native binary replaces this file during installation (see
// ./install.js). If this is running, that install script did not — build
install.js replaces that placeholder with the native executable. The lifecycle scripts changed in 12.3.0:
|
12.2.1 |
12.3.0+ |
preinstall |
node install.js |
node install.js |
postinstall |
— |
node install.js |
On 12.2.1 the swap lands before linkBins, so cmd-shim sees a native target with no shebang and emits a direct exec. From 12.3.0 the shim is generated while the target is still the shebang-bearing placeholder, so cmd-shim emits exec node <target> — and the subsequent swap replaces that target with a Mach-O executable, leaving a shim that permanently runs node <native binary>.
(The exact ordering claim is inferred from the postinstall addition being the only relevant diff between the two releases; the shim contents and target file types above are directly observed.)
Not a stale-cache effect. I moved the whole 12.3.1 store slot aside and let it re-download from scratch — the freshly generated shim is again exec node <target> and fails identically. It reproduces on a cold slot.
The artifact itself is fine; only the shim is wrong:
$ <slot>/node_modules/pnpm/pnpm --version # executed directly
12.3.1
$ node <slot>/node_modules/pnpm/pnpm # what the shim does
SyntaxError: Invalid or unexpected token
$ node <slot>/node_modules/pnpm/bin/pnpm.mjs # loadable wrapper, also present
12.3.1
Worth guarding separately: pnpm self-update 12.3.1 reported success and rewrote packageManager, but the next command in that project was broken:
Checking for updates...
The current project has been updated to use pnpm v12.3.1
Expected Behavior
packageManager: "pnpm@12.3.x" should keep working when the pnpm on PATH is an older release, as it did on 12.2.1.
The shim must not be generated from the placeholder's shebang and then have its target swapped underneath it. Options: run the placeholder→native swap before linkBins (restoring 12.2.1 ordering), regenerate/re-verify the shim after the swap, or point bin.pnpm at the already-shipped bin/pnpm.mjs wrapper so the target's shebang stays truthful.
Which Node.js version are you using?
v26.1.0
Which operating systems have you used?
macOS (Darwin 27.0.0, arm64)
pnpm version
12.3.0 and 12.3.1 (regression from 12.2.1)
Link to the code that reproduces this issue
https://github.com/ruchernchong/pnpm-12-vercel
It is currently pinned to
pnpm@12.2.1— the last version that works. ChangepackageManagertopnpm@12.3.0orpnpm@12.3.1and run any pnpm command with an older pnpm onPATHto reproduce. The self-contained shell repro below needs no clone.Reproduction steps
With any pnpm 11.x on
PATH, pinningpackageManagerto pnpm 12.3.0 or later makes every pnpm command in that project crash. 12.2.1 is fine.pnpm@12.2.112.2.1pnpm@12.3.0SyntaxError: Invalid or unexpected tokenpnpm@12.3.1SyntaxError: Invalid or unexpected tokenThe failure:
Not Corepack — it is not installed on this machine:
command -v corepack~/Library/Caches/node/corepacknodeCOREPACK_*envCOREPACK_ENABLE_AUTO_PIN=0,FNM_COREPACK_ENABLED=falseThe tarball is fetched into pnpm's own store (
$PNPM_HOME/store/v11/links/@/pnpm/<version>/...), so this is pnpm's built-inpackageManagerversion manager doing the install and the launch.Describe the Bug
The generated bin shim is baked with
exec node <target>while the target is still the JS placeholder, and the native binary replaces the target afterwards.switchCliVersionspawns rather than imports, so the earlier "import vs spawn" framing was wrong — it does:pnpmBinPathis the generated shim, not the package file. Comparing the two slots on the same machine — both targets are byte-identical in kind (Mach-O 64-bit executable arm64):@zkochan/cmd-shimdecidesexec node <target>vs. a direct exec by reading the target's shebang when the shim is generated. The published tarball'sbin.pnpmis a JS placeholder carrying a real shebang:install.jsreplaces that placeholder with the native executable. The lifecycle scripts changed in 12.3.0:preinstallnode install.jsnode install.jspostinstallnode install.jsOn 12.2.1 the swap lands before
linkBins, so cmd-shim sees a native target with no shebang and emits a direct exec. From 12.3.0 the shim is generated while the target is still the shebang-bearing placeholder, so cmd-shim emitsexec node <target>— and the subsequent swap replaces that target with a Mach-O executable, leaving a shim that permanently runsnode <native binary>.(The exact ordering claim is inferred from the
postinstalladdition being the only relevant diff between the two releases; the shim contents and target file types above are directly observed.)Not a stale-cache effect. I moved the whole
12.3.1store slot aside and let it re-download from scratch — the freshly generated shim is againexec node <target>and fails identically. It reproduces on a cold slot.The artifact itself is fine; only the shim is wrong:
Worth guarding separately:
pnpm self-update 12.3.1reported success and rewrotepackageManager, but the next command in that project was broken:Expected Behavior
packageManager: "pnpm@12.3.x"should keep working when the pnpm onPATHis an older release, as it did on 12.2.1.The shim must not be generated from the placeholder's shebang and then have its target swapped underneath it. Options: run the placeholder→native swap before
linkBins(restoring 12.2.1 ordering), regenerate/re-verify the shim after the swap, or pointbin.pnpmat the already-shippedbin/pnpm.mjswrapper so the target's shebang stays truthful.Which Node.js version are you using?
v26.1.0
Which operating systems have you used?
macOS (Darwin 27.0.0, arm64)