@npmcli/arborist 3.0.0 → 4.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.
package/bin/shrinkwrap.js CHANGED
@@ -5,7 +5,7 @@ require('./lib/timers.js')
5
5
 
6
6
  const { quiet } = options
7
7
  Shrinkwrap.load(options)
8
- .then(s => quiet || console.log(JSON.stringify(s.data, 0, 2)))
8
+ .then(s => quiet || console.log(JSON.stringify(s.commit(), 0, 2)))
9
9
  .catch(er => {
10
10
  console.error('shrinkwrap load failure', er)
11
11
  process.exit(1)
@@ -26,6 +26,7 @@ const debug = require('../debug.js')
26
26
  const fromPath = require('../from-path.js')
27
27
  const calcDepFlags = require('../calc-dep-flags.js')
28
28
  const Shrinkwrap = require('../shrinkwrap.js')
29
+ const { defaultLockfileVersion } = Shrinkwrap
29
30
  const Node = require('../node.js')
30
31
  const Link = require('../link.js')
31
32
  const addRmPkgDeps = require('../add-rm-pkg-deps.js')
@@ -307,8 +308,10 @@ module.exports = cls => class IdealTreeBuilder extends cls {
307
308
  // reconstructing it anyway.
308
309
  .then(root => this[_global] ? root
309
310
  : !this[_usePackageLock] || this[_updateAll]
310
- ? Shrinkwrap.reset({ path: this.path })
311
- .then(meta => Object.assign(root, {meta}))
311
+ ? Shrinkwrap.reset({
312
+ path: this.path,
313
+ lockfileVersion: this.options.lockfileVersion,
314
+ }).then(meta => Object.assign(root, {meta}))
312
315
  : this.loadVirtual({ root }))
313
316
 
314
317
  // if we don't have a lockfile to go from, then start with the
@@ -326,6 +329,9 @@ module.exports = cls => class IdealTreeBuilder extends cls {
326
329
  // dep flags before assuming that any mutations were reflected.
327
330
  if (tree.children.size) {
328
331
  root.meta.loadedFromDisk = true
332
+ // set these so that we don't try to ancient lockfile reload it
333
+ root.meta.originalLockfileVersion = defaultLockfileVersion
334
+ root.meta.lockfileVersion = defaultLockfileVersion
329
335
  }
330
336
  }
331
337
  return root
@@ -345,7 +351,10 @@ module.exports = cls => class IdealTreeBuilder extends cls {
345
351
  // this is a gross kludge to handle the fact that we don't save
346
352
  // metadata on the root node in global installs, because the "root"
347
353
  // node is something like /usr/local/lib.
348
- const meta = new Shrinkwrap({ path: this.path })
354
+ const meta = new Shrinkwrap({
355
+ path: this.path,
356
+ lockfileVersion: this.options.lockfileVersion,
357
+ })
349
358
  meta.reset()
350
359
  root.meta = meta
351
360
  return root
@@ -696,14 +705,18 @@ module.exports = cls => class IdealTreeBuilder extends cls {
696
705
  // least it's just a one-time hit.
697
706
  process.emit('time', 'idealTree:inflate')
698
707
 
708
+ // don't warn if we're not gonna actually write it back anyway.
699
709
  const heading = ancient ? 'ancient lockfile' : 'old lockfile'
700
- this.log.warn(heading,
701
- `
710
+ if (ancient || !this.options.lockfileVersion ||
711
+ this.options.lockfileVersion >= defaultLockfileVersion) {
712
+ this.log.warn(heading,
713
+ `
702
714
  The ${meta.type} file was created with an old version of npm,
703
715
  so supplemental metadata must be fetched from the registry.
704
716
 
705
717
  This is a one-time fix-up, please be patient...
706
718
  `)
719
+ }
707
720
 
708
721
  this.addTracker('idealTree:inflate')
709
722
  const queue = []
@@ -748,7 +761,7 @@ This is a one-time fix-up, please be patient...
748
761
  // yes, yes, this isn't the "original" version, but now that it's been
749
762
  // upgraded, we need to make sure we don't do the work to upgrade it
750
763
  // again, since it's now as new as can be.
751
- meta.originalLockfileVersion = 2
764
+ meta.originalLockfileVersion = defaultLockfileVersion
752
765
  this.finishTracker('idealTree:inflate')
753
766
  process.emit('timeEnd', 'idealTree:inflate')
754
767
  }
@@ -880,7 +893,7 @@ This is a one-time fix-up, please be patient...
880
893
  const tasks = []
881
894
  const peerSource = this[_peerSetSource].get(node) || node
882
895
  for (const edge of this[_problemEdges](node)) {
883
- if (edge.overridden) {
896
+ if (edge.peerConflicted) {
884
897
  continue
885
898
  }
886
899
 
@@ -967,8 +980,8 @@ This is a one-time fix-up, please be patient...
967
980
  if (edgeIn === edge) {
968
981
  continue
969
982
  }
970
- const { from, valid, overridden } = edgeIn
971
- if (!overridden && !valid && !this[_depsSeen].has(from)) {
983
+ const { from, valid, peerConflicted } = edgeIn
984
+ if (!peerConflicted && !valid && !this[_depsSeen].has(from)) {
972
985
  this.addTracker('idealTree', from.name, from.location)
973
986
  this[_depsQueue].push(edgeIn.from)
974
987
  }
@@ -984,8 +997,8 @@ This is a one-time fix-up, please be patient...
984
997
  continue
985
998
  }
986
999
 
987
- const { valid, overridden } = edgeIn
988
- if (!valid && !overridden) {
1000
+ const { valid, peerConflicted } = edgeIn
1001
+ if (!valid && !peerConflicted) {
989
1002
  // if it's already been visited, we have to re-visit
990
1003
  // otherwise, just enqueue normally.
991
1004
  this[_depsSeen].delete(edgeIn.from)
@@ -1301,7 +1314,7 @@ This is a one-time fix-up, please be patient...
1301
1314
  // that will be installed by default anyway, and we'll fail when
1302
1315
  // we get to the point where we need to, if we need to.
1303
1316
  if (conflictOK || !required.has(dep)) {
1304
- edge.overridden = true
1317
+ edge.peerConflicted = true
1305
1318
  continue
1306
1319
  }
1307
1320
 
@@ -44,9 +44,25 @@ const mixins = [
44
44
  require('./reify.js'),
45
45
  ]
46
46
 
47
+ const _workspacesEnabled = Symbol.for('workspacesEnabled')
47
48
  const Base = mixins.reduce((a, b) => b(a), require('events'))
48
49
  const getWorkspaceNodes = require('../get-workspace-nodes.js')
49
50
 
51
+ // if it's 1, 2, or 3, set it explicitly that.
52
+ // if undefined or null, set it null
53
+ // otherwise, throw.
54
+ const lockfileVersion = lfv => {
55
+ if (lfv === 1 || lfv === 2 || lfv === 3) {
56
+ return lfv
57
+ }
58
+
59
+ if (lfv === undefined || lfv === null) {
60
+ return null
61
+ }
62
+
63
+ throw new TypeError('Invalid lockfileVersion config: ' + lfv)
64
+ }
65
+
50
66
  class Arborist extends Base {
51
67
  constructor (options = {}) {
52
68
  process.emit('time', 'arborist:ctor')
@@ -59,7 +75,11 @@ class Arborist extends Base {
59
75
  packumentCache: options.packumentCache || new Map(),
60
76
  log: options.log || procLog,
61
77
  workspacesEnabled: options.workspacesEnabled !== false,
78
+ lockfileVersion: lockfileVersion(options.lockfileVersion),
62
79
  }
80
+
81
+ this[_workspacesEnabled] = this.options.workspacesEnabled
82
+
63
83
  if (options.saveType && !saveTypeMap.get(options.saveType)) {
64
84
  throw new Error(`Invalid saveType ${options.saveType}`)
65
85
  }
@@ -137,11 +137,9 @@ module.exports = cls => class ActualLoader extends cls {
137
137
  real: await realpath(this.path, this[_rpcache], this[_stcache]),
138
138
  })
139
139
 
140
- // XXX only rely on this if the hidden lockfile is the newest thing?
141
- // need some kind of heuristic, like if the package.json or sw have
142
- // been touched more recently, then ignore it? This is a hazard if
143
- // user switches back and forth between Arborist and another way of
144
- // mutating the node_modules folder.
140
+ // Note: hidden lockfile will be rejected if it's not the latest thing
141
+ // in the folder, or if any of the entries in the hidden lockfile are
142
+ // missing.
145
143
  const meta = await Shrinkwrap.load({
146
144
  path: this[_actualTree].path,
147
145
  hiddenLockfile: true,
@@ -152,6 +150,7 @@ module.exports = cls => class ActualLoader extends cls {
152
150
  } else {
153
151
  const meta = await Shrinkwrap.load({
154
152
  path: this[_actualTree].path,
153
+ lockfileVersion: this.options.lockfileVersion,
155
154
  })
156
155
  this[_actualTree].meta = meta
157
156
  return this[_loadActualActually]({ root, ignoreMissing })
@@ -54,7 +54,10 @@ module.exports = cls => class VirtualLoader extends cls {
54
54
  return treeCheck(this.virtualTree)
55
55
  }
56
56
 
57
- const s = await Shrinkwrap.load({ path: this.path })
57
+ const s = await Shrinkwrap.load({
58
+ path: this.path,
59
+ lockfileVersion: this.options.lockfileVersion,
60
+ })
58
61
  if (!s.loadedFromDisk && !options.root) {
59
62
  const er = new Error('loadVirtual requires existing shrinkwrap file')
60
63
  throw Object.assign(er, { code: 'ENOLOCK' })
@@ -1,4 +1,6 @@
1
1
  const _idealTreePrune = Symbol.for('idealTreePrune')
2
+ const _workspacesEnabled = Symbol.for('workspacesEnabled')
3
+ const _addNodeToTrashList = Symbol.for('addNodeToTrashList')
2
4
 
3
5
  module.exports = cls => class Pruner extends cls {
4
6
  async prune (options = {}) {
@@ -10,6 +12,19 @@ module.exports = cls => class Pruner extends cls {
10
12
 
11
13
  this[_idealTreePrune]()
12
14
 
15
+ if (!this[_workspacesEnabled]) {
16
+ const excludeNodes = this.excludeWorkspacesDependencySet(this.idealTree)
17
+ for (const node of this.idealTree.inventory.values()) {
18
+ if (
19
+ node.parent !== null
20
+ && !node.isProjectRoot
21
+ && !excludeNodes.has(node)
22
+ ) {
23
+ this[_addNodeToTrashList](node)
24
+ }
25
+ }
26
+ }
27
+
13
28
  return this.reify(options)
14
29
  }
15
30
  }
@@ -35,6 +35,7 @@ const _checkBins = Symbol.for('checkBins')
35
35
  const _queues = Symbol('queues')
36
36
  const _scriptShell = Symbol('scriptShell')
37
37
  const _includeWorkspaceRoot = Symbol.for('includeWorkspaceRoot')
38
+ const _workspacesEnabled = Symbol.for('workspacesEnabled')
38
39
 
39
40
  const _force = Symbol.for('force')
40
41
 
@@ -77,8 +78,14 @@ module.exports = cls => class Builder extends cls {
77
78
  // the actual tree on disk.
78
79
  if (!nodes) {
79
80
  const tree = await this.loadActual()
80
- if (this[_workspaces] && this[_workspaces].length) {
81
- const filterSet = this.workspaceDependencySet(
81
+ let filterSet
82
+ if (!this[_workspacesEnabled]) {
83
+ filterSet = this.excludeWorkspacesDependencySet(tree)
84
+ nodes = tree.inventory.filter(node =>
85
+ filterSet.has(node) || node.isProjectRoot
86
+ )
87
+ } else if (this[_workspaces] && this[_workspaces].length) {
88
+ filterSet = this.workspaceDependencySet(
82
89
  tree,
83
90
  this[_workspaces],
84
91
  this[_includeWorkspaceRoot]
@@ -40,8 +40,9 @@ const _savePrefix = Symbol('savePrefix')
40
40
  const _retireShallowNodes = Symbol.for('retireShallowNodes')
41
41
  const _getBundlesByDepth = Symbol('getBundlesByDepth')
42
42
  const _registryResolved = Symbol('registryResolved')
43
- const _addNodeToTrashList = Symbol('addNodeToTrashList')
43
+ const _addNodeToTrashList = Symbol.for('addNodeToTrashList')
44
44
  const _workspaces = Symbol.for('workspaces')
45
+ const _workspacesEnabled = Symbol.for('workspacesEnabled')
45
46
 
46
47
  // shared by rebuild mixin
47
48
  const _trashList = Symbol.for('trashList')
@@ -314,6 +315,10 @@ module.exports = cls => class Reifier extends cls {
314
315
  // to just invalidate the parts that changed, but avoid walking the
315
316
  // whole tree again.
316
317
 
318
+ const includeWorkspaces = this[_workspacesEnabled]
319
+ const includeRootDeps = !this[_workspacesEnabled]
320
+ || this[_includeWorkspaceRoot] && this[_workspaces].length > 0
321
+
317
322
  const filterNodes = []
318
323
  if (this[_global] && this.explicitRequests.size) {
319
324
  const idealTree = this.idealTree.target
@@ -331,17 +336,21 @@ module.exports = cls => class Reifier extends cls {
331
336
  }
332
337
  }
333
338
  } else {
334
- for (const ws of this[_workspaces]) {
335
- const ideal = this.idealTree.children.get(ws)
336
- if (ideal) {
337
- filterNodes.push(ideal)
338
- }
339
- const actual = this.actualTree.children.get(ws)
340
- if (actual) {
341
- filterNodes.push(actual)
339
+ if (includeWorkspaces) {
340
+ // add all ws nodes to filterNodes
341
+ for (const ws of this[_workspaces]) {
342
+ const ideal = this.idealTree.children.get(ws)
343
+ if (ideal) {
344
+ filterNodes.push(ideal)
345
+ }
346
+ const actual = this.actualTree.children.get(ws)
347
+ if (actual) {
348
+ filterNodes.push(actual)
349
+ }
342
350
  }
343
351
  }
344
- if (this[_includeWorkspaceRoot] && (this[_workspaces].length > 0)) {
352
+ if (includeRootDeps) {
353
+ // add all non-workspace nodes to filterNodes
345
354
  for (const tree of [this.idealTree, this.actualTree]) {
346
355
  for (const {type, to} of tree.edgesOut.values()) {
347
356
  if (type !== 'workspace' && to) {
@@ -451,10 +460,12 @@ module.exports = cls => class Reifier extends cls {
451
460
 
452
461
  const filter = node =>
453
462
  node.top.isProjectRoot &&
454
- (node.peer && this[_omitPeer] ||
463
+ (
464
+ node.peer && this[_omitPeer] ||
455
465
  node.dev && this[_omitDev] ||
456
466
  node.optional && this[_omitOptional] ||
457
- node.devOptional && this[_omitOptional] && this[_omitDev])
467
+ node.devOptional && this[_omitOptional] && this[_omitDev]
468
+ )
458
469
 
459
470
  for (const node of this.idealTree.inventory.filter(filter)) {
460
471
  this[_addNodeToTrashList](node)
@@ -145,7 +145,12 @@ class CanPlaceDep {
145
145
  return CONFLICT
146
146
  }
147
147
 
148
- if (targetEdge && !dep.satisfies(targetEdge) && targetEdge !== this.edge) {
148
+ // skip this test if there's a current node, because we might be able
149
+ // to dedupe against it anyway
150
+ if (!current &&
151
+ targetEdge &&
152
+ !dep.satisfies(targetEdge) &&
153
+ targetEdge !== this.edge) {
149
154
  return CONFLICT
150
155
  }
151
156
 
@@ -167,10 +172,10 @@ class CanPlaceDep {
167
172
  const { version: newVer } = dep
168
173
  const tryReplace = curVer && newVer && semver.gte(newVer, curVer)
169
174
  if (tryReplace && dep.canReplace(current)) {
170
- /* XXX-istanbul ignore else - It's extremely rare that a replaceable
171
- * node would be a conflict, if the current one wasn't a conflict,
172
- * but it is theoretically possible if peer deps are pinned. In
173
- * that case we treat it like any other conflict, and keep trying */
175
+ // It's extremely rare that a replaceable node would be a conflict, if
176
+ // the current one wasn't a conflict, but it is theoretically possible
177
+ // if peer deps are pinned. In that case we treat it like any other
178
+ // conflict, and keep trying.
174
179
  const cpp = this.canPlacePeers(REPLACE)
175
180
  if (cpp !== CONFLICT) {
176
181
  return cpp
package/lib/edge.js CHANGED
@@ -37,7 +37,7 @@ const printableEdge = (edge) => {
37
37
  ...(edgeFrom != null ? { from: edgeFrom } : {}),
38
38
  ...(edgeTo ? { to: edgeTo } : {}),
39
39
  ...(edge.error ? { error: edge.error } : {}),
40
- ...(edge.overridden ? { overridden: true } : {}),
40
+ ...(edge.peerConflicted ? { peerConflicted: true } : {}),
41
41
  })
42
42
  }
43
43
 
@@ -78,7 +78,7 @@ class Edge {
78
78
  }
79
79
  this[_setFrom](from)
80
80
  this[_error] = this[_loadError]()
81
- this.overridden = false
81
+ this.peerConflicted = false
82
82
  }
83
83
 
84
84
  satisfiedBy (node) {
@@ -29,7 +29,7 @@ const peerEntrySets = node => {
29
29
  for (const peer of unionSet) {
30
30
  for (const edge of peer.edgesIn) {
31
31
  // if not valid, it doesn't matter anyway. either it's been previously
32
- // overridden, or it's the thing we're interested in replacing.
32
+ // peerConflicted, or it's the thing we're interested in replacing.
33
33
  if (!edge.valid) {
34
34
  continue
35
35
  }
package/lib/place-dep.js CHANGED
@@ -247,7 +247,7 @@ class PlaceDep {
247
247
  // if we're placing in the tree with --force, we can get here even though
248
248
  // it's a conflict. Treat it as a KEEP, but warn and move on.
249
249
  if (placementType === KEEP) {
250
- // this was an overridden peer dep
250
+ // this was a peerConflicted peer dep
251
251
  if (edge.peer && !edge.valid) {
252
252
  this.warnPeerConflict()
253
253
  }
@@ -305,7 +305,7 @@ class PlaceDep {
305
305
  this.placed.parent = target
306
306
  }
307
307
 
308
- // if it's an overridden peer dep, warn about it
308
+ // if it's a peerConflicted peer dep, warn about it
309
309
  if (edge.peer && !this.placed.satisfies(edge)) {
310
310
  this.warnPeerConflict()
311
311
  }
@@ -339,7 +339,7 @@ class PlaceDep {
339
339
  // otherwise they'd be gone and the peer set would change throughout
340
340
  // this loop.
341
341
  for (const peerEdge of this.placed.edgesOut.values()) {
342
- if (peerEdge.valid || !peerEdge.peer || peerEdge.overridden) {
342
+ if (peerEdge.valid || !peerEdge.peer || peerEdge.peerConflicted) {
343
343
  continue
344
344
  }
345
345
 
@@ -353,7 +353,7 @@ class PlaceDep {
353
353
  continue
354
354
  }
355
355
 
356
- // overridden peerEdge, just accept what's there already
356
+ // peerConflicted peerEdge, just accept what's there already
357
357
  if (!peer.satisfies(peerEdge)) {
358
358
  continue
359
359
  }
@@ -398,7 +398,7 @@ class PlaceDep {
398
398
  if (this.placed.satisfies(edge) ||
399
399
  !edge.peer ||
400
400
  edge.from.parent !== target ||
401
- edge.overridden) {
401
+ edge.peerConflicted) {
402
402
  // not a peer dep, not invalid, or not from this level, so it's fine
403
403
  // to just let it re-evaluate as a problemEdge later, or let it be
404
404
  // satisfied by the new dep being placed.
@@ -406,14 +406,14 @@ class PlaceDep {
406
406
  }
407
407
  for (const entryEdge of peerEntrySets(edge.from).keys()) {
408
408
  // either this one needs to be pruned and re-evaluated, or marked
409
- // as overridden and warned about. If the entryEdge comes in from
409
+ // as peerConflicted and warned about. If the entryEdge comes in from
410
410
  // the root, then we have to leave it alone, and in that case, it
411
411
  // will have already warned or crashed by getting to this point.
412
412
  const entryNode = entryEdge.to
413
413
  const deepestTarget = deepestNestingTarget(entryNode)
414
414
  if (deepestTarget !== target && !entryEdge.from.isRoot) {
415
415
  prunePeerSets.push(...gatherDepSet([entryNode], e => {
416
- return e.to !== entryNode && !e.overridden
416
+ return e.to !== entryNode && !e.peerConflicted
417
417
  }))
418
418
  } else {
419
419
  this.warnPeerConflict(edge, this.dep)
@@ -532,7 +532,7 @@ class PlaceDep {
532
532
  warnPeerConflict (edge, dep) {
533
533
  edge = edge || this.edge
534
534
  dep = dep || this.dep
535
- edge.overridden = true
535
+ edge.peerConflicted = true
536
536
  const expl = this.explainPeerConflict(edge, dep)
537
537
  log.warn('ERESOLVE', 'overriding peer dependency', expl)
538
538
  }
package/lib/printable.js CHANGED
@@ -130,8 +130,8 @@ class Edge {
130
130
  if (edge.error) {
131
131
  this.error = edge.error
132
132
  }
133
- if (edge.overridden) {
134
- this.overridden = edge.overridden
133
+ if (edge.peerConflicted) {
134
+ this.peerConflicted = edge.peerConflicted
135
135
  }
136
136
  }
137
137
  }
@@ -149,7 +149,7 @@ class EdgeOut extends Edge {
149
149
  }${
150
150
  this.error ? ' ' + this.error : ''
151
151
  }${
152
- this.overridden ? ' overridden' : ''
152
+ this.peerConflicted ? ' peerConflicted' : ''
153
153
  } }`
154
154
  }
155
155
  }
@@ -165,7 +165,7 @@ class EdgeIn extends Edge {
165
165
  return `{ ${this.from || '""'} ${this.type} ${this.name}@${this.spec}${
166
166
  this.error ? ' ' + this.error : ''
167
167
  }${
168
- this.overridden ? ' overridden' : ''
168
+ this.peerConflicted ? ' peerConflicted' : ''
169
169
  } }`
170
170
  }
171
171
  }
package/lib/shrinkwrap.js CHANGED
@@ -10,7 +10,7 @@
10
10
  // definitely not before npm v8.
11
11
 
12
12
  const localeCompare = require('@isaacs/string-locale-compare')('en')
13
- const lockfileVersion = 2
13
+ const defaultLockfileVersion = 2
14
14
 
15
15
  // for comparing nodes to yarn.lock entries
16
16
  const mismatch = (a, b) => a && b && a !== b
@@ -226,6 +226,10 @@ const _filenameSet = Symbol('_filenameSet')
226
226
  const _maybeRead = Symbol('_maybeRead')
227
227
  const _maybeStat = Symbol('_maybeStat')
228
228
  class Shrinkwrap {
229
+ static get defaultLockfileVersion () {
230
+ return defaultLockfileVersion
231
+ }
232
+
229
233
  static load (options) {
230
234
  return new Shrinkwrap(options).load()
231
235
  }
@@ -234,21 +238,31 @@ class Shrinkwrap {
234
238
  return swKeyOrder
235
239
  }
236
240
 
237
- static reset (options) {
241
+ static async reset (options) {
238
242
  // still need to know if it was loaded from the disk, but don't
239
243
  // bother reading it if we're gonna just throw it away.
240
244
  const s = new Shrinkwrap(options)
241
245
  s.reset()
242
246
 
243
- return s[_maybeStat]().then(([sw, lock]) => {
244
- s.filename = resolve(s.path,
245
- (s.hiddenLockfile ? 'node_modules/.package-lock'
246
- : s.shrinkwrapOnly || sw ? 'npm-shrinkwrap'
247
- : 'package-lock') + '.json')
248
- s.loadedFromDisk = !!(sw || lock)
249
- s.type = basename(s.filename)
250
- return s
251
- })
247
+ const [sw, lock] = await s[_maybeStat]()
248
+
249
+ s.filename = resolve(s.path,
250
+ (s.hiddenLockfile ? 'node_modules/.package-lock'
251
+ : s.shrinkwrapOnly || sw ? 'npm-shrinkwrap'
252
+ : 'package-lock') + '.json')
253
+ s.loadedFromDisk = !!(sw || lock)
254
+ s.type = basename(s.filename)
255
+
256
+ try {
257
+ if (s.loadedFromDisk && !s.lockfileVersion) {
258
+ const json = parseJSON(await maybeReadFile(s.filename))
259
+ if (json.lockfileVersion > defaultLockfileVersion) {
260
+ s.lockfileVersion = json.lockfileVersion
261
+ }
262
+ }
263
+ } catch (e) {}
264
+
265
+ return s
252
266
  }
253
267
 
254
268
  static metaFromNode (node, path) {
@@ -316,8 +330,12 @@ class Shrinkwrap {
316
330
  shrinkwrapOnly = false,
317
331
  hiddenLockfile = false,
318
332
  log = procLog,
333
+ lockfileVersion,
319
334
  } = options
320
335
 
336
+ this.lockfileVersion = hiddenLockfile ? 3
337
+ : lockfileVersion ? parseInt(lockfileVersion, 10)
338
+ : null
321
339
  this.log = log
322
340
  this[_awaitingUpdate] = new Map()
323
341
  this.tree = null
@@ -372,6 +390,7 @@ class Shrinkwrap {
372
390
  reset () {
373
391
  this.tree = null
374
392
  this[_awaitingUpdate] = new Map()
393
+ const lockfileVersion = this.lockfileVersion || defaultLockfileVersion
375
394
  this.originalLockfileVersion = lockfileVersion
376
395
  this.data = {
377
396
  lockfileVersion,
@@ -460,14 +479,23 @@ class Shrinkwrap {
460
479
  this.ancientLockfile = false
461
480
  return {}
462
481
  }).then(lock => {
482
+ const lockfileVersion = this.lockfileVersion ? this.lockfileVersion
483
+ : Math.max(lock.lockfileVersion || 0, defaultLockfileVersion)
463
484
  this.data = {
464
485
  ...lock,
465
- lockfileVersion,
486
+ lockfileVersion: lockfileVersion,
466
487
  requires: true,
467
488
  packages: lock.packages || {},
468
- ...(this.hiddenLockfile ? {} : {dependencies: lock.dependencies || {}}),
489
+ dependencies: lock.dependencies || {},
469
490
  }
491
+
470
492
  this.originalLockfileVersion = lock.lockfileVersion
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
+ }
471
499
  this.ancientLockfile = this.loadedFromDisk &&
472
500
  !(lock.lockfileVersion >= 2) && !lock.requires
473
501
 
@@ -878,15 +906,32 @@ class Shrinkwrap {
878
906
  }
879
907
  }
880
908
 
909
+ // if we haven't set it by now, use the default
910
+ if (!this.lockfileVersion) {
911
+ this.lockfileVersion = defaultLockfileVersion
912
+ }
913
+ this.data.lockfileVersion = this.lockfileVersion
914
+
881
915
  // hidden lockfiles don't include legacy metadata or a root entry
882
916
  if (this.hiddenLockfile) {
883
917
  delete this.data.packages['']
884
918
  delete this.data.dependencies
885
- } else if (this.tree) {
919
+ } else if (this.tree && this.lockfileVersion <= 3) {
886
920
  this[_buildLegacyLockfile](this.tree, this.data)
887
921
  }
888
922
 
889
- return this.data
923
+ // lf version 1 = dependencies only
924
+ // lf version 2 = dependencies and packages
925
+ // lf version 3 = packages only
926
+ if (this.lockfileVersion >= 3) {
927
+ const { dependencies, ...data } = this.data
928
+ return data
929
+ } else if (this.lockfileVersion < 2) {
930
+ const { packages, ...data } = this.data
931
+ return data
932
+ } else {
933
+ return { ...this.data }
934
+ }
890
935
  }
891
936
 
892
937
  [_buildLegacyLockfile] (node, lock, path = []) {
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "3.0.0",
3
+ "version": "4.0.3",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.0.1",
7
7
  "@npmcli/installed-package-contents": "^1.0.7",
8
- "@npmcli/map-workspaces": "^1.0.2",
8
+ "@npmcli/map-workspaces": "^2.0.0",
9
9
  "@npmcli/metavuln-calculator": "^2.0.0",
10
10
  "@npmcli/move-file": "^1.1.0",
11
11
  "@npmcli/name-from-folder": "^1.0.1",
12
12
  "@npmcli/node-gyp": "^1.0.1",
13
13
  "@npmcli/package-json": "^1.0.1",
14
14
  "@npmcli/run-script": "^2.0.0",
15
- "bin-links": "^2.2.1",
15
+ "bin-links": "^2.3.0",
16
16
  "cacache": "^15.0.3",
17
17
  "common-ancestor-path": "^1.0.1",
18
18
  "json-parse-even-better-errors": "^2.3.1",
@@ -45,11 +45,10 @@
45
45
  "tcompare": "^5.0.6"
46
46
  },
47
47
  "scripts": {
48
- "test": "npm run test-only --",
49
- "test-only": "tap",
50
- "posttest": "npm run lint --",
48
+ "test": "tap",
49
+ "posttest": "npm run lint",
51
50
  "snap": "tap",
52
- "postsnap": "npm run lintfix --",
51
+ "postsnap": "npm run lintfix",
53
52
  "test-proxy": "ARBORIST_TEST_PROXY=1 tap --snapshot",
54
53
  "preversion": "npm test",
55
54
  "postversion": "npm publish",
@@ -88,7 +87,7 @@
88
87
  "--no-warnings",
89
88
  "--no-deprecation"
90
89
  ],
91
- "timeout": "240"
90
+ "timeout": "360"
92
91
  },
93
92
  "engines": {
94
93
  "node": "^12.13.0 || ^14.15.0 || >=16"