Skip to content

Commit 83ea1a1

Browse files
fix(arborist): report ETARGET instead of ERESOLVE for unresolvable peer targets (#9987)
When a dependency has no matching version and another dependency peers on it, `npm install` failed with `ERESOLVE ... Found: <pkg>@undefined` instead of the real `ETARGET`. The failed fetch becomes a versionless load-failure node, and [`#loadPeerSet`](https://github.com/npm/cli/blob/latest/workspaces/arborist/lib/arborist/build-ideal-tree.js#L1663) treated it as a peer conflict before `#pruneFailedOptional` could report the load error. Both conflict checks in `#loadPeerSet` now defer when the conflicting node is a load failure, so non-optional failures throw their own error and fully optional sets are pruned as usual. ## References Fixes #9979
1 parent b317f16 commit 83ea1a1

2 files changed

Lines changed: 73 additions & 2 deletions

File tree

‎workspaces/arborist/lib/arborist/build-ideal-tree.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,7 +1660,7 @@ This is a one-time fix-up, please be patient...
16601660
// allow it. either we're overriding, or it's not something
16611661
// that will be installed by default anyway, and we'll fail when
16621662
// we get to the point where we need to, if we need to.
1663-
if (conflictOK || !required.has(dep)) {
1663+
if (conflictOK || !required.has(dep) || this.#loadFailures.has(dep)) {
16641664
edge.peerConflicted = true
16651665
continue
16661666
}
@@ -1694,7 +1694,7 @@ This is a one-time fix-up, please be patient...
16941694
// isn't also required, then there's a good chance we won't need it,
16951695
// so allow it for now and let it conflict if it turns out to actually
16961696
// be necessary for the installation.
1697-
if (conflictOK || !required.has(edge.from)) {
1697+
if (conflictOK || !required.has(edge.from) || this.#loadFailures.has(current)) {
16981698
continue
16991699
}
17001700

‎workspaces/arborist/test/arborist/build-ideal-tree.js‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5435,3 +5435,74 @@ t.test('incomplete manifest from proxy registry prunes optional dep (#9342)', as
54355435
t.equal(aixNode.errors[0].code, 'EINCOMPLETEMANIFEST',
54365436
'node has EINCOMPLETEMANIFEST error')
54375437
})
5438+
5439+
t.test('unresolvable dep that is also a peer target reports the load failure, not ERESOLVE', async t => {
5440+
const registry = createRegistry(t, false)
5441+
5442+
const reactManifest = registry.manifest({ name: 'react', versions: ['18.2.0'] })
5443+
await registry.package({ manifest: reactManifest, times: 3 })
5444+
5445+
const reactDomManifest = registry.manifest({
5446+
name: 'react-dom',
5447+
packuments: [{ version: '18.2.0', peerDependencies: { react: '^18.2.0' } }],
5448+
})
5449+
await registry.package({ manifest: reactDomManifest })
5450+
5451+
const path = t.testdir({
5452+
'package.json': JSON.stringify({
5453+
dependencies: { react: '^99.0.0', 'react-dom': '18.2.0' },
5454+
}),
5455+
})
5456+
5457+
await t.rejects(buildIdeal(path), { code: 'ETARGET' })
5458+
})
5459+
5460+
t.test('unresolvable dep already placed in a peer set reports the load failure, not ERESOLVE', async t => {
5461+
const registry = createRegistry(t, false)
5462+
5463+
const aManifest = registry.manifest({
5464+
name: 'a',
5465+
packuments: [{ version: '1.0.0', peerDependencies: { b: '^1.0.0', x: '^1.0.0' } }],
5466+
})
5467+
await registry.package({ manifest: aManifest })
5468+
5469+
const bManifest = registry.manifest({
5470+
name: 'b',
5471+
packuments: [{ version: '1.0.0', peerDependencies: { x: '^1.0.0' } }],
5472+
})
5473+
await registry.package({ manifest: bManifest })
5474+
5475+
const xManifest = registry.manifest({ name: 'x' })
5476+
await registry.package({ manifest: xManifest, times: 3 })
5477+
5478+
const path = t.testdir({
5479+
'package.json': JSON.stringify({
5480+
dependencies: { a: '1.0.0', x: '^99.0.0' },
5481+
}),
5482+
})
5483+
5484+
await t.rejects(buildIdeal(path), { code: 'ETARGET' })
5485+
})
5486+
5487+
t.test('unresolvable optional dep drops its optional peer dependent instead of ERESOLVE', async t => {
5488+
const registry = createRegistry(t, false)
5489+
5490+
const reactManifest = registry.manifest({ name: 'react', versions: ['18.2.0'] })
5491+
await registry.package({ manifest: reactManifest, times: 3 })
5492+
5493+
const reactDomManifest = registry.manifest({
5494+
name: 'react-dom',
5495+
packuments: [{ version: '18.2.0', peerDependencies: { react: '^18.2.0' } }],
5496+
})
5497+
await registry.package({ manifest: reactDomManifest })
5498+
5499+
const path = t.testdir({
5500+
'package.json': JSON.stringify({
5501+
optionalDependencies: { react: '^99.0.0', 'react-dom': '18.2.0' },
5502+
}),
5503+
})
5504+
5505+
const tree = await buildIdeal(path)
5506+
t.ok(tree.children.get('react').inert, 'failed react is inert')
5507+
t.ok(tree.children.get('react-dom').inert, 'react-dom is inert with its missing peer')
5508+
})

0 commit comments

Comments
 (0)