build: adopt esbuild for frontend bundling (build:web → public/build/)

- esbuild dev dep; build:web/dev:web bundle public/main.ts → public/build/main.js
- gitignore public/build/; tsconfig.web.json now typecheck-only
- frontend build convention documented in PLAN §1, DISPATCH, ARCHITECTURE §5
- resolves the T1 deferred decision
This commit is contained in:
Yaojia Wang
2026-06-16 07:36:01 +02:00
parent 987cab8eb7
commit 409b208928
8 changed files with 38 additions and 18 deletions

View File

@@ -360,6 +360,9 @@ client → {resize, cols, rows} // 独立消息,不混进 input
## 5. 前端架构(`public/`)
**构建**:打包器 = esbuild(T1 )。入口 `public/main.ts` `npm run build:web` `public/build/main.js`(+ `main.css`);
`index.html` `<script type="module" src="./build/main.js">` 加载;server 静态托管 `public/``tsconfig.web.json` 仅类型检查
| 文件 | 职责 | 关键点 |
|------|------|--------|
| `main.ts` | xterm + FitAddon 初始化WS 客户端重连 | WS URL 同源,**scheme 随页面协议**:`(location.protocol === 'https:' ? 'wss' : 'ws') + '://' + location.host + '/term'`(M6:HTTPS 页面下硬写 `ws://` 会被浏览器 mixed-content 拦截, Tailscale/TLS 部署正是 HTTPS);`term.onData → ws.send {input}`;`onmessage → term.write(output)` |

View File

@@ -129,8 +129,9 @@ Accept:vitest run origin 全绿。歧义则 [!] BLOCKED;不写 LOG;结尾给日
### T8 — index.html · `Agent(module-builder, model:"haiku", isolation:"worktree")`
```
执行 PLAN 任务 T8(public/index.html)。先读 PLAN 任务 T8、ARCHITECTURE §5。
硬边界 —— 只创建:public/index.html。要点:终端容器 #term、键栏挂载点 #keybar、引 main.ts/style.css、
移动端 viewport meta。Accept:浏览器打开无控制台报错。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目
硬边界 —— 只创建:public/index.html。要点:终端容器 #term、键栏挂载点 #keybar、移动端 viewport meta;
**按 PLAN §1 前端构建约定**用 `<script type="module" src="./build/main.js">` 加载(及 `<link>` 引 ./build/main.css 与 style.css)
Accept:浏览器打开无控制台报错。歧义则 [!] BLOCKED;不写 LOG;结尾给日志条目。
```
### T9 — style.css · `Agent(module-builder, model:"haiku", isolation:"worktree")`
@@ -152,8 +153,8 @@ Accept:vitest run keybar 全绿。歧义则 [!] BLOCKED;不写 LOG;结尾给日
### T11 — main.ts · `Agent(module-builder, model:"sonnet", isolation:"worktree")`
```
执行 PLAN 任务 T11(public/main.ts)。先读 PLAN 任务 T11、ARCHITECTURE §5/§6,搜 "M6" 照做;背景 TECH_DOC §6/§9。
依赖 src/types.ts 的 mountKeybar 签名(仅签名,可并行)。硬边界 —— 只创建:public/main.ts。
要点:xterm + FitAddon,fit() 必须在容器有真实尺寸后调用(display:none 得 NaN);
依赖 src/types.ts 的 mountKeybar 签名(仅签名,可并行)。硬边界 —— 只创建:public/main.ts。本文件是 **esbuild 打包入口**(PLAN §1 约定)。
要点:xterm + FitAddon + `import '@xterm/xterm/css/xterm.css'`,fit() 必须在容器有真实尺寸后调用(display:none 得 NaN);
**WS scheme 随页面协议 https→wss(M6)**,URL …/term,首帧发 attach(localStorage 取 sessionId);
term.onData→ws.send {input};onmessage 按 type 处理(attached 存 id、output→term.write、exit→提示重连);
ResizeObserver→fit→发 resize(防抖 100ms);断线指数退避重连 1/2/4…≤30s 携带 sessionId;mountKeybar(d=>ws.send)。
@@ -194,7 +195,7 @@ Accept:vitest run manager 全绿。歧义则 [!] BLOCKED;不写 LOG;结尾给日
```
执行 PLAN 任务 T14(server.ts)。先读 PLAN 任务 T14、ARCHITECTURE §3.6,逐条搜 "M4" "M5" "L3" "L5" 照做;
用 config/protocol/origin/manager。硬边界 —— 只创建:src/server.ts。
要点:Express 托管 public/;**WebSocketServer({noServer:true})**;HTTP upgrade:非 /term→destroy、Origin 不过→
要点:Express 静态托管 public/(含 esbuild 产物 public/build/);**WebSocketServer({noServer:true})**;HTTP upgrade:非 /term→destroy、Origin 不过→
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')+destroy、通过→handleUpgrade→emit connection(L3);
connection 等首帧 attach→try handleAttach 回 attached,**catch→send exit(-1,reason)+close 仅此连接,不 fail-fast(M4)**;
message:parse,ok:false 丢弃记日志,否则路由 writeInput/resize;close→detach;SIGINT/SIGTERM→shutdown;

