ModReleaser/src/lib/utils/mcVersion.ts
CPTProgrammer 9d63c2028f
Add Modrinth/CurseForge MC version ranges to publish modal
Compute per-platform version ranges using each platform's available MC
version list and display them as tags in the publish modal.
2026-07-16 20:26:13 +08:00

127 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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