@npmcli/arborist 9.0.1 → 9.1.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.
package/README.md CHANGED
@@ -267,53 +267,19 @@ are updated by arborist when necessary whenever the tree is modified in
267
267
  such a way that the dependency graph can change, and are relevant when
268
268
  pruning nodes from the tree.
269
269
 
270
- ```
271
- | extraneous | peer | dev | optional | devOptional | meaning | prune? |
272
- |------------+------+-----+----------+-------------+---------------------+-------------------|
273
- | | | | | | production dep | never |
274
- |------------+------+-----+----------+-------------+---------------------+-------------------|
275
- | X | N/A | N/A | N/A | N/A | nothing depends on | always |
276
- | | | | | | this, it is trash | |
277
- |------------+------+-----+----------+-------------+---------------------+-------------------|
278
- | | | X | | X | devDependency, or | if pruning dev |
279
- | | | | | not in lock | only depended upon | |
280
- | | | | | | by devDependencies | |
281
- |------------+------+-----+----------+-------------+---------------------+-------------------|
282
- | | | | X | X | optionalDependency, | if pruning |
283
- | | | | | not in lock | or only depended on | optional |
284
- | | | | | | by optionalDeps | |
285
- |------------+------+-----+----------+-------------+---------------------+-------------------|
286
- | | | X | X | X | Optional dependency | if pruning EITHER |
287
- | | | | | not in lock | of dep(s) in the | dev OR optional |
288
- | | | | | | dev hierarchy | |
289
- |------------+------+-----+----------+-------------+---------------------+-------------------|
290
- | | | | | X | BOTH a non-optional | if pruning BOTH |
291
- | | | | | in lock | dep within the dev | dev AND optional |
292
- | | | | | | hierarchy, AND a | |
293
- | | | | | | dep within the | |
294
- | | | | | | optional hierarchy | |
295
- |------------+------+-----+----------+-------------+---------------------+-------------------|
296
- | | X | | | | peer dependency, or | if pruning peers |
297
- | | | | | | only depended on by | |
298
- | | | | | | peer dependencies | |
299
- |------------+------+-----+----------+-------------+---------------------+-------------------|
300
- | | X | X | | X | peer dependency of | if pruning peer |
301
- | | | | | not in lock | dev node hierarchy | OR dev deps |
302
- |------------+------+-----+----------+-------------+---------------------+-------------------|
303
- | | X | | X | X | peer dependency of | if pruning peer |
304
- | | | | | not in lock | optional nodes, or | OR optional deps |
305
- | | | | | | peerOptional dep | |
306
- |------------+------+-----+----------+-------------+---------------------+-------------------|
307
- | | X | X | X | X | peer optional deps | if pruning peer |
308
- | | | | | not in lock | of the dev dep | OR optional OR |
309
- | | | | | | hierarchy | dev |
310
- |------------+------+-----+----------+-------------+---------------------+-------------------|
311
- | | X | | | X | BOTH a non-optional | if pruning peers |
312
- | | | | | in lock | peer dep within the | OR: |
313
- | | | | | | dev hierarchy, AND | BOTH optional |
314
- | | | | | | a peer optional dep | AND dev deps |
315
- +------------+------+-----+----------+-------------+---------------------+-------------------+
316
- ```
270
+ | extraneous | peer | dev | optional | devOptional | meaning | prune? |
271
+ |:----------:|:----:|:---:|:--------:|:----------------:|:-------------------------------------------------------------------------------------------------|:-------------------------------------------------------|
272
+ | | | | | | production dep | never |
273
+ | X | N/A | N/A | N/A | N/A | nothing depends on this, it is trash | always |
274
+ | | | X | | X<br>not in lock | devDependency, or only depended<br>on by devDependencies | if pruning dev |
275
+ | | | | X | X<br>not in lock | optionalDependency, or only depended<br>on by optionalDeps | if pruning optional |
276
+ | | | X | X | X<br>not in lock | Optional dependency of dep(s) in the<br>dev hierarchy | if pruning EITHER<br>dev OR optional |
277
+ | | | | | X<br>in lock | BOTH a non-optional dep within the<br>dev hierarchy, AND a dep within<br>the optional hierarchy | if pruning BOTH<br>dev AND optional |
278
+ | | X | | | | peer dependency, or only depended<br>on by peer dependencies | if pruning peers |
279
+ | | X | X | | X<br>not in lock | peer dependency of dev node hierarchy | if pruning peer OR<br>dev deps |
280
+ | | X | | X | X<br>not in lock | peer dependency of optional nodes, or<br>peerOptional dep | if pruning peer OR<br>optional deps |
281
+ | | X | X | X | X<br>not in lock | peer optional deps of the dev dep hierarchy | if pruning peer OR<br>optional OR dev |
282
+ | | X | | | X<br>in lock | BOTH a non-optional peer dep within the<br>dev hierarchy, AND a peer optional dep | if pruning peer deps OR:<br>BOTH optional AND dev deps |
317
283
 
