Files
pnpm/.github/workflows
Zoltan Kochan dbe2dad610 feat(pacquet): ship node-gyp beside the native binary (#13586)
pnpm v12 shipped no node-gyp, so any package that shells out to it during
an install script died with `spawn node-gyp ENOENT`: `node-gyp-build` with
no matching prebuild, `node-pre-gyp`, a plain `"install": "node-gyp
rebuild"`, and the `node-gyp rebuild` script pnpm synthesizes for a package
that ships a `binding.gyp` without one. Embedders worked around it by
putting their own node-gyp on the lifecycle PATH.

pnpm 11 solves this without a JS bundle: node-gyp is declared `external`,
and `pnpm deploy` resolves its whole tree from this repo's lockfile at
release time into a `dist/node_modules/node-gyp` directory that sits beside
the entry point, with `dist/node-gyp-bin` holding the wrappers that go on a
lifecycle script's PATH. Those are ordinary files next to the executable,
so the native binary can ship them the same way — nothing about the
mechanism depended on there being a bundle to hide node-gyp inside.

Resolving the tree at release time rather than at install time is the point,
not an implementation detail: the dependency tree is frozen and reviewed per
pnpm release instead of being fetched from the registry at the moment an
install script is about to execute arbitrary code.

scripts/bundle-node-gyp.mjs builds the payload, reusing the TypeScript CLI's
wrapper scripts so the two stacks cannot drift on how a script's node-gyp is
dispatched, and refusing to produce a partial dist/ — a PATH entry that
resolves nothing is worse than shipping none at all. Both wrappers ship it,
since each places the binary next to itself.

node-gyp moves to a catalog entry so both stacks pin one version; the
Node.js-floor hold-back note moves with it.

At runtime bundled_node_gyp_bin finds the wrapper dir relative to the
executable and returns None when nothing was shipped, which is what a
checkout build sees — lifecycle scripts then fall back to whatever node-gyp
the environment provides, exactly as before. The wrapper itself honors
npm_config_node_gyp and only falls back to the shipped copy when it is
unset, so a user-supplied node-gyp, a workspace one, and a package's own
dependency all keep winning.

Refs https://github.com/pnpm/pnpm/issues/13580
2026-08-02 15:37:41 +02:00
..