Skip to content

fix(exec): prevent shared binPaths pollution across workspace runs#9650

Open
arjun-vegeta wants to merge 5 commits into
npm:latestfrom
arjun-vegeta:fix-exec-binpaths
Open

fix(exec): prevent shared binPaths pollution across workspace runs#9650
arjun-vegeta wants to merge 5 commits into
npm:latestfrom
arjun-vegeta:fix-exec-binpaths

Conversation

@arjun-vegeta

@arjun-vegeta arjun-vegeta commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description:
Fixes #9640

Bug:
When running npm exec across multiple workspaces, the binary from the first workspace would accidentally bleed into the second one if they shared the same name.

Root Cause:
In libnpmexec, the binPaths array was scoped at the module level instead of inside the exec() function. Since execWorkspaces runs everything in a single process, binPaths just kept accumulating paths. When the second workspace ran, the first workspace's .bin path was still sitting at the front of PATH, so it hijacked the execution.

Fix:
Moved binPaths inside exec() so it gets a fresh array on every call. I left the manifests map at the module level (keyed by spec.raw) because we still want to cache registry specs across calls, and absolute paths won't collide anyway.

Testing:

  • Added a regression test in test/local.js with two workspaces using the exact same local bin name (shared-bin).
  • Gave each test call its own args array to stop @npmcli/run-script from mutating it and hanging the tests.
  • Fails correctly without the fix. With the fix, all tests pass 100%.

@arjun-vegeta arjun-vegeta requested review from a team as code owners June 24, 2026 20:07

@owlstronaut owlstronaut left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for digging into this, but I ran your exact repro on the branch and it still prints  A bin / A bin  under workspace hoisting only one  shared-bin  link gets created (→tool-a), so the  binPaths  change doesn't actually fix #9640

@arjun-vegeta

Copy link
Copy Markdown
Contributor Author

Thanks for digging into this, but I ran your exact repro on the branch and it still prints  A bin / A bin  under workspace hoisting only one  shared-bin  link gets created (→tool-a), so the  binPaths  change doesn't actually fix #9640

Thanks for testing it out. The old binPaths approach was a dead end because of how the hoisted .bin symlink gets overwritten at the root

I just pushed a complete rewrite. I dropped the binPaths idea entirely and instead hooked into the Arborist tree. Now, if it detects it's running inside a workspace, it walks the actual dependency graph (edgesOut) for that specific workspace node to find the correct binary path directly (e.g., node_modules/tool-b/cli.js), completely bypassing the hoisted .bin folder

The repro now outputs A bin / B bin correctly, and I've added full integration tests for the hoisted edge-case. Can you test it out once more and let me know how it goes

@arjun-vegeta arjun-vegeta changed the title fix(exec): prevent shared binPaths pollution across workspace runs fix(libnpmexec): correctly resolve workspace dependency binaries from graph Jun 25, 2026
@arjun-vegeta arjun-vegeta requested a review from owlstronaut June 25, 2026 10:53
@manzoorwanijk

Copy link
Copy Markdown
Contributor

@owlstronaut I think the issue here is the bin-hoisting collision in hoisted mode. For that, we need only the changes from the earlier version of the PR, a6f9044.

Then we can create a separate issue for bin collision.

@manzoorwanijk

Copy link
Copy Markdown
Contributor

Thanks for iterating on this. A few concerns with the graph + node <binFile> approach:

1. It assumes every bin is a Node script. Running the resolved bin as node <binFile> breaks non-node bins — shell scripts, native binaries, or a non-node shebang. The .bin shim path (symlink / cmd-shim) honors the shebang; node <binFile> does not.

2. It bypasses the hoisted .bin for all workspace execs, not just collisions. Any npm exec -w <ws> -- <dep-bin> now runs node <binFile> directly even when the root shim is already correct, a broad behavior change that drops shim/shebang handling for cases that were fine.

3. Same-named bins across multiple direct deps resolve non-deterministically. The first matching edgesOut entry wins, i.e. by package.json order, not by the package that owns the installed shim.

4. The tests mock loadActual() by hand, so they don't verify a real installed tree resolves as assumed.

On scope: the binPaths reset is a valid, separate fix (it makes the linked strategy correct across sequential runs) but doesn't address the hoisted collision on its own. The collision is that npm links only one shared-bin into the shared root .bin (first-wins in bin-links), so no correct per-workspace shim exists. Fixing that cleanly - cross-platform, all bin types - wants a fix at the install layer (arborist creating a per-workspace .bin link on a hoisted name collision), not a special case in libnpmexec.

@arjun-vegeta

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I'll revert the graph walk and scope this PR back to just the binPaths reset for #9640
I'll dig more into the hoisted bin collision on the arborist/bin-links side and open a separate PR for that

@arjun-vegeta arjun-vegeta changed the title fix(libnpmexec): correctly resolve workspace dependency binaries from graph fix(exec): prevent shared binPaths pollution across workspace runs Jun 27, 2026
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.

[BUG] npm exec -w a -w b -- <bin> runs the first workspace's bin for every workspace

3 participants