JointJS 仓库 Changesets 变更集实践指南:格式规范、常用命令与自动发布链路

原创2026-10-05 23:57:441,941 阅读
文章标签:前端UI组件

JointJS 仓库 Changesets 变更集实践指南:格式规范、常用命令与自动发布链路

导读

本文以 JointJS 仓库中 .changeset/README.md 为骨架,系统讲解该仓库基于 Changesets 工具的版本管理与变更集(changeset)工作流:包括变更集文件的完整格式(frontmatter、正文、作用域、描述规则)、yarn changeset 与 yarn changeset add --empty 等常用命令、CI 对缺失变更集的强制校验,以及从「Version Packages」PR 到 npm 发布的全自动链路。读完本文,你将能够在 JointJS 的 Yarn workspace 多包仓库中,为任何涉及可发布代码的 PR 正确编写变更集,让 CHANGELOG.md 与版本号自动演进。

一、什么是 Changesets,为什么 JointJS 用它

Changesets 是一套管理 monorepo 版本号与 changelog 的工具链。在 JointJS 中,它的作用链路是:

.changeset/ 目录中挂起的变更集(pending changesets)→ 驱动版本号提升、各包 CHANGELOG.md 生成与 npm 发布。

这套流程由 @changesets/cli,格式规则见 CONTRIBUTING.md 中的 Changeset format 小节。仓库根目录的 package.json 将 @changesets/cli(版本 3.0.0)声明为根 devDependency,而 .changeset/ 目录下目前包含三个文件:

  • README.md —— 本文讲解的格式速览;
  • config.json —— Changesets 配置(linked 分组、变更文件匹配规则等);
  • changelog.cjs —— 自定义 changelog 行生成逻辑(为每条记录追加短 commit SHA)。

