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; }