Refactor type-safe constants and extract utility helpers

This commit is contained in:
CPTProgrammer 2026-07-13 09:29:00 +08:00
parent 77b7db9dd1
commit 7aa84fd41e
No known key found for this signature in database
10 changed files with 81 additions and 50 deletions

View File

@ -20,26 +20,28 @@ import {
getModrinthEnvironments, getModrinthEnvironments,
getCurseForgeMeta, getCurseForgeMeta,
} from "@/services/meta"; } from "@/services/meta";
import type { Config } from "@/types"; import type { Config, DependencyType, UploadRelationType } from "@/types";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const DEPENDENCY_TYPES = [ const DEPENDENCY_TYPE_LABEL_MAP: Record<DependencyType, string> = {
{ label: "Required", value: "required" }, required: "Required",
{ label: "Optional", value: "optional" }, optional: "Optional",
{ label: "Incompatible", value: "incompatible" }, incompatible: "Incompatible",
{ label: "Embedded", value: "embedded" }, embedded: "Embedded"
]; };
const DEPENDENCY_TYPES = Object.entries(DEPENDENCY_TYPE_LABEL_MAP).map(v => ({ value: v[0], label: v[1] }));
const RELATION_TYPES = [ const RELATION_TYPE_LABEL_MAP: Record<UploadRelationType, string> = {
{ label: "Embedded Library", value: "embeddedLibrary" }, incompatible: "Incompatible",
{ label: "Incompatible", value: "incompatible" }, embeddedLibrary: "Embedded Library",
{ label: "Optional Dependency", value: "optionalDependency" }, optionalDependency: "Optional Dependency",
{ label: "Required Dependency", value: "requiredDependency" }, requiredDependency: "Required Dependency",
{ label: "Tool", value: "tool" }, 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 // Data-fetching hooks — each Select loads independently, no blocking

View File

@ -16,8 +16,9 @@ import {
Empty, Empty,
} from "antd"; } from "antd";
import { ReloadOutlined } from "@ant-design/icons"; 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 type { ParsedProject } from "@/services/project";
import { platforms } from "@/services/publish.schemas";
const { Text } = Typography; const { Text } = Typography;
@ -44,6 +45,19 @@ interface PublishModalProps {
onClose: () => void; onClose: () => void;
} }
type ReleaseType = Extract<BaseVersion["version_type"], UploadReleaseType>;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const RELEASE_TYPE_LABEL_MAP: Record<ReleaseType, string> = {
release: "Release",
beta: "Beta",
alpha: "Alpha"
};
const RELEASE_TYPES = Object.entries(RELEASE_TYPE_LABEL_MAP).map(v => ({ value: v[0], label: v[1] }));
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Component // Component
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -63,16 +77,18 @@ export function PublishModal({
// Publish settings // Publish settings
const [changelog, setChangelog] = useState(""); const [changelog, setChangelog] = useState("");
const [versionType, setVersionType] = useState<"release" | "beta" | "alpha">( const [versionType, setVersionType] = useState<ReleaseType>(
"release", "release",
); );
// Progress // Progress
const [publishing, setPublishing] = useState(false); const [publishing, setPublishing] = useState(false);
const [progress, setProgress] = useState<{ const [progress, setProgress] = useState<
modrinth: { done: number; total: number }; Record<typeof platforms[number], { done: number; total: number }>
curseforge: { done: number; total: number }; >({
}>({ modrinth: { done: 0, total: 0 }, curseforge: { done: 0, total: 0 } }); modrinth: { done: 0, total: 0 },
curseforge: { done: 0, total: 0 }
});
// Platform existing versions — loaded when modal opens // Platform existing versions — loaded when modal opens
const [loadingExisting, setLoadingExisting] = useState(false); const [loadingExisting, setLoadingExisting] = useState(false);
@ -320,11 +336,7 @@ export function PublishModal({
value={versionType} value={versionType}
onChange={(v) => setVersionType(v)} onChange={(v) => setVersionType(v)}
style={{ width: 160 }} style={{ width: 160 }}
options={[ options={RELEASE_TYPES}
{ label: "Release", value: "release" },
{ label: "Beta", value: "beta" },
{ label: "Alpha", value: "alpha" },
]}
/> />
</div> </div>

View File

@ -1,9 +1,9 @@
export function createTtlCache<T>( export function createTtlCache<R, A extends any[]>(
factory: (...args: any[]) => Promise<T>, factory: (...args: A) => Promise<R>,
ttlMs: number, ttlMs: number,
keyFn?: (...args: any[]) => string, keyFn?: (...args: A) => string,
): (...args: any[]) => Promise<T> { ): (...args: A) => Promise<R> {
const cache = new Map<string, { data: T; ts: number }>(); const cache = new Map<string, { data: R; ts: number }>();
return async (...args) => { return async (...args) => {
const key = keyFn ? keyFn(...args) : "_"; const key = keyFn ? keyFn(...args) : "_";

View File

@ -0,0 +1,9 @@
export function createRecord<K extends readonly PropertyKey[], V>(
keys: K, value: V
): Record<K[number], V> {
const result = {} as Record<K[number], V>;
for (const key of keys) {
result[key as K[number]] = value;
}
return result;
}

View File

@ -2,6 +2,7 @@ import path from "node:path";
import { Template } from "@/lib/utils/template"; import { Template } from "@/lib/utils/template";
import type { Config } from "@/types"; import type { Config } from "@/types";
import type { McVersionEntry } from "@/lib/utils/mcVersion"; import type { McVersionEntry } from "@/lib/utils/mcVersion";
import { getBuildDir } from "./gradle";
export function resolveVersionName( export function resolveVersionName(
template: string, template: string,
@ -26,5 +27,5 @@ export function buildJarPath(
mcVersion: string, mcVersion: string,
): string { ): string {
const filename = template.format({ version, mc_version: mcVersion }); const filename = template.format({ version, mc_version: mcVersion });
return path.join(projectDir, "build", "libs", filename); return path.join(getBuildDir(projectDir), filename);
} }

5
src/lib/utils/gradle.ts Normal file
View File

@ -0,0 +1,5 @@
import path from "path";
export function getBuildDir(projectDir: string) {
return path.join(projectDir, "build", "libs");
}

View File

@ -4,9 +4,9 @@ import { modrinthClient, curseforgeClient } from "./clients";
import { getLoaders, getGameVersions as getModrinthGameVersions } from "@/lib/modrinth"; import { getLoaders, getGameVersions as getModrinthGameVersions } from "@/lib/modrinth";
import { getVersionTypes, getGameVersions, getMinecraftVersions } from "@/lib/curseforge"; import { getVersionTypes, getGameVersions, getMinecraftVersions } from "@/lib/curseforge";
import { SemVer } from "@/lib/utils/templates/semver"; import { SemVer } from "@/lib/utils/templates/semver";
import { EnvironmentSchema } from "@/types"; import { Environment, EnvironmentSchema } from "@/types";
const ENVIRONMENT_LABELS: Record<string, string> = { const ENVIRONMENT_LABELS: Record<Environment, string> = {
unknown: "Unknown environment", unknown: "Unknown environment",
client_only: "Client-side only", client_only: "Client-side only",
server_only: "Server-side only (singleplayer compatible)", server_only: "Server-side only (singleplayer compatible)",
@ -43,12 +43,10 @@ export async function getModrinthMcVersions(): Promise<string[]> {
* Modrinth Ant Design Select * Modrinth Ant Design Select
* 使 OptGroup Client / Server / Both / Other * 使 OptGroup Client / Server / Both / Other
*/ */
export async function getModrinthEnvironments(): Promise< export async function getModrinthEnvironments() {
{ value: string; label: string }[]
> {
return EnvironmentSchema.options.map((value) => ({ return EnvironmentSchema.options.map((value) => ({
value, value,
label: ENVIRONMENT_LABELS[value] ?? value, label: ENVIRONMENT_LABELS[value],
})); }));
} }

View File

@ -4,6 +4,7 @@ import { readFile, readdir, stat } from "fs/promises";
import path from "path"; import path from "path";
import type { Config } from "@/types"; import type { Config } from "@/types";
import { Template } from "@/lib/utils/template"; import { Template } from "@/lib/utils/template";
import { getBuildDir } from "@/lib/utils/gradle";
export interface ParsedProject { export interface ParsedProject {
version: string; version: string;
@ -64,7 +65,7 @@ export async function parseProject(config: Config): Promise<ParsedProject> {
.map((f) => f.slice(0, -".properties".length)); .map((f) => f.slice(0, -".properties".length));
// 3. Format filenames and check existence under build/libs/ // 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 artifactTmpl = new Template(config.filename_format);
const sourcesTmpl = new Template(config.source_filename_format); const sourcesTmpl = new Template(config.source_filename_format);

View File

@ -1,5 +1,6 @@
import { createRecord } from "@/lib/utils/createRecord";
import { McVersionEntry } from "@/lib/utils/mcVersion"; import { McVersionEntry } from "@/lib/utils/mcVersion";
import { Config } from "@/types"; import { BaseVersionSchema, Config } from "@/types";
import { z } from "zod"; import { z } from "zod";
export const platforms = ["modrinth", "curseforge"] as const; export const platforms = ["modrinth", "curseforge"] as const;
@ -28,13 +29,12 @@ export type PlatformAdaptor = {
export const PublishInputSchema = z.object({ export const PublishInputSchema = z.object({
configName: z.string(), configName: z.string(),
version: z.string(), version: z.string(),
mcVersions: z.object(Object.fromEntries(platforms.map(platform => [ mcVersions: z.object(createRecord(
platform, platforms, z.array(z.object({ mc_version: z.string() }))
z.array(z.object({ mc_version: z.string() })) )),
]))),
cutoffMcVersion: z.string(), // 最后一个版本的截止 MC 版本,两个平台共用 cutoffMcVersion: z.string(), // 最后一个版本的截止 MC 版本,两个平台共用
changelog: z.string(), changelog: z.string(),
versionType: z.enum(["release", "beta", "alpha"]), versionType: BaseVersionSchema.shape.version_type,
}); });
export const ProgressEventSchema = z.object({ export const ProgressEventSchema = z.object({
@ -51,9 +51,9 @@ export const PlatformResultSchema = z.object({
errors: z.array(z.string()), errors: z.array(z.string()),
}); });
export const PublishResultSchema = z.object(Object.fromEntries(platforms.map(platform => [ export const PublishResultSchema = z.object(createRecord(
platform, PlatformResultSchema platforms, PlatformResultSchema
]))); ));
// ── Inferred types ───────────────────────────────────── // ── Inferred types ─────────────────────────────────────

View File

@ -10,6 +10,9 @@ import {
platforms, platforms,
type PlatformContext, type PlatformContext,
type PlatformAdaptor, type PlatformAdaptor,
type PublishInput,
PublishResultSchema,
ProgressEventSchema,
} from "./publish.schemas"; } from "./publish.schemas";
import { publishModrinthVersion } from "./platforms/modrinth"; import { publishModrinthVersion } from "./platforms/modrinth";
import { publishCurseForgeVersion } from "./platforms/curseforge"; import { publishCurseForgeVersion } from "./platforms/curseforge";
@ -44,7 +47,7 @@ const ee = new EventEmitter();
async function runPlatform( async function runPlatform(
platform: typeof platforms[number], platform: typeof platforms[number],
mcVersions: { mc_version: string }[], mcVersions: PublishInput["mcVersions"][keyof PublishInput["mcVersions"]],
ctx: PlatformContext, ctx: PlatformContext,
): Promise<PlatformResult> { ): Promise<PlatformResult> {
const result: PlatformResult = { success: 0, fail: 0, errors: [] }; 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) { progress: t.procedure.subscription(async function* (opts) {
for await (const [data] of on(ee, "progress", { for await (const [data] of on(ee, "progress", {
signal: opts.signal, signal: opts.signal,
})) { })) {
yield data as ProgressEvent; yield ProgressEventSchema.parse(data);
} }
}), }),
}); });