pnpm 11 正式发布:安全策略和存储架构的两次重要转向
pnpm 11 发布了。一开始我没太在意,以为又是常规的功能迭代。直到仔细读了一遍更新日志,才发现这次有两个底层逻辑发生了根本变化。
第一,pnpm 默认不再信任任何新发布的包。刚发到 registry 上的包,pnpm 会让你先等一天才能安装。
第二,store 的存储结构从几百万个零散的 JSON 文件,换成了单个 SQLite 数据库。在初次安装大型项目时,这个改动可以减少大约 3 万次系统调用。
下面我们分开来细说。
安全策略:先验证,后放行
pnpm 11 在安全方面调整了三个关键默认值。它们的共同思路都是一样的:把“默认允许”改成“默认确认”。
新包默认需要一天冷静期
minimumReleaseAge 这个配置项,默认值现在是 1440 分钟,也就是整整一天。
这个规则的实际效果是:如果你今天把一个包发布到 npm,那么今天之内,任何人都无法通过 pnpm 11 安装这个包。pnpm 会直接告诉你:这个包太新了,需要等一等。
这个设计显然会给一部分人带来麻烦。比如你的团队有自己的私有 registry,内部工具包发布之后,CI 流水线立刻就要用。升级到 pnpm 11 之后,CI 可能会突然失败。解决办法是有的,在 pnpm-workspace.yaml 里把 minimumReleaseAge 设成 0 就行。
那为什么要默认设成一天?
回答这个问题,只需要看一件事:前端社区发生过多次的供应链攻击事件。攻击者会抢注一个和知名包名字高度相似的包,或者在某个已有包的更新版本里悄悄植入恶意代码。这种攻击从出手到被发现,通常会有一个时间差。冷静期的价值就在这里:哪怕攻击者动作再快,也至少留出了一天的窗口让社区和安全研究人员发现异常、进行举报和下架。
对于个人开发者和企业来说,这一天的时间本身不解决所有问题,但它给开源社区的安全响应留出了宝贵空间。
陌生子依赖默认拦截
blockExoticSubDeps 这个开关,现在默认是打开的。
“Exotic sub-dependencies”的意思是:你的依赖树里冒出来一个你从来没见过的、也没在其他地方声明过的子包。pnpm 11 会停下来,问你到底要不要装。
以前这种包可能静悄悄地就进到了 node_modules 里面,你看不到,也注意不到。现在你需要显式点头,它才会被加进来。
这个改动最直接影响的是老项目。升级之后,有的项目可能会发现某些依赖装不全了。你需要去看一眼被拦截的是哪些包,确认它们确实是项目所需要的,再手动放行。
构建脚本执行需要显式确认
strictDepBuilds 和 verifyDepsBeforeRun 这两个配置,现在也都是默认开启的。
任何想在开发者的机器上执行 postinstall 脚本的包,都必须得到明确的允许。
这个改动被低估了。发生过真实案例,攻击者不是修改包的核心功能代码,而是在构建脚本里偷偷插入恶意操作,比如窃取环境变量、上传本地文件。postinstall 脚本在 npm 生态里长期处于灰色地带,很多包在安装完毕之后会自动执行一段你根本没看过的代码。
pnpm 11 做的不多,就是把执行权从默认放行,改成了默认拒绝。
allowBuilds:用一套规则管构建权限
和构建脚本相关的配置,pnpm 以前有五个:onlyBuiltDependencies、onlyBuiltDependenciesFile、neverBuiltDependencies、ignoredBuiltDependencies、ignoreDepScripts。
现在这五个全部被移除,统一成一个 allowBuilds。
规则从“白名单机制”变成了更直接的“显式映射”。以前你可能这样写:
onlyBuiltDependencies:
- electron
neverBuiltDependencies:
- core-js现在要改成:
allowBuilds:
electron: true
core-js: false这个改动的意义不是增加了什么新能力,而是把“哪个包允许在安装时运行脚本”这个信息,从多套规则的隐式覆盖关系,变成了一个一目了然的映射表。
性能改进:少做,而不是多做
pnpm 11 的性能提升思路很直接。不是往系统里塞更多缓存策略或者多线程机制,而是尽可能让系统少做一些事。
用 SQLite 替代几百万个 JSON 文件
这是这次更新里最值得关注的一个底层变动。
pnpm 的 store 目录以前是怎么存包信息的?每个包一个独立的 JSON 文件,全部放在 $STORE/index/ 下面。一个依赖数量比较多的大型项目,这个目录里可能会躺着好几百万个 JSON 文件。
每查一个包的信息,就要执行一次“打开文件、读取内容、关闭文件”的流程。安装几千个包的时候,这套动作就要重复几千遍。
pnpm 11 把所有包的元信息统一存进了单个 SQLite 数据库文件,路径在 $STORE/index.db。数据库用 WAL 模式打开,支持并发读写。查询一个包的信息变成了一行 SQL。而且原来的 bundled manifest 也直接存入数据库,以前查完索引还要再去翻 package.json,这一步也省掉了。
简单说就是:以前查一个包的完整信息需要翻两个文件,现在一条 SQL 查询完成。
CAS 写入省掉了 3 万次重命名操作
CAS 是 Content Addressable Storage 的缩写,是 pnpm 存储实际包内容的地方。
之前的写入步骤是:先写到临时文件,确认没有问题,再把这个临时文件重命名到最终路径。写一次,就是两次系统调用。
pnpm 11 改成直接写入内容寻址路径,省掉了临时文件和重命名的步骤。根据官方的数据,在全新安装一个大型项目时,这个优化可以减少大约 3 万次 rename 调用。
这个数字听着很大,但需要注意一点:只有初次冷装的时候才会积累到这么明显的差距。平时日常的增量安装,不会有这么大的感知区别。
HTTP 层从 node-fetch 换成 undici
下载 tarball 包的 HTTP 客户端,从 node-fetch 换成了 undici。
undici 是 Node.js 官方维护的高性能 HTTP 客户端,它带来了几个实际的提升:Happy Eyeballs 策略让 IPv4 和 IPv6 可以并行尝试,哪个先通就用哪个;连接池复用做得更好;全局调度也做了优化。
翻译成日常用语就是:下载包的时候,连接复用率更高了,TCP 握手次数更少,整体下载流程更顺畅。
升级 Node 版本不用重新下载全部包
全局虚拟存储做了一项关键优化:计算包哈希的时候,去掉了引擎信息。
之前升级 Node.js 版本,或者从 x86 架构换到 arm64,pnpm 会判断包的哈希值变了,需要把所有包重新导入一遍。现在大约 95% 的包不再需要这样做。升级 Node 之后运行 pnpm install,不会再重新把所有的包下一遍。
升级前需要掂量的几件事
首先,pnpm 11 要求 Node.js 22 及以上版本。如果团队还在用 Node 20 甚至 18,要先升级 Node。
其次,工作目录下的 .npmrc 被限制为只能保留 auth 和 registry 相关的设置,其他配置都要迁移到 pnpm-workspace.yaml 或者全局的 config.yaml。如果你的项目重度依赖 .npmrc 里的自定义配置,迁移成本需要提前评估。
还有前面提到过的 onlyBuiltDependencies 那一套旧配置,需要手动迁移成 allowBuilds 格式。如果白名单本来就长,这一步会有点繁琐。
实际升级的建议是这样:先在非关键项目上跑一周,看看有没有隐藏的问题。检查 CI 里指定的 Node 版本是否满足 22 的要求。把 .npmrc 里非 auth 的配置提前搬到新位置。官方提供了一个从 v10 迁移到 v11 的自动化脚本,但 allowBuilds 的转换结果不要完全依赖脚本,还是人工检查一遍更稳妥。
最后
pnpm 11 的安全策略会让一部分开发者在前几周觉得“怎么装个包这么麻烦”。但这恰恰是它有价值的地方。供应链攻击已经不是理论上的威胁,而是每个月都在真实发生的事件。包管理器作为整个前端工具链的入口关卡,默认不安全这件事,本身就是应该被修掉的问题。pnpm 11 把它修了。
性能方面的改进不会让你脱口而出“快了 10 倍”,但会让你的 CI 账单慢慢降低,让升级 Node 版本不再变成一场需要重新下载一切漫长等待的噩梦。
包管理器正在从单纯的工具变成前端工作流的基础设施。基础设施的第一原则很简单:默认安全,尽可能透明。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!