news 2026/9/23 10:48:33

@changesets/apply-release-plan 源码级解析:版本号与 Changelog 的自动化应用引擎

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
@changesets/apply-release-plan 源码级解析:版本号与 Changelog 的自动化应用引擎

@changesets/apply-release-plan 源码级解析:版本号与 Changelog 的自动化应用引擎

【免费下载链接】changesets🦋 A tool to manage versioning and changelogs with a focus on monorepos项目地址: https://gitcode.com/gh_mirrors/ch/changesets

@changesets/apply-release-plan是 changesets 发布流程中负责"落地执行"的核心包:它接收一份由@changesets/get-release-plan生成的发布计划(ReleasePlan),将计划中的版本号提升、内部依赖范围更新、Changelog 生成与写入等一系列变更真实地应用到 monorepo 的各个package.jsonCHANGELOG.md文件上。本文以该包 v8.1.1 的变更历史(CHANGELOG.md)为骨架,结合 源码 与测试用例,讲透其 API、工作流程、依赖范围改写规则、格式化保留策略以及 prerelease / snapshot 等进阶场景,帮助你理解changeset version命令底层究竟做了什么,以及在二次开发或排查发布问题时应当关注哪些关键点。

一、包定位:发布计划如何变成真实文件变更

该包的 README(packages/apply-release-plan/README.md)给出了最直接的定义:

This takes areleasePlanobject for changesets and applies the expected changes from that release. This includes updating package versions, and updating changelogs.

即:输入发布计划,输出"期望中的变更结果"——包括更新包版本、更新依赖范围、更新 Changelog,以及清理已消费的 changeset 文件。它自身不校验发布计划的准确性,计划是否合理由上游@changesets/get-release-plan负责;它也不负责 Git 提交,提交动作自 v6.0.0 起已完全移交至@changesets/cli

1.1 核心 API 与参数说明

从 src/index.ts 可以拿到完整函数签名:

