Merge pull request 'chore(dev): add pnpm dev hot-reload workflow' (#39) from dev/hot-reload-workflow into main
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
root = "."
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
cmd = "go build -o ./tmp/control-plane ./cmd/control-plane"
|
||||
stop_signal = "SIGTERM"
|
||||
kill_delay = 500
|
||||
ps1 = []
|
||||
entrypoint = "./tmp/control-plane"
|
||||
include_ext = ["go"]
|
||||
exclude_dir = ["tmp", "web", "docs", ".git", ".codegraph"]
|
||||
exclude_file = []
|
||||
delay = 200
|
||||
|
||||
[log]
|
||||
time = false
|
||||
main_log_only = true
|
||||
@@ -2,3 +2,5 @@ web/dist/
|
||||
web/node_modules/
|
||||
web/test-results/
|
||||
.env
|
||||
.dev-credentials/
|
||||
tmp/
|
||||
|
||||
@@ -25,6 +25,32 @@ DOCKER_GID=$(stat -c %g /var/run/docker.sock) docker compose up --build
|
||||
打开 <http://127.0.0.1:8080>,使用 `CONTROL_PLANE_USERNAME` / `CONTROL_PLANE_PASSWORD` 登录;局域网内用宿主机 IP 访问同一端口。首次使用:在「网关管理」用 Compose 里的 `GATEWAY_TOKEN` 注册 `http://docker-gateway:8081`,在「镜像版本」添加可用的指纹浏览器镜像引用,即可创建环境;网关会在镜像缺失时自动拉取。架构、API 契约、失败语义和 `docker.sock` 风险边界见
|
||||
[《浏览器容器控制面》](docs/architecture/container-control.md)。
|
||||
|
||||
## 本地开发(热加载)
|
||||
|
||||
日常开发不再整仓重建镜像,改用源码热加载:
|
||||
|
||||
```bash
|
||||
npm --prefix web ci
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
- `pnpm dev`:同时起后端(air 热重载)与前端(vite HMR);
|
||||
- `pnpm dev:backend` / `pnpm dev:frontend`:单独启动其中一端;
|
||||
- `pnpm dev:deps`:仅启动 postgres 与 docker-gateway 两个容器(`compose.dev.yaml` 会把 5432/8081 映射到宿主机)。
|
||||
|
||||
服务地址:
|
||||
|
||||
| 端 | 地址 | 说明 |
|
||||
| --- | --- |
|
||||
| 前端 | <http://127.0.0.1:5173> | `/api` 由 vite 代理到本地 control-plane |
|
||||
| 后端 control-plane | <http://127.0.0.1:8082> | Go 源码改动即自动重启 |
|
||||
| docker-gateway | <http://127.0.0.1:8081> | 容器内常驻,重启不频繁 |
|
||||
| postgres | `127.0.0.1:5432` | 容器内常驻,数据库重建直接销毁 volume |
|
||||
|
||||
开发默认值(`.env` 缺失或留空时):登录 `admin` / `admin123`;控制面监听 `:8082`;凭据主密钥使用本地开发专用密钥;本地网关注册地址用 `http://127.0.0.1:8081`(容器名 `docker-gateway` 仅存在于 Compose 网络内)。
|
||||
|
||||
注意:本地开发后端占用 `:8082`,与整仓 `docker compose up` 的容器端口互斥,两者二选一运行。
|
||||
|
||||
最小验证:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# 本地热加载开发用 override:只跑 postgres + docker-gateway,宿主机直接跑 control-plane 与前端。
|
||||
# 用法:docker compose -f compose.yaml -f compose.dev.yaml up -d postgres docker-gateway
|
||||
services:
|
||||
creator-hub:
|
||||
# 本地开发不跑容器版 control-plane;profile 化后不启动。
|
||||
profiles: [compose-only]
|
||||
|
||||
postgres:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
docker-gateway:
|
||||
ports:
|
||||
- "8081:8081"
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "creator-hub",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev:deps": "docker compose -f compose.yaml -f compose.dev.yaml up -d postgres docker-gateway",
|
||||
"dev:backend": "node scripts/dev-backend.mjs",
|
||||
"dev:frontend": "pnpm -C web run dev",
|
||||
"dev": "node scripts/dev.mjs"
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
// dev: 同时跑 dev:backend(air 热加载 control-plane)与 dev:frontend(vite HMR)。
|
||||
// Ctrl-C 结束两个子进程。
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
||||
const pnpm = process.env.npm_execpath || "pnpm";
|
||||
|
||||
const procs = [
|
||||
spawn(pnpm, ["run", "dev:backend"], { cwd: root, stdio: "inherit" }),
|
||||
spawn(pnpm, ["run", "dev:frontend"], { cwd: root, stdio: "inherit" }),
|
||||
];
|
||||
|
||||
for (const signal of ["SIGINT", "SIGTERM"]) {
|
||||
process.on(signal, () => {
|
||||
for (const p of procs) p.kill(signal);
|
||||
});
|
||||
}
|
||||
|
||||
let remaining = procs.length;
|
||||
for (const p of procs) {
|
||||
p.on("exit", (code) => {
|
||||
if (--remaining === 0) process.exit(code ?? 0);
|
||||
});
|
||||
}
|
||||
+11
-9
@@ -1,21 +1,23 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
server: {
|
||||
proxy: { '/api': 'http://127.0.0.1:8080' },
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
proxy: { "/api": "http://127.0.0.1:8082" },
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
pool: 'threads',
|
||||
environment: "jsdom",
|
||||
pool: "threads",
|
||||
maxWorkers: 1,
|
||||
isolate: false, // ponytail: one worker avoids 90s startup stalls; restore isolation if tests leak state.
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text'],
|
||||
provider: "v8",
|
||||
reporter: ["text"],
|
||||
thresholds: { lines: 65 },
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user