chore(deno): migrate toolchain from Node 24 + pnpm to Deno 2.9.7 + TypeScript 7.0.2 (W1.5)

- root deno.json workspace (nodeModulesDir auto), npm versions registered once in imports, deno.lock;
  remove pnpm-lock/pnpm-workspace/root package.json; members keep minimal package.json (name/type/exports)
- tasks: dev, typecheck (tsc 7.0.2 per member + deno check), lint/fmt (biome, useImportExtensions),
  test (vitest via deno), check:tokens, build (web + deno compile), ci, db:migrate/reset, gen:types
- tsconfig base moved to config/ (no root tsconfig.json); deno types -> types/deno.d.ts; web drops baseUrl
- server: Deno.serve entry with graceful shutdown, --role migrate|preflight|probe, WEB_ROOT static SPA hosting;
  drop @hono/node-server, tsx, esbuild build.mjs
- logger: redact child() bindings and serialize err by message/stack; sync stdout destination; tests
- relative imports get explicit extensions (sites/mp-sync paths left for follow-up)
- crash-recovery tests spawn `deno run -A`; tasks tests use unique per-file schemas
- single-container Dockerfile + entrypoint (migrate -> preflight -> api+worker watchdog), docker-compose.yml
- docs: README, runbook, ADR-001 superseded, plan §1 validation row

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-27 18:53:06 +08:00
1 parent d9f2af2f3c
commit 304feea141
226 files changed
+6601 -8620

No files matched your search

+82 -27
View File
@@ -1,21 +1,34 @@
import { parseArgs } from "node:util";
import { serve } from "@hono/node-server";
import { createDb } from "@umt/db";
import pkg from "../package.json" with { type: "json" };
import { loadConfig } from "./config";
import { createApp } from "./http/app";
import { EventBus } from "./http/sse";
import { createLogger } from "./logger";
import { startWorker } from "./worker";
import { createDb, migrateToLatest } from "@umt/db";
import denoJson from "../deno.json" with { type: "json" };
import { loadConfig } from "./config.ts";
import { createApp } from "./http/app.ts";
import { EventBus } from "./http/sse.ts";
import { createLogger } from "./logger.ts";
import { runPreflight } from "./preflight.ts";
import { startWorker } from "./worker/index.ts";
const Roles = ["api", "worker", "all"] as const;
// api/worker/all 是常驻角色;migrate/preflight/probe 是容器入口脚本用的一次性子命令
const Roles = ["api", "worker", "all", "migrate", "preflight", "probe"] as const;
type Role = (typeof Roles)[number];
const { values } = parseArgs({ options: { role: { type: "string", default: "all" } }, strict: true });
const { values } = parseArgs({ args: Deno.args, options: { role: { type: "string", default: "all" } }, strict: true });
const role = values.role as Role;
if (!Roles.includes(role)) {
console.error(`--role 必须是 ${Roles.join("|")}`);
process.exit(2);
Deno.exit(2);
}
// 健康探测:只打本机回环端口,不需要完整配置(HEALTHCHECK 用,镜像里不装 curl)
if (role === "probe") {
const port = Deno.env.get("PORT") ?? "4380";
try {
const res = await fetch(`http://127.0.0.1:${port}/api/v1/health`, { signal: AbortSignal.timeout(3000) });
const body = (await res.json()) as { data?: { status?: string } };
Deno.exit(res.ok && body.data?.status === "ok" ? 0 : 1);
} catch {
Deno.exit(1);
}
}
let config: ReturnType<typeof loadConfig>;
@@ -23,30 +36,69 @@ try {
config = loadConfig();
} catch (err) {
console.error((err as Error).message);
process.exit(2);
Deno.exit(2);
}
const logger = createLogger({
level: config.LOG_LEVEL,
pretty: config.NODE_ENV === "development" && process.stdout.isTTY,
pretty: config.NODE_ENV === "development" && Deno.stdout.isTerminal(),
});
const db = createDb({ connectionString: config.DATABASE_URL });
if (role === "migrate" || role === "preflight") {
let code = 0;
try {
if (role === "migrate") {
const { results } = await migrateToLatest(db);
for (const r of results ?? []) logger.info({ migration: r.migrationName, status: r.status }, "migration");
logger.info({ applied: results?.length ?? 0 }, "迁移完成");
} else {
const problems = await runPreflight({ config, db });
for (const p of problems) logger.error({ check: p.check }, p.message);
if (problems.length) code = 1;
else logger.info("启动自检通过");
}
} catch (err) {
logger.error({ err }, `${role} 失败`);
code = 1;
} finally {
await db.destroy();
}
Deno.exit(code);
}
const bus = new EventBus();
const stops: Array<() => Promise<void>> = [];
if (role === "api" || role === "all") {
const app = createApp({ config, logger, bus, db, role, version: pkg.version });
const server = serve({ fetch: app.fetch, port: config.PORT }, (info) => {
logger.info({ port: info.port, role }, `API 已启动 http://localhost:${info.port}/api/v1/health`);
const app = createApp({
config,
logger,
bus,
db,
role,
version: denoJson.version,
...(config.WEB_ROOT ? { webRoot: config.WEB_ROOT } : {}),
});
stops.push(
() =>
new Promise((resolve) => {
// SSE 长连接不会自行结束,强制关闭
(server as unknown as { closeAllConnections?: () => void }).closeAllConnections?.();
server.close(() => resolve());
}),
const ac = new AbortController();
const server = Deno.serve(
{
port: config.PORT,
hostname: config.HOST,
signal: ac.signal,
onListen: ({ port }) =>
logger.info(
{ port, role, webRoot: config.WEB_ROOT ?? null },
`API 已启动 http://localhost:${port}/api/v1/health`,
),
},
app.fetch,
);
stops.push(async () => {
// abort 后 Deno.serve 会断开 SSE 等长连接,finished 在所有连接关闭后 resolve
ac.abort();
await server.finished;
});
}
if (role === "worker" || role === "all") {
const worker = startWorker({ logger, heartbeatMs: config.WORKER_HEARTBEAT_MS, bus });
@@ -54,13 +106,16 @@ if (role === "worker" || role === "all") {
}
let stopping = false;
async function shutdown(signal: string) {
async function shutdown(signal: Deno.Signal) {
if (stopping) return;
stopping = true;
logger.info({ signal }, "正在退出");
// 兜底:10 秒内没退干净就强退,避免容器 stop 超时被 SIGKILL
const force = setTimeout(() => Deno.exit(1), 10_000);
await Promise.allSettled(stops.map((s) => s()));
await db.destroy();
process.exit(0);
clearTimeout(force);
Deno.exit(0);
}
process.on("SIGINT", () => void shutdown("SIGINT"));
process.on("SIGTERM", () => void shutdown("SIGTERM"));
Deno.addSignalListener("SIGINT", () => void shutdown("SIGINT"));
Deno.addSignalListener("SIGTERM", () => void shutdown("SIGTERM"));