2026-07-10 14:59:52 +08:00
|
|
|
|
import { SemVer } from "./templates/semver";
|
|
|
|
|
|
|
|
|
|
|
|
/** 单个 MC 版本及其兼容范围信息 */
|
|
|
|
|
|
export interface McVersionEntry {
|
|
|
|
|
|
/** MC 版本号,如 "1.20" */
|
|
|
|
|
|
version: string;
|
|
|
|
|
|
/** 区间内所有版本,如 ["1.20", "1.20.1", "1.20.2"] */
|
|
|
|
|
|
range: string[];
|
|
|
|
|
|
/** 通配符显示,如 "1.20.x";无法通配时回退为显式范围 */
|
|
|
|
|
|
wildcard: string;
|
|
|
|
|
|
/** 显式范围显示,如 "1.20-1.20.2"、"1.20.1-1.20.3" */
|
|
|
|
|
|
explicit: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-16 20:26:13 +08:00
|
|
|
|
* 计算各项目版本的兼容范围。
|
2026-07-10 14:59:52 +08:00
|
|
|
|
*
|
2026-07-16 20:26:13 +08:00
|
|
|
|
* @param projectVersions 项目 MC 版本列表(无需排序,内部自动升序)
|
|
|
|
|
|
* @param allAvailable 全平台可用 MC 版本(无需排序,内部自动升序)
|
2026-07-16 14:17:49 +08:00
|
|
|
|
* @param lastVersionEnd 最后一个版本的截止版本(包含关系),不传则自动泛匹配
|
2026-07-10 14:59:52 +08:00
|
|
|
|
*/
|
|
|
|
|
|
export function computeVersionRanges(
|
|
|
|
|
|
projectVersions: string[],
|
|
|
|
|
|
allAvailable: string[],
|
|
|
|
|
|
lastVersionEnd?: string,
|
|
|
|
|
|
): McVersionEntry[] {
|
2026-07-16 20:26:13 +08:00
|
|
|
|
const sortedProjectVersions = [...projectVersions].sort((a, b) => SemVer.compare(a, b));
|
|
|
|
|
|
const sortedAvailable = [...allAvailable].sort((a, b) => SemVer.compare(a, b));
|
|
|
|
|
|
return sortedProjectVersions.map((version, i) => {
|
|
|
|
|
|
const isLast = i === sortedProjectVersions.length - 1;
|
|
|
|
|
|
const end = isLast ? (lastVersionEnd ?? null) : sortedProjectVersions[i + 1];
|
2026-07-16 14:17:49 +08:00
|
|
|
|
const inclusiveEnd = isLast && lastVersionEnd != null;
|
2026-07-16 20:26:13 +08:00
|
|
|
|
return computeEntry(version, end, sortedAvailable, inclusiveEnd);
|
2026-07-10 14:59:52 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 内部实现 ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
function computeEntry(
|
|
|
|
|
|
start: string,
|
|
|
|
|
|
end: string | null,
|
|
|
|
|
|
allAvailable: string[],
|
2026-07-16 14:17:49 +08:00
|
|
|
|
inclusiveEnd?: boolean,
|
2026-07-10 14:59:52 +08:00
|
|
|
|
): McVersionEntry {
|
|
|
|
|
|
const range = end !== null
|
2026-07-16 14:17:49 +08:00
|
|
|
|
? collectRange(start, end, allAvailable, inclusiveEnd)
|
2026-07-10 14:59:52 +08:00
|
|
|
|
: collectAutoRange(start, allAvailable);
|
|
|
|
|
|
|
|
|
|
|
|
const lastInRange = range[range.length - 1] ?? start;
|
2026-07-16 20:26:13 +08:00
|
|
|
|
const explicit = start === lastInRange ? start : `${start}-${lastInRange}`;
|
2026-07-10 14:59:52 +08:00
|
|
|
|
const wildcard = computeWildcard(start, range, allAvailable, explicit);
|
|
|
|
|
|
|
|
|
|
|
|
return { version: start, range, wildcard, explicit };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-16 14:17:49 +08:00
|
|
|
|
/** 收集区间内的所有版本。inclusiveEnd 为 true 时使用 [start, end],否则 [start, end) */
|
2026-07-10 14:59:52 +08:00
|
|
|
|
function collectRange(
|
|
|
|
|
|
start: string,
|
|
|
|
|
|
end: string,
|
|
|
|
|
|
allAvailable: string[],
|
2026-07-16 14:17:49 +08:00
|
|
|
|
inclusiveEnd?: boolean,
|
2026-07-10 14:59:52 +08:00
|
|
|
|
): string[] {
|
2026-07-16 14:17:49 +08:00
|
|
|
|
const op = inclusiveEnd
|
|
|
|
|
|
? (v: string) => SemVer.compare(v, end) <= 0
|
|
|
|
|
|
: (v: string) => SemVer.compare(v, end) < 0;
|
2026-07-10 14:59:52 +08:00
|
|
|
|
const result = allAvailable.filter(
|
|
|
|
|
|
(v) =>
|
|
|
|
|
|
SemVer.compare(v, start) >= 0 &&
|
2026-07-16 14:17:49 +08:00
|
|
|
|
op(v),
|
2026-07-10 14:59:52 +08:00
|
|
|
|
);
|
|
|
|
|
|
if (!result.some((v) => SemVer.compare(v, start) === 0)) {
|
|
|
|
|
|
result.unshift(start);
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 自动匹配:收集与 start 同系列的所有版本 */
|
2026-07-16 14:17:49 +08:00
|
|
|
|
export function collectAutoRange(
|
2026-07-10 14:59:52 +08:00
|
|
|
|
start: string,
|
|
|
|
|
|
allAvailable: string[],
|
|
|
|
|
|
): string[] {
|
|
|
|
|
|
const startSv = SemVer.parse(start);
|
|
|
|
|
|
if (startSv.major < 26) {
|
|
|
|
|
|
// 1.x:匹配相同 major.minor
|
|
|
|
|
|
return allAvailable.filter((v) => {
|
|
|
|
|
|
const sv = SemVer.parse(v);
|
|
|
|
|
|
return sv.major === startSv.major && sv.minor === startSv.minor;
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// >= 26:匹配相同 major
|
|
|
|
|
|
return allAvailable.filter((v) => {
|
|
|
|
|
|
const sv = SemVer.parse(v);
|
|
|
|
|
|
return sv.major === startSv.major;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 确定通配符显示:覆盖完整系列时用 .x,否则回退为 explicit */
|
|
|
|
|
|
function computeWildcard(
|
|
|
|
|
|
start: string,
|
|
|
|
|
|
range: string[],
|
|
|
|
|
|
allAvailable: string[],
|
|
|
|
|
|
explicit: string,
|
|
|
|
|
|
): string {
|
|
|
|
|
|
const startSv = SemVer.parse(start);
|
|
|
|
|
|
const completeSeries = startSv.major < 26
|
|
|
|
|
|
? allAvailable.filter((v) => {
|
|
|
|
|
|
const sv = SemVer.parse(v);
|
|
|
|
|
|
return sv.major === startSv.major && sv.minor === startSv.minor;
|
|
|
|
|
|
})
|
|
|
|
|
|
: allAvailable.filter((v) => {
|
|
|
|
|
|
const sv = SemVer.parse(v);
|
|
|
|
|
|
return sv.major === startSv.major;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const rangeSet = new Set(range);
|
|
|
|
|
|
if (completeSeries.every((v) => rangeSet.has(v))) {
|
|
|
|
|
|
if (startSv.major < 26) {
|
|
|
|
|
|
return `${startSv.major}.${startSv.minor}.x`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return `${startSv.major}.x`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return explicit;
|
|
|
|
|
|
}
|