ModReleaser/src/lib/utils/mcVersion.ts

127 lines
4.0 KiB
TypeScript
Raw Normal View History

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;
}
/**
*
*
* @param projectVersions MC
* @param allAvailable MC
* @param lastVersionEnd
*/
export function computeVersionRanges(
projectVersions: string[],
allAvailable: string[],
lastVersionEnd?: string,
): McVersionEntry[] {
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];
const inclusiveEnd = isLast && lastVersionEnd != null;
return computeEntry(version, end, sortedAvailable, inclusiveEnd);
});
}
// ── 内部实现 ──────────────────────────────────────────
function computeEntry(
start: string,
end: string | null,
allAvailable: string[],
inclusiveEnd?: boolean,
): McVersionEntry {
const range = end !== null
? collectRange(start, end, allAvailable, inclusiveEnd)
: collectAutoRange(start, allAvailable);
const lastInRange = range[range.length - 1] ?? start;
const explicit = start === lastInRange ? start : `${start}-${lastInRange}`;
const wildcard = computeWildcard(start, range, allAvailable, explicit);
return { version: start, range, wildcard, explicit };
}
/** 收集区间内的所有版本。inclusiveEnd 为 true 时使用 [start, end],否则 [start, end) */
function collectRange(
start: string,
end: string,
allAvailable: string[],
inclusiveEnd?: boolean,
): string[] {
const op = inclusiveEnd
? (v: string) => SemVer.compare(v, end) <= 0
: (v: string) => SemVer.compare(v, end) < 0;
const result = allAvailable.filter(
(v) =>
SemVer.compare(v, start) >= 0 &&
op(v),
);
if (!result.some((v) => SemVer.compare(v, start) === 0)) {
result.unshift(start);
}
return result;
}
/** 自动匹配:收集与 start 同系列的所有版本 */
export function collectAutoRange(
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;
}