Skip to content

fix(arborist): preserve bin links when allowScripts denies a node (backport release/v11) (#9681)#9682

Open
Sanjays2402 wants to merge 1 commit into
npm:release/v11from
Sanjays2402:fix/issue-9681
Open

fix(arborist): preserve bin links when allowScripts denies a node (backport release/v11) (#9681)#9682
Sanjays2402 wants to merge 1 commit into
npm:release/v11from
Sanjays2402:fix/issue-9681

Conversation

@Sanjays2402

Copy link
Copy Markdown

Description

Backport of the bin-link half of the v12 allowScripts gate change to release/v11.

On npm 11 (released and release/v11 HEAD), a package denied via allowScripts: { "<pkg>": false } is installed into node_modules/<pkg> but its node_modules/.bin/<bin> symlinks are never created. The package directory exists and the bin file is on disk and executable, but nothing puts it on PATH, so downstream build steps that shell out to the bin fail with command not found. Only denied packages that declare a bin are affected (esbuild, nx, etc.).

The cause is in workspaces/arborist/lib/arborist/rebuild.js. The deny gate was an early return at the top of #addToBuildSet, which dropped the node out of the build set entirely. The build set is also what build:queue reads to populate the bin queue used by binLinks, so denied nodes lose their bin links along with their scripts. That mismatches --ignore-scripts, which skips lifecycle scripts but still links bins.

The fix moves the deny check out of the early return and into the per-key dispatch loop, gating only preinstall/install/postinstall/prepare while leaving bin to be queued normally. Same shape as the v12 fix in #9480 / #9424, scoped down to the bin issue so v11 keeps its current opt-in semantics (only false denies, null/unreviewed still falls through).

--ignore-scripts and --dangerously-allow-all-scripts precedence is unchanged.

Existing Issue

Fixes #9681

Screenshots / repro

Repro from the issue (npm 11.17.0, before this PR):

mkdir esb && cd esb
cat > package.json <<'JSON'
{
  "name": "esb",
  "version": "1.0.0",
  "dependencies": { "esbuild": "0.25.0" },
  "allowScripts": { "esbuild": false }
}
JSON
npm install --no-audit --no-fund
ls node_modules/.bin/esbuild        # ENOENT
node_modules/.bin/esbuild --version # sh: esbuild: command not found

After this PR the bin link is created and esbuild --version runs; the install script is still skipped.

AI disclosure

This change was written with the assistance of Claude. I have reviewed the diff and the test and take responsibility for the code.

Test Coverage

New regression test in workspaces/arborist/test/arborist/rebuild.js:

allowScripts deny still links bins for that node

with a dedicated fixture at workspaces/arborist/test/fixtures/reify-cases/testing-allow-scripts-deny-bin.js (one dep denied-with-bin that has both a postinstall and a bin). The test asserts:

  1. The denied package's postinstall did NOT run (sentinel file absent).
  2. node_modules/.bin/denied-with-bin IS a symlink (bin linking happened).

Verified both directions against release/v11:

  • Without the patch: assertion 1 passes, assertion 2 fails with ENOENT on .bin/denied-with-bin (bug reproduced).
  • With the patch: both assertions pass.

Full workspaces/arborist rebuild suite is green (30/30), including the existing allowScripts deny entry skips the build set entry for that node and dangerouslyAllowAllScripts bypasses the deny gate cases. Lint clean on changed files. Diff is contained to:

workspaces/arborist/lib/arborist/rebuild.js        | 34 ++++++++-------
workspaces/arborist/test/arborist/rebuild.js       | 23 +++++++++++
.../reify-cases/testing-allow-scripts-deny-bin.js  | 48 ++++++++++++++++++++++
3 files changed, 91 insertions(+), 14 deletions(-)

No .github/, tooling, or dependency changes.

…m#9681)

A node denied via `allowScripts: { "<pkg>": false }` was dropped from
the build set entirely, so its `node_modules/.bin/<bin>` symlinks were
never created. This stranded any package that declared both an install
script and a bin (e.g. esbuild, nx): the binary lived under
`node_modules/<pkg>` but nothing put it on PATH, breaking downstream
build steps with `command not found`.

Move the deny check out of the early-return in `#addToBuildSet` and
into the per-queue dispatch, mirroring the v12 fix. Lifecycle scripts
(preinstall/install/postinstall/prepare) for denied nodes are still
skipped; only bin linking continues, matching the semantics of
`--ignore-scripts`.

Regression test added in test/arborist/rebuild.js: with
`allowScripts: { 'denied-with-bin': false }` the postinstall does not
run but `.bin/denied-with-bin` is still created.
@Sanjays2402 Sanjays2402 requested review from a team as code owners June 27, 2026 21:17
@vbjay

vbjay commented Jun 28, 2026

Copy link
Copy Markdown

Not sure if this was intentional. I could see it potentially as a defense in depth action. I could be wrong but if bin\blaa doesn't exist, then it adds extra protection. There is the case of other approved script providing the bin. Thinking of esbuild for example.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants