npm ls marks satisfied edges invalid when sibling override rulesets disagree about an unrelated, not-installed package
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 10.1k
- Forks
- 4.7k
- Avg merge
- 2d 2h
- Merged PRs (30d)
- 19
Description
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:
$ npm@10.9.9 install
$ npm@11.19.1 ci
$ npm@11.19.1 ls --all
...
│ ├─┬ debug@4.4.3 invalid: "^4.3.4" from node_modules/http-proxy-agent
│ │ └── ms@2.1.3
...
npm error code ELSPROBLEMS
npm error invalid: debug@4.4.3 .../node_modules/debug
debug@4.4.3 satisfies ^4.3.4. Running npm@10.9.9 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):
debug@4.4.3:
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 -> INVALID
Workarounds
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the report with the supplied package.json and npm install/ci/ls commands, then inspect workspaces/arborist/lib/edge.js around line 278, especially haveConflictingRules and findSpecificOverrideSet. Trace how sibling override rules are compared and verify that unrelated, mutually exclusive rules do not invalidate the http-proxy-agent → debug edge while genuine conflicts remain detectable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, nodejs
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100