@npmcli/arborist 8.0.3 → 8.0.4

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.
@@ -139,9 +139,11 @@ module.exports = cls => class Reifier extends cls {
139
139
  // of Node/Link trees
140
140
  log.warn('reify', 'The "linked" install strategy is EXPERIMENTAL and may contain bugs.')
141
141
  this.idealTree = await this.createIsolatedTree()
142
- this.#linkedActualForDiff = this.#buildLinkedActualForDiff(
143
- this.idealTree, this.actualTree
144
- )
142
+ if (this.actualTree) {
143
+ this.#linkedActualForDiff = this.#buildLinkedActualForDiff(
144
+ this.idealTree, this.actualTree
145
+ )
146
+ }
145
147
  }
146
148
  await this[_diffTrees]()
147
149
  await this[_reifyPackages]()
@@ -849,6 +851,10 @@ module.exports = cls => class Reifier extends cls {
849
851
  if (combined.has(child.path) || !existsSync(child.path)) {
850
852
  continue
851
853
  }
854
+ // Skip store links whose ideal realpath doesn't exist on disk yet — the store hash changed and the symlink needs recreating via ADD.
855
+ if (child.isLink && child.resolved?.startsWith('file:.store/') && !existsSync(child.realpath)) {
856
+ continue
857
+ }
852
858
  let entry
853
859
  if (child.isLink) {
854
860
  entry = new IsolatedLink(child)
@@ -1,5 +1,6 @@
1
1
  const npa = require('npm-package-arg')
2
2
  const semver = require('semver')
3
+ const { log } = require('proc-log')
3
4
 
4
5
  class OverrideSet {
5
6
  constructor ({ overrides, key, parent }) {
@@ -44,6 +45,43 @@ class OverrideSet {
44
45
  }
45
46
  }
46
47
 
48
+ childrenAreEqual (other) {
49
+ if (this.children.size !== other.children.size) {
50
+ return false
51
+ }
52
+ for (const [key] of this.children) {
53
+ if (!other.children.has(key)) {
54
+ return false
55
+ }
56
+ if (this.children.get(key).value !== other.children.get(key).value) {
57
+ return false
58
+ }
59
+ if (!this.children.get(key).childrenAreEqual(other.children.get(key))) {
60
+ return false
61
+ }
62
+ }
63
+ return true
64
+ }
65
+
66
+ isEqual (other) {
67
+ if (this === other) {
68
+ return true
69
+ }
70
+ if (!other) {
71
+ return false
72
+ }
73
+ if (this.key !== other.key || this.value !== other.value) {
74
+ return false
75
+ }
76
+ if (!this.childrenAreEqual(other)) {
77
+ return false
78
+ }
79
+ if (!this.parent) {
80
+ return !other.parent
81
+ }
82
+ return this.parent.isEqual(other.parent)
83
+ }
84
+
47
85
  getEdgeRule (edge) {
48
86
  for (const rule of this.ruleset.values()) {
49
87
  if (rule.name !== edge.name) {
@@ -142,6 +180,123 @@ class OverrideSet {
142
180
 
143
181
  return ruleset
144
182
  }
183
+
184
+ static findSpecificOverrideSet (first, second) {
185
+ for (let overrideSet = second; overrideSet; overrideSet = overrideSet.parent) {
186
+ if (overrideSet.isEqual(first)) {
187
+ return second
188
+ }
189
+ }
190
+ for (let overrideSet = first; overrideSet; overrideSet = overrideSet.parent) {
191
+ if (overrideSet.isEqual(second)) {
192
+ return first
193
+ }
194
+ }
195
+
196
+ // The override sets are incomparable (e.g. siblings like the "react" and "react-dom" children of the root override set). Check if they have semantically conflicting rules before treating this as an error.
197
+ if (this.haveConflictingRules(first, second)) {
198
+ log.silly('Conflicting override sets', first, second)
199
+ return undefined
200
+ }
201
+
202
+ // The override sets are structurally incomparable but have compatible rules. Fall back to their nearest common ancestor so the node still has a valid override set.
203
+ return this.findCommonAncestor(first, second)
204
+ }
205
+
206
+ static findCommonAncestor (first, second) {
207
+ const firstAncestors = []
208
+ for (const ancestor of first.ancestry()) {
209
+ firstAncestors.push(ancestor)
210
+ }
211
+ for (const secondAnc of second.ancestry()) {
212
+ for (const firstAnc of firstAncestors) {
213
+ if (firstAnc.isEqual(secondAnc)) {
214
+ return firstAnc
215
+ }
216
+ }
217
+ }
218
+ return null
219
+ }
220
+
221
+ static doOverrideSetsConflict (first, second) {
222
+ // If override sets contain one another then we can try to use the more specific one.
223
+ // If neither one is more specific, check for semantic conflicts.
224
+ const specificSet = this.findSpecificOverrideSet(first, second)
225
+ if (specificSet !== undefined) {
226
+ // One contains the other, so no conflict
227
+ return false
228
+ }
229
+
230
+ // The override sets are structurally incomparable, but this doesn't necessarily
231
+ // mean they conflict. We need to check if they have conflicting version requirements
232
+ // for any package that appears in both rulesets.
233
+ return this.haveConflictingRules(first, second)
234
+ }
235
+
236
+ static haveConflictingRules (first, second) {
237
+ // Get all rules from both override sets
238
+ const firstRules = first.ruleset
239
+ const secondRules = second.ruleset
240
+
241
+ // Check each package that appears in both rulesets
242
+ for (const [key, firstRule] of firstRules) {
243
+ const secondRule = secondRules.get(key)
244
+ if (!secondRule) {
245
+ // Package only appears in one ruleset, no conflict
246
+ continue
247
+ }
248
+
249
+ // Same rule object means no conflict
250
+ if (firstRule === secondRule || firstRule.isEqual(secondRule)) {
251
+ continue
252
+ }
253
+
254
+ // Both rulesets have rules for this package with different values.
255
+ // Check if the version requirements are actually incompatible.
256
+ const firstValue = firstRule.value
257
+ const secondValue = secondRule.value
258
+
259
+ // If either value is a reference (starts with $), we can't determine
260
+ // compatibility here - the reference might resolve to compatible versions.
261
+ // We defer to runtime resolution rather than failing early.
262
+ if (firstValue.startsWith('$') || secondValue.startsWith('$')) {
263
+ continue
264
+ }
265
+
266
+ // Check if the version ranges are compatible using semver
267
+ // If both specify version ranges, they conflict only if they have no overlap
268
+ try {
269
+ const firstSpec = npa(`${firstRule.name}@${firstValue}`)
270
+ const secondSpec = npa(`${secondRule.name}@${secondValue}`)
271
+
272
+ // For range/version types, check if they intersect
273
+ if ((firstSpec.type === 'range' || firstSpec.type === 'version') &&
274
+ (secondSpec.type === 'range' || secondSpec.type === 'version')) {
275
+ // Check if the ranges intersect
276
+ const firstRange = firstSpec.fetchSpec
277
+ const secondRange = secondSpec.fetchSpec
278
+
279
+ // If the ranges don't intersect, we have a real conflict
280
+ if (!semver.intersects(firstRange, secondRange)) {
281
+ log.silly('Found conflicting override rules', {
282
+ package: firstRule.name,
283
+ first: firstValue,
284
+ second: secondValue,
285
+ })
286
+ return true
287
+ }
288
+ }
289
+ // For other types (git, file, directory, tag), we can't easily determine
290
+ // compatibility, so we conservatively assume no conflict
291
+ } catch {
292
+ // If we can't parse the specs, conservatively assume no conflict
293
+ // Real conflicts will be caught during dependency resolution
294
+ }
295
+ }
296
+
297
+ // No conflicting rules found
298
+ return false
299
+ }
145
300
  }
146
301
 
147
302
  module.exports = OverrideSet
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@npmcli/arborist",
3
- "version": "8.0.3",
3
+ "version": "8.0.4",
4
4
  "description": "Manage node_modules trees",
5
5
  "dependencies": {
6
6
  "@isaacs/string-locale-compare": "^1.1.0",