Skip to content

Commit 01afa39

Browse files
committed
fix: workspaces respect overrides on subsequent installs
1 parent 2f5392a commit 01afa39

File tree

2 files changed

+114
-8
lines changed

2 files changed

+114
-8
lines changed

workspaces/arborist/lib/arborist/build-ideal-tree.js

+41-8
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { lstat, readlink } = require('node:fs/promises')
1313
const { depth } = require('treeverse')
1414
const { log, time } = require('proc-log')
1515
const { redact } = require('@npmcli/redact')
16+
const semver = require('semver')
1617

1718
const {
1819
OK,
@@ -279,14 +280,23 @@ module.exports = cls => class IdealTreeBuilder extends cls {
279280
// When updating all, we load the shrinkwrap, but don't bother
280281
// to build out the full virtual tree from it, since we'll be
281282
// reconstructing it anyway.
282-
.then(root => this.options.global ? root
283-
: !this[_usePackageLock] || this[_updateAll]
284-
? Shrinkwrap.reset({
285-
path: this.path,
286-
lockfileVersion: this.options.lockfileVersion,
287-
resolveOptions: this.options,
288-
}).then(meta => Object.assign(root, { meta }))
289-
: this.loadVirtual({ root }))
283+
.then(root => {
284+
if (this.options.global) {
285+
return root
286+
} else if (!this[_usePackageLock] || this[_updateAll]) {
287+
return Shrinkwrap.reset({
288+
path: this.path,
289+
lockfileVersion: this.options.lockfileVersion,
290+
resolveOptions: this.options,
291+
}).then(meta => Object.assign(root, { meta }))
292+
} else {
293+
return this.loadVirtual({ root })
294+
.then(tree => {
295+
this.#applyRootOverridesToWorkspaces(tree)
296+
return tree
297+
})
298+
}
299+
})
290300

291301
// if we don't have a lockfile to go from, then start with the
292302
// actual tree, so we only make the minimum required changes.
@@ -1475,6 +1485,29 @@ This is a one-time fix-up, please be patient...
14751485
timeEnd()
14761486
}
14771487

1488+
#applyRootOverridesToWorkspaces (tree) {
1489+
const rootOverrides = tree.root?.package?.overrides || {}
1490+
1491+
for (const node of tree.root.inventory.values()) {
1492+
if (!node.isWorkspace) {
1493+
continue
1494+
}
1495+
1496+
for (const [depName] of Object.entries(rootOverrides)) {
1497+
const edge = node.edgesOut.get(depName)
1498+
const rootNode = tree.root.children.get(depName)
1499+
1500+
if (edge && rootNode && rootNode.package && edge.spec) {
1501+
const resolvedRootVersion = rootNode.package.version
1502+
if (!semver.satisfies(resolvedRootVersion, edge.spec)) {
1503+
edge.detach()
1504+
node.children.delete(depName)
1505+
}
1506+
}
1507+
}
1508+
}
1509+
}
1510+
14781511
#idealTreePrune () {
14791512
for (const node of this.idealTree.inventory.values()) {
14801513
if (node.extraneous) {

workspaces/arborist/test/arborist/build-ideal-tree.js

+73
Original file line numberDiff line numberDiff line change
@@ -3984,6 +3984,79 @@ t.test('overrides', async t => {
39843984
t.equal(fooBarEdge.valid, true)
39853985
t.equal(fooBarEdge.to.version, '2.0.0')
39863986
})
3987+
3988+
t.test('root overrides should be respected by workspaces on subsequent installs', async t => {
3989+
// • The root package.json declares a workspaces field, a direct dependency on "abbrev" with version constraint "^1.1.1", and an overrides field for "abbrev" also "^1.1.1".
3990+
// • The workspace "onepackage" depends on "abbrev" at "1.0.3".
3991+
const rootPkg = {
3992+
name: 'root',
3993+
version: '1.0.0',
3994+
workspaces: ['onepackage'],
3995+
dependencies: {
3996+
abbrev: '^1.1.1',
3997+
},
3998+
overrides: {
3999+
abbrev: '^1.1.1',
4000+
},
4001+
}
4002+
const workspacePkg = {
4003+
name: 'onepackage',
4004+
version: '1.0.0',
4005+
dependencies: {
4006+
abbrev: '1.0.3',
4007+
},
4008+
}
4009+
4010+
createRegistry(t, true)
4011+
4012+
const dir = t.testdir({
4013+
'package.json': JSON.stringify(rootPkg, null, 2),
4014+
onepackage: {
4015+
'package.json': JSON.stringify(workspacePkg, null, 2),
4016+
},
4017+
})
4018+
4019+
// fresh install
4020+
const tree1 = await buildIdeal(dir)
4021+
4022+
// The ideal tree should resolve "abbrev" at the root to 1.1.1.
4023+
t.equal(
4024+
tree1.children.get('abbrev').package.version,
4025+
'1.1.1',
4026+
'first install: root "abbrev" is forced to version 1.1.1'
4027+
)
4028+
// The workspace "onepackage" should not have its own nested "abbrev".
4029+
const onepackageNode1 = tree1.children.get('onepackage').target
4030+
t.notOk(
4031+
onepackageNode1.children.has('abbrev'),
4032+
'first install: workspace does not install "abbrev" separately'
4033+
)
4034+
4035+
// Write out the package-lock.json to disk to mimic a real install.
4036+
await tree1.meta.save()
4037+
4038+
// Simulate re-running install (which reads the package-lock).
4039+
const tree2 = await buildIdeal(dir)
4040+
4041+
// tree2 should NOT have its own abbrev dependency.
4042+
const onepackageNode2 = tree2.children.get('onepackage').target
4043+
t.notOk(
4044+
onepackageNode2.children.has('abbrev'),
4045+
'workspace should NOT have nested "abbrev" after subsequent install'
4046+
)
4047+
4048+
// The root "abbrev" should still be 1.1.1.
4049+
t.equal(
4050+
tree2.children.get('abbrev').package.version,
4051+
'1.1.1',
4052+
'second install: root "abbrev" is still forced to version 1.1.1')
4053+
4054+
// Overrides should NOT persist unnecessarily
4055+
t.notOk(
4056+
onepackageNode2.overrides && onepackageNode2.overrides.has('abbrev'),
4057+
'workspace node should not unnecessarily retain overrides after subsequent install'
4058+
)
4059+
})
39874060
})
39884061

39894062
t.test('store files with a custom indenting', async t => {

0 commit comments

Comments
 (0)