pnpm 是一个快速、磁盘空间高效的 Node.js 包管理器,使用内容寻址存储和硬链接来节省磁盘空间并加速安装。与 npm 和 Yarn 不同,pnpm 创建非扁平的 node_modules 结构,强制执行严格的依赖解析,防止幻影依赖,确保项目只能访问显式声明的包。凭借内置的 monorepo 工作区支持、补丁功能和出色的 CI/CD 集成,pnpm 已成为许多生产团队和开源项目的首选包管理器。
pnpm 是一个 Node.js 包管理器,使用全局内容寻址存储和硬链接,与 npm 相比可节省高达 70% 的磁盘空间。它创建严格的非扁平 node_modules 布局以防止幻影依赖,通过 pnpm-workspace.yaml 支持 monorepo,安装速度比 npm 快 2 倍,并提供包补丁、覆盖和 side-by-side 协议等功能。
- pnpm 使用内容寻址存储,每个包的每个版本在磁盘上只存储一次,跨项目节省大量空间。
- 严格的非扁平 node_modules 结构防止幻影依赖,代码不能意外导入未在 package.json 中声明的包。
- pnpm 工作区通过 pnpm-workspace.yaml、workspace 协议和 catalogs 提供一流的 monorepo 支持。
- 安装速度比 npm 快 2 倍,特别是在有缓存的 CI 环境中。
- pnpm patch 命令允许你在不 fork 的情况下对第三方包应用补丁,通过 patchedDependencies 在仓库中跟踪。
- pnpm 完全兼容 npm 注册表,可通过 Corepack(Node.js 内置的包管理器管理工具)激活。
什么是 pnpm 和内容寻址存储?
pnpm 全称 performant npm,由 Zoltan Kochan 于 2017 年创建,旨在解决 npm 的两个根本问题:跨项目重复包导致的磁盘空间浪费和允许幻影依赖的扁平 node_modules 结构。pnpm 通过使用全局内容寻址存储结合硬链接和符号链接,引入了一种根本不同的包管理方法。
使用 pnpm 安装包时,包文件存储在单个全局存储中(通常在 ~/.local/share/pnpm/store)。每个文件按内容哈希存储,不同包版本中的相同文件只存储一次。项目的 node_modules 目录包含指向存储的硬链接而非实际文件的副本。
pnpm 创建的 node_modules 布局是非扁平的。只有 package.json 中列出的包可以直接访问。传递依赖存储在隐藏的 .pnpm 目录中。这种严格隔离防止代码导入未显式依赖的包。
# How pnpm node_modules structure works
#
# node_modules/
# .pnpm/
# react@19.0.0/
# node_modules/
# react/ -> hard links to store
# lodash@4.17.21/
# node_modules/
# lodash/ -> hard links to store
# react/ -> symlink to .pnpm/react@19.0.0/node_modules/react
# lodash/ -> symlink to .pnpm/lodash@4.17.21/node_modules/lodash
#
# Only react and lodash are visible at the top level
# because they are in your package.json.
# Transitive dependencies are isolated inside .pnpm/pnpm vs npm vs Yarn vs Bun
每个 Node.js 包管理器在速度、磁盘使用、严格性和生态支持方面有不同的权衡:
| 特性 | pnpm | npm | Yarn (Berry) | Bun |
|---|---|---|---|---|
| 磁盘使用 | 最小(内容寻址 + 硬链接) | 高(每个项目完整拷贝) | PnP 模式低(无 node_modules) | 高(每个项目完整拷贝) |
| 安装速度 | 快(平均 npm 的 2 倍) | 基准 | PnP 快,node_modules 中等 | 非常快(原生运行时) |
| 严格性 | 严格(无幻影依赖) | 宽松(扁平提升) | PnP 严格,node_modules 宽松 | 宽松(扁平提升) |
| Monorepo 支持 | 内置工作区 + catalogs | 基本工作区 | 内置工作区 + constraints | 基本工作区 |
| 锁文件 | pnpm-lock.yaml (YAML) | package-lock.json (JSON) | yarn.lock(自定义格式) | bun.lock(二进制) |
| 补丁 | 内置 pnpm patch | 需要 patch-package | 内置 yarn patch | 内置 bun patch |
安装和 Corepack 设置
推荐通过 Corepack 安装 pnpm,Corepack 是 Node.js 16.13+ 内置的包管理器版本管理工具。Corepack 确保团队中每个开发者使用完全相同的 pnpm 版本。
也可以通过 npm、Homebrew 或直接安装脚本独立安装 pnpm。安装后,pnpm 使用相同的 npm 注册表并遵循 .npmrc 配置。
# Method 1: Corepack (recommended)
corepack enable
corepack prepare pnpm@latest --activate
# Add to package.json for team-wide version pinning
# "packageManager": "pnpm@10.5.0"
# Method 2: npm
npm install -g pnpm
# Method 3: Homebrew (macOS)
brew install pnpm
# Method 4: Standalone script
curl -fsSL https://get.pnpm.io/install.sh | sh -
# Verify installation
pnpm --version
pnpm store path核心 pnpm 命令
pnpm 命令与 npm 命令非常相似,迁移很简单。关键区别在于 pnpm 默认执行更严格的行为,并提供额外的 monorepo 管理命令。
安装依赖
pnpm install 命令读取 package.json 并安装所有依赖。它创建或更新 pnpm-lock.yaml 并设置内容寻址的 node_modules 结构。
# Install all dependencies from package.json
pnpm install
# Install with frozen lockfile (CI mode)
pnpm install --frozen-lockfile
# Install production dependencies only
pnpm install --prod添加和移除包
使用 pnpm add 添加新包,pnpm remove 卸载包。pnpm add 支持与 npm install 相同的标志。
# Add a production dependency
pnpm add react react-dom
# Add a dev dependency
pnpm add -D typescript @types/react
# Add a peer dependency
pnpm add --save-peer react
# Add an optional dependency
pnpm add -O fsevents
# Add a specific version
pnpm add lodash@4.17.21
# Remove a package
pnpm remove lodash
# Remove from a specific workspace package
pnpm --filter @myorg/web remove lodash更新包
pnpm update 检查已安装包的新版本。使用 --latest 标志忽略 semver 范围并更新到最新版本。
# Update all packages within semver ranges
pnpm update
# Update to latest versions (ignore semver)
pnpm update --latest
# Update a specific package
pnpm update react
# Interactive update (select which to update)
pnpm update --interactive
# Update recursively in all workspaces
pnpm -r update运行脚本
pnpm run 执行 package.json 中定义的脚本。与 npm 不同,pnpm 对生命周期脚本更严格。
# Run a script
pnpm run build
pnpm run dev
pnpm run test
# Shorthand (no "run" needed for common scripts)
pnpm build
pnpm dev
pnpm test
# Run in all workspace packages
pnpm -r run build
# Run in a specific workspace package
pnpm --filter @myorg/api run build
# Run a binary from node_modules/.bin
pnpm exec tsc --version
pnpm dlx create-next-app@latest # like npx使用 pnpm-workspace.yaml 的 Monorepo 工作区
pnpm 通过工作区功能提供一流的 monorepo 支持。在仓库根目录的 pnpm-workspace.yaml 文件中定义工作区包。
workspace 协议(workspace:*)让你引用 monorepo 中的其他包。pnpm 在开发时解析为本地版本,发布时替换为实际版本范围。
# pnpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"
- "tools/*"
# With catalogs for shared versions
catalog:
react: ^19.0.0
react-dom: ^19.0.0
typescript: ^5.7.0
vitest: ^3.0.0// apps/web/package.json
{
"name": "@myorg/web",
"dependencies": {
"@myorg/ui": "workspace:*",
"@myorg/utils": "workspace:^",
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"vitest": "catalog:"
}
}
// workspace:* -> resolves to local package during dev
// workspace:^ -> replaced with ^1.2.3 when publishing
// catalog: -> resolves to version from pnpm-workspace.yaml# Workspace filtering commands
# Run build in a specific package
pnpm --filter @myorg/web build
# Run build in package and all its dependencies
pnpm --filter @myorg/web... build
# Run tests in all packages that depend on @myorg/ui
pnpm --filter ...@myorg/ui test
# Run in all packages matching a pattern
pnpm --filter "@myorg/*" build
# Run in packages changed since main branch
pnpm --filter "...[origin/main]" build
# Add a dependency to a specific workspace
pnpm --filter @myorg/api add express.npmrc 配置
pnpm 从项目、用户和全局级别的 .npmrc 文件读取配置。多个 pnpm 特定设置控制依赖解析和存储行为。
# .npmrc - pnpm configuration
# Strict mode (recommended)
strict-peer-dependencies=true
auto-install-peers=true
# Do not hoist (default, keeps strict isolation)
shamefully-hoist=false
# Hoist specific packages that need flat access
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*
# Store location (default: ~/.local/share/pnpm/store)
# store-dir=~/.pnpm-store
# Use lockfile v9 format
lockfile-format=v9
# Registry (default: https://registry.npmjs.org/)
# registry=https://registry.npmmirror.com/
# Node.js version for pnpm env
use-node-version=22.14.0严格依赖解析
pnpm 最重要的特性之一是严格的依赖解析。在 npm 扁平布局中,依赖树中的任何包都可从应用代码导入,即使你没有显式声明。这导致幻影依赖。
pnpm 通过创建只有直接依赖在顶层可见的 node_modules 结构来解决这个问题。如果尝试导入未列在 package.json 中的包,Node.js 会抛出 MODULE_NOT_FOUND 错误。
// Example: Phantom dependency problem
// package.json only declares "express"
// But code imports "debug" (a transitive dep of express)
// With npm (works by accident):
const debug = require("debug"); // OK - hoisted to root
// With pnpm (fails correctly):
// Error: Cannot find module 'debug'
// MODULE_NOT_FOUND
// Fix: explicitly add the dependency
// pnpm add debug
// Now it works correctly with pnpm:
const debug = require("debug"); // OK - declared in package.json补丁第三方包
有时你需要修复依赖中的 bug 而不等待上游发布。pnpm 提供内置的补丁机制。
pnpm patch 命令将包提取到临时目录进行修改。编辑后,pnpm patch-commit 创建 diff 文件并添加 patchedDependencies 条目。
# Step 1: Start patching a package
pnpm patch lodash@4.17.21
# Output: You can now edit the package at:
# /tmp/xxxx/node_modules/lodash
# Step 2: Make your changes in the temp directory
# Edit the files you need to fix
# Step 3: Commit the patch
pnpm patch-commit /tmp/xxxx/node_modules/lodash
# This creates patches/lodash@4.17.21.patch
# and adds to package.json:
# "pnpm": {
# "patchedDependencies": {
# "lodash@4.17.21": "patches/lodash@4.17.21.patch"
# }
# }
# Remove a patch
pnpm patch-remove lodash@4.17.21覆盖和钩子
pnpm 支持覆盖来强制指定传递依赖的版本。覆盖在 pnpm.overrides 字段中声明。
pnpm 支持 .pnpmfile.cjs 钩子文件,允许在解析期间修改包元数据。
// package.json - overrides
{
"pnpm": {
"overrides": {
"lodash": "^4.17.21",
"got@<11.8.5": ">=11.8.5",
"express>debug": "~4.3.0",
"node-fetch": "npm:undici"
}
}
}// .pnpmfile.cjs - hooks for advanced customization
function readPackage(pkg, context) {
// Fix missing peer dependency
if (pkg.name === "some-broken-package") {
pkg.dependencies = {
...pkg.dependencies,
"missing-peer": "^2.0.0"
};
context.log("Fixed missing peer dep for " + pkg.name);
}
// Force a specific version of a transitive dependency
if (pkg.dependencies && pkg.dependencies["old-package"]) {
pkg.dependencies["old-package"] = "^3.0.0";
}
return pkg;
}
module.exports = {
hooks: {
readPackage
}
};理解 pnpm-lock.yaml
pnpm-lock.yaml 记录每个已安装包的确切版本、完整性哈希和依赖关系。YAML 格式比 JSON 更可读,产生更小的版本控制差异。
锁文件包含将包标识符映射到解析版本和完整性校验的 packages 部分,确保整个依赖图可确定性重建。
# pnpm-lock.yaml structure (simplified)
lockfileVersion: "9.0"
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
react:
specifier: ^19.0.0
version: 19.0.0
devDependencies:
typescript:
specifier: ^5.7.0
version: 5.7.3
packages:
react@19.0.0:
resolution:
integrity: sha512-abc123...
engines:
node: ">=18"
# Useful commands:
# pnpm install --frozen-lockfile (CI - fail if lockfile outdated)
# pnpm install --lockfile-only (update lockfile without installing)
# pnpm dedupe (reduce duplicate versions)CI/CD 使用
pnpm 非常适合 CI/CD 管道。pnpm install --frozen-lockfile 确保锁文件不被修改。结合缓存 pnpm 存储,CI 构建可实现近乎即时的依赖安装。
大多数 CI 提供商原生支持 pnpm 或通过简单设置步骤支持。GitHub Actions 有官方的 pnpm/action-setup。
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run lint
- run: pnpm run test
- run: pnpm run buildDocker 集成
在 Docker 容器中使用 pnpm 时,可以利用内容寻址存储实现高效的层缓存。推荐先只复制锁文件和工作区配置,安装依赖,然后复制源代码。
# Dockerfile with pnpm
FROM node:22-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="\$PNPM_HOME:\$PATH"
RUN corepack enable
FROM base AS deps
WORKDIR /app
# Copy only files needed for install (better caching)
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \\
pnpm install --frozen-lockfile
FROM base AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm run build
FROM base AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]从 npm 或 Yarn 迁移
从 npm 或 Yarn 迁移到 pnpm 很简单。pnpm 可以导入两个包管理器的现有锁文件。主要挑战是修复幻影依赖。
- 1. 安装 pnpm 并删除旧锁文件。
- 2. 运行 pnpm import 转换旧锁文件(可选)。
- 3. 运行 pnpm install 创建严格的 node_modules 结构。
- 4. 修复 MODULE_NOT_FOUND 错误,显式添加缺失依赖。
- 5. 更新 CI/CD 脚本使用 pnpm 命令。
- 6. 在 package.json 中添加 packageManager 字段。
# Migration from npm
corepack enable
pnpm import # converts package-lock.json
rm package-lock.json
pnpm install # creates node_modules + pnpm-lock.yaml
# Migration from yarn
corepack enable
pnpm import # converts yarn.lock
rm yarn.lock
pnpm install
# Add packageManager field
# package.json:
# "packageManager": "pnpm@10.5.0"
# Update scripts in package.json
# "scripts": {
# "preinstall": "npx only-allow pnpm"
# }性能基准
pnpm 在安装速度基准测试中始终优于 npm,尤其是有缓存的场景。典型的 500+ 依赖项目中,有缓存时 pnpm 比 npm 快 2-3 倍,冷缓存快 20-40%。
内容寻址存储意味着使用相同依赖版本的第二个项目零下载和接近零的磁盘开销。
# Benchmark: 500-dependency project (approximate times)
#
# Cold cache (first install):
# npm install ~45s
# yarn install ~38s
# pnpm install ~30s
# bun install ~12s
#
# Warm cache (store populated):
# npm install ~20s
# yarn install ~12s
# pnpm install ~8s
# bun install ~4s
#
# Disk usage (10 identical projects):
# npm: ~1.5 GB (10 copies)
# pnpm: ~150 MB (1 copy + hard links)
#
# Run your own benchmark:
# hyperfine "pnpm install" "npm install" --prepare "rm -rf node_modules"Side-by-Side 协议和 Catalogs
workspace 协议(workspace:*)确保本地包始终从工作区解析。发布时 pnpm 自动替换为实际版本号。
Catalogs 是 pnpm 工作区功能,允许在中心位置定义共享依赖版本,通过 catalog: 协议引用。
# pnpm-workspace.yaml with named catalogs
packages:
- "packages/*"
- "apps/*"
# Default catalog
catalog:
react: ^19.0.0
react-dom: ^19.0.0
typescript: ^5.7.0
# Named catalogs for different groups
catalogs:
testing:
vitest: ^3.0.0
"@testing-library/react": ^16.0.0
linting:
eslint: ^9.0.0
prettier: ^3.4.0// packages/ui/package.json
{
"name": "@myorg/ui",
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"vitest": "catalog:testing",
"eslint": "catalog:linting"
}
}
// Workspace protocol variants:
// "workspace:*" -> exact local version
// "workspace:^" -> ^major.minor.patch when published
// "workspace:~" -> ~major.minor.patch when published最佳实践
- 始终将 pnpm-lock.yaml 提交到版本控制,确保可重复构建。
- 使用 Corepack 和 packageManager 字段固定 pnpm 版本。
- 在 .npmrc 中设置 strict-peer-dependencies=true 以尽早发现对等依赖冲突。
- 使用 pnpm --filter 在 monorepo 中限定命令范围。
- 在 CI 中缓存 pnpm 存储目录以加速后续构建。
- 定期运行 pnpm dedupe 减少重复包版本。
- 保持 shamefully-hoist=false(默认值)以维持严格依赖隔离。
- 对于小修复优先使用 pnpm patch 而非 fork 依赖。
- 在 monorepo 中使用 catalogs 集中管理共享依赖版本。
- 定期运行 pnpm audit 并使用 overrides 强制升级有漏洞的传递依赖。
常见问题
什么是 pnpm,它和 npm 有什么不同?
pnpm 是一个 Node.js 包管理器,使用内容寻址存储和硬链接而非将文件复制到每个项目中。这节省了大量磁盘空间并加速安装。与 npm 不同,pnpm 创建严格的非扁平 node_modules 结构,防止幻影依赖。
pnpm 如何节省磁盘空间?
pnpm 将所有包文件存储在全局内容寻址存储中,按内容哈希标识。需要包时创建硬链接而非复制文件。10 个项目使用同一版本的 React 时,文件只在磁盘上存在一份。
可以在现有 npm 项目中使用 pnpm 吗?
可以。pnpm 完全兼容 npm 注册表和 package.json 格式。可以运行 pnpm import 转换 package-lock.json,然后运行 pnpm install。
什么是幻影依赖,为什么 pnpm 要防止它?
幻影依赖是代码导入但未列在 package.json 中的包。它们在 npm 中工作是因为扁平提升。pnpm 只暴露显式声明的包,在开发时就捕获这些问题。
pnpm 工作区和 npm 工作区有什么区别?
pnpm 工作区提供更多功能,包括 workspace: 协议、--filter 标志、catalogs 共享版本和 pnpm -r 递归命令。
如何用 Corepack 设置 pnpm?
运行 corepack enable,然后在 package.json 中添加 packageManager 字段指定 pnpm 版本。Corepack 会自动下载和使用该版本。
pnpm 兼容所有 npm 包吗?
pnpm 兼容绝大多数 npm 包。少数兼容性问题出现在依赖扁平布局的包上,可以使用 shamefully-hoist 或 public-hoist-pattern 设置解决。
pnpm 和 npm、Yarn 相比有多快?
有缓存时 pnpm 比 npm 快 2-3 倍,冷缓存快 20-40%。与 Yarn node_modules 模式速度相当。Bun 最快但 pnpm 在速度、严格性和磁盘效率之间提供最佳平衡。