news 2026/9/20 22:50:52

@commitlint/config-pnpm-scopes:为 pnpm workspace 仓库自动生成 scope 枚举的 commitlint 共享配置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
@commitlint/config-pnpm-scopes:为 pnpm workspace 仓库自动生成 scope 枚举的 commitlint 共享配置

@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规则的枚举值。

该配置的定位是:

Shareablecommitlintconfig enforcing pnpm workspaces names as scopes.

它需要配合 @commitlint/cli(命令行校验)与 @commitlint/prompt-cli(交互式提交工具)一起使用。包的元信息见 package.json:模块类型为 ESM("type": "module"),声明了 Node.js>=22.12.0的引擎要求,运行时依赖@pnpm/read-project-manifestread-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

核心步骤拆解:

  1. 安装两个包@commitlint/config-pnpm-scopes提供规则,@commitlint/cli提供commitlint命令行入口;
  2. 写入配置文件:在commitlint.config.js中通过extends引入该共享配置,module.exports采用 CommonJS 导出,与 commitlint 的常规配置加载机制一致。

之后的提交校验会按 Configuration guide 中描述的流程加载配置:extends中的配置项会被递归合并进当前配置,而本包通过extends暴露的rulesutils会一并生效。

提示:若想同时保留其他约定(如 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 匹配路径

normalizePatternspnpm-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.jsonpackage.json5package.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_modulesbower_components,避免把依赖目录误判为工作区包;
  • 去重:通过Set对匹配到的 manifest 路径去重,防止多个模式命中同一文件;
  • 精确解析:使用@pnpm/read-project-manifestreadExactProjectManifest解析每个 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(); }); }

四条关键规则:

  1. 忽略无name字段的包:manifest 中没有name的包不会进入 scope 白名单;
  2. scoped 包名取/后段:对于@scope/a这种 npm scoped 包名,只保留a作为合法 scope——提交时写a(...)而非@scope/a(...),这与 commitlint 的 scope 语法兼容性更好(该处理方式与 @commitlint/config-lerna-scopes 等同类配置保持一致);
  3. 始终追加global:无论仓库里有多少包,global永远是一个合法 scope,用于表达「与具体包无关的全局性改动」;
  4. 排序输出:最终列表按字典序排序,保证枚举值稳定、可读。

3.5 执行目录从哪来

getProjects(context)的上下文ctx若提供了cwd则以其为扫描根目录,否则回退到process.cwd()。这意味着在 @commitlint/cli 的--cwd参数、lint 阶段传入的上下文等因素影响下,配置会基于实际执行目录发现工作区包。

四、运行示例:实际校验效果

原文档给出了一个完整的三段式示例。假设commitlint.config.js内容为:

{ extends: ['@commitlint/config-pnpm-scopes'] }

仓库结构如下(三个包:apiappweb):

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 warnings

foo不在[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-enumconfig.rules["scope-enum"]为函数
规则三元组severity 为2,modifier 为always
空 workspace 仓库枚举值为["global"]empty
普通 pnpm 仓库(包名ab枚举值为["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-scopespnpm-workspace.yamlpnpm workspace 项目
@commitlint/config-lerna-scopeslerna.jsonpackages字段(并向后兼容 npm/yarn workspaces)lerna 管理的 monorepo
@commitlint/config-workspace-scopes原生 npm/yarn workspaces 声明非 pnpm/lerna 的 workspace 项目
@commitlint/config-nx-scopesnx.json/ project graphNx 管理的仓库
@commitlint/config-rush-scopesrush.jsonRush 管理的仓库

如果你的项目使用 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),仅供参考

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

大健康私域运营:基于企业微信的智能医患管理平台实战

简介&#xff1a;PDF文档《大健康行业私域流量数智化解决方案》面向医药、民营医院、医美、保险、保健品等企业的运营与管理人员&#xff0c;系统阐述基于企业微信的智能医患管理服务平台建设路径。文档从行业背景、方案架构到场景部署层层展开&#xff0c;清晰呈现AISCRM双引擎…

作者头像 李华
网站建设 2026/9/20 22:48:02

如何用Open Mercato AI Playground调试智能体:Playground完整指南

如何用Open Mercato AI Playground调试智能体&#xff1a;Playground完整指南 【免费下载链接】open-mercato The AI-Engineering Foundation Framework for CRM/ERP and commerce: open-source TypeScript, with multi-tenancy, RBAC, events and domain modules already deci…

作者头像 李华
网站建设 2026/9/20 22:47:35

Claude Code vs Codex:同一把 TaoToken Key 跑 AES-GCM 封装

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 22:45:17

ChatTTS-ui 语音合成音色定制:10分钟拿到3种选音色方法

ChatTTS-ui 语音合成音色定制&#xff1a;10分钟拿到3种选音色方法 【免费下载链接】ChatTTS-ui 一个简单的本地网页界面&#xff0c;使用ChatTTS将文字合成为语音&#xff0c;同时支持对外提供API接口。A simple native web interface that uses ChatTTS to synthesize text i…

作者头像 李华
网站建设 2026/9/20 22:45:13

油猴脚本装完不生效?从匹配规则到CSP的完整排查指南

油猴脚本装好了&#xff0c;脚本也显示“安装成功”&#xff0c;打开网页却一动不动——这个情况我见得太多了。不管是 Tampermonkey 还是 Violentmonkey&#xff0c;凡是折腾过用户脚本的人&#xff0c;十有八九都栽过这个跟头。明明安装步骤没毛病&#xff0c;油猴扩展也在工…

作者头像 李华