@commitlint/config-pnpm-scopes:为 pnpm workspace 仓库自动生成 scope 枚举的 commitlint 共享配置
【免费下载链接】commitlint📓 Lint commit messages项目地址: https://gitcode.com/gh_mirrors/co/commitlint
导读
@commitlint/config-pnpm-scopes是 commitlint 官方维护的一个共享配置(shareable config),它专门服务于使用 pnpm workspace 的 monorepo 项目:配置启用后,commitlint 会自动扫描pnpm-workspace.yaml中声明的所有工作区包,把每个包名作为合法的scope(作用域)白名单,从而强制提交信息中的scope必须来自当前仓库的真实包名。读完本文,你将掌握该配置的安装与接入方法、其底层自动发现包名的完整实现原理、与scope-enum规则的联动机制,以及面对@scope/a这类 scoped 包名时的处理规则。
一、这是什么:为 pnpm workspaces 量身定制的共享配置
在 pnpm monorepo 中,常见的提交格式是type(scope): subject,例如build(api): change something in api's build。如果 scope 写错成仓库中不存在的包名(例如test(foo)),提交应被拦截。手工维护一份包名枚举清单既繁琐又容易过期,而 @commitlint/config-pnpm-scopes 的价值在于:它把「当前 pnpm workspace 中有哪些包」这件事完全自动化,每次运行校验时动态读取仓库结构,生成scope-enum规则的枚举值。
该配置的定位是:
Shareable
commitlintconfig enforcing pnpm workspaces names as scopes.
它需要配合 @commitlint/cli(命令行校验)与 @commitlint/prompt-cli(交互式提交工具)一起使用。包的元信息见 package.json:模块类型为 ESM("type": "module"),声明了 Node.js>=22.12.0的引擎要求,运行时依赖@pnpm/read-project-manifest与read-yaml-file两个库,前者用于精确解析各包的 manifest 文件,后者用于解析pnpm-workspace.yaml。
二、快速开始:安装与接入
在仓库根目录执行以下两条命令即可完成安装与配置生成:
npm install --save-dev @commitlint/config-pnpm-scopes @commitlint/cli echo "module.exports = {extends: ['@commitlint/config-pnpm-scopes']};" > commitlint.config.js核心步骤拆解:
- 安装两个包:
@commitlint/config-pnpm-scopes提供规则,@commitlint/cli提供commitlint命令行入口; - 写入配置文件:在
commitlint.config.js中通过extends引入该共享配置,module.exports采用 CommonJS 导出,与 commitlint 的常规配置加载机制一致。
之后的提交校验会按 Configuration guide 中描述的流程加载配置:extends中的配置项会被递归合并进当前配置,而本包通过extends暴露的rules与utils会一并生效。
提示:若想同时保留其他约定(如 conventional commits 的 type 枚举),可以将本配置与其他共享配置并列放入
extends数组,例如extends: ['@commitlint/config-conventional', '@commitlint/config-pnpm-scopes']。
三、工作原理:从 pnpm-workspace.yaml 到 scope 枚举
配置的真正实现非常精简,核心源码见 index.ts。整个配置对象只包含两个键:
export default { utils: { getProjects }, rules: { "scope-enum": (ctx = {}) => getProjects(ctx).then((packages: any) => [2, "always", packages]), }, };utils.getProjects:向 commitlint 的插件/配置机制暴露「获取包列表」的工具函数,便于其他配置或插件复用;rules["scope-enum"]:定义规则,其返回值为三元组[2, "always", packages],其中2表示错误级别(severity,即不满足时报错而非警告)、"always"表示修饰符(modifier,即 scope 必须命中枚举)、packages为动态计算出的合法 scope 列表。
3.1 读取 workspace 声明
requirePackagesManifest负责读取根目录的pnpm-workspace.yaml:
function requirePackagesManifest(dir: any) { return readYamlFile(path.join(dir, "pnpm-workspace.yaml")).catch((err: any) => { if (err.code === "ENOENT") { return null; } throw err; }); }- 文件缺失(
ENOENT)时返回null,不抛错——这意味着没有pnpm-workspace.yaml的普通仓库也能正常加载该配置; - 其他读取错误(如 YAML 语法错误)会被向上抛出,让用户在配置阶段就发现问题。
3.2 将 packages 模式规范化为 manifest 匹配路径
normalizePatterns把pnpm-workspace.yaml中的 glob 模式统一补全为指向包清单文件的模式:
function normalizePatterns(patterns: any) { const normalizedPatterns = []; for (const pattern of patterns) { normalizedPatterns.push(pattern.replace(/\/?$/, "/package.{json,json5,yaml}")); } return normalizedPatterns; }例如声明packages: ['packages/*']时,实际用于 glob 的模式为packages/*/package.{json,json5,yaml},即同时覆盖package.json、package.json5、package.yaml三种包清单格式。
3.3 用 glob 发现所有包并解析 manifest
findWorkspacePackages是发现流程的核心:
function findWorkspacePackages(cwd: any) { return requirePackagesManifest(cwd) .then(async (manifest: any) => { const patterns = normalizePatterns((manifest && manifest.packages) || ["**"]); const entries: string[] = []; for (const pattern of patterns) { for await (const entry of glob(pattern, { cwd, exclude: (p) => p.includes("node_modules") || p.includes("bower_components"), })) { entries.push(entry); } } return entries; }) .then((entries: any) => { const paths = Array.from(new Set(entries.map((entry: any) => path.join(cwd, entry)))); return Promise.all(paths.map((manifestPath: any) => readExactProjectManifest(manifestPath))); }) .then((manifests: any) => { return manifests.map((manifest: any) => manifest.manifest); }); }值得注意的实现细节:
- 默认兜底模式:若
pnpm-workspace.yaml缺失或其packages字段为空,则使用["**"],即递归扫描整个工作目录下的所有包清单文件; - 排除目录:glob 过程中显式排除
node_modules与bower_components,避免把依赖目录误判为工作区包; - 去重:通过
Set对匹配到的 manifest 路径去重,防止多个模式命中同一文件; - 精确解析:使用
@pnpm/read-project-manifest的readExactProjectManifest解析每个 manifest——该库能正确处理 JSON、JSON5、YAML 等格式,并返回标准化的 manifest 对象。
3.4 提取包名并构造 scope 白名单
最后,getProjects把 manifest 列表收敛为一组 scope:
function getProjects(context: any) { const ctx = context || {}; const cwd = ctx.cwd || process.cwd(); return findWorkspacePackages(cwd).then((projects: any) => { const scopes = projects.reduce((acc: any, project: any) => { const name = project.name; if (name) { acc.add(name.charAt(0) === "@" ? name.split("/")[1] : name); } return acc; }, new Set()); scopes.add("global"); return Array.from(scopes).sort(); }); }四条关键规则:
- 忽略无
name字段的包:manifest 中没有name的包不会进入 scope 白名单; - scoped 包名取
/后段:对于@scope/a这种 npm scoped 包名,只保留a作为合法 scope——提交时写a(...)而非@scope/a(...),这与 commitlint 的 scope 语法兼容性更好(该处理方式与 @commitlint/config-lerna-scopes 等同类配置保持一致); - 始终追加
global:无论仓库里有多少包,global永远是一个合法 scope,用于表达「与具体包无关的全局性改动」; - 排序输出:最终列表按字典序排序,保证枚举值稳定、可读。
3.5 执行目录从哪来
getProjects(context)的上下文ctx若提供了cwd则以其为扫描根目录,否则回退到process.cwd()。这意味着在 @commitlint/cli 的--cwd参数、lint 阶段传入的上下文等因素影响下,配置会基于实际执行目录发现工作区包。
四、运行示例:实际校验效果
原文档给出了一个完整的三段式示例。假设commitlint.config.js内容为:
{ extends: ['@commitlint/config-pnpm-scopes'] }仓库结构如下(三个包:api、app、web):
packages ├── api ├── app └── web那么:
1. scope 命中合法包名,校验通过:
❯ echo "build(api): change something in api's build" | commitlint无任何输出,即校验通过。
2. scope 不在枚举内,校验失败:
❯ echo "test(foo): this won't pass" | commitlint ⧗ --- input --- test(foo): this won't pass ✖ scope must be one of [api, app, web] [scope-enum] ✖ found 1 problems, 0 warningsfoo不在[api, app, web]中,因此命中scope-enum规则,以错误级别(severity 2)报告 1 个问题。
3. 不带 scope 的提交不受影响:
❯ echo "ci: do some general maintenance" | commitlint校验通过。原因见scope-enum规则实现:当提交信息没有 scope 时规则直接放行(见下文第五节的规则源码)。
示例中的错误消息格式scope must be one of [api, app, web] [scope-enum]正是由 @commitlint/rules/src/scope-enum.ts 中errorMessage = ["scope must",be one of [${scopes.join(", ")}]]生成的,枚举列表来自配置动态计算出的包名数组。
五、底层联动:scope-enum 规则如何消费枚举
本配置的规则输出会被 commitlint 核心机制转化为 @commitlint/rules 中scopeEnum规则的参数。其核心逻辑如下:
export const scopeEnum: SyncRule<string[] | { scopes: string[]; delimiters?: string[] }> = ( { scope }, when = "always", value = [], ) => { const scopes = Array.isArray(value) ? value : value.scopes; if (!scope || !scopes.length) { return [true, ""]; } // 按 / \ , 等分隔符切分 scope,逐个校验是否在枚举中 // when === "always" 时:所有切分出的 scope 都必须在枚举中,或整体命中枚举 };从实现中可以确认两点行为:
- 无 scope 直接通过:
!scope时返回[true, ""],所以像ci: do some general maintenance这样不带 scope 的提交不受scope-enum约束(如需强制必须写 scope,应配合scope-empty规则); - 多级 scope 支持:规则会按
/、\、,等分隔符切分 scope 后逐段校验,scope-enum的完整参数说明与相关规则可查阅 Rules reference 与 Rules configuration。
六、测试验证:行为由用例锁定
该包的行为有完整的测试覆盖,见 index.test.ts,测试基于仓库自带的三个 fixture 目录(fixtures):
| 测试点 | 断言结果 | 对应 fixture |
|---|---|---|
配置导出rules键且包含scope-enum | config.rules["scope-enum"]为函数 | — |
| 规则三元组 | severity 为2,modifier 为always | — |
| 空 workspace 仓库 | 枚举值为["global"] | empty |
普通 pnpm 仓库(包名a、b) | 枚举值为["a", "b", "global"] | basic |
scoped 仓库(包名@scope/a、@scope/b) | 枚举值仍为["a", "b", "global"](前缀被剥离) | scoped |
其中 basic 的 workspace 声明 为packages: ['packages/*'],对应包的name见 packages/a/package.json("name": "a");而 scoped fixture 的包 声明为"name": "@scope/a",验证了「scoped 包名取/后段」的行为。
七、同类配置横向对比与选型
commitlint 仓库中还有多个基于 monorepo 结构生成 scope 的共享配置,可以从源码结构上对比各自的适用场景:
| 配置 | 依赖的仓库元数据 | 适用场景 |
|---|---|---|
| @commitlint/config-pnpm-scopes | pnpm-workspace.yaml | pnpm workspace 项目 |
| @commitlint/config-lerna-scopes | lerna.json的packages字段(并向后兼容 npm/yarn workspaces) | lerna 管理的 monorepo |
| @commitlint/config-workspace-scopes | 原生 npm/yarn workspaces 声明 | 非 pnpm/lerna 的 workspace 项目 |
| @commitlint/config-nx-scopes | nx.json/ project graph | Nx 管理的仓库 |
| @commitlint/config-rush-scopes | rush.json | Rush 管理的仓库 |
如果你的项目使用 pnpm workspace,直接选用本文介绍的配置即可;如果用的是原生 npm/yarn workspaces,则应考虑@commitlint/config-workspace-scopes(lerna 配置在检测到原生 workspaces 时也会输出迁移提示,见 config-lerna-scopes 源码)。
八、小结
@commitlint/config-pnpm-scopes通过「动态计算scope-enum枚举值」的方式,把 pnpm workspace 包名与提交规范强绑定:接入只需一条安装命令和一行extends配置;其实现(index.ts)展示了读取pnpm-workspace.yaml、规范化 glob 模式、遍历包清单、剥离 scoped 前缀并追加global的完整链路,且行为由 index.test.ts 中的多组 fixture 用例锁定。对于以 pnpm 组织 monorepo 的团队,这是让 commitlint scope 校验与仓库结构保持同步的即插即用方案。
【免费下载链接】commitlint📓 Lint commit messages项目地址: https://gitcode.com/gh_mirrors/co/commitlint
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考