Skip to content

[BUG] 11.12.1 peerOptional dependencies marked extraneous and removed #9249

@everett1992

Description

@everett1992

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

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

  • npm 11.12.1

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:
; none

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