diff --git a/workspaces/arborist/lib/arborist/load-actual.js b/workspaces/arborist/lib/arborist/load-actual.js index d6615caa42fb2..ea21e97900767 100644 --- a/workspaces/arborist/lib/arborist/load-actual.js +++ b/workspaces/arborist/lib/arborist/load-actual.js @@ -438,8 +438,8 @@ module.exports = cls => class ActualLoader extends cls { const depPromises = [] for (const [name, edge] of node.edgesOut.entries()) { - const notMissing = !edge.missing && - !(edge.to && (edge.to.dummy || edge.to.parent !== node)) + // An unresolved optional edge has edge.missing === false, so walk any edge with no target to find a store sibling. + const notMissing = edge.to && !(edge.to.dummy || edge.to.parent !== node) if (notMissing) { continue } diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index ccd1ebef48bc0..7fd1f2ee3aaf4 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -4491,6 +4491,39 @@ t.test('install strategy linked', async (t) => { await reify(path, { installStrategy: 'hoisted' }) t.notOk(fs.existsSync(resolve(nm, '.store')), '.store removed by a full hoisted install') }) + + t.test('installed transitive optional dep is materialized in the FS-scanned actual tree', async t => { + // Regression for #9622: under linked, #findMissingEdges skipped unresolved optional edges, so an installed transitive optional dep was reported UNMET. + const optionalDependencies = { bar: '^1.0.0', baz: '^1.0.0' } + const path = t.testdir({ + 'package.json': JSON.stringify({ name: 'root', version: '1.0.0', dependencies: { foo: '1.0.0' } }), + src: { + // bar is installed (store sibling); baz is platform-incompatible so its edge must stay unresolved. + foo: { 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0', optionalDependencies }) }, + bar: { 'package.json': JSON.stringify({ name: 'bar', version: '1.2.3' }) }, + }, + }) + const registry = new MockRegistry({ strict: false, tap: t, registry: 'https://registry.npmjs.org' }) + const fooManifest = registry.manifest({ name: 'foo', packuments: [{ version: '1.0.0', optionalDependencies }] }) + const barManifest = registry.manifest({ name: 'bar', packuments: [{ version: '1.2.3' }] }) + const bazManifest = registry.manifest({ name: 'baz', packuments: [{ version: '1.0.0', os: ['nonexistent-platform'] }] }) + await registry.package({ manifest: fooManifest, tarballs: { '1.0.0': join(path, 'src/foo') } }) + await registry.package({ manifest: barManifest, tarballs: { '1.2.3': join(path, 'src/bar') } }) + await registry.package({ manifest: bazManifest }) + + await reify(path, { installStrategy: 'linked' }) + const fooStore = fs.readdirSync(resolve(path, 'node_modules/.store')).find(d => d.startsWith('foo@')) + t.ok(fooStore && fs.existsSync(resolve(path, 'node_modules/.store', fooStore, 'node_modules/bar')), + 'bar is installed as a store sibling of foo') + + // forceActual skips the hidden lockfile, exercising the FS-scan path. + const actual = await newArb({ path, installStrategy: 'linked' }).loadActual({ forceActual: true }) + const foo = [...actual.inventory.values()].find(n => n.name === 'foo' && !n.isLink) + const barEdge = foo.edgesOut.get('bar') + t.ok(barEdge.to, 'the optional bar edge resolves to an installed node, not UNMET') + t.equal(barEdge.to.version, '1.2.3', 'bar resolved to the installed store node') + t.notOk(foo.edgesOut.get('baz').to, 'a genuinely uninstalled optional dep stays unresolved') + }) }) t.test('linked strategy --workspaces=false and --include-workspace-root do not crash', async t => {