三语言模型

ttsc 的代码同时活在三种"语言层"里:JavaScript 启动层、Go 原生宿主、以及把 typescript-go 暴露给两者的 shim 层。这一页把每层的职责、边界、以及它们之间的契约讲清楚,因为几乎每个非平凡的改动都跨这条边界。

三层各自负责什么

位置职责绝不做
JS 启动层packages/ttsc/src/**CLI 参数解析、tsconfig/package.json 解析、插件发现、惰性 go build、进程编排、退出码/诊断翻译类型检查、emit、AST transform(这些是 Go 的活)
Go 原生宿主packages/ttsc/{cmd,driver,internal,utility}packages/*/...go建 Program、跑 Checker、emit、文本/AST 重写、lint 规则遍历、图谱构建、LSP 代理决定加载哪些插件、解析 npm 依赖(这些是 JS 的活)
shim 层packages/ttsc/shim/*把 typescript-go 的 internal/* 表面逐个再导出成可被外部模块 import 的公共表面加业务逻辑(shim 只做再导出与极薄包装)

为什么需要这条边界

JS 在 Node 生态里是天然入口npx ttsc 拿到的是 Node 进程;解析 tsconfig.jsonextends 链、做 require.resolve({ paths })、读 package.jsonttsc.plugin、管理跨平台缓存目录(XDG/AppData/Library/Caches),这些都是 Node 的强项。src/plugin/internal/loadProjectPlugins.ts 几乎全是这类工作。

Go 在编译重活上是天然执行者。typescript-go 本身是 Go;要在它的 Program/Checker 之上跑插件 transform、lint 规则、图谱构建,最自然的方式就是用 Go 链接进同一进程,直接拿到 *ast.SourceFile*checker.Checker

两者用进程边界连接,因为:跨进程能隔离编译失败与 panic、能让"一个二进制构建一次被多进程复用"成立、能让 ttsc 自己掌控 emit 的串行化。代价是 spawn + JSON 序列化开销,热路径用常驻宿主摊薄。

JS → Go 的契约

JS 通过三种机制把信息传给 Go 原生宿主:

1. CLI 子命令与参数

cmd/ttsc 接受 build / check / api-compile / api-transform,参数如 --tsconfig=--cwd=--plugins-json=--tsgo-args=--outDir=--emit。构造逻辑在 src/compiler/internal/runBuild.ts::createNativeBuildArgstransformProjectInMemory.ts::createNativeTransformArgs

--plugins-json 携带每个插件的 { config, name, stage }runBuild.ts::serializeNativePlugins)——注意只传协议需要的字段,让命令行短小。

--tsgo-args 是一个 JSON 数组,装着 ttsc 自己没识别、需要原样转发给 tsgo 的 flag(如 --strict--target es2020)。Go 侧 driver.parseTsgoArgs 用 tsgo 自己的命令行解析器把它们解析成 CompilerOptions 覆盖层(driver/program.go:264)。这让 ttsc --strict 在插件构建路径里也能生效——插件构建是 in-process 建 Program,而不是 shell 出 tsgo。

2. 环境变量握手

JS 在 spawn 原生宿主时注入一组环境变量,让旁车不必重新搜 PATH(runBuild.ts::nativePluginEnvtransformProjectInMemory.ts::nativePluginEnv):

环境变量含义
TTSC_NODE_BINARY当前 Node 二进制路径,供旁车回调 Node
TTSC_TSGO_BINARYtsgo 二进制路径
TTSC_TTSX_BINARYttsx launcher 路径(用于加载 .ts 插件描述符)
TTSC_LINKED_PLUGINS_JSON链接插件 manifest(仅 transform 阶段、有链接源时)
TTSC_LSP_PLUGINS_JSONLSP 插件 manifest(ttscserver 读)
TTSC_LINKED_PLUGINS_JSONdriver 读它来配对链接插件(driver/plugins.go:14

TTSC_LINKED_PLUGINS_JSON 是 JS→driver 的关键握手:JS 决定哪些链接 Go 包要在同一进程里运行并把它们的 manifest 序列化进去,driver 在 loadLinkedPluginState 里反序列化,再按注册顺序与 init() 注册的插件实现配对(driver/plugins.go:75)。

3. stdout 上的 JSON 信封

原生宿主把结构化结果写到 stdout 的一行 JSON 里:

  • api-compile{ diagnostics, output }output 是按项目相对路径键控的 emit 文本(cmd/ttsc/api_compile.go:22)。
  • api-transform{ diagnostics, typescript }typescript 是每个非库源文件的文本(cmd/ttsc/api_transform.go:18)。
  • transform 旁车额外可带 dependencies(每文件咨询过的源列表,watch 元数据)。

JS 侧用 parseNativeTransformOutputtransformProjectInMemory.ts:333)解析;信封形状错误被当作协议错误抛出,并带上 stderr 上下文。

Go → JS 的契约:退出码 + 诊断

Go 宿主用退出码表达成败(0 成功,2 用户错误/构建失败,3 emit/host 失败),用 stderr/stdout 表达诊断。JS 侧 normalizeBuildOutputrunBuild.ts:936)把原始 spawn 结果归一化成 TtscBuildResult,并在缺少结构化诊断时用 parseCompilerDiagnostics 从文本里解析出 file:line:col - category TSxxxx: message 三种格式之一。

编程式 API 进一步把它翻成 embed-typescript 风格的判别联合:success / failure / exception,并用 classifyException 把异常归到 plugin / host / unknownsrc/TtscCompiler.ts:290)。

shim 层的契约

shim 是第三层,专门服务前两层里跑在 Go 进程内的代码:

  • 只有 driver import shim。其余 Go 代码消费 *driver.Program,看不到 shim 类型。这是刻意的:把 typescript-go 的类型泄漏面收敛到一个包。
  • shim 的"客户"是插件作者。typia、nestia、第三方 lint 规则都只能 import github.com/microsoft/typescript-go/shim/<name>,永远不能 import tsgo 的 internal/
  • shim 的同步与完整性是 ttsc 的核心职责,有专门的机械闸门 shim_audit 强制(见 shim 审计与同步)。

一个具体例子:ttsc --strict 在 typia 项目里怎么走

  1. 用户跑 npx ttsc --strict
  2. JS runTtsc--strict 归到 passthrough(runTtsc.ts::parseBuildArgs,因为 schema 不认识它)。
  3. resolveExecutionContext 发现项目有 typia 这个 transform 插件,于是 loadProjectPlugins 惰性把 typia 的 Go transform 源构建成缓存二进制。
  4. createNativeBuildArgs--strict 包进 --tsgo-args=["--strict"]runBuild.ts::createNativeTsgoArgs),spawn typia 的原生宿主。
  5. 原生宿主 cmd/ttsc builddriver.LoadProgramparseTsgoArgs 用 tsgo 解析器把 --strict 变成 CompilerOptions 覆盖层,合并进 tsconfig(driver/program.go:361)。
  6. typia 的链接 transform 在 emit 阶段注入 AST,driver emit 出 JS,退出码与诊断回到 JS,翻译成结构化结果。

这条链路同时穿过三层,是理解 ttsc 的最佳单一例子。

维护者提示

  • 改 JS→Go 协议(新参数、新环境变量、新信封字段)时,三处都要改:JS 构造侧(runBuild.ts / transformProjectInMemory.ts)、Go 解析侧(cmd/ttsc/*.goflag.FlagSetdriver)、以及测试。filterHostArgscmd/ttsc/filter.go)会在 flag.Parse 前剥掉未知转发 flag,别让新参数被它吃掉。
  • 别在 Go 宿主里 hardcode 消费者特定行为(见 .codex/skills/development/SKILL.md 的"尊重包边界")。
  • 别引入 JS transform 函数;它们不是公共契约,且会被 rejectJsTransformFunctions 拒绝。

接下来