318
284
  * If none of these flags are set, then the node is required by the
319
285
  dependency and/or peerDependency hierarchy. It should not be pruned.
@@ -13,6 +13,7 @@ const { lstat, readlink } = require('node:fs/promises')
13
13
  const { depth } = require('treeverse')
14
14
  const { log, time } = require('proc-log')
15
15
  const { redact } = require('@npmcli/redact')
16
+ const semver = require('semver')
16
17
 
17
18
  const {
18
19
  OK,
@@ -279,14 +280,23 @@ module.exports = cls => class IdealTreeBuilder extends cls {
279
280
  // When updating all, we load the shrinkwrap, but don't bother
280
281
  // to build out the full virtual tree from it, since we'll be
281
282
  // reconstructing it anyway.
282
- .then(root => this.options.global ? root
283
- : !this[_usePackageLock] || this[_updateAll]
284
- ? Shrinkwrap.reset({
285
- path: this.path,
286
- lockfileVersion: this.options.lockfileVersion,
287
- resolveOptions: this.options,
288
- }).then(meta => Object.assign(root, { meta }))
289
- : this.loadVirtual({ root }))
283
+ .then(root => {
284
+ if (this.options.global) {
285
+ return root
286
+ } else if (!this[_usePackageLock] || this[_updateAll]) {
287
+ return Shrinkwrap.reset({
288
+ path: this.path,
289
+ lockfileVersion: this.options.lockfileVersion,
290
+ resolveOptions: this.options,
291
+ }).then(meta => Object.assign(root, { meta }))
292
+ } else {
293
+ return this.loadVirtual({ root })
294
+ .then(tree => {
295
+ this.#applyRootOverridesToWorkspaces(tree)
296
+ return tree
297
+ })
298
+ }
299
+ })
290
300
 
291
301
  // if we don't have a lockfile to go from, then start with the
292
302
  // actual tree, so we only make the minimum required changes.
@@ -799,7 +809,8 @@ This is a one-time fix-up, please be patient...
799
809
  const crackOpen = this.#complete &&
800
810
  node !== this.idealTree &&
801
811
  node.resolved &&
802
- (hasBundle || hasShrinkwrap)
812
+ (hasBundle || hasShrinkwrap) &&
813
+ !node.ideallyInert
803
814
  if (crackOpen) {
804
815
  const Arborist = this.constructor
805
816
  const opt = { ...this.options }
@@ -1475,6 +1486,32 @@ This is a one-time fix-up, please be patient...
1475
1486
  timeEnd()
1476
1487
  }
1477
1488
 
1489
+ #applyRootOverridesToWorkspaces (tree) {
1490
+ const rootOverrides = tree.root.package.overrides || {}
1491
+
1492
+ for (const node of tree.root.inventory.values()) {
1493
+ if (!node.isWorkspace) {
1494
+ continue
1495
+ }
1496
+
1497
+ for (const depName of Object.keys(rootOverrides)) {
1498
+ const edge = node.edgesOut.get(depName)
1499
+ const rootNode = tree.root.children.get(depName)
1500
+
1501
+ // safely skip if either edge or rootNode doesn't exist yet
1502
+ if (!edge || !rootNode) {
1503
+ continue
1504
+ }
1505
+
1506
+ const resolvedRootVersion = rootNode.package.version
1507
+ if (!semver.satisfies(resolvedRootVersion, edge.spec)) {
1508
+ edge.detach()
1509
+ node.children.delete(depName)
1510
+ }
1511
+ }
1512
+ }
1513
+ }
1514
+
1478
1515
  #idealTreePrune () {
1479
1516
  for (const node of this.idealTree.inventory.values()) {
1480
1517
  if (node.extraneous) {
@@ -1491,7 +1528,7 @@ This is a one-time fix-up, please be patient...
1491
1528
 
1492
1529
  const set = optionalSet(node)
1493
1530
  for (const node of set) {
1494
- node.parent = null
1531
+ node.ideallyInert = true
1495
1532
  }
1496
1533
  }
1497
1534
  }
@@ -1512,6 +1549,7 @@ This is a one-time fix-up, please be patient...
1512
1549
  node.parent !== null
1513
1550
  && !node.isProjectRoot
1514
1551
  && !excludeNodes.has(node)
1552
+ && !node.ideallyInert
1515
1553
  ) {
1516
1554
  this[_addNodeToTrashList](node)
1517
1555
  }
