Skip to content

Commit 26fa40e

Browse files
committed
fix: fix workspace-filtered install with linked strategy
1 parent 6565eeb commit 26fa40e

File tree

4 files changed

+119
-20
lines changed

4 files changed

+119
-20
lines changed

workspaces/arborist/lib/arborist/isolated-reifier.js

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ module.exports = cls => class IsolatedReifier extends cls {
267267
}
268268

269269
const root = {
270-
fsChildren: [],
270+
fsChildren: new Set(),
271271
integrity: null,
272272
inventory: new Map(),
273273
isLink: false,
@@ -286,30 +286,67 @@ module.exports = cls => class IsolatedReifier extends cls {
286286
meta: { loadedFromDisk: false },
287287
global: false,
288288
isProjectRoot: true,
289-
children: [],
289+
children: new Map(),
290+
workspaces: new Map(),
291+
tops: new Set(),
292+
linksIn: new Set(),
290293
}
291-
// root.inventory.set('', t)
292-
// root.meta = this.idealTree.meta
293-
// TODO We should mock better the inventory object because it is used by audit-report.js ... maybe
294+
root.inventory.set('', root)
295+
// TODO inventory.query is a stub; audit-report needs 'packageName' support
294296
root.inventory.query = () => {
295297
return []
296298
}
297299
const processed = new Set()
298300
proxiedIdealTree.workspaces.forEach(c => {
301+
const wsName = c.packageName
299302
const workspace = {
300303
edgesIn: new Set(),
301304
edgesOut: new Map(),
302-
children: [],
305+
children: new Map(),
306+
fsChildren: new Set(),
303307
hasInstallScript: c.hasInstallScript,
304308
binPaths: [],
305309
package: c.package,
306310
location: c.localLocation,
307311
path: c.localPath,
308312
realpath: c.localPath,
309313
resolved: c.resolved,
314+
isLink: false,
315+
isRoot: false,
316+
name: wsName,
317+
linksIn: new Set(),
310318
}
311-
root.fsChildren.push(workspace)
319+
workspace.target = workspace
320+
root.fsChildren.add(workspace)
312321
root.inventory.set(workspace.location, workspace)
322+
323+
// Create workspace Link entry in children for _diffTrees lookup
324+
const wsLink = {
325+
name: wsName,
326+
location: join('node_modules', wsName),
327+
path: join(root.path, 'node_modules', wsName),
328+
realpath: workspace.path,
329+
isLink: true,
330+
target: workspace,
331+
children: new Map(),
332+
fsChildren: new Set(),
333+
edgesIn: new Set(),
334+
edgesOut: new Map(),
335+
binPaths: [],
336+
root,
337+
parent: root,
338+
isRoot: false,
339+
isProjectRoot: false,
340+
isTop: false,
341+
global: false,
342+
globalTop: false,
343+
package: workspace.package,
344+
linksIn: new Set(),
345+
}
346+
root.children.set(wsLink.name, wsLink)
347+
root.inventory.set(wsLink.location, wsLink)
348+
root.workspaces.set(wsName, workspace.path)
349+
workspace.linksIn.add(wsLink)
313350
})
314351
const generateChild = (node, location, pkg, inStore) => {
315352
const newChild = {
@@ -321,11 +358,11 @@ module.exports = cls => class IsolatedReifier extends cls {
321358
name: node.packageName || node.name,
322359
optional: node.optional,
323360
top: { path: proxiedIdealTree.root.localPath },
324-
children: [],
361+
children: new Map(),
325362
edgesIn: new Set(),
326363
edgesOut: new Map(),
327364
binPaths: [],
328-
fsChildren: [],
365+
fsChildren: new Set(),
329366
/* istanbul ignore next -- emulate Node */
330367
getBundler () {
331368
return null
@@ -343,7 +380,7 @@ module.exports = cls => class IsolatedReifier extends cls {
343380
package: pkg,
344381
}
345382
newChild.target = newChild
346-
root.children.push(newChild)
383+
root.children.set(newChild.location, newChild)
347384
root.inventory.set(newChild.location, newChild)
348385
}
349386
proxiedIdealTree.external.forEach(c => {
@@ -379,10 +416,10 @@ module.exports = cls => class IsolatedReifier extends cls {
379416
let from, nmFolder
380417
if (externalEdge) {
381418
const fromLocation = join('node_modules', '.store', key, 'node_modules', node.packageName)
382-
from = root.children.find(c => c.location === fromLocation)
419+
from = root.children.get(fromLocation)
383420
nmFolder = join('node_modules', '.store', key, 'node_modules')
384421
} else {
385-
from = node.isProjectRoot ? root : root.fsChildren.find(c => c.location === node.localLocation)
422+
from = node.isProjectRoot ? root : root.inventory.get(node.localLocation)
386423
nmFolder = join(node.localLocation, 'node_modules')
387424
}
388425
/* istanbul ignore next - strict-peer-deps can exclude nodes from the tree */
@@ -401,9 +438,9 @@ module.exports = cls => class IsolatedReifier extends cls {
401438
let target
402439
if (external) {
403440
const toLocation = join('node_modules', '.store', toKey, 'node_modules', dep.packageName)
404-
target = root.children.find(c => c.location === toLocation)
441+
target = root.children.get(toLocation)
405442
} else {
406-
target = root.fsChildren.find(c => c.location === dep.localLocation)
443+
target = root.inventory.get(dep.localLocation)
407444
}
408445
// TODO: we should no-op is an edge has already been created with the same fromKey and toKey
409446
/* istanbul ignore next - strict-peer-deps can exclude nodes from the tree */
@@ -430,8 +467,8 @@ module.exports = cls => class IsolatedReifier extends cls {
430467
name: toKey,
431468
resolved: dep.resolved,
432469
top: { path: dep.root.localPath },
433-
children: [],
434-
fsChildren: [],
470+
children: new Map(),
471+
fsChildren: new Set(),
435472
isLink: true,
436473
isStoreLink: true,
437474
isRoot: false,
@@ -444,7 +481,7 @@ module.exports = cls => class IsolatedReifier extends cls {
444481
const newEdge2 = { optional: false, from: link, to: target }
445482
link.edgesOut.set(dep.name, newEdge2)
446483
target.edgesIn.add(newEdge2)
447-
root.children.push(link)
484+
root.children.set(link.location, link)
448485
}
449486

450487
for (const dep of node.localDependencies) {

workspaces/arborist/lib/arborist/reify.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ module.exports = cls => class Reifier extends cls {
146146
// was not changed, delete anything in the ideal and not actual.
147147
// Then we move the entire idealTree over to this.actualTree, and
148148
// save the hidden lockfile.
149-
if (this.diff && this.diff.filterSet.size) {
149+
if (this.diff && this.diff.filterSet.size && !linked) {
150150
const reroot = new Set()
151151

152152
const { filterSet } = this.diff
@@ -422,7 +422,7 @@ module.exports = cls => class Reifier extends cls {
422422
if (includeWorkspaces) {
423423
// add all ws nodes to filterNodes
424424
for (const ws of this.options.workspaces) {
425-
const ideal = this.idealTree.children.get && this.idealTree.children.get(ws)
425+
const ideal = this.idealTree.children.get(ws)
426426
if (ideal) {
427427
filterNodes.push(ideal)
428428
}

workspaces/arborist/lib/diff.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class Diff {
7171
tree: filterNode,
7272
visit: node => filterSet.add(node),
7373
getChildren: node => {
74+
const orig = node
7475
node = node.target
7576
const loc = node.location
7677
const idealNode = ideal.inventory.get(loc)
@@ -87,7 +88,12 @@ class Diff {
8788
}
8889
}
8990

90-
return ideals.concat(actuals)
91+
const result = ideals.concat(actuals)
92+
// Include link targets so store entries end up in filterSet
93+
if (orig.isLink) {
94+
result.push(node)
95+
}
96+
return result
9197
},
9298
})
9399
}

workspaces/arborist/test/isolated-mode.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,62 @@ tap.test('postinstall scripts run once for store packages', async t => {
16011601
t.equal(count, 1, 'postinstall ran exactly once')
16021602
})
16031603

1604+
tap.test('workspace-filtered install with linked strategy', async t => {
1605+
// Two workspaces sharing the same dependency must not crash when installing with --workspace + --install-strategy=linked.
1606+
const graph = {
1607+
registry: [
1608+
{ name: 'abbrev', version: '2.0.0' },
1609+
],
1610+
root: {
1611+
name: 'myroot', version: '1.0.0',
1612+
},
1613+
workspaces: [
1614+
{ name: 'ws-a', version: '1.0.0', dependencies: { abbrev: '2.0.0' } },
1615+
{ name: 'ws-b', version: '1.0.0', dependencies: { abbrev: '2.0.0' } },
1616+
],
1617+
}
1618+
1619+
const { dir, registry } = await getRepo(graph)
1620+
1621+
const cache = fs.mkdtempSync(`${getTempDir()}/test-`)
1622+
const arborist = new Arborist({ path: dir, registry, packumentCache: new Map(), cache })
1623+
1624+
// Full install first
1625+
await arborist.reify({ installStrategy: 'linked' })
1626+
1627+
// Verify store entry exists
1628+
const storeDir = path.join(dir, 'node_modules', '.store')
1629+
const storeEntries = fs.readdirSync(storeDir)
1630+
t.ok(storeEntries.some(e => e.startsWith('abbrev@')), 'store has abbrev entry after full install')
1631+
1632+
// Workspace-filtered install must not crash
1633+
const arborist2 = new Arborist({
1634+
path: dir,
1635+
registry,
1636+
packumentCache: new Map(),
1637+
cache,
1638+
workspaces: ['ws-a'],
1639+
})
1640+
await arborist2.reify({
1641+
installStrategy: 'linked',
1642+
workspaces: ['ws-a'],
1643+
})
1644+
1645+
// Verify workspace filtering was actually applied (not silently skipped)
1646+
t.ok(arborist2.diff.filterSet.size > 0, 'workspace filter was applied to diff')
1647+
1648+
// Store entries still intact
1649+
const storeEntries2 = fs.readdirSync(storeDir)
1650+
t.ok(storeEntries2.some(e => e.startsWith('abbrev@')), 'store entries preserved after ws install')
1651+
1652+
// Workspace symlinks preserved
1653+
const wsALink = fs.readlinkSync(path.join(dir, 'packages', 'ws-a', 'node_modules', 'abbrev'))
1654+
t.ok(wsALink.includes('.store'), 'workspace a abbrev symlink points to store')
1655+
1656+
const wsBLink = fs.readlinkSync(path.join(dir, 'packages', 'ws-b', 'node_modules', 'abbrev'))
1657+
t.ok(wsBLink.includes('.store'), 'workspace b abbrev symlink preserved')
1658+
})
1659+
16041660
tap.test('bins are installed', async t => {
16051661
// Input of arborist
16061662
const graph = {

0 commit comments

Comments
 (0)