diff --git a/workspaces/arborist/lib/arborist/reify.js b/workspaces/arborist/lib/arborist/reify.js index 5f376e94a4cec..52f7d063e24b3 100644 --- a/workspaces/arborist/lib/arborist/reify.js +++ b/workspaces/arborist/lib/arborist/reify.js @@ -110,16 +110,23 @@ module.exports = cls => class Reifier extends cls { await this[_loadTrees](options) const oldTree = this.idealTree + let swappedToIsolated = false if (linked) { // swap out the tree with the isolated tree // this is currently technical debt which will be resolved in a refactor // of Node/Link trees log.warn('reify', 'The "linked" install strategy is EXPERIMENTAL and may contain bugs.') - this.idealTree = await this[_createIsolatedTree]() + // Workspace-filtered installs are incompatible with the isolated tree + // which uses plain objects with Array children and a stub inventory. + // Use the normal reify path for workspace installs. + if (!this.options.workspaces.length) { + this.idealTree = await this[_createIsolatedTree]() + swappedToIsolated = true + } } await this[_diffTrees]() await this.#reifyPackages() - if (linked) { + if (swappedToIsolated) { // swap back in the idealTree // so that the lockfile is preserved this.idealTree = oldTree diff --git a/workspaces/arborist/lib/node.js b/workspaces/arborist/lib/node.js index c0891df4af1e4..f8fcf60e72fdd 100644 --- a/workspaces/arborist/lib/node.js +++ b/workspaces/arborist/lib/node.js @@ -593,7 +593,12 @@ class Node { return false } for (const edge of this.edgesIn) { - if (!npa(edge.spec).registry) { + try { + if (!npa(edge.spec).registry) { + return false + } + } catch { + // unsupported spec types (e.g. link:) are not registry deps return false } } diff --git a/workspaces/arborist/test/isolated-mode.js b/workspaces/arborist/test/isolated-mode.js index cd8434e638966..d054babe50b95 100644 --- a/workspaces/arborist/test/isolated-mode.js +++ b/workspaces/arborist/test/isolated-mode.js @@ -1637,6 +1637,47 @@ tap.test('bins are installed', async t => { t.ok(binFromBarToWhich) }) +tap.test('workspace-filtered install with linked strategy does not crash', async t => { + const graph = { + registry: [ + { name: 'which', version: '1.0.0' }, + { name: 'isexe', version: '1.0.0' }, + ], + root: { + name: 'dog', version: '1.2.3', + }, + workspaces: [ + { name: 'bar', version: '1.0.0', dependencies: { which: '1.0.0' } }, + { name: 'baz', version: '1.0.0', dependencies: { isexe: '1.0.0' } }, + ], + } + + const { dir, registry } = await getRepo(graph) + + const cache = fs.mkdtempSync(`${getTempDir()}/test-`) + + // First, do a full linked install + const arb1 = new Arborist({ path: dir, registry, packumentCache: new Map(), cache }) + await arb1.reify({ installStrategy: 'linked' }) + + // Now do a workspace-filtered install - this used to crash with + // "Cannot read properties of null (reading 'package')" because the + // isolated tree's plain objects are incompatible with workspace filtering. + const arb2 = new Arborist({ + path: dir, + registry, + packumentCache: new Map(), + cache, + installStrategy: 'linked', + workspaces: ['bar'], + }) + await arb2.reify({ installStrategy: 'linked' }) + + // The workspace's dependency should still be resolvable + const whichPath = resolvePackage('which', path.join(dir, 'packages', 'bar')) + t.ok(whichPath, 'bar workspace can still resolve which') +}) + function setupRequire (cwd) { return function requireChain (...chain) { return chain.reduce((path, name) => {