Refactor type-safe constants and extract utility helpers
This commit is contained in:
parent
77b7db9dd1
commit
7aa84fd41e
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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) : "_";
|
||||
|
||||
9
src/lib/utils/createRecord.ts
Normal file
9
src/lib/utils/createRecord.ts
Normal 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;
|
||||
}
|
||||
@ -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
5
src/lib/utils/gradle.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import path from "path";
|
||||
|
||||
export function getBuildDir(projectDir: string) {
|
||||
return path.join(projectDir, "build", "libs");
|
||||
}
|
||||
@ -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],
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 ─────────────────────────────────────
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user