From 71de5408db81b68b42dedd36fec4395b2b3b1edc Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Thu, 18 Jun 2026 23:59:52 +0530 Subject: [PATCH] fix(arborist): re-apply packageExtensions to the linked actual tree --- .../arborist/lib/arborist/load-actual.js | 31 ++++++++++++ .../arborist/test/arborist/load-actual.js | 47 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/workspaces/arborist/lib/arborist/load-actual.js b/workspaces/arborist/lib/arborist/load-actual.js index 2356227a5baa3..2a8538ea9f6e7 100644 --- a/workspaces/arborist/lib/arborist/load-actual.js +++ b/workspaces/arborist/lib/arborist/load-actual.js @@ -13,6 +13,7 @@ const calcDepFlags = require('../calc-dep-flags.js') const Node = require('../node.js') const Link = require('../link.js') const realpath = require('../realpath.js') +const PackageExtensions = require('../package-extensions.js') // public symbols const _changePath = Symbol.for('_changePath') @@ -173,6 +174,8 @@ module.exports = cls => class ActualLoader extends cls { await Promise.all(promises) } + this.#applyPackageExtensions() + if (!ignoreMissing) { await this.#findMissingEdges() } @@ -352,6 +355,34 @@ module.exports = cls => class ActualLoader extends cls { } } + // packageExtensions never rewrite a package's package.json, so a filesystem-scanned actual tree lacks the extension-created edges and provenance. + // Re-derive them from the root rule set, as buildIdealTree does. + // This is always required under the linked strategy, whose store layout forces the filesystem-scan path. + #applyPackageExtensions () { + const rootPkg = this.#actualTree.target?.package + const pe = new PackageExtensions(rootPkg?.packageExtensions) + if (!pe.present || !pe.selectors.length) { + return + } + for (const node of this.#actualTree.inventory.values()) { + // only installed dependencies are extended, never the root or a workspace + if (node.isLink || node.isProjectRoot || !node.name || !node.inNodeModules()) { + continue + } + const res = pe.apply(node.package) + if (res) { + node.package = res.pkg + node.packageExtensionsApplied = res.applied + } + } + // mirror the provenance onto links so the logical tree location reports it too + for (const node of this.#actualTree.inventory.values()) { + if (node.isLink && node.target?.packageExtensionsApplied) { + node.packageExtensionsApplied = node.target.packageExtensionsApplied + } + } + } + async #findMissingEdges () { // try to resolve any missing edges by walking up the directory tree, // checking for the package in each node_modules folder. stop at the diff --git a/workspaces/arborist/test/arborist/load-actual.js b/workspaces/arborist/test/arborist/load-actual.js index 94ad4e7269286..290107eb4f3d9 100644 --- a/workspaces/arborist/test/arborist/load-actual.js +++ b/workspaces/arborist/test/arborist/load-actual.js @@ -498,3 +498,50 @@ t.test('loading a workspace maintains overrides', async t => { const fooEdge = tree.edgesOut.get('foo') t.equal(tree.overrides, fooEdge.overrides, 'foo edge got the correct overrides') }) + +t.test('applies root packageExtensions to a linked actual tree', async t => { + // packageExtensions never rewrite a package's package.json, so the extension edge lives only in lockfile metadata. + // The linked store layout forces loadActual onto the filesystem-scan path, where the edge must be re-derived from the root rule set. + const path = t.testdir({ + 'package.json': JSON.stringify({ + name: 'root', + version: '1.0.0', + dependencies: { broken: '1.0.0', safe: '1.0.0' }, + packageExtensions: { 'broken@1': { dependencies: { missing: '^1.0.0' } } }, + }), + node_modules: { + broken: t.fixture('symlink', '.store/broken@1.0.0/node_modules/broken'), + // safe matches no selector, exercising the non-extended path + safe: t.fixture('symlink', '.store/safe@1.0.0/node_modules/safe'), + '.store': { + 'broken@1.0.0': { + node_modules: { + // physical manifest deliberately omits the extension-added dependency + broken: { 'package.json': JSON.stringify({ name: 'broken', version: '1.0.0' }) }, + missing: t.fixture('symlink', '../../missing@1.0.0/node_modules/missing'), + }, + }, + 'missing@1.0.0': { + node_modules: { + missing: { 'package.json': JSON.stringify({ name: 'missing', version: '1.0.0' }) }, + }, + }, + 'safe@1.0.0': { + node_modules: { + safe: { 'package.json': JSON.stringify({ name: 'safe', version: '1.0.0' }) }, + }, + }, + }, + }, + }) + + const tree = await loadActual(path) + const brokenLink = tree.children.get('broken') + const broken = brokenLink.target + const edge = broken.edgesOut.get('missing') + t.ok(edge && !edge.error, 'extension-added edge is present and resolves') + t.equal(edge.to.name, 'missing', 'edge resolves to the installed package') + const applied = { selector: 'broken@1', dependencies: ['missing'] } + t.strictSame(broken.packageExtensionsApplied, applied, 'provenance recorded on the store node') + t.strictSame(brokenLink.packageExtensionsApplied, applied, 'provenance mirrored onto the link') +})