值得注意的架构事实是:JointJS 采用 Yarn workspaces monorepo(见 package.json 的 workspaces 字段,包含 ./packages/* 与 ./examples/*),每个包维护独立的 CHANGELOG.md;绝大多数包独立发版,而 @joint/core、@joint/layout-directed-graph、@joint/layout-msagl 三者是 linked(联动) 的——若同一轮发布涉及其中两个或以上,它们会共享同一个版本号。这一点在 .changeset/config.json 的 linked 字段中亦有印证,实际链接组还包括 @joint/router-avoid。

二、创建变更集:命令与手写两种方式

为 PR 添加变更集有两种等价途径:

方式一:交互式命令

yarn changeset

执行后按提示选择受影响的包与 bump 类型(patch / minor / major),并编写变更摘要。生成的 .changeset/*.md 文件需要随 PR 一起提交(见 CONTRIBUTING.md)。

方式二:手写文件

直接在 .changeset/ 目录下新建一个 Markdown 文件,例如:

---
"@joint/core": minor
---

dia.Paper - add `originX` and `originY` options to `getFitToContentArea()`

这个示例并非虚构:dia.Paper - add originXandoriginYoptions togetFitToContentArea()` 这条记录实际出现在 packages/joint-core/CHANGELOG.md 的 4.3.0 Minor Changes 中,可对照参考其最终渲染形态。

空变更集(empty changeset)

当 PR 触及了可发布文件但不应触发发布时(例如纯测试改动被匹配规则之外的路径触碰、或需要占位说明的 PR),使用:

yarn changeset add --empty

空变更集标记该 PR 不产生发布动作。若无需任何变更集,则可用 yarn changeset add --empty 的判定边界依赖于 changedFilePatterns 配置(见下文第三节)。

三、变更集格式规范:Frontmatter + 正文

一个变更集本质上是一个带 YAML frontmatter 的 Markdown 文件,位于 .changeset/ 目录。frontmatter 声明哪些包随本次变更发布;正文则是将写入这些包 CHANGELOG.md 的条目。

3.1 Frontmatter:包名与 bump 类型

每个键是包名,每个值是 bump 类型(patch、minor、major),例如:

---
"@joint/core": minor
---

关键规则:单个变更集中列出多个包应当很少见。 因为正文会被原样复制到每个列出包的 changelog 中,只有当同一句话对所有这些包的读者都准确时才是正确的。若一个 commit 涉及多个包,应当为每个包单独写一个变更集,正文针对该包的用户分别措辞(见 CONTRIBUTING.md#changeset-format)。

3.2 正文:一行 = 一条 changelog bullet

正文必须保持单行——一个变更集恰好产出一条 changelog bullet。 只有正文的第一行会获得 - 标记;后续行会被缩进并渲染为同一条 bullet 的续行文本,而不是独立条目。因此:

  • 不要在正文中自己写 - / * 列表符号或 Markdown 标题;
  • 每需要一条额外的 changelog 行,就新建一个变更集文件。

这一规则在底层由自定义 changelog 生成器 .changeset/changelog.cjs 强制执行:getReleaseLine 将 summary 按 \n 拆分,首行输出为 - <首行> (<short SHA>),续行以两个空格缩进拼接;同时若变更集尚无 commit(如本地创建),则省略 SHA 后缀。该文件还实现了 getDependencyReleaseLine,用于生成 - Updated dependencies 块,按依赖列出 <dependency>@<newVersion>,并对 SHA 去重。

3.3 作用域(Scope):从类级到包级

正文遵循已有 CHANGELOG.md 的风格:作用域 - 描述(scope,空格-连字符-空格,再描述)。作用域的分级规则如下:

最常见:namespace.Class 类级作用域

dia.Paper - add `getCellView()` method for strict view lookup
mvc.View - add `classNamePrefix` instance property to override the `joint-` CSS class prefix
elementTools.Control - respect the `padding` option when computing the handle position
layout.DirectedGraph - add `rankSep` option

@joint/react 包:组件或 hook 作为作用域——组件写成 JSX 标签形式,hooks 直接用名字:

<Paper /> - fix the visual grid to redraw reactively when `drawGrid` changes
useCells - fix ghost cells reported after `resetCells()`

较少见:裸 namespace——当改动覆盖整个命名空间的所有内容、逐类列出显得冗余时使用:

anchors - add `rotate` option to all built-in anchors
connectionPoints - fix stroke-width handling on transformed elements

最罕见:完全无作用域——用于影响整个包的全局性或架构性改动,此时正文只有描述,没有作用域也没有前导的 -:

drop support for Internet Explorer 11
publish native ESM alongside the UMD bundle

唯一的固定例外:全新包的变更集使用字面量作用域 new package:

new package - idiomatic React components and hooks for JointJS, built directly on the core engine

3.4 描述(Description):约 100 字符内的动词开头

描述规范(见 CONTRIBUTING.md#changeset-format):

  • 长度:约 100 字符以内;
  • 大小写:小写字母开头;
  • 结尾:不加句号;
  • 开头动词:以动词起头(add、fix、deprecate、remove、support),缺陷修复自然读作 fix <what> 或 fix to <do what>;
  • 代码工件:描述中提到的代码标识符一律放进反引号,如 `changeId`, `batch:start`, `initializeUnmounted: true`, `drawGrid`;
  • 函数/方法名:永远带尾随括号 ()。
推荐写法 不推荐写法
of `layout()` of the layout function
of `layout()` of `layout()` function
fix `toJSON()` to honor the option fix toJSON to honor the option

四、CI 强制校验:没有变更集就挂 PR

变更集不是可选项,而是被 CI 强制执行的提交要求。根据 CONTRIBUTING.md:

CI 运行 changeset status --since=origin/master,若 PR 修改了公开包中的可发布代码而没有变更集,则 CI 失败。

其中「哪些文件算可发布代码」由 .changeset/config.json 的 changedFilePatterns 精确划定。该配置使用 glob 白名单加排除规则:

  • 匹配 "**"(全部文件);
  • 排除 **/*.test.{js,jsx,ts,tsx,mjs}、**/test/**、**/__tests__/**、**/__mocks__/**、**/bench/**、**/demo/**、**/stories/**、**/.storybook/**、**/dist/**、**/build/**、**/coverage/**、**/coverage.json、**/.tscache/**、**/scripts/**、**/grunt/**、**/Gruntfile.js、**/Makefile、**/rollup.config.*、**/rollup.resources.mjs、**/vite.config.*、**/vitest.workspace.*、**/jest.config.js、**/jest.react18.config.mjs、**/karma.conf.js、**/tsconfig*.json、**/dts-generator.config.js、**/knip.json、**/typedoc.*、**/.prettierrc*、**/.gitignore、**/eslint.config.mjs、**/*.md、**/LICENSE。

也就是说:测试、文档、demo 与构建配置文件改动被豁免;而 .md、LICENSE、lint/构建配置等一律不计入「可发布代码」。当 PR 触及可发布文件但不应触发发布时,使用 yarn changeset add --empty 通过校验。同文件还配置了 baseBranch: "master"、access: "public"、changelog: "./changelog.cjs"(指向上述自定义生成器)、commit: false 以及 updateInternalDependencies: "patch" 等选项。

五、发布链路:从变更集到 npm 与 Git tag

发布完全由 .github/workflows/release.yml 自动化(每次 push 到 master 触发),分两个阶段:

  1. Version(版本):只要存在挂起的变更集,工作流就持续维护一个名为 changeset-release/master 的「Version Packages」PR。该 PR 应用版本提升、写入各包 CHANGELOG.md 条目,并删除被消费掉的变更集文件。
  2. Publish(发布):合并该 PR 即触发发布。工作流随后构建整个 workspace,运行 changeset publish(内部通过 yarn npm publish 发布),并为每个已发布包创建 git tag 与 GitHub Release。

依赖驱动的连带发布

一个包何时会被依赖拖入同一轮发布,取决于版本声明范围(见 CONTRIBUTING.md):

  • 仅当新版本超出已声明范围时才会被拖入,且仅限运行时依赖——devDependencies 永远不会导致连带发布;
  • workspace:~ 会展开为 ~<core 的当前版本>,因此 @joint/layout-directed-graph 与 @joint/layout-msagl 在 @joint/core 发 minor 或 major 时自动获得一个 patch 补发,但 @joint/core 发 patch(如 4.3.2 仍满足 ~4.3.1)时不触发;
  • @joint/decorators 与 @joint/react 使用 workspace:^,因此只在 @joint/core 发 major 时随同发版。

Linked 联动组的实际版本行为

@joint/core、@joint/layout-directed-graph、@joint/layout-msagl(以及配置中的 @joint/router-avoid)构成 linked 组(见 .changeset/config.json):同轮发布的任意成员共享一个版本号(取该轮最高 bump 类型,应用于组内当前最高版本)。联动只覆盖本轮实际发布的包——组内没有发布内容的包保持当前版本,因此 @joint/core 可以是 4.3.1 而两个 layout 包仍是 4.3.0;其余包各自独立发版。

预发布与快照发布

  • 预发布(pre mode):标准做法是在 master 上执行 yarn changeset pre enter beta,按常规流程发布,结束后执行 yarn changeset pre exit;
  • 快照发布(snapshot release):yarn changeset version --snapshot 搭配 yarn changeset publish --tag 使用。

六、仓库中的真实样例对照

将规范落到实物上,可以对照 packages/joint-core/CHANGELOG.md 验证格式的实际渲染效果。例如其 4.3.0 Minor Changes 下的条目:

- dia.Paper - add `originX` and `originY` options to `getFitToContentArea()`
- dia.Paper - add typed `EventMap` for IDE autocomplete and type-checking on `on()` calls
- mvc.View - add `classNamePrefix` instance property to override the `joint-` CSS class prefix
- routers.rightAngle - add `minPathMargin`, `sourceMargin`, and `targetMargin` options

这些条目正是「namespace.Class 作用域 + 动词开头描述 + 反引号代码工件 + 方法带 ()」规则的完整体现;而 4.3.2 Patch Changes 中 alg.rightAnglePath: refactor to consolidate duplicate code into helpers 一类则展示了无作用域(包级/架构级)写法的变体。注意补丁条目末尾出现的短 SHA(如 0a2991b),正是 .changeset/changelog.cjs 中 getReleaseLine 追加 (<short SHA>) 的结果,与「一个变更集 = 一条 bullet、续行缩进」的生成逻辑完全吻合。

七、实用速查

场景 操作
常规添加变更集 yarn changeset
手写变更集 在 .changeset/ 新建 Markdown,frontmatter 写 "包名": bump类型,正文写 scope - description
PR 触及可发布文件但不应发布 yarn changeset add --empty
校验是否缺变更集 changeset status --since=origin/master(CI 自动执行)
预发布 beta yarn changeset pre enter beta / ... / yarn changeset pre exit
快照发布 yarn changeset version --snapshot + yarn changeset publish --tag
自定义 changelog 行生成 见 .changeset/changelog.cjs
可发布文件匹配规则 见 .changeset/config.json 的 changedFilePatterns

总结:Changesets 在 JointJS 中既约束了贡献者的提交习惯(一个 PR 一个变更集、正文单行、作用域规范),又通过 linked 分组与 changedFilePatterns 精确控制了版本演进的粒度。理解 .changeset/ 这套约定,是参与 JointJS 日常开发、读懂其 changelog 与发布节奏的前提。

登录后查看全文
joint