From a8eb7153c6222fd95697114bf5fc71d34507568e Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Fri, 27 Feb 2026 22:30:11 +0530 Subject: [PATCH 1/2] fix: skip isolated tree for workspace-filtered installs with linked strategy The isolated tree built by _createIsolatedTree() uses plain objects with Array children and a stub inventory.query(), which are incompatible with workspace filtering in _diffTrees() and the post-reify re-rooting logic. Skip the isolated tree swap when --workspace is specified and fall back to the normal reify path. --- workspaces/arborist/lib/arborist/reify.js | 11 ++++-- workspaces/arborist/test/isolated-mode.js | 41 +++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) 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/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) => { From 88376a915bdcc2658821ddd0e96b728aba808147 Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Fri, 27 Feb 2026 22:58:19 +0530 Subject: [PATCH 2/2] fix: handle unsupported spec types in isRegistryDependency Packages with link: specs in their dependencies (e.g. link:.) cause npa to throw EUNSUPPORTEDPROTOCOL when isRegistryDependency is accessed during lockfile save. Catch the error and treat unsupported spec types as non-registry dependencies. --- workspaces/arborist/lib/node.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 } }