From be670cdaa250fca6f1e759361399a2016c8dab21 Mon Sep 17 00:00:00 2001 From: Michael Pelikan Date: Mon, 21 Sep 2026 18:21:23 -0600 Subject: [PATCH] fix(arborist): check deps installed under unlinked workspaces when there is no lockfile --- .../arborist/lib/arborist/build-ideal-tree.js | 9 ++++++++ .../test/arborist/build-ideal-tree.js | 21 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/workspaces/arborist/lib/arborist/build-ideal-tree.js b/workspaces/arborist/lib/arborist/build-ideal-tree.js index f7497c2bda474..c907336a988c8 100644 --- a/workspaces/arborist/lib/arborist/build-ideal-tree.js +++ b/workspaces/arborist/lib/arborist/build-ideal-tree.js @@ -497,6 +497,15 @@ module.exports = cls => class IdealTreeBuilder extends cls { for (const edge of node.edgesOut.values()) { children.push(edge.to) } + // An unlinked workspace is unreachable from the root, so walk into what it has installed; walking the workspace itself would queue it ahead of the root, bypassing the workspaces filter. + if (node === tree.target && node.workspaces) { + for (const path of node.workspaces.values()) { + const ws = node.inventory.get(relpath(node.realpath, path)) + for (const { to } of ws?.edgesOut.values() ?? []) { + children.push(to) + } + } + } return children }, filter: node => node, diff --git a/workspaces/arborist/test/arborist/build-ideal-tree.js b/workspaces/arborist/test/arborist/build-ideal-tree.js index 400ff7b238a69..eb07c900f78c9 100644 --- a/workspaces/arborist/test/arborist/build-ideal-tree.js +++ b/workspaces/arborist/test/arborist/build-ideal-tree.js @@ -2312,6 +2312,27 @@ t.test('peer dep that needs to be replaced', async t => { t.matchSnapshot(await printIdeal(path)) }) +t.test('no lockfile or root node_modules, but packages installed under a workspace', async t => { + const path = t.testdir({ + 'package.json': JSON.stringify({ name: 'root', workspaces: ['packages/a'] }), + packages: { + a: { + 'package.json': JSON.stringify({ name: 'a', version: '1.0.0', dependencies: { foo: '1.0.0' } }), + node_modules: { + foo: { + 'package.json': JSON.stringify({ name: 'foo', version: '1.0.0', dependencies: { baz: '1.0.0' } }), + }, + }, + }, + }, + }) + const registry = createRegistry(t, false) + const bazPackuments = registry.packuments(['1.0.0'], 'baz') + await registry.package({ manifest: registry.manifest({ name: 'baz', packuments: bazPackuments }) }) + const tree = await buildIdeal(path) + t.equal(tree.children.get('baz')?.version, '1.0.0', 'dep of the package under the workspace is added') +}) + t.test('transitive conflicted peer dependency', async t => { // see test/fixtures/testing-transitive-conflicted-peer/README.md // for a thorough explanation.