Visitar URL original
Hoisted `.bin` shim runs `node` on esbuild's native binary after install · Issue #17402 · pnpm/pnpm · GitHub
Skip to content

Hoisted .bin shim runs node on esbuild's native binary after install #17402

Closed

Description

@zhuan-chen

Verify latest release

  • I verified that the issue exists in the latest pnpm release

pnpm version

No response

Which area(s) of pnpm are affected? (leave empty if unsure)

No response

Link to the code that reproduces this issue or a replay of the bug

No response

Reproduction steps

mkdir esbuild-shim-repro && cd esbuild-shim-repro
echo '{ "private": true, "dependencies": { "tsx": "4.23.15" } }' > package.json
printf 'allowBuilds:\n  esbuild: true\n' > pnpm-workspace.yaml
pnpm install

# The shim in the hidden hoisted directory fails:
node_modules/.pnpm/node_modules/.bin/esbuild --version
# .../node_modules/.pnpm/esbuild@0.28.2/node_modules/esbuild/bin/esbuild:1
# <ELF header bytes>
# SyntaxError: Invalid or unexpected token

# tsx's own shim for the same bin works:
node_modules/.pnpm/tsx@4.23.15/node_modules/tsx/node_modules/.bin/esbuild --version
# 0.28.2

Describe the Bug

esbuild publishes bin/esbuild as a Node.js script, and its postinstall replaces that file with the native executable, so the command doesn't start Node.js on every run (esbuild docs).

After pnpm install, the shims in tsx's and esbuild's own node_modules/.bin exec the binary directly, but the one in node_modules/.pnpm/node_modules/.bin still runs it with node (POSIX lines shown):

$ grep exec node_modules/.pnpm/tsx@4.23.15/node_modules/tsx/node_modules/.bin/esbuild
exec "$basedir_abs/../../../../../esbuild@0.28.2/node_modules/esbuild/bin/esbuild"  "$@"

$ grep exec node_modules/.pnpm/node_modules/.bin/esbuild
  exec "$basedir/node"  "$basedir_abs/../../esbuild@0.28.2/node_modules/esbuild/bin/esbuild" "$@"
  exec node  "$basedir_abs/../../esbuild@0.28.2/node_modules/esbuild/bin/esbuild" "$@"

Running pnpm rebuild esbuild afterwards fixes it: the hoisted shim then execs the binary directly and prints 0.28.2. So install seems to skip relinking hoisted bins after a dependency's build changes a bin target, while rebuild relinks them.

This breaks dependency build scripts, because pnpm puts node_modules/.pnpm/node_modules/.bin on their PATH. When a dependency's build script runs esbuild and esbuild is only a transitive dependency of it, the script gets the broken shim and the whole install fails. Here, dep depends on tsx, which depends on esbuild:

mkdir -p dep-repro/dep && cd dep-repro
echo '{ "private": true, "dependencies": { "dep": "file:./dep" } }' > package.json
echo '{ "name": "dep", "version": "1.0.0", "dependencies": { "tsx": "4.23.15" }, "scripts": { "postinstall": "esbuild --version" } }' > dep/package.json
printf 'allowBuilds:\n  esbuild: true\n  "dep@file:dep": true\n' > pnpm-workspace.yaml
pnpm install
# dep@file:dep postinstall: SyntaxError: Invalid or unexpected token
# ERR_PNPM_EXECUTOR_LIFECYCLE_SCRIPT_FAILED

Tools that search .bin directories hit it too; for example, johnnyelwailer/t3code#425 worked around it on macOS.

What I checked, each with a fresh store:

  • It reproduces on pnpm 12.15.0, 11.28.8, and 10.34.9, so it isn't a recent regression.
  • It also reproduces on a second install from a warm store, where pnpm restores esbuild's build from the side-effects cache instead of rerunning its postinstall.
  • With publicHoistPattern: [esbuild], shamefullyHoist: true, or nodeLinker: hoisted, the root node_modules/.bin/esbuild works. Only the hidden hoisted .bin is affected.
  • preferSymlinkedExecutables: true avoids it, as does denying esbuild's build (bin/esbuild then stays a Node.js script).

#14502 and #16594 hit the same failure with pnpm's own npm package, which also swaps a placeholder bin for a native binary during install: the shim recorded node from the placeholder's #!/usr/bin/env node line. Their fixes (#14506, #16597) removed that line from the placeholder. That changed pnpm's package, not how pnpm writes shims, so it doesn't help other packages whose install script replaces a bin, such as esbuild.

Expected Behavior

After pnpm install, node_modules/.pnpm/node_modules/.bin/esbuild should exec the native binary directly, as the other esbuild shims do and as it does after pnpm rebuild esbuild. More generally, the hoisted .bin shims of a package whose build scripts ran should be written from the bin targets as they are after the build.

Which Node.js version are you using?

24.22.1

Which operating systems have you used?

  • macOS
  • Windows
  • Linux

If your OS is a Linux based, which one it is? (Include the version if relevant)

Ubuntu 26.04 LTS (x86_64)

Contributing a fix

  • I am working on a fix

Written by an agent (Claude Code, claude-opus-5-5).

Activity

  1. added theissue type on Oct 26, 2026
  2. self-assigned this
    on Oct 27, 2026
  3. zkochan commented on Oct 27, 2026

    @zkochan
    Member

    Opened a pull request to fix this in both pnpm v11 and pnpm v12: #17420


    🤖 Generated with Antigravity

  4. added a commit that references this issue on Oct 27, 2026
    65d6a18
  5. added a commit that references this issue on Oct 27, 2026
    1ff9d3f
  6. added a commit that references this issue on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions