Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions workspaces/arborist/lib/arborist/load-actual.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -173,6 +174,8 @@ module.exports = cls => class ActualLoader extends cls {
await Promise.all(promises)
}

this.#applyPackageExtensions()

if (!ignoreMissing) {
await this.#findMissingEdges()
}
Expand Down Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions workspaces/arborist/test/arborist/load-actual.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]/node_modules/broken'),
// safe matches no selector, exercising the non-extended path
safe: t.fixture('symlink', '.store/[email protected]/node_modules/safe'),
'.store': {
'[email protected]': {
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', '../../[email protected]/node_modules/missing'),
},
},
'[email protected]': {
node_modules: {
missing: { 'package.json': JSON.stringify({ name: 'missing', version: '1.0.0' }) },
},
},
'[email protected]': {
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')
})
Loading