Skip to content

[BUG] Direct dependency's bin can be overwritten by a transitive dependency based on lexical package name #9868

Description

@RyanCavanaugh

Is there an existing issue for this?

  • I have searched the existing issues

This issue exists in the latest npm version

  • I am using the latest npm

This is not just a request to bump a dependency for a CVE

  • This is not solely a request to bump a dependency for a CVE

Current Behavior

When a direct dependency and one of its transitive dependencies expose the same
bin name, npm can link the transitive dependency's executable into
node_modules/.bin. The winner appears to be determined by the lexical order
of the installed package paths rather than dependency depth.

This is the npm equivalent of yarnpkg/berry#7215, but Yarn's winner depends on
dependency-tree order while npm's depends on lexical package name:
yarnpkg/berry#7215

This affects the documented setup for running TypeScript 6 and TypeScript 7
side by side. In the reproduction below:

  • @typescript/z-native is a direct alias for TypeScript 7 and exposes tsc.
  • typescript is a direct alias for @typescript/typescript6.
  • @typescript/typescript6 transitively installs @typescript/old, which also
    exposes tsc.

npm links node_modules/.bin/tsc to the transitive @typescript/old package,
so invoking tsc unexpectedly runs TypeScript 6.

Actual Behavior

node_modules/.bin/tsc points to node_modules/@typescript/old/bin/tsc, and
invoking it reports TypeScript 6.0.3.

Renaming only the direct alias from @typescript/z-native to
@typescript/a-native, then reinstalling, makes the same bin point to the
direct TypeScript 7 package. This suggests that lexical package-name ordering,
rather than directness, decides the collision.

Expected Behavior

For a bin-name collision, an executable from a direct dependency should take
precedence over one from a transitive dependency. npm exec tsc -- --version
should therefore run TypeScript 7.0.2.

Package alias names should not change which dependency supplies the executable.

Steps To Reproduce

Create this package.json in an empty directory:

{
  "name": "npm-alias-bin-repro",
  "version": "1.0.0",
  "private": true,
  "devDependencies": {
    "@typescript/z-native": "npm:typescript@~7.0.2",
    "typescript": "npm:@typescript/typescript6@~6.0.2"
  }
}

Then run:

npm install
npm exec tsc -- --version

Actual output:

Version 6.0.3

On Unix, the selected target can also be seen with:

readlink -f node_modules/.bin/tsc

It resolves to:

<repro>/node_modules/@typescript/old/bin/tsc

As a control, change @typescript/z-native to @typescript/a-native, remove
node_modules and package-lock.json, and run npm install again. Now:

npm exec tsc -- --version

reports:

Version 7.0.2

even though the dependency graph is otherwise equivalent.

Environment

Reproduced with:

Node.js: v22.18.0
npm: 11.19.0
OS: Windows

Metadata

Metadata

Assignees

No one assigned

    Labels

    Bugthing that needs fixingcmd:installrelated to `npm install`ws:arboristRelated to the arborist workspace

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions