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,
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<DependencyType, string> = {
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<UploadRelationType, string> = {
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

View File

@ -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<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
// ---------------------------------------------------------------------------
@ -63,16 +77,18 @@ export function PublishModal({
// Publish settings
const [changelog, setChangelog] = useState("");
const [versionType, setVersionType] = useState<"release" | "beta" | "alpha">(
const [versionType, setVersionType] = useState<ReleaseType>(
"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<typeof platforms[number], { done: number; total: number }>
>({
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}
/>
</div>

View File

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

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 { 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<string, string> = {
const ENVIRONMENT_LABELS: Record<Environment, string> = {
unknown: "Unknown environment",
client_only: "Client-side only",
server_only: "Server-side only (singleplayer compatible)",
@ -43,12 +43,10 @@ export async function getModrinthMcVersions(): Promise<string[]> {
* 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],
}));
}

View File

@ -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<ParsedProject> {
.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);

View File

@ -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 ─────────────────────────────────────

View File

@ -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<PlatformResult> {
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);
}
}),
});