链接插件(driver/plugins.go)
driver 的 plugins.go(packages/ttsc/driver/plugins.go)是"链接式插件"在 Go 进程内的注册、配对与生命周期管理。链接插件是非 main 包的 Go transform 源,被静态链接进通用原生宿主(cmd/utility-host),通过 driver.RegisterPlugin 在 init() 里注册。
本页讲三类 hook 接口、按构建顺序的配对机制、以及为什么用顺序而非包名配对。
链接插件 vs 可执行插件
ttsc 的 transform 插件源有两种"种类"(kind),由 loadProjectPlugins.ts::resolveNativeSourceKind 按 Go 包名判定:
executable:package main,构建成独立旁车二进制,自己 spawn。linked:非main包,没有自己的main,被静态链接进选定的原生宿主,在宿主main之前由init()注册。
只有 transform 阶段插件能被链接(loadProjectPlugins.ts:116:kind === "linked" && stage !== "transform" 报错)。链接是 ttsc 把多个 transform 插件塞进同一个 emit 进程的方式。
manifest 与环境握手
JS 侧把链接插件的 manifest 序列化进 TTSC_LINKED_PLUGINS_JSON(LinkedPluginsEnv,plugins.go:14),每条是一个 PluginEntry:
driver 用 loadLinkedPluginState(cwd, tsconfigPath)(plugins.go:75)反序列化;环境变量缺失或空时返回零条目状态(不是错误)。
三类生命周期 hook
注册的插件可以实现以下任意接口的子集,driver 在不同时机调用:
未实现某接口的条目被静默跳过。三类 hook 的调用都遍历 state.entries,按注册顺序配对(sourcePreamble、apply、emitTransforms)。
按构建顺序配对(关键设计)
RegisterPlugin(plugins.go:65)只是把实现追加到全局 pluginRegistry。配对靠位置:
注释(plugins.go:62)明说:"ttsc pairs registrations with linked manifest entries by build order, not by package name."
为什么用顺序不用包名:链接插件的 Go 源被 ttsc 的插件构建器拷进宿主模块的子包,并合成一个空白 import 文件让每个贡献者的 init() 在 main 前跑(见 go build 缓存 的 mergeContributors)。这个空白 import 列表是 ttsc 按确定顺序生成的(贡献者按名字排序),所以 init() 注册顺序与 JS 侧 manifest 顺序一一对应。用包名配对反而要求 driver 知道每个插件的包名并做名字解析,徒增耦合。
registeredPlugin(index)(plugins.go:161)越界返回 (nil, false),于是"manifest 要某位置但没注册"会变成清晰错误而非 panic。
PluginContext
每个 hook 收到的上下文(plugins.go:23):
state.context(entry)(plugins.go:169)为每个条目构造它,把 cwd、tsconfig 与该条目的 PluginEntry 打包给插件。
与 Program 的集成点
- source preamble:
LoadProgram(program.go:342)先调pluginState.sourcePreamble()收集所有 preamble,拼进options.SourcePreamble,再用sourcePreambleFS包 VFS。 - ApplyProgram:
(*Program).ApplyLinkedPlugins()(program.go:524)用pluginsApplied守卫保证至多跑一次,内部调state.apply(p)。SourceFiles()、EmitAll、EmitAllRaw、emit都会先触发它。 - EmitTransform:
EmitLinkedTransforms(emit_plugin.go:85)收集所有EmitTransformPlugin的 transformer,交给EmitWithPluginTransformers按注册顺序在同一 EmitContext 里跑。
不变量
RegisterPlugin(nil)panic(plugins.go:66)——nil 插件是编程错误。- 三类 hook 都按注册顺序遍历,顺序即语义(preamble 拼接顺序、transform 应用顺序)。
ApplyLinkedPlugins幂等(守卫标志),避免重入;内部sourceFilesRaw()用于需要源文件但不能触发 apply 的场合。
失败模式
维护者提示
- 加新 hook 类型时,遵循同样的"接口 + 按位置遍历 + 未实现则跳过"模式,别引入包名解析。
- 链接插件源不能带自己的
go.mod(必须活在宿主模块里),这既是工作区 overlay 规则的要求,也是供应链特性(贡献者不能拉任意 Go 模块)。这一约束由buildSourcePlugin.ts::mergeContributors强制(见 go build 缓存)。