Skip to Content
流程Sandbank Integration

Sandbank ESM / CommonJS 集成方案

问题

sandbank v0.5.4 + @sandbank.dev/core v0.3.6 + adapter 子包都是 ESM-only"type": "module"exports 字段定义)。

apps/api 当前 tsconfig:

  • module: "CommonJS"
  • moduleResolution: "Node"(不是 Node16/NodeNext/Bundler)

直接 import { createProvider } from '@sandbank.dev/core' 编译失败(找不到模块类型)+ 运行时 require() 会抛 ERR_REQUIRE_ESM

这是真实工程障碍,POC 启动前必须先选一个方案。

方案对比

方案 A · 整个 apps/api 切到 NodeNext ESM

module: CommonJSNode16NodeNextmoduleResolution → 同步。

优点:彻底解决,未来用其他 ESM 包零摩擦 缺点

  • 所有相对 import 必须显式带 .js 扩展(NestJS 现有约 150 处 import 都要改)
  • Jest 配置改成 ESM(extensionsToTreatAsEsmglobalSetup 等)
  • NestJS 启动入口 bootstrap.ts 需要确认与 ESM 兼容
  • 现有 unit / e2e 测试可能全部重新跑通

工作量:1-2 天 风险:可能踩到 NestJS 5.x ESM 兼容坑(@nestjs/swagger 等周边包历史上对 ESM 支持迟)

方案 B · Dynamic import(推荐

保留 CommonJS 主体,只在 task-runner-provider.factory.tsawait import()

// factory.ts —— 改成 async export async function createTaskRunnerProvider(env: Env): Promise<TaskRunnerProvider> { const { createProvider } = await import('@sandbank.dev/core'); if (env.TASK_RUNNER_MODE === 'private') { const { BoxLiteAdapter } = await import('@sandbank.dev/boxlite'); return createProvider(new BoxLiteAdapter({ baseUrl: env.BOXLITE_HOST! })) as TaskRunnerProvider; } const { FlyioAdapter } = await import('@sandbank.dev/flyio'); return createProvider(new FlyioAdapter({ apiToken: env.FLY_API_TOKEN!, appName: env.FLY_APP_NAME, })) as TaskRunnerProvider; }

配套

  • TaskRunnerDispatcher constructor 改用 lazy init(首次 dispatch 时 await provider)
  • 类型用本地最小子集(已实现:见 apps/api/src/task-runner/task-runner-provider.types.ts)——避免 TS nodenext resolution 强制
  • 或用 tsconfig"moduleResolution": "Node16" 仅为类型解析(编译仍 CJS)

优点

  • 影响面最小,只动 factory 一个文件
  • TS / Jest / NestJS 启动无需改造
  • Node.js 自 v12 起 CommonJS 模块可以 await import() ESM

缺点

  • factory 变 async,调用方要 await(dispatcher 已经是 async,问题不大)
  • 类型用本地最小子集,要手动跟 sandbank API 对齐(升级 sandbank 时注意)

工作量:2-4 小时 推荐理由:风险最小、与 POC “13 天双轨”工期最匹配

方案 C · 直接调 Fly Machines REST(放弃 Sandbank 抽象)

把 Sandbank 抽象去掉,主 API 直接 fetch('https://api.machines.dev/v1/apps/...')

优点

  • 完全绕开 ESM 问题
  • POC 公有云路径最快上线

缺点

  • 破坏双轨架构核心 —— 私有化路径要自己再实现一份 BoxLite REST 调用
  • 报告里所有关于”Sandbank adapter 切换”的论述失效
  • 未来加 backend 工作量直接变 1-2 天/个

判定:仅作为公有云 demo 紧急上线时的退路;不推荐作为本期 POC 决策。

推荐路径

采纳方案 B

  1. task-runner-provider.factory.tscreateTaskRunnerProvider 改为 async + dynamic import
  2. dispatcher 的 constructor 改为 lazy init(首次 dispatch()await this.factory(env)
  3. 单元测试相应改 async
  4. typecheck + jest 全过即完成

预计 0.5 天工作量,比方案 A 的 1-2 天小一个数量级,且不引入 ESM 切换风险。

已装的依赖

// apps/api/package.json { "dependencies": { "sandbank": "^0.5.4", "@sandbank.dev/core": "^0.3.6", "@sandbank.dev/flyio": "^0.2.0", "@sandbank.dev/boxlite": "^0.2.0" } }

这些包已 pnpm add 完毕。方案 B 实施后无需重装。

何时切方案 A

如果未来发现:

  • 业务需要更多 ESM-only 包(sandbank 之外)
  • Dynamic import 的 async 传染让 dispatcher 变得难读
  • @nestjs 生态在 2026 后期对 ESM 支持更完善

→ 那时再考虑统一切 ESM。本期 POC 不做。