
Effect 不稳定版 CLI 命令注解用Command.annotate与Command.annotateMerge向HelpDoc注入自定义元数据【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effecteffect的 unstable CLI 模块effect/unstable/cli在 4.0.0 中新增了Command.annotate与Command.annotateMerge两个组合子允许为命令挂载基于Context的自定义注解并将这些注解透传到HelpDoc供自定义帮助格式化器读取。本文结合 changeset 说明 与仓库源码、测试用例讲解注解的 API 语义、内部数据流以及如何编写自定义帮助格式化器消费这些元数据帮助你在构建 CLI 应用时为命令附加团队归属、废弃状态、文档链接等机器可读信息。一、这次 changeset 引入了什么mean-dingos-share.md 是本次预发布pre-release变更记录内容如下AddCommand.annotateandCommand.annotateMergeto unstable CLI commands, and include command annotations inHelpDocso custom help formatters can access command metadata.翻译过来包含两个能力点在 unstable CLI 命令上新增Command.annotate与Command.annotateMerge两个 API用于为命令附加注解命令注解会被包含进生成的HelpDoc从而让自定义帮助格式化器custom help formatters能够访问命令元数据。从源码看这两个 API 定义在 packages/effect/src/unstable/cli/Command.ts标注为since 4.0.0并且位于effect/unstable/cli模块——即 API 仍在演进中使用前需注意其 unstable 属性。二、命令注解的本质基于Context的元数据Command 结构中的 annotations 字段Command接口本身就声明了注解字段Command.ts#L163-L166/** * Custom annotations associated with this command. */ readonly annotations: Context.Contextnever注解的类型是Context.Contextnever——也就是说注解不是简单的字符串键值对而是 Effect 的依赖注入Context可以用任意Context.Key作为键、以任意类型作为值。这与 Effect 生态中服务注册、Cause 注解Cause.annotate等设计一脉相承类型安全且可组合。Command.Any类型中也保留了同样的字段Command.ts#L355保证无论命令的具体泛型参数如何注解能力始终可用。定义注解键注解键使用Context.Service或Context.Tag/Context.Reference创建。仓库测试 Command.test.ts#L53-L54 展示了标准用法const Team Context.Servicenever, string(effect/test/unstable/cli/Team) const Priority Context.Servicenever, number(effect/test/unstable/cli/Priority)这里Team的键关联一个字符串值如团队名Priority关联一个数字值如优先级。自定义帮助格式化器正是通过这些键从HelpDoc的注解上下文中取值。三、Command.annotate为命令挂载单个注解Command.annotate用于给命令附加一个Context.Key对应的值签名如下Command.ts#L1273-L1292export const annotate: { I, S( service: Context.KeyI, S, value: NoInferS ): Name extends string, Input, E, R, ContextInput( self: CommandName, Input, ContextInput, E, R ) CommandName, Input, ContextInput, E, R Name extends string, Input, E, R, ContextInput, I, S( self: CommandName, Input, ContextInput, E, R, service: Context.KeyI, S, value: NoInferS ): CommandName, Input, ContextInput, E, R }它是用dual实现的因此既支持 pipe 风格也支持直接传参风格。底层实现非常直接Command.ts#L1285-L1292const impl toImpl(self) return makeCommand({ ...impl, annotations: Context.add(impl.annotations, service, value) })即取出命令内部实现在其注解Context上执行Context.add再重建命令。使用示例import { Context } from effect import { Command } from effect/unstable/cli const Team Context.Servicenever, string(app/cli/Team) const deploy Command.make(deploy).pipe( Command.annotate(Team, runtime) ) // 读取命令注解 const team Context.get(deploy.annotations, Team) // runtime需要注意的陷阱源码文档明确标注了一个 GotchaCommand.ts#L1266Adding the sameContext.Keyagain replaces the earlier value.即对同一个Context.Key重复调用annotate新值会替换旧值而不是叠加。这源于Context.add的语义——一个键在Context中只能存在一个值。四、Command.annotateMerge合并已有的注解 ContextannotateMerge用于将一整个已构建好的Context.ContextI合并进命令注解Command.ts#L1317-L1333export const annotateMerge: { I( annotations: Context.ContextI ): Name extends string, Input, E, R, ContextInput( self: CommandName, Input, ContextInput, E, R ) CommandName, Input, ContextInput, E, R Name extends string, Input, E, R, ContextInput, I( self: CommandName, Input, ContextInput, E, R, annotations: Context.ContextI ): CommandName, Input, ContextInput, E, R }底层实现使用Context.mergeCommand.ts#L1327-L1333const impl toImpl(self) return makeCommand({ ...impl, annotations: Context.merge(impl.annotations, annotations) })使用示例import { Context } from effect import { Command } from effect/unstable/cli const Team Context.Servicenever, string(app/cli/Team) const Priority Context.Servicenever, number(app/cli/Priority) const deploy Command.make(deploy).pipe( Command.annotate(Team, runtime), Command.annotateMerge(Context.make(Priority, 2)) )合并优先级源码文档同样记录了一个 GotchaCommand.ts#L1308-L1310If both contexts contain the sameContext.Key, the incoming annotations context wins.当命令已有注解与传入的Context中存在相同键时传入的新Context胜出。这与annotate的后写覆盖语义保持一致方便用一组默认注解构造Context再按需覆盖单个键。五、注解如何流入HelpDoc内部数据流理解整条链路需要看三层源码1. 命令内部实现持有注解在 internal/command.ts#L110-L229 的makeCommand中注解被持久化到命令内部const annotations options.annotations ?? Context.empty()所有组合子包括withSubcommands、withSharedFlags等在重建命令时都会通过...impl保留annotations字段例如 Command.ts#L933因此注解不会在管道操作中丢失。2.buildHelpDoc将注解写入 HelpDocmakeCommand内部定义了buildHelpDocinternal/command.ts#L130-L202它负责把命令的描述、用法、参数、标志、子命令、示例以及注解组装成结构化HelpDoc。关键的返回对象internal/command.ts#L193-L201return { description: options.description ?? , usage, flags, annotations, ...(args.length 0 { args }), ...(subcommandDocs.length 0 { subcommands: subcommandDocs }), ...(examples.length 0 { examples }) }注意annotations直接原样透传。此外buildHelpDoc还承担了隐藏标志hidden与不列出子命令unlisted的过滤逻辑internal/command.ts#L165-L189这些与注解是正交的功能。3.HelpDoc类型声明注解字段HelpDoc接口在 HelpDoc.ts#L87-L90 声明/** * Custom command annotations. */ readonly annotations: Context.Contextnever这意味着任何拿到HelpDoc的消费者默认帮助输出、自定义格式化器、文档生成工具都能读取注解。4. 帮助文档生成入口当用户执行--help时getHelpForCommandPath会沿命令路径找到当前命令调用其buildHelpDoc生成基础文档再叠加共享标志与全局标志internal/help.ts#L152-L183const baseDoc toImpl(currentCommand).buildHelpDoc(commandPath) // ... return { ...baseDoc, flags: [...sharedFlags, ...baseDoc.flags], globalFlags: globalFlagDocs }由于使用...baseDoc展开annotations字段天然保留在最终文档中。六、消费注解编写自定义 HelpDoc 格式化器注解存在的意义是让自定义 help formatters 可以访问命令元数据。默认格式化器defaultFormatter的formatHelpDoc实现CliOutput.ts#L474-L630会输出DESCRIPTION、USAGE、ARGUMENTS、FLAGS、GLOBAL FLAGS、SUBCOMMANDS、EXAMPLES等区块但不会渲染annotations——注解是给程序化消费方而非终端用户准备的元数据。Formatter 服务CliOutput.Formatter是一个Context.ReferenceCliOutput.ts#L227-L229默认实现即defaultFormatter()。接口定义CliOutput.ts#L52-L93包含formatError、formatVersion、formatHelpDoc等方法。有两种方式注入自定义格式化器通过CliOutput.layer(formatter)提供 LayerCliOutput.ts#L259const layer (formatter: Formatter): Layer.Layernever Layer.succeed(Formatter)(formatter)通过Effect.provideService(CliOutput.Formatter, formatter)直接提供服务——仓库测试采用的就是这种方式。完整示例在帮助头部渲染团队归属import { Context, Effect, Option } from effect import { CliOutput, Command } from effect/unstable/cli import { NodeRuntime, NodeServices } from effect/platform-node const Team Context.Servicenever, string(app/cli/Team) const formatter: CliOutput.Formatter { ...CliOutput.defaultFormatter({ colors: false }), formatHelpDoc: (doc) { const team Context.getOption(doc.annotations, Team) const header Option.match(team, { onNone: () Team: unassigned\n, onSome: (t) Team: ${t}\n }) // 拼接默认帮助输出 return header CliOutput.defaultFormatter({ colors: false }).formatHelpDoc(doc) } } const deploy Command.make(deploy).pipe( Command.annotate(Team, runtime), Command.withDescription(Deploy the service to a target environment) ) const app Command.make(app).pipe( Command.withSubcommands([deploy]), Command.run({ version: 1.0.0 }), Effect.provideService(CliOutput.Formatter, formatter), Effect.provide(NodeServices.layer), NodeRuntime.runMain )这里使用了Context.getOptionContext.ts#L1093 附近定义——它返回Option缺失时是Option.none而不是抛错非常适合格式化器这种注解可有可无的消费场景。如果不希望使用getOption也可先用Context.has判断再Context.get。当用户运行app deploy --help时输出会以Team: runtime开头随后是默认的DESCRIPTION/USAGE等区块。七、源码级验证测试用例仓库测试 packages/effect/test/unstable/cli/Command.test.ts#L49-L115 为注解功能提供了两组针对性用例直接印证了 changeset 描述的行为。用例 1注解应暴露在帮助文档中it.effect(should expose annotations in help docs, () Effect.gen(function*() { const Team Context.Servicenever, string(effect/test/unstable/cli/Team) const Priority Context.Servicenever, number(effect/test/unstable/cli/Priority) const docs: ArrayParametersCliOutput.Formatter[formatHelpDoc][0] [] const formatter: CliOutput.Formatter { ...CliOutput.defaultFormatter({ colors: false }), formatHelpDoc: (doc) { docs.push(doc) return } } const command Command.make(deploy).pipe( Command.annotate(Team, runtime), Command.annotateMerge(Context.make(Priority, 2)) ) yield* Command.runWith(command, { version: 1.0.0 })([--help]).pipe( Effect.provide(TestLayerWithoutFormatter), Effect.provideService(CliOutput.Formatter, formatter) ) assert.strictEqual(docs.length, 1) const annotations docs[0].annotations assert.strictEqual(Context.get(annotations, Team), runtime) assert.strictEqual(Context.get(annotations, Priority), 2) }))该用例验证了annotate与annotateMerge设置的注解在--help生成的HelpDoc.annotations中都能通过原Context.Key读到且类型无损。用例 2添加子命令后注解得以保留it.effect(should keep annotations when adding subcommands, () Effect.gen(function*() { const Scope Context.Servicenever, string(effect/test/unstable/cli/Scope) const docs: ArrayParametersCliOutput.Formatter[formatHelpDoc][0] [] // ... 自定义 formatter 收集 doc ... const child Command.make(child).pipe(Command.annotate(Scope, child)) const command Command.make(root).pipe( Command.annotate(Scope, root), Command.withSubcommands([child]) ) const run Command.runWith(command, { version: 1.0.0 }) yield* run([--help]).pipe(/* 提供 formatter */) yield* run([child, --help]).pipe(/* 提供 formatter */) assert.strictEqual(docs.length, 2) assert.strictEqual(Context.get(docs[0].annotations, Scope), root) assert.strictEqual(Context.get(docs[1].annotations, Scope), child) }))这个用例揭示了一个重要语义注解是命令作用域的不会被子命令继承。root --help拿到Scope rootchild --help拿到Scope child——每个命令维护自己的注解Context。如果你的格式化器需要跨层级聚合元数据需要自行沿HelpDoc的subcommands结构或命令路径遍历框架不做隐式继承。八、与现有 CLI 应用的结合实践仓库的 ai-docs/src/70_cli/10_basics.ts 给出了完整的 CLI 应用骨架用Flag.String/Flag.Boolean/Flag.Literals定义参数Command.make创建命令Command.withSharedFlags共享父级标志Command.withSubcommands组合子命令最后用Command.runNodeServices.layerNodeRuntime.runMain启动。注解能力可以无缝嵌入这一流程常见用法包括团队/模块归属标注命令由哪个团队负责便于帮助输出或 CI 检查废弃状态Command.annotate(Deprecated, true)自定义格式化器可渲染 DEPRECATED 提示或警告文档链接与元数据挂载内部文档 URL、SLI 指标名、权限等级等机器可读信息供文档生成器或自动化脚本消费诊断信息与--verbose等全局标志配合帮助输出中附带命令内部信息。这些注解与Command.withExamples、Command.withDescription等元数据组合子正交共存——buildHelpDoc会同时收集它们见 internal/command.ts#L130-L202互不干扰。九、注意事项总结unstable 模块effect/unstable/cli中的 API包括Command.annotate/annotateMerge尚不稳定可能随版本演进调整签名或语义升级依赖时请关注 CHANGELOG.md。版本前提两个 API 标注since 4.0.0需使用 effect 4.0.0 及以上版本。键冲突语义annotate重复同键会替换旧值annotateMerge遇到同键时传入的Context胜出。作用域注解按命令隔离子命令不会继承父命令注解withSubcommands等组合操作会保留各自已有的注解。消费方默认帮助输出不渲染注解注解主要面向自定义CliOutput.Formatter、文档生成器等程序化消费场景读取时推荐使用Context.getOption处理可选性。一句话总结Command.annotate/Command.annotateMerge为 Effect CLI 命令补上了结构化元数据通道配合HelpDoc.annotations与自定义Formatter你可以在保持类型安全的前提下为每一个命令注入并读取任意元数据让帮助系统从给人看升级为人机两用。【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考