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 fixingNeeds Triageneeds review for next steps

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions