Is there an existing issue for this?
This issue exists in the latest npm version
Current Behavior
npm ci in v11.14.0+ rejects a lockfile that v11.13.0 accepts. The verbose log shows npm ci fetching the packument for a workspace dep, picking the latest version that matches the workspace's ^ range, calling placeDep ROOT … REPLACE, and then erroring because the replacement doesn't match the (perfectly valid) version in the lockfile.
npm error code EUSAGE
npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm error
npm error Invalid: lock file's @types/[email protected] does not satisfy @types/[email protected]
Verbose trace (key lines):
silly fetch manifest @types/node@^20.19.39
silly placeDep ROOT @types/[email protected] REPLACE for: <workspace-name>@<version> want: ^20.19.39
@types/[email protected] satisfies ^20.19.39, so npm ci should never need to talk to the registry to verify that — but in v11.14.0+ it does, and then errors against its own re-resolution.
This breaks npm ci whenever a workspace's ^ range has had a newer patch published since the lockfile was generated, which is the steady state for any long-lived monorepo.
Expected Behavior
npm ci's contract: install exactly what the lockfile says; only fail if the lockfile cannot satisfy package.json. A lockfile that resolves 20.19.39 against a workspace's ^20.19.39 range satisfies that range — npm ci must not consult the registry, and must not "upgrade" the resolution.
This is what v11.13.0 does on the same tree.
Bisect
Tested against the same monorepo lockfile (@types/[email protected] in lockfile, ^20.19.39 in workspace package.json, 20.19.41 available on registry):
The regression is introduced by e7805c3 (PR #9198). That commit:
- Removes the post-
loadVirtual #applyRootOverridesToWorkspaces reconciliation pass in workspaces/arborist/lib/arborist/build-ideal-tree.js.
- Adds
loadOverrides: true to the root Node constructed by #rootNodeFromPackage so the root's OverrideSet is built up at construction time.
- Adds
Link.recalculateOutEdgesOverrides in workspaces/arborist/lib/link.js to forward overrides from a Link to its target node.
After this change, override propagation runs through every workspace target's edgesOut, calling edge.reload(true) on each. For the affected workspace dep (@types/node here, but also @wordpress/scripts via a file: workspace spec in the same trace), the reload appears to invalidate an edge that the lockfile already satisfied. That edge is then collected by #problemEdges(node) and sent through #fetchManifest → npm-pick-manifest, which returns the latest version matching the ^ range (20.19.41), and placeDep calls REPLACE at the root. validateLockfile() then rejects the resulting ideal tree against the (still 20.19.39) virtual tree.
Confirmed reverting e7805c3 makes the bug disappear (parent commit 524590fea passes the same lockfile that e7805c3a4 rejects).
Steps To Reproduce
The bug fires reliably against a real-world npm-workspaces monorepo with:
- root
package.json declaring overrides (e.g. for react / react-dom via $react self-reference syntax),
- a workspace whose
package.json declares a dep with a ^ range (e.g. "@types/node": "^20.19.39"),
- a
package-lock.json that resolves that dep to the lower bound of the range (20.19.39),
- the registry currently publishing a newer matching patch (
20.19.41).
Reproduction commands against such a tree:
# 1. Confirm the lockfile is internally consistent
node -e "const l = require('./package-lock.json'); console.log(l.packages['node_modules/@types/node'].version)"
# -> 20.19.39
# 2. v11.13.0 accepts it
rm -rf node_modules && npx -y [email protected] ci --dry-run --ignore-scripts
# -> exits 0
# 3. v11.14.1 rejects it
rm -rf node_modules && npx -y [email protected] ci --dry-run --ignore-scripts
# -> EUSAGE: Invalid: lock file's @types/[email protected] does not satisfy @types/[email protected]
A fully self-contained minimal repro (without the surrounding tree) has not yet reduced — the bug needs the override-propagation cascade to reach a workspace target whose recalculateOutEdgesOverrides invalidates a previously-valid edge, and the conditions for that invalidation in a small synthetic tree are still being narrowed. Reproducing under the bisect against any monorepo with the shape above is sufficient evidence of the regression, since the failing/passing bracket isolates a single commit.
Where it was noticed
Linked-strategy exploration in WordPress/gutenberg. The first failing CI run is WordPress/gutenberg actions/runs/25867266895/job/76012604708. Multiple Compressed Size and Run performance tests jobs were broken on every PR until the lockfile was force-bumped via npm install.
Environment
- npm:
11.14.0 and 11.14.1 (and latest HEAD) — fail. 11.12.1 and 11.13.0 — pass.
- Node.js:
20.20.2 (which ships npm 11.14.1) and 24.15.0 (with npx -y npm@<ver>) — both reproduce.
- OS: macOS (Darwin 25.5.0). CI: Ubuntu via
actions/setup-node@v6 (also reproduces).
- npm config (representative
.npmrc from the affected repo):
The bug also reproduces with prefer-dedupe, lockfile-version = 3, min-release-age = 1, and install-strategy = linked set or unset — none of those are required to trigger it. legacy-peer-deps = true is needed only to bypass an unrelated ERESOLVE peer error in the affected repo.
References
Is there an existing issue for this?
This issue exists in the latest npm version
Current Behavior
npm ciin v11.14.0+ rejects a lockfile that v11.13.0 accepts. The verbose log showsnpm cifetching the packument for a workspace dep, picking the latest version that matches the workspace's^range, callingplaceDep ROOT … REPLACE, and then erroring because the replacement doesn't match the (perfectly valid) version in the lockfile.Verbose trace (key lines):
@types/[email protected]satisfies^20.19.39, sonpm cishould never need to talk to the registry to verify that — but in v11.14.0+ it does, and then errors against its own re-resolution.This breaks
npm ciwhenever a workspace's^range has had a newer patch published since the lockfile was generated, which is the steady state for any long-lived monorepo.Expected Behavior
npm ci's contract: install exactly what the lockfile says; only fail if the lockfile cannot satisfypackage.json. A lockfile that resolves20.19.39against a workspace's^20.19.39range satisfies that range —npm cimust not consult the registry, and must not "upgrade" the resolution.This is what
v11.13.0does on the same tree.Bisect
Tested against the same monorepo lockfile (
@types/[email protected]in lockfile,^20.19.39in workspacepackage.json,20.19.41available on registry):[email protected][email protected](524590f)fix(arborist): propagate overrides through Link nodes to targets[email protected](409a717)[email protected](0fc09e8)latest(post-11.14.1)The regression is introduced by e7805c3 (PR #9198). That commit:
loadVirtual#applyRootOverridesToWorkspacesreconciliation pass inworkspaces/arborist/lib/arborist/build-ideal-tree.js.loadOverrides: trueto the rootNodeconstructed by#rootNodeFromPackageso the root'sOverrideSetis built up at construction time.Link.recalculateOutEdgesOverridesinworkspaces/arborist/lib/link.jsto forward overrides from a Link to its target node.After this change, override propagation runs through every workspace target's
edgesOut, callingedge.reload(true)on each. For the affected workspace dep (@types/nodehere, but also@wordpress/scriptsvia afile:workspace spec in the same trace), the reload appears to invalidate an edge that the lockfile already satisfied. That edge is then collected by#problemEdges(node)and sent through#fetchManifest→npm-pick-manifest, which returns the latest version matching the^range (20.19.41), andplaceDepcallsREPLACEat the root.validateLockfile()then rejects the resulting ideal tree against the (still 20.19.39) virtual tree.Confirmed reverting e7805c3 makes the bug disappear (parent commit
524590feapasses the same lockfile thate7805c3a4rejects).Steps To Reproduce
The bug fires reliably against a real-world npm-workspaces monorepo with:
package.jsondeclaringoverrides(e.g. forreact/react-domvia$reactself-reference syntax),package.jsondeclares a dep with a^range (e.g."@types/node": "^20.19.39"),package-lock.jsonthat resolves that dep to the lower bound of the range (20.19.39),20.19.41).Reproduction commands against such a tree:
A fully self-contained minimal repro (without the surrounding tree) has not yet reduced — the bug needs the override-propagation cascade to reach a workspace target whose
recalculateOutEdgesOverridesinvalidates a previously-valid edge, and the conditions for that invalidation in a small synthetic tree are still being narrowed. Reproducing under the bisect against any monorepo with the shape above is sufficient evidence of the regression, since the failing/passing bracket isolates a single commit.Where it was noticed
Linked-strategy exploration in WordPress/gutenberg. The first failing CI run is WordPress/gutenberg actions/runs/25867266895/job/76012604708. Multiple
Compressed SizeandRun performance testsjobs were broken on every PR until the lockfile was force-bumped vianpm install.Environment
11.14.0and11.14.1(andlatestHEAD) — fail.11.12.1and11.13.0— pass.20.20.2(which ships npm11.14.1) and24.15.0(withnpx -y npm@<ver>) — both reproduce.actions/setup-node@v6(also reproduces)..npmrcfrom the affected repo):legacy-peer-deps = trueprefer-dedupe,lockfile-version = 3,min-release-age = 1, andinstall-strategy = linkedset or unset — none of those are required to trigger it.legacy-peer-deps = trueis needed only to bypass an unrelatedERESOLVEpeer error in the affected repo.References
fix(arborist): propagate overrides through Link nodes to targets(commit e7805c3 onrelease/v11, originally bc32d94 onlatest).