跳到主要内容

pnpm

pnpm 是一个高效、快速的 JavaScript 包管理器,由 npm 的早期开发者 Zoltan Kochan 创建。它使用硬链接和符号链接来共享依赖,显著节省磁盘空间。

主要特点

  • 节省磁盘空间:通过内容寻址存储,相同的依赖只存储一份
  • 快速安装:比 npm 和 yarn 更快,依赖链接而非复制
  • 严格的依赖管理:防止"幽灵依赖",只能访问 package.json 中声明的依赖
  • 确定性:生成锁文件确保跨环境一致性
  • 支持单体仓库:内置 workspace 支持

安装

# 使用 npm 安装
npm install -g pnpm

# 使用 Corepack(Node.js 16.13+)
corepack enable
corepack prepare pnpm@latest --activate

# 使用安装脚本(Windows PowerShell)
iwr https://get.pnpm.io/install.ps1 -useb | iex

# 使用 Homebrew(macOS)
brew install pnpm

基本使用

初始化项目

# 创建 package.json
pnpm init

# 使用默认值
pnpm init --ignore-scripts

安装依赖

# 安装所有依赖
pnpm install

# 安装生产依赖
pnpm add lodash

# 安装指定版本
pnpm add [email protected]

# 安装开发依赖
pnpm add -D jest
pnpm add --save-dev jest

# 安装可选依赖
pnpm add -O eslint

# 全局安装
pnpm add -g typescript

# 安装精确版本
pnpm add --save-exact lodash

# 安装到特定 workspace
pnpm add lodash --filter @myproject/core

# 安装 peer 依赖(同时添加到 devDependencies)
pnpm add --save-peer react

# 安装到 catalog(v10.12.1+)
pnpm add lodash --save-catalog

# 安装到指定的命名 catalog
pnpm add lodash --save-catalog-name react18

# 允许特定包运行 postinstall 脚本(v10.4.0+)
pnpm add --allow-build=esbuild my-bundler

平台特定选项(v10.14.0+)

pnpm 支持覆盖原生模块的目标平台,这对于交叉编译场景非常有用:

# 覆盖 CPU 架构
pnpm install --cpu=arm64
pnpm install --cpu=x64

# 覆盖操作系统
pnpm install --os=linux
pnpm install --os=darwin
pnpm install --os=win32

# 覆盖 libc 实现
pnpm install --libc=glibc
pnpm install --libc=musl

# 组合使用(例如为 Linux ARM64 安装依赖)
pnpm install --os=linux --cpu=arm64

这些选项对应的值与 package.json 中的 cpuoslibc 字段一致。--cpu 接受 process.arch 的值,--os 接受 process.platform 的值。

删除依赖

# 删除依赖
pnpm remove lodash

# 删除开发依赖
pnpm remove -D jest

# 删除全局依赖
pnpm remove -g typescript

# 从所有 workspace 删除
pnpm remove lodash --recursive

更新依赖

# 检查可更新的依赖
pnpm outdated

# 更新依赖
pnpm update

# 更新到最新版本(忽略版本范围)
pnpm update --latest

# 更新特定包
pnpm update lodash

# 交互式更新
pnpm update --interactive

node_modules 结构

pnpm 使用独特的 node_modules 结构:

node_modules/
├── .pnpm/ # 所有包的硬链接存储
│ ├── [email protected]/
│ │ └── node_modules/
│ │ └── lodash/ # 实际包内容
│ └── ...
├── lodash -> .pnpm/[email protected]/node_modules/lodash # 符号链接
└── .modules.yaml

优点

  • 防止访问未声明的依赖
  • 节省磁盘空间
  • 安装速度快

兼容性设置

如果需要兼容 npm 的扁平化结构:

# .npmrc
shamefully-hoist=true
node-linker=hoisted

pnpm-lock.yaml

pnpm 使用 pnpm-lock.yaml 锁定依赖版本。

lockfileVersion: '6.0'

importers:
.:
dependencies:
lodash:
specifier: ^4.17.21
version: 4.17.21

packages:
/[email protected]:
resolution: {integrity: sha512-v3k...}
dev: false

配置管理

.npmrc 文件

# 设置镜像源
registry=https://registry.npmmirror.com

# 兼容性设置
shamefully-hoist=true
strict-peer-dependencies=false

# 存储位置
store-dir=~/.pnpm-store

# 缓存设置
cache-dir=~/.pnpm-cache

常用配置命令

# 查看所有配置
pnpm config list

# 查看特定配置
pnpm config get registry

# 设置配置
pnpm config set registry https://registry.npmmirror.com

# 设置全局配置
pnpm config set registry https://registry.npmmirror.com --global

# 删除配置
pnpm config delete registry

工作区 (Workspaces)

pnpm 原生支持单体仓库管理。

