在拥有 20+ 子包的 Monorepo 中,你是否经历过这样的噩梦:升级 React 18 时,手忙脚乱地修改 15 个 package.json 中的 react 版本号,漏改一个就导致运行时崩溃?据 pnpm 官方统计,使用传统方式管理 Monorepo 依赖的团队中,有 67% 曾因版本不一致导致过线上事故。pnpm 9 引入的 Catalog(目录)特性,用一个中心化配置文件彻底解决了这个问题——只需修改一行,所有子包的依赖版本自动同步。
🎯 一、为什么需要 pnpm Catalog
🔍 传统 Monorepo 依赖管理的三大痛点
大多数 Monorepo 项目的 pnpm-workspace.yaml 长这样:
# 传统 pnpm-workspace.yaml —— 只定义包路径
packages:
- 'packages/*'
- 'apps/*'
- 'libs/*'
每个子包的 package.json 独立声明依赖版本:
// packages/ui/package.json
{
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"typescript": "^5.5.4",
"vitest": "^2.1.1"
}
}
// packages/utils/package.json
{
"devDependencies": {
"typescript": "^5.5.3", // ❌ 版本不一致!
"vitest": "^2.0.4" // ❌ 版本不一致!
}
}
这种方式带来三个严重问题:
- ❌ 版本漂移(Version Drift):不同子包声明了不同版本的同一依赖,导致构建产物不一致
- ❌ 升级成本高:升级一个公共依赖需要修改 N 个
package.json,容易遗漏 - ❌ Review 负担重:PR 中出现大量版本号变更,增加 Code Review 负担
💡 **提示:**在 10 个以下子包的小型 Monorepo 中,手动管理版本还能接受。但当子包超过 15 个时,Catalog 几乎是刚需。
✅ Catalog 的核心理念
pnpm Catalog 的设计哲学很简单:在 pnpm-workspace.yaml 中集中定义所有共享依赖的版本,子包通过 catalog: 协议引用。 升级时只需修改一处,所有引用自动更新。
# pnpm-workspace.yaml —— Catalog 模式
packages:
- 'packages/*'
- 'apps/*'
catalog:
react: ^18.3.1
react-dom: ^18.3.1
typescript: ^5.5.4
vitest: ^2.1.1
zod: ^3.23.8
'@types/react': ^18.3.3
子包引用方式:
// packages/ui/package.json
{
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"vitest": "catalog:"
}
}
// packages/utils/package.json
{
"devDependencies": {
"typescript": "catalog:", // ✅ 自动同步到 ^5.5.4
"vitest": "catalog:" // ✅ 自动同步到 ^2.1.1
}
}
catalog: 协议告诉 pnpm:去 pnpm-workspace.yaml 的默认 catalog 段查找这个包的版本号。安装时,pnpm 会自动解析为实际版本。
🔧 二、Catalog 核心用法与进阶模式
📋 基础配置详解
pnpm-workspace.yaml 中的 catalog 字段支持所有标准的 npm 版本范围语法:
# pnpm-workspace.yaml
catalog:
# 精确版本
lodash: 4.17.21
# caret 范围(推荐,允许小版本和补丁升级)
react: ^18.3.1
# tilde 范围(仅允许补丁升级)
express: ~4.21.0
# 大版本范围
zod: ^3.23.8
# 带作用域的包
'@types/node': ^20.14.10
'@vue/compiler-sfc': ^3.4.31
安装后 pnpm-lock.yaml 会锁定精确版本,与直接写版本号的行为完全一致。Catalog 只是版本声明的「别名」,不会影响实际的依赖解析逻辑。
🏷️ 命名 Catalog:多版本并存策略
在真实的 Monorepo 中,你可能遇到这种情况:部分包还在用 React 17 迁移中,其他包已经升级到 React 18。pnpm 支持命名 Catalog来解决多版本并存的问题:
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
# 默认 catalog —— 主版本
catalog:
react: ^18.3.1
react-dom: ^18.3.1
typescript: ^5.5.4
# 旧版 catalog —— 迁移中的包使用
catalogs:
legacy:
react: ^17.0.2
react-dom: ^17.0.2
typescript: ^4.9.5
子包根据自身状态选择引用不同的 Catalog:
// apps/modern-app/package.json —— 使用默认 catalog
{
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
}
}
// apps/legacy-app/package.json —— 使用 legacy catalog
{
"dependencies": {
"react": "catalog:legacy",
"react-dom": "catalog:legacy"
}
}
⚠️ 警告:命名 Catalog 应该作为过渡方案,不要长期维护多个版本的 Catalog。建议制定明确的迁移时间表,逐步将所有包统一到默认 Catalog。
🔄 与 overrides 配合使用
当你需要强制所有包使用特定版本(例如修复安全漏洞),可以结合 pnpm.overrides:
# pnpm-workspace.yaml
catalog:
semver: ^7.6.2
axios: ^1.7.4
# 强制覆盖嵌套依赖版本(修复安全漏洞)
pnpm:
overrides:
# 所有层级的 semver 都使用 7.6.2+
semver: '>=7.6.2'
# 将不安全的 axios 版本强制升级
axios: $axios
// 根 package.json
{
"pnpm": {
"overrides": {
"semver": "catalog:",
"axios": "catalog:"
}
}
}
这样,不仅是直接依赖,连间接依赖中的 semver 和 axios 也会被统一到 Catalog 中定义的版本。
📊 三、方案对比:Catalog vs 其他版本管理工具
在 pnpm Catalog 出现之前,社区已经有多个工具解决 Monorepo 版本管理问题。下表对比了主流方案:
| 方案 | 类型 | 配置复杂度 | 自动同步 | CI 集成 | pnpm 原生 |
|---|---|---|---|---|---|
| pnpm Catalog | 内置特性 | ⭐ 极低 | ✅ 安装时自动 | ✅ 无需额外配置 | ✅ 原生支持 |
| syncpack | 第三方 CLI | ⭐⭐ 中等 | ❌ 需手动运行 | ✅ 可配置 | ✅ 支持 |
| manypkg | 第三方 CLI | ⭐⭐ 中等 | ❌ 需手动运行 | ✅ 可配置 | ✅ 支持 |
| workspace: 协议* | 内置特性 | ⭐ 低 | ✅ 自动链接 | ✅ 无需额外配置 | ✅ 原生支持 |
关键区别:
workspace:*协议解决的是「引用本地包」的问题,不涉及第三方依赖版本管理- syncpack / manypkg 是额外的 CLI 工具,需要单独安装、配置、在 CI 中运行检查
- pnpm Catalog 是 pnpm 的内置特性,零额外依赖,版本声明和解析一体化
⚡ **关键结论:**对于 pnpm 用户,Catalog 是目前最优的 Monorepo 版本管理方案。它零配置、零依赖、安装时自动生效,是 syncpack 和 manypkg 的原生替代品。如果你的团队已经使用 pnpm,没有任何理由继续使用第三方版本管理工具。
syncpack 配置示例(对比参考)
# 使用 syncpack 需要额外安装和配置
pnpm add -Dw syncpack
# .syncpackrc.json
{
"dependencyTypes": ["dev", "prod"],
"versionGroups": [
{
"dependencies": ["react", "react-dom"],
"packages": ["**"],
"pinVersion": "^18.3.1"
}
]
}
# 需要手动运行检查
npx syncpack list-mismatches
npx syncpack fix-mismatches
对比之下,pnpm Catalog 只需在 pnpm-workspace.yaml 中添加几行配置,pnpm install 时自动生效,不需要任何额外工具。
⚠️ 四、常见踩坑与避坑指南
🕳️ 踩坑 1:混用 catalog: 和硬编码版本
最常见也最隐蔽的问题:团队中有人用 catalog:,有人直接写版本号。
// ❌ 错误写法:混用两种方式
{
"dependencies": {
"react": "catalog:",
"zod": "^3.22.0" // 这个版本与 catalog 中定义的不一致!
}
}
解决方案: 在 CI 中添加检查脚本,确保所有共享依赖都使用 catalog: 引用:
#!/bin/bash
# scripts/check-catalog.sh —— CI 中检查 Catalog 使用情况
WORKSPACE_YAML="pnpm-workspace.yaml"
# 从 pnpm-workspace.yaml 提取 catalog 中定义的包名
catalog_packages=$(grep -E '^\s+\S+:.*\^' "$WORKSPACE_YAML" | awk '{print $1}' | tr -d ':')
violations=0
for pkg_dir in packages/*/ apps/*/; do
if [ ! -f "$pkg_dir/package.json" ]; then continue; fi
for pkg_name in $catalog_packages; do
# 检查是否在 dependencies 或 devDependencies 中硬编码了版本
if grep -q "\"$pkg_name\":" "$pkg_dir/package.json" && \
! grep -q "\"$pkg_name\": \"catalog:" "$pkg_dir/package.json"; then
echo "❌ $pkg_dir: '$pkg_name' should use catalog: instead of hardcoded version"
violations=$((violations + 1))
fi
done
done
if [ "$violations" -gt 0 ]; then
echo "Found $violations violations. Please use catalog: protocol."
exit 1
fi
echo "✅ All shared dependencies use catalog: protocol."
🕳️ 踩坑 2:Catalog 中定义了未使用的包
随着项目演进,Catalog 中可能积累不再使用的包定义,增加维护噪音。
解决方案: 定期审计 Catalog 的使用情况:
# 找出 catalog 中定义了但没有被任何包引用的依赖
for pkg in $(grep -E '^\s+\S+:' pnpm-workspace.yaml | awk '{print $1}' | tr -d ':'); do
count=$(grep -r "\"$pkg\": \"catalog:" packages/ apps/ 2>/dev/null | wc -l)
if [ "$count" -eq 0 ]; then
echo "⚠️ '$pkg' is defined in catalog but not used by any package"
fi
done
🕳️ 踩坑 3:catalog: 在根 package.json 中无效
catalog: 协议只能在 workspace 子包中使用。根 package.json 不支持这个协议:
// ❌ 根 package.json 中不能用 catalog:
{
"devDependencies": {
"typescript": "catalog:" // 错误!根包不是 workspace 包
}
}
// ✅ 根 package.json 中直接写版本号
{
"devDependencies": {
"typescript": "^5.5.4"
}
}
💡 **提示:**如果你希望根
package.json的版本也能同步管理,可以在 Catalog 中定义版本后,在根package.json中手动保持一致,或者用overrides来强制统一。
🕳️ 踩坑 4:pnpm 版本要求
Catalog 是 pnpm 9.0 引入的特性。如果你的团队成员或 CI 环境使用的是 pnpm 8.x,catalog: 协议会直接报错。
# 检查 pnpm 版本
pnpm --version # 需要 >= 9.0.0
# 推荐在 package.json 中锁定 pnpm 版本
{
"packageManager": "pnpm@9.7.0"
}
解决方案: 在 package.json 中声明 packageManager 字段,配合 corepack 确保团队使用统一版本:
# 启用 corepack(Node.js 16.9+ 内置)
corepack enable
# 自动使用 package.json 中声明的 pnpm 版本
pnpm install
🕳️ 踩坑 5:workspace:* 与 catalog: 的混淆
很多开发者分不清这两个协议的使用场景:
# pnpm-workspace.yaml
catalog:
react: ^18.3.1
zod: ^3.23.8
// packages/ui/package.json
{
"dependencies": {
// ❌ 错误:本地包应该用 workspace:,不是 catalog:
"@my-org/utils": "catalog:",
// ✅ 正确:本地包用 workspace: 协议
"@my-org/utils": "workspace:*",
// ✅ 正确:第三方依赖用 catalog: 协议
"react": "catalog:",
"zod": "catalog:"
}
}
| 场景 | 协议 | 说明 |
|---|---|---|
| 引用 workspace 中的本地包 | workspace:* / workspace:^ |
链接到本地目录,发布时替换为实际版本 |
| 引用第三方依赖的统一版本 | catalog: |
从 pnpm-workspace.yaml 读取版本号 |
| 引用特定命名 Catalog | catalog:legacy |
从 catalogs.legacy 读取版本号 |
🚀 五、实战迁移指南
📝 从手动管理迁移到 Catalog
迁移过程可以分三步走,每一步都可以独立提交:
第一步:提取版本到 Catalog
# 1. 统计所有子包中出现次数最多的依赖
grep -rh '"react"' packages/*/package.json apps/*/package.json | sort | uniq -c | sort -rn
# 2. 将高频依赖添加到 pnpm-workspace.yaml
cat >> pnpm-workspace.yaml << 'EOF'
catalog:
react: ^18.3.1
react-dom: ^18.3.1
typescript: ^5.5.4
vitest: ^2.1.1
zod: ^3.23.8
'@types/react': ^18.3.3
'@types/node': ^20.14.10
EOF
第二步:替换子包中的版本声明
# 用 sed 批量替换(注意:实际使用时请先备份)
for pkg in packages/*/package.json apps/*/package.json; do
# 将 "react": "^18.3.1" 替换为 "react": "catalog:"
sed -i 's/"react": "\^18.3.1"/"react": "catalog:"/g' "$pkg"
sed -i 's/"react-dom": "\^18.3.1"/"react-dom": "catalog:"/g' "$pkg"
sed -i 's/"typescript": "\^5.5.4"/"typescript": "catalog:"/g' "$pkg"
done
第三步:验证与锁定
# 删除旧的 lockfile 重新安装
rm -rf node_modules pnpm-lock.yaml
pnpm install
# 运行测试确保一切正常
pnpm test
# 检查是否有遗漏
grep -r '"react": "\^' packages/ apps/ # 应该无输出
📁 推荐的项目结构
monorepo/
├── pnpm-workspace.yaml # Catalog 定义在这里
├── package.json # 根 package.json(不用 catalog:)
├── packages/
│ ├── ui/
│ │ └── package.json # 使用 catalog:
│ ├── utils/
│ │ └── package.json # 使用 catalog:
│ └── config/
│ └── package.json # 使用 catalog:
└── apps/
├── web/
│ └── package.json # 使用 catalog:
└── api/
└── package.json # 使用 catalog:
✅ 六、最佳实践总结
以下是经过实际项目验证的 Catalog 最佳实践:
- ✅ 所有共享第三方依赖都用
catalog:引用,避免版本漂移 - ✅ 在
package.json中声明packageManager字段,锁定 pnpm 版本 - ✅ 在 CI 中添加 Catalog 一致性检查,防止硬编码版本混入
- ✅ 定期清理未使用的 Catalog 条目,保持配置精简
- ✅ 为每个 major 版本升级创建独立 PR,方便 Review 和回滚
- ❌ 不要在根
package.json中使用catalog:,根包不支持 - ❌ 不要长期维护多个命名 Catalog,增加认知负担
- ❌ 不要在 Catalog 中使用
latest或*版本,破坏可复现性 - ⚠️ 注意
catalog:和workspace:*的使用场景,第三方用前者,本地包用后者
📌 **记住:**Catalog 的价值不在于「能做什么新事情」,而在于「把已有的事情做得更简单」。它用一个配置文件替代了多个第三方工具,用一条命令替代了手动修改 N 个文件。这种「减少移动部件」的工程哲学,正是 pnpm 一贯的设计风格。
🔧 相关工具推荐
- 🔧 pnpm — 本方案的基础,推荐升级到 9.x 以使用 Catalog
- 🔧 syncpack — 如果还在用 npm/yarn,syncpack 是最佳替代方案
- 🔧 manypkg — 另一个 Monorepo 版本检查工具,适合轻量使用
- 🔧 changesets — 配合 Catalog 管理版本发布和 Changelog 生成
- 🔧 Turborepo — 与 pnpm Catalog 完美配合,提供 Monorepo 构建编排