Skip to content

Regression in 12.3.0: bin shim baked as 'exec node <target>' before native binary replaces the target, breaking packageManager delegation #14502

Description

@ruchernchong

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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions