使用 Scalar AsyncAPI Upgrader 将 AsyncAPI 文档一键升级至最新版本
【免费下载链接】scalarScalar is an open-source API platform: 🌐 Modern REST API Client 📖 Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalar
本文围绕 @scalar/asyncapi-upgrader 的核心能力展开:该包可将 AsyncAPI 1.x 文档依次升级为 2.6、3.0 直至最新的 3.1 版本。它面向需要长期维护 AsyncAPI 描述文件的团队,读者读完本文后将掌握该包的完整升级管线、每一步的底层结构变换规则(servers / channels / operations / security / messages $ref 重写),以及如何用一行代码完成文档迁移并验证结果。
一、为什么需要 AsyncAPI 升级器
AsyncAPI 规范自 1.x 演进到 3.1,结构变化非常大:topics变成了channels、servers从数组变成了键值映射、publish/subscribe被提升为顶层operations、server 的url被拆分为host与pathname、消息引用从 components 作用域迁移到 channel 作用域……如果只把文档顶部的asyncapi版本字段改成3.1.0,旧文档的其余结构并不符合新版规范,解析器与渲染器无法正确处理。
这正是 @scalar/asyncapi-upgrader 出现的原因。根据其 0.1.0 版本的变更记录(CHANGELOG.md),该包在初版就实现了三项关键能力:
- 将 AsyncAPI1.x 文档真正升级到 2.x(处理 servers、channels、parameters、stream、events),而不仅是改动版本字符串;
- 将 AsyncAPI2.x 文档真正升级到 3.0(处理 servers、channels、operations、security/OAuth),而不仅是改动版本字符串;
- 在3.0 → 3.1升级时将 operation 的
messages$ref 从#/components/messages/X重写为 3.1 明确要求的 channel 作用域形式#/channels/{id}/messages/X。
后续版本(0.1.3–0.1.9)仅涉及 README 元数据修正与 npm trusted publishing 重新发布,无功能性变化,因此 0.1.0 确立的升级能力就是该包的功能全貌。
二、整体升级管线:一次调用,三段迁移
包的入口暴露了唯一的upgrade函数(入口文件),实际实现在 src/upgrade.ts:
import type { UnknownObject } from '@scalar/types/utils' import { upgradeFromOneToTwo } from './1.2-to-2.6' import { upgradeFromTwoToThree } from './2.6-to-3.0' import { upgradeFromThreeToThreeOne } from './3.0-to-3.1' /** * Upgrade an AsyncAPI document to the latest version. * 每一步将文档迁移过一个主版本,应用该版本要求的结构变换 * (例如 2.x → 3.0 会把 channel 的 publish/subscribe 提升为顶层 operations 映射, * 并把 server 的 url 拆分为 host/pathname),与 @scalar/openapi-upgrader 的思路一致。 */ export function upgrade(value: UnknownObject): UnknownObject { // AsyncAPI 1.x -> 2.6 const asyncapi26 = upgradeFromOneToTwo(value) // AsyncAPI 2.x -> 3.0 const asyncapi30 = upgradeFromTwoToThree(asyncapi26) // AsyncAPI 3.0 -> 3.1 return upgradeFromThreeToThreeOne(asyncapi30) }三段管线以流水线方式串联:1.x → 2.6.0、2.6.0 → 3.0.0、3.0.0 → 3.1.0。每段升级器都会先检查输入文档的asyncapi版本前缀,不匹配的输入会被原样返回(不做任何修改),因此:
- 传入 1.x 文档,会完整走完三段,最终输出 3.1.0;
- 传入 2.x 文档,第一段直接放行,从 2→3 开始执行;
- 传入 3.0 文档,只执行最后一段;
- 传入 OpenAPI 文档或没有
asyncapi字段的对象,整体保持不变。
这一行为在 src/upgrade.test.ts 中有完整测试覆盖:1.2.0、2.6.0、3.0.0 三种输入最终都升级到3.1.0,而{ openapi: '3.1.0' }会被原样返回,null也不会报错。
import { describe, expect, it } from 'vitest' import { upgrade } from './upgrade' describe('upgrade', () => { it('upgrades an AsyncAPI 1.x document to 3.1.0', () => { const document = upgrade({ asyncapi: '1.2.0', info: {} }) expect(document.asyncapi).toBe('3.1.0') }) it('leaves documents without an asyncapi field untouched', () => { const document = { openapi: '3.1.0' } expect(upgrade(document)).toBe(document) }) })三、第一段:1.x → 2.6 的结构变换
实现在 src/1.2-to-2.6/upgrade-from-one-to-two.ts,入口为upgradeFromOneToTwo。它会将asyncapi字段置为2.6.0,并依次执行四个内部变换。
3.1 servers:数组 → 键值映射,scheme → protocol
AsyncAPI 1.x 的servers是数组,且每个 server 用scheme/schemeVersion描述协议;2.x 要求servers是「键 → Server Object」的映射,协议字段改名为protocol/protocolVersion。
变换逻辑(upgrade-from-one-to-two.ts)逐项处理每个 server:把scheme改名为protocol、schemeVersion改名为protocolVersion,其余字段原样保留;然后用 server 的description做 slugify 生成映射键,没有 description 时回退为server-{index},键冲突时自动追加-2、-3后缀去重。
对应的测试用例(upgrade-from-one-to-two.test.ts)验证了三种典型输入:
// 输入 { asyncapi: '1.2.0', servers: [ { url: 'api.example.com', scheme: 'mqtt', description: 'Production' }, { url: 'staging.example.com', scheme: 'mqtt', description: 'Staging' }, ], } // 输出 { servers: { production: { url: 'api.example.com', protocol: 'mqtt', description: 'Production' }, staging: { url: 'staging.example.com', protocol: 'mqtt', description: 'Staging' }, }, }3.2 topics → channels,publish/subscribe 包一层 message
1.x 用topics表示主题,2.x 改为channels。变换(upgrade-from-one-to-two.ts)做了三件事:
- 将
topics重命名为channels; - 如果存在
baseTopic,把它作为前缀拼接到每个 channel 名上(如baseTopic: 'smartylighting.streetlights.1.0'+ topicevent.lighting.measured→ channelsmartylighting.streetlights.1.0.event.lighting.measured),随后删除baseTopic; - 1.x 的
publish/subscribe直接引用 message({ $ref: ... }或{ oneOf: [...] }),2.x 要求包在{ message: ... }里,因此变换会把publish: { $ref: '...' }重写为publish: { message: { $ref: '...' } }。
测试用例(upgrade-from-one-to-two.test.ts)逐一验证了这些规则,包括oneOf的包裹:
// 输入 topics: { 'user.signup': { publish: { $ref: '#/components/messages/userSignedUp' }, subscribe: { oneOf: [{ $ref: '#/components/messages/userConfirmation' }] }, }, } // 输出 channels: { 'user.signup': { publish: { message: { $ref: '#/components/messages/userSignedUp' } }, subscribe: { message: { oneOf: [{ $ref: '#/components/messages/userConfirmation' }] } }, }, }3.3 parameters:数组 → 映射
1.x 的 channel 参数是[{ name, ... }]数组,2.x 要求{ name: { ... } }映射。变换(upgrade-from-one-to-two.ts)以name为键重组,$ref类型的参数则直接取引用路径最后一段作为键。
3.4 stream / events → 根 channel/
1.x 文档根部的stream与events对象会被合并进channels['/'](upgrade-from-one-to-two.ts):
stream.read(应用读取的消息)→channels['/'].subscribe.message.oneOf;stream.write(应用写入的消息)→channels['/'].publish.message.oneOf;events.receive→subscribe,events.send→publish,同样用oneOf包裹;- 原
stream/events对象被删除。stream中的framing信息在 2.x 没有对应字段,会被丢弃。
由于 AsyncAPI 1.x 规定topics、stream、events在文档根层互斥,合法的输入不会在/channel 上产生冲突(源码注释对此有专门说明)。
四、第二段:2.x → 3.0 的结构变换
实现在 src/2.6-to-3.0/upgrade-from-two-to-three.ts,入口为upgradeFromTwoToThree。这是三段中变换最重的一段:servers、OAuth scopes、channels 与 operations 全部要重写,且 OAuth 变换必须先于 security 需求变换执行。
4.1 servers:url → host / pathname
AsyncAPI 3.0 中 server 不再用url字段,而是用host承载主机名(可含端口),可选pathname承载路径;协议信息只保留在protocol字段中。
变换(upgrade-from-two-to-three.ts)先剥掉 URL 的 scheme(如mqtt://),再把剩余部分按第一个/拆成host与pathname:
mqtt://broker.example.com:1883/mqtt → host: 'broker.example.com:1883', pathname: '/mqtt'代码注释明确解释:协议已经在protocol字段中独占承载,若再保留在host上会造成信息重复、违反规范。
4.2 OAuth:scopes → availableScopes
3.0 将 OAuth 流中的scopes字段改名为availableScopes。变换(upgrade-from-two-to-three.ts)遍历components.securitySchemes中所有type: 'oauth2'方案的每个 flow,把scopes重命名为availableScopes。这段必须最先执行,因为后面的 security 需求变换(4.4)要读取重命名后的 scheme。
4.3 channels + operations:publish/subscribe 提升为顶层 operations
这是 2.x → 3.0 最核心的变化(upgrade-from-two-to-three.ts):
- 每个 channel 的键由 channel 路径 slugify 而来(
user/{id}/signedup→user-id-signedup,见 slugifyChannelPath),冲突时追加-2、-3去重; - channel 对象新增
address字段保存原始路径,messages字段收纳该 channel 的全部消息(键取消息的 $ref 末段或name,匿名消息回退为message-0、message-1……); - 原来的
publish/subscribe被从 channel 中剥离,提升为顶层operations映射。注意语义翻转:2.x 的publish(应用接收消息)映射为 3.0 的action: 'receive',2.x 的subscribe(应用发送消息)映射为 3.0 的action: 'send'(源码注释对此有明确说明,upgrade-from-two-to-three.ts); - operation 的键优先复用 2.x 的
operationId,缺失时生成为{action}-{channelId}(如receive-user-id-signedup); - operation 通过
channel: { $ref: '#/channels/{channelId}' }回指 channel,其messages数组则重写为 channel 作用域引用#/channels/{channelId}/messages/{id}(见 buildOperation); - channel 级
servers数组会重写为[{ $ref: '#/servers/{name}' }]引用形式。
2.x 中一条典型的 channel:
channels: user/signedup: publish: operationId: onUserSignedUp message: $ref: '#/components/messages/userSignedUp'升级后变为:
channels: user-signedup: address: user/signedup messages: userSignedUp: $ref: '#/components/messages/userSignedUp' operations: onUserSignedUp: action: receive channel: $ref: '#/channels/user-signedup' messages: - $ref: '#/channels/user-signedup/messages/userSignedUp'4.4 security:普通方案引用、OAuth 方案内联 scopes
2.x 的 security 需求是「方案名 → scopes 数组」的映射;3.0 要求要么是$ref引用,要么是内联的 OAuth 方案加上scopes数组。变换(upgrade-from-two-to-three.ts)对每个需求取唯一的键名:
- 如果对应方案是
oauth2且有 scopes,输出{ ...scheme, scopes }(内联展开); - 其余方案一律输出
{ $ref: '#/components/securitySchemes/{name}' }。
channel 与 operation 上的security字段都会被套用这一规则。
五、第三段:3.0 → 3.1 的 messages $ref 规范化
3.1 属于澄清型版本,唯一的实质规则收紧是:operation 的messages$ref必须使用 channel 作用域形式#/channels/{id}/messages/{name}。但 3.0 规范自己的示例用的是 components 作用域形式#/components/messages/{name},导致大量真实 3.0 文档都写着 components 形式。
实现在 src/3.0-to-3.1/upgrade-from-three-to-three-one.ts:将版本置为3.1.0后,调用rewriteOperationMessageRefs对所有 operation(含reply,reply 未声明 channel 时沿用父 operation 的 channel)执行 $ref 重写:
- 匹配
#/components/messages/{name}形式的引用,重写为#/channels/{channelId}/messages/{name}; - 如果该 channel 的
messages映射中还没有这条消息,会自动注册一条指向原 components 消息的$ref(registerChannelMessage); - 重写后对
messages列表做去重,重复的$ref条目只保留第一处(dedupeRefs)。
这段逻辑直接回应了 CHANGELOG 中 #9358 的描述:将 operationmessages的 $ref 从#/components/messages/X重写为#/channels/{id}/messages/X。
六、安装、使用与验证
安装
包以 ESM 形式发布,要求 Node.js >= 22(package.json):
npm install @scalar/asyncapi-upgrader # 或 pnpm add @scalar/asyncapi-upgrader基本使用
import { upgrade } from '@scalar/asyncapi-upgrader' import fs from 'node:fs' const source = JSON.parse(fs.readFileSync('./asyncapi.json', 'utf-8')) const upgraded = upgrade(source) fs.writeFileSync('./asyncapi-3.1.json', JSON.stringify(upgraded, null, 2)) console.log('升级完成,新版本:', upgraded.asyncapi)整个升级过程原地修改传入对象并返回它,函数签名是upgrade(value: UnknownObject): UnknownObject。如果你的文档还没有解析成对象,需要先用 AsyncAPI 解析器(如@asyncapi/parser)把 YAML 转成 JSON 对象再传入。
直接使用单一升级步骤
除统一入口外,包还通过子路径导出三个独立升级器(package.json),适合只做单段迁移的场景:
import { upgradeFromOneToTwo } from '@scalar/asyncapi-upgrader/1.2-to-2.6' import { upgradeFromTwoToThree } from '@scalar/asyncapi-upgrader/2.6-to-3.0' import { upgradeFromThreeToThreeOne } from '@scalar/asyncapi-upgrader/3.0-to-3.1'运行测试
仓库内为每个升级步骤都配备了 vitest 测试(每一条变换规则对应一个测试用例,源码注释明确说明测试是规则的「source of truth」):
pnpm --filter @scalar/asyncapi-upgrader test三个测试文件分别覆盖:
- 1.2-to-2.6 测试:servers 数组转映射、scheme/protocol 改名、topics 转 channels、baseTopic 前缀拼接、publish/subscribe 包裹 message、parameters 数组转映射、stream/events 合并;
- 2.6-to-3.0 测试:url 拆分 host/pathname、OAuth scopes 改名、channels/operations 提升、security 引用转换;
- 3.0-to-3.1 测试:messages $ref 从 components 作用域重写为 channel 作用域。
七、注意事项与边界
- 输入必须是合法对象:
upgrade接受任意对象,非 1.x/2.x/3.0 的输入会原样返回;null也能安全处理(见 upgrade.test.ts)。 - 原地修改:升级器直接修改传入对象,如需保留原始文档请先深拷贝。
- 信息丢失是规范演进的结果:1.x 的
stream.framing在 2.x 中没有对应字段会被丢弃;2.x 的url中的协议部分在 3.0 中不再保留在 host 上。这些不是升级器的缺陷,而是 AsyncAPI 规范本身的取舍。 - 版本边界:
upgrade只保证把文档升级到当前最新 3.1.0;如果你的工具链仍依赖 2.x 或 3.0,可以分别使用对应的子路径导出,避免一次性跨版本带来的兼容性冲击。 - 与 OpenAPI 升级器的关系:从源码注释看,src/upgrade.ts 明确说明该实现「镜像」了
@scalar/openapi-upgrader的分步升级思路。两者同为 Scalar 生态中负责规范文档版本迁移的工具,本包专攻事件驱动的 AsyncAPI 文档。
八、小结
@scalar/asyncapi-upgrader 用三段式管线解决了 AsyncAPI 跨主版本升级的完整问题:1.x → 2.6 完成 servers/channels/parameters/stream/events 的基础重写,2.6 → 3.0 完成 channels/operations/security/OAuth 的架构级提升,3.0 → 3.1 完成 messages $ref 的规范化。每个版本号的递增都伴随着真实的结构变换而非字符串替换,这一点由仓库内三组测试文件逐条验证。对于任何维护 AsyncAPI 文档超过一个主版本的团队,这个包都能把「升级 AsyncAPI 文档到最新版本」这件事压缩成一行调用。
【免费下载链接】scalarScalar is an open-source API platform: 🌐 Modern REST API Client 📖 Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support项目地址: https://gitcode.com/GitHub_Trending/sc/scalar
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考