Gutenberg 包管理实践:npm Workspaces 单体仓库、Lerna 独立版本与自动化 npm 发布
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
Gutenberg 仓库用 npm workspaces 统一管理 100+ 个@wordpress/*包,用 Lerna 以独立版本(independent versioning)模式将它们发布到 npm,并通过每个包强制维护的CHANGELOG.md驱动半自动化发布。本文基于仓库中的 包管理文档 展开,结合根目录 package.json、lerna.json 与 tools/release 下的发布 CLI 源码,讲清包目录约定、Changelog 规则、依赖管理方式、内部 workspace 模式,以及 npm 发布的实际执行链路。
总体架构:npm workspaces 管包,Lerna 管发布
Gutenberg 的包管理策略可以概括为一句话:npm workspaces 负责仓库内的依赖组织,Lerna 负责把包发布到 npm。两者的分工在仓库根部的两个配置文件中体现得非常清楚。
根 package.json 中的 workspace 声明
根 package.json 的workspaces字段(L216-L224)声明了所有被纳入 workspace 体系的目录:
"workspaces": [ "packages/*", "routes/*", "storybook", "test/*", "tools/*", "widgets/*", "!test/ai-development" ]几点值得注意:
packages/*是对外发布到 npm 的@wordpress/*包所在的目录;tools/*与test/*是内部 workspace,用于开发工具和测试基础设施,不发布到 npm;routes/*、widgets/*是构建产物侧的 bundle 入口,storybook是 Storybook 宿主;- 末尾的
!test/ai-development表示从 workspace 集合中显式排除该目录。
此外,根package.json的devDependencies(L84-L103)大量使用file:协议引用本仓库内部的 workspace,例如:
"@wordpress/eslint-tools": "file:./tools/eslint", "@wordpress/monorepo-tools": "file:./tools/monorepo", "@wordpress/release-tools": "file:./tools/release", "@wordpress/scripts": "file:./packages/scripts"这正是内部 workspace 模式的直接体现:工具以file:引用挂载到根,而不是把依赖散落进根目录。同时,环境要求被声明在engines字段中(Node.js>=24.18.0、npm>=11.16.0,L17-L20),并通过devEngines强制校验。
lerna.json 的配置含义
发布侧的配置集中在根目录 lerna.json,全部内容只有 16 行:
{ "command": { "publish": { "message": "chore(release): publish" } }, "ignoreChanges": [ "**/benchmark/*.js", "**/CHANGELOG.md", "**/{__mocks__,__tests__,test}/**", "**/{storybook,stories}/**" ], "packages": [ "packages/*" ], "version": "independent", "$schema": "node_modules/lerna/schemas/lerna-schema.json" }逐字段解读:
| 配置项 | 取值 | 含义 |
|---|---|---|
packages | ["packages/*"] | Lerna 只管理packages/下的目录,tools/、test/等内部 workspace 不参与版本计算 |
version | independent | 各包独立版本,一个包发版不会带动其他包升版 |
ignoreChanges | 测试、mock、storybook、benchmark、CHANGELOG.md文件 | 修改这些文件不会触发 Lerna 判定"该包需要发版" |
command.publish.message | chore(release): publish | 自动生成的发布 commit 信息 |
ignoreChanges中把**/CHANGELOG.md排除掉是有意为之:Changelog 由发布流程统一改写(见后文),其变更本身不应被视为"代码变更"。
每个包的 CHANGELOG.md:发布流程的驱动数据
Gutenberg 文档明确指出:维护几十个 npm 包时很难跟踪变化,因此每个包都必须维护CHANGELOG.md,贡献者在提交生产代码时必须向该文件追加条目。这不是单纯的文档习惯——发布 CLI 会直接解析 Changelog 文本来决定版本号如何提升,是发布流水线的输入数据。
写入规范:Unreleased 区块
每个 PR 若包含用户可见变更,都应在对应包的CHANGELOG.md顶部## Unreleased标题下追加条目(若标题不存在则需创建)。规范格式见 packages/README.md 的 Maintaining Changelogs 一节,例如:
<!-- Learn how to maintain this file at https://github.com/WordPress/gutenberg/tree/HEAD/packages#maintaining-changelogs. --> ## Unreleased ### Bug Fixes - Fixed an off-by-one error with the `sum` function.子标题与语义化版本映射
Changelog 子标题与 semver 版本提升的对应关系是发布流程的核心约定(完整列表来自 packages/README.md):
| 子标题 | 含义 | 版本提升要求 |
|---|---|---|
Breaking Changes | 向后不兼容变更,需要使用者特别注意 | 稳定包(1.0.0+)提升 major |
New Features | 新增向后兼容的公共 API 或功能 | 提升 minor |
Enhancements | 对现有功能的向后兼容改进 | 提升 minor |
Deprecations | 弃用通知,不影响公共接口或行为 | 提升 minor |
Bug Fixes | 修复既有错误行为 | 提升 patch |
Internal | 不影响公共接口或行为的内部变更 | 提升 patch |
Stable Release | 标记 0.x 预发布包为稳定可用 | 提升到 1.0.0 |
文档特别强调:描述必须准确,因为发布流程会用它来判定下一个版本号;当拿不准时以 semver 规范为准。
Stable Release:从 0.x 晋升 1.0.0
自动化发布流程对预发布包(0.x.x)最多只提升 minor 版本,即使包含破坏性变更——这符合 semver 中0.x阶段 API 可能频繁变动的约定。当某个包的 API 被认为足够稳定、可以投入生产时,通过在CHANGELOG.md中写入### Stable Release小节触发晋升:
## Unreleased ### Stable Release This package is now considered stable and production-ready. The API will follow semantic versioning from this point forward. ### Breaking Changes - Final API adjustments before 1.0.0 release.Stable Release小节只用于 1.0 之前的包;晋升之后,破坏性变更将按 semver 惯例触发 major 提升。
源码印证:版本提升是怎么算出来的
上述规则不是停留在文档层面的约定。发布 CLI 的核心函数 calculateVersionBumpFromChangelog 逐行扫描 Changelog,逻辑与文档描述一一对应:
- 从
## Unreleased开始扫描,遇到下一个##已发布版本标题即停止(common.js L46-L59); - 当前版本小于
1.0.0时遇到### Stable Release→ 提升 major(即跳升到 1.0.0); - 遇到
### Breaking Changes→ 稳定包提升 major,0.x包只提升 minor(common.js L70-L79); - 遇到
### Deprecations/### Enhancements/### New API/### New Features→ 提升 minor; - 其他任何新小节或变更条目 → 提升
minimumVersionBump(CLI 中默认为patch)。
也就是说,Changelog 中写的每一个子标题都会被机器读取,标题拼写直接决定版本号。
依赖管理:生产依赖进包,开发依赖进根或工具包
文档与 packages/README.md 将依赖分为两类,处理方式截然不同:
生产依赖:用-w安装到具体包
生产依赖写在包的package.json的dependencies中,从仓库根目录执行:
# 添加:把 change-case 安装为 packages/a11y 的依赖 npm install change-case -w packages/a11y # 锁定 lockfile 中已有版本时复用;强制换版本用 @ 后缀 npm install change-case@latest -w packages/a11y # 移除 npm uninstall change-case -w packages/a11y更新依赖的推荐策略(文档称之为 monorepo 中最容易困惑的部分)是:先npm uninstall,再npm install回同名的包——除非显式指定版本,否则装回来的是最新版本。
开发依赖:不进包,进根或内部工具包
开发依赖不应写进各个@wordpress/*包,而应安装在根package.json(或内部工具包)中,使所有包共享同一套工具配置。例如:
# 在仓库根目录执行 npm install glob --save-dev新包创建后的必要步骤
新建包目录并写好package.json后,需要运行npm install更新package-lock.json,包才会被 npm workspaces 识别。packages/README.md给出了完整的新包模板(含wpScript/wpScriptModuleExports/types/sideEffects/publishConfig字段),并规定每个包必须附带 README 与CHANGELOG.md,新包的 CHANGELOG 初始内容如下:
<!-- Learn how to maintain this file at https://github.com/WordPress/gutenberg/tree/HEAD/packages#maintaining-changelogs. --> ## Unreleased Initial release.内部 workspaces:tools/ 与 test/
关联文档的最后一节专门讲内部 workspace:仓库在tools/和test/下维护开发工具与测试基础设施的 workspace。**当需要为仓库级工作新增工具、脚本或依赖时,应在tools/下创建(或复用已有的)workspace,而不是往根package.json里加依赖。**详细规范见 Workspace Development 指南,要点如下:
为什么依赖不该进根 package.json
- 职责分离:每个 workspace 显式声明自己需要的依赖,代码与依赖的关系可读;
- 根目录干净:根
package.json只保留仓库级工具(lint、format、typecheck、git hooks、monorepo 编排),评审依赖变更更容易; - 减少合并冲突:更新某个 workspace 的依赖不需要触碰根文件;
- 防止幻影依赖:npm 的 hoist 策略会把依赖提升到根
node_modules,workspace 可能"偷看"到未声明的依赖。保持根目录精简也是未来迁移到 pnpm(隔离依赖模型)的前提。
各目录的分工
| 位置 | 用途 | 是否发布 npm |
|---|---|---|
packages/* | 发布的@wordpress/*包 | 是 |
tools/* | 内部开发工具(ESLint 配置、发布 CLI、API 文档生成器、校验工具等) | 否 |
test/* | 测试基础设施(unit、integration、e2e、performance 等) | 否 |
storybook | Storybook 宿主 | 否 |
routes/* | 编辑器路由入口 | 否 |
widgets/* | Widget bundle | 否 |
凡是被根package.json的workspacesglob 匹配到的目录(如tools/*),只要其中放了一个package.json就会被自动纳入 workspace,无需逐一手动注册。
新建内部 workspace 的标准步骤
转换模式参考 Storybook 迁移实践,共六步:
- 在 workspace 目录添加
package.json,内部工具设"private": true,只列该 workspace 真正 import/执行的依赖;依赖同仓库其他 workspace 时用file:引用(如"@wordpress/scripts": "file:../../packages/scripts"); - 含 TypeScript 时添加
tsconfig.json,extends共享基配置@wordpress/monorepo-tools/tsconfig/base.json,并在根tsconfig.json的 project references 中登记; - 若路径已被现有 glob 覆盖则自动注册,否则在根
workspaces数组中补一条; - 在根
package.json用npm run --workspace <name> <script> --暴露转发脚本,末尾的--会把额外 CLI 参数透传给 workspace; - 补一份 README 说明用途、脚本与非常规配置;
- 更新 CI:统一通过根目录的
npm run包装脚本调用 workspace,而不是cd进目录,保证本地与 CI 跑的是同一条命令。
日常命令模式
# 在特定 workspace 中增删依赖(只改该包 package.json 与根 lockfile) npm install <pkg> --workspace @wordpress/<name> npm uninstall <pkg> --workspace @wordpress/<name> # 运行某个 workspace 的脚本 npm run <script> --workspace @wordpress/<name> # 在定义了该脚本的所有 workspace 中运行(如根 prelint:js 的用法) npm run --if-present --workspaces <script>根 package.json 中可见这些模式的真实用例,例如"prelint:js": "npm run --if-present --workspaces prelint:js"、"test:unit": "npm run --workspace @wordpress/unit-tests test:unit --"。
npm 发布流程:与插件 RC1 同步的自动化链路
文档说明:WordPress 包发布到 npm 是自动化的,与每两周一次的 Gutenberg 插件 RC1 发布同步;其他发布方式记录在 Gutenberg Release Process 文档。仓库内的实现位于 tools/release 这个内部 workspace(在根package.json中以@wordpress/release-tools: file:./tools/release挂载),入口是 tools/release/cli.js,基于 commander 注册了四条包发布命令:
| 命令(别名) | 用途 | 关键选项 |
|---|---|---|
publish-npm-packages-latest(npm-latest) | 从 Gutenberg 插件同步发布生产版本,latestdist-tag | --semver <patch\|minor\|major>(默认 patch)、-c, --ci、--repository-path |
publish-npm-packages-bugfix-latest(npm-bugfix) | 面向latestdist-tag 的补丁发布 | -c, --ci、--repository-path |
publish-npm-packages-wordpress-core(npm-wp) | 面向 WordPress core 的补丁发布,wp-X.Ydist-tag | 必填--wp-version <wpVersion> |
publish-npm-packages-next(npm-next) | 发布开发版本,nextdist-tag,预发布版本号 | --semver、-c, --ci |
其中--semver指定本次发布的最小版本提升等级(见 cli.js L19-L60);与 Changelog 推导出的提升取两者中较大的一个,这与 packages/README.md 中"选 semver 与 Gutenberg 发布最低提升中更大者"的规则一致。
发布时 Changelog 如何被改写
updatePackages 函数展示了 Changelog 从"数据"到"版本记录"的落地过程:
- glob 扫描
packages/*/CHANGELOG.md,并跳过private: true的包(packages.js L195-L202),私有包不发布、不改写; - 对每个包调用
calculateVersionBumpFromChangelog计算提升等级,再用semver.inc算出下一个版本; - 把
## Unreleased改写为## Unreleased+## <新版本> (<发布日期>)(next类型则写成<版本>-next.0),同时把package.json的 version 更新为<新版本>-prerelease; - 打印
包名: 旧版本 -> 新版本清单,提交Update changelog files并推送到 npm 发布分支。
这也解释了为什么贡献者只写## Unreleased:版本标题和日期是发布时由工具自动生成的。
工程化细节:重试、校验与恢复
tools/release/commands/packages.js 顶部的一组常量揭示了大规模发布的健壮性设计:
NPM_RELEASE_PHASE_ATTEMPTS = 3:每个发布阶段最多重试 3 次,退避采用指数策略(2^(n-1) * 5s,上限 120s);NPM_RELEASE_VERIFICATION_ATTEMPTS = 18:注册表传播校验重试 18 次——注释说明一次 121 个包的发布中,最后一个包可见曾耗时约 25 分钟,预算按"尾部"而非"常态"设置;NPM_RELEASE_TAG_PUSH_BATCH_SIZE = 25:Git tag 按每批 25 个推送,避免单批过大;refs/npm-release/*预备态 ref:lerna version --no-push生成的发布 commit 会先持久化到 scratch ref(L35-L43 的注释),使 CI runner 中途失败后可以重跑恢复,且不会提前对外暴露尚未上 npm 的版本。
发布前还有一个 npm 预检步骤 runNpmPublishPreflight:先用npm whoami验证凭据,再逐个npm view <name>@<version>核对注册表中已有版本的gitHead与 dist-tag 是否与本次发布一致,防止重复发布或 dist-tag 被其他发布移动后的不安全恢复。
小结与延伸阅读
Gutenberg 的包管理把"人写 Changelog、机器算版本、Lerna 打 tag 发包、发布 CLI 做同步与校验"串成了一条可审计的流水线,核心文件对照:
- 包管理总览:docs/contributors/code/managing-packages.md
- 包创建/Changelog/依赖/TypeScript 细则:packages/README.md
- 内部 workspace 规范:docs/contributors/code/workspace-development.md
- 发布流程入口:docs/contributors/code/release/README.md
- Lerna 配置:lerna.json;workspace 声明:package.json
- 发布 CLI 与版本计算:tools/release/cli.js、tools/release/commands/common.js、tools/release/commands/packages.js
需要说明的适用前提:本文所有命令与配置均以当前仓库状态为准(Node.js ≥24.18.0、npm ≥11.16.0、Lerna 10);发布命令的执行还需要 Gutenberg 开发团队的仓库权限,普通贡献者的日常工作面是"改代码 + 写 Changelog + 用-w管理依赖"这三件事。
【免费下载链接】gutenbergThe Block Editor project for WordPress and beyond. Plugin is available from the official repository.项目地址: https://gitcode.com/GitHub_Trending/gu/gutenberg
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考