三语言模型
ttsc 的代码同时活在三种"语言层"里:JavaScript 启动层、Go 原生宿主、以及把 typescript-go 暴露给两者的 shim 层。这一页把每层的职责、边界、以及它们之间的契约讲清楚,因为几乎每个非平凡的改动都跨这条边界。
三层各自负责什么
为什么需要这条边界
JS 在 Node 生态里是天然入口。npx ttsc 拿到的是 Node 进程;解析 tsconfig.json 的 extends 链、做 require.resolve({ paths })、读 package.json 的 ttsc.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::createNativeBuildArgs 与 transformProjectInMemory.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::nativePluginEnv、transformProjectInMemory.ts::nativePluginEnv):
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 侧用 parseNativeTransformOutput(transformProjectInMemory.ts:333)解析;信封形状错误被当作协议错误抛出,并带上 stderr 上下文。
Go → JS 的契约:退出码 + 诊断
Go 宿主用退出码表达成败(0 成功,2 用户错误/构建失败,3 emit/host 失败),用 stderr/stdout 表达诊断。JS 侧 normalizeBuildOutput(runBuild.ts:936)把原始 spawn 结果归一化成 TtscBuildResult,并在缺少结构化诊断时用 parseCompilerDiagnostics 从文本里解析出 file:line:col - category TSxxxx: message 三种格式之一。
编程式 API 进一步把它翻成 embed-typescript 风格的判别联合:success / failure / exception,并用 classifyException 把异常归到 plugin / host / unknown(src/TtscCompiler.ts:290)。
shim 层的契约
shim 是第三层,专门服务前两层里跑在 Go 进程内的代码:
- 只有
driverimport 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 项目里怎么走
- 用户跑
npx ttsc --strict。 - JS
runTtsc把--strict归到 passthrough(runTtsc.ts::parseBuildArgs,因为 schema 不认识它)。 resolveExecutionContext发现项目有 typia 这个 transform 插件,于是loadProjectPlugins惰性把 typia 的 Go transform 源构建成缓存二进制。createNativeBuildArgs把--strict包进--tsgo-args=["--strict"](runBuild.ts::createNativeTsgoArgs),spawn typia 的原生宿主。- 原生宿主
cmd/ttsc build调driver.LoadProgram,parseTsgoArgs用 tsgo 解析器把--strict变成CompilerOptions覆盖层,合并进 tsconfig(driver/program.go:361)。 - typia 的链接 transform 在 emit 阶段注入 AST,driver emit 出 JS,退出码与诊断回到 JS,翻译成结构化结果。
这条链路同时穿过三层,是理解 ttsc 的最佳单一例子。
维护者提示
- 改 JS→Go 协议(新参数、新环境变量、新信封字段)时,三处都要改:JS 构造侧(
runBuild.ts/transformProjectInMemory.ts)、Go 解析侧(cmd/ttsc/*.go的flag.FlagSet、driver)、以及测试。filterHostArgs(cmd/ttsc/filter.go)会在flag.Parse前剥掉未知转发 flag,别让新参数被它吃掉。 - 别在 Go 宿主里 hardcode 消费者特定行为(见
.codex/skills/development/SKILL.md的"尊重包边界")。 - 别引入 JS transform 函数;它们不是公共契约,且会被
rejectJsTransformFunctions拒绝。