@@ -81,7 +81,7 @@ module.exports = cls => class IsolatedReifier extends cls {
81
81
  }
82
82
  queue.push(e.to)
83
83
  })
84
- if (!next.isProjectRoot && !next.isWorkspace) {
84
+ if (!next.isProjectRoot && !next.isWorkspace && !next.ideallyInert) {
85
85
  root.external.push(await this.externalProxyMemo(next))
86
86
  }
87
87
  }
@@ -147,8 +147,8 @@ module.exports = cls => class IsolatedReifier extends cls {
147
147
  const nonOptionalDeps = edges.filter(e => !e.optional).map(e => e.to.target)
148
148
 
149
149
  result.localDependencies = await Promise.all(nonOptionalDeps.filter(n => n.isWorkspace).map(this.workspaceProxyMemo))
150
- result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !n.isWorkspace).map(this.externalProxyMemo))
151
- result.externalOptionalDependencies = await Promise.all(optionalDeps.map(this.externalProxyMemo))
150
+ result.externalDependencies = await Promise.all(nonOptionalDeps.filter(n => !n.isWorkspace && !n.ideallyInert).map(this.externalProxyMemo))
151
+ result.externalOptionalDependencies = await Promise.all(optionalDeps.filter(n => !n.ideallyInert).map(this.externalProxyMemo))
152
152
  result.dependencies = [
153
153
  ...result.externalDependencies,
154
154
  ...result.localDependencies,
@@ -200,6 +200,18 @@ module.exports = cls => class VirtualLoader extends cls {
200
200
  const targetPath = resolve(this.path, meta.resolved)
201
201
  const targetLoc = relpath(this.path, targetPath)
202
202
  const target = nodes.get(targetLoc)
203
+
204
+ if (!target) {
205
+ const err = new Error(
206
+ `Missing target in lock file: "${targetLoc}" is referenced by "${location}" but does not exist.
207
+ To fix:
208
+ 1. rm package-lock.json
209
+ 2. npm install`
210
+ )
211
+ err.code = 'EMISSINGTARGET'
212
+ throw err
213
+ }
214
+
203
215
  const link = this.#loadLink(location, targetLoc, target, meta)
204
216
  nodes.set(location, link)
205
217
  nodes.set(targetLoc, link.target)
@@ -267,6 +279,7 @@ module.exports = cls => class VirtualLoader extends cls {
267
279
  integrity: sw.integrity,
268
280
  resolved: consistentResolve(sw.resolved, this.path, path),
269
281
  pkg: sw,
282
+ ideallyInert: sw.ideallyInert,
270
283
  hasShrinkwrap: sw.hasShrinkwrap,
271
284
  dev,
272
285
  optional,
@@ -8,7 +8,6 @@ const semver = require('semver')
8
8
  const debug = require('../debug.js')
9
9
  const { walkUp } = require('walk-up-path')
10
10
  const { log, time } = require('proc-log')
11
- const hgi = require('hosted-git-info')
12
11
  const rpj = require('read-package-json-fast')
13
12
 
14
13
  const { dirname, resolve, relative, join } = require('node:path')
@@ -150,7 +149,7 @@ module.exports = cls => class Reifier extends cls {
150
149
  for (const path of this[_trashList]) {
151
150
  const loc = relpath(this.idealTree.realpath, path)
152
151
  const node = this.idealTree.inventory.get(loc)
153
- if (node && node.root === this.idealTree) {
152
+ if (node && node.root === this.idealTree && !node.ideallyInert) {
154
153
  node.parent = null
155
154
  }
156
155
  }
@@ -238,6 +237,18 @@ module.exports = cls => class Reifier extends cls {
238
237
  this.idealTree.meta.hiddenLockfile = true
239
238
  this.idealTree.meta.lockfileVersion = defaultLockfileVersion
240
239
 
240
+ // Preserve inertness for failed stuff.
241
+ if (this.actualTree) {
242
+ for (const [loc, actual] of this.actualTree.inventory.entries()) {
243
+ if (actual.ideallyInert) {
244
+ const ideal = this.idealTree.inventory.get(loc)
245
+ if (ideal) {
246
+ ideal.ideallyInert = true
247
+ }
248
+ }
249
+ }
250
+ }
251
+
241
252
  this.actualTree = this.idealTree
242
253
  this.idealTree = null
243
254
 
@@ -600,6 +611,9 @@ module.exports = cls => class Reifier extends cls {
600
611
  // retire the same path at the same time.
601
612
  const dirsChecked = new Set()
602
613
  return promiseAllRejectLate(leaves.map(async node => {
614
+ if (node.ideallyInert) {
615
+ return
616
+ }
603
617
  for (const d of walkUp(node.path)) {
604
618
  if (d === node.top.path) {
605
619
  break
@@ -744,6 +758,10 @@ module.exports = cls => class Reifier extends cls {
744
758
  }
745
759
 
746
760
  async #extractOrLink (node) {
761
+ if (node.ideallyInert) {
762
+ return
763
+ }
764
+
747
765
  const nm = resolve(node.parent.path, 'node_modules')
748
766
  await this.#validateNodeModules(nm)
749
767
 
@@ -807,7 +825,14 @@ module.exports = cls => class Reifier extends cls {
807
825
  // symlink
808
826
  const dir = dirname(node.path)
809
827
  const target = node.realpath
810
- const rel = relative(dir, target)
828
+
829
+ let rel
830
+ if (node.resolved?.startsWith('file:')) {
831
+ rel = this.#calculateRelativePath(node, dir, target, nm)
832
+ } else {
833
+ rel = relative(dir, target)
834
+ }
835
+
811
836
  await mkdir(dir, { recursive: true })
812
837
  return symlink(rel, node.path, 'junction')
813
838
  }
@@ -819,11 +844,42 @@ module.exports = cls => class Reifier extends cls {
819
844
  const set = optionalSet(node)
820
845
  for (node of set) {
821
846
  log.verbose('reify', 'failed optional dependency', node.path)
847
+ node.ideallyInert = true
822
848
  this[_addNodeToTrashList](node)
823
849
  }
824
850
  }) : p).then(() => node)
825
851
  }
826
852
 
853
+ #calculateRelativePath (node, dir, target) {
854
+ // Check if the node is affected by a root override
855
+ let hasRootOverride = [...node.edgesIn].some(edge => edge.from.isRoot && edge.overrides)
856
+ // If not set via edges, see if the root package.json explicitly lists an override
857
+ if (!hasRootOverride && node.root) {
858
+ const rootPackage = node.root.target
859
+ hasRootOverride = !!(rootPackage &&
860
+ rootPackage.package.overrides &&
861
+ rootPackage.package.overrides[node.name])
862
+ }
863
+ if (!hasRootOverride) {
864
+ return relative(dir, target)
865
+ }
866
+ // If an override is detected, attempt to retrieve the override spec from the root package.json
867
+ const overrideSpec = node.root?.target?.package?.overrides?.[node.name]
868
+ if (typeof overrideSpec === 'string' && overrideSpec.startsWith('file:')) {
869
+ const overridePath = overrideSpec.replace(/^file:/, '')
870
+ const rootDir = node.root.target.path
871
+ return relative(dir, resolve(rootDir, overridePath))
872
+ }
873
+
874
+ // Fallback: derive the package name from node.resolved in a platform-agnostic way
875
+ const filePath = node.resolved.replace(/^file:/, '')
876
+ // A node.package.name could be different than the folder name
877
+ const pathParts = filePath.split(/[\\/]/)
878
+ const packageName = pathParts[pathParts.length - 1]
879
+
880
+ return join('..', packageName)
881
+ }
882
+
827
883
  #registryResolved (resolved) {
828
884
  // the default registry url is a magic value meaning "the currently
829
885
  // configured registry".
@@ -833,21 +889,33 @@ module.exports = cls => class Reifier extends cls {
833
889
  // ${REGISTRY} or something. This has to be threaded through the
834
890
  // Shrinkwrap and Node classes carefully, so for now, just treat
835
891
  // the default reg as the magical animal that it has been.
836
- const resolvedURL = hgi.parseUrl(resolved)
892
+ try {
893
+ const resolvedURL = new URL(resolved)
837
894
 
838
- if (!resolvedURL) {
895
+ if ((this.options.replaceRegistryHost === resolvedURL.hostname) ||
896
+ this.options.replaceRegistryHost === 'always') {
897
+ const registryURL = new URL(this.registry)
898
+
899
+ // Replace the host with the registry host while keeping the path intact
900
+ resolvedURL.hostname = registryURL.hostname
901
+ resolvedURL.port = registryURL.port
902
+
903
+ // Make sure we don't double-include the path if it's already there
904
+ const registryPath = registryURL.pathname.replace(/\/$/, '')
905
+
906
+ if (registryPath && registryPath !== '/' && !resolvedURL.pathname.startsWith(registryPath)) {
907
+ // Since hostname is changed, we need to ensure the registry path is included
908
+ resolvedURL.pathname = registryPath + resolvedURL.pathname
909
+ }
910
+
911
+ return resolvedURL.toString()
912
+ }
913
+ return resolved
914
+ } catch (e) {
839
915
  // if we could not parse the url at all then returning nothing
840
916
  // here means it will get removed from the tree in the next step
841
- return
842
- }
843
-
844
- if ((this.options.replaceRegistryHost === resolvedURL.hostname)
845
- || this.options.replaceRegistryHost === 'always') {
846
- // this.registry always has a trailing slash
847
- return `${this.registry.slice(0, -1)}${resolvedURL.pathname}${resolvedURL.searchParams}`
917
+ return undefined
848
918
  }
849
-
850
- return resolved
851
919
  }
852
920
 
853
921
  // bundles are *sort of* like shrinkwraps, in that the branch is defined
@@ -1151,6 +1219,9 @@ module.exports = cls => class Reifier extends cls {
1151
1219
 
1152
1220
  this.#retiredUnchanged[retireFolder] = []
1153
1221
  return promiseAllRejectLate(diff.unchanged.map(node => {
1222
+ if (node.ideallyInert) {
1223
+ return
1224
+ }
1154
1225
  // no need to roll back links, since we'll just delete them anyway
1155
1226
  if (node.isLink) {
1156
1227
  return mkdir(dirname(node.path), { recursive: true, force: true })
@@ -1230,7 +1301,7 @@ module.exports = cls => class Reifier extends cls {
1230
1301
  // skip links that only live within node_modules as they are most
1231
1302
  // likely managed by packages we installed, we only want to rebuild
1232
1303
  // unchanged links we directly manage
1233
- const linkedFromRoot = node.parent === tree || node.target.fsTop === tree
1304
+ const linkedFromRoot = (node.parent === tree && !node.ideallyInert) || node.target.fsTop === tree
1234
1305
  if (node.isLink && linkedFromRoot) {
1235
1306
  nodes.push(node)
1236
1307
  }
package/lib/edge.js CHANGED
@@ -206,22 +206,21 @@ class Edge {
206
206
  if (this.overrides?.value && this.overrides.value !== '*' && this.overrides.name === this.#name) {
207
207
  if (this.overrides.value.startsWith('$')) {
208
208
  const ref = this.overrides.value.slice(1)
209
- const pkg = this.#from?.sourceReference
209
+ let pkg = this.#from?.sourceReference
210
210
  ? this.#from?.sourceReference.root.package
211
211
  : this.#from?.root?.package
212
- if (pkg.devDependencies?.[ref]) {
213
- return pkg.devDependencies[ref]
214
- }
215
- if (pkg.optionalDependencies?.[ref]) {
216
- return pkg.optionalDependencies[ref]
217
- }
218
- if (pkg.dependencies?.[ref]) {
219
- return pkg.dependencies[ref]
220
- }
221
- if (pkg.peerDependencies?.[ref]) {
222
- return pkg.peerDependencies[ref]
212
+
213
+ let specValue = this.#calculateReferentialOverrideSpec(ref, pkg)
214
+
215
+ // If the package isn't found in the root package, fall back to the local package.
216
+ if (!specValue) {
217
+ pkg = this.#from?.package
218
+ specValue = this.#calculateReferentialOverrideSpec(ref, pkg)
223
219
  }
224
220
 
221
+ if (specValue) {
222
+ return specValue
223
+ }
225
224
  throw new Error(`Unable to resolve reference ${this.overrides.value}`)
226
225
  }
227
226
  return this.overrides.value
@@ -229,6 +228,21 @@ class Edge {
229
228
  return this.#spec
230
229
  }
231
230
 
231
+ #calculateReferentialOverrideSpec (ref, pkg) {
232
+ if (pkg.devDependencies?.[ref]) {
233
+ return pkg.devDependencies[ref]
234
+ }
235
+ if (pkg.optionalDependencies?.[ref]) {
236
+ return pkg.optionalDependencies[ref]
237
+ }
238
+ if (pkg.dependencies?.[ref]) {
239
+ return pkg.dependencies[ref]
240
+ }
241
+ if (pkg.peerDependencies?.[ref]) {
242
+ return pkg.peerDependencies[ref]
243
+ }
244
+ }
245
+
232
246
  get accept () {
233
247
  return this.#accept
234
248
  }
package/lib/node.js CHANGED
@@ -103,6 +103,7 @@ class Node {
103
103
  global = false,
104
104
  dummy = false,
105
105
  sourceReference = null,
106
+ ideallyInert = false,
106
107
  } = options
107
108
  // this object gives querySelectorAll somewhere to stash context about a node
108
109
  // while processing a query
@@ -197,6 +198,8 @@ class Node {
197
198
  this.extraneous = false
198
199
  }
199
200
 
201
+ this.ideallyInert = ideallyInert
202
+
200
203
  this.edgesIn = new Set()
201
204
  this.edgesOut = new CaseInsensitiveMap()
202
205
 
@@ -1074,7 +1077,7 @@ class Node {
1074
1077
  // return true if it's safe to remove this node, because anything that
1075
1078
  // is depending on it would be fine with the thing that they would resolve
1076
1079
  // to if it was removed, or nothing is depending on it in the first place.
1077
- canDedupe (preferDedupe = false) {
1080
+ canDedupe (preferDedupe = false, explicitRequest = false) {
1078
1081
  // not allowed to mess with shrinkwraps or bundles
1079
1082
  if (this.inDepBundle || this.inShrinkwrap) {
1080
1083
  return false
@@ -1117,6 +1120,11 @@ class Node {
1117
1120
  return true
1118
1121
  }
1119
1122
 
1123
+ // if the other version was an explicit request, then prefer to take the other version
1124
+ if (explicitRequest) {
1125
+ return true
1126
+ }
1127
+
1120
1128
  return false
1121
1129
  }
1122
1130
 
package/lib/place-dep.js CHANGED
@@ -423,7 +423,7 @@ class PlaceDep {
423
423
  // is another satisfying node further up the tree, and if so, dedupes.
424
424
  // Even in installStategy is nested, we do this amount of deduplication.
425
425
  pruneDedupable (node, descend = true) {
426
- if (node.canDedupe(this.preferDedupe)) {
426
+ if (node.canDedupe(this.preferDedupe, this.explicitRequest)) {
427
427
  // gather up all deps that have no valid edges in from outside
428
428
  // the dep set, except for this node we're deduping, so that we
429
429
  // also prune deps that would be made extraneous.
package/lib/shrinkwrap.js CHANGED
@@ -109,6 +109,7 @@ const nodeMetaKeys = [
109
109
  'inBundle',
110
110
  'hasShrinkwrap',
111
111
  'hasInstallScript',
112
+ 'ideallyInert',
112
113
  ]
113
114
 
114
115
  const metaFieldFromPkg = (pkg, key) => {
@@ -135,6 +136,10 @@ const assertNoNewer = async (path, data, lockTime, dir, seen) => {
135
136
 
136
137
  const parent = isParent ? dir : resolve(dir, 'node_modules')
137
138
  const rel = relpath(path, dir)
139
+ const inert = data.packages[rel]?.ideallyInert
140
+ if (inert) {
141
+ return
142
+ }
138
143
  seen.add(rel)
139
144
  let entries
140
145
  if (dir === path) {
@@ -173,7 +178,7 @@ const assertNoNewer = async (path, data, lockTime, dir, seen) => {
173
178
 
174
179
  // assert that all the entries in the lockfile were seen
175
180
  for (const loc in data.packages) {
176
- if (!seen.has(loc)) {
181
+ if (!seen.has(loc) && !data.packages[loc].ideallyInert) {
177
182
  throw new Error(`missing from node_modules: ${loc}`)
178
183
  }
179
184
  }
@@ -783,6 +788,10 @@ class Shrinkwrap {
783
788
  // ok, I did my best! good luck!
784
789
  }
785
790
 
791
+ if (lock.ideallyInert) {
792
+ meta.ideallyInert = true
793
+ }
794
+
786
795
  if (lock.bundled) {
787
796
  meta.inBundle = true
788
797
  }
@@ -953,6 +962,12 @@ class Shrinkwrap {
953
962
  this.#buildLegacyLockfile(this.tree, this.data)
954
963
  }
955
964
 
965
+ if (!this.hiddenLockfile) {
966
+ for (const node of Object.values(this.data.packages)) {
967
+ delete node.ideallyInert
968
+ }
969
+ }
970
+
956
971
  // lf version 1 = dependencies only
957
972
  // lf version 2 = dependencies and packages
958
973
  // lf version 3 = packages only
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "9.0.1",
3
+ "version": "9.1.0",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",