ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

pnpm 10 的 Ignored build scripts 安全拦截与放行配置指南

pnpm 10 的 Ignored build scripts 安全拦截与放行配置指南 1. Ignored build scripts 到底在拦什么1.1 一次典型报错现场先把场景铺开。我最近在维护一个内部后台项目依赖里有better-sqlite3、sharp、esbuild这些带原生二进制的包。某天同事升级了 pnpm 版本之后重新安装依赖pnpm install跑完没有红字报错但启动服务时直接抛异常Error: Cannot find module bindings或者更直白一点Ignored build scripts: esbuild, sharp. Run pnpm approve-builds to pick which dependencies should be allowed to run scripts.如果你也看到这句 Ignored build scripts说明你用的 pnpm 版本已经默认开启了依赖构建脚本拦截策略。这不是装坏了也不是网络问题而是 pnpm 从某个版本开始默认不再执行依赖包里的preinstall、install、postinstall脚本。我见过太多人在这里绕远路有人反复删除node_modules重新安装有人把 pnpm 卸载重装有人干脆换回 npm。这些操作都没用因为问题根本不在安装过程而在 pnpm 的配置策略。1.2 默认拦截背后的安全逻辑pnpm 10 这次改动不是拍脑袋它针对的是软件供应链攻击。npm 生态里依赖包在安装时可以运行任意脚本这意味着你只是装了个包它就能在你的机器上执行任何代码。过去几年里ua-parser-js、coa、rc这些流行包都被攻击者篡改过往postinstall里塞挖矿脚本或者窃取环境变量的恶意代码。一旦这些包进入依赖树所有安装它的人都会中招。pnpm 的做法是我默认不执行依赖的构建脚本你明确点头我才执行。这个逻辑放在安全视角下完全合理——默认拒绝而不是默认放行。代价就是一大批依赖正常安装流程被打破因为它们确实需要install脚本来下载二进制或者执行编译。这里有个反直觉的点pnpm 10 并不是 pnpm 第一个引入拦截机制的版本。在 pnpm 9 及更早版本里如果你在.npmrc里设置了enable-pre-post-scriptsfalse或者使用了特定配置也会出现类似行为。但 pnpm 10 把它变成了默认值所以大量用户是升级完突然就出问题。1.3 被拦的是依赖的脚本不是你项目自己的脚本很多人一开始会误以为所有脚本都被禁了连自己项目里的predev、postinstall都不跑了。其实不是。pnpm 拦截的是依赖包的构建脚本。你自己项目根目录里package.json定义的生命周期脚本比如preinstall、postinstall、prepare仍然照常执行因为那是你主动写的代码属于受信任范围。真正被拦的是node_modules里那些第三方包自己的安装脚本。pnpm 在解析依赖时发现某个包声明了postinstall或install脚本就把它记录到待审批列表里然后在安装日志里给你打一行警告。搞清楚这个边界后面所有操作就不会迷糊了。2. 先盘点哪些依赖最容易被拦2.1 警告信息与 pnpm ignored-builds 的配合使用pnpm install的警告信息通常长这样Scope: all 3 workspace projects Lockfile is up to date, resolution step is skipped ... Ignored build scripts: esbuild, sharp. Run pnpm approve-builds to pick which dependencies should be allowed to run scripts.注意这行警告只列出了被拦截的包名不会告诉你每个包为什么需要构建脚本。如果你用的 pnpm 版本比较新可以直接执行pnpm ignored-builds它会列出当前项目里所有被拦截了构建脚本的依赖比安装日志里的信息更全。这个命令很适合在 CI 或者新环境里快速摸底。还有一个容易忽略的点警告信息里的包名来自整个依赖树可能包含间接依赖。也就是说你没直接装esbuild但某个构建工具链依赖它它一样会被拦截。这种情况下你仍然需要处理只是处理的位置在配置层面不是去你项目里加依赖。2.2 最容易踩雷的几类包根据我自己的项目经验和社区里高频出现的问题下面这几类包几乎必然触发拦截包名构建脚本作用被拦截后的典型症状esbuild校验/安装平台二进制构建时提示 esbuild 安装不正确或平台不匹配sharp安装 libvips 预编译二进制运行时Could not load the sharp moduleelectronpostinstall 下载 Electron 二进制运行 electron 提示安装不完整puppeteerpostinstall 下载 Chromium启动浏览器时找不到对应版本better-sqlite3 / sqlite3prebuild-install 或 node-gyp 编译require 时找不到.node原生模块node-sass下载/编译 binding编译报 Missing bindingtailwindcss/oxide安装 native bindingTailwind v4 初始化报错simple-git-hookspostinstall 安装 git hookshooks 没生效core-js仅 opencollective 赞助提示无实际影响可忽略这里面core-js比较特殊它的postinstall只是弹一个赞助广告不执行也不影响功能。遇到这种包你完全不用放行直接无视警告即可。判断需不需要放行的标准很简单这个包被拦之后我的程序还能不能跑起来如果一个包只是用postinstall做点非必需的事比如打印个提示、发个埋点那它不跑也无所谓。只有那些依赖构建脚本来安装原生二进制、执行编译的包才是必须处理的硬需求。2.3 拿不准时怎么查一个包有没有构建脚本当你对一个包是否需要放行拿不准最快的方法就是直接看它的package.json。在node_modules里找到对应包cat node_modules/.pnpm/esbuild0.24.0/node_modules/esbuild/package.json重点看scripts字段里有没有preinstall、install、postinstall这三个键。如果有再看脚本内容。以esbuild为例它的postinstall实际上是执行一个校验脚本确认当前平台对应的二进制包是否装好better-sqlite3的install则是prebuild-install || node-gyp rebuild属于必须执行的编译动作。更省事的做法是直接看包的文档或 GitHub 仓库。一般需要构建脚本的包README 里都会有明确的提示比如如果你使用 pnpm请将本包加入onlyBuiltDependencies。3. 三条放行路径按场景选3.1 交互式命令 pnpm approve-builds最直观的方式是执行pnpm approve-builds这个命令会弹出一个交互式列表用空格键勾选你要放行的包回车确认。确认之后 pnpm 会把结果写进package.json的pnpm.onlyBuiltDependencies字段如果是 monorepo可能会写进pnpm-workspace.yaml然后自动重新安装受影响的包并执行它们的构建脚本。这个方案的优点是零记忆成本适合第一次遇到问题、列出来的包不多的情况。缺点也明显交互式操作没法在 CI 里用而且如果后续新增依赖又触发拦截你还得再跑一次。它更适合作为急救手段而不是长期配置方案。有个细节要注意执行pnpm approve-builds时列表里可能包含很多你根本不该放行的包。别一股脑全选只勾选你确认有必要的。这一步其实就是安全审查选错等于把 pnpm 的安全策略直接废了。3.2 声明式配置package.json 的 pnpm.onlyBuiltDependencies我更推荐的方式是在package.json里显式声明{ pnpm: { onlyBuiltDependencies: [ esbuild, sharp, better-sqlite3 ] } }写完之后重新执行pnpm installpnpm 会读取这个白名单只放行清单里的包其他包继续默认拦截。这个方案的好处是声明式的配置跟着仓库走提交到 Git 之后所有开发者和 CI 共享同一份规则。以后任何人拉代码执行pnpm install都不会再出现我这边没报错你那边报错的 dev 环境不一致问题。注意字段名是onlyBuiltDependencies语义是只有这些依赖允许执行构建脚本。它的反面是ignoredBuiltDependencies语义是这些依赖明确不允许执行构建脚本。3.3 monorepo 工作区pnpm-workspace.yaml如果你的项目是 monorepo配置位置要换一下。pnpm 10 里工作区根目录的pnpm-workspace.yaml是配置的权威来源比package.json优先级更高。packages: - apps/* - packages/* onlyBuiltDependencies: - esbuild - sharp为什么 monorepo 要特意用工作区配置因为 monorepo 下有多个子包每个子包都有自己的package.json如果各自声明一套pnpm.onlyBuiltDependencies很难维护。统一放在工作区根部一份清单管所有子包清晰也省事。这里有个容易踩的坑pnpm 10 对 monorepo 的配置读取规则是以pnpm-workspace.yaml为准。如果你已经建了pnpm-workspace.yaml还在某个子包里写了pnpm.onlyBuiltDependencies子包里的配置可能不会生效。所以 monorepo 项目统一用工作区文件别混着写。3.4 相关字段辨析ignoredBuiltDependencies 与 dangerouslyAllowAllBuilds除了白名单pnpm 还提供了几个容易混淆的配置。ignoredBuiltDependencies是黑名单。它配合dangerouslyAllowAllBuilds使用的场景比较多如果你在.npmrc里开了dangerouslyAllowAllBuildstrue所有依赖的构建脚本都会执行这时候你可以用黑名单把个别不信任的包单独拦掉。{ pnpm: { ignoredBuiltDependencies: [ some-suspicious-package ] } }dangerouslyAllowAllBuilds这个名字本身就是警告。我见过有人为了省事直接开它结果所有依赖的脚本全部放行pnpm 10 的安全策略形同虚设。除非你清楚自己在做什么并且对项目依赖链有足够信任否则不建议开。真遇到依赖特别多、逐个人工审批不现实的情况也应该先开一段时间跑通流程之后再把白名单补全而不是长期挂着一个全放行开关。另一个相关配置是onlyBuiltDependenciesFile它可以指向一个 JSON 文件把白名单独立出来管理适合白名单特别长的场景。普通项目用不上知道有这么个东西就行。4. 放行之后的验证与构建失败排查4.1 怎么确认脚本真的执行了配置白名单之后重新跑一次pnpm install观察输出。如果之前有 Ignored build scripts 警告配置正确的话警告会消失或者只剩下那些你确实没放行的包。但没有警告不等于构建成功。某些原生模块的构建脚本执行了但可能因为缺少编译工具链而失败。这时候 pnpm 会在 install 过程中报具体错误而不是默默吞掉。所以关键是看 install 的完整输出不要只看最后有没有报错。更确定的验证方式是手动重建单个包pnpm rebuild better-sqlite3pnpm rebuild会重新执行指定包的所有生命周期脚本并把输出打印到终端。如果脚本本身有问题这一步会暴露出来。全量重建用pnpm rebuild在 monorepo 里可以加-r参数递归处理所有子包或者用--filter指定子包范围。4.2 放行后构建仍失败的常见原因白名单配置没问题但构建脚本跑挂了这种情况我也遇到过不少。常见原因排序大概是这几类第一缺少原生编译工具链。node-gyp编译需要 Python 和 C/C 工具链。Windows 上要装 Visual Studio Build ToolsmacOS 要装 Xcode Command Line ToolsLinux 上要装build-essential、python3。报错信息里出现gyp ERR!基本都是这个原因。第二Node 版本不匹配。有些原生模块的预编译二进制只支持特定 Node ABI 版本切换 Node 版本后旧二进制失效需要重新构建。报错特征是NODE_MODULE_VERSION之类字样。这类问题的最新讨论大多来自 Node 版本升级到 22 之后的原生模块兼容性。第三包本身的安装脚本有 bug 或者对 pnpm 的目录结构处理不当。常见表现是脚本在安装时硬编码了npm命令或者仅处理扁平化的node_modules。遇到过就查一下包仓库的 issue看看有没有针对 pnpm 的已知问题。通用排查路径先记下来比盲目重装有效我几乎每次都这么处理这类问题# 1. 清掉 pnpm 的安装缓存和模块目录 pnpm store prune rm -rf node_modules # 2. 重新安装并保留完整日志 pnpm install --reporterappend-only # 3. 单独重建出问题的包 pnpm rebuild 包名4.3 二进制下载失败和镜像源另一类高频失败是包在install脚本里下载预编译二进制比如 Electron、Puppeteer、sharp 的二进制下载这类操作受网络环境影响很大。如果你发现 build 脚本卡在下载阶段或者报某个域名连接超时说明是网络问题不是 pnpm 的问题。常规做法是给这些工具配置国内镜像。每个包有自己的环境变量比如 Electron 可以用ELECTRON_MIRRORPuppeteer 可以设置下载源sharp 也可以用镜像。另一种更通用的思路是把 npm registry 切换到镜像源pnpm config set registry https://registry.npmmirror.com注意 registry 只影响 npm 包的下载不影响那些包在 install 脚本里自己去外部域名下载二进制文件的行为。如果你同时遇到包下载失败和构建脚本失败先分开判断包下载失败查 registry构建脚本里的二进制下载失败查对应包自己的镜像配置。5. 与 pnpm 环境绑定的那几个高频坑5.1 找不到 pnpmPATH 没配好pnpm 不是内部或外部命令也不是可运行的程序或批处理文件——这句话经常出现在刚装完 pnpm 的命令行里。Windows 上尤甚。原因很简单pnpm 装完了但它的安装目录不在PATH里终端找不到这个命令。先查一下 npm 的全局安装目录npm config get prefix如果输出的是C:\Users\你的用户名\AppData\Roaming\npm那 pnpm 就装在这个目录下你把这个目录加进PATH就行了。Windows 上可以执行setx PATH %PATH%;C:\Users\你的用户名\AppData\Roaming\npm注意setx设置的是用户级 PATH只对之后新开的终端生效。改完记得重开命令行窗口。Linux 和 macOS 上用 npm 全局安装的 pnpm 通常在$(npm prefix -g)/bin下也需要确认这个目录在 PATH 里。很多人用 nvm 管理 Nodenvm 会自动把当前版本的 bin 目录加进 PATH但如果 pnpm 是用系统 Node 装的切到 nvm 的 Node 之后就会找不到命令。5.2 corepack 缓存坏了找不到 pnpm.cjs这条热词背后的报错很典型Cannot find module /root/.cache/node/corepack/v1/pnpm/12.4.2/bin/pnpm.cjs看到corepack和.cache路径就知道这个是 corepack 缓存出了问题。corepack 是 Node 自带的包管理器工具你通过corepack enable启用的 pnpm 命令实际上是个 shim它会到缓存目录里找真正的 pnpm。如果缓存目录被清理过、权限不对或者 Node 版本切换导致缓存路径冲突就会出现上面这个报错。最干净的处理方式是放弃 corepack 这条链路直接用 npm 全局安装 pnpmcorepack disable npm install -g pnpm这样 pnpm 就变成 npm 的普通全局包不再依赖 corepack 的缓存路径问题自然消失。如果你还是想用 corepack 管理版本那就把旧缓存清掉重新激活rm -rf ~/.cache/node/corepack corepack prepare pnpmlatest --activate在 Windows 上对应的缓存目录是%LOCALAPPDATA%\node\corepack一类的位置清理思路一样。5.3 nvm 切换之后 pnpm 消失nvm 按 Node 版本隔离全局包这是它最重要的特性。你在 Node 18 下用npm i -g pnpm装的 pnpm切到 Node 20 之后就会消失因为 nvm 切换时换了整套全局路径。这不是故障是设计如此。应对办法有两个。一个是每个 Node 版本都单独装一次 pnpm切换后缺了再装。另一个是交给 corepack但基于上一节的经验我对 corepack 的稳定性持保留态度。实际项目中我更推荐用 nvm 的别名管理。在.nvmrc或项目文档里固定 Node 版本然后基于固定版本装 pnpm。这样团队里所有人在同一套环境链路上操作少很多玄学问题。项目里我一般会在.npmrc或文档里写清楚 Node 版本和 pnpm 版本配合package.json的packageManager字段把版本锁住。{ packageManager: pnpm10.4.1 }这个字段在配合 corepack 使用时会自动激活对应版本的 pnpm但如前所述corepack 本身可能出问题所以它更多是版本声明作用。5.4 彻底删除并重装 pnpm排查到最后如果决定重装 pnpm建议按顺序做下面四步避免残留文件影响新版本# 1. 卸载 npm 全局的 pnpm npm rm -g pnpm # 2. 关闭 corepack 的 shim corepack disable # 3. 清理 corepack 缓存目录Linux/macOS rm -rf ~/.cache/node/corepack # 4. 清掉 pnpm 的全局 store确认不再需要本地缓存时再执行 pnpm store path rm -rf 上面输出的路径重装之后记得验证pnpm --version which pnpmwhich pnpmWindows 上是where pnpm能告诉你 pnpm 到底挂在哪条路径下。如果pnpm --version有输出但运行项目时仍然报不是内部或外部命令八成是 PATH 顺序问题检查当前 shell 实际用的路径和which输出是否一致。6. 我推荐的安全放行方案6.1 安全与效率的平衡处理 Ignored build scripts 这件事核心是在安全和效率之间找平衡点。我的原则很简单能让它不跑脚本就不跑必须跑脚本的包单独审查后放行。具体操作上每次pnpm install出现新的 Ignored build scripts 警告我先看包名。像core-js这种纯广告脚本的直接加入ignoredBuiltDependencies让警告列表干净一点。像esbuild、sharp这种明确需要构建脚本的先看一眼包来源和保护伞组织确认没问题再加进白名单。dangerouslyAllowAllBuilds对我来说是最后的兜底手段。有时候从老项目迁移历史依赖特别多一个个加白名单确实费时间。这时候我会临时开一下跑通流程之后马上关掉再稳下来补白名单。它也适合本地开发临时用但绝不能让它活着进 CI。这不算什么高大上的方案但很实际。6.2 把放行清单锁进仓库团队协作场景下最关键的一步是把白名单写进仓库而不是靠每个人本地执行pnpm approve-builds。否则一定会出现新同事拉代码装依赖就报错、CI 上构建失败、线上环境闪崩这些破事。我现在的标准做法是普通项目package.json里维护pnpm.onlyBuiltDependenciesmonorepo 项目pnpm-workspace.yaml里维护onlyBuiltDependencies配合packageManager字段锁死 pnpm 版本在仓库的 CONTRIBUTING 里写一句新依赖触发了 build scripts 警告按需添加白名单而不是全放行这套组合拳做好之后我本地没问题这句话的可信度会大幅提升。因为依赖安装路径统一了行为一致了剩下的差异就只可能是 Node 版本和操作系统本身。6.3 一次线上发布事故给我的教训最后说一个真实教训。有一次我在一个 Node 服务项目里加了sharp做图片压缩pnpm install之后本地跑得好好的因为当时sharp的二进制其实是被onlyBuiltDependencies放行的。但我同事在 CI 里重新构建时因为锁文件更新后 pnpm 解析的sharp小版本变了新版本的安装脚本行为有差异CI 里没走白名单服务启动直接报错。事后复盘才发现当时我把白名单写在本地环境里没提交到仓库两个环境的 pnpm 配置根本不一致。那次事故之后我强制自己在所有项目里把 pnpm 的放行配置写进仓库并且每次更新依赖后都要在干净环境里验证一遍安装。我现在处理这类问题时有个习惯**永远先看 install 输出里有没有 Ignored build scripts 这一行有就先处理配置再去排查代码问题。**很多人把报错归因于代码或者依赖版本其实根源只是 pnpm 的安全策略拦截了构建脚本。先把这个变量排除掉后面的一切排查才会高效。
返回列表