export async function applyReleasePlan( releasePlan: ReleasePlan, // 发布计划:含 releases、changesets、preState packages: Packages, // @manypkg/get-packages 返回的包信息 config: Config = defaultConfig, // @changesets/config 校验后的配置 snapshot?: string | boolean, // 快照发布:true 或自定义 tag contextDir = import.meta.dirname, // changelog 模块解析的备用目录 ): Promise<string[]> // 返回所有被改动文件的绝对路径

要点:

  • packages必须是@manypkg/get-packages的产物(v1.0.0 起不再接收cwd),内部通过packages.rootDir定位仓库根,通过packagesByName把 release 中的包名映射到真实包对象,找不到对应包时直接抛出Could not find matching package for release of: xxx(src/index.ts)。
  • 返回值为touchedFiles,即所有被修改文件的绝对路径列表,CLI 拿到后统一执行 Git 提交。这是"本包不负责提交"这一职责划分的直接体现。
  • v8.0.0 引入了具名导出applyReleasePlan,与默认导出等价;默认导出已标记@deprecated,计划在下一个大版本移除(见 src/index.ts),新代码应优先使用具名导出。
  • 包自 v8.0.0 起以ES Module形式发布(package.json"type": "module"exports指向./dist/index.mjs),Node 支持范围提升为^22.11 || ^24 || >=26(见 packages/apply-release-plan/package.json)。

1.2 一次调用背后的完整流程

梳理 src/index.ts 的主函数体,可以得到如下执行链:

  1. 匹配包对象:将releasePlan.releases中的每个 release 与packages.packages合并,附加dirpackageJson等字段。
  2. 预生成 Changelog 条目:调用getNewChangelogEntry为每个 release 生成新版本条目文本(详见第三节)。
  3. 处理 prerelease 退出:若releasePlan.preState?.mode === "exit"且非 snapshot,删除根目录的.changeset/pre.json
  4. 逐包更新:对每个 release,先计算依赖范围编辑(getDependencyVersionEdits),再追加version字段的新版本值,通过editJson写回package.json;若生成了 changelog 文本则写入CHANGELOG.md
  5. 更新根包依赖:若存在packages.rootPackage,同样对根package.json中的 workspace 内部依赖范围做更新(该行为由 v8.0.0 引入,见变更记录中"Update dependency ranges in the workspace root package.json"一条)。
  6. 格式化:把所有改动过的CHANGELOG.md交给 formatter(v8.0.0 起改用@changesets/formatformat配置为auto时自动探测 Prettier / Biome,Biome 因不支持 Markdown 被排除,见 src/index.ts)。
  7. 消费 changeset 文件:删除已应用且不涉及跳过包的.changeset/<id>.md;若处于 prerelease 模式,则移动至.changeset/pre/目录。
  8. 返回touchedFiles

这一流程在 src/index.test.ts 中通过FakeReleasePlanfixture 与testSetup辅助函数做了大量端到端验证,例如"单个包版本更新""两个包不同新版本""根包依赖更新但不版本化根包"等用例。

二、版本号与依赖范围:最精细的改写逻辑

版本号提升只是把version字段替换成新值,真正的复杂度集中在依赖范围的处理上,全部实现在 src/version-package.ts 的getDependencyVersionEdits与 src/utils.ts 的shouldUpdateDependencyBasedOnConfig中。

2.1 扫描哪些依赖字段

DEPENDENCY_TYPES覆盖四类字段(src/version-package.ts):

const DEPENDENCY_TYPES = [ "dependencies", "devDependencies", "peerDependencies", "optionalDependencies", ] as const;

注意:v2.0.0 起更新devDependencies不再连带提升依赖方自身版本(dev 依赖不影响最终用户),且 dev 依赖的变更不再写入 Changelog——这一行为变化是当年的 breaking change,如今已是稳定预期。

2.2 判断"是否需要更新":shouldUpdateDependencyBasedOnConfig

对每个被发布包在依赖方 manifest 中的范围,按以下优先级决策(src/utils.ts):

  1. workspace:协议优先处理
    • workspace:*直接返回true(表示总会被重写);
    • workspace:^/workspace:~先还原成^oldVersion/~oldVersion再参与判断;
    • workspace:后面跟的是相对路径引用(如workspace:../pkg),则与包目录的相对路径比对,路径一致才更新。
  2. 新版本已不在范围内(!semverSatisfies(newVersion, range))→ 必须更新,这是兜底规则,保证发布后依赖永远指向可满足的版本。
  3. 否则按updateInternalDependencies阈值"patch" | "minor")判断:依赖方只会在被依赖包的提升级别达到阈值时同步提升。该配置项由 v3.0.0 引入,用于关闭"仅 patch 提升也连带内部依赖"的旧行为。
  4. peerDependencies特殊规则:当实验性配置onlyUpdatePeerDependentsWhenOutOfRangetrue时,peer 依赖方仅在 peer 范围失去满足性时才被提升;为false(默认)则与普通依赖一致按阈值更新。该实验性开关位于___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH下,由 v4.0.0 引入。

2.3 通配范围与workspace:范围的"不重写"策略

  • */x/X这类通配范围默认不重写(新Range(range).range === ""即为空范围),因为通配范围本就能匹配任意版本。唯一的例外是新版本本身是 prerelease(如1.0.0-beta.0),此时必须重写为精确版本,否则 prerelease 不满足通配范围会导致安装到错误版本(该修复见 v5.0.5 变更记录,且与 src/version-package.ts 中的判断一一对应)。
  • workspace:*workspace:^workspace:~保持不变,它们由包管理器在发布时解析,无需写入具体版本;而workspace:1.0.0这类显式范围会被改写为workspace:1.1.0。仓库测试"should update workspace ranges""should not update workspace version aliases"分别验证了这两种行为(见 src/index.test.ts)。
  • file:/link:开头的依赖一律跳过

2.4 有界范围(bounded range)的正确重写:v8.1.1 的核心修复

>=1.0.0 <2.0.0这类双端范围在此前版本会被错误截断成>=2.0.0(丢失上界)。v8.1.1 修复后,getNewDependencyRange(src/version-package.ts)对满足"恰好一个下界 + 一个上界"的范围做整体重写:

  • 下界刷新为>=新版本(原来的>会被规范化为>=,确保新版本落在范围内);
  • 上界在新版本越界时按发布类型递增:<2.0.0<3.0.0(major)、<1.4.0(minor)、<1.2.5(patch);
  • 保持原始比较符顺序以最小化 manifest diff(<2.0.0 >=1.0.0会写成<3.0.0 >=2.0.0而非重排);
  • 没有有限<=等价形式时转为<>=1.0.0 <=1.9.9>=2.0.0 <3.0.0)。

