Skip to content

Commit ad446af

Browse files
fix(enhanced): support OR || operations in SemVer (#3808)
1 parent 79abcff commit ad446af

File tree

2 files changed

+108
-40
lines changed

2 files changed

+108
-40
lines changed

.changeset/early-tools-compete.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@module-federation/enhanced': patch
3+
---
4+
5+
support `||` pipes in semver parsing

packages/runtime-core/src/utils/semver/index.ts

Lines changed: 103 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,12 @@ export function satisfy(version: string, range: string): boolean {
6969
return false;
7070
}
7171

72-
const parsedRange = parseRange(range);
73-
const parsedComparator = parsedRange
74-
.split(' ')
75-
.map((rangeVersion) => parseComparatorString(rangeVersion))
76-
.join(' ');
77-
const comparators = parsedComparator
78-
.split(/\s+/)
79-
.map((comparator) => parseGTE0(comparator));
72+
// Extract version details once
8073
const extractedVersion = extractComparator(version);
81-
8274
if (!extractedVersion) {
75+
// If the version string is invalid, it can't satisfy any range
8376
return false;
8477
}
85-
8678
const [
8779
,
8880
versionOperator,
@@ -106,42 +98,113 @@ export function satisfy(version: string, range: string): boolean {
10698
preRelease: versionPreRelease?.split('.'),
10799
};
108100

109-
for (const comparator of comparators) {
110-
const extractedComparator = extractComparator(comparator);
101+
// Split the range by || to handle OR conditions
102+
const orRanges = range.split('||');
111103

112-
if (!extractedComparator) {
113-
return false;
104+
for (const orRange of orRanges) {
105+
const trimmedOrRange = orRange.trim();
106+
if (!trimmedOrRange) {
107+
// An empty range string signifies wildcard *, satisfy any valid version
108+
// (We already checked if the version itself is valid)
109+
return true;
114110
}
115111

116-
const [
117-
,
118-
rangeOperator,
119-
,
120-
rangeMajor,
121-
rangeMinor,
122-
rangePatch,
123-
rangePreRelease,
124-
] = extractedComparator;
125-
const rangeAtom: CompareAtom = {
126-
operator: rangeOperator,
127-
version: combineVersion(
128-
rangeMajor,
129-
rangeMinor,
130-
rangePatch,
131-
rangePreRelease,
132-
), // exclude build atom
133-
major: rangeMajor,
134-
minor: rangeMinor,
135-
patch: rangePatch,
136-
preRelease: rangePreRelease?.split('.'),
137-
};
138-
139-
if (!compare(rangeAtom, versionAtom)) {
140-
return false; // early return
112+
// Handle simple wildcards explicitly before complex parsing
113+
if (trimmedOrRange === '*' || trimmedOrRange === 'x') {
114+
return true;
115+
}
116+
117+
try {
118+
// Apply existing parsing logic to the current OR sub-range
119+
const parsedSubRange = parseRange(trimmedOrRange); // Handles hyphens, trims etc.
120+
121+
// Check if the result of initial parsing is empty, which can happen
122+
// for some wildcard cases handled by parseRange/parseComparatorString.
123+
// E.g. `parseStar` used in `parseComparatorString` returns ''.
124+
if (!parsedSubRange.trim()) {
125+
// If parsing results in empty string, treat as wildcard match
126+
return true;
127+
}
128+
129+
const parsedComparatorString = parsedSubRange
130+
.split(' ')
131+
.map((rangeVersion) => parseComparatorString(rangeVersion)) // Expands ^, ~
132+
.join(' ');
133+
134+
// Check again if the comparator string became empty after specific parsing like ^ or ~
135+
if (!parsedComparatorString.trim()) {
136+
return true;
137+
}
138+
139+
// Split the sub-range by space for implicit AND conditions
140+
const comparators = parsedComparatorString
141+
.split(/\s+/)
142+
.map((comparator) => parseGTE0(comparator))
143+
// Filter out empty strings that might result from multiple spaces
144+
.filter(Boolean);
145+
146+
// If a sub-range becomes empty after parsing (e.g., invalid characters),
147+
// it cannot be satisfied. This check might be redundant now but kept for safety.
148+
if (comparators.length === 0) {
149+
continue;
150+
}
151+
152+
let subRangeSatisfied = true;
153+
for (const comparator of comparators) {
154+
const extractedComparator = extractComparator(comparator);
155+
156+
// If any part of the AND sub-range is invalid, the sub-range is not satisfied
157+
if (!extractedComparator) {
158+
subRangeSatisfied = false;
159+
break;
160+
}
161+
162+
const [
163+
,
164+
rangeOperator,
165+
,
166+
rangeMajor,
167+
rangeMinor,
168+
rangePatch,
169+
rangePreRelease,
170+
] = extractedComparator;
171+
const rangeAtom: CompareAtom = {
172+
operator: rangeOperator,
173+
version: combineVersion(
174+
rangeMajor,
175+
rangeMinor,
176+
rangePatch,
177+
rangePreRelease,
178+
),
179+
major: rangeMajor,
180+
minor: rangeMinor,
181+
patch: rangePatch,
182+
preRelease: rangePreRelease?.split('.'),
183+
};
184+
185+
// Check if the version satisfies this specific comparator in the AND chain
186+
if (!compare(rangeAtom, versionAtom)) {
187+
subRangeSatisfied = false; // This part of the AND condition failed
188+
break; // No need to check further comparators in this sub-range
189+
}
190+
}
191+
192+
// If all AND conditions within this OR sub-range were met, the overall range is satisfied
193+
if (subRangeSatisfied) {
194+
return true;
195+
}
196+
} catch (e) {
197+
// Log error and treat this sub-range as unsatisfied
198+
console.error(
199+
`[semver] Error processing range part "${trimmedOrRange}":`,
200+
e,
201+
);
202+
continue;
141203
}
142204
}
143205

144-
return true;
206+
// If none of the OR sub-ranges were satisfied
207+
return false;
145208
}
146209

147210
export function isLegallyVersion(version: string): boolean {

0 commit comments

Comments
 (0)