Refactor type-safe constants and extract utility helpers
This commit is contained in:
parent
77b7db9dd1
commit
7aa84fd41e
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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) : "_";
|
||||||
|
|||||||
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 { 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
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 { 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],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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 ─────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user