Add version range computation utility for MC versions
This commit is contained in:
parent
f7e468f28f
commit
e321ecfd70
127
src/lib/utils/mcVersion.test.ts
Normal file
127
src/lib/utils/mcVersion.test.ts
Normal 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
118
src/lib/utils/mcVersion.ts
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user