@npmcli/arborist 7.2.1 → 7.2.2

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.
@@ -333,7 +333,7 @@ module.exports = cls => class ActualLoader extends cls {
333
333
 
334
334
  async #loadFSTree (node) {
335
335
  const did = this.#actualTreeLoaded
336
- if (!did.has(node.target.realpath)) {
336
+ if (!node.isLink && !did.has(node.target.realpath)) {
337
337
  did.add(node.target.realpath)
338
338
  await this.#loadFSChildren(node.target)
339
339
  return Promise.all(
package/lib/shrinkwrap.js CHANGED
@@ -48,7 +48,7 @@ const { resolve, basename, relative } = require('path')
48
48
  const specFromLock = require('./spec-from-lock.js')
49
49
  const versionFromTgz = require('./version-from-tgz.js')
50
50
  const npa = require('npm-package-arg')
51
- const rpj = require('read-package-json-fast')
51
+ const pkgJson = require('@npmcli/package-json')
52
52
  const parseJSON = require('parse-conflict-json')
53
53
 
54
54
  const stringify = require('json-stringify-nice')
@@ -81,28 +81,6 @@ const relpath = require('./relpath.js')
81
81
  const consistentResolve = require('./consistent-resolve.js')
82
82
  const { overrideResolves } = require('./override-resolves.js')
83
83
 
84
- const maybeReadFile = file => {
85
- return readFile(file, 'utf8').then(d => d, er => {
86
- /* istanbul ignore else - can't test without breaking module itself */
87
- if (er.code === 'ENOENT') {
88
- return ''
89
- } else {
90
- throw er
91
- }
92
- })
93
- }
94
-
95
- const maybeStatFile = file => {
96
- return stat(file).then(st => st.isFile(), er => {
97
- /* istanbul ignore else - can't test without breaking module itself */
98
- if (er.code === 'ENOENT') {
99
- return null
100
- } else {
101
- throw er
102
- }
103
- })
104
- }
105
-
106
84
  const pkgMetaKeys = [
107
85
  // note: name is included if necessary, for alias packages
108
86
  'version',
@@ -134,81 +112,72 @@ const nodeMetaKeys = [
134
112
 
135
113
  const metaFieldFromPkg = (pkg, key) => {
136
114
  const val = pkg[key]
137
- // get the license type, not an object
138
- return (key === 'license' && val && typeof val === 'object' && val.type)
139
- ? val.type
115
+ if (val) {
116
+ // get only the license type, not the full object
117
+ if (key === 'license' && typeof val === 'object' && val.type) {
118
+ return val.type
119
+ }
140
120
  // skip empty objects and falsey values
141
- : (val && !(typeof val === 'object' && !Object.keys(val).length)) ? val
142
- : null
121
+ if (typeof val !== 'object' || Object.keys(val).length) {
122
+ return val
123
+ }
124
+ }
125
+ return null
143
126
  }
144
127
 
145
- // check to make sure that there are no packages newer than the hidden lockfile
146
- const assertNoNewer = async (path, data, lockTime, dir = path, seen = null) => {
128
+ // check to make sure that there are no packages newer than or missing from the hidden lockfile
129
+ const assertNoNewer = async (path, data, lockTime, dir, seen) => {
147
130
  const base = basename(dir)
148
131
  const isNM = dir !== path && base === 'node_modules'
149
- const isScope = dir !== path && !isNM && base.charAt(0) === '@'
150
- const isParent = dir === path || isNM || isScope
132
+ const isScope = dir !== path && base.startsWith('@')
133
+ const isParent = (dir === path) || isNM || isScope
151
134
 
135
+ const parent = isParent ? dir : resolve(dir, 'node_modules')
152
136
  const rel = relpath(path, dir)
153
- if (dir !== path) {
154
- const dirTime = (await stat(dir)).mtime
137
+ seen.add(rel)
138
+ let entries
139
+ if (dir === path) {
140
+ entries = [{ name: 'node_modules', isDirectory: () => true }]
141
+ } else {
142
+ const { mtime: dirTime } = await stat(dir)
155
143
  if (dirTime > lockTime) {
156
- throw 'out of date, updated: ' + rel
144
+ throw new Error(`out of date, updated: ${rel}`)
157
145
  }
158
146
  if (!isScope && !isNM && !data.packages[rel]) {
159
- throw 'missing from lockfile: ' + rel
147
+ throw new Error(`missing from lockfile: ${rel}`)
160
148
  }
161
- seen.add(rel)
162
- } else {
163
- seen = new Set([rel])
149
+ entries = await readdir(parent, { withFileTypes: true }).catch(() => [])
164
150
  }
165
151
 
166
- const parent = isParent ? dir : resolve(dir, 'node_modules')
167
- const children = dir === path
168
- ? Promise.resolve([{ name: 'node_modules', isDirectory: () => true }])
169
- : readdir(parent, { withFileTypes: true })
170
-
171
- const ents = await children.catch(() => [])
172
- await Promise.all(ents.map(async ent => {
173
- const child = resolve(parent, ent.name)
174
- if (ent.isDirectory() && !/^\./.test(ent.name)) {
152
+ // TODO limit concurrency here, this is recursive
153
+ await Promise.all(entries.map(async dirent => {
154
+ const child = resolve(parent, dirent.name)
155
+ if (dirent.isDirectory() && !dirent.name.startsWith('.')) {
175
156
  await assertNoNewer(path, data, lockTime, child, seen)
176
- } else if (ent.isSymbolicLink()) {
157
+ } else if (dirent.isSymbolicLink()) {
177
158
  const target = resolve(parent, await readlink(child))
178
159
  const tstat = await stat(target).catch(
179
160
  /* istanbul ignore next - windows */ () => null)
180
161
  seen.add(relpath(path, child))
181
162
  /* istanbul ignore next - windows cannot do this */
182
- if (tstat && tstat.isDirectory() && !seen.has(relpath(path, target))) {
163
+ if (tstat?.isDirectory() && !seen.has(relpath(path, target))) {
183
164
  await assertNoNewer(path, data, lockTime, target, seen)
184
165
  }
185
166
  }
186
167
  }))
168
+
187
169
  if (dir !== path) {
188
170
  return
189
171
  }
190
172
 
191
173
  // assert that all the entries in the lockfile were seen
192
- for (const loc of new Set(Object.keys(data.packages))) {
174
+ for (const loc in data.packages) {
193
175
  if (!seen.has(loc)) {
194
- throw 'missing from node_modules: ' + loc
176
+ throw new Error(`missing from node_modules: ${loc}`)
195
177
  }
196
178
  }
197
179
  }
198
180
 
199
- const _awaitingUpdate = Symbol('_awaitingUpdate')
200
- const _updateWaitingNode = Symbol('_updateWaitingNode')
201
- const _lockFromLoc = Symbol('_lockFromLoc')
202
- const _pathToLoc = Symbol('_pathToLoc')
203
- const _loadAll = Symbol('_loadAll')
204
- const _metaFromLock = Symbol('_metaFromLock')
205
- const _resolveMetaNode = Symbol('_resolveMetaNode')
206
- const _fixDependencies = Symbol('_fixDependencies')
207
- const _buildLegacyLockfile = Symbol('_buildLegacyLockfile')
208
- const _filenameSet = Symbol('_filenameSet')
209
- const _maybeRead = Symbol('_maybeRead')
210
- const _maybeStat = Symbol('_maybeStat')
211
-
212
181
  class Shrinkwrap {
213
182
  static get defaultLockfileVersion () {
214
183
  return defaultLockfileVersion
@@ -228,13 +197,18 @@ class Shrinkwrap {
228
197
  const s = new Shrinkwrap(options)
229
198
  s.reset()
230
199
 
231
- const [sw, lock] = await s[_maybeStat]()
200
+ const [sw, lock] = await s.resetFiles
232
201
 
233
- s.filename = resolve(s.path,
234
- (s.hiddenLockfile ? 'node_modules/.package-lock'
235
- : s.shrinkwrapOnly || sw ? 'npm-shrinkwrap'
236
- : 'package-lock') + '.json')
202
+ // XXX this is duplicated in this.load(), but using loadFiles instead of resetFiles
203
+ if (s.hiddenLockfile) {
204
+ s.filename = resolve(s.path, 'node_modules/.package-lock.json')
205
+ } else if (s.shrinkwrapOnly || sw) {
206
+ s.filename = resolve(s.path, 'npm-shrinkwrap.json')
207
+ } else {
208
+ s.filename = resolve(s.path, 'package-lock.json')
209
+ }
237
210
  s.loadedFromDisk = !!(sw || lock)
211
+ // TODO what uses this?
238
212
  s.type = basename(s.filename)
239
213
 
240
214
  return s
@@ -249,12 +223,12 @@ class Shrinkwrap {
249
223
  }
250
224
 
251
225
  const meta = {}
252
- pkgMetaKeys.forEach(key => {
226
+ for (const key of pkgMetaKeys) {
253
227
  const val = metaFieldFromPkg(node.package, key)
254
228
  if (val) {
255
229
  meta[key.replace(/^_/, '')] = val
256
230
  }
257
- })
231
+ }
258
232
  // we only include name if different from the node path name, and for the
259
233
  // root to help prevent churn based on the name of the directory the
260
234
  // project is in
@@ -267,11 +241,11 @@ class Shrinkwrap {
267
241
  meta.devDependencies = node.package.devDependencies
268
242
  }
269
243
 
270
- nodeMetaKeys.forEach(key => {
244
+ for (const key of nodeMetaKeys) {
271
245
  if (node[key]) {
272
246
  meta[key] = node[key]
273
247
  }
274
- })
248
+ }
275
249
 
276
250
  const resolved = consistentResolve(node.resolved, node.path, path, true)
277
251
  // hide resolved from registry dependencies.
@@ -302,6 +276,8 @@ class Shrinkwrap {
302
276
  return meta
303
277
  }
304
278
 
279
+ #awaitingUpdate = new Map()
280
+
305
281
  constructor (options = {}) {
306
282
  const {
307
283
  path,
@@ -313,11 +289,14 @@ class Shrinkwrap {
313
289
  resolveOptions = {},
314
290
  } = options
315
291
 
316
- this.lockfileVersion = hiddenLockfile ? 3
317
- : lockfileVersion ? parseInt(lockfileVersion, 10)
318
- : null
292
+ if (hiddenLockfile) {
293
+ this.lockfileVersion = 3
294
+ } else if (lockfileVersion) {
295
+ this.lockfileVersion = parseInt(lockfileVersion, 10)
296
+ } else {
297
+ this.lockfileVersion = null
298
+ }
319
299
 
320
- this[_awaitingUpdate] = new Map()
321
300
  this.tree = null
322
301
  this.path = resolve(path || '.')
323
302
  this.filename = null
@@ -354,9 +333,12 @@ class Shrinkwrap {
354
333
  // don't use the simple version if the "registry" url is
355
334
  // something else entirely!
356
335
  const tgz = isReg && versionFromTgz(spec.name, resolved) || {}
357
- const yspec = tgz.name === spec.name && tgz.version === version ? version
358
- : isReg && tgz.name && tgz.version ? `npm:${tgz.name}@${tgz.version}`
359
- : resolved
336
+ let yspec = resolved
337
+ if (tgz.name === spec.name && tgz.version === version) {
338
+ yspec = version
339
+ } else if (isReg && tgz.name && tgz.version) {
340
+ yspec = `npm:${tgz.name}@${tgz.version}`
341
+ }
360
342
  if (yspec) {
361
343
  options.resolved = resolved.replace(yarnRegRe, 'https://registry.npmjs.org/')
362
344
  options.integrity = integrity
@@ -370,7 +352,7 @@ class Shrinkwrap {
370
352
  // still worth doing a load() first so we know which files to write.
371
353
  reset () {
372
354
  this.tree = null
373
- this[_awaitingUpdate] = new Map()
355
+ this.#awaitingUpdate = new Map()
374
356
  const lockfileVersion = this.lockfileVersion || defaultLockfileVersion
375
357
  this.originalLockfileVersion = lockfileVersion
376
358
 
@@ -382,58 +364,83 @@ class Shrinkwrap {
382
364
  }
383
365
  }
384
366
 
385
- [_filenameSet] () {
386
- return this.shrinkwrapOnly ? [
387
- this.path + '/npm-shrinkwrap.json',
388
- ] : this.hiddenLockfile ? [
389
- null,
390
- this.path + '/node_modules/.package-lock.json',
391
- ] : [
392
- this.path + '/npm-shrinkwrap.json',
393
- this.path + '/package-lock.json',
394
- this.path + '/yarn.lock',
367
+ // files to potentially read from and write to, in order of priority
368
+ get #filenameSet () {
369
+ if (this.shrinkwrapOnly) {
370
+ return [`${this.path}/npm-shrinkwrap.json`]
371
+ }
372
+ if (this.hiddenLockfile) {
373
+ return [`${this.path}/node_modules/.package-lock.json`]
374
+ }
375
+ return [
376
+ `${this.path}/npm-shrinkwrap.json`,
377
+ `${this.path}/package-lock.json`,
378
+ `${this.path}/yarn.lock`,
395
379
  ]
396
380
  }
397
381
 
398
- [_maybeRead] () {
399
- return Promise.all(this[_filenameSet]().map(fn => fn && maybeReadFile(fn)))
382
+ get loadFiles () {
383
+ return Promise.all(
384
+ this.#filenameSet.map(file => file && readFile(file, 'utf8').then(d => d, er => {
385
+ /* istanbul ignore else - can't test without breaking module itself */
386
+ if (er.code === 'ENOENT') {
387
+ return ''
388
+ } else {
389
+ throw er
390
+ }
391
+ }))
392
+ )
400
393
  }
401
394
 
402
- [_maybeStat] () {
403
- // throw away yarn, we only care about lock or shrinkwrap when checking
395
+ get resetFiles () {
396
+ // slice out yarn, we only care about lock or shrinkwrap when checking
404
397
  // this way, since we're not actually loading the full lock metadata
405
- return Promise.all(this[_filenameSet]().slice(0, 2)
406
- .map(fn => fn && maybeStatFile(fn)))
398
+ return Promise.all(this.#filenameSet.slice(0, 2)
399
+ .map(file => file && stat(file).then(st => st.isFile(), er => {
400
+ /* istanbul ignore else - can't test without breaking module itself */
401
+ if (er.code === 'ENOENT') {
402
+ return null
403
+ } else {
404
+ throw er
405
+ }
406
+ })
407
+ )
408
+ )
407
409
  }
408
410
 
409
411
  inferFormattingOptions (packageJSONData) {
410
- // don't use detect-indent, just pick the first line.
411
- // if the file starts with {" then we have an indent of '', ie, none
412
- // which will default to 2 at save time.
413
412
  const {
414
413
  [Symbol.for('indent')]: indent,
415
414
  [Symbol.for('newline')]: newline,
416
415
  } = packageJSONData
417
- this.indent = indent !== undefined ? indent : this.indent
418
- this.newline = newline !== undefined ? newline : this.newline
416
+ if (indent !== undefined) {
417
+ this.indent = indent
418
+ }
419
+ if (newline !== undefined) {
420
+ this.newline = newline
421
+ }
419
422
  }
420
423
 
421
424
  async load () {
422
425
  // we don't need to load package-lock.json except for top of tree nodes,
423
426
  // only npm-shrinkwrap.json.
424
- return this[_maybeRead]().then(([sw, lock, yarn]) => {
425
- const data = sw || lock || ''
427
+ let data
428
+ try {
429
+ const [sw, lock, yarn] = await this.loadFiles
430
+ data = sw || lock || '{}'
426
431
 
427
432
  // use shrinkwrap only for deps, otherwise prefer package-lock
428
433
  // and ignore npm-shrinkwrap if both are present.
429
434
  // TODO: emit a warning here or something if both are present.
430
- this.filename = resolve(this.path,
431
- (this.hiddenLockfile ? 'node_modules/.package-lock'
432
- : this.shrinkwrapOnly || sw ? 'npm-shrinkwrap'
433
- : 'package-lock') + '.json')
434
-
435
+ if (this.hiddenLockfile) {
436
+ this.filename = resolve(this.path, 'node_modules/.package-lock.json')
437
+ } else if (this.shrinkwrapOnly || sw) {
438
+ this.filename = resolve(this.path, 'npm-shrinkwrap.json')
439
+ } else {
440
+ this.filename = resolve(this.path, 'package-lock.json')
441
+ }
435
442
  this.type = basename(this.filename)
436
- this.loadedFromDisk = !!data
443
+ this.loadedFromDisk = Boolean(sw || lock)
437
444
 
438
445
  if (yarn) {
439
446
  this.yarnLock = new YarnLock()
@@ -445,85 +452,84 @@ class Shrinkwrap {
445
452
  }
446
453
  }
447
454
 
448
- return data ? parseJSON(data) : {}
449
- }).then(async data => {
455
+ data = parseJSON(data)
450
456
  this.inferFormattingOptions(data)
451
457
 
452
- if (!this.hiddenLockfile || !data.packages) {
453
- return data
458
+ if (this.hiddenLockfile && data.packages) {
459
+ // add a few ms just to account for jitter
460
+ const lockTime = +(await stat(this.filename)).mtime + 10
461
+ await assertNoNewer(this.path, data, lockTime, this.path, new Set())
454
462
  }
455
463
 
456
- // add a few ms just to account for jitter
457
- const lockTime = +(await stat(this.filename)).mtime + 10
458
- await assertNoNewer(this.path, data, lockTime)
459
-
460
464
  // all good! hidden lockfile is the newest thing in here.
461
- return data
462
- }).catch(er => {
465
+ } catch (er) {
463
466
  /* istanbul ignore else */
464
467
  if (typeof this.filename === 'string') {
465
468
  const rel = relpath(this.path, this.filename)
466
- log.verbose('shrinkwrap', `failed to load ${rel}`, er)
469
+ log.verbose('shrinkwrap', `failed to load ${rel}`, er.message)
467
470
  } else {
468
- log.verbose('shrinkwrap', `failed to load ${this.path}`, er)
471
+ log.verbose('shrinkwrap', `failed to load ${this.path}`, er.message)
469
472
  }
470
473
  this.loadingError = er
471
474
  this.loadedFromDisk = false
472
475
  this.ancientLockfile = false
473
- return {}
474
- }).then(lock => {
475
- // auto convert v1 lockfiles to v3
476
- // leave v2 in place unless configured
477
- // v3 by default
478
- const lockfileVersion =
479
- this.lockfileVersion ? this.lockfileVersion
480
- : lock.lockfileVersion === 1 ? defaultLockfileVersion
481
- : lock.lockfileVersion || defaultLockfileVersion
482
-
483
- this.data = {
484
- ...lock,
485
- lockfileVersion: lockfileVersion,
486
- requires: true,
487
- packages: lock.packages || {},
488
- dependencies: lock.dependencies || {},
489
- }
476
+ data = {}
477
+ }
478
+ // auto convert v1 lockfiles to v3
479
+ // leave v2 in place unless configured
480
+ // v3 by default
481
+ let lockfileVersion = defaultLockfileVersion
482
+ if (this.lockfileVersion) {
483
+ lockfileVersion = this.lockfileVersion
484
+ } else if (data.lockfileVersion && data.lockfileVersion !== 1) {
485
+ lockfileVersion = data.lockfileVersion
486
+ }
487
+
488
+ this.data = {
489
+ ...data,
490
+ lockfileVersion,
491
+ requires: true,
492
+ packages: data.packages || {},
493
+ dependencies: data.dependencies || {},
494
+ }
490
495
 
491
- this.originalLockfileVersion = lock.lockfileVersion
496
+ this.originalLockfileVersion = data.lockfileVersion
492
497
 
493
- // use default if it wasn't explicitly set, and the current file is
494
- // less than our default. otherwise, keep whatever is in the file,
495
- // unless we had an explicit setting already.
496
- if (!this.lockfileVersion) {
497
- this.lockfileVersion = this.data.lockfileVersion = lockfileVersion
498
- }
499
- this.ancientLockfile = this.loadedFromDisk &&
500
- !(lock.lockfileVersion >= 2) && !lock.requires
501
-
502
- // load old lockfile deps into the packages listing
503
- // eslint-disable-next-line promise/always-return
504
- if (lock.dependencies && !lock.packages) {
505
- return rpj(this.path + '/package.json').then(pkg => pkg, er => ({}))
506
- // eslint-disable-next-line promise/always-return
507
- .then(pkg => {
508
- this[_loadAll]('', null, this.data)
509
- this[_fixDependencies](pkg)
510
- })
498
+ // use default if it wasn't explicitly set, and the current file is
499
+ // less than our default. otherwise, keep whatever is in the file,
500
+ // unless we had an explicit setting already.
501
+ if (!this.lockfileVersion) {
502
+ this.lockfileVersion = this.data.lockfileVersion = lockfileVersion
503
+ }
504
+ this.ancientLockfile = this.loadedFromDisk &&
505
+ !(data.lockfileVersion >= 2) && !data.requires
506
+
507
+ // load old lockfile deps into the packages listing
508
+ if (data.dependencies && !data.packages) {
509
+ let pkg
510
+ try {
511
+ pkg = await pkgJson.normalize(this.path)
512
+ pkg = pkg.content
513
+ } catch {
514
+ pkg = {}
511
515
  }
512
- })
513
- .then(() => this)
516
+ this.#loadAll('', null, this.data)
517
+ this.#fixDependencies(pkg)
518
+ }
519
+ return this
514
520
  }
515
521
 
516
- [_loadAll] (location, name, lock) {
522
+ #loadAll (location, name, lock) {
517
523
  // migrate a v1 package lock to the new format.
518
- const meta = this[_metaFromLock](location, name, lock)
524
+ const meta = this.#metaFromLock(location, name, lock)
519
525
  // dependencies nested under a link are actually under the link target
520
526
  if (meta.link) {
521
527
  location = meta.resolved
522
528
  }
523
529
  if (lock.dependencies) {
524
- for (const [name, dep] of Object.entries(lock.dependencies)) {
530
+ for (const name in lock.dependencies) {
525
531
  const loc = location + (location ? '/' : '') + 'node_modules/' + name
526
- this[_loadAll](loc, name, dep)
532
+ this.#loadAll(loc, name, lock.dependencies[name])
527
533
  }
528
534
  }
529
535
  }
@@ -531,20 +537,20 @@ class Shrinkwrap {
531
537
  // v1 lockfiles track the optional/dev flags, but they don't tell us
532
538
  // which thing had what kind of dep on what other thing, so we need
533
539
  // to correct that now, or every link will be considered prod
534
- [_fixDependencies] (pkg) {
540
+ #fixDependencies (pkg) {
535
541
  // we need the root package.json because legacy shrinkwraps just
536
542
  // have requires:true at the root level, which is even less useful
537
543
  // than merging all dep types into one object.
538
544
  const root = this.data.packages['']
539
- pkgMetaKeys.forEach(key => {
545
+ for (const key of pkgMetaKeys) {
540
546
  const val = metaFieldFromPkg(pkg, key)
541
- const k = key.replace(/^_/, '')
542
547
  if (val) {
543
- root[k] = val
548
+ root[key.replace(/^_/, '')] = val
544
549
  }
545
- })
550
+ }
546
551
 
547
- for (const [loc, meta] of Object.entries(this.data.packages)) {
552
+ for (const loc in this.data.packages) {
553
+ const meta = this.data.packages[loc]
548
554
  if (!meta.requires || !loc) {
549
555
  continue
550
556
  }
@@ -555,25 +561,30 @@ class Shrinkwrap {
555
561
  // This isn't perfect, but it's a pretty good approximation, and at
556
562
  // least gets us out of having all 'prod' edges, which throws off the
557
563
  // buildIdealTree process
558
- for (const [name, spec] of Object.entries(meta.requires)) {
559
- const dep = this[_resolveMetaNode](loc, name)
564
+ for (const name in meta.requires) {
565
+ const dep = this.#resolveMetaNode(loc, name)
560
566
  // this overwrites the false value set above
561
- const depType = dep && dep.optional && !meta.optional
562
- ? 'optionalDependencies'
563
- : /* istanbul ignore next - dev deps are only for the root level */
564
- dep && dep.dev && !meta.dev ? 'devDependencies'
565
- // also land here if the dep just isn't in the tree, which maybe
566
- // should be an error, since it means that the shrinkwrap is
567
- // invalid, but we can't do much better without any info.
568
- : 'dependencies'
569
- meta[depType] = meta[depType] || {}
570
- meta[depType][name] = spec
567
+ // default to dependencies if the dep just isn't in the tree, which
568
+ // maybe should be an error, since it means that the shrinkwrap is
569
+ // invalid, but we can't do much better without any info.
570
+ let depType = 'dependencies'
571
+ /* istanbul ignore else - dev deps are only for the root level */
572
+ if (dep?.optional && !meta.optional) {
573
+ depType = 'optionalDependencies'
574
+ } else if (dep?.dev && !meta.dev) {
575
+ // XXX is this even reachable?
576
+ depType = 'devDependencies'
577
+ }
578
+ if (!meta[depType]) {
579
+ meta[depType] = {}
580
+ }
581
+ meta[depType][name] = meta.requires[name]
571
582
  }
572
583
  delete meta.requires
573
584
  }
574
585
  }
575
586
 
576
- [_resolveMetaNode] (loc, name) {
587
+ #resolveMetaNode (loc, name) {
577
588
  for (let path = loc; true; path = path.replace(/(^|\/)[^/]*$/, '')) {
578
589
  const check = `${path}${path ? '/' : ''}node_modules/${name}`
579
590
  if (this.data.packages[check]) {
@@ -587,7 +598,7 @@ class Shrinkwrap {
587
598
  return null
588
599
  }
589
600
 
590
- [_lockFromLoc] (lock, path, i = 0) {
601
+ #lockFromLoc (lock, path, i = 0) {
591
602
  if (!lock) {
592
603
  return null
593
604
  }
@@ -604,12 +615,12 @@ class Shrinkwrap {
604
615
  return null
605
616
  }
606
617
 
607
- return this[_lockFromLoc](lock.dependencies[path[i]], path, i + 1)
618
+ return this.#lockFromLoc(lock.dependencies[path[i]], path, i + 1)
608
619
  }
609
620
 
610
621
  // pass in a path relative to the root path, or an absolute path,
611
622
  // get back a /-normalized location based on root path.
612
- [_pathToLoc] (path) {
623
+ #pathToLoc (path) {
613
624
  return relpath(this.path, resolve(this.path, path))
614
625
  }
615
626
 
@@ -617,13 +628,13 @@ class Shrinkwrap {
617
628
  if (!this.data) {
618
629
  throw new Error('run load() before getting or setting data')
619
630
  }
620
- const location = this[_pathToLoc](nodePath)
621
- this[_awaitingUpdate].delete(location)
631
+ const location = this.#pathToLoc(nodePath)
632
+ this.#awaitingUpdate.delete(location)
622
633
 
623
634
  delete this.data.packages[location]
624
635
  const path = location.split(/(?:^|\/)node_modules\//)
625
636
  const name = path.pop()
626
- const pLock = this[_lockFromLoc](this.data, path)
637
+ const pLock = this.#lockFromLoc(this.data, path)
627
638
  if (pLock && pLock.dependencies) {
628
639
  delete pLock.dependencies[name]
629
640
  }
@@ -634,9 +645,9 @@ class Shrinkwrap {
634
645
  throw new Error('run load() before getting or setting data')
635
646
  }
636
647
 
637
- const location = this[_pathToLoc](nodePath)
638
- if (this[_awaitingUpdate].has(location)) {
639
- this[_updateWaitingNode](location)
648
+ const location = this.#pathToLoc(nodePath)
649
+ if (this.#awaitingUpdate.has(location)) {
650
+ this.#updateWaitingNode(location)
640
651
  }
641
652
 
642
653
  // first try to get from the newer spot, which we know has
@@ -649,12 +660,12 @@ class Shrinkwrap {
649
660
  // get the node in the shrinkwrap corresponding to this spot
650
661
  const path = location.split(/(?:^|\/)node_modules\//)
651
662
  const name = path[path.length - 1]
652
- const lock = this[_lockFromLoc](this.data, path)
663
+ const lock = this.#lockFromLoc(this.data, path)
653
664
 
654
- return this[_metaFromLock](location, name, lock)
665
+ return this.#metaFromLock(location, name, lock)
655
666
  }
656
667
 
657
- [_metaFromLock] (location, name, lock) {
668
+ #metaFromLock (location, name, lock) {
658
669
  // This function tries as hard as it can to figure out the metadata
659
670
  // from a lockfile which may be outdated or incomplete. Since v1
660
671
  // lockfiles used the "version" field to contain a variety of
@@ -679,7 +690,7 @@ class Shrinkwrap {
679
690
  // also save the link target, omitting version since we don't know
680
691
  // what it is, but we know it isn't a link to itself!
681
692
  if (!this.data.packages[target]) {
682
- this[_metaFromLock](target, name, { ...lock, version: null })
693
+ this.#metaFromLock(target, name, { ...lock, version: null })
683
694
  }
684
695
  return this.data.packages[location]
685
696
  }
@@ -799,10 +810,14 @@ class Shrinkwrap {
799
810
  version,
800
811
  } = this.get(node.path)
801
812
 
802
- const pathFixed = !resolved ? null
803
- : !/^file:/.test(resolved) ? resolved
804
- // resolve onto the metadata path
805
- : `file:${resolve(this.path, resolved.slice(5)).replace(/#/g, '%23')}`
813
+ let pathFixed = null
814
+ if (resolved) {
815
+ if (!/^file:/.test(resolved)) {
816
+ pathFixed = resolved
817
+ } else {
818
+ pathFixed = `file:${resolve(this.path, resolved.slice(5)).replace(/#/g, '%23')}`
819
+ }
820
+ }
806
821
 
807
822
  // if we have one, only set the other if it matches
808
823
  // otherwise it could be for a completely different thing.
@@ -831,7 +846,7 @@ class Shrinkwrap {
831
846
  node.hasShrinkwrap = node.hasShrinkwrap || hasShrinkwrap || false
832
847
  }
833
848
  }
834
- this[_awaitingUpdate].set(loc, node)
849
+ this.#awaitingUpdate.set(loc, node)
835
850
  }
836
851
 
837
852
  addEdge (edge) {
@@ -852,10 +867,15 @@ class Shrinkwrap {
852
867
  }
853
868
 
854
869
  // we relativize the path here because that's how it shows up in the lock
855
- // XXX how is this different from pathFixed above??
856
- const pathFixed = !node.resolved ? null
857
- : !/file:/.test(node.resolved) ? node.resolved
858
- : consistentResolve(node.resolved, node.path, this.path, true)
870
+ // XXX why is this different from pathFixed in this.add??
871
+ let pathFixed = null
872
+ if (node.resolved) {
873
+ if (!/file:/.test(node.resolved)) {
874
+ pathFixed = node.resolved
875
+ } else {
876
+ pathFixed = consistentResolve(node.resolved, node.path, this.path, true)
877
+ }
878
+ }
859
879
 
860
880
  const spec = npa(`${node.name}@${edge.spec}`)
861
881
  const entry = this.yarnLock.entries.get(`${node.name}@${edge.spec}`)
@@ -875,12 +895,12 @@ class Shrinkwrap {
875
895
  node.resolved = node.resolved ||
876
896
  consistentResolve(entry.resolved, this.path, node.path) || null
877
897
 
878
- this[_awaitingUpdate].set(relpath(this.path, node.path), node)
898
+ this.#awaitingUpdate.set(relpath(this.path, node.path), node)
879
899
  }
880
900
 
881
- [_updateWaitingNode] (loc) {
882
- const node = this[_awaitingUpdate].get(loc)
883
- this[_awaitingUpdate].delete(loc)
901
+ #updateWaitingNode (loc) {
902
+ const node = this.#awaitingUpdate.get(loc)
903
+ this.#awaitingUpdate.delete(loc)
884
904
  this.data.packages[loc] = Shrinkwrap.metaFromNode(
885
905
  node,
886
906
  this.path,
@@ -911,9 +931,9 @@ class Shrinkwrap {
911
931
  this.path,
912
932
  this.resolveOptions)
913
933
  }
914
- } else if (this[_awaitingUpdate].size > 0) {
915
- for (const loc of this[_awaitingUpdate].keys()) {
916
- this[_updateWaitingNode](loc)
934
+ } else if (this.#awaitingUpdate.size > 0) {
935
+ for (const loc of this.#awaitingUpdate.keys()) {
936
+ this.#updateWaitingNode(loc)
917
937
  }
918
938
  }
919
939
 
@@ -928,7 +948,7 @@ class Shrinkwrap {
928
948
  delete this.data.packages['']
929
949
  delete this.data.dependencies
930
950
  } else if (this.tree && this.lockfileVersion <= 3) {
931
- this[_buildLegacyLockfile](this.tree, this.data)
951
+ this.#buildLegacyLockfile(this.tree, this.data)
932
952
  }
933
953
 
934
954
  // lf version 1 = dependencies only
@@ -945,7 +965,7 @@ class Shrinkwrap {
945
965
  }
946
966
  }
947
967
 
948
- [_buildLegacyLockfile] (node, lock, path = []) {
968
+ #buildLegacyLockfile (node, lock, path = []) {
949
969
  if (node === this.tree) {
950
970
  // the root node
951
971
  lock.name = node.packageName || node.name
@@ -966,9 +986,13 @@ class Shrinkwrap {
966
986
  const aloc = a.from.location.split('node_modules')
967
987
  const bloc = b.from.location.split('node_modules')
968
988
  /* istanbul ignore next - sort calling order is indeterminate */
969
- return aloc.length > bloc.length ? 1
970
- : bloc.length > aloc.length ? -1
971
- : localeCompare(aloc[aloc.length - 1], bloc[bloc.length - 1])
989
+ if (aloc.length > bloc.length) {
990
+ return 1
991
+ }
992
+ if (bloc.length > aloc.length) {
993
+ return -1
994
+ }
995
+ return localeCompare(aloc[aloc.length - 1], bloc[bloc.length - 1])
972
996
  })[0]
973
997
 
974
998
  const res = consistentResolve(node.resolved, this.path, this.path, true)
@@ -979,8 +1003,10 @@ class Shrinkwrap {
979
1003
  // if we don't have either, just an empty object so nothing matches below.
980
1004
  // This will effectively just save the version and resolved, as if it's
981
1005
  // a standard version/range dep, which is a reasonable default.
982
- const spec = !edge ? rSpec
983
- : npa.resolve(node.name, edge.spec, edge.from.realpath)
1006
+ let spec = rSpec
1007
+ if (edge) {
1008
+ spec = npa.resolve(node.name, edge.spec, edge.from.realpath)
1009
+ }
984
1010
 
985
1011
  if (node.isLink) {
986
1012
  lock.version = `file:${relpath(this.path, node.realpath).replace(/#/g, '%23')}`
@@ -1086,7 +1112,7 @@ class Shrinkwrap {
1086
1112
  if (path.includes(kid.realpath)) {
1087
1113
  continue
1088
1114
  }
1089
- dependencies[name] = this[_buildLegacyLockfile](kid, {}, kidPath)
1115
+ dependencies[name] = this.#buildLegacyLockfile(kid, {}, kidPath)
1090
1116
  found = true
1091
1117
  }
1092
1118
  if (found) {
package/lib/tree-check.js CHANGED
@@ -90,7 +90,7 @@ const checkTree = (tree, checkUnreachable = true) => {
90
90
  })
91
91
  }
92
92
 
93
- if (node.path === tree.root.path && node !== tree.root) {
93
+ if (node.path === tree.root.path && node !== tree.root && !tree.root.isLink) {
94
94
  throw Object.assign(new Error('node with same path as root'), {
95
95
  node: node.path,
96
96
  tree: tree.path,
package/lib/yarn-lock.js CHANGED
@@ -341,10 +341,10 @@ class YarnLock {
341
341
  }
342
342
  }
343
343
 
344
- const _specs = Symbol('_specs')
345
344
  class YarnLockEntry {
345
+ #specs
346
346
  constructor (specs) {
347
- this[_specs] = new Set(specs)
347
+ this.#specs = new Set(specs)
348
348
  this.resolved = null
349
349
  this.version = null
350
350
  this.integrity = null
@@ -354,7 +354,7 @@ class YarnLockEntry {
354
354
 
355
355
  toString () {
356
356
  // sort objects to the bottom, then alphabetical
357
- return ([...this[_specs]]
357
+ return ([...this.#specs]
358
358
  .sort(localeCompare)
359
359
  .map(quoteIfNeeded).join(', ') +
360
360
  ':\n' +
@@ -370,7 +370,7 @@ class YarnLockEntry {
370
370
  }
371
371
 
372
372
  addSpec (spec) {
373
- this[_specs].add(spec)
373
+ this.#specs.add(spec)
374
374
  }
375
375
  }
376
376
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "7.2.1",
3
+ "version": "7.2.2",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@npmcli/eslint-config": "^4.0.0",
42
- "@npmcli/template-oss": "4.19.0",
42
+ "@npmcli/template-oss": "4.21.3",
43
43
  "benchmark": "^2.1.4",
44
44
  "minify-registry-metadata": "^3.0.0",
45
45
  "nock": "^13.3.3",
@@ -49,11 +49,11 @@
49
49
  },
50
50
  "scripts": {
51
51
  "test": "tap",
52
- "posttest": "node ../.. run lint",
52
+ "posttest": "npm run lint",
53
53
  "snap": "tap",
54
54
  "test-proxy": "ARBORIST_TEST_PROXY=1 tap --snapshot",
55
- "lint": "eslint \"**/*.js\"",
56
- "lintfix": "node ../.. run lint -- --fix",
55
+ "lint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\"",
56
+ "lintfix": "npm run lint -- --fix",
57
57
  "benchmark": "node scripts/benchmark.js",
58
58
  "benchclean": "rm -rf scripts/benchmark/*/",
59
59
  "postlint": "template-oss-check",
@@ -90,7 +90,7 @@
90
90
  },
91
91
  "templateOSS": {
92
92
  "//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
93
- "version": "4.19.0",
93
+ "version": "4.21.3",
94
94
  "content": "../../scripts/template-oss/index.js"
95
95
  }
96
96
  }