Monorepo 提效指南:如何配置差异化 pre-commit 增量校验?
在 Monorepo(单仓多包)架构中,随着项目数量的增加,开发者往往会面临一个尴尬的问题:每次提交代码时,Git Hooks 触发的 lint 或测试脚本会对整个仓库进行扫描。即使你只改动了 packages/user-api 的一个小函数,系统却在疯狂校验 packages/admin-dashboard。
这种“全量校验”不仅浪费时间,更挑战开发者的耐心。本文将分享如何通过 Husky + lint-staged 实现精准的差异化校验,确保“谁变动,校验谁”。
一、 核心架构思路
在一个标准的 Monorepo 中,.git 文件夹位于根目录。因此,所有的 Git Hooks(pre-commit 等)本质上都是在根目录上下文执行的。
要实现差异化校验,核心逻辑不是在每个子包里装一个 Husky,而是在根目录统一调度,通过**文件路径匹配(Glob Patterns)**将校验任务分发给对应的子包。
二、 基础环境搭建
首先,我们需要在根目录安装必要工具(以 pnpm 为例):
pnpm add -w husky lint-staged
初始化 Husky:
npx husky init
这会在根目录生成 .husky/pre-commit 文件。我们需要将其内容修改为:
# .husky/pre-commit
pnpm lint-staged
三、 实现差异化校验的两种策略
方案 A:根目录统一分发(推荐,维护成本低)
在根目录创建 .lintstagedrc.js。利用 lint-staged 的路径匹配能力,为不同目录指定不同的指令。
// .lintstagedrc.js
module.exports = {
// 针对 apps/web 目录下的 TypeScript 文件
'apps/web/**/*.{ts,tsx}': (filenames) => {
return `pnpm --filter ./apps/web exec eslint ${filenames.join(' ')} --fix`;
},
// 针对 packages/ui 库的样式文件
'packages/ui/**/*.scss': (filenames) => {
return `pnpm --filter ./packages/ui exec stylelint ${filenames.join(' ')} --fix`;
},
// 全局性的 Prettier 格式化(可选)
'**/*.{json,md}': [
'prettier --write'
]
};
关键点:
- 路径前缀:通过
apps/web/**锁定了校验范围。 - pnpm --filter:使用过滤器确保指令在正确的子包上下文中执行,且能利用子包局部的
eslint配置。 - 函数式配置:当需要处理大量文件路径时,使用函数形式返回字符串指令比数组更灵活。
方案 B:级联配置文件(适用于超大型仓库)
如果你的 Monorepo 有几十个项目,根目录配置会变得非常臃肿。此时可以将逻辑拆分。
根目录
.lintstagedrc.js:module.exports = { 'apps/app-a/**/*': 'pnpm --filter app-a lint-staged', 'apps/app-b/**/*': 'pnpm --filter app-b lint-staged', };子项目
apps/app-a/package.json:{ "lint-staged": { "*.ts": "eslint --fix" } }
这种方案实现了配置解耦,每个子项目只需要关心自己的 lint-staged 规则。
四、 避坑与进阶技巧
1. 解决路径溢出问题
在 Windows 等系统下,如果一次性提交的文件过多,filenames.join(' ') 生成的命令行字符串可能会超过系统限制。
对策:可以使用 lint-staged 的默认行为,或者在函数中分批处理文件名。
2. 与 Turborepo/Nx 结合
如果你的 Monorepo 使用了 Turborepo,你可以更进一步,不使用 lint-staged 逐个文件校验,而是执行:
# 只校验受改动影响的包
turbo lint --filter=[HEAD^1]
但注意,lint-staged 的优势在于它能只校验暂存区(Staged)的代码片段,而 Turborepo 的 Task 通常是针对整个包的。对于大型项目,推荐两者结合:lint-staged 做快速语法检查,turbo 做受影响范围的集成测试。
3. 处理配置文件依赖
确保根目录执行指令时能找到子包的 node_modules。使用 pnpm exec 或 pnpm --filter <pkg> exec 是最稳妥的方式,它会自动处理环境变量中的 PATH。
五、 总结
在 Monorepo 中配置 pre-commit 并不是要在每个角落都塞入脚本,而是要收敛入口,分散执行。
- 小规模仓库:直接在根目录
.lintstagedrc.json用 Glob 区分路径。 - 中大规模仓库:根目录 lint-staged 负责分发,子包 lint-staged 负责具体执行。
- 极致性能:结合
pnpm filter或构建工具的缓存机制。
通过这种配置,你可以将提交代码时的等待时间从分钟级降低到秒级,极大地提升团队的开发体验。