From e321ecfd70d9a88e1b00721d5273078e93b5e542 Mon Sep 17 00:00:00 2001 From: CPTProgrammer <46586216+CPTProgrammer@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:59:52 +0800 Subject: [PATCH] Add version range computation utility for MC versions --- src/lib/utils/mcVersion.test.ts | 127 ++++++++++++++++++++++++++++++++ src/lib/utils/mcVersion.ts | 118 +++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 src/lib/utils/mcVersion.test.ts create mode 100644 src/lib/utils/mcVersion.ts diff --git a/src/lib/utils/mcVersion.test.ts b/src/lib/utils/mcVersion.test.ts new file mode 100644 index 0000000..7e62fc5 --- /dev/null +++ b/src/lib/utils/mcVersion.test.ts @@ -0,0 +1,127 @@ +import { describe, expect, test } from "vitest"; +import { computeVersionRanges } from "./mcVersion"; + +const ALL_AVAILABLE = [ + "1.19", + "1.19.1", + "1.19.2", + "1.20", + "1.20.1", + "1.20.2", + "1.20.3", + "1.20.4", + "1.21", + "1.21.1", + "1.21.2", + "1.21.3", + "1.21.4", + "26.1", + "26.2", + "26.3", + "27.0", + "27.1", +]; + +describe("computeVersionRanges", () => { + test("非最后版本:相邻范围", () => { + const result = computeVersionRanges( + ["1.20", "1.20.3", "1.21"], + ALL_AVAILABLE, + ); + // [1.20, 1.20.3) → 1.20, 1.20.1, 1.20.2,不含 1.20.3/1.20.4,未覆盖完整 1.20.x + expect(result[0]).toMatchObject({ + version: "1.20", + range: ["1.20", "1.20.1", "1.20.2"], + wildcard: "1.20-1.20.2", + explicit: "1.20-1.20.2", + }); + // [1.20.3, 1.21) → 1.20.3, 1.20.4 + expect(result[1]).toMatchObject({ + version: "1.20.3", + range: ["1.20.3", "1.20.4"], + wildcard: "1.20.3-1.20.4", + explicit: "1.20.3-1.20.4", + }); + }); + + test("非最后版本:紧邻无间隙", () => { + const result = computeVersionRanges( + ["1.20.3", "1.20.4"], + ALL_AVAILABLE, + ); + expect(result[0].range).toEqual(["1.20.3"]); + expect(result[0].explicit).toBe("1.20.3-1.20.3"); + }); + + test("最后版本:自动泛匹配 1.x", () => { + const result = computeVersionRanges( + ["1.20.3", "1.21"], + ALL_AVAILABLE, + ); + const last = result[1]; + expect(last.version).toBe("1.21"); + expect(last.range).toEqual(["1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4"]); + expect(last.wildcard).toBe("1.21.x"); + expect(last.explicit).toBe("1.21-1.21.4"); + }); + + test("最后版本:自动泛匹配 >=26", () => { + const result = computeVersionRanges( + ["26.1", "27.0"], + ALL_AVAILABLE, + ); + // [26.1, 27.0) 包含 26.1~26.3 — 完整 26.x 系列 + expect(result[0]).toMatchObject({ + version: "26.1", + range: ["26.1", "26.2", "26.3"], + wildcard: "26.x", + explicit: "26.1-26.3", + }); + const last = result[1]; + expect(last.version).toBe("27.0"); + expect(last.range).toEqual(["27.0", "27.1"]); + expect(last.wildcard).toBe("27.x"); + expect(last.explicit).toBe("27.0-27.1"); + }); + + test("最后版本:手动指定截止版本", () => { + const result = computeVersionRanges( + ["1.20", "1.20.3", "1.21"], + ALL_AVAILABLE, + "1.21.3", + ); + const last = result[2]; + // [1.21, 1.21.3) → 1.21, 1.21.1, 1.21.2 + expect(last.range).toEqual(["1.21", "1.21.1", "1.21.2"]); + expect(last.wildcard).toBe("1.21-1.21.2"); + expect(last.explicit).toBe("1.21-1.21.2"); + }); + + test("单个版本:自动泛匹配", () => { + const result = computeVersionRanges( + ["1.21"], + ALL_AVAILABLE, + ); + expect(result[0]).toMatchObject({ + version: "1.21", + range: ["1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4"], + wildcard: "1.21.x", + explicit: "1.21-1.21.4", + }); + }); + + test("最后版本手动选完整系列:显示通配符", () => { + const result = computeVersionRanges( + ["26.1"], + ALL_AVAILABLE, + "27.0", + ); + // [26.1, 27.0) 包含 26.1~26.3 — 完整 26.x 系列 + expect(result[0]).toMatchObject({ + version: "26.1", + range: ["26.1", "26.2", "26.3"], + wildcard: "26.x", + explicit: "26.1-26.3", + }); + }); +}); diff --git a/src/lib/utils/mcVersion.ts b/src/lib/utils/mcVersion.ts new file mode 100644 index 0000000..bde42ed --- /dev/null +++ b/src/lib/utils/mcVersion.ts @@ -0,0 +1,118 @@ +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[] { + return projectVersions.map((version, i) => { + const isLast = i === projectVersions.length - 1; + const end = isLast ? (lastVersionEnd ?? null) : projectVersions[i + 1]; + return computeEntry(version, end, allAvailable); + }); +} + +// ── 内部实现 ────────────────────────────────────────── + +function computeEntry( + start: string, + end: string | null, + allAvailable: string[], +): McVersionEntry { + const range = end !== null + ? collectRange(start, end, allAvailable) + : collectAutoRange(start, allAvailable); + + const lastInRange = range[range.length - 1] ?? start; + const explicit = `${start}-${lastInRange}`; + const wildcard = computeWildcard(start, range, allAvailable, explicit); + + return { version: start, range, wildcard, explicit }; +} + +/** 收集 [start, end) 区间内的所有版本 */ +function collectRange( + start: string, + end: string, + allAvailable: string[], +): string[] { + const result = allAvailable.filter( + (v) => + SemVer.compare(v, start) >= 0 && + SemVer.compare(v, end) < 0, + ); + if (!result.some((v) => SemVer.compare(v, start) === 0)) { + result.unshift(start); + } + return result; +} + +/** 自动匹配:收集与 start 同系列的所有版本 */ +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; +}