[BUG] Direct dependency's bin can be overwritten by a transitive dependency based on lexical package name
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 10.1k
- Forks
- 4.7k
- Avg merge
- 2d 2h
- Merged PRs (30d)
- 19
Description
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:
https://github.com/yarnpkg/berry/issues/7215
This affects the documented setup for running TypeScript 6 and TypeScript 7
side by side. In the reproduction below:
@typescript/z-nativeis a direct alias for TypeScript 7 and exposestsc.typescriptis a direct alias for@typescript/typescript6.@typescript/typescript6transitively installs@typescript/old, which also
exposestsc.
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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
No source file or test is named. Start by reproducing the collision with the supplied package.json, npm install, and npm exec tsc -- --version commands, then trace npm's executable-linking path and related tests. Done means the direct dependency's bin wins regardless of alias lexical order, with the reproduction selecting TypeScript 7.0.2.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100