diff --git a/src/lib/utils/error.ts b/src/lib/utils/error.ts new file mode 100644 index 0000000..7db4add --- /dev/null +++ b/src/lib/utils/error.ts @@ -0,0 +1,3 @@ +export function errorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} diff --git a/src/lib/utils/file.ts b/src/lib/utils/file.ts new file mode 100644 index 0000000..644eca0 --- /dev/null +++ b/src/lib/utils/file.ts @@ -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}`); + } +} diff --git a/src/lib/utils/format.ts b/src/lib/utils/format.ts new file mode 100644 index 0000000..db53517 --- /dev/null +++ b/src/lib/utils/format.ts @@ -0,0 +1,22 @@ +import path from "node:path"; +import { Template } from "@/lib/utils/template"; + +export function resolveVersionName( + template: string, + templates: Record | undefined, + values: Record, +): 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); +} diff --git a/src/services/platforms/curseforge.ts b/src/services/platforms/curseforge.ts new file mode 100644 index 0000000..1bb50fb --- /dev/null +++ b/src/services/platforms/curseforge.ts @@ -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 { + 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, + ); +} diff --git a/src/services/platforms/modrinth.ts b/src/services/platforms/modrinth.ts new file mode 100644 index 0000000..b1fde02 --- /dev/null +++ b/src/services/platforms/modrinth.ts @@ -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 { + 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, + }); +} diff --git a/src/services/platforms/types.ts b/src/services/platforms/types.ts new file mode 100644 index 0000000..7171d57 --- /dev/null +++ b/src/services/platforms/types.ts @@ -0,0 +1,7 @@ +import type { PublishInput } from "../publish.schemas"; +import type { getConfig } from "@/services/config"; + +export interface PlatformContext { + input: PublishInput; + config: Awaited>; +} diff --git a/src/services/publish.schemas.ts b/src/services/publish.schemas.ts new file mode 100644 index 0000000..243fc40 --- /dev/null +++ b/src/services/publish.schemas.ts @@ -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; +export type ProgressEvent = z.infer; +export type PlatformResult = z.infer; +export type PublishResult = z.infer; diff --git a/src/services/publish.ts b/src/services/publish.ts index 7a4bb35..08439db 100644 --- a/src/services/publish.ts +++ b/src/services/publish.ts @@ -1,53 +1,16 @@ import { initTRPC } from "@trpc/server"; -import { z } from "zod"; import EventEmitter, { on } from "node:events"; -import { readFileSync } from "node:fs"; -import path from "node:path"; import { getConfig } from "./config"; -import { modrinthClient, curseforgeClient } from "./clients"; -import { createVersion } from "@/lib/modrinth"; -import { uploadFile } from "@/lib/curseforge"; -import { Template } from "@/lib/utils/template"; -import type { CreatableVersion, UploadMetadata } from "@/types"; - -// ── Zod schemas ──────────────────────────────────────── - -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"]), -}); - -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; -type ProgressEvent = z.infer; -type PlatformResult = z.infer; -type PublishResult = z.infer; +import { errorMessage } from "@/lib/utils/error"; +import { + PublishInputSchema, + type ProgressEvent, + type PlatformResult, + type PublishResult, +} from "./publish.schemas"; +import { publishModrinthVersion } from "./platforms/modrinth"; +import { publishCurseForgeVersion } from "./platforms/curseforge"; +import type { PlatformContext } from "./platforms/types"; // ── tRPC init ────────────────────────────────────────── @@ -57,149 +20,7 @@ const t = initTRPC.create(); const ee = new EventEmitter(); -// ── Helpers ──────────────────────────────────────────── - -function resolveVersionName( - template: string, - templates: Record | undefined, - values: Record, -): 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>; -} - -async function publishModrinthVersion( - ctx: PlatformContext, - mcVersion: string, -): Promise { - 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 { - 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, - ); -} +// ── Platform orchestrator ────────────────────────────── async function runPlatform( platform: "modrinth" | "curseforge",