Is there an existing issue for this?
This issue exists in the latest npm version
Current Behavior
npm install with no node_modules or lockfile will create node_modules with nodes marked extraneous.
re-installing will remove those extraneous nodes.
In my case this leads to runtime failures when the module isn't found, and 21 duplicates when fewer would create a valid node_modules. I think the runtime failure is caused by a poorly defined dependency (ts-jest should not mark jest-util optional if it fails when it's not found)
But npm's behavior is weird, and causes packages that were building with npm 10.9.8 or 11.6.1 to fail to build.
Even when the build is successful there's more duplication than necessary.
Expected Behavior
I expect both installs to produce the same trees, or the second 'optimized' install to dedupe and hoist dependencies after removing the extraneous deps. I also expect npm to hoist the required dependency, not the optional dependency, or the hoist the dependency that satisfies more requirements and leads to less duplication.
Steps To Reproduce
Setup
cat > package.json << 'EOF'
{
"name": "repro",
"version": "1.0.0",
"private": true,
"devDependencies": {
"jest": "^29.7.0",
"ts-jest": "^29.1.4",
"@types/jest": "^28.1.8"
}
}
EOF
This is a weird set of dependencies that causes mixing packages from 28, 29, and 30. But I found packages like this in the wild when updating npm from 10 to 11.
Initial install
When we install dependencies there's a top-level node_modules/jest-util @ 30, which seems to be installed for the
optional peer dependency of ts-jest.
$ npm install
$ npm ls
├── @jest/pattern@30.0.1 extraneous
├── @types/jest@28.1.8
├── jest-util@30.3.0 extraneous
├── jest@29.7.0
└── ts-jest@29.4.9
$ npm explain jest-util@30 @jest/pattern@30
jest-util@30.3.0 extraneous
node_modules/jest-util
peerOptional jest-util@"^29.0.0 || ^30.0.0" from ts-jest@29.4.9
node_modules/ts-jest
dev ts-jest@"^29.1.4" from the root project
@jest/pattern@30.0.1 extraneous
node_modules/@jest/pattern
@jest/pattern@"30.0.1" from @jest/types@30.3.0
node_modules/jest-util/node_modules/@jest/types
@jest/types@"30.3.0" from jest-util@30.3.0 extraneous
node_modules/jest-util
peerOptional jest-util@"^29.0.0 || ^30.0.0" from ts-jest@29.4.9
node_modules/ts-jest
dev ts-jest@"^29.1.4" from the root project
The installed tree duplicates jest-util@29 21 times because node_modules/jest-util was claimed by v30, which only had one consumer, and was optional.
$ npm query '#jest-util' --json | jq '.[] | [.name, .version]' -c | sort | uniq -c | sort -nr
21 ["jest-util","29.7.0"]
1 ["jest-util","30.3.0"]
1 ["jest-util","28.1.3"]
Subsequent installs with package-lock.json
$ npm install
removed 8 packages, and audited 365 packages in 402ms
$ npm ls
repro@1.0.0
├── @types/jest@28.1.8
├── jest@29.7.0
└── ts-jest@29.4.9
$ npm explain jest-util@30 @jest/pattern@30
npm error No dependencies found matching jest-util@30, @jest/pattern@30
npm error A complete log of this run can be found in: /home/ANT.AMAZON.COM/calebev/.npm/_logs/2026-04-16T23_47_51_649Z-debug-0.log
$ npm ls jest-util
repro@1.0.0
├─┬ @types/jest@28.1.8
│ └─┬ expect@28.1.3
│ └── jest-util@28.1.3
├─┬ jest@29.7.0
│ └── jest-util@29.7.0 (... 25 more, collapsed)
└─┬ ts-jest@29.4.9
└─┬ @jest/transform@29.7.0
└── jest-util@29.7.0
$ ls node_modules/jest-util
ls: cannot access 'node_modules/jest-util': No such file or directory
$ npm query '#jest-util' --json | jq '.[] | [.name, .version]' -c | sort | uniq -c | sort -nr
21 ["jest-util","29.7.0"]
1 ["jest-util","28.1.3"]
On the second install node_modules/jest-util is gone, but jest-util 29 is still duplicated 21 times.
Now ts-jest's peer dependency is not satisfied.
This seems to be a regression since 11.6.1 (It does not remove node_modules/jest-util)
Environment
- npm: 11.12.1
- Node.js: 24.14.1
- OS Name: Ubuntu
- System Model Name: Lenovo
- npm config:
Is there an existing issue for this?
This issue exists in the latest npm version
Current Behavior
npm installwith no node_modules or lockfile will create node_modules with nodes markedextraneous.re-installing will remove those extraneous nodes.
In my case this leads to runtime failures when the module isn't found, and 21 duplicates when fewer would create a valid node_modules. I think the runtime failure is caused by a poorly defined dependency (ts-jest should not mark jest-util optional if it fails when it's not found)
But npm's behavior is weird, and causes packages that were building with npm 10.9.8 or 11.6.1 to fail to build.
Even when the build is successful there's more duplication than necessary.
Expected Behavior
I expect both installs to produce the same trees, or the second 'optimized' install to dedupe and hoist dependencies after removing the extraneous deps. I also expect npm to hoist the required dependency, not the optional dependency, or the hoist the dependency that satisfies more requirements and leads to less duplication.
Steps To Reproduce
Setup
Initial install
When we install dependencies there's a top-level node_modules/jest-util @ 30, which seems to be installed for the
optional peer dependency of ts-jest.
The installed tree duplicates jest-util@29 21 times because node_modules/jest-util was claimed by v30, which only had one consumer, and was optional.
Subsequent installs with package-lock.json
On the second install node_modules/jest-util is gone, but jest-util 29 is still duplicated 21 times.
Now ts-jest's peer dependency is not satisfied.
This seems to be a regression since 11.6.1 (It does not remove node_modules/jest-util)
Environment
; none