测试中有一张完整的参数化用例表覆盖这些场景(src/index.test.ts),是理解该逻辑的最佳入口。单端范围则按前缀保留^~>=<=>或空(精确版本)。

2.5 两个影响范围更新的配置开关

  • bumpVersionsWithWorkspaceProtocolOnly: true(v4.2.0 引入):只在依赖以workspace:为前缀时才更新版本,普通 semver 范围不动。典型场景是希望发布时保留普通范围、只同步 workspace 协议依赖。仓库测试"should update workspace ranges only with bumpVersionsWithWorkspaceProtocolOnly"验证了同一发布中 workspace 依赖被更新而普通依赖保持1.0.0不变。
  • snapshot 模式:传入snapshot参数时,依赖范围被直接改写为精确的新快照版本newNewRange = newVersion),确保快照发布可复现(该修复见 v6.0.1 变更记录)。

三、Changelog 生成与写入:从无到有、从有到"插入"

3.1 生成规则:getChangelogEntry

src/get-changelog-entry.ts 负责为单个 release 组装新条目:

  1. type === "none"的 release 不生成条目(但若存在,会输出"无变更"占位,见下文)。
  2. 把该包相关的 changeset 按major / minor / patch分组,分别调用changelogFuncs.getReleaseLine(cs, type, changelogOpts)渲染发布行。
  3. 找出"本次也会发布且需要更新依赖范围"的依赖包(复用shouldUpdateDependencyBasedOnConfig判定),收集其关联 changeset 后调用changelogFuncs.getDependencyReleaseLine(...)渲染依赖更新行。
  4. ## 新版本标题与各类型小节拼接为最终文本;若三类都没有任何行(例如因fixed packages联动产生的无 changeset 发布),v8.1.0 起会补上一句No changes in this release.(对应变更记录中"Add default changelog message if the release has no changes for a package, e.g. due to fixed packages releases")。
  5. v8.0.0 优化了默认未格式化 changelog 的换行处理(generateMarkdownForVersionType),保证标题后与条目之间有稳定间距,避免生成出不规整的 Markdown。

3.2 加载自定义 changelog 模块

config.changelog[模块路径, 选项对象]false

  • false时完全跳过 changelog 生成(v6.0.4 修复了此前false不生效的 bug)。
  • 模块解析顺序:先在.changeset/目录下用import-meta-resolve解析,失败则退回contextDir(v7.0.8 支持传入运行脚本的contextDir,CLI 即借此加载内置 changelog)。v8.0.0 修复了内置模块在目标项目未安装时仍可加载的问题。
  • 加载方式从 v7.1.0 起由require()改为动态import(),因此自定义 changelog同时支持 CJS 与 ESM;代码中会依次剥掉default包装(含 CJS__esModuleinterop 场景),最终要求导出getReleaseLinegetDependencyReleaseLine两个函数,否则抛错(src/index.ts)。
  • 变更记录还提到:生成 changelog 前会用git.getCommitsThatAddFiles查询每个 changeset 的引入 commit,并将其附加到 changeset 对象上供getReleaseLine使用(v7.0.0 起避免使用短 commit id)。

3.3 写入策略:保留 intro、插入到第一个版本标题之前

updateChangelog(src/index.ts)按文件状态分四种情况处理:

文件状态行为
不存在创建文件,写入# 包名标题 + 新条目
存在但为空补写标题与条目
存在且含版本标题用正则/^#{1,6}\s+\d+\.\d+/m定位第一个版本标题,在其之前插入新条目,从而把文件顶部的介绍性内容(intro)保持在最上方(v8.1.1 行为,配套测试 "should update a changelog and maintain non-version CHANGELOG intro for one package")
存在但无版本标题视为"头部 + 正文"结构,新条目插入第一行之后(v7.1.0 修复了无包名标题时的插入错位)

