news 2026/9/13 19:22:46

Effect v4 CLI 全局设置标志直接可 yield:解读 `GlobalFlag.setting` 与内置标志重命名

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Effect v4 CLI 全局设置标志直接可 yield:解读 `GlobalFlag.setting` 与内置标志重命名

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,同时将内置全局标志CompletionsFlagLogLevelFlag重命名为更简洁的CompletionsLogLevel。本文以变更集 .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.optionalFlag.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"):

  1. 值通道--log-level解析出的Option<LogLevel>通过Effect.provideService(program, GlobalFlag.LogLevel, logLevel)注入,处理器可以yield* GlobalFlag.LogLevel直接读取该值;
  2. 行为通道:同一个值又被包装成References.MinimumLogLevel上下文(Context.make(References.MinimumLogLevel, level)),通过Effect.provideContext生效,从而真正改变运行时最小日志级别。

换句话说,--log-level依旧在底层配置References.MinimumLogLevel,但与此同时它也成为可读的上下文值。自定义设置标志则只走第一条通道(provideService),由使用者自行消费。

四、内置全局标志重命名:CompletionsFlagCompletionsLogLevelFlagLogLevel

变更集同时简化了内置标志的命名:

旧名称新名称对应命令行标志类型
GlobalFlag.CompletionsFlagGlobalFlag.Completions--completions <bash\|zsh\|fish\|sh>Action<Option<"bash" \| "zsh" \| "fish">>
GlobalFlag.LogLevelFlagGlobalFlag.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) ) }) })

细节:接受的取值有bashzshfishsh,其中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>") ) })

注意warningwarn的同义词,二者都映射到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):可通过CliConfigbuiltIns过滤掉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 早期版本,可按以下对应关系迁移:

  1. 构造器形态GlobalFlag.setting({ flag, defaultValue })GlobalFlag.setting("id")({ flag })defaultValue改为在flag上使用Flag.withDefault(value)Flag.optional
  2. 读取方式:无需再自行维护"解析结果缓存",直接在Effect.genyield*设置引用即可拿到值;
  3. 标志命名GlobalFlag.CompletionsFlagGlobalFlag.CompletionsGlobalFlag.LogLevelFlagGlobalFlag.LogLevel
  4. 注入方式:设置值由Command.runWith自动通过provideService注入,你也可以在Command.provide*相关的 effect 编排中直接引用这些Context.Reference

七、小结

few-mirrors-pull.md这项 patch 把 Effect v4 unstable CLI 的全局设置标志提升为一等公民:GlobalFlag.setting返回的Context.Referenceyield*直接取值成为可能,--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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 19:22:23

快速上手 RemoveWindowsAI:备份与还原安全网完整指南

快速上手 RemoveWindowsAI&#xff1a;备份与还原安全网完整指南 【免费下载链接】RemoveWindowsAI Force Remove Copilot, Recall and More in Windows 11 项目地址: https://gitcode.com/GitHub_Trending/re/RemoveWindowsAI 删完之后想反悔怎么办&#xff1f;RemoveW…

作者头像 李华
网站建设 2026/9/13 19:22:22

brpc bvar 完全指南:多线程计数器库的原理、使用与监控导出

brpc bvar 完全指南&#xff1a;多线程计数器库的原理、使用与监控导出 【免费下载链接】brpc brpc is an Industrial-grade RPC framework using C Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Rec…

作者头像 李华
网站建设 2026/9/13 19:21:16

完整的方案与示例代码,按照“命令模式 + 执行器 + 可插拔解析器”抽象通信功能,满足高效、可维护、可扩展的需求。设计要点先列出,然后给出必要的代码文件(可直接复制到项目)

完整的方案与示例代码,按照“命令模式 + 执行器 + 可插拔解析器”抽象通信功能,满足高效、可维护、可扩展的需求。设计要点先列出,然后给出必要的代码文件(可直接复制到项目)。 总体方案(简明) • 抽象命令(ICommand):每个与下位机的动作为一个命令对象,包含构建请求…

作者头像 李华