Refactor platform publishing
Introduce a new `getMinecraftVersions` function to fetch Minecraft versions from CurseForge with caching, along with its Zod schema and exports. Refactor the publishing system to use a unified `PlatformAdaptor` interface, replace the old `PlatformContext` type, and update the publish flow to compute version ranges and pass files as structured objects. Improve `resolveVersionName` to accept a full config object and rework `buildJarPath` to use a Template instance. Add server functions to retrieve filtered MC version lists for both Modrinth and CurseForge.
This commit is contained in:
parent
e8bcd18c76
commit
ffcfa9c4f5
@ -1,6 +1,6 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { CurseForgeClient } from "./client";
|
||||
import { getVersionTypes, getGameVersions } from "./game";
|
||||
import { getVersionTypes, getGameVersions, getMinecraftVersions } from "./game";
|
||||
import secrets from "../../../secrets.json";
|
||||
|
||||
const client = new CurseForgeClient(secrets.curseforge);
|
||||
@ -18,3 +18,14 @@ describe("getGameVersions", () => {
|
||||
expect(versions.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMinecraftVersions", () => {
|
||||
test("GET /v1/minecraft/version", async () => {
|
||||
const versions = await getMinecraftVersions(client);
|
||||
expect(versions.length).toBeGreaterThan(0);
|
||||
for (const v of versions) {
|
||||
expect(v.versionString).toBeTruthy();
|
||||
expect(v.versionString).toMatch(/^\d+\.\d+/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { CurseForgeClient } from "./client";
|
||||
import {
|
||||
GameVersionTypeSchema,
|
||||
GameVersionSchema,
|
||||
MinecraftGameVersionSchema,
|
||||
type GameVersionType,
|
||||
type GameVersion,
|
||||
type MinecraftGameVersion,
|
||||
} from "@/types";
|
||||
import { createTtlCache } from "@/lib/utils/cache";
|
||||
|
||||
@ -38,3 +41,16 @@ export const getGameVersions = createTtlCache(
|
||||
},
|
||||
TTL,
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取 CurseForge 支持的 Minecraft 版本列表(1h 缓存)。
|
||||
* GET /v1/minecraft/version
|
||||
*/
|
||||
export const getMinecraftVersions = createTtlCache(
|
||||
async (client: CurseForgeClient): Promise<MinecraftGameVersion[]> => {
|
||||
const data = await client.get<unknown>("/v1/minecraft/version");
|
||||
const parsed = z.object({ data: MinecraftGameVersionSchema.array() }).parse(data);
|
||||
return parsed.data;
|
||||
},
|
||||
TTL,
|
||||
);
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export { CurseForgeClient } from "./client";
|
||||
export { getVersionTypes, getGameVersions } from "./game";
|
||||
export { getVersionTypes, getGameVersions, getMinecraftVersions } from "./game";
|
||||
export { getFiles, uploadFile, updateFile } from "./file";
|
||||
|
||||
@ -1,22 +1,30 @@
|
||||
import path from "node:path";
|
||||
import { Template } from "@/lib/utils/template";
|
||||
import type { Config } from "@/types";
|
||||
import type { McVersionEntry } from "@/lib/utils/mcVersion";
|
||||
|
||||
export function resolveVersionName(
|
||||
template: string,
|
||||
templates: Record<string, string> | undefined,
|
||||
values: Record<string, string>,
|
||||
config: Config,
|
||||
version: string,
|
||||
entry: McVersionEntry,
|
||||
): string {
|
||||
const tmpl = new Template(template, templates);
|
||||
return tmpl.format(values);
|
||||
const tmpl = new Template(template, {
|
||||
filename_format: config.filename_format,
|
||||
});
|
||||
return tmpl.format({
|
||||
version,
|
||||
mc_version: entry.version,
|
||||
mc_version_range: entry.explicit,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildJarPath(
|
||||
projectDir: string,
|
||||
format: string,
|
||||
template: Template,
|
||||
version: string,
|
||||
mcVersion: string,
|
||||
): string {
|
||||
const tmpl = new Template(format);
|
||||
const filename = tmpl.format({ version, mc_version: mcVersion });
|
||||
const filename = template.format({ version, mc_version: mcVersion });
|
||||
return path.join(projectDir, "build", "libs", filename);
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
"use server";
|
||||
|
||||
import { modrinthClient, curseforgeClient } from "./clients";
|
||||
import { getLoaders } from "@/lib/modrinth";
|
||||
import { getVersionTypes, getGameVersions } from "@/lib/curseforge";
|
||||
import { getLoaders, getGameVersions as getModrinthGameVersions } from "@/lib/modrinth";
|
||||
import { getVersionTypes, getGameVersions, getMinecraftVersions } from "@/lib/curseforge";
|
||||
import { SemVer } from "@/lib/utils/templates/semver";
|
||||
import { EnvironmentSchema } from "@/types";
|
||||
|
||||
const ENVIRONMENT_LABELS: Record<string, string> = {
|
||||
@ -27,6 +28,17 @@ export async function getModrinthLoaders(): Promise<string[]> {
|
||||
return loaders.map((l) => l.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Modrinth 支持的 Minecraft 版本号列表。
|
||||
* 用于 ConfigModal 中 game_versions 多选下拉框的选项。
|
||||
*/
|
||||
export async function getModrinthMcVersions(): Promise<string[]> {
|
||||
const versions = await getModrinthGameVersions(modrinthClient);
|
||||
return versions
|
||||
.filter((v) => v.version_type === "release")
|
||||
.map((v) => v.version);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Modrinth 运行环境枚举值,转为 Ant Design Select 的选项格式。
|
||||
* 前端使用 OptGroup 分组显示(Client / Server / Both / Other)。
|
||||
@ -77,3 +89,18 @@ export async function getCurseForgeMeta(): Promise<{
|
||||
|
||||
return { environments, loaders };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 CurseForge 支持的 Minecraft 版本号列表。
|
||||
* 通过 /v1/minecraft/version 接口获取,
|
||||
* 并排除 versionString 包含 "preRelease" 的预发布版本。
|
||||
*/
|
||||
export async function getCurseForgeMcVersions(): Promise<string[]> {
|
||||
const versions = await getMinecraftVersions(curseforgeClient);
|
||||
return versions
|
||||
.filter((v) => {
|
||||
const sv = SemVer.parse(v.versionString);
|
||||
return sv.preRelease === null;
|
||||
})
|
||||
.map((v) => v.versionString);
|
||||
}
|
||||
|
||||
@ -1,30 +1,13 @@
|
||||
import { curseforgeClient } from "@/services/clients";
|
||||
import { uploadFile, getGameVersions } from "@/lib/curseforge";
|
||||
import { resolveVersionName, buildJarPath } from "@/lib/utils/format";
|
||||
import { readAsFile } from "@/lib/utils/file";
|
||||
import { resolveVersionName } from "@/lib/utils/format";
|
||||
import type { UploadMetadata } from "@/types";
|
||||
import type { PlatformContext } from "./types";
|
||||
import { PlatformPublishFn } from "../publish.schemas";
|
||||
|
||||
export async function publishCurseForgeVersion(
|
||||
ctx: PlatformContext,
|
||||
mcVersion: string,
|
||||
): Promise<void> {
|
||||
export const publishCurseForgeVersion: PlatformPublishFn = async (ctx, mcVersion, files) => {
|
||||
const { input, config } = ctx;
|
||||
|
||||
const displayName = resolveVersionName(
|
||||
config.curseforge.version_name,
|
||||
{ filename_format: config.filename_format },
|
||||
{ version: input.version, mc_version: mcVersion },
|
||||
);
|
||||
|
||||
const primaryPath = buildJarPath(
|
||||
config.project_dir,
|
||||
config.filename_format,
|
||||
input.version,
|
||||
mcVersion,
|
||||
);
|
||||
|
||||
const primaryFile = await readAsFile(primaryPath);
|
||||
const displayName = resolveVersionName(config.curseforge.version_name, config, input.version, mcVersion);
|
||||
|
||||
const gameVersions = await getGameVersions(curseforgeClient);
|
||||
|
||||
@ -40,9 +23,9 @@ export async function publishCurseForgeVersion(
|
||||
displayName,
|
||||
releaseType: input.versionType,
|
||||
gameVersions: [
|
||||
resolveId(mcVersion),
|
||||
...config.curseforge.loaders.map(resolveId),
|
||||
...config.curseforge.environment.map(resolveId),
|
||||
...mcVersion.range.map(resolveId),
|
||||
],
|
||||
relations: config.curseforge.relations,
|
||||
};
|
||||
@ -51,6 +34,6 @@ export async function publishCurseForgeVersion(
|
||||
curseforgeClient,
|
||||
config.curseforge.project_id,
|
||||
metadata,
|
||||
primaryFile,
|
||||
files.primary,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,54 +1,21 @@
|
||||
import { modrinthClient } from "@/services/clients";
|
||||
import { createVersion } from "@/lib/modrinth";
|
||||
import { resolveVersionName, buildJarPath } from "@/lib/utils/format";
|
||||
import { readAsFile } from "@/lib/utils/file";
|
||||
import { resolveVersionName } from "@/lib/utils/format";
|
||||
import type { CreatableVersion } from "@/types";
|
||||
import type { PlatformContext } from "./types";
|
||||
import { PlatformPublishFn } from "../publish.schemas";
|
||||
|
||||
export async function publishModrinthVersion(
|
||||
ctx: PlatformContext,
|
||||
mcVersion: string,
|
||||
): Promise<void> {
|
||||
export const publishModrinthVersion: PlatformPublishFn = async (ctx, mcVersion, files) => {
|
||||
const { input, config } = ctx;
|
||||
|
||||
const versionNumber = resolveVersionName(
|
||||
config.modrinth.version,
|
||||
undefined,
|
||||
{ version: input.version, mc_version: mcVersion },
|
||||
);
|
||||
|
||||
const versionName = resolveVersionName(
|
||||
config.modrinth.version_name,
|
||||
undefined,
|
||||
{
|
||||
version: input.version,
|
||||
mc_version: mcVersion,
|
||||
mc_version_range: mcVersion,
|
||||
},
|
||||
);
|
||||
|
||||
const primaryPath = buildJarPath(
|
||||
config.project_dir,
|
||||
config.filename_format,
|
||||
input.version,
|
||||
mcVersion,
|
||||
);
|
||||
const sourcePath = buildJarPath(
|
||||
config.project_dir,
|
||||
config.source_filename_format,
|
||||
input.version,
|
||||
mcVersion,
|
||||
);
|
||||
|
||||
const primaryFile = await readAsFile(primaryPath);
|
||||
const sourceFile = await readAsFile(sourcePath);
|
||||
const versionNumber = resolveVersionName(config.modrinth.version, ctx.config, input.version, mcVersion);
|
||||
const versionName = resolveVersionName(config.modrinth.version_name, ctx.config, input.version, mcVersion);
|
||||
|
||||
const data: CreatableVersion = {
|
||||
project_id: config.modrinth.project_id,
|
||||
name: versionName,
|
||||
version_number: versionNumber,
|
||||
changelog: input.changelog,
|
||||
game_versions: [mcVersion],
|
||||
game_versions: mcVersion.range,
|
||||
version_type: input.versionType,
|
||||
loaders: config.modrinth.loaders,
|
||||
featured: false,
|
||||
@ -65,7 +32,7 @@ export async function publishModrinthVersion(
|
||||
};
|
||||
|
||||
await createVersion(modrinthClient, data, {
|
||||
primary: primaryFile,
|
||||
sources: sourceFile,
|
||||
primary: files.primary,
|
||||
sources: files.source,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
import { Config } from "@/types";
|
||||
import type { PublishInput } from "../publish.schemas";
|
||||
|
||||
export interface PlatformContext {
|
||||
input: PublishInput;
|
||||
config: Config;
|
||||
}
|
||||
@ -1,20 +1,44 @@
|
||||
import { McVersionEntry } from "@/lib/utils/mcVersion";
|
||||
import { Config } from "@/types";
|
||||
import { z } from "zod";
|
||||
|
||||
export const platforms = ["modrinth", "curseforge"] as const;
|
||||
|
||||
export interface PlatformContext {
|
||||
input: PublishInput;
|
||||
config: Config;
|
||||
}
|
||||
export type PlatformPublishFn = (
|
||||
ctx: PlatformContext,
|
||||
mcVersion: McVersionEntry,
|
||||
files: {
|
||||
primary: File,
|
||||
source: File,
|
||||
}
|
||||
) => Promise<void>;
|
||||
export type PlatformGetGameVersionsFn = () => Promise<string[]>;
|
||||
|
||||
export type PlatformAdaptor = {
|
||||
publish: PlatformPublishFn,
|
||||
getGameVersions: PlatformGetGameVersionsFn,
|
||||
}
|
||||
|
||||
// ── Schemas ────────────────────────────────────────────
|
||||
|
||||
export const PublishInputSchema = z.object({
|
||||
configName: z.string(),
|
||||
version: z.string(),
|
||||
mcVersions: z.object({
|
||||
modrinth: z.array(z.object({ mc_version: z.string() })),
|
||||
curseforge: z.array(z.object({ mc_version: z.string() })),
|
||||
}),
|
||||
mcVersions: z.object(Object.fromEntries(platforms.map(platform => [
|
||||
platform,
|
||||
z.array(z.object({ mc_version: z.string() }))
|
||||
]))),
|
||||
cutoffMcVersion: z.string(), // 最后一个版本的截止 MC 版本,两个平台共用
|
||||
changelog: z.string(),
|
||||
versionType: z.enum(["release", "beta", "alpha"]),
|
||||
});
|
||||
|
||||
export const ProgressEventSchema = z.object({
|
||||
platform: z.enum(["modrinth", "curseforge"]),
|
||||
platform: z.enum(platforms),
|
||||
current: z.number().int().min(0),
|
||||
total: z.number().int().min(0),
|
||||
status: z.enum(["running", "completed", "failed"]),
|
||||
|
||||
@ -7,10 +7,30 @@ import {
|
||||
type ProgressEvent,
|
||||
type PlatformResult,
|
||||
type PublishResult,
|
||||
platforms,
|
||||
type PlatformContext,
|
||||
type PlatformAdaptor,
|
||||
} from "./publish.schemas";
|
||||
import { publishModrinthVersion } from "./platforms/modrinth";
|
||||
import { publishCurseForgeVersion } from "./platforms/curseforge";
|
||||
import type { PlatformContext } from "./platforms/types";
|
||||
import { computeVersionRanges } from "@/lib/utils/mcVersion";
|
||||
import { getCurseForgeMcVersions, getModrinthMcVersions } from "./meta";
|
||||
import { readAsFile } from "@/lib/utils/file";
|
||||
import { Template } from "@/lib/utils/template";
|
||||
import { buildJarPath } from "@/lib/utils/format";
|
||||
|
||||
export const platformAdaptors: {
|
||||
[key in typeof platforms[number]]: PlatformAdaptor
|
||||
} = {
|
||||
modrinth: {
|
||||
publish: publishModrinthVersion,
|
||||
getGameVersions: getModrinthMcVersions,
|
||||
},
|
||||
curseforge: {
|
||||
publish: publishCurseForgeVersion,
|
||||
getGameVersions: getCurseForgeMcVersions,
|
||||
},
|
||||
};
|
||||
|
||||
// ── tRPC init ──────────────────────────────────────────
|
||||
|
||||
@ -23,7 +43,7 @@ const ee = new EventEmitter();
|
||||
// ── Platform orchestrator ──────────────────────────────
|
||||
|
||||
async function runPlatform(
|
||||
platform: "modrinth" | "curseforge",
|
||||
platform: typeof platforms[number],
|
||||
mcVersions: { mc_version: string }[],
|
||||
ctx: PlatformContext,
|
||||
): Promise<PlatformResult> {
|
||||
@ -50,16 +70,29 @@ async function runPlatform(
|
||||
errors: [],
|
||||
} satisfies ProgressEvent);
|
||||
|
||||
const publishFn =
|
||||
platform === "modrinth"
|
||||
? publishModrinthVersion
|
||||
: publishCurseForgeVersion;
|
||||
const publishFn = platformAdaptors[platform].publish;
|
||||
const getGameVersionsFn = platformAdaptors[platform].getGameVersions;
|
||||
|
||||
for (let i = 0; i < mcVersions.length; i++) {
|
||||
const { mc_version } = mcVersions[i];
|
||||
const mcVersionEntries = computeVersionRanges(
|
||||
mcVersions.map(v => v.mc_version),
|
||||
await getGameVersionsFn(),
|
||||
ctx.input.cutoffMcVersion
|
||||
);
|
||||
|
||||
const primaryFileTemplate = new Template(ctx.config.filename_format);
|
||||
const sourceFileTemplate = new Template(ctx.config.source_filename_format);
|
||||
|
||||
for (let i = 0; i < mcVersionEntries.length; i++) {
|
||||
const mcVersionEntry = mcVersionEntries[i];
|
||||
|
||||
try {
|
||||
await publishFn(ctx, mc_version);
|
||||
const primaryFile = await readAsFile(buildJarPath(ctx.config.project_dir, primaryFileTemplate, ctx.input.version, mcVersionEntry.version));
|
||||
const sourceFile = await readAsFile(buildJarPath(ctx.config.project_dir, sourceFileTemplate, ctx.input.version, mcVersionEntry.version));
|
||||
|
||||
await publishFn(ctx, mcVersionEntry, {
|
||||
primary: primaryFile,
|
||||
source: sourceFile,
|
||||
});
|
||||
result.success++;
|
||||
|
||||
const isLast = i + 1 === total;
|
||||
@ -72,7 +105,7 @@ async function runPlatform(
|
||||
} satisfies ProgressEvent);
|
||||
} catch (err) {
|
||||
result.fail++;
|
||||
const msg = `${mc_version}: ${errorMessage(err)}`;
|
||||
const msg = `${mcVersionEntry.version}: ${errorMessage(err)}`;
|
||||
result.errors.push(msg);
|
||||
|
||||
ee.emit("progress", {
|
||||
@ -101,10 +134,9 @@ export const appRouter = t.router({
|
||||
|
||||
const ctx: PlatformContext = { input, config };
|
||||
|
||||
const [modrinthSettled, curseforgeSettled] = await Promise.allSettled([
|
||||
runPlatform("modrinth", input.mcVersions.modrinth, ctx),
|
||||
runPlatform("curseforge", input.mcVersions.curseforge, ctx),
|
||||
]);
|
||||
const [modrinthSettled, curseforgeSettled] = await Promise.allSettled(
|
||||
platforms.map(platform => runPlatform(platform, input.mcVersions[platform], ctx))
|
||||
);
|
||||
|
||||
const modrinth: PlatformResult = modrinthSettled.status === "fulfilled"
|
||||
? modrinthSettled.value
|
||||
|
||||
@ -27,3 +27,29 @@ export const GameVersionSchema = z.object({
|
||||
});
|
||||
export type GameVersion = z.infer<typeof GameVersionSchema>;
|
||||
// endregion
|
||||
|
||||
// region MinecraftGameVersion — GET /v1/minecraft/version 响应元素
|
||||
export const MinecraftGameVersionSchema = z.object({
|
||||
/** 版本记录 ID */
|
||||
id: z.number().int(),
|
||||
/** Game Version ID */
|
||||
gameVersionId: z.number().int(),
|
||||
/** 版本号字符串,如 "1.21.5" */
|
||||
versionString: z.string(),
|
||||
/** JAR 文件下载地址 */
|
||||
jarDownloadUrl: z.string(),
|
||||
/** JSON 文件下载地址 */
|
||||
jsonDownloadUrl: z.string(),
|
||||
/** 是否已审核通过 */
|
||||
approved: z.boolean(),
|
||||
/** 最后修改时间(ISO 8601) */
|
||||
dateModified: z.string(),
|
||||
/** Game Version Type ID */
|
||||
gameVersionTypeId: z.number().int(),
|
||||
/** 版本状态:1=Approved 2=Deleted 3=New */
|
||||
gameVersionStatus: z.union([z.literal(1), z.literal(2), z.literal(3)]),
|
||||
/** 版本类型状态:1=Normal 2=Deleted */
|
||||
gameVersionTypeStatus: z.union([z.literal(1), z.literal(2)]),
|
||||
});
|
||||
export type MinecraftGameVersion = z.infer<typeof MinecraftGameVersionSchema>;
|
||||
// endregion
|
||||
|
||||
@ -30,8 +30,10 @@ export {
|
||||
export {
|
||||
GameVersionTypeSchema,
|
||||
GameVersionSchema,
|
||||
MinecraftGameVersionSchema,
|
||||
type GameVersionType,
|
||||
type GameVersion,
|
||||
type MinecraftGameVersion,
|
||||
} from "./game";
|
||||
|
||||
export {
|
||||
|
||||
@ -19,7 +19,7 @@ export const GameVersionTagSchema = z.object({
|
||||
/** 版本类型:release | snapshot | alpha | beta */
|
||||
version_type: z.enum(["release", "snapshot", "alpha", "beta"]),
|
||||
/** 版本发布日期(ISO-8601) */
|
||||
date: z.string().datetime(),
|
||||
date: z.string(),
|
||||
/** 是否为主要版本,用于 Featured Versions 标记 */
|
||||
major: z.boolean(),
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user