@npmcli/arborist 5.5.0 → 6.0.0-pre.0

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.
@@ -15,34 +15,30 @@ const Node = require('../node.js')
15
15
  const Link = require('../link.js')
16
16
  const realpath = require('../realpath.js')
17
17
 
18
- const _loadFSNode = Symbol('loadFSNode')
19
- const _newNode = Symbol('newNode')
20
- const _newLink = Symbol('newLink')
21
- const _loadFSTree = Symbol('loadFSTree')
22
- const _loadFSChildren = Symbol('loadFSChildren')
23
- const _findMissingEdges = Symbol('findMissingEdges')
24
- const _findFSParents = Symbol('findFSParents')
25
- const _resetDepFlags = Symbol('resetDepFlags')
26
-
27
- const _actualTreeLoaded = Symbol('actualTreeLoaded')
18
+ // public symbols
19
+ const _changePath = Symbol.for('_changePath')
20
+ const _global = Symbol.for('global')
21
+ const _loadWorkspaces = Symbol.for('loadWorkspaces')
28
22
  const _rpcache = Symbol.for('realpathCache')
29
23
  const _stcache = Symbol.for('statCache')
30
- const _topNodes = Symbol('linkTargets')
24
+
25
+ // private symbols
26
+ const _actualTree = Symbol('actualTree')
27
+ const _actualTreeLoaded = Symbol('actualTreeLoaded')
28
+ const _actualTreePromise = Symbol('actualTreePromise')
31
29
  const _cache = Symbol('nodeLoadingCache')
30
+ const _filter = Symbol('filter')
31
+ const _findMissingEdges = Symbol('findMissingEdges')
32
32
  const _loadActual = Symbol('loadActual')
33
- const _loadActualVirtually = Symbol('loadActualVirtually')
34
- const _loadActualActually = Symbol('loadActualActually')
35
- const _loadWorkspaces = Symbol.for('loadWorkspaces')
36
- const _loadWorkspaceTargets = Symbol('loadWorkspaceTargets')
37
- const _actualTreePromise = Symbol('actualTreePromise')
38
- const _actualTree = Symbol('actualTree')
33
+ const _loadFSChildren = Symbol('loadFSChildren')
34
+ const _loadFSNode = Symbol('loadFSNode')
35
+ const _loadFSTree = Symbol('loadFSTree')
36
+ const _newLink = Symbol('newLink')
37
+ const _newNode = Symbol('newNode')
38
+ const _topNodes = Symbol('linkTargets')
39
39
  const _transplant = Symbol('transplant')
40
40
  const _transplantFilter = Symbol('transplantFilter')
41
41
 
