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 中的 cpu、os、libc 字段一致。--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 命令对比
| npm | pnpm |
|---|---|
npm install | pnpm 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 list | pnpm list |
npm outdated | pnpm outdated |
npm update | pnpm update |
npm ci | pnpm 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 的优势:
- 版本一致性:确保整个 monorepo 中相同依赖使用相同版本
- 简化维护:只需在一个地方更新版本号
- 减少冲突:避免不同包使用不同版本导致的兼容性问题
- 清晰可读:一眼就能看出项目使用了哪些版本的依赖
更新 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 不再默认运行 preinstall 或 postinstall 脚本:
# 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
了解更多
- pnpm 官网:https://pnpm.io
- pnpm GitHub:https://github.com/pnpm/pnpm
- pnpm 文档:https://pnpm.io/motivation
- pnpm 博客:https://pnpm.io/blog