View File

@@ -64,6 +64,12 @@ W5 验收与收尾(按 F# 并行) T16T20 F1F9 实测 · T21 安全+README
> ARCH §6 的 S1S8 是线性视角;本计划是其**并行重排**。每个任务标了 `ARCH:` 做溯源。
**前端构建约定(T1 已定,T8/T11/T14 遵守)**:打包器 = **esbuild**
- 入口 `public/main.ts`;`npm run build:web` → 打包到 **`public/build/main.js`**(若 main.ts `import '@xterm/xterm/css/xterm.css'`,
esbuild 同时出 `public/build/main.css`)。开发用 `npm run dev:web`(--watch)。`public/build/` 已 gitignore。
- `index.html``<script type="module" src="./build/main.js">` 加载,并 `<link rel="stylesheet" href="./build/main.css">`(若有)+ 自己的 `style.css`
- `server.ts` 静态托管整个 `public/`(含 `public/build/`)。`tsconfig.web.json` 仅类型检查(noEmit)。
---
## 2. 任务清单
@@ -160,7 +166,7 @@ W5 验收与收尾(按 F# 并行) T16T20 F1F9 实测 · T21 安全+README
#### T8 · `public/index.html` `[ ]`
- **Wave/ARCH**: W1 / S7 §5 · **Owns**: `public/index.html`
- **Depends**: 无(可在 T2 前都行) · **Parallel-safe**: 全部
- **Steps**: [ ] 终端容器 `#term`、键栏挂载点 `#keybar`、引 main.ts/style.css;viewport meta(移动端)
- **Steps**: [ ] 终端容器 `#term`、键栏挂载点 `#keybar`;`<script type="module" src="./build/main.js">` + `style.css`(及 `./build/main.css` 若有);viewport meta(移动端)。**按§1 前端构建约定**
- **Accept**: 浏览器打开无控制台报错(静态)
#### T9 · `public/style.css` `[ ]`
@@ -183,7 +189,7 @@ W5 验收与收尾(按 F# 并行) T16T20 F1F9 实测 · T21 安全+README
- **Wave/ARCH**: W1 / S7 §5/§6 · **Owns**: `public/main.ts`
- **Depends**: T2 + T10(仅 `mountKeybar` 签名,可并行) · **Parallel-safe**: 后端全部
- **Steps**:
- [ ] xterm + FitAddon 初始化;`fit()` 在容器有真实尺寸后调用(§9 坑)
- [ ] (本文件是 esbuild 打包入口,见§1 约定)xterm + FitAddon 初始化 + `import '@xterm/xterm/css/xterm.css'`;`fit()` 在容器有真实尺寸后调用(§9 坑)
- [ ] WS:**scheme 随页面协议** `https→wss`(M6),URL `…/term`,首帧发 `attach`(localStorage 取 sessionId)
- [ ] `term.onData → ws.send {input}`;`onmessage` 按 type 处理(attached 存 id、output→`term.write`、exit→提示重连)
- [ ] `ResizeObserver`/resize → `fit()` → 发 `resize`(防抖 100ms)
@@ -234,7 +240,7 @@ W5 验收与收尾(按 F# 并行) T16T20 F1F9 实测 · T21 安全+README
- **Wave/ARCH**: W4 / S6 §3.6 · **Owns**: `src/server.ts`
- **Depends**: T4、T5、T7、T13 · **Parallel-safe**: T15 可并行起草用例
- **Steps**:
- [ ] Express 托管 `public/` + 编译产物
- [ ] Express 托管 `public/`(含 esbuild 产物 `public/build/`,见§1 约定);提示:start 前需 `npm run build:web`(或 `dev:web` 监听)
- [ ] `WebSocketServer({ noServer:true })`(L3);HTTP `upgrade`:非 `/term`→destroy、Origin 不过→`401\r\n\r\n`+destroy、通过→`handleUpgrade`→emit connection
- [ ] `connection`:等首帧 attach→`try handleAttach` 回 attached;`catch`→send `exit(-1,reason)`+close(仅此连接,不 fail-fast)(M4)
- [ ] `message`:parse→ok:false 丢弃记日志,否则路由 writeInput/resize

View File

@@ -27,9 +27,7 @@
- **Wave**: W0(T1 ✓)→ 下一步 **T2**(冻结 `src/types.ts`)
- **任务**: T2 — 共享类型契约,完成后 W0 全部 commit,即可放开 W1 并行
- **下一步**: 派 `module-builder`(sonnet)执行 T2,或主会话直接做;依据 ARCHITECTURE §3
- **阻塞**: 无。但有 1 个待决项(非阻塞):**前端浏览器打包策略未定**(tsconfig.web 当前只类型检查,
不产出可被浏览器加载的 JS;main.ts 引 @xterm 裸导入需 bundler/import-map/CDN)。需在 **T11/T14 前**决定,
建议加 esbuild 作为 `build:web`
- **阻塞**: 无。(前端打包待决项**已解决**:选 esbuild,已装好并接好 `build:web`/`dev:web`,约定写入 PLAN §1。)
- **最后更新**: 2026-06-16
---
@@ -117,11 +115,20 @@
2. 依赖解析到较新主版本(express 5 / xterm 6 / typescript 6 / vitest 4):**T7/T11/T14/T15 注意 API 差异**
(express 5 路由、xterm 6 import 形态)。
3. 加了 PLAN 未列的脚本 `dev`/`typecheck``postinstall`(便利 + 必需修复),属合理补充,未改 PLAN 意图。
- **遗留 / 待办**(非阻塞,需在 **T11/T14 前**决):**前端浏览器打包未定**`tsconfig.web.json` 现为 `noEmit`(仅类型检查);
`public/main.ts` 裸导入 `@xterm/xterm`,浏览器无法直接解析,需 bundler(建议 esbuild,加 `build:web`)/import-map/CDN 三选一。
建议下一波前由 orchestrator 拍板,若选 esbuild 则**现在补装**(避免后续并发 install)。
- **遗留 / 待办**: 无(原前端打包待决项已解决,见下补记)。
- **commit**: `b126cf0`
### 2026-06-16 · T1 补记 — 前端打包定为 esbuild
- **状态**: `[x]` DONE
- **改动**: 装 `esbuild`(dev);`package.json``build:web`/`dev:web`(打包 `public/main.ts``public/build/main.js`);
`.gitignore` 改忽略 `public/build/`;`tsconfig.web.json` 注释更新为"仅类型检查";前端构建约定写入
`PLAN §1` 并更新 T8/T11/T14 与 `DISPATCH.md``ARCHITECTURE §5`
- **验证**: esbuild 0.28.1;临时入口 `--bundle --format=esm --outdir=public/build``main.js`+`.map`,smoke 通过。
- **决策**: 三选一中选 **esbuild**(极快、零配置、与 node-pty/tsx 生态一致);**现在装好**避免 W1 并发 install。
约定:入口 main.ts、产物 public/build/(已 gitignore)、index.html 用 `<script type=module src=./build/main.js>`、server 静态托管 public/。
- **commit**: (见下次提交)
### 2026-06-15 · T0 项目记录起点(计划就绪)
- **状态**: `[x]` DONE