TypeScript 构建系统与 ESM 模块化
学习目标
- 理解 esbuild 多产物构建的架构决策
- 掌握 CLI/MCP Server/Bridge 的分产物策略
- 处理 ESM/CJS 互操作问题
- 识别产物不同步和 TypeScript 类型丢失的陷阱
项目实践
技术栈概览
| 层级 | 技术 |
|---|---|
| 语言 | TypeScript (ESM) |
| 编译 | tsc(类型检查 + ESM 输出) |
| 打包 | esbuild(CLI/MCP/Bridge 产物) |
| 测试 | Vitest |
| 运行时 | Node.js 20+ |
多产物构建策略
OMC 的构建系统生成多个独立产物,每个针对特定运行环境:
完整构建命令:
npm run build# = tsc && node scripts/build-skill-bridge.mjs && node scripts/build-mcp-server.mjs# && node scripts/build-bridge-entry.mjs && npm run compose-docs# && npm run build:runtime-cli && npm run build:team-server && npm run build:cli为什么需要多产物?
不同组件有不同的依赖和运行环境:
| 产物 | 运行环境 | 依赖约束 |
|---|---|---|
dist/index.js | Claude Code Plugin 运行时 | ESM,需要与 Agent SDK 兼容 |
bridge/cli.cjs | 终端 CLI | CJS,需要快速启动(esbuild bundle) |
bridge/mcp-server.cjs | MCP 服务器进程 | CJS,需要 MCP SDK 兼容 |
bridge/team.js | Team 编队运行时 | 大产物(439KB),需要独立加载 |
如果将所有产物合并为一个 bundle:
- 启动时间增加(需要加载不需要的依赖)
- 运行时冲突(ESM 和 CJS 依赖混用)
- npm 包体积膨胀
ESM/CJS 互操作
OMC 的核心库使用 ESM("type": "module"),但 CLI 和 MCP 产物使用 CJS(.cjs):
原因:Claude Code 的 Hook 系统通过 node script.mjs 执行脚本,某些环境对 ESM 的支持不稳定。CJS bundle 确保兼容性。
互操作模式:
// Hook 脚本(ESM)动态导入 dist 模块const { readBoulderState } = await import('../../dist/index.js');开发模式
npm run dev:full# = 同时运行 7 个 watch 进程:# tsc --watch# build-cli.mjs --watch# build-mcp-server.mjs --watch# build-bridge-entry.mjs --watch# build-skill-bridge.mjs --watch# build-runtime-cli.mjs --watch# build-team-server.mjs --watch使用 concurrently 并行运行所有 watch 进程,确保任何源码变更都触发对应的产物重建。
测试策略
OMC 使用 Vitest 作为测试框架,50+ 测试文件分布在:
src/__tests__/— 内联测试(30+ 文件)src/autoresearch/__tests__/— 自动研究的单元测试src/cli/__tests__/— CLI 命令的单元测试src/hooks/__tests__/— Hook 脚本的测试
测试命令:
npm run test # watch 模式npm run test:run # 一次性运行npm run test:coverage # 覆盖率报告发布流程
npm run release# = scripts/release.ts(交互式版本提升)
npm prepublishOnly# = npm run build && npm run compose-docs(确保产物最新)npm 包通过 prepublishOnly hook 确保每次发布前产物是最新的。
问题与规避
问题 1:ESM/CJS 互操作失败
import() 动态导入 ESM 模块时,如果路径不正确或模块未编译,会报 ERR_MODULE_NOT_FOUND。
规避策略:Hook 脚本使用 pathToFileURL() 构造正确的 file:// URL。构建脚本确保产物在 dist/ 目录中可用。
问题 2:产物不同步
开发过程中修改了源码但没有重建产物,导致运行时使用旧代码。
规避策略:npm run dev:full 使用 --watch 模式自动重建。发布时 prepublishOnly 强制完整构建。
问题 3:TypeScript 类型丢失
esbuild bundle 产物不包含类型信息,IDE 无法提供自动补全。
规避策略:tsc 生成 .d.ts 类型声明文件到 dist/。npm 包的 exports 字段同时声明 "import" 和 "types" 入口。
设计取舍
为什么选择 esbuild 而非 webpack?
| 维度 | esbuild | webpack |
|---|---|---|
| 构建速度 | 快(Go 实现,毫秒级) | 慢(JS 实现,秒级) |
| 配置复杂度 | 低(简单的 API) | 高(复杂的 plugin/loader 系统) |
| Tree Shaking | 中(基本支持) | 高(完整的 ESM tree shaking) |
| 代码分割 | 中(基础支持) | 高(复杂的 split chunks) |
| 需求覆盖 | 足够(bundle CLI 和 Hook 脚本) | 过度(OMC 不需要复杂代码分割) |
OMC 的选择:esbuild——更快的构建速度,足够的需求覆盖。OMC 的产物是 CLI 和 Hook 脚本,不需要 webpack 的复杂特性。
参考来源
- esbuild 官方文档 — 极速 JavaScript 打包器
- Vitest 官方文档 — 原生 ESM 测试框架
- TypeScript ESM 互操作 — TypeScript ESM 指南