codekingpro/portable-devtools
114k
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 