shim 设计

本页深入 shim 的设计:模块边界、三种再导出机制的选择规则、生成器与手写文件的协作、版本固定策略、以及插件构建时 shim 如何被 wire 进去。配合 .codex/skills/typescript-go-sync/SKILL.md 阅读。

设计目标与约束

shim 的设计被三件事框定:

  1. Go 模块规则:禁止跨模块 import internal/。这是 shim 存在的根本原因。
  2. 插件作者的稳定表面:插件只能 import github.com/microsoft/typescript-go/shim/<name>,所以 shim 必须暴露插件需要的全部 AST/transform/printer/emit 表面。
  3. 可机械验证的完整性:缺失再导出反复出现过(如 SignatureKindConstruct #230),所以需要一个 CI 闸门把"完整"变成可强制的不变量。

每个 shim 子模块的文件分工

shim/ast/ 为例(最大的 shim):

文件性质内容
surface.go生成导出 API 的纯类型别名 type Foo = innerast.Foo
enums_gen.go生成枚举家族补全(每个暴露枚举的全部成员)
shim.go手写包装函数、//go:linkname 声明(首行 // gen_shims:hand-maintained
parent.go手写额外手写文件(如 SetParentInChildren
lint.go safe_text.go手写其他领域特定包装
extra-shim.json配置喂生成器的额外符号定义
go.mod go.sum配置独立模块,固定 tsgo 伪版本
test/测试shim 级测试

生成器靠 shim.go 首行的 // gen_shims:hand-maintained 标记跳过手写文件。这让"生成"与"手写"在同一目录共存而不互相覆盖。

三种机制的选择规则

机制一:类型别名

surface.gotype Node = innerast.Node。这是最干净的形式,生成器自动产出。ast/surface.go 开头一大批 NodeFactoryCoercibleMutableNodeNodeBase……都是这类,注释说明"让插件用与 tsgo 内部相同的节点工厂与 printer 面向的 AST 形状"。

机制二:手写包装函数

当导出函数的签名命名了未导出类型,生成器会跳过它(无法用别名表达),需要在 shim.go 手写包装:

func SetParentInChildren(node *Node) { innerast.SetParentInChildren(node) }

extra-shim.jsonIgnoreFunctions 告诉生成器"这个导出函数有手写变体,别再生成"。

机制三://go:linkname

未导出符号无法别名也无法直接调用,用 //go:linkname 链接:

import _ "unsafe"

//go:linkname GetNodeAtPosition github.com/microsoft/typescript-go/internal/ast.getNodeAtPosition
func GetNodeAtPosition(...) ...  // 无函数体

ast/shim.go 里的 GetSourceFileOfNode / GetNodeAtPosition 就是这种形式。extra-shim.jsonExtraFunctions 列出要 linkname 的未导出函数。

一个真实的添加例子

.codex/skills/typescript-go-sync/SKILL.md 记录的工作例子:ast.SetParentInChildrenshim/ast/parent.go 作为薄包装暴露,让一个 transform 能在 emit 前重新设置合成节点的父链(emit resolver 会解引用 Parent,否则 nil panic)。这正是 Emit 与重写SetParentInChildrenUnset + restoreOriginalDeclarationSymbols 依赖的能力。

添加流程:

  1. go env GOMODCACHE/github.com/microsoft/typescript-go@<version>/internal/<pkg>/ 里找符号,确认名字、签名、是否导出。
  2. 加到对应 shim/<pkg>/:导出函数干净签名 → 重跑 go run ./tools/gen_shims(或生成器跳过则手写包装);导出类型 → 生成器加别名;未导出 → //go:linkname
  3. 构建 shim 模块与 packages/ttsc 验证链接。

版本固定策略

tsgo 版本逐 shim 模块固定:每个 shim/*/go.modrequire github.com/microsoft/typescript-go v0.0.0-<时间戳>-<hash>,全部保持一致,并作为间接 require 出现在 packages/ttsc/go.mod。同级 go.work 把 shim 子模块 wire 起来;将来 tagged 上游版本可替换这些本地 wire。

bump 流程(细节见 审计与同步):改所有 shim/*/go.modpackages/ttsc/go.mod 的 require 行(保持一致)、刷新各 go.sum、重跑生成器、复查手写 shim.goextra-shim.json(上游 rename/签名变更/导出翻转会破坏包装或 linkname)。

插件构建时的 wire-in

插件在用户项目构建,怎么解析到 shim/*?答案在 buildSourcePlugin.ts

validateSourceReplacementsbuildSourcePlugin.ts:579)拒绝插件自己 replace ttsc 管理的模块——"ttsc supplies its own compiler and shim modules"——防止插件偷换 shim。

不变量

  • surface.go / enums_gen.go 是生成物,手改会被覆盖。
  • shim.go 首行必须是 // gen_shims:hand-maintained,否则生成器会覆盖它。
  • 所有 shim/*/go.mod 的 tsgo 版本必须一致。
  • shim 只做再导出与极薄包装,不加业务逻辑。

接下来