feat: 迁移前端构建从 Bun fullstack 到 Vite
前端性能问题根因在于 Bun bundler 无法有效 code split、CSS
tree-shake 和产出优化的前端资源。经多轮 Bun 原生优化尝试
均无明显效果后,决定将前端构建迁回 Vite。
主要变更:
- 前端构建:从 Bun HTML import bundling 切换为 Vite build
(Rolldown code splitting、vendor chunk、CSS 优化)
- 开发模式:从 Bun fullstack 单进程 HMR 切换为 Vite dev
server + Bun API server 双进程(:5173 + :3000)
- 生产构建:三步流水线(Vite build → code generation →
Bun compile),通过 `import with { type: "file" }` 嵌入前端资源
- 静态资源服务:从 Bun HTML import manifest 切换为自定义
serveStaticAsset 函数,支持 SPA fallback 和正确的 Cache-Control
- Server 接口:BootstrapOptions 和 StartServerOptions 增加
staticAssets? 可选参数
- 文档更新:DEVELOPMENT.md 和 README.md 反映新的开发模式,
主 specs 同步 delta 变更
新增能力:
- static-asset-embedding: 构建时资源扫描与 code generation、
运行时静态资源服务
- vite-frontend-bundling: Vite 构建配置、code splitting 策略、
CSS 处理
This commit is contained in:
@@ -22,9 +22,9 @@ src/
|
||||
server/
|
||||
bootstrap.ts 后端统一启动引导(loadConfig → store → engine → startServer → shutdown)
|
||||
config.ts CLI 参数解析(仅提取配置文件路径)
|
||||
dev.ts 开发模式启动入口(mode: "development",HMR 自动注入)
|
||||
dev.ts 开发模式启动入口(mode: "development",仅 API server)
|
||||
main.ts 生产模式启动入口(mode: "production",安全头启用)
|
||||
server.ts HTTP server 启动工厂(Bun.serve routes 声明式路由 + HTML import)
|
||||
server.ts HTTP server 启动工厂(Bun.serve routes 声明式路由 + fetch fallback 静态资源服务)
|
||||
helpers.ts 共享响应格式化工具(见下方函数清单)
|
||||
middleware.ts API 参数校验中间件(validateTargetId、validateTimeRange、validatePagination)
|
||||
routes/ API 路由 handler(按端点拆分)
|
||||
@@ -113,7 +113,7 @@ probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动
|
||||
HTTP 请求:
|
||||
Request → Bun.serve routes 声明式匹配 → routes/*.ts(handler)
|
||||
→ middleware.ts(参数校验) → helpers.ts(响应格式化) → Response
|
||||
前端: "/*": homepage (HTML import) → SPA fallback + HMR(开发模式)
|
||||
前端: fetch fallback → serveStaticAsset (生产) / Vite proxy (开发)
|
||||
```
|
||||
|
||||
### 1.2 库使用优先级
|
||||
@@ -748,44 +748,58 @@ export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps)
|
||||
bun run dev probes.yaml
|
||||
```
|
||||
|
||||
`bun --watch src/server/dev.ts` 启动单进程 fullstack 开发服务器:
|
||||
`scripts/dev.ts` 同时启动两个进程:
|
||||
|
||||
- 后端 API + 前端 SPA 在同一端口(默认 3000)
|
||||
- `development` 模式自动注入 HMR,前端修改即时热更新
|
||||
- `--watch` 监听后端文件变更自动重启
|
||||
- 访问 `http://127.0.0.1:3000` 即可使用完整应用
|
||||
- **Bun API server**(端口 3000):后端 API 服务,`--watch` 监听后端文件变更自动重启
|
||||
- **Vite dev server**(端口 5173):前端 SPA + HMR 热更新
|
||||
|
||||
开发时访问 `http://127.0.0.1:5173`,Vite 自动将 `/api` 和 `/health` 请求代理到后端。
|
||||
|
||||
也可以单独启动:
|
||||
|
||||
```bash
|
||||
bun run dev:server probes.yaml # 仅启动后端 API server
|
||||
bun run dev:web # 仅启动 Vite dev server
|
||||
```
|
||||
|
||||
### 3.2 前后端集成方式
|
||||
|
||||
#### 统一进程架构
|
||||
#### 双进程开发架构
|
||||
|
||||
前后端通过 Bun 的 HTML import 机制集成为单进程应用:
|
||||
开发模式下前后端分别由 Vite 和 Bun 服务:
|
||||
|
||||
- Vite dev server 负责前端 SPA、HMR、模块热替换
|
||||
- Bun API server 负责后端 API 路由
|
||||
- Vite 通过 proxy 配置将 `/api/*` 和 `/health` 转发到 Bun
|
||||
|
||||
#### 生产模式架构
|
||||
|
||||
生产模式下前端通过 Vite 构建为静态资源,通过 `import with { type: "file" }` 嵌入 Bun 可执行文件:
|
||||
|
||||
```typescript
|
||||
// server.ts
|
||||
import homepage from "../web/index.html";
|
||||
|
||||
const server = Bun.serve({
|
||||
development: mode === "development" ? { hmr: true, console: true } : false,
|
||||
fetch(req) {
|
||||
// staticAssets 存在时服务嵌入的前端资源
|
||||
return serveStaticAsset(new URL(req.url).pathname, staticAssets);
|
||||
},
|
||||
routes: {
|
||||
"/*": homepage, // SPA fallback(开发模式自动注入 HMR)
|
||||
"/api/*": () => ..., // API 通配符(未匹配路由返回 404)
|
||||
"/api/dashboard": { GET: (req) => handleDashboard(new URL(req.url), store, mode) },
|
||||
"/api/dashboard": { GET: (req) => handleDashboard(...) },
|
||||
"/health": { GET: () => handleHealth(mode) },
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- 开发模式(`development: { hmr: true, console: true }`):Bun 自动为 HTML import 注入 HMR client,前端修改无需手动刷新,并将浏览器 console 回显到终端
|
||||
- 生产模式:HTML 及其引用的 JS/CSS 资源在 `bun build --compile` 时自动打包进可执行文件
|
||||
|
||||
#### 路由优先级
|
||||
|
||||
Bun routes 的匹配规则:具体路径 > 通配符。`/api/dashboard` 优先于 `/api/*`,`/health` 优先于 `/*`。
|
||||
|
||||
未匹配 method 的请求(如 POST /api/dashboard)会落入 `/api/*` 通配符返回 404。
|
||||
|
||||
非 API 路径由 fetch fallback 处理:有文件扩展名的返回对应静态资源或 404,无扩展名的返回 SPA index.html。
|
||||
|
||||
### 3.3 构建打包
|
||||
|
||||
#### 构建命令
|
||||
@@ -796,26 +810,25 @@ bun run build
|
||||
|
||||
#### 构建流程
|
||||
|
||||
`scripts/build.ts` 执行单步构建:
|
||||
`scripts/build.ts` 执行三步流水线:
|
||||
|
||||
```
|
||||
Bun.build({
|
||||
compile: { outfile: "dist/dial-server" },
|
||||
entrypoints: ["src/server/main.ts"],
|
||||
minify: true,
|
||||
sourcemap: "linked",
|
||||
})
|
||||
1. Vite build → dist/web/ (前端静态资源,含 code splitting)
|
||||
2. Code generation → .build/static-assets.ts + .build/server-entry.ts
|
||||
3. Bun compile → dist/dial-server (单可执行文件)
|
||||
```
|
||||
|
||||
- 入口为 `src/server/main.ts`(`mode: "production"`,启用安全头)
|
||||
- HTML import 的前端资源自动打包进可执行文件(Bun 自动生成 manifest)
|
||||
- 无需中间产物目录,一步生成最终 binary
|
||||
- Vite 构建前端资源到 `dist/web/`,自动 code splitting(vendor-react、vendor-tdesign、vendor-chart)
|
||||
- Code generation 扫描 `dist/web/` 生成 `import with { type: "file" }` 声明,将资源嵌入 binary
|
||||
- Bun compile 以 `.build/server-entry.ts` 为入口编译最终可执行文件
|
||||
- `.build/` 临时目录在构建完成后自动清理
|
||||
|
||||
#### 产物
|
||||
|
||||
| 产物 | 用途 |
|
||||
| ------------------ | ---------------------------------------- |
|
||||
| `dist/dial-server` | 生产可执行文件(含前端资源,单文件部署) |
|
||||
| `dist/web/` | Vite 构建的前端资源(构建中间产物) |
|
||||
|
||||
#### 构建参数
|
||||
|
||||
@@ -839,7 +852,7 @@ Bun.build({
|
||||
|
||||
```bash
|
||||
bun run clean
|
||||
# 清理 dist/ 构建产物和 *.bun-build 临时文件
|
||||
# 清理 dist/ 构建产物和 .build/ 临时文件
|
||||
```
|
||||
|
||||
### 3.4 开发工作流
|
||||
@@ -847,8 +860,9 @@ bun run clean
|
||||
#### 日常开发循环
|
||||
|
||||
```bash
|
||||
bun run dev probes.yaml # 启动开发环境(单进程,含 HMR)
|
||||
# 修改前端代码 → HMR 热更新 / 修改后端代码 → --watch 自动重启
|
||||
bun run dev probes.yaml # 启动双进程开发环境(Vite + API server)
|
||||
# 访问 http://127.0.0.1:5173
|
||||
# 修改前端代码 → Vite HMR 热更新 / 修改后端代码 → --watch 自动重启
|
||||
bun run check # 提交前运行完整质量检查
|
||||
```
|
||||
|
||||
@@ -863,17 +877,19 @@ bun run verify
|
||||
|
||||
### 3.5 Executable/E2E 验证
|
||||
|
||||
原 `scripts/smoke.ts` 覆盖过薄,已从当前工作流移除。后续如需验证 production executable 的 API、HTML import manifest、SPA fallback 和静态资源行为,应重新设计独立的 executable/E2E 测试。
|
||||
原 `scripts/smoke.ts` 覆盖过薄,已从当前工作流移除。后续如需验证 production executable 的 API、静态资源服务、SPA fallback 行为,应重新设计独立的 executable/E2E 测试。
|
||||
|
||||
### 3.6 脚本说明
|
||||
|
||||
| 脚本 | 文件 | 说明 |
|
||||
| ---------------------- | ----------------------------------- | ----------------------------------- |
|
||||
| `bun run dev` | `src/server/dev.ts` | 单进程 fullstack 开发服务(含 HMR) |
|
||||
| `bun run build` | `scripts/build.ts` | Bun 编译可执行文件(含前端资源) |
|
||||
| `bun run schema` | `scripts/generate-config-schema.ts` | 生成 `probe-config.schema.json` |
|
||||
| `bun run schema:check` | `scripts/generate-config-schema.ts` | 检查配置 schema 导出物是否同步 |
|
||||
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
|
||||
| 脚本 | 文件 | 说明 |
|
||||
| ---------------------- | ----------------------------------- | ---------------------------------------- |
|
||||
| `bun run dev` | `scripts/dev.ts` | 双进程开发服务(Vite :5173 + API :3000) |
|
||||
| `bun run dev:server` | `src/server/dev.ts` | 仅启动后端 API server |
|
||||
| `bun run dev:web` | Vite CLI | 仅启动 Vite dev server |
|
||||
| `bun run build` | `scripts/build.ts` | Vite → codegen → Bun compile 三步构建 |
|
||||
| `bun run schema` | `scripts/generate-config-schema.ts` | 生成 `probe-config.schema.json` |
|
||||
| `bun run schema:check` | `scripts/generate-config-schema.ts` | 检查配置 schema 导出物是否同步 |
|
||||
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
|
||||
|
||||
### 3.7 环境变量
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ cp probes.example.yaml probes.yaml
|
||||
bun run dev probes.yaml
|
||||
```
|
||||
|
||||
`bun run dev` 启动单进程 fullstack 开发服务器(后端 API + 前端 SPA + HMR),访问 `http://127.0.0.1:3000`。
|
||||
`bun run dev` 启动双进程开发服务器(Vite :5173 + Bun API :3000),访问 `http://127.0.0.1:5173`。
|
||||
|
||||
## 开发验证
|
||||
|
||||
|
||||
78
bun.lock
78
bun.lock
@@ -26,6 +26,7 @@
|
||||
"@types/bun": "^1.3.13",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
@@ -39,6 +40,7 @@
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.2",
|
||||
"vite": "^8.0.13",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -157,12 +159,46 @@
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.130.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.130.0.tgz", {}, "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q=="],
|
||||
|
||||
"@pkgr/core": ["@pkgr/core@0.2.9", "https://registry.npmmirror.com/@pkgr/core/-/core-0.2.9.tgz", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
|
||||
|
||||
"@popperjs/core": ["@popperjs/core@2.11.8", "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
|
||||
|
||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", { "os": "none", "cpu": "arm64" }, "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||
|
||||
"@simple-libs/child-process-utils": ["@simple-libs/child-process-utils@1.0.2", "https://registry.npmmirror.com/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", { "dependencies": { "@simple-libs/stream-utils": "^1.2.0" } }, "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw=="],
|
||||
@@ -283,6 +319,8 @@
|
||||
|
||||
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="],
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
@@ -417,6 +455,8 @@
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"doctrine": ["doctrine@2.1.0", "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||
|
||||
"dom-helpers": ["dom-helpers@5.2.1", "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-5.2.1.tgz", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
|
||||
@@ -527,6 +567,8 @@
|
||||
|
||||
"for-each": ["for-each@0.3.5", "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"function.prototype.name": ["function.prototype.name@1.1.8", "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
|
||||
@@ -681,6 +723,30 @@
|
||||
|
||||
"levn": ["levn@0.4.1", "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
|
||||
"lint-staged": ["lint-staged@17.0.4", "https://registry.npmmirror.com/lint-staged/-/lint-staged-17.0.4.tgz", { "dependencies": { "listr2": "^10.2.1", "picomatch": "^4.0.4", "string-argv": "^0.3.2", "tinyexec": "^1.1.2" }, "optionalDependencies": { "yaml": "^2.8.4" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-+rU9lSUyVOZ/hDUmRLVGzyS2v73cDdQjX+XQz1AaOdIE4RysLq0HoPW2HrrgeNCLklkhi904VBU1bmgWLHVnkA=="],
|
||||
@@ -711,6 +777,8 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.12", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
|
||||
|
||||
"napi-postinstall": ["napi-postinstall@0.3.4", "https://registry.npmmirror.com/napi-postinstall/-/napi-postinstall-0.3.4.tgz", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
@@ -773,6 +841,8 @@
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.14", "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.8.3", "https://registry.npmmirror.com/prettier/-/prettier-3.8.3.tgz", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
|
||||
@@ -823,6 +893,8 @@
|
||||
|
||||
"rfdc": ["rfdc@1.4.1", "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.1", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.1.tgz", { "dependencies": { "@oxc-project/types": "=0.130.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.1", "@rolldown/binding-darwin-arm64": "1.0.1", "@rolldown/binding-darwin-x64": "1.0.1", "@rolldown/binding-freebsd-x64": "1.0.1", "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", "@rolldown/binding-linux-arm64-gnu": "1.0.1", "@rolldown/binding-linux-arm64-musl": "1.0.1", "@rolldown/binding-linux-ppc64-gnu": "1.0.1", "@rolldown/binding-linux-s390x-gnu": "1.0.1", "@rolldown/binding-linux-x64-gnu": "1.0.1", "@rolldown/binding-linux-x64-musl": "1.0.1", "@rolldown/binding-openharmony-arm64": "1.0.1", "@rolldown/binding-wasm32-wasi": "1.0.1", "@rolldown/binding-win32-arm64-msvc": "1.0.1", "@rolldown/binding-win32-x64-msvc": "1.0.1" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.4", "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="],
|
||||
|
||||
"safe-push-apply": ["safe-push-apply@1.0.0", "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
||||
@@ -859,6 +931,8 @@
|
||||
|
||||
"sortablejs": ["sortablejs@1.15.7", "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.7.tgz", {}, "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"stable-hash-x": ["stable-hash-x@0.2.0", "https://registry.npmmirror.com/stable-hash-x/-/stable-hash-x-0.2.0.tgz", {}, "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ=="],
|
||||
|
||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||
@@ -929,6 +1003,8 @@
|
||||
|
||||
"victory-vendor": ["victory-vendor@37.3.6", "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
||||
|
||||
"vite": ["vite@8.0.13", "https://registry.npmmirror.com/vite/-/vite-8.0.13.tgz", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.1", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw=="],
|
||||
|
||||
"whatwg-encoding": ["whatwg-encoding@3.1.1", "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
|
||||
|
||||
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
||||
@@ -981,6 +1057,8 @@
|
||||
|
||||
"@reduxjs/toolkit/immer": ["immer@11.1.8", "https://registry.npmmirror.com/immer/-/immer-11.1.8.tgz", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
||||
|
||||
"@tybys/wasm-util/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
## Requirements
|
||||
|
||||
### Requirement: 声明式路由注册
|
||||
系统 SHALL 使用 Bun.serve 的 `routes` 对象以声明式方式注册所有 HTTP 路由,包括 HTML 页面路由和 API 端点路由。
|
||||
|
||||
#### Scenario: HTML 页面路由注册
|
||||
- **WHEN** server 启动时
|
||||
- **THEN** 系统 SHALL 通过 HTML import 将前端入口注册到 `routes` 对象的 `"/*"` 通配符路径
|
||||
系统 SHALL 使用 Bun.serve 的 `routes` 对象以声明式方式注册 API 端点路由,非 API 请求由 `fetch` fallback 处理。
|
||||
|
||||
#### Scenario: API 端点路由注册
|
||||
- **WHEN** server 启动时
|
||||
- **THEN** 系统 SHALL 将所有 API 端点以 method handler 对象形式注册到 `routes` 对象
|
||||
|
||||
#### Scenario: 非 API 请求处理
|
||||
- **WHEN** 请求路径不匹配任何 `routes` 中注册的路由
|
||||
- **THEN** `fetch` fallback SHALL 将请求交给静态资源服务处理(production)或返回提示文本(development)
|
||||
|
||||
### Requirement: 路径参数支持
|
||||
系统 SHALL 使用 routes 对象的 `:param` 语法声明路径参数,替代手动 regex 匹配。
|
||||
|
||||
@@ -38,12 +38,12 @@
|
||||
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 格式的 404 错误响应
|
||||
|
||||
### Requirement: Fetch Fallback 处理
|
||||
系统 SHALL 使用 `fetch` handler 作为兜底,理论上不应被触发(所有路径都被 routes 通配符覆盖)。
|
||||
系统 SHALL 使用 `fetch` handler 作为非 API 请求的入口,负责静态资源服务和 SPA fallback。
|
||||
|
||||
#### Scenario: 未匹配的 API 路由
|
||||
- **WHEN** 请求路径以 `/api/` 开头但未在具体 API 路由中注册
|
||||
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 格式的 404 错误响应
|
||||
#### Scenario: Production 模式 fetch fallback
|
||||
- **WHEN** production 模式下请求未匹配 routes 中的 API 路由
|
||||
- **THEN** `fetch` handler SHALL 调用 `serveStaticAsset` 返回对应静态资源或 SPA fallback
|
||||
|
||||
#### Scenario: 未匹配的非 API 路由
|
||||
- **WHEN** 请求路径不以 `/api/` 开头且未在具体路由中注册
|
||||
- **THEN** `"/*": homepage` 通配符 SHALL 返回前端入口 HTML 文档(带 HMR 注入)
|
||||
#### Scenario: Development 模式 fetch fallback
|
||||
- **WHEN** development 模式下请求未匹配 routes 中的 API 路由
|
||||
- **THEN** `fetch` handler SHALL 返回提示文本,引导开发者通过 Vite dev server 访问前端
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
## Purpose
|
||||
|
||||
定义 Bun.serve fullstack + React + TypeScript 前端开发工作流、开发期 API 访问和共享契约的行为要求。
|
||||
定义基于 Vite dev server + Bun API server 的前端开发工作流、开发期 API 访问和共享契约的行为要求。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Vite React 开发服务器
|
||||
系统 SHALL 提供基于 Bun.serve fullstack 模式的前端开发工作流,并支持热模块替换和 React Fast Refresh。
|
||||
系统 SHALL 提供基于 Vite dev server 的前端开发工作流,支持热模块替换和 React Fast Refresh。
|
||||
|
||||
#### Scenario: 启动前端开发服务器
|
||||
- **WHEN** 开发者启动开发命令
|
||||
- **THEN** 前端 SHALL 由 Bun.serve 的 HTML import 机制提供服务,并通过 `development: { hmr: true, console: true }` 启用 HMR、React Fast Refresh 和浏览器 console 回显
|
||||
- **THEN** 前端 SHALL 由 Vite dev server 提供服务,支持 HMR 和 React Fast Refresh,监听 :5173 端口
|
||||
|
||||
#### Scenario: 构建前端静态资源
|
||||
- **WHEN** 开发者运行前端生产构建命令
|
||||
- **THEN** 系统 SHALL 通过 Bun.build 的 HTML import ahead-of-time bundling 产出可由 Bun 后端服务的前端静态资源
|
||||
- **THEN** 系统 SHALL 通过 Vite build(Rolldown)产出优化的前端静态资源到 `dist/web/`
|
||||
|
||||
### Requirement: 前端开发期 API 代理
|
||||
前端开发服务器 SHALL 在本地开发期间无需代理配置即可访问后端 API,因为前后端运行在同一进程同一端口。
|
||||
前端开发服务器 SHALL 通过 Vite proxy 配置将 API 请求转发到后端 server。
|
||||
|
||||
#### Scenario: 前端开发期调用拨测 API
|
||||
- **WHEN** 浏览器从开发服务器请求 `/api/summary`、`/api/targets` 等拨测 API
|
||||
- **THEN** Bun.serve SHALL 直接由 routes 中注册的 API handler 处理请求,无需 proxy 转发
|
||||
- **WHEN** 浏览器从 Vite dev server 请求 `/api/*` 路径
|
||||
- **THEN** Vite SHALL 通过 proxy 将请求转发到 Bun API server(默认 :3000)
|
||||
|
||||
#### Scenario: 前端开发期访问健康检查
|
||||
- **WHEN** 浏览器从 Vite dev server 请求 `/health`
|
||||
- **THEN** Vite SHALL 通过 proxy 将请求转发到 Bun API server
|
||||
|
||||
#### Scenario: 开发期访问非 API 前端路由
|
||||
- **WHEN** 浏览器从开发服务器请求非 API 前端路由
|
||||
- **THEN** Bun.serve SHALL 将该请求作为前端应用流量处理(SPA fallback 返回 HTML)
|
||||
- **WHEN** 浏览器从 Vite dev server 请求非 API 前端路由
|
||||
- **THEN** Vite SHALL 将该请求作为前端应用流量处理(SPA fallback 返回 HTML)
|
||||
|
||||
### Requirement: 开发期单端口运行
|
||||
项目 SHALL 保证开发命令中前端页面、HMR 和后端 API 由同一个 Bun.serve 进程在同一端口提供服务。
|
||||
### Requirement: 开发期双进程运行
|
||||
项目 SHALL 在开发命令中同时启动 Vite dev server 和 Bun API server 两个进程。
|
||||
|
||||
#### Scenario: 使用默认开发端口
|
||||
- **WHEN** 开发者未提供端口覆盖并运行开发命令
|
||||
- **THEN** Bun.serve SHALL 在默认端口同时提供前端页面、HMR 和后端 API
|
||||
- **WHEN** 开发者运行开发命令
|
||||
- **THEN** Vite dev server SHALL 监听 :5173,Bun API server SHALL 监听配置文件指定的端口(默认 :3000)
|
||||
|
||||
#### Scenario: 使用配置覆盖开发端口
|
||||
- **WHEN** 开发者通过配置文件覆盖端口并运行开发命令
|
||||
- **THEN** Bun.serve SHALL 在配置端口同时提供前端页面、HMR 和后端 API
|
||||
#### Scenario: 开发者访问前端
|
||||
- **WHEN** 开发者打开浏览器
|
||||
- **THEN** 开发者 SHALL 访问 Vite dev server 地址(:5173)获取前端页面
|
||||
|
||||
### Requirement: 前端使用相对 API 路径
|
||||
除非有文档化的部署配置覆盖该行为,前端代码 MUST 通过相对 `/api/*` URL 调用后端 API。
|
||||
@@ -46,21 +50,14 @@
|
||||
|
||||
#### Scenario: 运行环境变化
|
||||
- **WHEN** host 或 port 在开发环境和生产环境之间变化
|
||||
- **THEN** 前端 API 调用 SHALL 无需修改源码即可继续工作
|
||||
- **THEN** 前端 API 调用 SHALL 无需修改源码即可继续工作(开发期通过 Vite proxy,生产期通过同源请求)
|
||||
|
||||
### Requirement: 集成开发命令
|
||||
项目 SHALL 提供一个文档化命令,用于在开发期间同时运行前端和后端。
|
||||
项目 SHALL 提供一个文档化命令,用于在开发期间同时运行 Vite dev server 和 Bun API server。
|
||||
|
||||
#### Scenario: 启动全栈开发
|
||||
- **WHEN** 开发者运行文档化的全栈开发命令
|
||||
- **THEN** 系统 SHALL 启动单个 Bun.serve 进程,同时提供前端 HMR 和后端 API 服务
|
||||
|
||||
### Requirement: 开发质量命令文档化
|
||||
项目 SHALL 在前端开发工作流文档中说明日常检查和完整验证命令。
|
||||
|
||||
#### Scenario: 查阅开发命令
|
||||
- **WHEN** 开发者阅读 README 的开发或测试章节
|
||||
- **THEN** 文档 SHALL 说明 `check` 用于日常开发检查,`verify` 用于提交前或发布前完整验证
|
||||
- **THEN** 系统 SHALL 同时启动 Vite dev server 和 Bun API server,任一进程异常退出时终止另一个
|
||||
|
||||
### Requirement: 共享 TypeScript 契约
|
||||
项目 SHALL 为前端和后端共同使用的请求与响应类型提供共享 TypeScript 边界。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## Purpose
|
||||
|
||||
定义 Bun 全栈应用运行时的 HTTP server、API 命名空间、健康检查、生产静态资源服务和 SPA fallback 行为。
|
||||
定义基于 Vite + Bun 的全栈应用运行时,包括 HTTP server、API 命名空间、健康检查、生产静态资源服务和 SPA fallback 行为。
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -64,26 +64,26 @@
|
||||
- **THEN** Bun server SHALL 返回成功的、机器可读的健康检查响应
|
||||
|
||||
### Requirement: 生产静态资源服务
|
||||
系统 SHALL 在生产模式下通过 Bun 内置的 HTML import manifest 机制服务前端资源。
|
||||
系统 SHALL 在生产模式下通过自定义 `serveStaticAsset` 函数服务嵌入的 Vite 前端产出。
|
||||
|
||||
#### Scenario: 请求构建后的资源
|
||||
- **WHEN** 客户端请求构建后的前端资源
|
||||
- **THEN** Bun server SHALL 通过 manifest 自动返回该资源并带有适当的 content type 和 content-addressable hash URL
|
||||
- **WHEN** 客户端请求 `/assets/*` 路径下的前端资源
|
||||
- **THEN** 系统 SHALL 从 StaticAssets 的 files map 中查找并返回对应资源,Content-Type 根据扩展名推断
|
||||
|
||||
#### Scenario: 请求前端根路径
|
||||
- **WHEN** 客户端请求 `/`
|
||||
- **THEN** Bun server SHALL 通过 routes 中注册的 HTML import 返回前端入口 HTML 文档
|
||||
- **THEN** 系统 SHALL 返回 StaticAssets 中的 indexHtml,Content-Type 为 `text/html; charset=utf-8`
|
||||
|
||||
### Requirement: 生产缓存策略
|
||||
系统 SHALL 利用 Bun 内置的缓存机制为生产静态资源提供缓存策略。
|
||||
系统 SHALL 为生产静态资源提供基于文件名 content hash 的缓存策略。
|
||||
|
||||
#### Scenario: 请求前端入口 HTML
|
||||
- **WHEN** 生产 Bun server 返回前端入口 HTML 文档
|
||||
- **THEN** 响应 SHALL 包含 Bun 自动生成的 ETag header
|
||||
- **WHEN** 生产 server 返回前端入口 HTML 文档
|
||||
- **THEN** 响应 SHALL 包含 `Cache-Control: no-cache` header
|
||||
|
||||
#### Scenario: 请求构建后的静态资源
|
||||
- **WHEN** 生产 Bun server 返回构建后的静态资源
|
||||
- **THEN** 响应 SHALL 包含 Bun 自动生成的 ETag header 和 content-addressable hash URL
|
||||
- **WHEN** 生产 server 返回 `/assets/*` 路径下的静态资源
|
||||
- **THEN** 响应 SHALL 包含 `Cache-Control: public, max-age=31536000, immutable` header
|
||||
|
||||
### Requirement: 低风险安全响应头
|
||||
系统 SHALL 在生产运行时的 JSON API 响应中附加低风险安全响应头;HTML 和静态资源响应由 Bun HTML import manifest 返回其内置 headers。
|
||||
@@ -92,20 +92,20 @@
|
||||
- **WHEN** 生产 Bun server 返回 `/health` 或 `/api/*` JSON 响应
|
||||
- **THEN** 响应 SHALL 包含 `X-Content-Type-Options: nosniff` 和 `Referrer-Policy` headers
|
||||
|
||||
#### Scenario: 生产 HTML 和静态资源响应使用 Bun 内置 headers
|
||||
- **WHEN** 生产 Bun server 返回前端 HTML 文档或构建后的静态资源
|
||||
- **THEN** 响应 SHALL 使用 Bun HTML import manifest 提供的内置 headers,不要求附加自定义安全 headers
|
||||
#### Scenario: 生产静态资源响应
|
||||
- **WHEN** 生产 server 返回前端 HTML 文档或构建后的静态资源
|
||||
- **THEN** 响应 SHALL 不要求附加自定义安全 headers(仅需 Content-Type 和 Cache-Control)
|
||||
|
||||
### Requirement: SPA fallback 行为
|
||||
系统 SHALL 通过 routes 中注册的 `"/*"` HTML import 通配符为非 API 路径返回前端入口 HTML 文档。
|
||||
系统 SHALL 通过 fetch fallback 为非 API、非静态资源路径返回前端入口 HTML 文档。
|
||||
|
||||
#### Scenario: 刷新前端路由
|
||||
- **WHEN** 客户端请求前端路由,例如 `/dashboard`
|
||||
- **THEN** routes 中的 `"/*"` 通配符 SHALL 返回前端入口 HTML 文档
|
||||
- **WHEN** 客户端请求不包含文件扩展名的非 API 路径(如 `/dashboard`)
|
||||
- **THEN** fetch fallback SHALL 返回前端入口 HTML 文档
|
||||
|
||||
#### Scenario: 保留 API 错误语义
|
||||
- **WHEN** 客户端请求未知的 `/api/*` 路由
|
||||
- **THEN** `/api/*` 通配符 MUST 返回 JSON 404 响应,而不是前端入口 HTML 文档
|
||||
- **THEN** routes 中的 `/api/*` 通配符 MUST 返回 JSON 404 响应,而不是前端入口 HTML 文档
|
||||
|
||||
### Requirement: 优雅关机
|
||||
系统 SHALL 在收到终止信号时正确清理资源。
|
||||
|
||||
@@ -5,15 +5,15 @@ TBD - 统一服务启动引导函数,封装开发和生产模式的完整启
|
||||
## Requirements
|
||||
|
||||
### Requirement: 统一启动引导函数
|
||||
系统 SHALL 提供 `src/server/bootstrap.ts` 导出 `bootstrap(options: BootstrapOptions)` 函数,封装完整的服务启动序列:加载配置、创建 store、同步 targets、创建并启动 engine、启动 HTTP server、注册 shutdown handler。`bootstrap` SHALL 不接收或传递静态资源对象,前端资源由 Bun HTML import manifest 自动接管。
|
||||
系统 SHALL 提供 `src/server/bootstrap.ts` 导出 `bootstrap(options: BootstrapOptions)` 函数,封装完整的服务启动序列:加载配置、创建 store、同步 targets、创建并启动 engine、启动 HTTP server、注册 shutdown handler。
|
||||
|
||||
#### Scenario: 开发模式启动
|
||||
- **WHEN** `dev.ts` 调用 `bootstrap({ configPath, mode: "development" })`
|
||||
- **THEN** 系统 SHALL 完成完整启动序列,不传入 staticAssets
|
||||
|
||||
#### Scenario: 生产模式启动
|
||||
- **WHEN** `main.ts` 调用 `bootstrap({ configPath, mode: "production" })`
|
||||
- **THEN** 系统 SHALL 完成完整启动序列,并由 `server.ts` 中的 HTML import 路由接管前端资源
|
||||
#### Scenario: 生产模式启动(带静态资源)
|
||||
- **WHEN** code-generated entry 调用 `bootstrap({ configPath, mode: "production", staticAssets })`
|
||||
- **THEN** 系统 SHALL 完成完整启动序列,并将 staticAssets 传递给 startServer
|
||||
|
||||
#### Scenario: 启动失败处理
|
||||
- **WHEN** 启动过程中任何步骤抛出异常
|
||||
@@ -24,11 +24,15 @@ TBD - 统一服务启动引导函数,封装开发和生产模式的完整启
|
||||
- **THEN** bootstrap 注册的 shutdown handler SHALL 调用 engine.stop() 和 store.close() 后退出
|
||||
|
||||
### Requirement: BootstrapOptions 接口
|
||||
`bootstrap` 函数 SHALL 接受 `BootstrapOptions` 参数,仅包含 `configPath: string` 和 `mode: RuntimeMode`。
|
||||
`bootstrap` 函数 SHALL 接受 `BootstrapOptions` 参数,包含 `configPath: string`、`mode: RuntimeMode` 和可选的 `staticAssets?: StaticAssets`。
|
||||
|
||||
#### Scenario: 最小配置
|
||||
#### Scenario: 最小配置(开发模式)
|
||||
- **WHEN** 仅传入 configPath 和 mode
|
||||
- **THEN** 系统 SHALL 正常启动
|
||||
- **THEN** 系统 SHALL 正常启动,startServer 不接收 staticAssets 参数
|
||||
|
||||
#### Scenario: 生产模式配置
|
||||
- **WHEN** 传入 configPath、mode 和 staticAssets
|
||||
- **THEN** 系统 SHALL 将 staticAssets 传递给 startServer
|
||||
|
||||
### Requirement: dev.ts 和生产入口使用 bootstrap
|
||||
`dev.ts` 和 `src/server/main.ts` SHALL 调用 `bootstrap()` 而非各自维护启动序列。
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
## Purpose
|
||||
|
||||
定义将 Bun HTML import 前端资源与 Bun 后端打包为单个 standalone executable 的生产构建、运行配置和验证要求。
|
||||
定义将 Vite 构建的前端资源通过 code generation 嵌入 Bun 后端,打包为单个 standalone executable 的生产构建、运行配置和验证要求。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 生产构建顺序
|
||||
生产构建 MUST 通过 Bun.build 的 HTML import 识别机制一步完成前端资源打包和后端编译。
|
||||
生产构建 MUST 通过三步流水线完成:Vite 前端构建 → code generation → Bun compile。
|
||||
|
||||
#### Scenario: 运行生产构建
|
||||
- **WHEN** 开发者运行生产构建命令
|
||||
- **THEN** 系统 MUST 调用 Bun.build,自动识别 server 入口中的 HTML import 并完成前端 bundling 和后端编译
|
||||
- **THEN** 系统 MUST 依次执行 Vite build、资源导入 code generation、Bun.build compile,最终输出单可执行文件
|
||||
|
||||
#### Scenario: 前端 bundling 失败
|
||||
- **WHEN** Bun.build 在处理 HTML import 中的前端资源时失败
|
||||
- **THEN** 系统 MUST 停止生产构建,且不能输出 stale executable
|
||||
#### Scenario: Vite 构建失败
|
||||
- **WHEN** Vite build 步骤失败
|
||||
- **THEN** 系统 MUST 停止后续步骤,不生成 code generation 文件或 executable
|
||||
|
||||
#### Scenario: Bun compile 失败
|
||||
- **WHEN** Bun.build compile 步骤失败
|
||||
- **THEN** 系统 MUST 清理 `.build/` 临时目录,不保留 stale executable
|
||||
|
||||
### Requirement: 单 executable 输出
|
||||
生产构建 SHALL 输出一个 standalone executable,其中包含 Bun 后端和通过 HTML import manifest 嵌入的前端资源。构建流程 SHALL 不再生成项目自定义中间产物目录,构建失败时 SHALL 不保留 stale executable。
|
||||
生产构建 SHALL 输出一个 standalone executable,其中包含 Bun 后端和通过 `import with { type: "file" }` 嵌入的 Vite 前端产出。
|
||||
|
||||
#### Scenario: 在目标机器运行 executable
|
||||
- **WHEN** 生成的 executable 在兼容目标平台上运行
|
||||
@@ -24,19 +28,22 @@
|
||||
|
||||
#### Scenario: 服务嵌入的前端
|
||||
- **WHEN** executable 收到前端根路径请求
|
||||
- **THEN** 它 SHALL 通过 Bun 内置的 HTML import manifest 机制服务前端资源,且不需要外部 `dist/` 目录
|
||||
- **THEN** 它 SHALL 通过内嵌的 Vite 构建产出服务前端资源,且不需要外部 `dist/` 目录
|
||||
|
||||
#### Scenario: 服务嵌入 API 和页面
|
||||
- **WHEN** 生成的 executable 启动,且浏览器打开前端根路径
|
||||
- **THEN** 页面 SHALL 展示同一个 executable 进程中 `/api/summary` 和 `/api/targets` 返回的数据
|
||||
|
||||
#### Scenario: 构建成功不生成自定义中间产物
|
||||
- **WHEN** 生产构建成功完成并输出 executable
|
||||
- **THEN** 系统 SHALL 不生成 `.build/` 静态资源清单或 server entry 中间产物
|
||||
### Requirement: 构建中间产物管理
|
||||
构建流程 SHALL 使用 `.build/` 临时目录存放 code generation 产物,构建完成后清理。
|
||||
|
||||
#### Scenario: 构建失败时不保留 stale executable
|
||||
- **WHEN** 生产构建在任意步骤失败
|
||||
- **THEN** 系统 SHALL 不输出上一次构建遗留的 stale executable
|
||||
#### Scenario: 构建成功后清理中间产物
|
||||
- **WHEN** 生产构建成功完成并输出 executable
|
||||
- **THEN** 系统 SHALL 删除 `.build/` 临时目录
|
||||
|
||||
#### Scenario: 构建失败时清理中间产物
|
||||
- **WHEN** 生产构建在 Bun compile 步骤失败
|
||||
- **THEN** 系统 SHALL 删除 `.build/` 临时目录和 stale executable
|
||||
|
||||
### Requirement: 外部运行时配置
|
||||
executable MUST 将环境相关运行时配置保留在嵌入的前端和 server bundle 之外。
|
||||
|
||||
58
openspec/specs/static-asset-embedding/spec.md
Normal file
58
openspec/specs/static-asset-embedding/spec.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Static Asset Embedding
|
||||
|
||||
定义构建时将 Vite 产出的前端静态资源嵌入 Bun 可执行文件的 code generation 流程和运行时静态资源服务逻辑。
|
||||
|
||||
## Purpose
|
||||
|
||||
支持将 Vite 构建的前端资源通过 `import with { type: "file" }` 嵌入 Bun 可执行文件,实现单文件交付的同时保持正确的缓存策略和 Content-Type 处理。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 构建时资源扫描与 Code Generation
|
||||
构建脚本 SHALL 在 Vite build 完成后扫描 `dist/web/` 目录,自动生成 TypeScript 文件,为每个静态资源创建 `import ... with { type: "file" }` 声明。
|
||||
|
||||
#### Scenario: 生成资源导入文件
|
||||
- **WHEN** 构建脚本扫描 `dist/web/` 目录
|
||||
- **THEN** 系统 SHALL 在 `.build/static-assets.ts` 中为每个文件生成 `import fN from "<path>" with { type: "file" }` 语句,并导出 `StaticAssets` 对象
|
||||
|
||||
#### Scenario: StaticAssets 对象结构
|
||||
- **WHEN** `static-assets.ts` 被生成
|
||||
- **THEN** 导出的对象 SHALL 包含 `indexHtml: Blob` 和 `files: Record<string, Blob>` 两个字段,其中 files 的 key 为 URL 路径(如 `/assets/index-a1b2c3.js`)
|
||||
|
||||
#### Scenario: 生成 production server entry
|
||||
- **WHEN** 构建脚本生成资源导入文件后
|
||||
- **THEN** 系统 SHALL 在 `.build/server-entry.ts` 中生成 production 入口,import bootstrap、config 和 staticAssets 并调用 bootstrap
|
||||
|
||||
### Requirement: 运行时静态资源服务
|
||||
系统 SHALL 提供 `serveStaticAsset` 函数,根据请求路径从 StaticAssets 中查找并返回对应资源。
|
||||
|
||||
#### Scenario: 请求根路径
|
||||
- **WHEN** 请求路径为 `/`
|
||||
- **THEN** 系统 SHALL 返回 `indexHtml`,Content-Type 为 `text/html; charset=utf-8`,Cache-Control 为 `no-cache`
|
||||
|
||||
#### Scenario: 请求已知静态资源
|
||||
- **WHEN** 请求路径匹配 `files` 中的某个 key
|
||||
- **THEN** 系统 SHALL 返回对应 Blob,Content-Type 根据文件扩展名推断,Cache-Control 为 `public, max-age=31536000, immutable`
|
||||
|
||||
#### Scenario: 请求未知带扩展名路径
|
||||
- **WHEN** 请求路径包含文件扩展名但未匹配任何已知资源
|
||||
- **THEN** 系统 SHALL 返回 404 响应
|
||||
|
||||
#### Scenario: SPA Fallback
|
||||
- **WHEN** 请求路径不包含文件扩展名且不以 `/api/` 开头
|
||||
- **THEN** 系统 SHALL 返回 `indexHtml`(SPA fallback)
|
||||
|
||||
### Requirement: Content-Type 推断
|
||||
系统 SHALL 根据文件扩展名推断正确的 Content-Type header。
|
||||
|
||||
#### Scenario: JavaScript 文件
|
||||
- **WHEN** 请求路径以 `.js` 或 `.mjs` 结尾
|
||||
- **THEN** Content-Type SHALL 为 `text/javascript; charset=utf-8`
|
||||
|
||||
#### Scenario: CSS 文件
|
||||
- **WHEN** 请求路径以 `.css` 结尾
|
||||
- **THEN** Content-Type SHALL 为 `text/css; charset=utf-8`
|
||||
|
||||
#### Scenario: SVG 文件
|
||||
- **WHEN** 请求路径以 `.svg` 结尾
|
||||
- **THEN** Content-Type SHALL 为 `image/svg+xml`
|
||||
46
openspec/specs/vite-frontend-bundling/spec.md
Normal file
46
openspec/specs/vite-frontend-bundling/spec.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Vite Frontend Bundling
|
||||
|
||||
定义 Vite 作为前端构建工具的配置、产出结构和优化策略。
|
||||
|
||||
## Purpose
|
||||
|
||||
使用 Vite 的 Rolldown 引擎完成前端打包,实现 code splitting、vendor chunk 分离和 CSS 优化,解决 Bun bundler 前端性能问题。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Vite 前端构建配置
|
||||
系统 SHALL 使用 Vite 作为前端构建工具,配置文件位于项目根目录 `vite.config.ts`,以 `src/web` 为 root,产出到 `dist/web/`。
|
||||
|
||||
#### Scenario: 运行 Vite 生产构建
|
||||
- **WHEN** 构建脚本执行 `bunx --bun vite build`
|
||||
- **THEN** Vite SHALL 将 `src/web/index.html` 及其引用的所有模块构建到 `dist/web/` 目录,包含 `index.html` 和 `assets/` 子目录
|
||||
|
||||
#### Scenario: 产出文件名包含 content hash
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `assets/` 目录下的 JS 和 CSS 文件名 SHALL 包含 content hash(如 `index-a1b2c3.js`)
|
||||
|
||||
### Requirement: Code Splitting 策略
|
||||
系统 SHALL 配置 Vite 的 Rolldown code splitting,将 vendor 库分离为独立 chunks。
|
||||
|
||||
#### Scenario: React 相关库分离
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `react`、`react-dom`、`scheduler` SHALL 被打包到名为 `vendor-react` 的独立 chunk
|
||||
|
||||
#### Scenario: TDesign 相关库分离
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `tdesign-react`、`tdesign-icons-react` 相关模块 SHALL 被打包到名为 `vendor-tdesign` 的独立 chunk
|
||||
|
||||
#### Scenario: 图表库分离
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** `recharts` 和 `d3-*` 相关模块 SHALL 被打包到名为 `vendor-chart` 的独立 chunk
|
||||
|
||||
### Requirement: CSS 处理
|
||||
系统 SHALL 通过 Vite 处理 CSS 导入,产出独立的 CSS 文件。
|
||||
|
||||
#### Scenario: CSS 文件产出
|
||||
- **WHEN** Vite 构建完成
|
||||
- **THEN** 所有 CSS 导入 SHALL 被提取为独立的 `.css` 文件到 `assets/` 目录
|
||||
|
||||
#### Scenario: CSS 压缩
|
||||
- **WHEN** Vite 执行生产构建
|
||||
- **THEN** 产出的 CSS 文件 SHALL 经过压缩处理
|
||||
@@ -3,7 +3,9 @@
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun --watch src/server/dev.ts",
|
||||
"dev": "bun run scripts/dev.ts",
|
||||
"dev:server": "bun --watch src/server/dev.ts",
|
||||
"dev:web": "bunx --bun vite --host",
|
||||
"build": "bun run scripts/build.ts",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier . --write",
|
||||
@@ -24,6 +26,7 @@
|
||||
"@types/bun": "^1.3.13",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
@@ -36,7 +39,8 @@
|
||||
"lint-staged": "^17.0.4",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.2"
|
||||
"typescript-eslint": "^8.59.2",
|
||||
"vite": "^8.0.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.34.49",
|
||||
|
||||
174
scripts/build.ts
174
scripts/build.ts
@@ -1,33 +1,151 @@
|
||||
import { rm } from "node:fs/promises";
|
||||
import { readdir, rm, writeFile } from "node:fs/promises";
|
||||
import { join, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const executablePath = fileURLToPath(new URL("../dist/dial-server", import.meta.url));
|
||||
const entrypoint = fileURLToPath(new URL("../src/server/main.ts", import.meta.url));
|
||||
const projectRoot = fileURLToPath(new URL("..", import.meta.url));
|
||||
const distWebDir = join(projectRoot, "dist/web");
|
||||
const buildDir = join(projectRoot, ".build");
|
||||
const executablePath = join(projectRoot, "dist/dial-server");
|
||||
|
||||
await rm(executablePath, { force: true });
|
||||
|
||||
const target = process.env["BUN_TARGET"] ?? process.env["BUILD_TARGET"];
|
||||
const result = await Bun.build({
|
||||
compile: target
|
||||
? {
|
||||
autoloadBunfig: true,
|
||||
autoloadDotenv: true,
|
||||
outfile: executablePath,
|
||||
target: target as Bun.Build.CompileTarget,
|
||||
}
|
||||
: {
|
||||
autoloadBunfig: true,
|
||||
autoloadDotenv: true,
|
||||
outfile: executablePath,
|
||||
},
|
||||
entrypoints: [entrypoint],
|
||||
minify: true,
|
||||
sourcemap: "linked",
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error("构建失败:", result.logs);
|
||||
process.exit(1);
|
||||
async function build() {
|
||||
try {
|
||||
await viteBuild();
|
||||
await codeGeneration();
|
||||
await bunCompile();
|
||||
await cleanup();
|
||||
console.log(`Built executable: ${executablePath}`);
|
||||
} catch (error) {
|
||||
await cleanup();
|
||||
console.error("Build failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Built executable: ${executablePath}`);
|
||||
async function bunCompile() {
|
||||
console.log("Step 3/3: Bun compile...");
|
||||
await rm(executablePath, { force: true });
|
||||
|
||||
const target = process.env["BUN_TARGET"] ?? process.env["BUILD_TARGET"];
|
||||
const result = await Bun.build({
|
||||
compile: target
|
||||
? {
|
||||
autoloadBunfig: true,
|
||||
autoloadDotenv: true,
|
||||
outfile: executablePath,
|
||||
target: target as Bun.Build.CompileTarget,
|
||||
}
|
||||
: {
|
||||
autoloadBunfig: true,
|
||||
autoloadDotenv: true,
|
||||
outfile: executablePath,
|
||||
},
|
||||
entrypoints: [join(buildDir, "server-entry.ts")],
|
||||
minify: true,
|
||||
sourcemap: "linked",
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error("Bun compile failed:", result.logs);
|
||||
await cleanup();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup() {
|
||||
await rm(buildDir, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
async function codeGeneration() {
|
||||
console.log("Step 2/3: Code generation...");
|
||||
await rm(buildDir, { force: true, recursive: true });
|
||||
await Bun.write(join(buildDir, ".gitkeep"), "");
|
||||
|
||||
const allFiles = await scanDir(distWebDir, "/");
|
||||
const importLines: string[] = [];
|
||||
const fileEntries: string[] = [];
|
||||
let indexHtmlVar = "";
|
||||
|
||||
for (let i = 0; i < allFiles.length; i++) {
|
||||
const urlPath = allFiles[i]!;
|
||||
const varName = `f${i}`;
|
||||
const filePath = relative(buildDir, join(distWebDir, urlPath.slice(1)));
|
||||
importLines.push(`import ${varName} from "./${filePath}" with { type: "file" };`);
|
||||
|
||||
if (urlPath === "/index.html") {
|
||||
indexHtmlVar = varName;
|
||||
} else {
|
||||
fileEntries.push(` "${urlPath}": Bun.file(${varName}),`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!indexHtmlVar) {
|
||||
console.error("index.html not found in dist/web/");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const staticAssetsTs = [
|
||||
`import type { StaticAssets } from "../src/server/static";`,
|
||||
"",
|
||||
...importLines,
|
||||
"",
|
||||
`export const staticAssets: StaticAssets = {`,
|
||||
` files: {`,
|
||||
...fileEntries,
|
||||
` },`,
|
||||
` indexHtml: Bun.file(${indexHtmlVar}),`,
|
||||
`};`,
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
await writeFile(join(buildDir, "static-assets.ts"), staticAssetsTs);
|
||||
|
||||
const serverEntryTs = [
|
||||
`import { bootstrap } from "../src/server/bootstrap";`,
|
||||
`import { readRuntimeConfig } from "../src/server/config";`,
|
||||
`import { staticAssets } from "./static-assets";`,
|
||||
"",
|
||||
`async function main() {`,
|
||||
` const { configPath } = readRuntimeConfig();`,
|
||||
` await bootstrap({ configPath, mode: "production", staticAssets });`,
|
||||
`}`,
|
||||
"",
|
||||
`void main().catch((error) => {`,
|
||||
` console.error("启动失败:", error instanceof Error ? error.message : error);`,
|
||||
` process.exit(1);`,
|
||||
`});`,
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
await writeFile(join(buildDir, "server-entry.ts"), serverEntryTs);
|
||||
}
|
||||
|
||||
async function scanDir(dir: string, prefix: string): Promise<string[]> {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
const paths: string[] = [];
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
const urlPath = `${prefix}${entry.name}`;
|
||||
if (entry.isDirectory()) {
|
||||
paths.push(...(await scanDir(fullPath, `${urlPath}/`)));
|
||||
} else {
|
||||
paths.push(urlPath);
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
async function viteBuild() {
|
||||
console.log("Step 1/3: Vite build...");
|
||||
const proc = Bun.spawn(["bunx", "--bun", "vite", "build"], {
|
||||
cwd: projectRoot,
|
||||
stderr: "inherit",
|
||||
stdout: "inherit",
|
||||
});
|
||||
const exitCode = await proc.exited;
|
||||
if (exitCode !== 0) {
|
||||
console.error("Vite build failed");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await build();
|
||||
|
||||
26
scripts/dev.ts
Normal file
26
scripts/dev.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const projectRoot = fileURLToPath(new URL("..", import.meta.url));
|
||||
|
||||
const apiServer = Bun.spawn(["bun", "--watch", "src/server/dev.ts", ...process.argv.slice(2)], {
|
||||
cwd: projectRoot,
|
||||
stderr: "inherit",
|
||||
stdout: "inherit",
|
||||
});
|
||||
|
||||
const viteServer = Bun.spawn(["bunx", "--bun", "vite", "--host"], {
|
||||
cwd: projectRoot,
|
||||
stderr: "inherit",
|
||||
stdout: "inherit",
|
||||
});
|
||||
|
||||
function shutdown() {
|
||||
apiServer.kill();
|
||||
viteServer.kill();
|
||||
}
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
await Promise.race([apiServer.exited, viteServer.exited]);
|
||||
shutdown();
|
||||
@@ -2,6 +2,7 @@ import { join } from "node:path";
|
||||
|
||||
import type { RuntimeMode } from "../shared/api";
|
||||
import type { StartServerOptions } from "./server";
|
||||
import type { StaticAssets } from "./static";
|
||||
|
||||
import { loadConfig, type ResolvedConfig } from "./checker/config-loader";
|
||||
import { ProbeEngine } from "./checker/engine";
|
||||
@@ -26,6 +27,7 @@ export interface BootstrapDependencies {
|
||||
export interface BootstrapOptions {
|
||||
configPath: string;
|
||||
mode: RuntimeMode;
|
||||
staticAssets?: StaticAssets;
|
||||
}
|
||||
|
||||
type BootstrapEngine = Pick<ProbeEngine, "start" | "stop">;
|
||||
@@ -69,6 +71,7 @@ export async function bootstrap(options: BootstrapOptions, dependencies: Bootstr
|
||||
serve({
|
||||
config: { host: config.host, port: config.port },
|
||||
mode: options.mode,
|
||||
staticAssets: options.staticAssets,
|
||||
store,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
import type { RuntimeMode } from "../shared/api";
|
||||
import type { ProbeStore } from "./checker/store";
|
||||
import type { RuntimeConfig } from "./config";
|
||||
import type { StaticAssets } from "./static";
|
||||
|
||||
import homepage from "../web/index.html";
|
||||
import { createApiError, jsonResponse } from "./helpers";
|
||||
import { handleDashboard } from "./routes/dashboard";
|
||||
import { handleHealth } from "./routes/health";
|
||||
import { handleHistory } from "./routes/history";
|
||||
import { handleMeta } from "./routes/meta";
|
||||
import { handleMetrics } from "./routes/metrics";
|
||||
import { serveStaticAsset } from "./static";
|
||||
|
||||
export interface StartServerOptions {
|
||||
config: RuntimeConfig;
|
||||
mode: RuntimeMode;
|
||||
staticAssets?: StaticAssets;
|
||||
store: ProbeStore;
|
||||
}
|
||||
|
||||
export function startServer(options: StartServerOptions) {
|
||||
const { config, mode, store } = options;
|
||||
const { config, mode, staticAssets, store } = options;
|
||||
|
||||
const server = Bun.serve({
|
||||
development: mode === "development" ? { console: true, hmr: true } : false,
|
||||
fetch() {
|
||||
return new Response("Not found", { status: 404 });
|
||||
fetch(req) {
|
||||
if (staticAssets) {
|
||||
return serveStaticAsset(new URL(req.url).pathname, staticAssets);
|
||||
}
|
||||
return new Response("Frontend is served by Vite dev server on :5173", { status: 404 });
|
||||
},
|
||||
hostname: config.host,
|
||||
port: config.port,
|
||||
routes: {
|
||||
"/*": homepage,
|
||||
"/api/*": () => jsonResponse(createApiError("API route not found", 404), { mode, status: 404 }),
|
||||
"/api/dashboard": {
|
||||
GET: (req) => handleDashboard(new URL(req.url), store, mode),
|
||||
|
||||
60
src/server/static.ts
Normal file
60
src/server/static.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
export interface StaticAssets {
|
||||
files: Record<string, Blob>;
|
||||
indexHtml: Blob;
|
||||
}
|
||||
|
||||
const CONTENT_TYPES: Record<string, string> = {
|
||||
".css": "text/css; charset=utf-8",
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".mjs": "text/javascript; charset=utf-8",
|
||||
".png": "image/png",
|
||||
".svg": "image/svg+xml",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
};
|
||||
|
||||
export function contentTypeFor(path: string): string {
|
||||
const dot = path.lastIndexOf(".");
|
||||
if (dot === -1) return "application/octet-stream";
|
||||
const ext = path.slice(dot);
|
||||
return CONTENT_TYPES[ext] ?? "application/octet-stream";
|
||||
}
|
||||
|
||||
export function hasFileExtension(path: string): boolean {
|
||||
const lastSlash = path.lastIndexOf("/");
|
||||
const segment = lastSlash === -1 ? path : path.slice(lastSlash + 1);
|
||||
return segment.includes(".");
|
||||
}
|
||||
|
||||
export function htmlResponse(html: Blob): Response {
|
||||
return new Response(html, {
|
||||
headers: {
|
||||
"Cache-Control": "no-cache",
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function serveStaticAsset(pathname: string, assets: StaticAssets): Response {
|
||||
if (pathname === "/") {
|
||||
return htmlResponse(assets.indexHtml);
|
||||
}
|
||||
|
||||
const file = assets.files[pathname];
|
||||
if (file) {
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
"Cache-Control": "public, max-age=31536000, immutable",
|
||||
"Content-Type": contentTypeFor(pathname),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (hasFileExtension(pathname)) {
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
|
||||
return htmlResponse(assets.indexHtml);
|
||||
}
|
||||
127
tests/server/static.test.ts
Normal file
127
tests/server/static.test.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import {
|
||||
contentTypeFor,
|
||||
hasFileExtension,
|
||||
htmlResponse,
|
||||
serveStaticAsset,
|
||||
type StaticAssets,
|
||||
} from "../../src/server/static";
|
||||
|
||||
function createTestAssets(): StaticAssets {
|
||||
return {
|
||||
files: {
|
||||
"/assets/index-a1b2c3.css": new Blob([".app{}"], { type: "text/css" }),
|
||||
"/assets/index-a1b2c3.js": new Blob(["console.log(1)"], { type: "text/javascript" }),
|
||||
"/assets/vendor-react-x9y8z7.js": new Blob(["react"], { type: "text/javascript" }),
|
||||
"/favicon.svg": new Blob(["<svg/>"], { type: "image/svg+xml" }),
|
||||
},
|
||||
indexHtml: new Blob(["<!doctype html><html></html>"], { type: "text/html" }),
|
||||
};
|
||||
}
|
||||
|
||||
describe("contentTypeFor", () => {
|
||||
test("JavaScript 文件", () => {
|
||||
expect(contentTypeFor("/assets/index-a1b2c3.js")).toBe("text/javascript; charset=utf-8");
|
||||
});
|
||||
|
||||
test("mjs 文件", () => {
|
||||
expect(contentTypeFor("/assets/chunk.mjs")).toBe("text/javascript; charset=utf-8");
|
||||
});
|
||||
|
||||
test("CSS 文件", () => {
|
||||
expect(contentTypeFor("/assets/style.css")).toBe("text/css; charset=utf-8");
|
||||
});
|
||||
|
||||
test("SVG 文件", () => {
|
||||
expect(contentTypeFor("/icon.svg")).toBe("image/svg+xml");
|
||||
});
|
||||
|
||||
test("未知扩展名返回 octet-stream", () => {
|
||||
expect(contentTypeFor("/file.xyz")).toBe("application/octet-stream");
|
||||
});
|
||||
|
||||
test("无扩展名返回 octet-stream", () => {
|
||||
expect(contentTypeFor("/noext")).toBe("application/octet-stream");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasFileExtension", () => {
|
||||
test("有扩展名", () => {
|
||||
expect(hasFileExtension("/assets/index.js")).toBe(true);
|
||||
expect(hasFileExtension("/favicon.svg")).toBe(true);
|
||||
});
|
||||
|
||||
test("无扩展名", () => {
|
||||
expect(hasFileExtension("/dashboard")).toBe(false);
|
||||
expect(hasFileExtension("/")).toBe(false);
|
||||
expect(hasFileExtension("/api/targets")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("htmlResponse", () => {
|
||||
test("返回 HTML 响应带正确 headers", async () => {
|
||||
const blob = new Blob(["<html></html>"]);
|
||||
const response = htmlResponse(blob);
|
||||
|
||||
expect(response.headers.get("Content-Type")).toBe("text/html; charset=utf-8");
|
||||
expect(response.headers.get("Cache-Control")).toBe("no-cache");
|
||||
expect(await response.text()).toBe("<html></html>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("serveStaticAsset", () => {
|
||||
test("根路径返回 indexHtml", async () => {
|
||||
const assets = createTestAssets();
|
||||
const response = serveStaticAsset("/", assets);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("Content-Type")).toBe("text/html; charset=utf-8");
|
||||
expect(response.headers.get("Cache-Control")).toBe("no-cache");
|
||||
expect(await response.text()).toBe("<!doctype html><html></html>");
|
||||
});
|
||||
|
||||
test("已知资源返回对应文件和 immutable 缓存", async () => {
|
||||
const assets = createTestAssets();
|
||||
const response = serveStaticAsset("/assets/index-a1b2c3.js", assets);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("Content-Type")).toBe("text/javascript; charset=utf-8");
|
||||
expect(response.headers.get("Cache-Control")).toBe("public, max-age=31536000, immutable");
|
||||
expect(await response.text()).toBe("console.log(1)");
|
||||
});
|
||||
|
||||
test("未知带扩展名路径返回 404", () => {
|
||||
const assets = createTestAssets();
|
||||
const response = serveStaticAsset("/assets/missing.js", assets);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
test("SPA fallback — 无扩展名路径返回 indexHtml", async () => {
|
||||
const assets = createTestAssets();
|
||||
const response = serveStaticAsset("/dashboard", assets);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("Content-Type")).toBe("text/html; charset=utf-8");
|
||||
expect(response.headers.get("Cache-Control")).toBe("no-cache");
|
||||
expect(await response.text()).toBe("<!doctype html><html></html>");
|
||||
});
|
||||
|
||||
test("SVG 资源返回正确 Content-Type", () => {
|
||||
const assets = createTestAssets();
|
||||
const response = serveStaticAsset("/favicon.svg", assets);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("Content-Type")).toBe("image/svg+xml");
|
||||
expect(response.headers.get("Cache-Control")).toBe("public, max-age=31536000, immutable");
|
||||
});
|
||||
|
||||
test("CSS 资源返回正确 Content-Type", () => {
|
||||
const assets = createTestAssets();
|
||||
const response = serveStaticAsset("/assets/index-a1b2c3.css", assets);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("Content-Type")).toBe("text/css; charset=utf-8");
|
||||
});
|
||||
});
|
||||
36
vite.config.ts
Normal file
36
vite.config.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: "../../dist/web",
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
codeSplitting: {
|
||||
groups: [
|
||||
{
|
||||
name: "vendor-react",
|
||||
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
|
||||
},
|
||||
{
|
||||
name: "vendor-tdesign",
|
||||
test: /[\\/]node_modules[\\/](tdesign-react|tdesign-icons-react)[\\/]/,
|
||||
},
|
||||
{
|
||||
name: "vendor-chart",
|
||||
test: /[\\/]node_modules[\\/](recharts|d3-.*)[\\/]/,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [react()],
|
||||
root: "src/web",
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": "http://127.0.0.1:3000",
|
||||
"/health": "http://127.0.0.1:3000",
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user