链接插件(driver/plugins.go)

driver 的 plugins.gopackages/ttsc/driver/plugins.go)是"链接式插件"在 Go 进程内的注册、配对与生命周期管理。链接插件是非 main 包的 Go transform 源,被静态链接进通用原生宿主(cmd/utility-host),通过 driver.RegisterPlugininit() 里注册。

本页讲三类 hook 接口、按构建顺序的配对机制、以及为什么用顺序而非包名配对。

链接插件 vs 可执行插件

ttsc 的 transform 插件源有两种"种类"(kind),由 loadProjectPlugins.ts::resolveNativeSourceKind 按 Go 包名判定:

  • executablepackage main,构建成独立旁车二进制,自己 spawn。
  • linked:非 main 包,没有自己的 main,被静态链接进选定的原生宿主,在宿主 main 之前由 init() 注册。

只有 transform 阶段插件能被链接(loadProjectPlugins.ts:116kind === "linked" && stage !== "transform" 报错)。链接是 ttsc 把多个 transform 插件塞进同一个 emit 进程的方式。

manifest 与环境握手

JS 侧把链接插件的 manifest 序列化进 TTSC_LINKED_PLUGINS_JSONLinkedPluginsEnvplugins.go:14),每条是一个 PluginEntry

type PluginEntry struct {
  Config map[string]any `json:"config"`
  Name   string         `json:"name"`
  Stage  string         `json:"stage"`
}

driver 用 loadLinkedPluginState(cwd, tsconfigPath)plugins.go:75)反序列化;环境变量缺失或空时返回零条目状态(不是错误)。

三类生命周期 hook

注册的插件可以实现以下任意接口的子集,driver 在不同时机调用:

type SourcePreamblePlugin interface {
  SourcePreamble(PluginContext) (string, error)
}
type ProgramPlugin interface {
  ApplyProgram(*Program, PluginContext) error
}
type EmitTransformPlugin interface {
  EmitTransform(PluginContext) (PluginTransform, error)
}
Hook时机用途例子
SourcePreamblePluginProgram 创建前,注入源文本在 tsgo parse 项目前前置代码@ttsc/banner 注入 @packageDocumentation JSDoc
ProgramPluginProgram 加载后、emit/输出前改变已加载 Program文本重写型插件(旧路径)
EmitTransformPluginemit 阶段贡献 AST transformer注入 AST 节点typia 等 transform(新路径)

未实现某接口的条目被静默跳过。三类 hook 的调用都遍历 state.entries,按注册顺序配对(sourcePreambleapplyemitTransforms)。

按构建顺序配对(关键设计)

RegisterPluginplugins.go:65)只是把实现追加到全局 pluginRegistry。配对靠位置

func (state linkedPluginState) apply(prog *Program) error {
  for index, entry := range state.entries {
    plugin, ok := registeredPlugin(index)
    if !ok {
      return fmt.Errorf("...entry %d requested but no linked plugin registered at that position", index)
    }
    transform, ok := plugin.(ProgramPlugin)
    if !ok { continue }
    if err := transform.ApplyProgram(prog, state.context(entry)); err != nil {
      return err
    }
  }
  return nil
}

注释(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):

type PluginContext struct {
  Cwd      string
  Entry    PluginEntry   // 含该插件的 config/name/stage
  Tsconfig string
}

state.context(entry)plugins.go:169)为每个条目构造它,把 cwdtsconfig 与该条目的 PluginEntry 打包给插件。

与 Program 的集成点

  • source preambleLoadProgramprogram.go:342)先调 pluginState.sourcePreamble() 收集所有 preamble,拼进 options.SourcePreamble,再用 sourcePreambleFS 包 VFS。
  • ApplyProgram(*Program).ApplyLinkedPlugins()program.go:524)用 pluginsApplied 守卫保证至多跑一次,内部调 state.apply(p)SourceFiles()EmitAllEmitAllRawemit 都会先触发它。
  • EmitTransformEmitLinkedTransformsemit_plugin.go:85)收集所有 EmitTransformPlugin 的 transformer,交给 EmitWithPluginTransformers 按注册顺序在同一 EmitContext 里跑。

不变量

  • RegisterPlugin(nil) panic(plugins.go:66)——nil 插件是编程错误。
  • 三类 hook 都按注册顺序遍历,顺序即语义(preamble 拼接顺序、transform 应用顺序)。
  • ApplyLinkedPlugins 幂等(守卫标志),避免重入;内部 sourceFilesRaw() 用于需要源文件但不能触发 apply 的场合。

失败模式

失败错误位置
manifest JSON 非法ttsc driver: invalid TTSC_LINKED_PLUGINS_JSONloadLinkedPluginState:82
manifest 条目无对应注册linked plugin entry N requested but no linked plugin registeredapply/sourcePreamble/emitTransforms
链接插件被声明为非 transform 阶段JS 侧报错(loadProjectPlugins.ts:116加载阶段

维护者提示

  • 加新 hook 类型时,遵循同样的"接口 + 按位置遍历 + 未实现则跳过"模式,别引入包名解析。
  • 链接插件源不能带自己的 go.mod(必须活在宿主模块里),这既是工作区 overlay 规则的要求,也是供应链特性(贡献者不能拉任意 Go 模块)。这一约束由 buildSourcePlugin.ts::mergeContributors 强制(见 go build 缓存)。

接下来