跳转到内容

TypeScript 构建系统与 ESM 模块化

学习目标

  • 理解 esbuild 多产物构建的架构决策
  • 掌握 CLI/MCP Server/Bridge 的分产物策略
  • 处理 ESM/CJS 互操作问题
  • 识别产物不同步和 TypeScript 类型丢失的陷阱

项目实践

技术栈概览

层级技术
语言TypeScript (ESM)
编译tsc(类型检查 + ESM 输出)
打包esbuild(CLI/MCP/Bridge 产物)
测试Vitest
运行时Node.js 20+

多产物构建策略

OMC 的构建系统生成多个独立产物,每个针对特定运行环境:

完整构建命令

Terminal window
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.jsClaude Code Plugin 运行时ESM,需要与 Agent SDK 兼容
bridge/cli.cjs终端 CLICJS,需要快速启动(esbuild bundle)
bridge/mcp-server.cjsMCP 服务器进程CJS,需要 MCP SDK 兼容
bridge/team.jsTeam 编队运行时大产物(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');

开发模式

Terminal window
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 脚本的测试

测试命令

Terminal window
npm run test # watch 模式
npm run test:run # 一次性运行
npm run test:coverage # 覆盖率报告

发布流程

Terminal window
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?

维度esbuildwebpack
构建速度快(Go 实现,毫秒级)慢(JS 实现,秒级)
配置复杂度低(简单的 API)高(复杂的 plugin/loader 系统)
Tree Shaking中(基本支持)高(完整的 ESM tree shaking)
代码分割中(基础支持)高(复杂的 split chunks)
需求覆盖足够(bundle CLI 和 Hook 脚本)过度(OMC 不需要复杂代码分割)

OMC 的选择:esbuild——更快的构建速度,足够的需求覆盖。OMC 的产物是 CLI 和 Hook 脚本,不需要 webpack 的复杂特性。

参考来源