@npmcli/arborist 9.0.1 → 9.0.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.
@@ -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,
@@ -267,6 +267,7 @@ module.exports = cls => class VirtualLoader extends cls {
267
267
  integrity: sw.integrity,
268
268
  resolved: consistentResolve(sw.resolved, this.path, path),
269
269
  pkg: sw,
270
+ ideallyInert: sw.ideallyInert,
270
271
  hasShrinkwrap: sw.hasShrinkwrap,
271
272
  dev,
272
273
  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
 
@@ -819,6 +837,7 @@ module.exports = cls => class Reifier extends cls {
819
837
  const set = optionalSet(node)
820
838
  for (node of set) {
821
839
  log.verbose('reify', 'failed optional dependency', node.path)
840
+ node.ideallyInert = true
822
841
  this[_addNodeToTrashList](node)
823
842
  }
824
843
  }) : p).then(() => node)
@@ -833,21 +852,23 @@ module.exports = cls => class Reifier extends cls {
833
852
  // ${REGISTRY} or something. This has to be threaded through the
834
853
  // Shrinkwrap and Node classes carefully, so for now, just treat
835
854
  // the default reg as the magical animal that it has been.
836
- const resolvedURL = hgi.parseUrl(resolved)
837
-
838
- if (!resolvedURL) {
855
+ try {
856
+ const resolvedURL = new URL(resolved)
857
+
858
+ if ((this.options.replaceRegistryHost === resolvedURL.hostname) ||
859
+ this.options.replaceRegistryHost === 'always') {
860
+ const registryURL = new URL(this.registry)
861
+ // Replace the host with the registry host while keeping the path intact
862
+ resolvedURL.hostname = registryURL.hostname
863
+ resolvedURL.port = registryURL.port
864
+ return resolvedURL.toString()
865
+ }
866
+ return resolved
867
+ } catch (e) {
839
868
  // if we could not parse the url at all then returning nothing
840
869
  // here means it will get removed from the tree in the next step
841
- return
870
+ return undefined
842
871
  }
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}`
848
- }
849
-
850
- return resolved
851
872
  }
852
873
 
853
874
  // bundles are *sort of* like shrinkwraps, in that the branch is defined
@@ -1151,6 +1172,9 @@ module.exports = cls => class Reifier extends cls {
1151
1172
 
1152
1173
  this.#retiredUnchanged[retireFolder] = []
1153
1174
  return promiseAllRejectLate(diff.unchanged.map(node => {
1175
+ if (node.ideallyInert) {
1176
+ return
1177
+ }
1154
1178
  // no need to roll back links, since we'll just delete them anyway
1155
1179
  if (node.isLink) {
1156
1180
  return mkdir(dirname(node.path), { recursive: true, force: true })
@@ -1230,7 +1254,7 @@ module.exports = cls => class Reifier extends cls {
1230
1254
  // skip links that only live within node_modules as they are most
1231
1255
  // likely managed by packages we installed, we only want to rebuild
1232
1256
  // unchanged links we directly manage
1233
- const linkedFromRoot = node.parent === tree || node.target.fsTop === tree
1257
+ const linkedFromRoot = (node.parent === tree && !node.ideallyInert) || node.target.fsTop === tree
1234
1258
  if (node.isLink && linkedFromRoot) {
1235
1259
  nodes.push(node)
1236
1260
  }
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.0.2",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",