Add version range computation utility for MC versions

This commit is contained in:
CPTProgrammer 2026-07-10 14:59:52 +08:00
parent f7e468f28f
commit e321ecfd70
No known key found for this signature in database
2 changed files with 245 additions and 0 deletions

View File

@ -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",
});
});
});

118
src/lib/utils/mcVersion.ts Normal file
View File

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