配置

# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
- '!**/test/**'

workspace 命令

# 在所有包中执行命令
pnpm -r run build

# 在特定包中执行命令
pnpm --filter @myproject/core run build

# 在包及其依赖中执行
pnpm --filter @myproject/core... run build

# 在包及其依赖者中执行
pnpm --filter ...@myproject/core run build

# 并行执行
pnpm -r --parallel run test

# 按拓扑顺序执行
pnpm -r --workspace-concurrency=4 run build

workspace 协议

{
"dependencies": {
"@myproject/core": "workspace:*",
"@myproject/utils": "workspace:^"
}
}
协议说明
workspace:*使用精确版本
workspace:^使用兼容版本
workspace:~使用近似版本
workspace:^1.0.0指定版本范围

脚本命令

定义脚本

{
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"build": "webpack --mode production"
}
}

运行脚本

# 运行脚本
pnpm run start
pnpm start # start 可省略 run

# 传递参数
pnpm run test -- --coverage

# 并行运行
pnpm -r run test

# 按拓扑顺序运行
pnpm -r run build

发布包

# 登录
pnpm login

# 发布前检查
pnpm pack --dry-run

# 发布
pnpm publish

# 发布到指定镜像源
pnpm publish --registry https://registry.npmjs.org

# 发布 scoped 包
pnpm publish --access public

# 发布 beta 版本
pnpm publish --tag beta

常用命令速查

命令说明
pnpm init初始化项目
pnpm install安装所有依赖
pnpm add <pkg>安装依赖
pnpm add -D <pkg>安装开发依赖
pnpm remove <pkg>删除依赖
pnpm update更新依赖
pnpm outdated检查过期依赖
pnpm run <script>运行脚本
pnpm list查看已安装依赖
pnpm list -g --depth=0查看全局依赖
pnpm why <pkg>查看依赖原因
pnpm store prune清理未使用的包
pnpm store path查看存储位置
pnpm doctor检查环境
pnpm rebuild重新构建依赖
pnpm fetch仅下载依赖到存储

与 npm 命令对比

npmpnpm
npm installpnpm install
npm install <pkg>pnpm add <pkg>
npm install -D <pkg>pnpm add -D <pkg>
npm uninstall <pkg>pnpm remove <pkg>
npm run <script>pnpm run <script>
npm listpnpm list
npm outdatedpnpm outdated
npm updatepnpm update
npm cipnpm install --frozen-lockfile
npm dedupe不需要(pnpm 自动优化)
npx <cmd>pnpm dlx <cmd>

Side Effects Cache

pnpm 的 Side Effects Cache 是一个独特的功能,可以缓存包构建的副作用结果,进一步加速安装过程。

工作原理

当安装包含构建脚本的包时,pnpm 可以缓存构建结果。下次安装相同版本的包时,直接使用缓存,无需重新构建。

配置

# .npmrc
side-effects-cache=true

优势

  • 加速包含原生模块的包安装
  • 减少构建工具的重复执行
  • 对于 CI 环境特别有价值

Hooks

pnpm 支持钩子系统,允许在特定生命周期执行自定义操作。

配置 Hooks

# pnpm-workspace.yaml
packages:
- 'packages/*'

hooks:
# 安装后执行
postinstall: "node scripts/postinstall.js"

# 读取包时执行
readPackage: "node scripts/read-package.js"

# 过滤依赖
filter: "node scripts/filter.js"

readPackage Hook

可以在读取 package.json 时修改包的配置:

// scripts/read-package.js
module.exports = {
hooks: {
readPackage(pkg, context) {
// 修改依赖版本
if (pkg.dependencies && pkg.dependencies.lodash) {
pkg.dependencies.lodash = '^4.17.21';
}

// 移除不必要的依赖
if (pkg.devDependencies) {
delete pkg.devDependencies['some-unnecessary-package'];
}

return pkg;
}
}
};

afterAllResolved Hook

在所有依赖解析完成后执行:

module.exports = {
hooks: {
afterAllResolved(lockfile, context) {
console.log('All dependencies resolved!');
return lockfile;
}
}
};

Auto Install

pnpm 支持在运行脚本前自动安装缺失的依赖。

配置

# .npmrc
auto-install-peers=true

使用场景

当你执行 pnpm run dev 但依赖尚未安装时,pnpm 可以自动安装依赖:

# 自动安装依赖后运行脚本
pnpm run dev

pnpm dlx

类似于 npx,用于执行包命令:

# 创建项目
pnpm dlx create-react-app my-app
pnpm dlx create-vite my-app

# 使用特定版本
pnpm dlx [email protected] my-app

# 从 GitHub 执行
pnpm dlx github:user/repo

最佳实践

1. 使用 .npmrc 配置

# 自动安装 peer dependencies
auto-install-peers=true

