npm 或 Yarn 项目迁移到 pnpm 需要注意哪些问题?
为什么要迁移到 pnpm?
npm 和 Yarn 采用扁平化的 node_modules 结构,所有依赖都被提升到顶层目录。这带来两个核心问题:
- 幽灵依赖(Phantom Dependencies):你可以在代码中引用未在 package.json 中声明的包,因为 npm/Yarn 会把间接依赖也提升到顶层。一旦上游包移除了该间接依赖,你的项目就会突然崩溃。
- 依赖分身(NPM Dups):同一个包的不同版本可能被多次安装,浪费磁盘空间,还可能导致类型不一致。
pnpm 通过内容寻址存储 + 符号链接的方案解决了这些问题:全局只存一份包,项目通过符号链接引用,未声明的依赖直接不可访问。
迁移前该做哪些准备?
不要上来就删 node_modules,先确认几件事:
- 确认 Node.js 版本:pnpm 需要 Node.js 16.14+,建议 18+。
- 记录当前依赖树:执行
npm ls --depth=0 > deps-backup.txt或yarn list --depth=0 > deps-backup.txt,留个快照方便后续排查。 - 确保测试覆盖:迁移后需要跑一遍完整测试,没有测试的项目建议先补关键路径的测试。
- 分支操作:在独立分支上迁移,确认无误后再合入主分支。
第一步:安装 pnpm
推荐三种方式,按优先级排序:
方式一:Corepack(官方推荐)
bashcorepack enable corepack prepare pnpm@latest --activate
Corepack 是 Node.js 自带的包管理器管理工具,不需要全局安装 pnpm,避免版本冲突。
方式二:独立安装脚本
bashcurl -fsSL https://get.pnpm.io/install.sh | sh -
方式三:npm 全局安装(不推荐)
bashnpm install -g pnpm
这种方式可能导致 pnpm 自身的依赖和项目依赖产生冲突,仅在前两种方式不可用时使用。
第二步:清理旧产物
bashrm -rf node_modules rm package-lock.json # npm 项目 rm yarn.lock # Yarn 项目
如果你用的是 npm shrinkwrap,也要删除 npm-shrinkwrap.json。
第三步:导入锁文件
bashpnpm import
这条命令会读取现有的 package-lock.json 或 yarn.lock,生成 pnpm-lock.yaml。导入完成后可以删掉旧锁文件。
如果锁文件有冲突或格式异常,pnpm import 可能报错,这时跳过导入直接 pnpm install 即可,pnpm 会根据 package.json 重新解析。
第四步:安装依赖
bashpnpm install
首次安装会建立全局 store(默认在 ~/.local/share/pnpm/store),后续项目会复用已下载的包,速度会明显加快。
迁移后一定会遇到的三个问题
幽灵依赖报错:Cannot find module
这是迁移后最常见的问题。之前能用的包突然找不到了,原因是这些包从未在 package.json 中声明,只是碰巧被提升到了 node_modules 顶层。
排查方法:
bash# 查看哪个包实际提供了这个模块 pnpm why lodash
解决方式:显式安装缺失的依赖。
bashpnpm add lodash
如果缺失的包太多,可以用 pnpm ls --depth=0 对比迁移前的 deps-backup.txt,逐个补上。
peer dependencies 报错
pnpm 默认严格检查 peer dependencies,这和 npm 的宽松行为不同。你可能会看到大量 UNMET PEER DEPENDENCY 警告。
推荐的处理方式(按优先级):
- 安装缺失的 peer 依赖:直接
pnpm add react react-dom,这是最正确的做法。 - 配置 peerDependencyRules 忽略特定警告:
json{ "pnpm": { "peerDependencyRules": { "ignoreMissing": ["webpack", "@babel/core"], "allowedVersions": { "react": "18" } } } }
- 自动安装 peer 依赖(.npmrc):
iniauto-install-peers=true
- 放宽严格检查(.npmrc):
inistrict-peer-dependencies=false
运行时找不到模块
部分包在运行时通过动态 require() 加载模块,pnpm 的严格隔离会导致找不到。这时候可以用针对性提升而非全量提升:
ini# .npmrc - 只提升特定包 hoist-pattern[]=*react* hoist-pattern[]=*emotion*
只有在针对性提升也无法解决时,才考虑使用 shamefully-hoist=true,它会创建类似 npm 的扁平结构,但会失去 pnpm 的严格依赖管理优势。
.npmrc 推荐配置
ini# 迁移初期建议的配置 auto-install-peers=true # 自动安装 peer 依赖,减少迁移阻力 strict-peer-dependencies=false # 不因 peer 依赖不兼容而中断安装 shamefully-hoist=false # 保持严格隔离,不要轻易开启 # 如果遇到运行时找不到模块的问题,优先用 hoist-pattern 替代 shamefully-hoist # hoist-pattern[]=*problematic-package*
package.json 调整
json{ "scripts": { "preinstall": "npx only-allow pnpm" }, "engines": { "pnpm": ">=9.0.0" } }
preinstall 脚本会在有人误用 npm install 时自动拦截,确保团队统一使用 pnpm。需要先安装 only-allow:
bashpnpm add -D only-allow
CI/CD 配置更新
GitHub Actions:
yaml- uses: pnpm/action-setup@v4 with: version: 9 - run: pnpm install --frozen-lockfile
GitLab CI:
yamlbefore_script: - corepack enable - pnpm install --frozen-lockfile
--frozen-lockfile 确保 CI 环境不会修改锁文件,和 npm 的 npm ci 作用一致。
Monorepo 迁移
如果你使用 Lerna 或 Yarn Workspaces,迁移到 pnpm workspace 非常简单。
创建 pnpm-workspace.yaml:
yamlpackages: - 'packages/*' - 'apps/*'
包间引用改用 workspace: 协议:
json{ "dependencies": { "@my-org/utils": "workspace:*" } }
workspace:* 表示引用本地 workspace 中的包,发布时 pnpm 会自动替换为实际版本号。
迁移检查清单
bash# 1. 确认依赖安装完整 pnpm ls --depth=0 # 2. 运行测试 pnpm test # 3. 构建项目 pnpm build # 4. 检查 lint pnpm lint # 5. 本地启动验证 pnpm dev
每一步都通过后再合入主分支。
如果迁移失败怎么回滚?
bashrm pnpm-lock.yaml .npmrc rm -rf node_modules npm install # 或 yarn install
别忘了恢复 CI/CD 配置和 package.json 中 pnpm 相关的改动。
迁移后你能获得什么?
- 安装速度提升 2-3 倍:全局 store 复用,跨项目共享已下载的包
- 磁盘空间节省 50-70%:同一个包全局只存一份,通过硬链接引用
- 杜绝幽灵依赖:未声明的包直接不可访问,提前暴露潜在风险
- 更严格的依赖管理:peer dependencies 严格检查,避免版本冲突
- 原生 monorepo 支持:workspace 协议比 Yarn Workspaces 更简洁