@npmcli/arborist 8.0.2 → 8.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@ const pacote = require('pacote')
3
3
  const { join } = require('node:path')
4
4
  const { depth } = require('treeverse')
5
5
  const crypto = require('node:crypto')
6
+ const { IsolatedNode, IsolatedLink } = require('../isolated-classes.js')
6
7
 
7
8
  // generate short hash key based on the dependency tree starting at this node
8
9
  const getKey = (startNode) => {
@@ -33,43 +34,25 @@ const getKey = (startNode) => {
33
34
 
34
35
  module.exports = cls => class IsolatedReifier extends cls {
35
36
  #externalProxies = new Map()
37
+ #omit = new Set()
38
+ #rootDeclaredDeps = new Set()
36
39
  #processedEdges = new Set()
37
40
  #workspaceProxies = new Map()
38
41
 
39
- #generateChild (node, location, pkg, inStore, root) {
40
- const newChild = {
41
- binPaths: [],
42
- children: new Map(),
43
- edgesIn: new Set(),
44
- edgesOut: new Map(),
45
- fsChildren: new Set(),
46
- /* istanbul ignore next -- emulate Node */
47
- getBundler () {
48
- return null
49
- },
50
- global: false,
51
- globalTop: false,
52
- hasShrinkwrap: false,
53
- inDepBundle: false,
54
- integrity: null,
55
- isInStore: inStore,
56
- isLink: false,
57
- isProjectRoot: false,
58
- isRoot: false,
59
- isTop: false,
42
+ #generateChild (node, location, pkg, isInStore, root) {
43
+ const newChild = new IsolatedNode({
44
+ isInStore,
60
45
  location,
61
46
  name: node.packageName || node.name,
62
47
  optional: node.optional,
63
48
  package: pkg,
64
49
  parent: root,
65
- path: join(this.idealGraph.root.localPath, location),
66
- realpath: join(this.idealGraph.root.localPath, location),
50
+ path: join(this.idealGraph.localPath, location),
67
51
  resolved: node.resolved,
68
52
  root,
69
- top: { path: this.idealGraph.root.localPath },
70
- version: pkg.version,
71
- }
72
- newChild.target = newChild
53
+ })
54
+ // XXX top is from place-dep not lib/node.js
55
+ newChild.top = { path: this.idealGraph.localPath }
73
56
  root.children.set(newChild.location, newChild)
74
57
  root.inventory.set(newChild.location, newChild)
75
58
  }
@@ -90,16 +73,31 @@ module.exports = cls => class IsolatedReifier extends cls {
90
73
  **/
91
74
  async makeIdealGraph () {
92
75
  const idealTree = this.idealTree
93
-
76
+ this.#omit = new Set(this.options.omit)
77
+ const omit = this.#omit
78
+
79
+ // npm auto-creates 'workspace' edges from root to all workspaces.
80
+ // For isolated/linked mode, only include workspaces that root explicitly declares as dependencies.
81
+ // When omitting dep types, exclude those from the declared set so their workspaces aren't hoisted.
82
+ const rootPkg = idealTree.package
83
+ this.#rootDeclaredDeps = new Set(Object.keys(Object.assign({},
84
+ rootPkg.dependencies,
85
+ (!omit.has('dev') && rootPkg.devDependencies),
86
+ (!omit.has('optional') && rootPkg.optionalDependencies),
87
+ (!omit.has('peer') && rootPkg.peerDependencies)
88
+ )))
89
+
90
+ // XXX this sometimes acts like a node too
94
91
  this.idealGraph = {
95
92
  external: [],
96
93
  isProjectRoot: true,
97
94
  localLocation: idealTree.location,
98
95
  localPath: idealTree.path,
96
+ path: idealTree.path,
99
97
  }
100
98
  this.counter = 0
101
99
 
102
- this.idealGraph.workspaces = await Promise.all(Array.from(idealTree.fsChildren.values(), w => this.workspaceProxy(w)))
100
+ this.idealGraph.workspaces = await Promise.all(Array.from(idealTree.fsChildren.values(), w => this.#workspaceProxy(w)))
103
101
  const processed = new Set()
104
102
  const queue = [idealTree, ...idealTree.fsChildren]
105
103
  while (queue.length !== 0) {
@@ -109,43 +107,43 @@ module.exports = cls => class IsolatedReifier extends cls {
109
107
  }
110
108
  processed.add(next.location)
111
109
  next.edgesOut.forEach(edge => {
112
- if (edge.to && !(next.package.bundleDependencies || next.package.bundledDependencies || []).includes(edge.to.name)) {
110
+ if (edge.to && !(next.package.bundleDependencies || next.package.bundledDependencies || []).includes(edge.to.name) && !edge.to.shouldOmit?.(omit)) {
113
111
  queue.push(edge.to)
114
112
  }
115
113
  })
116
114
  // local `file:` deps are in fsChildren but are not workspaces.
117
115
  // they are already handled as workspace-like proxies above and should not go through the external/store extraction path.
118
116
  if (!next.isProjectRoot && !next.isWorkspace && !next.inert && !idealTree.fsChildren.has(next) && !idealTree.fsChildren.has(next.target)) {
119
- this.idealGraph.external.push(await this.externalProxy(next))
117
+ this.idealGraph.external.push(await this.#externalProxy(next))
120
118
  }
121
119
  }
122
120
 
123
- await this.assignCommonProperties(idealTree, this.idealGraph)
121
+ await this.#assignCommonProperties(idealTree, this.idealGraph)
124
122
  }
125
123
 
126
- async workspaceProxy (node) {
124
+ async #workspaceProxy (node) {
127
125
  if (this.#workspaceProxies.has(node)) {
128
126
  return this.#workspaceProxies.get(node)
129
127
  }
130
128
  const result = {}
131
- // XXX this goes recursive if we don't set here because assignCommonProperties also calls this.workspaceProxy
129
+ // XXX this goes recursive if we don't set here because assignCommonProperties also calls this.#workspaceProxy
132
130
  this.#workspaceProxies.set(node, result)
133
131
  result.localLocation = node.location
134
132
  result.localPath = node.path
135
133
  result.isWorkspace = true
136
134
  result.resolved = node.resolved
137
- await this.assignCommonProperties(node, result)
135
+ await this.#assignCommonProperties(node, result)
138
136
  return result
139
137
  }
140
138
 
141
- async externalProxy (node) {
139
+ async #externalProxy (node) {
142
140
  if (this.#externalProxies.has(node)) {
143
141
  return this.#externalProxies.get(node)
144
142
  }
145
143
  const result = {}
146
- // XXX this goes recursive if we don't set here because assignCommonProperties also calls this.externalProxy
144
+ // XXX this goes recursive if we don't set here because assignCommonProperties also calls this.#externalProxy
147
145
  this.#externalProxies.set(node, result)
148
- await this.assignCommonProperties(node, result, !node.hasShrinkwrap)
146
+ await this.#assignCommonProperties(node, result, !node.hasShrinkwrap)
149
147
  if (node.hasShrinkwrap) {
150
148
  const dir = join(
151
149
  node.root.path,
@@ -187,8 +185,9 @@ module.exports = cls => class IsolatedReifier extends cls {
187
185
  return result
188
186
  }
189
187
 
190
- async assignCommonProperties (node, result, populateDeps = true) {
188
+ async #assignCommonProperties (node, result, populateDeps = true) {
191
189
  result.root = this.idealGraph
190
+ // XXX does anything need this?
192
191
  result.id = this.counter++
193
192
  /* istanbul ignore next - packageName is always set for real packages */
194
193
  result.name = result.isWorkspace ? (node.packageName || node.name) : node.name
@@ -196,17 +195,39 @@ module.exports = cls => class IsolatedReifier extends cls {
196
195
  result.packageName = node.packageName || node.name
197
196
  result.package = { ...node.package }
198
197
  result.package.bundleDependencies = undefined
199
- result.hasInstallScript = node.hasInstallScript
200
198
 
201
199
  if (!populateDeps) {
202
200
  return
203
201
  }
204
202
 
205
- const edges = [...node.edgesOut.values()].filter(edge =>
203
+ let edges = [...node.edgesOut.values()].filter(edge =>
206
204
  edge.to?.target &&
207
205
  !(node.package.bundledDependencies || node.package.bundleDependencies)?.includes(edge.to.name)
208
206
  )
209
- const nonOptionalDeps = edges.filter(edge => !edge.optional).map(edge => edge.to.target)
207
+
208
+ // Only omit edge types for root and workspace nodes (matching shouldOmit scope)
209
+ if ((node.isProjectRoot || node.isWorkspace) && this.#omit.size) {
210
+ edges = edges.filter(edge => {
211
+ if (edge.dev && this.#omit.has('dev')) {
212
+ return false
213
+ }
214
+ if (edge.optional && this.#omit.has('optional')) {
215
+ return false
216
+ }
217
+ if (edge.peer && this.#omit.has('peer')) {
218
+ return false
219
+ }
220
+ return true
221
+ })
222
+ }
223
+
224
+ let nonOptionalDeps = edges.filter(edge => !edge.optional).map(edge => edge.to.target)
225
+
226
+ // npm auto-creates 'workspace' edges from root to all workspaces.
227
+ // For isolated/linked mode, only include workspaces that root explicitly declares as dependencies.
228
+ if (node.isProjectRoot) {
229
+ nonOptionalDeps = nonOptionalDeps.filter(n => !n.isWorkspace || this.#rootDeclaredDeps.has(n.packageName))
230
+ }
210
231
 
211
232
  // When legacyPeerDeps is enabled, peer dep edges are not created on the node.
212
233
  // Resolve them from the tree so they get symlinked in the store.
@@ -226,9 +247,9 @@ module.exports = cls => class IsolatedReifier extends cls {
226
247
  // local `file:` deps (non-workspace fsChildren) should be treated as local dependencies, not external, so they get symlinked directly instead of being extracted into the store.
227
248
  const isLocal = (n) => n.isWorkspace || node.fsChildren?.has(n)
228
249
  const optionalDeps = edges.filter(edge => edge.optional).map(edge => edge.to.target)
229
- result.localDependencies = await Promise.all(nonOptionalDeps.filter(isLocal).map(n => this.workspaceProxy(n)))
230
- result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !isLocal(n) && !n.inert).map(n => this.externalProxy(n)))
231
- result.externalOptionalDependencies = await Promise.all(optionalDeps.filter(n => !n.inert).map(n => this.externalProxy(n)))
250
+ result.localDependencies = await Promise.all(nonOptionalDeps.filter(isLocal).map(n => this.#workspaceProxy(n)))
251
+ result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !isLocal(n) && !n.inert).map(n => this.#externalProxy(n)))
252
+ result.externalOptionalDependencies = await Promise.all(optionalDeps.filter(n => !n.inert).map(n => this.#externalProxy(n)))
232
253
  result.dependencies = [
233
254
  ...result.externalDependencies,
234
255
  ...result.localDependencies,
@@ -288,91 +309,47 @@ module.exports = cls => class IsolatedReifier extends cls {
288
309
 
289
310
  async createIsolatedTree () {
290
311
  await this.makeIdealGraph()
291
-
292
312
  const bundledTree = await this.#createBundledTree()
293
313
 
294
- const root = {
295
- binPaths: [],
296
- children: new Map(),
297
- edgesIn: new Set(),
298
- edgesOut: new Map(),
299
- fsChildren: new Set(),
300
- global: false,
301
- hasShrinkwrap: false,
302
- integrity: null,
303
- inventory: new Map(),
304
- isLink: false,
305
- isProjectRoot: true,
306
- isRoot: true,
307
- isTop: true,
308
- linksIn: new Set(),
309
- meta: { loadedFromDisk: false },
310
- package: this.idealGraph.root.package,
311
- parent: null,
312
- path: this.idealGraph.root.localPath,
313
- realpath: this.idealGraph.root.localPath,
314
- // TODO: we should probably not reference this.idealTree
315
- resolved: this.idealTree.resolved,
316
- tops: new Set(),
317
- workspaces: new Map(),
318
- }
319
- root.inventory.set('', root)
314
+ const root = new IsolatedNode(this.idealGraph)
320
315
  root.root = root
321
- root.target = root
322
- // TODO inventory.query is a stub; audit-report needs 'packageName' support
323
- root.inventory.query = () => {
324
- return []
325
- }
316
+ root.top = root
317
+ root.inventory.set('', root)
326
318
  const processed = new Set()
327
319
  for (const c of this.idealGraph.workspaces) {
328
320
  const wsName = c.packageName
329
- const workspace = {
330
- binPaths: [],
331
- children: new Map(),
332
- edgesIn: new Set(),
333
- edgesOut: new Map(),
334
- fsChildren: new Set(),
335
- hasInstallScript: c.hasInstallScript,
336
- isLink: false,
337
- isRoot: false,
338
- linksIn: new Set(),
321
+ // XXX parent? root?
322
+ const workspace = new IsolatedNode({
339
323
  location: c.localLocation,
340
324
  name: wsName,
341
325
  package: c.package,
342
326
  path: c.localPath,
343
- realpath: c.localPath,
344
327
  resolved: c.resolved,
345
- }
346
- workspace.target = workspace
328
+ })
329
+ workspace.top = workspace
330
+ workspace.isWorkspace = true
347
331
  root.fsChildren.add(workspace)
348
332
  root.inventory.set(workspace.location, workspace)
333
+ root.workspaces.set(wsName, workspace.path)
349
334
 
350
- // Create workspace Link entry in children for _diffTrees lookup
351
- const wsLink = {
352
- binPaths: [],
353
- children: new Map(),
354
- edgesIn: new Set(),
355
- edgesOut: new Map(),
356
- fsChildren: new Set(),
357
- global: false,
358
- globalTop: false,
359
- isLink: true,
360
- isProjectRoot: false,
361
- isRoot: false,
362
- isTop: false,
363
- linksIn: new Set(),
364
- location: join('node_modules', wsName),
335
+ // Create workspace Link. For root declared deps, link at root node_modules/. For undeclared deps, link at the workspace's own node_modules/ (self-link).
336
+ const isDeclared = this.#rootDeclaredDeps.has(wsName)
337
+ const wsLink = new IsolatedLink({
338
+ location: isDeclared ? join('node_modules', wsName) : join(c.localLocation, 'node_modules', wsName),
365
339
  name: wsName,
366
340
  package: workspace.package,
367
341
  parent: root,
368
- path: join(root.path, 'node_modules', wsName),
342
+ path: isDeclared ? join(root.path, 'node_modules', wsName) : join(root.path, c.localLocation, 'node_modules', wsName),
369
343
  realpath: workspace.path,
370
344
  root,
371
345
  target: workspace,
346
+ })
347
+ wsLink.top = root
348
+ if (!isDeclared) {
349
+ workspace.children.set(wsName, wsLink)
372
350
  }
373
- root.children.set(wsLink.name, wsLink)
351
+ root.children.set(wsName, wsLink)
374
352
  root.inventory.set(wsLink.location, wsLink)
375
- root.workspaces.set(wsName, workspace.path)
376
353
  workspace.linksIn.add(wsLink)
377
354
  }
378
355
 
@@ -466,35 +443,29 @@ module.exports = cls => class IsolatedReifier extends cls {
466
443
  }
467
444
  }
468
445
 
469
- const link = {
470
- binPaths: [],
471
- children: new Map(),
472
- edgesIn: new Set(),
473
- edgesOut: new Map(),
474
- fsChildren: new Set(),
475
- global: false,
476
- globalTop: false,
477
- isLink: true,
478
- isProjectRoot: false,
479
- isRoot: false,
446
+ const pkg = {
447
+ _id: dep.package._id,
448
+ bin: target.package.bin,
449
+ bundleDependencies: undefined,
450
+ deprecated: undefined,
451
+ scripts: dep.package.scripts,
452
+ version: dep.package.version,
453
+ }
454
+ const link = new IsolatedLink({
480
455
  isStoreLink: true,
481
- isTop: false,
482
456
  location: join(nmFolder, dep.name),
483
457
  name: toKey,
484
458
  optional,
485
- // TODO _id: 'abc' ?
486
- package: { _id: 'abc', bundleDependencies: undefined, deprecated: undefined, bin: target.package.bin, scripts: dep.package.scripts },
487
459
  parent: root,
460
+ package: pkg,
488
461
  path: join(dep.root.localPath, nmFolder, dep.name),
489
462
  realpath: target.path,
490
- resolved: external
491
- ? `file:.store/${toKey}/node_modules/${dep.packageName}`
492
- : dep.resolved,
463
+ resolved: external ? `file:.store/${toKey}/node_modules/${dep.packageName}` : dep.resolved,
493
464
  root,
494
465
  target,
495
- version: dep.version,
496
- top: { path: dep.root.localPath },
497
- }
466
+ })
467
+ // XXX top is from place-dep not lib/link.js
468
+ link.top = { path: dep.root.localPath }
498
469
  const newEdge1 = { optional, from, to: link }
499
470
  from.edgesOut.set(dep.name, newEdge1)
500
471
  link.edgesIn.add(newEdge1)
@@ -36,6 +36,7 @@ const { callLimit: promiseCallLimit } = require('promise-call-limit')
36
36
  const optionalSet = require('../optional-set.js')
37
37
  const calcDepFlags = require('../calc-dep-flags.js')
38
38
  const { saveTypeMap, hasSubKey } = require('../add-rm-pkg-deps.js')
39
+ const { IsolatedNode, IsolatedLink } = require('../isolated-classes.js')
39
40
 
40
41
  const Shrinkwrap = require('../shrinkwrap.js')
41
42
  const { defaultLockfileVersion } = Shrinkwrap
@@ -841,49 +842,23 @@ module.exports = cls => class Reifier extends cls {
841
842
  #buildLinkedActualForDiff (idealTree, actualTree) {
842
843
  const combined = new Map()
843
844
 
844
- for (const child of actualTree.children.values()) {
845
- combined.set(child.path, child)
846
- }
847
-
845
+ // Create synthetic actual entries for ALL ideal children that exist on disk.
846
+ // The isolated ideal tree is flat (all entries as root children), but loadActual() produces a nested tree where workspace deps are under fsChildren and store entries are deep link targets.
847
+ // Synthetic entries ensure the diff compares matching resolved/integrity values (e.g. workspace links have resolved=undefined in the ideal tree but resolved="file:../packages/..." in the actual tree).
848
848
  for (const child of idealTree.children.values()) {
849
- if (!combined.has(child.path) && (child.isInStore || child.isStoreLink) &&
850
- existsSync(child.path)) {
851
- const entry = {
852
- global: false,
853
- globalTop: false,
854
- isProjectRoot: false,
855
- isTop: false,
856
- location: child.location,
857
- name: child.name,
858
- optional: child.optional,
859
- top: child.top,
860
- children: [],
861
- edgesIn: new Set(),
862
- edgesOut: new Map(),
863
- binPaths: [],
864
- fsChildren: [],
865
- /* istanbul ignore next -- emulate Node */
866
- getBundler () {
867
- return null
868
- },
869
- hasShrinkwrap: false,
870
- inDepBundle: false,
871
- integrity: null,
872
- isLink: Boolean(child.isLink),
873
- isRoot: false,
874
- isInStore: Boolean(child.isInStore),
875
- path: child.path,
876
- realpath: child.realpath,
877
- resolved: child.resolved,
878
- version: child.version,
879
- package: child.package,
880
- }
881
- entry.target = entry
882
- if (child.isLink && combined.has(child.realpath)) {
883
- entry.target = combined.get(child.realpath)
884
- }
885
- combined.set(child.path, entry)
849
+ if (combined.has(child.path) || !existsSync(child.path)) {
850
+ continue
851
+ }
852
+ let entry
853
+ if (child.isLink) {
854
+ entry = new IsolatedLink(child)
855
+ } else {
856
+ entry = new IsolatedNode(child)
857
+ }
858
+ if (child.isLink && combined.has(child.realpath)) {
859
+ entry.target = combined.get(child.realpath)
886
860
  }
861
+ combined.set(child.path, entry)
887
862
  }
888
863
 
889
864
  const origGet = actualTree.children.get.bind(actualTree.children)
@@ -891,32 +866,22 @@ module.exports = cls => class Reifier extends cls {
891
866
  /* istanbul ignore next -- only reached during scoped workspace installs */
892
867
  combined.get = (key) => combinedGet(key) || origGet(key)
893
868
 
894
- const wrapper = {
895
- isRoot: true,
896
- isLink: actualTree.isLink,
897
- target: actualTree.target,
898
- fsChildren: actualTree.fsChildren,
899
- path: actualTree.path,
900
- realpath: actualTree.realpath,
901
- edgesOut: actualTree.edgesOut,
902
- inventory: actualTree.inventory,
903
- package: actualTree.package,
904
- resolved: actualTree.resolved,
905
- version: actualTree.version,
906
- integrity: actualTree.integrity,
907
- binPaths: actualTree.binPaths,
908
- hasShrinkwrap: false,
909
- inDepBundle: false,
910
- parent: null,
911
- children: combined,
912
- }
913
-
914
- for (const child of combined.values()) {
915
- if (!child.parent) {
916
- child.parent = wrapper
917
- child.root = wrapper
918
- }
869
+ let wrapper
870
+ /* istanbul ignore next - untested! */
871
+ if (actualTree.isLink) {
872
+ wrapper = new IsolatedLink(actualTree)
873
+ } else {
874
+ wrapper = new IsolatedNode(actualTree)
919
875
  }
876
+ wrapper.root = wrapper
877
+ wrapper.binPaths = actualTree.binPaths
878
+ wrapper.children = combined
879
+ wrapper.edgesOut = actualTree.edgesOut
880
+ // Use empty fsChildren so that allChildren() only picks up entries from the combined map.
881
+ // The actual fsChildren have real children with different resolved values (e.g. file:../../../node_modules/.store/... vs file:.store/...) that would overwrite our synthetic entries in allChildren().
882
+ wrapper.fsChildren = new Set()
883
+ wrapper.integrity = actualTree.integrity
884
+ wrapper.inventory = actualTree.inventory
920
885
 
921
886
  return wrapper
922
887
  }
package/lib/inventory.js CHANGED
@@ -1,5 +1,4 @@
1
- // a class to manage an inventory and set of indexes of a set of objects based
2
- // on specific fields.
1
+ // a class to manage an inventory and set of indexes of a set of objects based on specific fields.
3
2
  const { hasOwnProperty } = Object.prototype
4
3
  const debug = require('./debug.js')
5
4
 
@@ -0,0 +1,138 @@
1
+ // Alternate versions of different classes that we use for isolated mode
2
+ const CaseInsensitiveMap = require('./case-insensitive-map.js')
3
+ const { resolve } = require('node:path')
4
+
5
+ // fake lib/inventory.js
6
+ class IsolatedInventory extends Map {
7
+ query () {
8
+ return []
9
+ }
10
+ }
11
+
12
+ // fake lib/node.js
13
+ class IsolatedNode {
14
+ binPaths = []
15
+ children = new CaseInsensitiveMap()
16
+ edgesIn = new Set()
17
+ edgesOut = new CaseInsensitiveMap()
18
+ fsChildren = new Set()
19
+ hasShrinkwrap = false
20
+ integrity = null
21
+ inventory = new IsolatedInventory()
22
+ isInStore = false
23
+ linksIn = new Set()
24
+ meta = { loadedFromDisk: false }
25
+ optional = false
26
+ parent = null
27
+ root = null
28
+ tops = new Set()
29
+ workspaces = new Map()
30
+
31
+ constructor (options) {
32
+ this.location = options.location
33
+ this.name = options.name
34
+ this.package = options.package
35
+ this.path = options.path
36
+ this.realpath = !this.isLink ? this.path : resolve(options.realpath)
37
+
38
+ if (options.parent) {
39
+ this.parent = options.parent
40
+ }
41
+ if (options.resolved) {
42
+ this.resolved = options.resolved
43
+ }
44
+ if (options.root) {
45
+ this.root = options.root
46
+ }
47
+ if (options.isInStore) {
48
+ this.isInStore = true
49
+ }
50
+ if (options.optional) {
51
+ this.optional = true
52
+ }
53
+ }
54
+
55
+ get isRoot () {
56
+ return this === this.root
57
+ }
58
+
59
+ // The idealGraph is where this is set to true
60
+ get isProjectRoot () {
61
+ return false
62
+ }
63
+
64
+ get inDepBundle () {
65
+ return false
66
+ }
67
+
68
+ get isLink () {
69
+ return false
70
+ }
71
+
72
+ get isTop () {
73
+ return !this.parent
74
+ }
75
+
76
+ /* istanbul ignore next -- emulate lib/node.js */
77
+ get global () {
78
+ return false
79
+ }
80
+
81
+ get globalTop () {
82
+ return false
83
+ }
84
+
85
+ /* istanbul ignore next -- emulate lib/node.js */
86
+ set target (t) {
87
+ // nop
88
+ // In the real lib/node.js this throws in debug mode
89
+ }
90
+
91
+ get target () {
92
+ return this
93
+ }
94
+
95
+ /* istanbul ignore next -- emulate lib/node.js */
96
+ getBundler () {
97
+ return null
98
+ }
99
+
100
+ /* istanbul ignore next -- emulate lib/node.js */
101
+ get hasInstallScript () {
102
+ const { hasInstallScript, scripts } = this.package
103
+ const { install, preinstall, postinstall } = scripts || {}
104
+ return !!(hasInstallScript || install || preinstall || postinstall)
105
+ }
106
+
107
+ get version () {
108
+ return this.package.version
109
+ }
110
+ }
111
+
112
+ // fake lib/link.js
113
+ class IsolatedLink extends IsolatedNode {
114
+ #target
115
+ isStoreLink = false
116
+
117
+ constructor (options) {
118
+ super(options)
119
+ this.#target = options.target
120
+ if (options.isStoreLink) {
121
+ this.isStoreLink = true
122
+ }
123
+ }
124
+
125
+ get isLink () {
126
+ return true
127
+ }
128
+
129
+ set target (t) {
130
+ this.#target = t
131
+ }
132
+
133
+ get target () {
134
+ return this.#target
135
+ }
136
+ }
137
+
138
+ module.exports = { IsolatedNode, IsolatedLink }
package/lib/node.js CHANGED
@@ -74,33 +74,32 @@ class Node {
74
74
  constructor (options) {
75
75
  // NB: path can be null if it's a link target
76
76
  const {
77
- root,
78
- path,
79
- realpath,
80
- parent,
81
- error,
82
- meta,
83
- fsParent,
84
- resolved,
85
- integrity,
86
- // allow setting name explicitly when we haven't set a path yet
87
- name,
88
77
  children,
78
+ dev = true,
79
+ devOptional = true,
80
+ dummy = false,
81
+ error,
82
+ extraneous = true,
89
83
  fsChildren,
84
+ fsParent,
85
+ global = false,
86
+ hasShrinkwrap,
90
87
  installLinks = false,
88
+ integrity,
89
+ isInStore = false,
91
90
  legacyPeerDeps = false,
92
91
  linksIn,
93
- isInStore = false,
94
- hasShrinkwrap,
95
- overrides,
96
92
  loadOverrides = false,
97
- extraneous = true,
98
- dev = true,
93
+ meta,
94
+ name, // allow setting name explicitly when we haven't set a path yet
99
95
  optional = true,
100
- devOptional = true,
96
+ overrides,
97
+ parent,
98
+ path,
101
99
  peer = true,
102
- global = false,
103
- dummy = false,
100
+ realpath,
101
+ resolved,
102
+ root,
104
103
  sourceReference = null,
105
104
  } = options
106
105
  // this object gives querySelectorAll somewhere to stash context about a node
@@ -464,6 +463,27 @@ class Node {
464
463
  return false
465
464
  }
466
465
 
466
+ shouldOmit (omitSet) {
467
+ if (!omitSet.size) {
468
+ return false
469
+ }
470
+
471
+ const { top } = this
472
+
473
+ // if the top is not the root or workspace then we do not want to omit it
474
+ if (!top.isProjectRoot && !top.isWorkspace) {
475
+ return false
476
+ }
477
+
478
+ // omit node if the dep type matches any omit flags that were set
479
+ return (
480
+ this.peer && omitSet.has('peer') ||
481
+ this.dev && omitSet.has('dev') ||
482
+ this.optional && omitSet.has('optional') ||
483
+ this.devOptional && omitSet.has('optional') && omitSet.has('dev')
484
+ )
485
+ }
486
+
467
487
  getBundler (path = []) {
468
488
  // made a cycle, definitely not bundled!
469
489
  if (path.includes(this)) {
@@ -785,9 +785,14 @@ const hasParent = (node, compareNodes) => {
785
785
  compareNode = compareNode.target
786
786
  }
787
787
 
788
- // follows logical parent for link anscestors
788
+ // Follows logical parent for link ancestors (e.g. workspaces whose target lives outside node_modules).
789
+ // Only match if the node has a link whose parent is the compareNode. Without this check, nodes deep in the store (linked strategy) would incorrectly match as children of root via their fsParent chain.
789
790
  if (node.isTop && (node.resolveParent === compareNode)) {
790
- return true
791
+ for (const link of node.linksIn) {
792
+ if (link.parent === compareNode) {
793
+ return true
794
+ }
795
+ }
791
796
  }
792
797
  // follows edges-in to check if they match a possible parent
793
798
  for (const edge of node.edgesIn) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "8.0.2",
3
+ "version": "8.0.3",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",