From 7aa84fd41e01bcdcbeec09fbcee088fea106abc0 Mon Sep 17 00:00:00 2001 From: CPTProgrammer <46586216+CPTProgrammer@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:29:00 +0800 Subject: [PATCH] Refactor type-safe constants and extract utility helpers --- src/components/ConfigModal.tsx | 30 +++++++++++++++-------------- src/components/PublishModal.tsx | 34 ++++++++++++++++++++++----------- src/lib/utils/cache.ts | 10 +++++----- src/lib/utils/createRecord.ts | 9 +++++++++ src/lib/utils/format.ts | 3 ++- src/lib/utils/gradle.ts | 5 +++++ src/services/meta.ts | 10 ++++------ src/services/project.ts | 3 ++- src/services/publish.schemas.ts | 18 ++++++++--------- src/services/publish.ts | 9 ++++++--- 10 files changed, 81 insertions(+), 50 deletions(-) create mode 100644 src/lib/utils/createRecord.ts create mode 100644 src/lib/utils/gradle.ts diff --git a/src/components/ConfigModal.tsx b/src/components/ConfigModal.tsx index 4dec2ab..5444636 100644 --- a/src/components/ConfigModal.tsx +++ b/src/components/ConfigModal.tsx @@ -20,26 +20,28 @@ import { getModrinthEnvironments, getCurseForgeMeta, } from "@/services/meta"; -import type { Config } from "@/types"; +import type { Config, DependencyType, UploadRelationType } from "@/types"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -const DEPENDENCY_TYPES = [ - { label: "Required", value: "required" }, - { label: "Optional", value: "optional" }, - { label: "Incompatible", value: "incompatible" }, - { label: "Embedded", value: "embedded" }, -]; +const DEPENDENCY_TYPE_LABEL_MAP: Record = { + required: "Required", + optional: "Optional", + incompatible: "Incompatible", + embedded: "Embedded" +}; +const DEPENDENCY_TYPES = Object.entries(DEPENDENCY_TYPE_LABEL_MAP).map(v => ({ value: v[0], label: v[1] })); -const RELATION_TYPES = [ - { label: "Embedded Library", value: "embeddedLibrary" }, - { label: "Incompatible", value: "incompatible" }, - { label: "Optional Dependency", value: "optionalDependency" }, - { label: "Required Dependency", value: "requiredDependency" }, - { label: "Tool", value: "tool" }, -]; +const RELATION_TYPE_LABEL_MAP: Record = { + incompatible: "Incompatible", + embeddedLibrary: "Embedded Library", + optionalDependency: "Optional Dependency", + requiredDependency: "Required Dependency", + tool: "Tool" +}; +const RELATION_TYPES = Object.entries(RELATION_TYPE_LABEL_MAP).map(v => ({ value: v[0], label: v[1] })); // --------------------------------------------------------------------------- // Data-fetching hooks — each Select loads independently, no blocking diff --git a/src/components/PublishModal.tsx b/src/components/PublishModal.tsx index 83e8572..6824ed6 100644 --- a/src/components/PublishModal.tsx +++ b/src/components/PublishModal.tsx @@ -16,8 +16,9 @@ import { Empty, } from "antd"; import { ReloadOutlined } from "@ant-design/icons"; -import type { Config } from "@/types"; +import type { BaseVersion, Config, UploadReleaseType } from "@/types"; import type { ParsedProject } from "@/services/project"; +import { platforms } from "@/services/publish.schemas"; const { Text } = Typography; @@ -44,6 +45,19 @@ interface PublishModalProps { onClose: () => void; } +type ReleaseType = Extract; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const RELEASE_TYPE_LABEL_MAP: Record = { + release: "Release", + beta: "Beta", + alpha: "Alpha" +}; +const RELEASE_TYPES = Object.entries(RELEASE_TYPE_LABEL_MAP).map(v => ({ value: v[0], label: v[1] })); + // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- @@ -63,16 +77,18 @@ export function PublishModal({ // Publish settings const [changelog, setChangelog] = useState(""); - const [versionType, setVersionType] = useState<"release" | "beta" | "alpha">( + const [versionType, setVersionType] = useState( "release", ); // Progress const [publishing, setPublishing] = useState(false); - const [progress, setProgress] = useState<{ - modrinth: { done: number; total: number }; - curseforge: { done: number; total: number }; - }>({ modrinth: { done: 0, total: 0 }, curseforge: { done: 0, total: 0 } }); + const [progress, setProgress] = useState< + Record + >({ + modrinth: { done: 0, total: 0 }, + curseforge: { done: 0, total: 0 } + }); // Platform existing versions — loaded when modal opens const [loadingExisting, setLoadingExisting] = useState(false); @@ -320,11 +336,7 @@ export function PublishModal({ value={versionType} onChange={(v) => setVersionType(v)} style={{ width: 160 }} - options={[ - { label: "Release", value: "release" }, - { label: "Beta", value: "beta" }, - { label: "Alpha", value: "alpha" }, - ]} + options={RELEASE_TYPES} /> diff --git a/src/lib/utils/cache.ts b/src/lib/utils/cache.ts index 89e5030..61f1ceb 100644 --- a/src/lib/utils/cache.ts +++ b/src/lib/utils/cache.ts @@ -1,9 +1,9 @@ -export function createTtlCache( - factory: (...args: any[]) => Promise, +export function createTtlCache( + factory: (...args: A) => Promise, ttlMs: number, - keyFn?: (...args: any[]) => string, -): (...args: any[]) => Promise { - const cache = new Map(); + keyFn?: (...args: A) => string, +): (...args: A) => Promise { + const cache = new Map(); return async (...args) => { const key = keyFn ? keyFn(...args) : "_"; diff --git a/src/lib/utils/createRecord.ts b/src/lib/utils/createRecord.ts new file mode 100644 index 0000000..59c91b1 --- /dev/null +++ b/src/lib/utils/createRecord.ts @@ -0,0 +1,9 @@ +export function createRecord( + keys: K, value: V +): Record { + const result = {} as Record; + for (const key of keys) { + result[key as K[number]] = value; + } + return result; +} diff --git a/src/lib/utils/format.ts b/src/lib/utils/format.ts index 0a65d06..8e01abf 100644 --- a/src/lib/utils/format.ts +++ b/src/lib/utils/format.ts @@ -2,6 +2,7 @@ import path from "node:path"; import { Template } from "@/lib/utils/template"; import type { Config } from "@/types"; import type { McVersionEntry } from "@/lib/utils/mcVersion"; +import { getBuildDir } from "./gradle"; export function resolveVersionName( template: string, @@ -26,5 +27,5 @@ export function buildJarPath( mcVersion: string, ): string { const filename = template.format({ version, mc_version: mcVersion }); - return path.join(projectDir, "build", "libs", filename); + return path.join(getBuildDir(projectDir), filename); } diff --git a/src/lib/utils/gradle.ts b/src/lib/utils/gradle.ts new file mode 100644 index 0000000..820f9b6 --- /dev/null +++ b/src/lib/utils/gradle.ts @@ -0,0 +1,5 @@ +import path from "path"; + +export function getBuildDir(projectDir: string) { + return path.join(projectDir, "build", "libs"); +} diff --git a/src/services/meta.ts b/src/services/meta.ts index 8fee3f4..7be5370 100644 --- a/src/services/meta.ts +++ b/src/services/meta.ts @@ -4,9 +4,9 @@ import { modrinthClient, curseforgeClient } from "./clients"; 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"; +import { Environment, EnvironmentSchema } from "@/types"; -const ENVIRONMENT_LABELS: Record = { +const ENVIRONMENT_LABELS: Record = { unknown: "Unknown environment", client_only: "Client-side only", server_only: "Server-side only (singleplayer compatible)", @@ -43,12 +43,10 @@ export async function getModrinthMcVersions(): Promise { * 获取 Modrinth 运行环境枚举值,转为 Ant Design Select 的选项格式。 * 前端使用 OptGroup 分组显示(Client / Server / Both / Other)。 */ -export async function getModrinthEnvironments(): Promise< - { value: string; label: string }[] -> { +export async function getModrinthEnvironments() { return EnvironmentSchema.options.map((value) => ({ value, - label: ENVIRONMENT_LABELS[value] ?? value, + label: ENVIRONMENT_LABELS[value], })); } diff --git a/src/services/project.ts b/src/services/project.ts index 29ca281..ba25050 100644 --- a/src/services/project.ts +++ b/src/services/project.ts @@ -4,6 +4,7 @@ import { readFile, readdir, stat } from "fs/promises"; import path from "path"; import type { Config } from "@/types"; import { Template } from "@/lib/utils/template"; +import { getBuildDir } from "@/lib/utils/gradle"; export interface ParsedProject { version: string; @@ -64,7 +65,7 @@ export async function parseProject(config: Config): Promise { .map((f) => f.slice(0, -".properties".length)); // 3. Format filenames and check existence under build/libs/ - const buildDir = path.join(config.project_dir, "build", "libs"); + const buildDir = getBuildDir(config.project_dir); const artifactTmpl = new Template(config.filename_format); const sourcesTmpl = new Template(config.source_filename_format); diff --git a/src/services/publish.schemas.ts b/src/services/publish.schemas.ts index b470f4c..51f4f66 100644 --- a/src/services/publish.schemas.ts +++ b/src/services/publish.schemas.ts @@ -1,5 +1,6 @@ +import { createRecord } from "@/lib/utils/createRecord"; import { McVersionEntry } from "@/lib/utils/mcVersion"; -import { Config } from "@/types"; +import { BaseVersionSchema, Config } from "@/types"; import { z } from "zod"; export const platforms = ["modrinth", "curseforge"] as const; @@ -28,13 +29,12 @@ export type PlatformAdaptor = { export const PublishInputSchema = z.object({ configName: z.string(), version: z.string(), - mcVersions: z.object(Object.fromEntries(platforms.map(platform => [ - platform, - z.array(z.object({ mc_version: z.string() })) - ]))), + mcVersions: z.object(createRecord( + platforms, z.array(z.object({ mc_version: z.string() })) + )), cutoffMcVersion: z.string(), // 最后一个版本的截止 MC 版本,两个平台共用 changelog: z.string(), - versionType: z.enum(["release", "beta", "alpha"]), + versionType: BaseVersionSchema.shape.version_type, }); export const ProgressEventSchema = z.object({ @@ -51,9 +51,9 @@ export const PlatformResultSchema = z.object({ errors: z.array(z.string()), }); -export const PublishResultSchema = z.object(Object.fromEntries(platforms.map(platform => [ - platform, PlatformResultSchema -]))); +export const PublishResultSchema = z.object(createRecord( + platforms, PlatformResultSchema +)); // ── Inferred types ───────────────────────────────────── diff --git a/src/services/publish.ts b/src/services/publish.ts index 28c9fe4..aba0b74 100644 --- a/src/services/publish.ts +++ b/src/services/publish.ts @@ -10,6 +10,9 @@ import { platforms, type PlatformContext, type PlatformAdaptor, + type PublishInput, + PublishResultSchema, + ProgressEventSchema, } from "./publish.schemas"; import { publishModrinthVersion } from "./platforms/modrinth"; import { publishCurseForgeVersion } from "./platforms/curseforge"; @@ -44,7 +47,7 @@ const ee = new EventEmitter(); async function runPlatform( platform: typeof platforms[number], - mcVersions: { mc_version: string }[], + mcVersions: PublishInput["mcVersions"][keyof PublishInput["mcVersions"]], ctx: PlatformContext, ): Promise { const result: PlatformResult = { success: 0, fail: 0, errors: [] }; @@ -146,14 +149,14 @@ export const appRouter = t.router({ } })); - return Object.fromEntries(results.map(result => [result.platform, result.result])); + return PublishResultSchema.parse(Object.fromEntries(results.map(result => [result.platform, result.result]))); }), progress: t.procedure.subscription(async function* (opts) { for await (const [data] of on(ee, "progress", { signal: opts.signal, })) { - yield data as ProgressEvent; + yield ProgressEventSchema.parse(data); } }), });