插件加载与发现

packages/ttsc/src/plugin/internal/loadProjectPlugins.ts(~1240 LOC)是插件宿主的 JS 大脑:它读项目配置、发现插件条目(来自 tsconfig 与包自动发现)、校验并组合描述符、然后调 buildSourcePlugin 把每个 Go 源编译成缓存二进制。本页讲它的完整控制流与几个微妙的解析规则。

顶层流程

入口签名(loadProjectPlugins.ts:55)接受 binarycacheDircwdentriesfile/tsconfigprojectRootentries === false 表示完全禁用插件(跳过 tsconfig 条目与包自动发现),是 ttsx --no-plugins 的实现基础。

发现:两个来源

resolvePluginEntriesloadProjectPlugins.ts:342)合并两个来源:

1. tsconfig compilerOptions.plugins[]

每个配置条目算出 baseDir:裸/包说明符(如 "typia/lib/transform")从项目自己的 node_modules 解析,而非声明它的 tsconfig——因为一个 extends 的基础 config(共享 tests/config/tsconfig.json)可能声明插件,但包装在消费项目下。只有相对说明符("./plugin")才相对声明 config 的目录有意义(loadProjectPlugins.ts:352 注释)。

2. 包自动发现

discoverPackagePluginEntriesloadProjectPlugins.ts:371)扫项目 package.json 的直接 dependencies + devDependencies,对每个依赖读其 package.json#ttsc.plugin 标记(readPackagePluginConfig)。这让安装了带 ttsc.plugin 的包就自动启用,无需手写 tsconfig 条目。已在 tsconfig 显式配置的 transform 会去重(hasConfiguredTransform,按 raw 说明符与解析后路径双重判定)。

加载描述符:require 优先,ttsx 兜底

loadPluginEntryloadProjectPlugins.ts:571)解析 transform 说明符到一个 request 路径,然后 requirePluginEntryloadProjectPlugins.ts:639):

为什么需要 ttsx 兜底loadProjectPlugins.ts:624 注释):一个 .ts 源描述符——尤其是把运行时和描述符一起 re-export 的包根——会在 Node loader 的第一个无扩展名 import 或未剥离类型处失败,且其 import 会扇出成一整张源码包的传递图。与其重新实现那张图的构建,不如用 ttsx 跑它(ttsx 已能按需构建每个 .ts 依赖)。这次运行全图强制禁插件(入口用 --no-plugins,每个依赖用 TTSC_PLUGIN_DESCRIPTOR_LOAD),让描述符自己——可能自托管——的 transform 不运行、不死锁。

loadDescriptorViaTtsxloadProjectPlugins.ts:666)写一个临时 shim(@ts-nocheck,import 入口、调工厂、把描述符写成 JSON)和最小 tsconfig,spawn ttsx --no-plugins shim.mts,读回 JSON。

export 条件:ttsc 条件

resolvePluginRequestloadProjectPlugins.ts:1003)对裸包说明符先试 resolvePluginExportConditionloadProjectPlugins.ts:1055)。一个 . 入口是运行时 barrel 的包(如 typia,其 index re-export 整个 validator 运行时)不能直接当描述符入口——加载它会拖进运行时,对自托管 transform 还会成环。这类包通过 exports 加一个 ttsc 条件指向无运行时的描述符来 opt-in:

"exports": { ".": { "ttsc": "./lib/transform.js", "default": "./lib/index.js" } }

该条件只在插件入口解析时被尊重(PLUGIN_EXPORT_CONDITIONS = ["ttsc", "node", "require", "default"]),绝不能用进程级 --conditions=ttsc(那会把包的正常 import 也重定向到描述符、破坏其运行时)。selectExportTarget / containsCondition / resolveConditionalTarget 实现了一个小型 Node exports 解析器,处理子路径映射、条件对象、数组回退、显式 null 阻断。

组合与种类判定

加载后的逐条处理(loadProjectPlugins.ts:105):

  • resolvePluginStage:缺省 transform,拒绝移除的 output 阶段。
  • validatePluginSource / validatePluginContributors:见 描述符协议
  • resolveNativeSourceKindloadProjectPlugins.ts:833):找 go.mod(向上 3 层)、读包名,main → executable,否则 linked。linked 但非 transform 阶段报错。

composePluginSourcesloadProjectPlugins.ts:221)处理水平组合,检测环、拒绝多聚合者、拒绝被组合者自带 contributors、继承聚合者 capabilities(细节见描述符页)。

构建编排:共享宿主选择

加载结果里区分三类:

  • 链接 transform 插件被收集成 linkedContributors(名字形如 linked_000000loadProjectPlugins.ts:121)。
  • executable transform 宿主各自构建,并把链接 contributors 合并进去(mergeContributors)。
  • 若没有 executable transform 宿主但有链接源,构建 fallbackDriverHost——用 cmd/utility-host 作通用驱动宿主(loadProjectPlugins.ts:166)。
  • selectedTransformHost 是被链接插件共享的那个二进制。

最后 orderNativePluginsloadProjectPlugins.ts:562)把 check 排在 transform 前返回。

版本注入与缓存键输入

readTtscVersion(读 ttsc 自己 package.json)与 readTsgoVersion(从项目 node_modules 解析 typescript/package.json)的结果传给 buildSourcePlugin,进缓存键(见 go build 缓存)。

不变量与失败模式

检查失败信息位置
transform 缺失plugin entry is missing a string "transform" fieldloadPluginEntry:578
描述符无效does not export a valid ttsc pluginloadPluginEntry
含 JS transform 函数declares unsupported JS transform functionsrejectJsTransformFunctions
source 不存在(常因 __dirname undefined)长错误,提示用 context.dirnameresolveGoPackageDir:857
go.mod(3 层内)source must be inside a Go moduleresolveNativeSourceKind:843
包内无非测试 .gomust contain at least one non-test ".go" fileresolveNativeSourceKind:849
composes 成环composes cycle detectedcomposePluginSources:256

维护者提示

  • hasProjectPluginEntriesloadProjectPlugins.ts:330)是个便宜的 "有没有启用的插件" 探针,调用方用它跳过插件相关工作而不付完整 loadProjectPlugins 的代价。
  • withPluginLoaderEnvloadProjectPlugins.ts:743)临时设 TTSC_NODE_BINARY/TTSC_TTSX_BINARY,运行后恢复——改它时注意 try/finally 的恢复逻辑。
  • 改发现规则时,相对 vs 包说明符的 baseDir 差异是最易出错处;isRelativePluginSpecifier 是判定中心。

接下来