Effect v4 CLI 全局设置标志直接可 yield:解读GlobalFlag.setting与内置标志重命名
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
导读
在 Effect v4(当前处于 RC 阶段,main分支即 v4 开发分支)的 unstable CLI 模块中,一次关键的 API 演进让全局设置标志(global setting flag)从"仅参与命令解析"升级为"可直接在处理器中yield*取值"的Context.Reference,同时将内置全局标志CompletionsFlag、LogLevelFlag重命名为更简洁的Completions、LogLevel。本文以变更集 .repos/effect-smol/.changeset/pre/few-mirrors-pull.md 为核心,结合 GlobalFlag.ts 源码、Command.ts 运行器实现与 Command.test.ts 测试用例,讲清这一机制的原理、迁移方式与实战用法。读完本文,你将掌握如何自定义可yield*的全局设置、如何理解runWith对内置设置(如--log-level)的双通道处理,以及新旧 API 的对应关系。
一、变更背景:全局标志从"命令级声明"到"可直接取值"
Effect v4 的 CLI 模块在packages/effect/src/unstable/cli/下,其中 GlobalFlag.ts 定义了两种全局标志:
- Action 标志(
Action<A>):执行副作用并退出,例如--help、--version、--completions; - Setting 标志(
Setting<Id, A>):把解析出的值通过 Effect 上下文提供给命令处理器,例如--log-level、--config。
变更集few-mirrors-pull.md的核心改动是:
GlobalFlag.settingnow takes{ flag, defaultValue }and returns a setting that is aContext.Reference, so handlers andCommand.provide*effects canyield*global setting values directly.
即:设置标志本身就是一个Context.Reference(服务引用),命令处理器可以直接yield* Region拿到解析后的值,而无需再通过其他间接方式读取。从 GlobalFlag.ts 的类型定义可以看到这一设计:
export interface Setting<Id extends string, A> extends Context.Service<Setting.Identifier<Id>, A> { readonly _tag: "Setting" readonly id: Id readonly flag: Flag.Flag<A> }Setting接口直接继承了Context.Service(即 Effect 的Context.Reference),因此它天然具备"可被yield*、可被provideService注入"的能力。
需要说明:变更集描述的是当时合并时的写法
{ flag, defaultValue };在当前仓库代码中,该构造器已演化为柯里化形式GlobalFlag.setting("id")({ flag }),且默认值统一由Flag组合子(Flag.optional、Flag.withDefault)提供,而非由 setting 构造器自身负责(详见 CHANGELOG.md)。本文以当前仓库源码为准展开。
二、当前 API 形态:柯里化的GlobalFlag.setting
GlobalFlag.ts 中的构造器实现:
export const setting = <const Id extends string>(id: Id) => <A>(options: { readonly flag: Flag.Flag<A> }): Setting<Id, A> => { settingIdCounter += 1 const ref = Context.Service<Setting.Identifier<Id>, A>( `effect/unstable/cli/GlobalFlag/${id}/${settingIdCounter}` ) return Object.assign(ref, { _tag: "Setting" as const, id, flag: options.flag }) }要点:
- 第一参数
id是类型层面的服务标识(type-level identifier),最终上下文键为effect/unstable/cli/GlobalFlag/${id}(见 Setting.Identifier 类型),每次调用还附加一个递增计数以保证实例唯一; - 第二参数只接收
{ flag },默认值通过Flag.withDefault(...)或Flag.optional表达; - 返回的
Setting同时是一个Context.Service,因此在Effect.gen中可以直接yield*。
2.1 使用示例:定义一个可直接 yield 的全局设置
参考 Command.test.ts 中的测试模式:
import * as Command from "effect/unstable/cli/Command" import * as Flag from "effect/unstable/cli/Flag" import * as GlobalFlag from "effect/unstable/cli/GlobalFlag" import * as Option from "effect/Option" import * as Effect from "effect/Effect" const Region = GlobalFlag.setting("region")({ flag: Flag.string("region").pipe(Flag.optional) }) const deploy = Command.make("deploy", {}, () => Effect.gen(function*() { const region: Option.Option<string> = yield* Region // 直接 yield! console.log(region) }) ).pipe(Command.withGlobalFlags([Region])) const run = Command.runWith(deploy, { version: "1.0.0" }) // 命令行传入:--region us-east-1 → Option.some("us-east-1") // 命令行不传: → Option.none()测试断言验证了两种调用下yield* Region分别得到Option.some("us-east-1")与Option.none(),证明处理器中的yield*能直接拿到解析结果。
2.2 默认值由 Flag 组合子表达
如果不希望值是Option,可以改用Flag.withDefault。测试 Command.test.ts 展示了这一用法:
const Region = GlobalFlag.setting("region")({ flag: Flag.string("region").pipe(Flag.withDefault("us-west-2")) }) // 命令行传入:--region eu-west-1 → "eu-west-1" // 命令行不传: → "us-west-2"(默认值)这正是"Setting defaults are sourced fromFlagcombinators rather than setting constructor defaults"这条行为变更的落地体现——默认值语义完全收敛到Flag层,GlobalFlag.setting只负责创建上下文引用。
三、runWith如何注入设置值:provideService 驱动
设置值最终是通过Command.runWith注入到处理器上下文中的。Command.ts 的"Provide setting values"步骤:
let program = commandImpl.handle(parseResult.success, [command.name]) const logLevel = activeFlags.includes(GlobalFlag.LogLevel) ? (yield* GlobalFlag.LogLevel.flag.parse(emptyArgs))[1] : Option.none() program = Effect.provideService(program, GlobalFlag.LogLevel, logLevel) for (const flag of activeFlags) { if (flag._tag !== "Setting" || flag === GlobalFlag.LogLevel) continue const [, value] = yield* flag.flag.parse(emptyArgs) program = Effect.provideService(program, flag, value) } // Apply built-in setting behavior const services = Option.match(logLevel, { onNone: () => Context.empty(), onSome: (level) => Context.make(References.MinimumLogLevel, level) }) yield* Effect.provideContext(program, services)这里揭示了"双通道"机制(对应变更集中 "Built-in settings keep internal behavior inrunWith… while also being readable as values"):
- 值通道:
--log-level解析出的Option<LogLevel>通过Effect.provideService(program, GlobalFlag.LogLevel, logLevel)注入,处理器可以yield* GlobalFlag.LogLevel直接读取该值; - 行为通道:同一个值又被包装成
References.MinimumLogLevel上下文(Context.make(References.MinimumLogLevel, level)),通过Effect.provideContext生效,从而真正改变运行时最小日志级别。
换句话说,--log-level依旧在底层配置References.MinimumLogLevel,但与此同时它也成为可读的上下文值。自定义设置标志则只走第一条通道(provideService),由使用者自行消费。
四、内置全局标志重命名:CompletionsFlag→Completions,LogLevelFlag→LogLevel
变更集同时简化了内置标志的命名:
| 旧名称 | 新名称 | 对应命令行标志 | 类型 |
|---|---|---|---|
GlobalFlag.CompletionsFlag | GlobalFlag.Completions | --completions <bash\|zsh\|fish\|sh> | Action<Option<"bash" \| "zsh" \| "fish">> |
GlobalFlag.LogLevelFlag | GlobalFlag.LogLevel | --log-level <all\|trace\|debug\|info\|warn\|warning\|error\|fatal\|none> | Setting<"log-level", Option<LogLevel>> |
在当前源码中,这两个内置标志分别定义于 GlobalFlag.ts 与 GlobalFlag.ts。
4.1Completions:生成 shell 补全脚本
export const Completions: Action<Option.Option<"bash" | "zsh" | "fish">> = action({ flag: Flag.choice("completions", ["bash", "zsh", "fish", "sh"] as const) .pipe( Flag.optional, Flag.map((v) => Option.map(v, (s) => s === "sh" ? "bash" : s)), Flag.withMetavar("<bash|zsh|fish|sh>"), Flag.withDescription("Print shell completion script") ), run: Effect.fnUntraced(function*(shell, { command }) { if (Option.isNone(shell)) return const descriptor = CommandDescriptor.fromCommand(command) yield* Console.log( Completions_.generate(command.name, shell.value, descriptor) ) }) })细节:接受的取值有bash、zsh、fish、sh,其中sh会被规范化为bash;脚本生成逻辑委托给 Completions.ts 模块。
4.2LogLevel:内置日志级别设置
export const LogLevel: Setting<"log-level", Option.Option<LogLevelType>> = setting("log-level")({ flag: Flag.choiceWithValue( "log-level", [ ["all", "All"], ["trace", "Trace"], ["debug", "Debug"], ["info", "Info"], ["warn", "Warn"], ["warning", "Warn"], ["error", "Error"], ["fatal", "Fatal"], ["none", "None"] ] as const ).pipe( Flag.optional, Flag.withDescription("Sets the minimum log level"), Flag.withMetavar("<all|trace|debug|info|warn|warning|error|fatal|none>") ) })注意warning是warn的同义词,二者都映射到LogLevel.Warn。该设置除了可被yield* GlobalFlag.LogLevel读取外,还会如上一节所述配置References.MinimumLogLevel。
4.3 内置标志集合与优先级
BuiltIns数组(GlobalFlag.ts)以"默认优先级顺序"列出全部内置标志:
export const BuiltIns: readonly [...] = [Help, Version, Wizard, Completions, LogLevel]由于"Action 标志按活跃顺序处理、第一个出现的 Action 即退出"(见 Command.ts 中 "first present action wins, then exit" 的逻辑),该数组顺序直接决定了--help、--version、--wizard、--completions的裁决优先级。Command.runWith会把这组内置标志前置到用户自定义全局标志之前进行收集与解析。
五、从测试看行为契约
Command.test.ts 为本次变更提供了完整的行为验证:
- 直接 yield 设置值(L460-L482):无默认值时未传参得到
Option.none(); - Flag.withDefault 生效(L484-L506):未传参得到默认值;
- 内置标志可按次运行配置(L508-L532):可通过
CliConfig的builtIns过滤掉GlobalFlag.LogLevel,此时--help不再渲染--log-level,且传入--log-level debug会被报为 "Unrecognized flag",而默认配置下--log-level正常存在——这验证了Command.runWith是按CliConfig组装内置标志的; - Action 与 Setting 混用(L534-L569):
--verbose(Action)触发副作用并退出,--format json(Setting)把值注入处理器; - 作用域与别名冲突(L2066-L2119):局部标志的短别名可覆盖全局设置(
-o),局部标志也能覆盖另一命令分支声明的全局设置。
其中"混用"测试特别展示了迁移后的直观体验:Action 标志run直接执行副作用,Setting 标志通过yield*暴露给处理器,两者声明在同一Command.withGlobalFlags([...])数组中互不干扰。
六、从旧 API 迁移到新 API
如果你的代码基于 v4 RC 早期版本,可按以下对应关系迁移:
- 构造器形态:
GlobalFlag.setting({ flag, defaultValue })→GlobalFlag.setting("id")({ flag }),defaultValue改为在flag上使用Flag.withDefault(value)或Flag.optional; - 读取方式:无需再自行维护"解析结果缓存",直接在
Effect.gen内yield*设置引用即可拿到值; - 标志命名:
GlobalFlag.CompletionsFlag→GlobalFlag.Completions;GlobalFlag.LogLevelFlag→GlobalFlag.LogLevel; - 注入方式:设置值由
Command.runWith自动通过provideService注入,你也可以在Command.provide*相关的 effect 编排中直接引用这些Context.Reference。
七、小结
few-mirrors-pull.md这项 patch 把 Effect v4 unstable CLI 的全局设置标志提升为一等公民:GlobalFlag.setting返回的Context.Reference让yield*直接取值成为可能,--log-level在内置行为(References.MinimumLogLevel)与可读值两个通道上同时工作,内置命名也随之一并简化。若要在自己的项目中继续深入研究,可以顺藤摸瓜阅读:
- GlobalFlag.ts:
Action/Setting模型、构造器与全部内置标志; - Command.ts:
runWith中全局标志收集、Action 优先退出、Setting 注入与MinimumLogLevel应用; - Command.test.ts:覆盖默认值、作用域、别名覆盖与内置标志裁剪的行为测试;
- CHANGELOG.md:4.0.0-beta.14 与 4.0.0-beta.13 的完整演进记录。
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考