chore(dev): add pnpm dev hot-reload workflow for local development

Replace full docker compose rebuild during development with:
- pnpm dev / dev:backend (air hot-reload control-plane) / dev:frontend (vite HMR) / dev:deps
- compose.dev.yaml: postgres+docker-gateway only, host port mapping (5432/8081), container control-plane disabled via profile
- scripts/dev-backend.mjs: read .env, fill dev defaults (login admin/admin123, listen :8082, 32-byte dev master key with explicit warning on invalid .env value), ensure dep containers, run air
- vite: fixed port 5173, /api proxy to local control-plane (8082)
- README: document hot-reload dev workflow

Verified: pnpm dev end-to-end (login/API/frontend/proxy 200), Go+JSX edit hot-reload, compose config --quiet, go build ./..., web tests 65 passed, npm --prefix web run build.
This commit is contained in:
2026-09-02 14:38:44 +08:00
parent 2676df12c2
commit 208f6766af
8 changed files with 192 additions and 9 deletions
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env node
// dev-backend: 读取 .env,补齐开发默认值,确保 postgres/docker-gateway 容器在跑,然后用 air 热加载运行 control-plane。
import { spawn, spawnSync } from "node:child_process";
import { readFileSync, existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const envPath = path.join(root, ".env");
if (!existsSync(envPath)) {
console.error("缺少 .env:请参照 .env.example 创建(开发值任意填)。");
process.exit(1);
}
// 简易 KEY=value 解析(与 compose 的 env_file 语法一致,无需处理引号)。
const overrides = {};
for (const line of readFileSync(envPath, "utf8").split("\n")) {
const m = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line.trim());
if (m && !(m[1] in overrides)) overrides[m[1]] = m[2];
}
if (!overrides.CONTROL_PLANE_USERNAME)
overrides.CONTROL_PLANE_USERNAME = "admin";
if (!overrides.CONTROL_PLANE_PASSWORD)
overrides.CONTROL_PLANE_PASSWORD = "admin123";
// 主密钥必须是 32 字节的 base64;无效或缺失时换成本地开发专用密钥(与 .env 里的 dev 凭据同级机密性)。
const devMasterKey = Buffer.from(
"creatorhub dev local master key",
"utf8",
).toString("base64"); // 恰 32 字节
if (
Buffer.from(overrides.CREATORHUB_CREDENTIAL_MASTER_KEY ?? "", "base64")
.length !== 32
) {
if (overrides.CREATORHUB_CREDENTIAL_MASTER_KEY) {
console.error(
"[dev-backend] .env 的 CREATORHUB_CREDENTIAL_MASTER_KEY 不是 32 字节 base64,本地开发改用固定开发密钥。",
);
}
overrides.CREATORHUB_CREDENTIAL_MASTER_KEY = devMasterKey;
}
if (!overrides.LISTEN_ADDR) overrides.LISTEN_ADDR = ":8082";
if (!overrides.DATABASE_URL)
overrides.DATABASE_URL =
"postgres://creatorhub@127.0.0.1:5432/creatorhub?sslmode=disable";
if (!overrides.CREATORHUB_CREDENTIAL_STORE_DIR)
overrides.CREATORHUB_CREDENTIAL_STORE_DIR = path.join(
root,
".dev-credentials",
);
if (!overrides.WEB_DIR) overrides.WEB_DIR = path.join(root, "web", "dist");
if (!overrides.LOG_LEVEL) overrides.LOG_LEVEL = "debug";
if (!overrides.DOCKER_GID)
overrides.DOCKER_GID = String(process.getgid?.() ?? 1000);
const env = { ...process.env, ...overrides };
const compose = spawnSync(
"docker",
[
"compose",
"-f",
"compose.yaml",
"-f",
"compose.dev.yaml",
"up",
"-d",
"postgres",
"docker-gateway",
],
{ cwd: root, env, stdio: "inherit" },
);
if (compose.status !== 0) {
console.error("启动 postgres/docker-gateway 容器失败,请确认 docker 可用。");
process.exit(compose.status ?? 1);
}
const air = spawn("air", ["-c", ".air.toml", ...process.argv.slice(2)], {
cwd: root,
env,
stdio: "inherit",
});
air.on("exit", (code) => process.exit(code ?? 0));