## Purpose

定义将 Vite 构建的前端资源通过 code generation 嵌入 Bun 后端、静态资源服务与 Content-Type 处理、打包为单个 standalone executable 的生产构建、运行配置和验证要求。

## Requirements

### Requirement: 生产构建顺序
生产构建 MUST 通过三步流水线完成：Vite 前端构建 → code generation → Bun compile。

构建步骤 1-2（Vite build、code generation）的逻辑 SHALL 从 `scripts/build-common.ts` 导入，`scripts/build.ts` 只保留当前平台编译和编排逻辑。

#### Scenario: 运行生产构建
- **WHEN** 开发者运行生产构建命令
- **THEN** 系统 MUST 依次执行 Vite build、资源导入 code generation、Bun.build compile，最终输出单可执行文件

#### Scenario: Vite 构建失败
- **WHEN** Vite build 步骤失败
- **THEN** 系统 MUST 停止后续步骤，不生成 code generation 文件或 executable

#### Scenario: Bun compile 失败
- **WHEN** Bun.build compile 步骤失败
- **THEN** 系统 MUST 清理 `.build/` 临时目录，不保留 stale executable

#### Scenario: 重构后 build 行为不变
- **WHEN** `scripts/build.ts` 改为从 `scripts/build-common.ts` 导入共享构建函数
- **THEN** `bun run build` 的产出文件路径（`dist/dial-server`）、构建步骤顺序和错误处理行为 SHALL 与重构前完全一致

### Requirement: 单 executable 输出
生产构建 SHALL 输出一个 standalone executable，其中包含 Bun 后端和通过 `import with { type: "file" }` 嵌入的 Vite 前端产出。

#### Scenario: 在目标机器运行 executable
- **WHEN** 生成的 executable 在兼容目标平台上运行
- **THEN** 它 SHALL 启动全栈应用，且不要求目标机器安装 Node.js、Bun 或 `node_modules`

#### Scenario: 服务嵌入的前端
- **WHEN** executable 收到前端根路径请求
- **THEN** 它 SHALL 通过内嵌的 Vite 构建产出服务前端资源，且不需要外部 `dist/` 目录

#### Scenario: 服务嵌入 API 和页面
- **WHEN** 生成的 executable 启动，且浏览器打开前端根路径
- **THEN** 页面 SHALL 展示同一个 executable 进程中 `/api/summary` 和 `/api/targets` 返回的数据

### Requirement: 构建中间产物管理
构建流程 SHALL 使用 `.build/` 临时目录存放 code generation 产物，构建完成后清理。

清理逻辑 SHALL 定义在 `scripts/build-common.ts` 中，供 `build.ts` 和 `release.ts` 共用。

#### Scenario: 构建成功后清理中间产物
- **WHEN** 生产构建成功完成并输出 executable
- **THEN** 系统 SHALL 通过 `build-common.ts` 中的 `cleanup()` 函数删除 `.build/` 临时目录

#### Scenario: 构建失败时清理中间产物
- **WHEN** 生产构建在 Bun compile 步骤失败
- **THEN** 系统 SHALL 通过 `build-common.ts` 中的 `cleanup()` 函数删除 `.build/` 临时目录和 stale executable

### Requirement: 外部运行时配置
executable MUST 将环境相关运行时配置保留在嵌入的前端和 server bundle 之外。

#### Scenario: 修改监听端口
- **WHEN** 操作者修改受支持的 port 配置
- **THEN** 同一个 executable SHALL 在不重新构建的情况下监听新端口

#### Scenario: 缺少可选配置
- **WHEN** 可选运行时配置被省略
- **THEN** executable SHALL 使用文档化的默认值

### Requirement: 构建验证
项目 SHALL 提供 `verify` 命令执行质量检查和生产构建；原 smoke test 暂时移除，executable 路由验证由后续变更重新设计。

#### Scenario: 完整验证重新构建 executable
- **WHEN** 开发者运行完整验证命令
- **THEN** 系统 MUST 先执行质量检查，再基于当前源码执行生产构建

#### Scenario: 验证失败
- **WHEN** 质量检查或构建阶段失败
- **THEN** 验证 SHALL 使命令失败

### Requirement: 生产构建版本固化
生产构建 SHALL 在 code generation 阶段读取 `package.json.version`，并将该版本号固化到生成的 production server entry 中，使 standalone executable 能在运行时返回构建时版本。

#### Scenario: 构建时注入版本号
- **WHEN** 开发者运行生产构建命令
- **THEN** 构建脚本 SHALL 在生成 `.build/server-entry.ts` 时写入当前 `package.json.version` 对应的版本字面量

#### Scenario: executable 不依赖外部 package.json 返回版本
- **WHEN** 生成的 standalone executable 在目标机器运行且外部不存在项目根目录 `package.json`
- **THEN** `GET /api/meta` SHALL 仍返回构建时固化的 `version`

#### Scenario: 升迁后重新构建
- **WHEN** 开发者先升迁 `package.json.version` 再运行生产构建命令
- **THEN** 新生成的 standalone executable SHALL 返回升迁后的版本号

### 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`

### Requirement: 静态资源 import specifier SHALL 使用平台无关分隔符
构建时静态资源 code generation SHALL 将文件系统相对路径转换为 ESM import specifier，并确保生成的 import 路径在 Windows、macOS、Linux 开发环境下都使用 `/` 作为分隔符。

#### Scenario: Windows 相对路径转换为 import specifier
- **WHEN** code generation 将 Windows 文件系统相对路径 `..\\dist\\web\\assets\\app.js` 转换为静态资源 import specifier
- **THEN** 生成的 import specifier SHALL 为 `../dist/web/assets/app.js`，且 MUST NOT 包含 `\\`

#### Scenario: POSIX 相对路径保持 import specifier 形式
- **WHEN** code generation 将 POSIX 文件系统相对路径 `../dist/web/assets/app.js` 转换为静态资源 import specifier
- **THEN** 生成的 import specifier SHALL 保持为 `../dist/web/assets/app.js`
