Extract platform publishing logic into dedicated modules

This commit is contained in:
CPTProgrammer 2026-07-09 12:33:53 +08:00
parent db1cb28296
commit e57a1ac993
No known key found for this signature in database
8 changed files with 207 additions and 190 deletions

3
src/lib/utils/error.ts Normal file
View File

@ -0,0 +1,3 @@
export function errorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}

11
src/lib/utils/file.ts Normal file
View File

@ -0,0 +1,11 @@
import { readFileSync } from "node:fs";
export function readJarBlob(filePath: string): Blob {
try {
const buffer = readFileSync(filePath);
return new Blob([buffer]);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to read file "${filePath}": ${msg}`);
}
}

22
src/lib/utils/format.ts Normal file
View File

@ -0,0 +1,22 @@
import path from "node:path";
import { Template } from "@/lib/utils/template";
export function resolveVersionName(
template: string,
templates: Record<string, string> | undefined,
values: Record<string, string>,
): string {
const tmpl = new Template(template, templates);
return tmpl.format(values);
}
export function buildJarPath(
projectDir: string,
format: string,
version: string,
mcVersion: string,
): string {
const tmpl = new Template(format);
const filename = tmpl.format({ version, mc_version: mcVersion });
return path.join(projectDir, "build", "libs", filename);
}

View File

@ -0,0 +1,43 @@
import { curseforgeClient } from "@/services/clients";
import { uploadFile } from "@/lib/curseforge";
import { resolveVersionName, buildJarPath } from "@/lib/utils/format";
import { readJarBlob } from "@/lib/utils/file";
import type { UploadMetadata } from "@/types";
import type { PlatformContext } from "./types";
export async function publishCurseForgeVersion(
ctx: PlatformContext,
mcVersion: string,
): Promise<void> {
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 primaryBlob = readJarBlob(primaryPath);
const metadata: UploadMetadata = {
changelog: input.changelog,
changelogType: "markdown",
displayName,
releaseType: input.versionType,
relations: config.curseforge.relations,
};
await uploadFile(
curseforgeClient,
config.curseforge.project_id,
metadata,
primaryBlob,
);
}

View File

@ -0,0 +1,70 @@
import { modrinthClient } from "@/services/clients";
import { createVersion } from "@/lib/modrinth";
import { resolveVersionName, buildJarPath } from "@/lib/utils/format";
import { readJarBlob } from "@/lib/utils/file";
import type { CreatableVersion } from "@/types";
import type { PlatformContext } from "./types";
export async function publishModrinthVersion(
ctx: PlatformContext,
mcVersion: string,
): Promise<void> {
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 primaryBlob = readJarBlob(primaryPath);
const sourceBlob = readJarBlob(sourcePath);
const data: CreatableVersion = {
project_id: config.modrinth.project_id,
name: versionName,
version_number: versionNumber,
game_versions: [mcVersion],
version_type: input.versionType,
loaders: config.modrinth.loaders,
featured: false,
dependencies: config.modrinth.dependencies.map((d) => ({
version_id: null,
project_id: d.project_id,
file_name: null,
dependency_type: d.dependency_type,
})),
file_parts: ["primary", "sources"],
primary_file: "primary",
file_types: { sources: "sources-jar" },
environment: config.modrinth.environment,
};
await createVersion(modrinthClient, data, {
primary: primaryBlob,
sources: sourceBlob,
});
}

View File

@ -0,0 +1,7 @@
import type { PublishInput } from "../publish.schemas";
import type { getConfig } from "@/services/config";
export interface PlatformContext {
input: PublishInput;
config: Awaited<ReturnType<typeof getConfig>>;
}

View File

@ -0,0 +1,40 @@
import { z } from "zod";
// ── 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() })),
}),
changelog: z.string(),
versionType: z.enum(["release", "beta", "alpha"]),
});
export const ProgressEventSchema = z.object({
platform: z.enum(["modrinth", "curseforge"]),
current: z.number().int().min(0),
total: z.number().int().min(0),
status: z.enum(["running", "completed", "failed"]),
errors: z.array(z.string()),
});
export const PlatformResultSchema = z.object({
success: z.number().int().min(0),
fail: z.number().int().min(0),
errors: z.array(z.string()),
});
export const PublishResultSchema = z.object({
modrinth: PlatformResultSchema,
curseforge: PlatformResultSchema,
});
// ── Inferred types ─────────────────────────────────────
export type PublishInput = z.infer<typeof PublishInputSchema>;
export type ProgressEvent = z.infer<typeof ProgressEventSchema>;
export type PlatformResult = z.infer<typeof PlatformResultSchema>;
export type PublishResult = z.infer<typeof PublishResultSchema>;

View File

@ -1,53 +1,16 @@
import { initTRPC } from "@trpc/server"; import { initTRPC } from "@trpc/server";
import { z } from "zod";
import EventEmitter, { on } from "node:events"; import EventEmitter, { on } from "node:events";
import { readFileSync } from "node:fs";
import path from "node:path";
import { getConfig } from "./config"; import { getConfig } from "./config";
import { modrinthClient, curseforgeClient } from "./clients"; import { errorMessage } from "@/lib/utils/error";
import { createVersion } from "@/lib/modrinth"; import {
import { uploadFile } from "@/lib/curseforge"; PublishInputSchema,
import { Template } from "@/lib/utils/template"; type ProgressEvent,
import type { CreatableVersion, UploadMetadata } from "@/types"; type PlatformResult,
type PublishResult,
// ── Zod schemas ──────────────────────────────────────── } from "./publish.schemas";
import { publishModrinthVersion } from "./platforms/modrinth";
const PublishInputSchema = z.object({ import { publishCurseForgeVersion } from "./platforms/curseforge";
configName: z.string(), import type { PlatformContext } from "./platforms/types";
version: z.string(),
mcVersions: z.object({
modrinth: z.array(z.object({ mc_version: z.string() })),
curseforge: z.array(z.object({ mc_version: z.string() })),
}),
changelog: z.string(),
versionType: z.enum(["release", "beta", "alpha"]),
});
const ProgressEventSchema = z.object({
platform: z.enum(["modrinth", "curseforge"]),
current: z.number().int().min(0),
total: z.number().int().min(0),
status: z.enum(["running", "completed", "failed"]),
errors: z.array(z.string()),
});
const PlatformResultSchema = z.object({
success: z.number().int().min(0),
fail: z.number().int().min(0),
errors: z.array(z.string()),
});
const PublishResultSchema = z.object({
modrinth: PlatformResultSchema,
curseforge: PlatformResultSchema,
});
// ── Inferred types ─────────────────────────────────────
type PublishInput = z.infer<typeof PublishInputSchema>;
type ProgressEvent = z.infer<typeof ProgressEventSchema>;
type PlatformResult = z.infer<typeof PlatformResultSchema>;
type PublishResult = z.infer<typeof PublishResultSchema>;
// ── tRPC init ────────────────────────────────────────── // ── tRPC init ──────────────────────────────────────────
@ -57,149 +20,7 @@ const t = initTRPC.create();
const ee = new EventEmitter(); const ee = new EventEmitter();
// ── Helpers ──────────────────────────────────────────── // ── Platform orchestrator ──────────────────────────────
function resolveVersionName(
template: string,
templates: Record<string, string> | undefined,
values: Record<string, string>,
): string {
const tmpl = new Template(template, templates);
return tmpl.format(values);
}
function buildJarPath(
projectDir: string,
format: string,
version: string,
mcVersion: string,
): string {
const tmpl = new Template(format);
const filename = tmpl.format({ version, mc_version: mcVersion });
return path.join(projectDir, "build", "libs", filename);
}
function readJarBlob(filePath: string): Blob {
try {
const buffer = readFileSync(filePath);
return new Blob([buffer]);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to read file "${filePath}": ${msg}`);
}
}
function errorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
// ── Platform publish logic ─────────────────────────────
interface PlatformContext {
input: PublishInput;
config: Awaited<ReturnType<typeof getConfig>>;
}
async function publishModrinthVersion(
ctx: PlatformContext,
mcVersion: string,
): Promise<void> {
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 primaryBlob = readJarBlob(primaryPath);
const sourceBlob = readJarBlob(sourcePath);
const data: CreatableVersion = {
project_id: config.modrinth.project_id,
name: versionName,
version_number: versionNumber,
game_versions: [mcVersion],
version_type: input.versionType,
loaders: config.modrinth.loaders,
featured: false,
dependencies: config.modrinth.dependencies.map((d) => ({
version_id: null,
project_id: d.project_id,
file_name: null,
dependency_type: d.dependency_type,
})),
file_parts: ["primary", "sources"],
primary_file: "primary",
file_types: { sources: "sources-jar" },
environment: config.modrinth.environment,
};
await createVersion(modrinthClient, data, {
primary: primaryBlob,
sources: sourceBlob,
});
}
async function publishCurseForgeVersion(
ctx: PlatformContext,
mcVersion: string,
): Promise<void> {
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 primaryBlob = readJarBlob(primaryPath);
const metadata: UploadMetadata = {
changelog: input.changelog,
changelogType: "markdown",
displayName,
releaseType: input.versionType,
relations: config.curseforge.relations,
};
await uploadFile(
curseforgeClient,
config.curseforge.project_id,
metadata,
primaryBlob,
);
}
async function runPlatform( async function runPlatform(
platform: "modrinth" | "curseforge", platform: "modrinth" | "curseforge",