shim 子系统
packages/ttsc/shim/* 是 ttsc 把 typescript-go 暴露给自己和插件作者的唯一通道。它的存在源于一个 Go 语言约束,它的完整性是 ttsc 的核心职责,并由一套机械闸门强制。
本子系统页面:
为什么需要 shim
ttsc 建在 typescript-go(tsgo,TypeScript 编译器的 Go 移植版,模块 github.com/microsoft/typescript-go)之上。它几乎所有真正的编译器表面都活在那个模块的 internal/* 包里,而 Go 禁止跨模块 import 别的模块的 internal/ 树。
于是 ttsc 把它需要的部分通过 packages/ttsc/shim/<name> 逐个再导出。每个 shim 子模块(ast、checker、compiler、core、printer、scanner、parser、tsoptions、tspath、vfs、bundled、diagnosticwriter、lsp、transformers……)是自己独立的 Go 模块,包装对应的 internal/<name> 包。
核心理念:缺失再导出是 ttsc 的 bug
来自 .codex/skills/typescript-go-sync/SKILL.md 的核心判断:
保持 shim 同步且完整是 ttsc 的核心目的,不是杂活。shim 是源插件作者(typia、nestia、第三方规则)唯一能触碰的 typescript-go 表面。任务是跟踪 tsgo 源码变更,暴露插件需要的每一个 AST、transform、printer、emit API,让插件永远不必自己伸进
internal/。一个缺失的再导出是 ttsc 的 bug,不是插件的 bug。
这把"缺失再导出"从一类反复出现的 bug,变成了一个可被 CI 强制的不变量(见 审计与同步)。
模块结构概览
每个 shim/<name>/ 目录有:
surface.go——生成、勿手改。首行// Code generated ... DO NOT EDIT.。是该包导出 API 表面的纯类型别名(type Foo = innerast.Foo),由go run ./tools/gen_shims产出,重新生成会覆盖。shim.go——手写。首行// gen_shims:hand-maintained,生成器检测此标记并跳过。包装函数与//go:linkname声明住这里。像ast/parent.go这样的额外文件也是手写。extra-shim.json——喂生成器它无法自己推导的符号:ExtraFunctions(要 linkname 的未导出函数)、ExtraMethods、ExtraFields、IgnoreFunctions(生成器应跳过、已有手写变体的导出函数)。- 独立的
go.mod/go.sum,版本固定到同一个 tsgo 伪版本。
三种再导出机制
按符号性质选机制:
//go:linkname 形式需要 _ "unsafe" import 并声明无函数体的函数。
go.work 编排让插件能 import shim
shim 子模块各自独立,靠同级 go.work 串联。但插件是在用户项目里构建的——它们怎么能 import github.com/microsoft/typescript-go/shim/ast?
答案在 go build 缓存:buildSourcePlugin 构建插件时,findTtscOverlayDirs 收集 ttsc 包根 go.mod + 所有 shim/* 子模块 go.mod 作为 overlay 目录,writeGoWork 把它们 wire 进插件构建的临时 go.work,并对 ttsc 管理模块加 replace 指令。所以插件源 import shim 时,go.work 把它解析到 ttsc 仓库里的真实 shim 模块。
唯一的 driver 例外
注意:shim 是给"跑在 Go 进程里的代码"用的,包括 ttsc 自己的 driver 与插件作者。但在 ttsc 自己的 Go 代码里,只有 packages/ttsc/driver import shim(driver/host.go:1)。其余 ttsc Go 代码(cmd、internal、utility)消费 *driver.Program,看不到 shim 类型。这把 typescript-go 类型的泄漏面收敛到一个包。