From 6894d93ddbaa38de3b9119a189a128094e8c1a0b Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Tue, 3 Mar 2026 16:43:57 +0530 Subject: [PATCH 1/2] fix(arborist): unwrap Link nodes in legacyPeerDeps resolution for linked strategy --- .../arborist/lib/arborist/isolated-reifier.js | 2 +- workspaces/arborist/test/isolated-mode.js | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/workspaces/arborist/lib/arborist/isolated-reifier.js b/workspaces/arborist/lib/arborist/isolated-reifier.js index 08255313bcb20..2842ec3268b76 100644 --- a/workspaces/arborist/lib/arborist/isolated-reifier.js +++ b/workspaces/arborist/lib/arborist/isolated-reifier.js @@ -154,7 +154,7 @@ module.exports = cls => class IsolatedReifier extends cls { if (!edgeNames.has(peerName)) { const resolved = node.resolve(peerName) if (resolved && resolved !== node && !resolved.inert) { - nonOptionalDeps.push(resolved) + nonOptionalDeps.push(resolved.target) } } } diff --git a/workspaces/arborist/test/isolated-mode.js b/workspaces/arborist/test/isolated-mode.js index 3c2b14bbb6e68..6eca2ac306ef5 100644 --- a/workspaces/arborist/test/isolated-mode.js +++ b/workspaces/arborist/test/isolated-mode.js @@ -368,6 +368,56 @@ tap.test('peer dependencies with legacyPeerDeps', async t => { rule7.apply(t, dir, resolved, asserted) }) +tap.test('idempotent install with legacyPeerDeps and workspace peer deps', async t => { + // Regression: when legacyPeerDeps is enabled and a workspace has a peer + // dependency on another workspace, node.resolve() returns the Link node + // (not its target). This caused workspaceProxy to be called with the Link, + // producing store links under node_modules//node_modules/ that race + // with the workspace symlink at node_modules/, hitting EEXIST on the + // second install. + // + // Use many workspaces with cross-peer-deps to increase concurrency and + // make the race window large enough to trigger reliably. + const workspaces = [] + for (let i = 0; i < 20; i++) { + workspaces.push({ + name: `ws-${i}`, + version: '1.0.0', + dependencies: { abbrev: '1.0.0' }, + peerDependencies: { [`ws-${(i + 1) % 20}`]: '*' }, + }) + } + + const graph = { + registry: [ + { name: 'abbrev', version: '1.0.0' }, + ], + root: { + name: 'myroot', version: '1.0.0', + }, + workspaces, + } + + const { dir, registry } = await getRepo(graph) + + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + const opts = { path: dir, registry, packumentCache: new Map(), cache, legacyPeerDeps: true } + + // First install + const arb1 = new Arborist(opts) + await arb1.reify({ installStrategy: 'linked' }) + + // Second install must not throw EEXIST + const arb2 = new Arborist({ ...opts, packumentCache: new Map() }) + await arb2.reify({ installStrategy: 'linked' }) + + // Workspace symlinks should still be symlinks (not directories) + for (let i = 0; i < 20; i++) { + t.ok(fs.lstatSync(path.join(dir, 'node_modules', `ws-${i}`)).isSymbolicLink(), + `ws-${i} is still a symlink after second install`) + } +}) + tap.test('Lock file is same in hoisted and in isolated mode', async t => { const graph = { registry: [ From fe3be313293ea28690eaad972778809ce56bd4e4 Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Tue, 3 Mar 2026 23:31:45 +0530 Subject: [PATCH 2/2] Use single line comment. --- workspaces/arborist/test/isolated-mode.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/workspaces/arborist/test/isolated-mode.js b/workspaces/arborist/test/isolated-mode.js index 6eca2ac306ef5..14673546dfa86 100644 --- a/workspaces/arborist/test/isolated-mode.js +++ b/workspaces/arborist/test/isolated-mode.js @@ -369,15 +369,8 @@ tap.test('peer dependencies with legacyPeerDeps', async t => { }) tap.test('idempotent install with legacyPeerDeps and workspace peer deps', async t => { - // Regression: when legacyPeerDeps is enabled and a workspace has a peer - // dependency on another workspace, node.resolve() returns the Link node - // (not its target). This caused workspaceProxy to be called with the Link, - // producing store links under node_modules//node_modules/ that race - // with the workspace symlink at node_modules/, hitting EEXIST on the - // second install. - // - // Use many workspaces with cross-peer-deps to increase concurrency and - // make the race window large enough to trigger reliably. + // Regression: when legacyPeerDeps is enabled and a workspace has a peer dependency on another workspace, node.resolve() returns the Link node (not its target). This caused workspaceProxy to be called with the Link, producing store links under node_modules//node_modules/ that race with the workspace symlink at node_modules/, hitting EEXIST on the second install. + // Use many workspaces with cross-peer-deps to increase concurrency and make the race window large enough to trigger reliably. const workspaces = [] for (let i = 0; i < 20; i++) { workspaces.push({