|
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 |
0 commit comments