Skip to content

Commit 20c5ef2

Browse files
committed
fix(arborist): omit failed optional dependencies from installed deps, but keep them 'in the tree'
Fixes: #4828 Fixes: #7961 Replaces: #8127 Replaces: #8077 When optional dependencies fail, we don't want to install them, for whatever reason. The way this "uninstallation" is done right now is by simply removing the dependencies from the ideal tree during reification. Unfortunately, this means that what gets saved to the "hidden lockfile" is the edited ideal tree as well, and when NPM runs next, it'll use that when regenerating the "real" package-lock. This PR tags failed optional deps such that they're still added to the "trash list" (and thus have their directories cleaned up by the reifier), but prevents Arborist from removing them altogether from the ideal tree (which is usually done by setting their parent to `null`, making them unreachable, and letting them get GC'd).
1 parent 8b7bb12 commit 20c5ef2

File tree

6 files changed

+53
-6
lines changed

6 files changed

+53
-6
lines changed

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,8 @@ This is a one-time fix-up, please be patient...
809809
const crackOpen = this.#complete &&
810810
node !== this.idealTree &&
811811
node.resolved &&
812-
(hasBundle || hasShrinkwrap)
812+
(hasBundle || hasShrinkwrap) &&
813+
!node.ideallyInert
813814
if (crackOpen) {
814815
const Arborist = this.constructor
815816
const opt = { ...this.options }
@@ -1527,7 +1528,7 @@ This is a one-time fix-up, please be patient...
15271528

15281529
const set = optionalSet(node)
15291530
for (const node of set) {
1530-
node.parent = null
1531+
node.ideallyInert = true
15311532
}
15321533
}
15331534
}
@@ -1548,6 +1549,7 @@ This is a one-time fix-up, please be patient...
15481549
node.parent !== null
15491550
&& !node.isProjectRoot
15501551
&& !excludeNodes.has(node)
1552+
&& !node.ideallyInert
15511553
) {
15521554
this[_addNodeToTrashList](node)
15531555
}

workspaces/arborist/lib/arborist/isolated-reifier.js

+3
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ module.exports = cls => class IsolatedReifier extends cls {
100100
}
101101

