Skip to content

Commit 54fd27f

Browse files
fix: refactor node.ideallyInert to node.inert (#8602)
Discovered while investigating #8535 (comment) Similar to #8566, relates to #8184 Moves `inert` (uninstallable optional) calculation into `buildIdealTree` so it can be used in diff. Also removes most of #8184 in favor of a [simpler fix](https://github.com/npm/cli/pull/8602/files#diff-02626074e1a4a170693607e4a3a69dfc08ee52067734717833b22cf162923e07R354), see PR comments for the journey. Improvements: * we don't see uninstallable packages as "installed" in CLI output * `createSparseTree` no longer creates dirs that will only be cleaned later For the example in the linked issue, it changes output from `added 156 packages` to `added 12 packages` and combined with #8537 it changes to `added 6 packages`, the expected result.
1 parent c3e1790 commit 54fd27f

File tree

20 files changed

+383
-1796
lines changed

20 files changed

+383
-1796
lines changed

package-lock.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5249,6 +5249,21 @@
52495249
"dev": true,
52505250
"license": "ISC"
52515251
},
5252+
"node_modules/fsevents": {
5253+
"version": "2.3.3",
5254+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
5255+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
5256+
"dev": true,
5257+
"hasInstallScript": true,
5258+
"license": "MIT",
5259+
"optional": true,
5260+
"os": [
5261+
"darwin"
5262+
],
5263+
"engines": {
5264+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
5265+
}
5266+
},
52525267
"node_modules/function-bind": {
52535268
"version": "1.1.2",
52545269
"dev": true,

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
192192
}
193193

194194
async #checkEngineAndPlatform () {
195-
const { engineStrict, npmVersion, nodeVersion, omit = [] } = this.options
195+
const { engineStrict, npmVersion, nodeVersion, omit = [], cpu, os, libc } = this.options
196196
const omitSet = new Set(omit)
197197

198198
for (const node of this.idealTree.inventory.values()) {
@@ -214,6 +214,19 @@ module.exports = cls => class IdealTreeBuilder extends cls {
214214
}
215215
checkPlatform(node.package, this.options.force)
216216
}
217+
if (node.optional && !node.inert) {
218+
// Mark any optional packages we can't install as inert.
219+
// We ignore the --force and --engine-strict flags.
220+
try {
221+
checkEngine(node.package, npmVersion, nodeVersion, false)
222+
checkPlatform(node.package, false, { cpu, os, libc })
223+
} catch (error) {
224+
const set = optionalSet(node)
225+
for (const node of set) {
226+
node.inert = true
227+
}
228+
}
229+
}
217230
}
218231
}
219232

@@ -324,7 +337,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
324337
})
325338

326339
.then(tree => {
327-
// search the virtual tree for invalid edges, if any are found add their source to
340+
// search the virtual tree for missing/invalid edges, if any are found add their source to
328341
// the depsQueue so that we'll fix it later
329342
depth({
330343
tree,
@@ -338,7 +351,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
338351
filter: node => node,
339352
visit: node => {
340353
for (const edge of node.edgesOut.values()) {
341-
if (!edge.valid) {
354+
if (!edge.to || !edge.valid) {
342355
this.#depsQueue.push(node)
343356
break // no need to continue the loop after the first hit
344357
}
@@ -811,7 +824,7 @@ This is a one-time fix-up, please be patient...
811824
node !== this.idealTree &&
812825
node.resolved &&
813826
(hasBundle || hasShrinkwrap) &&
814-
!node.ideallyInert
827+
!node.inert
815828
if (crackOpen) {
816829
const Arborist = this.constructor
817830
const opt = { ...this.options }
@@ -1561,7 +1574,7 @@ This is a one-time fix-up, please be patient...
15611574

15621575
const set = optionalSet(node)
15631576
for (const node of set) {
1564-
node.ideallyInert = true
1577+
node.inert = true
15651578
}
15661579
}
15671580
}
@@ -1582,7 +1595,7 @@ This is a one-time fix-up, please be patient...
15821595
node.parent !== null
15831596
&& !node.isProjectRoot
15841597
&& !excludeNodes.has(node)
1585-
&& !node.ideallyInert
1598+
&& !node.inert
15861599
) {
15871600
this[_addNodeToTrashList](node)
15881601
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ module.exports = cls => class IsolatedReifier extends cls {
8181
}
8282
queue.push(e.to)
8383
})
84-
if (!next.isProjectRoot && !next.isWorkspace && !next.ideallyInert) {
84+
if (!next.isProjectRoot && !next.isWorkspace && !next.inert) {
8585
root.external.push(await this.externalProxyMemo(next))
8686
}
8787
}
@@ -147,8 +147,8 @@ module.exports = cls => class IsolatedReifier extends cls {
147147
const nonOptionalDeps = edges.filter(e => !e.optional).map(e => e.to.target)
148148

149149
result.localDependencies = await Promise.all(nonOptionalDeps.filter(n => n.isWorkspace).map(this.workspaceProxyMemo))
150-
result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !n.isWorkspace && !n.ideallyInert).map(this.externalProxyMemo))
151-
result.externalOptionalDependencies = await Promise.all(optionalDeps.filter(n => !n.ideallyInert).map(this.externalProxyMemo))
150+
result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !n.isWorkspace && !n.inert).map(this.externalProxyMemo))
151+
result.externalOptionalDependencies = await Promise.all(optionalDeps.filter(n => !n.inert).map(this.externalProxyMemo))
152152
result.dependencies = [
153153
...result.externalDependencies,
154154
...result.localDependencies,

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,6 @@ To fix:
269269
integrity: sw.integrity,
270270
resolved: consistentResolve(sw.resolved, this.path, path),
271271
pkg: sw,
272-
ideallyInert: sw.ideallyInert,
273272
hasShrinkwrap: sw.hasShrinkwrap,
274273
dev,
275274
optional,

workspaces/arborist/lib/arborist/reify.js

Lines changed: 9 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const pacote = require('pacote')
77
const promiseAllRejectLate = require('promise-all-reject-late')
88
const runScript = require('@npmcli/run-script')
99
const { callLimit: promiseCallLimit } = require('promise-call-limit')
10-
const { checkEngine, checkPlatform } = require('npm-install-checks')
1110
const { depth: dfwalk } = require('treeverse')
1211
const { dirname, resolve, relative, join } = require('node:path')
1312
const { log, time } = require('proc-log')
@@ -74,7 +73,6 @@ module.exports = cls => class Reifier extends cls {
7473
#dryRun
7574
#nmValidated = new Set()
7675
#omit
77-
#omitted
7876
#retiredPaths = {}
7977
#retiredUnchanged = {}
8078
#savePrefix
@@ -99,7 +97,6 @@ module.exports = cls => class Reifier extends cls {
9997
}
10098

10199
this.#omit = new Set(options.omit)
102-
this.#omitted = new Set()
103100

104101
// start tracker block
105102
this.addTracker('reify')
@@ -132,15 +129,17 @@ module.exports = cls => class Reifier extends cls {
132129
this.idealTree = oldTree
133130
}
134131
await this[_saveIdealTree](options)
135-
// clean omitted
136-
for (const node of this.#omitted) {
137-
node.parent = null
132+
// clean inert
133+
for (const node of this.idealTree.inventory.values()) {
134+
if (node.inert) {
135+
node.parent = null
136+
}
138137
}
139138
// clean up any trash that is still in the tree
140139
for (const path of this[_trashList]) {
141140
const loc = relpath(this.idealTree.realpath, path)
142141
const node = this.idealTree.inventory.get(loc)
143-
if (node && node.root === this.idealTree && !node.ideallyInert) {
142+
if (node && node.root === this.idealTree) {
144143
node.parent = null
145144
}
146145
}
@@ -228,18 +227,6 @@ module.exports = cls => class Reifier extends cls {
228227
this.idealTree.meta.hiddenLockfile = true
229228
this.idealTree.meta.lockfileVersion = defaultLockfileVersion
230229

231-
// Preserve inertness for failed stuff.
232-
if (this.actualTree) {
233-
for (const [loc, actual] of this.actualTree.inventory.entries()) {
234-
if (actual.ideallyInert) {
235-
const ideal = this.idealTree.inventory.get(loc)
236-
if (ideal) {
237-
ideal.ideallyInert = true
238-
}
239-
}
240-
}
241-
}
242-
243230
this.actualTree = this.idealTree
244231
this.idealTree = null
245232

@@ -465,7 +452,6 @@ module.exports = cls => class Reifier extends cls {
465452
// and ideal trees.
466453
this.diff = Diff.calculate({
467454
omit: this.#omit,
468-
omitted: this.#omitted,
469455
shrinkwrapInflated: this.#shrinkwrapInflated,
470456
filterNodes,
471457
actual: this.actualTree,
@@ -566,9 +552,6 @@ module.exports = cls => class Reifier extends cls {
566552
// retire the same path at the same time.
567553
const dirsChecked = new Set()
568554
return promiseAllRejectLate(leaves.map(async node => {
569-
if (node.ideallyInert) {
570-
return
571-
}
572555
for (const d of walkUp(node.path)) {
573556
if (d === node.top.path) {
574557
break
@@ -662,18 +645,7 @@ module.exports = cls => class Reifier extends cls {
662645
const timeEnd = time.start(`reifyNode:${node.location}`)
663646
this.addTracker('reify', node.name, node.location)
664647

665-
const { npmVersion, nodeVersion, cpu, os, libc } = this.options
666648
const p = Promise.resolve().then(async () => {
667-
// when we reify an optional node, check the engine and platform
668-
// first. be sure to ignore the --force and --engine-strict flags,
669-
// since we always want to skip any optional packages we can't install.
670-
// these checks throwing will result in a rollback and removal
671-
// of the mismatches
672-
// eslint-disable-next-line promise/always-return
673-
if (node.optional) {
674-
checkEngine(node.package, npmVersion, nodeVersion, false)
675-
checkPlatform(node.package, false, { cpu, os, libc })
676-
}
677649
await this[_checkBins](node)
678650
await this.#extractOrLink(node)
679651
const { _id, deprecated } = node.package
@@ -707,10 +679,6 @@ module.exports = cls => class Reifier extends cls {
707679
}
708680

709681
async #extractOrLink (node) {
710-
if (node.ideallyInert) {
711-
return
712-
}
713-
714682
const nm = resolve(node.parent.path, 'node_modules')
715683
await this.#validateNodeModules(nm)
716684

@@ -791,9 +759,9 @@ module.exports = cls => class Reifier extends cls {
791759
[_handleOptionalFailure] (node, p) {
792760
return (node.optional ? p.catch(() => {
793761
const set = optionalSet(node)
794-
for (node of set) {
762+
for (const node of set) {
795763
log.verbose('reify', 'failed optional dependency', node.path)
796-
node.ideallyInert = true
764+
node.inert = true
797765
this[_addNodeToTrashList](node)
798766
}
799767
}) : p).then(() => node)
@@ -1165,9 +1133,6 @@ module.exports = cls => class Reifier extends cls {
11651133

11661134
this.#retiredUnchanged[retireFolder] = []
11671135
return promiseAllRejectLate(diff.unchanged.map(node => {
1168-
if (node.ideallyInert) {
1169-
return
1170-
}
11711136
// no need to roll back links, since we'll just delete them anyway
11721137
if (node.isLink) {
11731138
return mkdir(dirname(node.path), { recursive: true, force: true })
@@ -1247,7 +1212,7 @@ module.exports = cls => class Reifier extends cls {
12471212
// skip links that only live within node_modules as they are most
12481213
// likely managed by packages we installed, we only want to rebuild
12491214
// unchanged links we directly manage
1250-
const linkedFromRoot = (node.parent === tree && !node.ideallyInert) || node.target.fsTop === tree
1215+
const linkedFromRoot = (node.parent === tree && !node.inert) || node.target.fsTop === tree
12511216
if (node.isLink && linkedFromRoot) {
12521217
nodes.push(node)
12531218
}

workspaces/arborist/lib/diff.js

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ const { existsSync } = require('node:fs')
1111
const ssri = require('ssri')
1212

1313
class Diff {
14-
constructor ({ actual, ideal, filterSet, shrinkwrapInflated, omit, omitted }) {
14+
constructor ({ actual, ideal, filterSet, shrinkwrapInflated, omit }) {
1515
this.omit = omit
16-
this.omitted = omitted
1716
this.filterSet = filterSet
1817
this.shrinkwrapInflated = shrinkwrapInflated
1918
this.children = []
@@ -39,7 +38,6 @@ class Diff {
3938
filterNodes = [],
4039
shrinkwrapInflated = new Set(),
4140
omit = new Set(),
42-
omitted = new Set(),
4341
}) {
4442
// if there's a filterNode, then:
4543
// - get the path from the root to the filterNode. The root or
@@ -98,28 +96,18 @@ class Diff {
9896
}
9997

10098
return depth({
101-
tree: new Diff({ actual, ideal, filterSet, shrinkwrapInflated, omit, omitted }),
99+
tree: new Diff({ actual, ideal, filterSet, shrinkwrapInflated, omit }),
102100
getChildren,
103101
leave,
104102
})
105103
}
106104
}
107105

108-
const getAction = ({ actual, ideal, omit, omitted }) => {
106+
const getAction = ({ actual, ideal }) => {
109107
if (!ideal) {
110108
return 'REMOVE'
111109
}
112110

113-
if (ideal.shouldOmit?.(omit)) {
114-
omitted.add(ideal)
115-
116-
if (actual) {
117-
return 'REMOVE'
118-
}
119-
120-
return null
121-
}
122-
123111
// bundled meta-deps are copied over to the ideal tree when we visit it,
124112
// so they'll appear to be missing here. There's no need to handle them
125113
// in the diff, though, because they'll be replaced at reify time anyway
@@ -199,7 +187,6 @@ const getChildren = diff => {
199187
filterSet,
200188
shrinkwrapInflated,
201189
omit,
202-
omitted,
203190
} = diff
204191

205192
// Note: we DON'T diff fsChildren themselves, because they are either
@@ -231,7 +218,6 @@ const getChildren = diff => {
231218
filterSet,
232219
shrinkwrapInflated,
233220
omit,
234-
omitted,
235221
})
236222
}
237223

@@ -251,21 +237,32 @@ const diffNode = ({
251237
filterSet,
252238
shrinkwrapInflated,
253239
omit,
254-
omitted,
255240
}) => {
256241
if (filterSet.size && !(filterSet.has(ideal) || filterSet.has(actual))) {
257242
return
258243
}
259244

260-
const action = getAction({ actual, ideal, omit, omitted })
245+
if (ideal?.shouldOmit?.(omit)) {
246+
ideal.inert = true
247+
}
248+
249+
// Treat inert nodes as undefined for the purposes of diffing.
250+
if (ideal?.inert) {
251+
ideal = undefined
252+
}
253+
if (!actual && !ideal) {
254+
return
255+
}
256+
257+
const action = getAction({ actual, ideal })
261258

262259
// if it's a match, then get its children
263260
// otherwise, this is the child diff node
264261
if (action || (!shrinkwrapInflated.has(ideal) && ideal.hasShrinkwrap)) {
265262
if (action === 'REMOVE') {
266263
removed.push(actual)
267264
}
268-
children.push(new Diff({ actual, ideal, filterSet, shrinkwrapInflated, omit, omitted }))
265+
children.push(new Diff({ actual, ideal, filterSet, shrinkwrapInflated, omit }))
269266
} else {
270267
unchanged.push(ideal)
271268
// !*! Weird dirty hack warning !*!
@@ -306,7 +303,6 @@ const diffNode = ({
306303
filterSet,
307304
shrinkwrapInflated,
308305
omit,
309-
omitted,
310306
}))
311307
}
312308
}

workspaces/arborist/lib/node.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class Node {
101101
global = false,
102102
dummy = false,
103103
sourceReference = null,
104-
ideallyInert = false,
104+
inert = false,
105105
} = options
106106
// this object gives querySelectorAll somewhere to stash context about a node
107107
// while processing a query
@@ -207,7 +207,7 @@ class Node {
207207
this.extraneous = false
208208
}
209209

210-
this.ideallyInert = ideallyInert
210+
this.inert = inert
211211

212212
this.edgesIn = new Set()
213213
this.edgesOut = new CaseInsensitiveMap()

0 commit comments

Comments
 (0)