npm ls marks satisfied edges invalid when sibling override rulesets disagree about an unrelated, not-installed package
Summary
npm ls --all reports a dependency edge as invalid even though the target version satisfies the edge's range, when:
- the edge's override set is the rule for a root-level overridden package (package A),
- the target node's override set is the rule for a different root-level scoped override (package B),
- A's rule and B's rule are siblings under the root override set, and
- the collapsed rulesets of the two siblings disagree about the version of some third package — one side sees the root's global override value, the other side sees a scoped override value that shadows it.
In the repro below the third package (js-yaml) is not installed anywhere in the tree, and no override rule references either the edge's from or to package (http-proxy-agent → debug).
Environment
- Node v22.22.0, Linux
- Affected: npm 11.2.0 → 11.19.1 (arborist 9.0.1 → 9.9.1) and npm 12.0.2 (arborist 10.0.2)
- Not affected: npm 11.1.0 (arborist 9.0.0) and npm 10.9.9
- Introduced by arborist 9.0.1 (shipped in npm 11.2.0) via #8089 (commit
b9225e52), which added thedoOverrideSetsConflictINVALIDbranch toEdge#get error()
Reproduction
package.json:
{
"name": "override-ruleset-repro",
"version": "1.0.0",
"private": true,
"dependencies": {
"http-proxy-agent": "^7.0.0",
"@babel/core": "^7.29.7"
},
"overrides": {
"js-yaml": "5.3.0",
"@babel/core": "^7.29.7",
"http-proxy-agent": {
"js-yaml": "4.3.2"
}
}
}Build the lockfile with an unaffected npm, then re-install and list with a current npm:
$ [email protected] install
$ [email protected] ci
$ [email protected] ls --all
...
│ ├─┬ [email protected] invalid: "^4.3.4" from node_modules/http-proxy-agent
│ │ └── [email protected]
...
npm error code ELSPROBLEMS
npm error invalid: [email protected] .../node_modules/debug[email protected] satisfies ^4.3.4. Running [email protected] ls --all against the same node_modules and lockfile exits 0 with no invalid edges — only the npm version differs.
Why it happens
With npm 11.19.1's bundled arborist (9.9.1):
workspaces/arborist/lib/edge.jsline 278 marks the edgeINVALIDwhendoOverrideSetsConflict(edge.overrides, target.overrides)returns true.- The edge from
http-proxy-agentcarries the override sethttp-proxy-agent=* > ROOT; the hoisteddebugnode carries@babel/core=^7.29.7 > ROOT(assigned from the@babel/coreplacement). These rulesets are siblings, sofindSpecificOverrideSet()finds no containment in either direction and falls through tohaveConflictingRules(). haveConflictingRules()comparesfirst.rulesetvssecond.ruleset. Therulesetgetter collapses the whole ancestry (self first, so a scoped child shadows the root value for the same key):@babel/core's collapsed set containsjs-yaml -> 5.3.0(the root global override),http-proxy-agent's collapsed set containsjs-yaml -> 4.3.2(its own scoped rule shadows the root's),semver.intersects('5.3.0', '4.3.2')is false → conflict declared → edgeINVALID.
Neither ruleset contains a rule for debug, and the two js-yaml rules can never apply to the same package instance (one is scoped to http-proxy-agent's subtree), so this is a false positive for the http-proxy-agent → debug edge specifically.
Programmatic probe (arborist 9.9.1, loadActual() on the repro):
[email protected]:
node.ov chain : @babel/core=^7.29.7 > ROOT
node.ov rules : @babel/core->^7.29.7, http-proxy-agent->*, js-yaml->5.3.0
js-yaml installed anywhere: NO
edge ^4.1.0 from @babel/core: edge.ov=@babel/core=^7.29.7 > ROOT -> OK
edge ^4.3.1 from @babel/traverse: edge.ov=@babel/core=^7.29.7 > ROOT -> OK
edge ^4.3.4 from http-proxy-agent: edge.ov=http-proxy-agent=* > ROOT -> INVALIDWorkarounds
Removing either the global js-yaml override or the scoped http-proxy-agent → js-yaml override makes the edge valid again — but both exist for real reasons in the original project (global security pin + a scoped legacy pin). Our CI currently has to tolerate non-zero npm ls --all exit codes.
Suggested direction
haveConflictingRules (or findSpecificOverrideSet) could restrict the comparison to rules that can actually apply to the edge's target — for example only declaring a conflict when a rule for the target package's name (or for a package in its dependency closure) differs between the two sets. Comparing the full collapsed rulesets of sibling subtrees compares rules that are mutually exclusive by construction.
Related
- #7087 — invalid edges with overrides (different trigger)
- #9514 — overrides within workspaces
- #4834 — overrides not applied with workspaces
This report differs in that no workspaces are involved and the "conflicting" override key refers to a package that is not installed at all.
Source: npm/cli