插件宿主子系统

插件宿主是 ttsc 最具特色的设计:插件是用户项目里的 Go 源码,描述符是 JS,ttsc 用打包的 Go 工具链按需把源码编译成缓存里的二进制。这一套横跨 JS 启动层(发现、加载、构建、缓存)与 Go driver(注册、配对、执行)。

本子系统的页面:

  • 描述符协议ITtscPlugin 契约、阶段、composescontributors、能力。
  • 加载与发现loadProjectPlugins 全流程、tsconfig 与包自动发现、ttsx 描述符加载、export 条件。
  • go build 缓存buildSourcePlugin、缓存键、跨进程构建锁、贡献者合并、go.work 编排、全局缓存 GC。
  • transform 派发:check/transform 阶段如何 spawn、共享宿主选择、--plugins-json 与链接 manifest。

一图看懂

三个核心概念

1. kind:executable vs linked

由 Go 包名判定(loadProjectPlugins.ts::resolveNativeSourceKind):package mainexecutable(独立旁车),否则 → linked(链接进通用宿主)。只有 transform 阶段能链接。

2. stage:check vs transform

  • check:先跑,失败即止。ttsc fix/format 走 check 插件的 fix/format 子命令。@ttsc/lint 是典型 check 插件。
  • transform:在 emit 阶段注入 AST/文本。typia、nestia 是典型 transform 插件。默认 stage 是 transform(resolvePluginStage,缺省即 transform)。

orderNativePluginsloadProjectPlugins.ts:562)保证 check 在 transform 前。

3. 两种组合:composes(水平) vs contributors(垂直)

这是 ITtscPlugin 里两个易混的字段:

  • composes(水平):多个 plugin 条目派发到同一个二进制。每个条目仍是 compilerOptions.plugins[] 的顶层公民,有自己的生命周期槽。包自动发现可能找到多个必须共享一个 emit 宿主的 transform 包;一个描述符在 composes 里列出另一个时,ttsc 保留原 plugin config 但把被组合条目的源指向本描述符的原生源,让两者解析到同一二进制。
  • contributors(垂直):一个二进制静态链接额外 Go 源,这些源作为顶层插件条目出现。贡献的 npm 包通过宿主插件自己的配置文件发现(如 @ttsc/lintlint.config.ts)。@ttsc/lint 的第三方规则插件就走这条路。

详见 描述符协议

关键不变量

  • JS 描述符,Go 逻辑。带 transformSource/transformOutput 的描述符被 rejectJsTransformFunctions 拒绝——JS transform 函数不是公共契约。
  • 源码 only,不接受预编译二进制source 必须是 Go 包目录或 go.mod,ttsc 自己 go build
  • 缓存键覆盖一切能改变二进制的输入:ttsc/tsgo 版本、平台、entry、Go 编译器身份、Go 构建环境变量、overlay 模块源、插件源文件、贡献者源文件。一个 toolchain 升级会自动产生新二进制。
  • 一个 cache key 只构建一次:跨进程 mkdir 锁让 fan-out(并行测试套件、基准、worker pool)只跑一次 go build,其余进程轮询等二进制出现。

设计取舍

取舍选择原因
插件分发形态Go 源码而非二进制必须按用户项目依赖图编译、跨进程隔离编译失败、供应链可控
何时构建惰性、首次需要时npx ttsc 干净环境也能跑;用 prepare 可预热
缓存键粒度整段 go.mod + 全部源文件哈希任何间接依赖 bump 都失效缓存
多插件共享进程composes(命名派发)+ linked(静态链接)一次 emit pass 跑多个 transform
贡献者隔离必须无 go.mod,活在宿主模块内供应链:贡献者不能拉任意 Go 模块

接下来

描述符协议 开始读最自然,然后是 加载与发现go build 缓存transform 派发