# 严格的 peer dependencies 检查
strict-peer-dependencies=true

# 提升依赖(兼容旧项目)
shamefully-hoist=true

2. 使用 Catalogs 管理版本

Catalogs 是 pnpm 的一个强大功能,允许在 monorepo 根目录定义依赖版本,然后在所有 workspace 包中引用。这确保了整个仓库中相同依赖使用一致的版本。

配置 Catalogs

# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'

# 默认 catalog
catalog:
react: ^18.2.0
react-dom: ^18.2.0
typescript: ^5.0.0
jest: ^29.5.0
eslint: ^8.0.0

# 命名 catalog(用于不同场景)
catalogs:
# 默认 catalog(可省略名称)
default:
react: ^18.2.0
typescript: ^5.0.0

# React 19 catalog(用于实验性项目)
react19:
react: ^19.0.0
react-dom: ^19.0.0

# 旧版本 catalog(用于遗留项目)
legacy:
react: ^17.0.0
typescript: ^4.0.0

使用 Catalogs

// packages/core/package.json
{
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"jest": "catalog:"
}
}

使用命名 Catalog

// packages/experimental/package.json(使用 React 19)
{
"dependencies": {
"react": "catalog:react19",
"react-dom": "catalog:react19"
}
}

// packages/legacy/package.json(使用旧版本)
{
"dependencies": {
"react": "catalog:legacy"
}
}

Catalog 协议语法

协议说明
catalog:使用默认 catalog 中的版本
catalog:default显式指定默认 catalog
catalog:name使用命名 catalog

实际场景示例

# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
- 'tools/*'

catalog:
# 构建工具
typescript: ^5.3.0
esbuild: ^0.20.0
vite: ^5.0.0

# 测试工具
jest: ^29.7.0
vitest: ^1.2.0

# 代码质量
eslint: ^8.56.0
prettier: ^3.2.0

# React 生态
react: ^18.2.0
react-dom: ^18.2.0
next: ^14.1.0
// packages/ui/package.json
{
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"eslint": "catalog:",
"prettier": "catalog:"
}
}

// apps/web/package.json
{
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:",
"next": "catalog:"
},
"devDependencies": {
"typescript": "catalog:",
"vitest": "catalog:"
}
}

Catalog 的优势

  1. 版本一致性:确保整个 monorepo 中相同依赖使用相同版本
  2. 简化维护:只需在一个地方更新版本号
  3. 减少冲突:避免不同包使用不同版本导致的兼容性问题
  4. 清晰可读:一眼就能看出项目使用了哪些版本的依赖

更新 Catalog 版本

# 更新 catalog 中的包版本
pnpm update react --recursive

# 查看哪些包使用了 catalog
pnpm list --depth=0

3. 使用 onlyBuiltDependencies

限制哪些依赖可以执行构建脚本,提高安全性:

{
"pnpm": {
"onlyBuiltDependencies": ["esbuild"]
}
}

4. 配置 workspace 注入方式

pnpm 提供了灵活的 workspace 依赖处理方式:

injectWorkspacePackages

启用硬链接代替符号链接,适用于需要实际文件副本的场景:

# .npmrc
inject-workspace-packages=true

或通过 dependenciesMeta 选择性启用:

{
"dependenciesMeta": {
"@myorg/core": {
"injected": true
}
}
}

dedupeInjectedDeps

当启用注入时,pnpm 会尝试通过符号链接去重注入的依赖:

# .npmrc
dedupe-injected-deps=true

syncInjectedDepsAfterScripts

对于 TypeScript 等需要构建的项目,注入的依赖是硬链接集合,源文件变化时不会自动同步。可以配置在特定脚本执行后同步:

# .npmrc
sync-injected-deps-after-scripts[]=build
sync-injected-deps-after-scripts[]=compile

5. 循环依赖处理

在 workspace 中检测到循环依赖时,pnpm 会发出警告。可以配置处理方式:

# .npmrc
# 忽略循环依赖警告
ignore-workspace-cycles=true

# 或严格禁止循环依赖(安装失败)
disallow-workspace-cycles=true

4. 安全审计

# 检查安全漏洞
pnpm audit

# 自动修复
pnpm audit --fix

5. 清理存储

# 清理未使用的包
pnpm store prune

# 检查存储状态
pnpm store status

pnpm 10 新特性

pnpm 10 是一个重大版本更新,引入了"安全默认"理念,并带来了多项重要功能改进。

安全默认(Security by Default)

pnpm 10 最重要的变化是停止隐式信任已安装的包,从根本上改变了许多供应链攻击的攻击面。

默认阻止生命周期脚本

从 pnpm 10 开始,pnpm install 不再默认运行 preinstallpostinstall 脚本:

# pnpm 10 之前:自动运行所有脚本
# pnpm 10 及以后:默认不运行脚本

