changesets/apply-release-plan 源码级解析版本号与 Changelog 的自动化应用引擎【免费下载链接】changesets A tool to manage versioning and changelogs with a focus on monorepos项目地址: https://gitcode.com/gh_mirrors/ch/changesetschangesets/apply-release-plan是 changesets 发布流程中负责落地执行的核心包它接收一份由changesets/get-release-plan生成的发布计划ReleasePlan将计划中的版本号提升、内部依赖范围更新、Changelog 生成与写入等一系列变更真实地应用到 monorepo 的各个package.json与CHANGELOG.md文件上。本文以该包 v8.1.1 的变更历史CHANGELOG.md为骨架结合 源码 与测试用例讲透其 API、工作流程、依赖范围改写规则、格式化保留策略以及 prerelease / snapshot 等进阶场景帮助你理解changeset version命令底层究竟做了什么以及在二次开发或排查发布问题时应当关注哪些关键点。一、包定位发布计划如何变成真实文件变更该包的 READMEpackages/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 模块解析的备用目录 ): Promisestring[] // 返回所有被改动文件的绝对路径要点packages必须是manypkg/get-packages的产物v1.0.0 起不再接收cwd内部通过packages.rootDir定位仓库根通过packagesByName把 release 中的包名映射到真实包对象找不到对应包时直接抛出Could not find matching package for release of: xxxsrc/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.mjsNode 支持范围提升为^22.11 || ^24 || 26见 packages/apply-release-plan/package.json。1.2 一次调用背后的完整流程梳理 src/index.ts 的主函数体可以得到如下执行链匹配包对象将releasePlan.releases中的每个 release 与packages.packages合并附加dir、packageJson等字段。预生成 Changelog 条目调用getNewChangelogEntry为每个 release 生成新版本条目文本详见第三节。处理 prerelease 退出若releasePlan.preState?.mode exit且非 snapshot删除根目录的.changeset/pre.json。逐包更新对每个 release先计算依赖范围编辑getDependencyVersionEdits再追加version字段的新版本值通过editJson写回package.json若生成了 changelog 文本则写入CHANGELOG.md。更新根包依赖若存在packages.rootPackage同样对根package.json中的 workspace 内部依赖范围做更新该行为由 v8.0.0 引入见变更记录中Update dependency ranges in the workspace root package.json一条。格式化把所有改动过的CHANGELOG.md交给 formatterv8.0.0 起改用changesets/formatformat配置为auto时自动探测 Prettier / BiomeBiome 因不支持 Markdown 被排除见 src/index.ts。消费 changeset 文件删除已应用且不涉及跳过包的.changeset/id.md若处于 prerelease 模式则移动至.changeset/pre/目录。返回touchedFiles。这一流程在 src/index.test.ts 中通过FakeReleasePlanfixture 与testSetup辅助函数做了大量端到端验证例如单个包版本更新两个包不同新版本根包依赖更新但不版本化根包等用例。二、版本号与依赖范围最精细的改写逻辑版本号提升只是把version字段替换成新值真正的复杂度集中在依赖范围的处理上全部实现在 src/version-package.ts 的getDependencyVersionEdits与 src/utils.ts 的shouldUpdateDependencyBasedOnConfig中。2.1 扫描哪些依赖字段DEPENDENCY_TYPES覆盖四类字段src/version-package.tsconst DEPENDENCY_TYPES [ dependencies, devDependencies, peerDependencies, optionalDependencies, ] as const;注意v2.0.0 起更新devDependencies不再连带提升依赖方自身版本dev 依赖不影响最终用户且 dev 依赖的变更不再写入 Changelog——这一行为变化是当年的 breaking change如今已是稳定预期。2.2 判断是否需要更新shouldUpdateDependencyBasedOnConfig对每个被发布包在依赖方 manifest 中的范围按以下优先级决策src/utils.tsworkspace:协议优先处理workspace:*直接返回true表示总会被重写workspace:^/workspace:~先还原成^oldVersion/~oldVersion再参与判断若workspace:后面跟的是相对路径引用如workspace:../pkg则与包目录的相对路径比对路径一致才更新。新版本已不在范围内!semverSatisfies(newVersion, range)→ 必须更新这是兜底规则保证发布后依赖永远指向可满足的版本。否则按updateInternalDependencies阈值patch | minor判断依赖方只会在被依赖包的提升级别达到阈值时同步提升。该配置项由 v3.0.0 引入用于关闭仅 patch 提升也连带内部依赖的旧行为。peerDependencies特殊规则当实验性配置onlyUpdatePeerDependentsWhenOutOfRange为true时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 rangesshould 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 修复后getNewDependencyRangesrc/version-package.ts对满足恰好一个下界 一个上界的范围做整体重写下界刷新为新版本原来的会被规范化为确保新版本落在范围内上界在新版本越界时按发布类型递增2.0.0→3.0.0major、1.4.0minor、1.2.5patch保持原始比较符顺序以最小化 manifest diff2.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: truev4.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 生成规则getChangelogEntrysrc/get-changelog-entry.ts 负责为单个 release 组装新条目type none的 release 不生成条目但若存在会输出无变更占位见下文。把该包相关的 changeset 按major / minor / patch分组分别调用changelogFuncs.getReleaseLine(cs, type, changelogOpts)渲染发布行。找出本次也会发布且需要更新依赖范围的依赖包复用shouldUpdateDependencyBasedOnConfig判定收集其关联 changeset 后调用changelogFuncs.getDependencyReleaseLine(...)渲染依赖更新行。将## 新版本标题与各类型小节拼接为最终文本若三类都没有任何行例如因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。v8.0.0 优化了默认未格式化 changelog 的换行处理generateMarkdownForVersionType保证标题后与条目之间有稳定间距避免生成出不规整的 Markdown。3.2 加载自定义 changelog 模块config.changelog为[模块路径, 选项对象]或falsefalse时完全跳过 changelog 生成v6.0.4 修复了此前false不生效的 bug。模块解析顺序先在.changeset/目录下用import-meta-resolve解析失败则退回contextDirv7.0.8 支持传入运行脚本的contextDirCLI 即借此加载内置 changelog。v8.0.0 修复了内置模块在目标项目未安装时仍可加载的问题。加载方式从 v7.1.0 起由require()改为动态import()因此自定义 changelog同时支持 CJS 与 ESM代码中会依次剥掉default包装含 CJS__esModuleinterop 场景最终要求导出getReleaseLine与getDependencyReleaseLine两个函数否则抛错src/index.ts。变更记录还提到生成 changelog 前会用git.getCommitsThatAddFiles查询每个 changeset 的引入 commit并将其附加到 changeset 对象上供getReleaseLine使用v7.0.0 起避免使用短 commit id。3.3 写入策略保留 intro、插入到第一个版本标题之前updateChangelogsrc/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]实现会定位到目标值节点仅替换其offset与length区间其余字符缩进、换行、引号风格、逗号原样保留。指定的键路径不存在会抛Key path xxx not found in JSONJSON 解析失败会报告首个错误的偏移位置。测试覆盖了大量格式化场景不重排小数组、保留 tab 缩进、已有尾部换行不删除、没有尾部换行不添加见 src/edit-json.test.ts 与 src/index.test.ts 中 formatting 分组。这条链路同时呼应了变更记录中两条历史条目v4.1.0 起用JSON.stringify更新 manifest 以避免 Prettier 干扰v8.0.0 起改为上述格式化保留方案PR #2070并且无论是否配置了 Prettierpackage.json都不会被重新格式化。五、Prerelease 与 Snapshot两种特殊发布形态5.1 Prerelease 的文件结构迁移v8.0.0 breaking change旧机制下每个 prerelease 版本都会把已消费的 changeset留在根目录并记录 id 到.changeset/pre.jsonv8.0.0 改为已应用的 prerelease changeset 被移动到.changeset/pre/子目录对应 src/index.ts 中fs.mkdir(.changeset/pre)fs.rename的逻辑旧的pre.json会在下次执行changeset version或changeset 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。六、跳过机制ignore与privatePackagesv4.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修复有界范围被截断新条目插入首个版本标题之前以保留 introv8.1.0fixed packages 无变更时输出默认 changelog 消息v8.0.0转 ESMNode^22.11 || ^24 || 26.changeset/pre/结构changesets/format格式化移除 legacy v1 changeset 格式保留package.json格式新增具名导出更新根包依赖范围fs-extra→node:fs、spawndamnit→tinyexec移除get-version-range-type依赖v7.1.ximport()加载 ESM changelogworkspace 别名/路径引用正确保留v7.0.xcontextDir解析交叉编译安全修复privatePackages生效无包名标题的插入修复v6.xchangelogfalse生效premode 下通配范围改写为精确版本本地 Prettier 优先v5.0.xworkspace:^/workspace:~支持*范围在 prerelease 下改写v4.xonlyUpdatePeerDependentsWhenOutOfRangeignore配置bumpVersionsWithWorkspaceProtocolOnlyv3.xupdateInternalDependenciessnapshot 支持v2.xdevDependencies 更新不再提升依赖方、不写 changelogworkspace 范围支持自引用跳过v1.0.0改为接收Packages对象而非cwd八、调试与二次开发建议复现测试该包测试集中在 src/index.test.ts3615 行覆盖版本化、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。配置联动updateInternalDependencies、bumpVersionsWithWorkspaceProtocolOnly、onlyUpdatePeerDependentsWhenOutOfRange三个配置共同决定了依赖范围何时被重写排查为什么某个依赖版本没被同步更新时应首先核对这三项完整配置项说明可参考 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),仅供参考
