pnpm Catalog 实战:Monorepo 依赖版本统一管理完全指南

pnpm 9 引入的 Catalog 特性彻底改变了 Monorepo 依赖管理方式。本文深入讲解 catalog 配置、命名分类、跨包引用、迁移策略与踩坑经验,附完整代码示例与工具对比。

前端开发 2026-06-12 18 分钟

在拥有 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:"
    }
  }
}

这样,不仅是直接依赖,连间接依赖中的 semveraxios 也会被统一到 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 构建编排

📚 相关文章