本文记录了工作中升级 PNPM 11 过程中遇到的问题。
本文并非操作手册,而是记录「升级过程中遇到的问题以及如何解决」的复盘。文中提到的问题和解决方案可能未必适用于广泛的情况,仅供参考。
突如其来的错误
pnpm 的升级始于一场意外。因为目前正在做 Node.js 24 的升级和 Monorepo 的迁移。
7 月 15 日,Pipeline 上开始报错,用于做 Audit 的 Step 出现 410 错误。同时本地执行 pnpm audit 时也出现同样的错误信息。
原因就是错误信息中提到的 Audit 的 API 退休了,而解决方法就是把 PNPM 升级到 11。
由于有 300+ 的仓库且 PNPM 的版本各不相同(8、9、10、11 都存在),并且各版本之间都有不小的改动。仅靠 corepack use pnpm@11 是不够的,整个升级的过程中有不少需要手动介入的地方。
pnpm 11 改了什么
pnpm 11 最大改动在安全模型上。为了应对供应链攻击提高安全性,默认开启 strictDepBuilds——依赖如果想跑 postinstall、prepare 等脚本,必须先出现在 allowBuilds 白名单里,否则不是静默跳过,而是直接报错。
很巧的是,我们大量的 Bitbucket 私有依赖都有 prepare 或类似构建脚本。这意味着:安装能过,不等于脚本能跑;脚本能跑,也不等于原生 .node 文件已经生成。
在此前升级 Node.js 24 的任务中,因为有二进制文件(这是个重点)的编译,所以本地和 CI 的安装脚本拆分为 pnpm install --ignore-scripts → pnpm rebuild 两步。升级前这套在 pnpm 8/9/10 下基本够用;但升级到 11 后,每一步的边界都变窄了。
问题 1:面对 60 多个 Bitbucket 私有依赖,白名单怎么维护?
处于安全角度,我们不会关闭 strictDepBuilds 配置而是启用 allowBuilds 白名单,
而通常在一个 Service 中就有几十个 Bitbucket 私有依赖。第一个问题便是白名单的构建,lockfile 里大约有 60 多个私有依赖,每个几乎都有 prepare 或类似构建脚本。如果全部手填,既容易漏,也很难在 code review 里看清变更。
并且在查文档发现两条硬约束:
- pnpm 不支持 Git 依赖的通配符,不能写
*@git+ssh://git@bitbucket.org/example-org/*了事; - Git 依赖的键必须是
包名@git+ssh://.../repo.git,只写包名对 Git 依赖无效。
解决方案也很简单:
- 针对 Bitbucket 私有依赖:私有依赖脚本的安全性是可信的。可以直接让 AI 从
package.json和lockfile中提取私有依赖添加到白名单。同时让 AI 编写 sync 脚本负责校验 CI 中 lockfile 与 yaml 是否一致。新增 Git 依赖后,开发者跑一遍 sync 脚本,把 yaml 和 lockfile 一起提交即可。 - 针对 npm registry 公共包:保留手动区。谁加了需要 postinstall 的包,走
pnpm approve-builds <包名>或在 PR 里补条目,经 review 合并。
在解决了依赖安装问题后,下一步就到了 rebuild 步骤了。
问题 2 :CI 报二进制依赖的编译问题
配置好 allowBuilds 白名单后,在执行 pnpm rebuild 时突然报错:
Error: Cannot find module './build/Release/example-native-lib'
错误信息指向某个私有依赖的间接依赖 example-native-lib。于是立刻就想:是不是白名单漏了?
检查 pnpm-workspace.yaml,example-native-lib 对应的 Git 条目已经在里面。再跑 pnpm rebuild example-native-lib,命令成功退出,但 build/Release/example-native-lib.node 依然不存在。
继续看这个依赖包本身的 package.json:"gypfile": true,没有 install,也没有 prepare 这样的构建脚本。
这就对上了 pnpm 11 的行为:pnpm rebuild 只会为显式定义了 lifecycle script 的包重跑脚本。gypfile: true 只是告诉 npm/pnpm「这个包需要 node-gyp」,并不会自动触发编译。在 pnpm 8 下,全量 pnpm rebuild 有时会把这类包顺带编过去;pnpm 11 下不会。
问题 3 :CI 能过,但同事 pull 后 install 失败
处理完前面安装和编译的步骤后,CI 能过 PR 也终于 Merge 了。本以为升级已经完成,谁曾想当同事 pull 了最新的代码后发现安装失败了。出现错误信息 [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: ...。
经过排查后发现报错指向的包,是某个私有依赖升级后新带进来的传递依赖——例如把 example-model 从 v71 升到 v74 后,lockfile 里多出来一批 example-spec-*、example-aws-utilities 之类的 Bitbucket Git 包。CI 日志里列出的正是这类名字。
第一反应很容易误判:「我这边明明没问题,是不是同事环境不对?」
但检查后才发现原来是私有依赖变更后引入的问题。解决方案也很简单,运行一下前面创建的 sync 脚本,然后再执行一下 pnpm approve-builds <包名> 即可。
这一轮的教训可以压成一句话:pnpm 11 下,lockfile 和 allowBuilds 是绑定的;你只更新了前者,就等于只把门钥匙发了一半。
总结一下
回头看,pnpm 11 升级里最容易混淆的是把三件事当成一件事:
allowBuilds: true → 允许该依赖执行 lifecycle script(门禁)
pnpm rebuild → 为已有 install/prepare 的包重跑脚本(触发器)
gypfile: true → 需要 node-gyp,但没有 lifecycle script(无人触发)
白名单解决的是「能不能跑」;rebuild 解决的是「谁来跑、跑哪些」;gypfile-only 包两个都不够用,必须另有显式编译步骤。
pnpm 8 到 11 的差异就是对于依赖安装变得更加严格,处于安全目的 allowBuilds、gypfile-only、rebuild 边界都要心里有数。同时也暴露了一点,跨大版本升级时,有非常多的坑要踩。针对于包管理器这类基础工具,还是要勤加升级,保持在较新的 LTS 版本上。
参考资料:
欢迎关注同名公众号:此方的手账
不止技术。科技新闻解析、编程语言史、大厂史、世界简史正在连载中。
希望成为让你了解一些奇怪知识的电子榨菜和厕所读物。