Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
override-set.js305 linesDownload Raw Back to lib
1const npa = require('npm-package-arg')
2const semver = require('semver')
3const { log } = require('proc-log')
4
5class OverrideSet {
6  constructor ({ overrides, key, parent }) {
7    this.parent = parent
8    this.children = new Map()
9
10    if (typeof overrides === 'string') {
11      overrides = { '.': overrides }
12    }
13
14    // change a literal empty string to * so we can use truthiness checks on
15    // the value property later
16    if (overrides['.'] === '') {
17      overrides['.'] = '*'
18    }
19
20    if (parent) {
21      const spec = npa(key)
22      if (!spec.name) {
23        throw new Error(`Override without name: ${key}`)
24      }
25
26      this.name = spec.name
27      spec.name = ''
28      this.key = key
29      this.keySpec = spec.toString()
30      this.value = overrides['.'] || this.keySpec
31    }
32
33    for (const [key, childOverrides] of Object.entries(overrides)) {
34      if (key === '.') {
35        continue
36      }
37
38      const child = new OverrideSet({
39        parent: this,
40        key,
41        overrides: childOverrides,
42      })
43
44      this.children.set(child.key, child)
45    }
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
85  getEdgeRule (edge) {
86    for (const rule of this.ruleset.values()) {
87      if (rule.name !== edge.name) {
88        continue
89      }
90
91      // if keySpec is * we found our override
92      if (rule.keySpec === '*') {
93        return rule
94      }
95
96      // We need to use the rawSpec here, because the spec has the overrides applied to it already.
97      // rawSpec can be undefined, so we need to use the fallback value of spec if it is.
98      let spec = npa(`${edge.name}@${edge.rawSpec || edge.spec}`)
99      if (spec.type === 'alias') {
100        spec = spec.subSpec
101      }
102
103      if (spec.type === 'git') {
104        if (spec.gitRange && semver.intersects(spec.gitRange, rule.keySpec)) {
105          return rule
106        }
107
108        continue
109      }
110
111      if (spec.type === 'range' || spec.type === 'version') {
112        if (semver.intersects(spec.fetchSpec, rule.keySpec)) {
113          return rule
114        }
115
116        continue
117      }
118
119      // if we got this far, the spec type is one of tag, directory or file
120      // which means we have no real way to make version comparisons, so we
121      // just accept the override
122      return rule
123    }
124
125    return this
126  }
127
128  getNodeRule (node) {
129    for (const rule of this.ruleset.values()) {
130      if (rule.name !== node.name) {
131        continue
132      }
133
134      if (semver.satisfies(node.version, rule.keySpec) ||
135        semver.satisfies(node.version, rule.value)) {
136        return rule
137      }
138    }
139
140    return this
141  }
142
143  getMatchingRule (node) {
144    for (const rule of this.ruleset.values()) {
145      if (rule.name !== node.name) {
146        continue
147      }
148
149      if (semver.satisfies(node.version, rule.keySpec) ||
150        semver.satisfies(node.version, rule.value)) {
151        return rule
152      }
153    }
154
155    return null
156  }
157
158  * ancestry () {
159    for (let ancestor = this; ancestor; ancestor = ancestor.parent) {
160      yield ancestor
161    }
162  }
163
164  get isRoot () {
165    return !this.parent
166  }
167
168  get ruleset () {
169    const ruleset = new Map()
170
171    for (const override of this.ancestry()) {
172      for (const kid of override.children.values()) {
173        if (!ruleset.has(kid.key)) {
174          ruleset.set(kid.key, kid)
175        }
176      }
177
178      if (!override.isRoot && !ruleset.has(override.key)) {
179        ruleset.set(override.key, override)
180      }
181    }
182
183    return ruleset
184  }
185
186  static findSpecificOverrideSet (first, second) {
187    for (let overrideSet = second; overrideSet; overrideSet = overrideSet.parent) {
188      if (overrideSet.isEqual(first)) {
189        return second
190      }
191    }
192    for (let overrideSet = first; overrideSet; overrideSet = overrideSet.parent) {
193      if (overrideSet.isEqual(second)) {
194        return first
195      }
196    }
197
198    // 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.
199    if (this.haveConflictingRules(first, second)) {
200      log.silly('Conflicting override sets', first, second)
201      return undefined
202    }
203
204    // 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.
205    return this.findCommonAncestor(first, second)
206  }
207
208  static findCommonAncestor (first, second) {
209    const firstAncestors = []
210    for (const ancestor of first.ancestry()) {
211      firstAncestors.push(ancestor)
212    }
213    for (const secondAnc of second.ancestry()) {
214      for (const firstAnc of firstAncestors) {
215        if (firstAnc.isEqual(secondAnc)) {
216          return firstAnc
217        }
218      }
219    }
220    return null
221  }
222
223  static doOverrideSetsConflict (first, second) {
224    // If override sets contain one another then we can try to use the more specific one.
225    // If neither one is more specific, check for semantic conflicts.
226    const specificSet = this.findSpecificOverrideSet(first, second)
227    if (specificSet !== undefined) {
228      // One contains the other, so no conflict
229      return false
230    }
231
232    // The override sets are structurally incomparable, but this doesn't necessarily
233    // mean they conflict. We need to check if they have conflicting version requirements
234    // for any package that appears in both rulesets.
235    return this.haveConflictingRules(first, second)
236  }
237
238  static haveConflictingRules (first, second) {
239    // Get all rules from both override sets
240    const firstRules = first.ruleset
241    const secondRules = second.ruleset
242
243    // Check each package that appears in both rulesets
244    for (const [key, firstRule] of firstRules) {
245      const secondRule = secondRules.get(key)
246      if (!secondRule) {
247        // Package only appears in one ruleset, no conflict
248        continue
249      }
250
251      // Same rule object means no conflict
252      if (firstRule === secondRule || firstRule.isEqual(secondRule)) {
253        continue
254      }
255
256      // Both rulesets have rules for this package with different values.
257      // Check if the version requirements are actually incompatible.
258      const firstValue = firstRule.value
259      const secondValue = secondRule.value
260
261      // If either value is a reference (starts with $), we can't determine
262      // compatibility here - the reference might resolve to compatible versions.
263      // We defer to runtime resolution rather than failing early.
264      if (firstValue.startsWith('$') || secondValue.startsWith('$')) {
265        continue
266      }
267
268      // Check if the version ranges are compatible using semver
269      // If both specify version ranges, they conflict only if they have no overlap
270      try {
271        const firstSpec = npa(`${firstRule.name}@${firstValue}`)
272        const secondSpec = npa(`${secondRule.name}@${secondValue}`)
273
274        // For range/version types, check if they intersect
275        if ((firstSpec.type === 'range' || firstSpec.type === 'version') &&
276            (secondSpec.type === 'range' || secondSpec.type === 'version')) {
277          // Check if the ranges intersect
278          const firstRange = firstSpec.fetchSpec
279          const secondRange = secondSpec.fetchSpec
280
281          // If the ranges don't intersect, we have a real conflict
282          if (!semver.intersects(firstRange, secondRange)) {
283            log.silly('Found conflicting override rules', {
284              package: firstRule.name,
285              first: firstValue,
286              second: secondValue,
287            })
288            return true
289          }
290        }
291        // For other types (git, file, directory, tag), we can't easily determine
292        // compatibility, so we conservatively assume no conflict
293      } catch {
294        // If we can't parse the specs, conservatively assume no conflict
295        // Real conflicts will be caught during dependency resolution
296      }
297    }
298
299    // No conflicting rules found
300    return false
301  }
302}
303
304module.exports = OverrideSet
305 
codekingpro/portable-devtools · Team Ai