Skip to content

[BUG] npm ci re-resolves workspace deps from the registry in v11.14.x, fails on lockfiles v11.13.x accepts #9357

Description

@manzoorwanijk

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 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):

npm version / commit Result
[email protected] ✅ passes
[email protected] (524590f) ✅ passes
e7805c3 fix(arborist): propagate overrides through Link nodes to targets fails — first failing commit
[email protected] (409a717) ❌ fails
[email protected] (0fc09e8) ❌ fails
local HEAD on latest (post-11.14.1) ❌ fails

The regression is introduced by e7805c3 (PR #9198). That commit:

  1. Removes the post-loadVirtual #applyRootOverridesToWorkspaces reconciliation pass in workspaces/arborist/lib/arborist/build-ideal-tree.js.
  2. Adds loadOverrides: true to the root Node constructed by #rootNodeFromPackage so the root's OverrideSet is built up at construction time.
  3. 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 #fetchManifestnpm-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):
    legacy-peer-deps = true
    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions