Skip to content

Commit f6c868d

Browse files
fix: calculate omit in diff (#8566)
Discovered while investigating #8535 `npm install --omit` output doesn't show any removed packages. This PR moves the omit calc into the diff calc so omits are handled like other resolution logic. Improvements: * we see removals in CLI output * _createSparseTree no longer creates dirs that will only be cleaned later * no duplicate filterSet calculation, omit is calculated right after filters I removed the trashList check on reifying node. Code coverage complained that this branch wasn't hit in any tests so I assume it should always be empty. I think this uncovered one bug with workspaces in the test `workspaces > reify workspaces omit dev dependencies > workspaces only`. From my understanding of the [docs](https://docs.npmjs.com/cli/v11/commands/npm-install#include-workspace-root), only workspace `a` should be touched, `root` and workspace `b` should still have their packages. I updated the test. Co-authored-by: Liam Mitchell <[email protected]>
1 parent 7949cff commit f6c868d

File tree

5 files changed

+63
-46
lines changed

5 files changed

+63
-46
lines changed

workspaces/arborist/lib/arborist/reify.js

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ const { defaultLockfileVersion } = Shrinkwrap
4242
const _retireShallowNodes = Symbol.for('retireShallowNodes')
4343
const _loadBundlesAndUpdateTrees = Symbol.for('loadBundlesAndUpdateTrees')
4444
const _submitQuickAudit = Symbol('submitQuickAudit')
45-
const _addOmitsToTrashList = Symbol('addOmitsToTrashList')
4645
const _unpackNewModules = Symbol.for('unpackNewModules')
4746
const _build = Symbol.for('build')
4847

@@ -85,6 +84,7 @@ module.exports = cls => class Reifier extends cls {
8584
#dryRun
8685
#nmValidated = new Set()
8786
#omit
87+
#omitted
8888
#retiredPaths = {}
8989
#retiredUnchanged = {}
9090
#savePrefix
@@ -109,6 +109,7 @@ module.exports = cls => class Reifier extends cls {
109109
}
110110

111111
this.#omit = new Set(options.omit)
112+
this.#omitted = new Set()
112113

113114
// start tracker block
114115
this.addTracker('reify')
@@ -141,6 +142,10 @@ module.exports = cls => class Reifier extends cls {
141142
this.idealTree = oldTree
142143
}
143144
await this[_saveIdealTree](options)
145+
// clean omitted
146+
for (const node of this.#omitted) {
147+
node.parent = null
148+
}
144149
// clean up any trash that is still in the tree
145150
for (const path of this[_trashList]) {
146151
const loc = relpath(this.idealTree.realpath, path)
@@ -315,7 +320,6 @@ module.exports = cls => class Reifier extends cls {
315320
]],
316321
[_rollbackCreateSparseTree, [
317322
_createSparseTree,
318-
_addOmitsToTrashList,
319323
_loadShrinkwrapsAndUpdateTrees,
320324
_loadBundlesAndUpdateTrees,
321325
_submitQuickAudit,
@@ -470,6 +474,8 @@ module.exports = cls => class Reifier extends cls {
470474
// find all the nodes that need to change between the actual
471475
// and ideal trees.
472476
this.diff = Diff.calculate({
477+
omit: this.#omit,
478+
omitted: this.#omitted,
473479
shrinkwrapInflated: this.#shrinkwrapInflated,
474480
filterNodes,
475481
actual: this.actualTree,
@@ -554,37 +560,6 @@ module.exports = cls => class Reifier extends cls {
554560
})
555561
}
556562

557-
// adding to the trash list will skip reifying, and delete them
558-
// if they are currently in the tree and otherwise untouched.
559-
[_addOmitsToTrashList] () {
560-
if (!this.#omit.size) {
561-
return
562-
}
563-
564-
const timeEnd = time.start('reify:trashOmits')
565-
for (const node of this.idealTree.inventory.values()) {
566-
const { top } = node
567-
568-
// if the top is not the root or workspace then we do not want to omit it
569-
if (!top.isProjectRoot && !top.isWorkspace) {
570-
continue
571-
}
572-
573-
// if a diff filter has been created, then we do not omit the node if the
574-
// top node is not in that set
575-
if (this.diff?.filterSet?.size && !this.diff.filterSet.has(top)) {
576-
continue
577-
}
578-
579-
// omit node if the dep type matches any omit flags that were set
580-
if (node.shouldOmit(this.#omit)) {
581-
this[_addNodeToTrashList](node)
582-
}
583-
}
584-
585-
timeEnd()
586-
}
587-
588563
[_createSparseTree] () {
589564
const timeEnd = time.start('reify:createSparse')
590565
// if we call this fn again, we look for the previous list
@@ -683,23 +658,17 @@ module.exports = cls => class Reifier extends cls {
683658
// reload the diff and sparse tree because the ideal tree changed
684659
.then(() => this[_diffTrees]())
685660
.then(() => this[_createSparseTree]())
686-
.then(() => this[_addOmitsToTrashList]())
687661
.then(() => this[_loadShrinkwrapsAndUpdateTrees]())
688662
.then(timeEnd)
689663
}
690664

691665
// create a symlink for Links, extract for Nodes
692666
// return the node object, since we usually want that
693667
// handle optional dep failures here
694-
// If node is in trash list, skip it
695668
// If reifying fails, and the node is optional, add it and its optionalSet
696669
// to the trash list
697670
// Always return the node.
698671
[_reifyNode] (node) {
699-
if (this[_trashList].has(node.path)) {
700-
return node
701-
}
702-
703672
const timeEnd = time.start(`reifyNode:${node.location}`)
704673
this.addTracker('reify', node.name, node.location)
705674

workspaces/arborist/lib/diff.js

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

1313
class Diff {
14-
constructor ({ actual, ideal, filterSet, shrinkwrapInflated }) {
14+
constructor ({ actual, ideal, filterSet, shrinkwrapInflated, omit, omitted }) {
15+
this.omit = omit
16+
this.omitted = omitted
1517
this.filterSet = filterSet
1618
this.shrinkwrapInflated = shrinkwrapInflated
1719
this.children = []
@@ -36,6 +38,8 @@ class Diff {
3638
ideal,
3739
filterNodes = [],
3840
shrinkwrapInflated = new Set(),
41+
omit = new Set(),
42+
omitted = new Set(),
3943
}) {
4044
// if there's a filterNode, then:
4145
// - get the path from the root to the filterNode. The root or
@@ -94,18 +98,28 @@ class Diff {
9498
}
9599

96100
return depth({
97-
tree: new Diff({ actual, ideal, filterSet, shrinkwrapInflated }),
101+
tree: new Diff({ actual, ideal, filterSet, shrinkwrapInflated, omit, omitted }),
98102
getChildren,
99103
leave,
100104
})
101105
}
102106
}
103107

104-
const getAction = ({ actual, ideal }) => {
108+
const getAction = ({ actual, ideal, omit, omitted }) => {
105109
if (!ideal) {
106110
return 'REMOVE'
107111
}
108112

113+
if (ideal.shouldOmit?.(omit)) {
114+
omitted.add(ideal)
115+
116+
if (actual) {
117+
return 'REMOVE'
118+
}
119+
120+
return null
121+
}
122+
109123
// bundled meta-deps are copied over to the ideal tree when we visit it,
110124
// so they'll appear to be missing here. There's no need to handle them
111125
// in the diff, though, because they'll be replaced at reify time anyway
@@ -184,6 +198,8 @@ const getChildren = diff => {
184198
removed,
185199
filterSet,
186200
shrinkwrapInflated,
201+
omit,
202+
omitted,
187203
} = diff
188204

189205
// Note: we DON'T diff fsChildren themselves, because they are either
@@ -214,6 +230,8 @@ const getChildren = diff => {
214230
removed,
215231
filterSet,
216232
shrinkwrapInflated,
233+
omit,
234+
omitted,
217235
})
218236
}
219237

@@ -232,20 +250,22 @@ const diffNode = ({
232250
removed,
233251
filterSet,
234252
shrinkwrapInflated,
253+
omit,
254+
omitted,
235255
}) => {
236256
if (filterSet.size && !(filterSet.has(ideal) || filterSet.has(actual))) {
237257
return
238258
}
239259

240-
const action = getAction({ actual, ideal })
260+
const action = getAction({ actual, ideal, omit, omitted })
241261

242262
// if it's a match, then get its children
243263
// otherwise, this is the child diff node
244264
if (action || (!shrinkwrapInflated.has(ideal) && ideal.hasShrinkwrap)) {
245265
if (action === 'REMOVE') {
246266
removed.push(actual)
247267
}
248-
children.push(new Diff({ actual, ideal, filterSet, shrinkwrapInflated }))
268+
children.push(new Diff({ actual, ideal, filterSet, shrinkwrapInflated, omit, omitted }))
249269
} else {
250270
unchanged.push(ideal)
251271
// !*! Weird dirty hack warning !*!
@@ -285,6 +305,8 @@ const diffNode = ({
285305
removed,
286306
filterSet,
287307
shrinkwrapInflated,
308+
omit,
309+
omitted,
288310
}))
289311
}
290312
}

workspaces/arborist/lib/node.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,18 @@ class Node {
490490
}
491491

492492
shouldOmit (omitSet) {
493+
if (!omitSet.size) {
494+
return false
495+
}
496+
497+
const { top } = this
498+
499+
// if the top is not the root or workspace then we do not want to omit it
500+
if (!top.isProjectRoot && !top.isWorkspace) {
501+
return false
502+
}
503+
504+
// omit node if the dep type matches any omit flags that were set
493505
return (
494506
this.peer && omitSet.has('peer') ||
495507
this.dev && omitSet.has('dev') ||

workspaces/arborist/tap-snapshots/test/arborist/reify.js.test.cjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17365,7 +17365,6 @@ Array [
1736517365
"reify:retireShallow",
1736617366
"reify:save",
1736717367
"reify:trash",
17368-
"reify:trashOmits",
1736917368
"reify:unpack",
1737017369
"reify:unretire",
1737117370
"reifyNode:node_modules/@isaacs/testing-peer-deps-b",

workspaces/arborist/test/arborist/reify.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,21 @@ t.test('dev, optional, devOptional flags and omissions', t => {
394394
}))
395395
})
396396

397+
t.test('omit reports no diff on second run', async t => {
398+
const path = fixture(t, 'testing-dev-optional-flags')
399+
createRegistry(t, true)
400+
const arb = newArb({ path })
401+
await arb.reify({ omit: ['dev'] })
402+
t.equal(arb.actualTree.children.get('once'), undefined, 'no once in tree')
403+
t.ok(arb.diff.children.length, 'first reify has changes')
404+
await arb.reify({ omit: ['dev'] })
405+
t.equal(arb.actualTree.children.get('once'), undefined, 'no once in tree')
406+
t.notOk(arb.diff.children.length, 'second reify has no changes')
407+
await arb.reify({})
408+
t.ok(arb.actualTree.children.get('once'), 'once in tree')
409+
t.ok(arb.diff.children.length, 'removing omit has changes')
410+
})
411+
397412
t.test('omits when both dev and optional flags are set', t => {
398413
const path = 'testing-dev-optional-flags-2'
399414
const omits = [['dev'], ['optional']]
@@ -1329,7 +1344,7 @@ t.test('workspaces', async t => {
13291344
await t.test('workspaces only', async t => {
13301345
createRegistry(t, false)
13311346
const { root, a, b } = await runCase(t, { workspaces: ['a'] })
1332-
t.equal(root.exists(), false, 'root')
1347+
t.equal(root.exists(), true, 'root')
13331348
t.equal(a.exists(), false, 'a')
13341349
t.equal(b.exists(), true, 'b')
13351350
})

0 commit comments

Comments
 (0)