# 如果需要运行脚本,必须显式配置

使用 allowBuilds 配置允许特定包运行构建脚本(v10.26+,替代了早期的 onlyBuiltDependencies):

# .npmrc 或 pnpm-workspace.yaml
allowBuilds:
esbuild: true
# 只允许特定版本
[email protected]: true

或通过命令行:

# 安装包并自动添加到 allowBuilds
pnpm add --allow-build=esbuild my-bundler

minimumReleaseAge(最小发布年龄)

这是针对供应链攻击的深度防御措施。设置后,pnpm 会阻止安装发布时间过短的包:

# .npmrc
# 设置包必须发布至少 1 天(1440分钟)后才能安装
minimumReleaseAge=1440

大多数恶意包攻击在几小时内就会被发现并下架,延迟安装可以显著降低风险。

排除特定包:

# 对某些包跳过年龄检查
minimumReleaseAgeExclude:
- webpack
- react

trustPolicy: no-downgrade

防止锁文件被降级到旧版本,防止攻击者通过修改锁文件引入已知的漏洞版本:

# .npmrc
trustPolicy=no-downgrade

blockExoticSubdeps

阻止依赖使用"异国情调"的依赖协议(如 git、file、link 等),减少攻击面:

# .npmrc
blockExoticSubdeps=true

全局虚拟存储(Global Virtual Store)

pnpm 10.12 引入了全局虚拟存储,进一步节省磁盘空间并加速安装:

# .npmrc
enableGlobalVirtualStore=true

工作原理

传统上,每个项目都有自己的 node_modules 结构。启用全局虚拟存储后,pnpm 可以从磁盘上的中央位置直接链接依赖到你的项目中。

优势

  • 极大节省磁盘空间:相同的依赖图在多个项目间共享
  • 更快的安装速度:如果你有 10 个项目使用 react@19,pnpm 只需要全局链接一次

原生 JSR 支持

pnpm 10.9 原生支持 JSR(Deno 的包注册表):

# 直接从 JSR 安装包
pnpm add jsr:@std/collections

# 使用 JSR 包
import { groupBy } from "@std/collections";

这会在 package.json 中正确映射,并无缝处理 JSR 包的独特解析规则。

配置依赖(Config Dependencies)

对于 monorepo 和复杂设置,pnpm 10 引入了配置依赖功能,允许在多个项目间共享和集中管理 pnpm 配置:

{
"pnpm": {
"configDependencies": {
"pnpm-plugin-my-company": "1.0.0+sha512-..."
}
}
}

配置依赖会在主依赖图解析之前安装到 node_modules/.pnpm-config。可以用于:

  • 在仓库间共享 .pnpmfile.cjs 钩子
  • 集中管理 patchedDependencies 的补丁文件
  • 维护 allowBuilds 的共享构建脚本白名单

自动运行时管理

pnpm 可以自动管理 JavaScript 运行时版本,支持 Node.js、Deno 和 Bun:

{
"devEngines": {
"runtime": {
"name": "node",
"version": "24.6.0"
}
}
}
{
"devEngines": {
"runtime": {
"name": "bun",
"version": "1.2.0"
}
}
}

pnpm 会自动下载并使用指定版本的运行时执行项目中的脚本。这让"在我机器上能跑"的问题成为历史——团队中的每个人都使用完全相同的运行时版本。

高级依赖查找功能

pnpm 10.16 引入了 Finder 函数,可以按依赖的各种属性(不仅仅是名称)进行搜索:

// .pnpmfile.cjs
module.exports = {
finders: {
// 查找所有 peer dependencies 中有 React 17 的包
react17: (ctx) => {
return ctx.readManifest().peerDependencies?.react === "^17.0.0";
},
},
};

使用查找函数:

pnpm why --find-by=react17

输出示例:

@apollo/client 4.0.4
├── @graphql-typed-document-node/core 3.2.0
└── graphql-tag 2.12.6

还可以在输出中打印额外信息:

module.exports = {
finders: {
react17: (ctx) => {
const manifest = ctx.readManifest();
if (manifest.peerDependencies?.react === "^17.0.0") {
return `license: ${manifest.license}`;
}
return false;
},
},
};

平台特定依赖安装

pnpm 10.14+ 支持覆盖原生模块的目标平台,对于交叉编译场景非常有用:

# 覆盖 CPU 架构
pnpm install --cpu=arm64
pnpm install --cpu=x64

# 覆盖操作系统
pnpm install --os=linux
pnpm install --os=darwin
pnpm install --os=win32

# 覆盖 libc 实现
pnpm install --libc=glibc
pnpm install --libc=musl

# 组合使用(例如为 Linux ARM64 安装依赖)
pnpm install --os=linux --cpu=arm64

了解更多