插件加载与发现
packages/ttsc/src/plugin/internal/loadProjectPlugins.ts(~1240 LOC)是插件宿主的 JS 大脑:它读项目配置、发现插件条目(来自 tsconfig 与包自动发现)、校验并组合描述符、然后调 buildSourcePlugin 把每个 Go 源编译成缓存二进制。本页讲它的完整控制流与几个微妙的解析规则。
顶层流程
入口签名(loadProjectPlugins.ts:55)接受 binary、cacheDir、cwd、entries、file/tsconfig、projectRoot。entries === false 表示完全禁用插件(跳过 tsconfig 条目与包自动发现),是 ttsx --no-plugins 的实现基础。
发现:两个来源
resolvePluginEntries(loadProjectPlugins.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. 包自动发现
discoverPackagePluginEntries(loadProjectPlugins.ts:371)扫项目 package.json 的直接 dependencies + devDependencies,对每个依赖读其 package.json#ttsc.plugin 标记(readPackagePluginConfig)。这让安装了带 ttsc.plugin 的包就自动启用,无需手写 tsconfig 条目。已在 tsconfig 显式配置的 transform 会去重(hasConfiguredTransform,按 raw 说明符与解析后路径双重判定)。
加载描述符:require 优先,ttsx 兜底
loadPluginEntry(loadProjectPlugins.ts:571)解析 transform 说明符到一个 request 路径,然后 requirePluginEntry(loadProjectPlugins.ts:639):
为什么需要 ttsx 兜底(loadProjectPlugins.ts:624 注释):一个 .ts 源描述符——尤其是把运行时和描述符一起 re-export 的包根——会在 Node loader 的第一个无扩展名 import 或未剥离类型处失败,且其 import 会扇出成一整张源码包的传递图。与其重新实现那张图的构建,不如用 ttsx 跑它(ttsx 已能按需构建每个 .ts 依赖)。这次运行全图强制禁插件(入口用 --no-plugins,每个依赖用 TTSC_PLUGIN_DESCRIPTOR_LOAD),让描述符自己——可能自托管——的 transform 不运行、不死锁。
loadDescriptorViaTtsx(loadProjectPlugins.ts:666)写一个临时 shim(@ts-nocheck,import 入口、调工厂、把描述符写成 JSON)和最小 tsconfig,spawn ttsx --no-plugins shim.mts,读回 JSON。
export 条件:ttsc 条件
resolvePluginRequest(loadProjectPlugins.ts:1003)对裸包说明符先试 resolvePluginExportCondition(loadProjectPlugins.ts:1055)。一个 . 入口是运行时 barrel 的包(如 typia,其 index re-export 整个 validator 运行时)不能直接当描述符入口——加载它会拖进运行时,对自托管 transform 还会成环。这类包通过 exports 加一个 ttsc 条件指向无运行时的描述符来 opt-in:
该条件只在插件入口解析时被尊重(PLUGIN_EXPORT_CONDITIONS = ["ttsc", "node", "require", "default"]),绝不能用进程级 --conditions=ttsc(那会把包的正常 import 也重定向到描述符、破坏其运行时)。selectExportTarget / containsCondition / resolveConditionalTarget 实现了一个小型 Node exports 解析器,处理子路径映射、条件对象、数组回退、显式 null 阻断。
组合与种类判定
加载后的逐条处理(loadProjectPlugins.ts:105):
resolvePluginStage:缺省 transform,拒绝移除的output阶段。validatePluginSource/validatePluginContributors:见 描述符协议。resolveNativeSourceKind(loadProjectPlugins.ts:833):找go.mod(向上 3 层)、读包名,main→ executable,否则 linked。linked 但非 transform 阶段报错。
composePluginSources(loadProjectPlugins.ts:221)处理水平组合,检测环、拒绝多聚合者、拒绝被组合者自带 contributors、继承聚合者 capabilities(细节见描述符页)。
构建编排:共享宿主选择
加载结果里区分三类:
- 链接 transform 插件被收集成
linkedContributors(名字形如linked_000000,loadProjectPlugins.ts:121)。 - executable transform 宿主各自构建,并把链接 contributors 合并进去(
mergeContributors)。 - 若没有 executable transform 宿主但有链接源,构建
fallbackDriverHost——用cmd/utility-host作通用驱动宿主(loadProjectPlugins.ts:166)。 selectedTransformHost是被链接插件共享的那个二进制。
最后 orderNativePlugins(loadProjectPlugins.ts:562)把 check 排在 transform 前返回。
版本注入与缓存键输入
readTtscVersion(读 ttsc 自己 package.json)与 readTsgoVersion(从项目 node_modules 解析 typescript/package.json)的结果传给 buildSourcePlugin,进缓存键(见 go build 缓存)。
不变量与失败模式
维护者提示
hasProjectPluginEntries(loadProjectPlugins.ts:330)是个便宜的 "有没有启用的插件" 探针,调用方用它跳过插件相关工作而不付完整loadProjectPlugins的代价。withPluginLoaderEnv(loadProjectPlugins.ts:743)临时设TTSC_NODE_BINARY/TTSC_TTSX_BINARY,运行后恢复——改它时注意 try/finally 的恢复逻辑。- 改发现规则时,相对 vs 包说明符的
baseDir差异是最易出错处;isRelativePluginSpecifier是判定中心。