42
- const _filter = Symbol('filter')
43
- const _global = Symbol.for('global')
44
- const _changePath = Symbol.for('_changePath')
45
-
46
42
  module.exports = cls => class ActualLoader extends cls {
47
43
  constructor (options) {
48
44
  super(options)
@@ -75,37 +71,44 @@ module.exports = cls => class ActualLoader extends cls {
75
71
  this[_topNodes] = new Set()
76
72
  }
77
73
 
78
- [_resetDepFlags] (tree, root) {
79
- // reset all deps to extraneous prior to recalc
80
- if (!root) {
81
- for (const node of tree.inventory.values()) {
82
- node.extraneous = true
83
- }
84
- }
85
-
86
- // only reset root flags if we're not re-rooting,
87
- // otherwise leave as-is
88
- calcDepFlags(tree, !root)
89
- return tree
90
- }
91
-
92
74
  // public method
93
75
  async loadActual (options = {}) {
94
- // allow the user to set options on the ctor as well.
95
- // XXX: deprecate separate method options objects.
96
- options = { ...this.options, ...options }
97
-
98
- // stash the promise so that we don't ever have more than one
99
- // going at the same time. This is so that buildIdealTree can
100
- // default to the actualTree if no shrinkwrap present, but
101
- // reify() can still call buildIdealTree and loadActual in parallel
102
- // safely.
103
- return this.actualTree ? this.actualTree
104
- : this[_actualTreePromise] ? this[_actualTreePromise]
105
- : this[_actualTreePromise] = this[_loadActual](options)
106
- .then(tree => this[_resetDepFlags](tree, options.root))
107
- .then(tree => this.actualTree = treeCheck(tree))
76
+ // In the past this.actualTree was set as a promise that eventually
77
+ // resolved, and overwrite this.actualTree with the resolved value. This
78
+ // was a problem because virtually no other code expects this.actualTree to
79
+ // be a promise. Instead we only set it once resolved, and also return it
80
+ // from the promise so that it is what's returned from this function when
81
+ // awaited.
82
+ if (this.actualTree) {
83
+ return this.actualTree
84
+ }
85
+ if (!this[_actualTreePromise]) {
86
+ // allow the user to set options on the ctor as well.
87
+ // XXX: deprecate separate method options objects.
88
+ options = { ...this.options, ...options }
89
+
90
+ this[_actualTreePromise] = this[_loadActual](options)
91
+ .then(tree => {
92
+ // reset all deps to extraneous prior to recalc
93
+ if (!options.root) {
94
+ for (const node of tree.inventory.values()) {
95
+ node.extraneous = true
96
+ }
97
+ }
98
+
99
+ // only reset root flags if we're not re-rooting,
100
+ // otherwise leave as-is
101
+ calcDepFlags(tree, !options.root)
102
+ this.actualTree = treeCheck(tree)
103
+ return this.actualTree
104
+ })
105
+ }
106
+ return this[_actualTreePromise]
108
107
  }
108
+ // return the promise so that we don't ever have more than one going at the
109
+ // same time. This is so that buildIdealTree can default to the actualTree
110
+ // if no shrinkwrap present, but reify() can still call buildIdealTree and
111
+ // loadActual in parallel safely.
109
112
 
110
113
  async [_loadActual] (options) {
111
114
  // mostly realpath to throw if the root doesn't exist
@@ -122,75 +125,102 @@ module.exports = cls => class ActualLoader extends cls {
122
125
 
123
126
  if (global) {
124
127
  const real = await realpath(this.path, this[_rpcache], this[_stcache])
125
- const newNodeOrLink = this.path === real ? _newNode : _newLink
126
- this[_actualTree] = await this[newNodeOrLink]({
128
+ const params = {
127
129
  path: this.path,
128
130
  realpath: real,
129
131
  pkg: {},
130
132
  global,
131
133
  loadOverrides: true,
134
+ }
135
+ if (this.path === real) {
136
+ this[_actualTree] = this[_newNode](params)
137
+ } else {
138
+ this[_actualTree] = await this[_newLink](params)
139
+ }
140
+ } else {
141
+ // not in global mode, hidden lockfile is allowed, load root pkg too
142
+ this[_actualTree] = await this[_loadFSNode]({
143
+ path: this.path,
144
+ real: await realpath(this.path, this[_rpcache], this[_stcache]),
145
+ loadOverrides: true,
132
146
  })
133
- return this[_loadActualActually]({ root, ignoreMissing, global })
134
- }
135
147
 
136
- // not in global mode, hidden lockfile is allowed, load root pkg too
137
- this[_actualTree] = await this[_loadFSNode]({
138
- path: this.path,
139
- real: await realpath(this.path, this[_rpcache], this[_stcache]),
140
- loadOverrides: true,
141
- })
148
+ this[_actualTree].assertRootOverrides()
149
+
150
+ // if forceActual is set, don't even try the hidden lockfile
151
+ if (!forceActual) {
152
+ // Note: hidden lockfile will be rejected if it's not the latest thing
153
+ // in the folder, or if any of the entries in the hidden lockfile are
154
+ // missing.
155
+ const meta = await Shrinkwrap.load({
156
+ path: this[_actualTree].path,
157
+ hiddenLockfile: true,
158
+ resolveOptions: this.options,
159
+ })
160
+
161
+ if (meta.loadedFromDisk) {
162
+ this[_actualTree].meta = meta
163
+ // have to load on a new Arborist object, so we don't assign
164
+ // the virtualTree on this one! Also, the weird reference is because
165
+ // we can't easily get a ref to Arborist in this module, without
166
+ // creating a circular reference, since this class is a mixin used
167
+ // to build up the Arborist class itself.
168
+ await new this.constructor({ ...this.options }).loadVirtual({
169
+ root: this[_actualTree],
170
+ })
171
+ await this[_loadWorkspaces](this[_actualTree])
142
172
 
143
- this[_actualTree].assertRootOverrides()
173
+ this[_transplant](root)
174
+ return this[_actualTree]
175
+ }
176
+ }
144
177
 
145
- // if forceActual is set, don't even try the hidden lockfile
146
- if (!forceActual) {
147
- // Note: hidden lockfile will be rejected if it's not the latest thing
148
- // in the folder, or if any of the entries in the hidden lockfile are
149
- // missing.
150
178
  const meta = await Shrinkwrap.load({
151
179
  path: this[_actualTree].path,
152
- hiddenLockfile: true,
180
+ lockfileVersion: this.options.lockfileVersion,
153
181
  resolveOptions: this.options,
154
182
  })
155
-
156
- if (meta.loadedFromDisk) {
157
- this[_actualTree].meta = meta
158
- return this[_loadActualVirtually]({ root })
159
- }
183
+ this[_actualTree].meta = meta
160
184
  }
161
185
 
162
- const meta = await Shrinkwrap.load({
163
- path: this[_actualTree].path,
164
- lockfileVersion: this.options.lockfileVersion,
165
- resolveOptions: this.options,
166
- })
167
- this[_actualTree].meta = meta
168
- return this[_loadActualActually]({ root, ignoreMissing })
169
- }
170
-
171
- async [_loadActualVirtually] ({ root }) {
172
- // have to load on a new Arborist object, so we don't assign
173
- // the virtualTree on this one! Also, the weird reference is because
174
- // we can't easily get a ref to Arborist in this module, without
175
- // creating a circular reference, since this class is a mixin used
176
- // to build up the Arborist class itself.
177
- await new this.constructor({ ...this.options }).loadVirtual({
178
- root: this[_actualTree],
179
- })
186
+ await this[_loadFSTree](this[_actualTree])
180
187
  await this[_loadWorkspaces](this[_actualTree])
181
188
 
182
- this[_transplant](root)
183
- return this[_actualTree]
184
- }
189
+ // if there are workspace targets without Link nodes created, load
190
+ // the targets, so that we know what they are.
191
+ if (this[_actualTree].workspaces && this[_actualTree].workspaces.size) {
192
+ const promises = []
193
+ for (const path of this[_actualTree].workspaces.values()) {
194
+ if (!this[_cache].has(path)) {
195
+ // workspace overrides use the root overrides
196
+ const p = this[_loadFSNode]({ path, root: this[_actualTree], useRootOverrides: true })
197
+ .then(node => this[_loadFSTree](node))
198
+ promises.push(p)
199
+ }
200
+ }
201
+ await Promise.all(promises)
202
+ }
185
203
 
186
- async [_loadActualActually] ({ root, ignoreMissing, global }) {
187
- await this[_loadFSTree](this[_actualTree])
188
- await this[_loadWorkspaces](this[_actualTree])
189
- await this[_loadWorkspaceTargets](this[_actualTree])
190
204
  if (!ignoreMissing) {
191
205
  await this[_findMissingEdges]()
192
206
  }
193
- this[_findFSParents]()
207
+
208
+ // try to find a node that is the parent in a fs tree sense, but not a
209
+ // node_modules tree sense, of any link targets. this allows us to
210
+ // resolve deps that node will find, but a legacy npm view of the
211
+ // world would not have noticed.
212
+ for (const path of this[_topNodes]) {
213
+ const node = this[_cache].get(path)
214
+ if (node && !node.parent && !node.fsParent) {
215
+ for (const p of walkUp(dirname(path))) {
216
+ if (this[_cache].has(p)) {
217
+ node.fsParent = this[_cache].get(p)
218
+ break
219
+ }
220
+ }
221
+ }
222
+ }
223
+
194
224
  this[_transplant](root)
195
225
 
196
226
  if (global) {
@@ -209,25 +239,6 @@ module.exports = cls => class ActualLoader extends cls {
209
239
  return this[_actualTree]
210
240
  }
211
241
 
212
- // if there are workspace targets without Link nodes created, load
213
- // the targets, so that we know what they are.
214
- async [_loadWorkspaceTargets] (tree) {
215
- if (!tree.workspaces || !tree.workspaces.size) {
216
- return
217
- }
218
-
219
- const promises = []
220
- for (const path of tree.workspaces.values()) {
221
- if (!this[_cache].has(path)) {
222
- // workspace overrides use the root overrides
223
- const p = this[_loadFSNode]({ path, root: this[_actualTree], useRootOverrides: true })
224
- .then(node => this[_loadFSTree](node))
225
- promises.push(p)
226
- }
227
- }
228
- await Promise.all(promises)
229
- }
230
-
231
242
  [_transplant] (root) {
232
243
  if (!root || root === this[_actualTree]) {
233
244
  return
@@ -248,80 +259,63 @@ module.exports = cls => class ActualLoader extends cls {
248
259
  this[_actualTree] = root
249
260
  }
250
261
 
251
- [_loadFSNode] ({ path, parent, real, root, loadOverrides, useRootOverrides }) {
262
+ async [_loadFSNode] ({ path, parent, real, root, loadOverrides, useRootOverrides }) {
252
263
  if (!real) {
253
- return realpath(path, this[_rpcache], this[_stcache])
254
- .then(
255
- real => this[_loadFSNode]({
256
- path,
257
- parent,
258
- real,
259
- root,
260
- loadOverrides,
261
- useRootOverrides,
262
- }),
263
- // if realpath fails, just provide a dummy error node
264
- error => new Node({
265
- error,
266
- path,
267
- realpath: path,
268
- parent,
269
- root,
270
- loadOverrides,
271
- })
272
- )
273
- }
274
-
275
- // cache temporarily holds a promise placeholder so we don't try to create
276
- // the same node multiple times. this is rare to encounter, given the
277
- // aggressive caching on realpath and lstat calls, but it's possible that
278
- // it's already loaded as a tree top, and then gets its parent loaded
279
- // later, if a symlink points deeper in the tree.
280
- const cached = this[_cache].get(path)
281
- if (cached && !cached.dummy) {
282
- return Promise.resolve(cached).then(node => {
283
- node.parent = parent
284
- return node
285
- })
286
- }
287
-
288
- const p = rpj(join(real, 'package.json'))
289
- // soldier on if read-package-json raises an error
290
- .then(pkg => [pkg, null], error => [null, error])
291
- .then(([pkg, error]) => {
292
- return this[normalize(path) === real ? _newNode : _newLink]({
293
- installLinks: this.installLinks,
294
- legacyPeerDeps: this.legacyPeerDeps,
295
- path,
296
- realpath: real,
297
- pkg,
264
+ try {
265
+ real = await realpath(path, this[_rpcache], this[_stcache])
266
+ } catch (error) {
267
+ // if realpath fails, just provide a dummy error node
268
+ return new Node({
298
269
  error,
270
+ path,
271
+ realpath: path,
299
272
  parent,
300
273
  root,
301
274
  loadOverrides,
302
- ...(useRootOverrides && root.overrides
303
- ? { overrides: root.overrides.getNodeRule({ name: pkg.name, version: pkg.version }) }
304
- : {}),
305
275
  })
306
- })
307
- .then(node => {
308
- this[_cache].set(path, node)
309
- return node
310
- })
276
+ }
277
+ }
278
+
279
+ const cached = this[_cache].get(path)
280
+ let node
281
+ // missing edges get a dummy node, assign the parent and return it
282
+ if (cached && !cached.dummy) {
283
+ cached.parent = parent
284
+ return cached
285
+ } else {
286
+ const params = {
287
+ installLinks: this.installLinks,
288
+ legacyPeerDeps: this.legacyPeerDeps,
289
+ path,
290
+ realpath: real,
291
+ parent,
292
+ root,
293
+ loadOverrides,
294
+ }
295
+
296
+ try {
297
+ const pkg = await rpj(join(real, 'package.json'))
298
+ params.pkg = pkg
299
+ if (useRootOverrides && root.overrides) {
300
+ params.overrides = root.overrides.getNodeRule({ name: pkg.name, version: pkg.version })
301
+ }
302
+ } catch (err) {
303
+ params.error = err
304
+ }
311
305
 
312
- this[_cache].set(path, p)
313
- return p
306
+ // soldier on if read-package-json raises an error, passing it to the
307
+ // Node which will attach it to its errors array (Link passes it along to
308
+ // its target node)
309
+ if (normalize(path) === real) {
310
+ node = this[_newNode](params)
311
+ } else {
312
+ node = await this[_newLink](params)
313
+ }
314
+ }
315
+ this[_cache].set(path, node)
316
+ return node
314
317
  }
315
318
 
316
- // this is the way it is to expose a timing issue which is difficult to
317
- // test otherwise. The creation of a Node may take slightly longer than
318
- // the creation of a Link that targets it. If the Node has _begun_ its
319
- // creation phase (and put a Promise in the cache) then the Link will
320
- // get a Promise as its cachedTarget instead of an actual Node object.
321
- // This is not a problem, because it gets resolved prior to returning
322
- // the tree or attempting to load children. However, it IS remarkably
323
- // difficult to get to happen in a test environment to verify reliably.
324
- // Hence this kludge.
325
319
  [_newNode] (options) {
326
320
  // check it for an fsParent if it's a tree top. there's a decent chance
327
321
  // it'll get parented later, making the fsParent scan a no-op, but better
@@ -330,69 +324,56 @@ module.exports = cls => class ActualLoader extends cls {
330
324
  if (!parent) {
331
325
  this[_topNodes].add(realpath)
332
326
  }
333
- return process.env._TEST_ARBORIST_SLOW_LINK_TARGET_ === '1'
334
- ? new Promise(res => setTimeout(() => res(new Node(options)), 100))
335
- : new Node(options)
327
+ return new Node(options)
336
328
  }
337
329
 
338
- [_newLink] (options) {
330
+ async [_newLink] (options) {
339
331
  const { realpath } = options
340
332
  this[_topNodes].add(realpath)
341
333
  const target = this[_cache].get(realpath)
342
334
  const link = new Link({ ...options, target })
343
335
 
344
336
  if (!target) {
337
+ // Link set its target itself in this case
345
338
  this[_cache].set(realpath, link.target)
346
339
  // if a link target points at a node outside of the root tree's
347
340
  // node_modules hierarchy, then load that node as well.
348
- return this[_loadFSTree](link.target).then(() => link)
349
- } else if (target.then) {
350
- target.then(node => link.target = node)
341
+ await this[_loadFSTree](link.target)
351
342
  }
352
343
 
353
344
  return link
354
345
  }
355
346
 
356
- [_loadFSTree] (node) {
347
+ async [_loadFSTree] (node) {
357
348
  const did = this[_actualTreeLoaded]
358
- node = node.target
359
-
360
- // if a Link target has started, but not completed, then
361
- // a Promise will be in the cache to indicate this.
362
- if (node.then) {
363
- return node.then(node => this[_loadFSTree](node))
364
- }
365
-
366
- // impossible except in pathological ELOOP cases
367
- /* istanbul ignore if */
368
- if (did.has(node.realpath)) {
369
- return Promise.resolve(node)
370
- }
371
-
372
- did.add(node.realpath)
373
- return this[_loadFSChildren](node)
374
- .then(() => Promise.all(
375
- [...node.children.entries()]
349
+ if (!did.has(node.target.realpath)) {
350
+ did.add(node.target.realpath)
351
+ await this[_loadFSChildren](node.target)
352
+ return Promise.all(
353
+ [...node.target.children.entries()]
376
354
  .filter(([name, kid]) => !did.has(kid.realpath))
377
- .map(([name, kid]) => this[_loadFSTree](kid))))
355
+ .map(([name, kid]) => this[_loadFSTree](kid))
356
+ )
357
+ }
378
358
  }
379
359
 
380
360
  // create child nodes for all the entries in node_modules
381
361
  // and attach them to the node as a parent
382
- [_loadFSChildren] (node) {
362
+ async [_loadFSChildren] (node) {
383
363
  const nm = resolve(node.realpath, 'node_modules')
384
- return readdir(nm).then(kids => {
364
+ try {
365
+ const kids = await readdir(nm)
385
366
  return Promise.all(
386
- // ignore . dirs and retired scoped package folders
367
+ // ignore . dirs and retired scoped package folders
387
368
  kids.filter(kid => !/^(@[^/]+\/)?\./.test(kid))
388
369
  .filter(kid => this[_filter](node, kid))
389
370
  .map(kid => this[_loadFSNode]({
390
371
  parent: node,
391
372
  path: resolve(nm, kid),
392
373
  })))
393
- },
394
- // error in the readdir is not fatal, just means no kids
395
- () => {})
374
+ } catch {
375
+ // error in the readdir is not fatal, just means no kids
376
+ }
396
377
  }
397
378
 
398
379
  async [_findMissingEdges] () {
@@ -439,6 +420,7 @@ module.exports = cls => class ActualLoader extends cls {
439
420
 
440
421
  const d = this[_cache].has(p) ? await this[_cache].get(p)
441
422
  : new Node({ path: p, root: node.root, dummy: true })
423
+ // not a promise
442
424
  this[_cache].set(p, d)
443
425
  if (d.dummy) {
444
426
  // it's a placeholder, so likely would not have loaded this dep,
@@ -459,22 +441,4 @@ module.exports = cls => class ActualLoader extends cls {
459
441
  await Promise.all(depPromises)
460
442
  }
461
443
  }
462
-
463
- // try to find a node that is the parent in a fs tree sense, but not a
464
- // node_modules tree sense, of any link targets. this allows us to
465
- // resolve deps that node will find, but a legacy npm view of the
466
- // world would not have noticed.
467
- [_findFSParents] () {
468
- for (const path of this[_topNodes]) {
469
- const node = this[_cache].get(path)
470
- if (node && !node.parent && !node.fsParent) {
471
- for (const p of walkUp(dirname(path))) {
472
- if (this[_cache].has(p)) {
473
- node.fsParent = this[_cache].get(p)
474
- break
475
- }
476
- }
477
- }
478
- }
479
- }
480
444
  }
@@ -359,6 +359,9 @@ module.exports = cls => class Builder extends cls {
359
359
  pkg,
360
360
  path,
361
361
  event,
362
+ // I do not know why this needs to be on THIS line but refactoring
363
+ // this function would be quite a process
364
+ // eslint-disable-next-line promise/always-return
362
365
  cmd: args && args[args.length - 1],
363
366
  env,
364
367
  code,