另外 v7.0.13 修复了 changeset 摘要中含$等特殊替换模式导致 Changelog 内容被错误替换的问题;v7.0.6 通过升级spawndamnit修复了cross-spawn安全漏洞(v8.0.0 进一步把spawndamnit替换为tinyexec)。

四、保留package.json原始格式:editJson的"外科手术"

v8.0.0 起,版本号与依赖范围写入不再走"解析→序列化"(那会毁掉手写格式),而是基于 jsonc-parser):

  • 每个操作由{ keys, value }描述,keys是 JSON 键路径(如["dependencies", "pkg-b"]),实现会定位到目标值节点,仅替换其offsetlength区间,其余字符(缩进、换行、引号风格、逗号)原样保留。
  • 指定的键路径不存在会抛Key path "xxx" not found in JSON;JSON 解析失败会报告首个错误的偏移位置。
  • 测试覆盖了大量格式化场景:不重排小数组、保留 tab 缩进、已有尾部换行不删除、没有尾部换行不添加(见 src/edit-json.test.ts 与 src/index.test.ts 中 "formatting" 分组)。

这条链路同时呼应了变更记录中两条历史条目:v4.1.0 起用JSON.stringify更新 manifest 以避免 Prettier 干扰;v8.0.0 起改为上述"格式化保留"方案(PR #2070),并且无论是否配置了 Prettier,package.json都不会被重新格式化。

五、Prerelease 与 Snapshot:两种特殊发布形态

5.1 Prerelease 的文件结构迁移(v8.0.0 breaking change)

旧机制下,每个 prerelease 版本都会把已消费的 changeset留在根目录并记录 id 到.changeset/pre.json;v8.0.0 改为:

  • 已应用的 prerelease changeset 被移动到.changeset/pre/子目录(对应 src/index.ts 中fs.mkdir(".changeset/pre")+fs.rename的逻辑);
  • 旧的pre.json会在下次执行changeset versionchangeset status自动迁移到新结构;
  • 好处是:pre/目录里的 changeset 代表"为最终稳定版保留"的内容,可直接编辑或删除,且删除后无需再手工同步pre.json中的 id
  • preState.mode === "exit"(退出 prerelease)时,pre.json会被删除,之后执行稳定版发布。

5.2 Snapshot 快照发布

applyReleasePlan接受snapshot?: string | boolean参数,对应 CLI 的changeset version --snapshot [tag](v3.1.0 引入)。快照模式下版本形如0.0.0[-tag]-YYYYMMDDHHMMSS,并且依赖范围会被改写为精确的快照版本而非保留范围修饰符,保证快照安装可复现;与changeset publish --tag experimental搭配可在功能分支发布实验性 tag。

六、跳过机制:ignoreprivatePackages

  • v4.0.0 引入ignore配置:被忽略的包版本号不提升,但其依赖方仍正常提升,适用于"开发中的私有包"场景。对应地,applyReleasePlan在删除已应用 changeset 前会用shouldSkipPackage检查其中是否存在被忽略/不允许版本的包,存在则保留该 changeset 文件(src/index.ts)。
  • v7.0.2 修复了privatePackages(默认{ version: false, tag: false })在部分命令中未被尊重的问题,如今版本提升与打 tag 都会遵守该配置;v8.0.0 的 patch 中还避免了在未版本化的私有包里写入undefined版本。

七、版本演进时间线:一张图看懂该包的能力积累

结合 CHANGELOG.md 可将核心能力沉淀梳理如下:

版本关键变化
v8.1.1修复有界范围被截断;新条目插入首个版本标题之前以保留 intro
v8.1.0fixed packages 无变更时输出默认 changelog 消息
v8.0.0转 ESM;Node^22.11 || ^24 || >=26.changeset/pre/结构;@changesets/format格式化;移除 legacy v1 changeset 格式;保留package.json格式;新增具名导出;更新根包依赖范围;fs-extra→node:fsspawndamnit→tinyexec;移除get-version-range-type依赖
v7.1.ximport()加载 ESM changelog;workspace 别名/路径引用正确保留
v7.0.xcontextDir解析;交叉编译安全修复;privatePackages生效;无包名标题的插入修复
v6.xchangelogfalse生效;premode 下通配范围改写为精确版本;本地 Prettier 优先
v5.0.xworkspace:^/workspace:~支持;*范围在 prerelease 下改写
v4.xonlyUpdatePeerDependentsWhenOutOfRangeignore配置;bumpVersionsWithWorkspaceProtocolOnly
v3.xupdateInternalDependencies;snapshot 支持
v2.xdevDependencies 更新不再提升依赖方、不写 changelog;workspace 范围支持;自引用跳过
v1.0.0改为接收Packages对象而非cwd

八、调试与二次开发建议

  • 复现测试:该包测试集中在 src/index.test.ts(3615 行,覆盖版本化、changelog、workspace、snapshot、prerelease 等全部分支)与 src/edit-json.test.ts,运行pnpm vitest即可;其中FakeReleasePlanfixture 是构造最小复现的绝佳模板。
  • 关键决策点都在三个文件里:范围改写看 src/version-package.ts、判断逻辑看 src/utils.ts、changelog 组装看 src/get-changelog-entry.ts。
  • 配置联动updateInternalDependenciesbumpVersionsWithWorkspaceProtocolOnlyonlyUpdatePeerDependentsWhenOutOfRange三个配置共同决定了"依赖范围何时被重写",排查"为什么某个依赖版本没被同步更新"时,应首先核对这三项;完整配置项说明可参考 docs/config-file-options.md。
  • 整条链路的上游与下游:发布计划由 packages/get-release-plan/src/index.ts 生成,CLI 在 packages/cli/src/commands/version/index.ts 中调用本包并负责最终的 Git 提交与pre.json维护,排查问题时建议沿"get-release-plan → apply-release-plan → cli"的顺序定位。

总而言之,@changesets/apply-release-plan是 changesets 语义化发布中最"落地"的一环:它把抽象的版本决策翻译成精确、可复现、且尊重既有文件格式的真实变更。理解它的依赖范围改写规则与 changelog 写入策略,是深入使用甚至扩展 changesets 的必修课。

【免费下载链接】changesets🦋 A tool to manage versioning and changelogs with a focus on monorepos项目地址: https://gitcode.com/gh_mirrors/ch/changesets

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

淘宝蘑菇街实战:3步搞定电商后端,附速查手册

淘宝蘑菇街实战:3步搞定电商后端,附速查手册 刚学完语法,满脑子是变量和循环,但真让你搭个像样的项目,手就开始抖?别慌,这就是典型的“纸上谈兵”后遗症。很多新人卡在“从Hello World到实际业务”的鸿沟里,觉得电商系统高不可攀,其实只要拆解得当, 淘宝蘑菇街…

作者头像 李华
网站建设 2026/9/23 10:48:16

MFC新手避坑:3个致命错误让你项目直接报废

MFC新手避坑:3个致命错误让你项目直接报废 看了一堆教程还是不会写项目?别慌,这太正常了。MFC(Microsoft Foundation Classes)这套老古董框架,文档晦涩、报错迷之自信,新手一上手就懵圈,简直是编程界的“劝退神器”。今天不聊虚的,直接拆解我在十年实战中踩过的三个最狠的坑,…

作者头像 李华
网站建设 2026/9/23 10:47:56

阿里通官网配置卡半天?3步搞定性能优化避坑指南

阿里通官网配置卡半天?3步搞定性能优化避坑指南 配置环境就卡半天,是不是让你怀疑人生?很多开发者一碰到【阿里通官网】相关的依赖或工具链,第一步就卡在镜像源配置、版本兼容上,导致后续的性能优化根本无从谈起。别急,这不仅是环境问题,更是工程效率问题。今天咱们不聊虚的,直接拆解如何在【阿里通官网】生态下,…

作者头像 李华
网站建设 2026/9/23 10:47:12

搞定颜色的英语:新手避坑指南,告别配置地狱

搞定颜色的英语:新手避坑指南,告别配置地狱 第一次写前端或者做数据可视化时,是不是经常遇到这种情况?你想给按钮加个渐变色,或者想根据数据大小映射不同的颜色深浅,结果一查文档,满屏都是 #FF5733 、 rgb(255, 87, 51) 、 hsl(10, 100%, 65%)…

作者头像 李华