统一 flag 解析器

packages/ttsc/src/flags/schema.ts(~520 LOC)是 ttsc/ttsx 命令行表面的唯一真相源:一个 flag 一条声明,被每个需要知道这个 flag 的层消费。本页讲 schema 驱动的设计、它生成什么、以及 parser.ts 引擎如何工作。

为什么需要单一 schema

ttsc 的 flag 要穿过多个层:JS launcher → runBuild → tsgo / 原生旁车(host、lint)。如果每层各自硬编码 flag 列表,一个新 flag 要在五个地方改、还容易漏。schema 把它收成一条声明,其余层从生成产物读。

schema.ts:1 注释列出消费者:

消费者用途
src/flags/parser.ts运行时 TS 解析引擎(runTtsc.ts/runTtsx.ts 用)
scripts/gen-flags.mtscodegen,emit Go allow-list 与文档表
cmd/ttsc/flags_gen.go生成的 Go allow-list(cmd/ttsc/*.goutility/host.go 共享)
linthost/flags_gen.go生成的 lint 子命令 allow-list
website/.../ttsc/flags.mdx生成的参考表

生成器从 pnpm format 跑(pnpm gen:flags),committed 产物就是 Go 侧读的 spec——与 gen_shims:hand-maintained 同样的模式。手改生成文件被 format 检查拒绝。

flag 声明模型

每个 flag 声明(FLAG_SCHEMA)带几个维度:

  • kindschema.ts:70):boolean--flag/--flag=true/false,不吃 value token)、value--flag value--flag=value,必需值)、valueOptional(未来用)。
  • layer/consumerschema.ts:53):flag 被哪些层消费——launcherrunBuildtsgohostlint。一个 flag 必须声明至少一个 consumer。
  • forwardTo:当消费层不吸收 flag 时它去哪(如 ttsc 拥有的 flag 被 JS launcher 消费再重新 emit 成不同的 tsgo flag)。
  • terminal:print-and-exit 类 flag(--showConfig--listFilesOnly--help),ttsc 不能给它们加 build-only 守卫。
  • internalShadow:ttsc 内部会加给 tsgo 的 flag(--listEmittedFiles--noEmit--pretty)。用户也转发同名时,post-processing 要保留用户可见效果而非当 ttsc-internal 噪音剥掉。

TERMINAL_FLAGSINTERNAL_SHADOW_FLAGS(从 FLAG_SCHEMA[*].terminal/internalShadow 派生的集合)被 runBuild.ts 用来做 schema 驱动的判定(forwardsTerminalTsgoFlagforwardsInternalShadowFlag),而非手维护的 if 列表。

subcommand 模型

TtscSubcommandschema.ts:26):ttsc/build/check/fix/format/prepare/clean。裸 "ttsc" 覆盖默认 lane(无显式子命令,如 ttsc -p tsconfig.json)。TtsxSubcommand 只有 "ttsx",但 schema 镜像 ttsc 的形状让未来子命令无需第二引擎就能插进来(schema.ts:35 注释)。

每个 flag 声明它在哪些子命令可用,引擎据此决定某子命令下某 flag 是否被识别。

parser.ts 引擎

parseFlagsparser.ts)是运行时解析引擎,被 runTtsc.ts/runTtsx.ts 调。它接受 argverrorPrefixsubcommand,以及行为开关:

  • forwardAfterFirstPositional:首个 positional(如 ttsx 的入口文件)之后的 token 进 tail(用户程序 argv),不进 passthrough。
  • honorDoubleDashSeparator-- 后的 token 进 tail。

返回 { positional, passthrough, tail, values }。访问器 getString/getNumber/getBooleanparser.ts)从 values 取规范化后的 flag 值。

关键设计:未识别的 flag 进 passthrough 而非报错。schema 不认识 --strict,于是它进 passthrough,最终被 createNativeTsgoArgs 包进 --tsgo-args 转发给 tsgo(见 transform 派发)。这让 ttsc 不必枚举 tsgo 的全部 flag。

几个 schema 驱动判定的实例

runBuild.ts 里多处用 schema 派生集合替代手列表:

  • forwardsTerminalTsgoFlagrunBuild.ts:321):用户转发了 print-and-exit flag 时,ttsc 不加 compile-only 守卫(命令不会编译)。
  • forwardsInternalShadowFlagrunBuild.ts:339):用户也转发了 --listEmittedFiles 时,保留 tsgo 的 TSFILE 输出(不当 ttsc-internal 噪音剥掉);--pretty 同理。
  • createTsgoDiagnosticArgsrunBuild.ts:516):结构化诊断时加 --pretty false,但用户显式转发 --pretty 时让用户赢。

这些注释都引 RC-2/RC-3/RC-4(issue #125 的 RCA)——它们是把"每个 shadow flag 一个 bespoke if"重构成"一次结构化查找"的成果。

不变量

  • 一个 flag 必须声明至少一个 consumer。
  • 生成文件(flags_gen.goflags.mdx)手改被 format 检查拒绝。
  • 未识别 flag 进 passthrough,转发给 tsgo。
  • terminal / internalShadow 行为从 schema 派生,不手维护。

维护者提示

  • 加 flag:编辑 schema.ts 一处,跑 pnpm gen:flags(或 pnpm format),生成的 Go allow-list 与文档表自动更新。别手改 flags_gen.go
  • filterHostArgscmd/ttsc/filter.go)在 Go flag.Parse 前剥掉未知转发 flag,靠生成的 allow-list;新 flag 若该到达某子命令,确保它在 schema 里声明了该 consumer。
  • check:flagsscripts/check-flags.cjs)验证生成产物与 schema 一致,CI 跑它。

接下来