102102
async externalProxy (result, node) {
103+
if (node.ideallyInert) {
104+
return
105+
}
103106
await this.assignCommonProperties(node, result)
104107
if (node.hasShrinkwrap) {
105108
const dir = join(

workspaces/arborist/lib/arborist/load-virtual.js

+1
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ module.exports = cls => class VirtualLoader extends cls {
267267
integrity: sw.integrity,
268268
resolved: consistentResolve(sw.resolved, this.path, path),
269269
pkg: sw,
270+
ideallyInert: sw.ideallyInert,
270271
hasShrinkwrap: sw.hasShrinkwrap,
271272
dev,
272273
optional,

workspaces/arborist/lib/arborist/reify.js

+25-2
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ module.exports = cls => class Reifier extends cls {
150150
for (const path of this[_trashList]) {
151151
const loc = relpath(this.idealTree.realpath, path)
152152
const node = this.idealTree.inventory.get(loc)
153-
if (node && node.root === this.idealTree) {
153+
if (node && node.root === this.idealTree && !node.ideallyInert) {
154154
node.parent = null
155155
}
156156
}
@@ -238,6 +238,18 @@ module.exports = cls => class Reifier extends cls {
238238
this.idealTree.meta.hiddenLockfile = true
239239
this.idealTree.meta.lockfileVersion = defaultLockfileVersion
240240

241+
// Preserve inertness for failed stuff.
242+
if (this.actualTree) {
243+
for (const [loc, actual] of this.actualTree.inventory.entries()) {
244+
if (actual.ideallyInert) {
245+
const ideal = this.idealTree.inventory.get(loc)
246+
if (ideal) {
247+
ideal.ideallyInert = true
248+
}
249+
}
250+
}
251+
}
252+
241253
this.actualTree = this.idealTree
242254
this.idealTree = null
243255

@@ -600,6 +612,9 @@ module.exports = cls => class Reifier extends cls {
600612
// retire the same path at the same time.
601613
const dirsChecked = new Set()
602614
return promiseAllRejectLate(leaves.map(async node => {
615+
if (node.ideallyInert) {
616+
return
617+
}
603618
for (const d of walkUp(node.path)) {
604619
if (d === node.top.path) {
605620
break
@@ -744,6 +759,10 @@ module.exports = cls => class Reifier extends cls {
744759
}
745760

746761
async #extractOrLink (node) {
762+
if (node.ideallyInert) {
763+
return
764+
}
765+
747766
const nm = resolve(node.parent.path, 'node_modules')
748767
await this.#validateNodeModules(nm)
749768

@@ -819,6 +838,7 @@ module.exports = cls => class Reifier extends cls {
819838
const set = optionalSet(node)
820839
for (node of set) {
821840
log.verbose('reify', 'failed optional dependency', node.path)
841+
node.ideallyInert = true
822842
this[_addNodeToTrashList](node)
823843
}
824844
}) : p).then(() => node)
@@ -1151,6 +1171,9 @@ module.exports = cls => class Reifier extends cls {
11511171

11521172
this.#retiredUnchanged[retireFolder] = []
11531173
return promiseAllRejectLate(diff.unchanged.map(node => {
1174+
if (node.ideallyInert) {
1175+
return
1176+
}
11541177
// no need to roll back links, since we'll just delete them anyway
11551178
if (node.isLink) {
11561179
return mkdir(dirname(node.path), { recursive: true, force: true })
@@ -1230,7 +1253,7 @@ module.exports = cls => class Reifier extends cls {
12301253
// skip links that only live within node_modules as they are most
12311254
// likely managed by packages we installed, we only want to rebuild
12321255
// unchanged links we directly manage
1233-
const linkedFromRoot = node.parent === tree || node.target.fsTop === tree
1256+
const linkedFromRoot = (node.parent === tree && !node.ideallyInert) || node.target.fsTop === tree
12341257
if (node.isLink && linkedFromRoot) {
12351258
nodes.push(node)
12361259
}

workspaces/arborist/lib/node.js

+3
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class Node {
103103
global = false,
104104
dummy = false,
105105
sourceReference = null,
106+
ideallyInert = false,
106107
} = options
107108
// this object gives querySelectorAll somewhere to stash context about a node
108109
// while processing a query
@@ -197,6 +198,8 @@ class Node {
197198
this.extraneous = false
198199
}
199200

201+
this.ideallyInert = ideallyInert
202+
200203
this.edgesIn = new Set()
201204
this.edgesOut = new CaseInsensitiveMap()
202205

workspaces/arborist/lib/shrinkwrap.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ const nodeMetaKeys = [
109109
'inBundle',
110110
'hasShrinkwrap',
111111
'hasInstallScript',
112+
'ideallyInert',
112113
]
113114

114115
const metaFieldFromPkg = (pkg, key) => {
@@ -135,11 +136,15 @@ const assertNoNewer = async (path, data, lockTime, dir, seen) => {
135136

136137
const parent = isParent ? dir : resolve(dir, 'node_modules')
137138
const rel = relpath(path, dir)
139+
const inert = data.packages[rel]?.ideallyInert
140+
if (inert) {
141+
return
142+
}
138143
seen.add(rel)
139144
let entries
140145
if (dir === path) {
141146
entries = [{ name: 'node_modules', isDirectory: () => true }]
142-
} else {
147+
} else if (!inert) {
143148
const { mtime: dirTime } = await stat(dir)
144149
if (dirTime > lockTime) {
145150
throw new Error(`out of date, updated: ${rel}`)
@@ -173,7 +178,7 @@ const assertNoNewer = async (path, data, lockTime, dir, seen) => {
173178

174179
// assert that all the entries in the lockfile were seen
175180
for (const loc in data.packages) {
176-
if (!seen.has(loc)) {
181+
if (!seen.has(loc) && !data.packages[loc].ideallyInert) {
177182
throw new Error(`missing from node_modules: ${loc}`)
178183
}
179184
}
@@ -783,6 +788,10 @@ class Shrinkwrap {
783788
// ok, I did my best! good luck!
784789
}
785790

791+
if (lock.ideallyInert) {
792+
meta.ideallyInert = true
793+
}
794+
786795
if (lock.bundled) {
787796
meta.inBundle = true
788797
}
@@ -953,6 +962,12 @@ class Shrinkwrap {
953962
this.#buildLegacyLockfile(this.tree, this.data)
954963
}
955964

965+
if (!this.hiddenLockfile) {
966+
for (const node of Object.values(this.data.packages)) {
967+
delete node.ideallyInert
968+
}
969+
}
970+
956971
// lf version 1 = dependencies only
957972
// lf version 2 = dependencies and packages
958973
// lf version 3 = packages only

0 commit comments

Comments
 (0)