尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

pnpm Ignored Build Scripts 报错怎么办?白名单配置与安全机制详解

pnpm Ignored Build Scripts 报错怎么办?白名单配置与安全机制详解 如果你最近在个人电脑或 CI 流水线里用 pnpm 装依赖看到控制台冒出一句 “Ignored build scripts”然后项目启动时又报一堆和 esbuild、sharp、node-sass 之类的原生模块有关的错误别慌这不是你配置写错了也不一定是网的问题。这个现象从 pnpm 7.x 开始出现到 pnpm 10 之后几乎成了每个迁移者都会撞上的第一道坎。先说结论pnpm 为了保护供应链安全默认不再执行依赖包里的 postinstall、preinstall 这类构建脚本只有你明确批准的白名单包才有权限跑。这个设计本身是合理的但代价就是像 esbuild 这类依赖安装阶段做二进制下载的包会被直接卡住然后你启动项目时就等来一串串红字报错。这篇文章我把我踩过的坑、试过的方案、以及最后沉淀下来的标准操作都整理出来不管是个人项目还是 monorepo 工作区看完都能直接处理掉。1. 理解 pnpm 的脚本拦截机制1.1 为什么 pnpm 会默认阻止构建脚本不少从 npm 或 yarn 迁移过来的朋友第一次看到 “Ignored build scripts” 时都会懵npm 装了包就乖乖跑脚本怎么到了 pnpm 这里就罢工了核心原因在于 pnpm 做了一个安全取舍。npm 和 yarn 在安装依赖时会无条件执行包里的 install、postinstall 这类生命周期脚本这在供应链攻击面前等于敞开了大门。只要某个依赖包被恶意篡改或者某个小工具包夹带了 postinstall 脚本你的机器就在安装过程中被植入了一段任意代码而你毫无感知。近几年开源生态里多次出现过这类投毒事件npm 官方也在持续收紧相关政策。pnpm 的策略简单粗暴默认情况下所有依赖包的构建脚本我都不执行除非你明确告诉我哪些包可以信任。这样一来即使某个包真的带了恶意脚本它也跑不起来。代价就是一部分真正需要构建脚本才能正常工作的包比如需要编译原生模块的、需要下载平台二进制文件的也会被一同拦截导致运行时缺文件。这个机制经历了几个版本演变。pnpm 7 和 8 一开始只是打印警告很多人根本没注意到。到了 pnpm 9它开始把被忽略的包列成一张表提醒你处理。pnpm 10 直接把配置项改了以前放在 package.json 里的pnpm.neverBuiltDependencies和pnpm.onlyBuiltDependencies还继续兼容但新增了更明确的pnpm.onlyBuiltDependencies作为主流推荐方式。可以说这个“默认不信任”的机制已经在持续强化。1.2 哪些类型的包最常受影响理解了这个机制你还需要知道哪些包最容易中招。我经历过的大致可以分三类第一类是“安装时下载二进制文件”的包典型代表是 esbuild、sharp、prisma、swc/core、rollup 的 optional native 依赖。这类包发布在 npm 上的其实是平台判断脚本真正干活的是安装阶段从 GitHub 或其他 CDN 拉下来的二进制文件。脚本被拦截后运行时就会报 “You installed esbuild for another platform” 或者找不到 binding 文件。第二类是“安装时编译原生 C/C 模块”的包典型代表是 node-sass、better-sqlite3、bcrypt、canvas、sqlite3。它们需要 node-gyp 在本地把源码编译成.node文件。脚本被拦截后包虽然装在 node_modules 里但根本没有可加载的编译产物。第三类是“需要在安装后生成代码或修改配置”的包常见的有 husky要为 git 配置钩子脚本、angular/compiler-cli 的部分版本、一些 monorepo 工具链。它们跑不了脚本功能就会静默缺失比如 husky 安装后 git 钩子不生效但项目还能跑问题更隐蔽。先搞清楚自己的项目里有没有这类包后面处理起来就有的放矢。如果项目很干净全是一些纯 JS 工具库那就算看到 “Ignored build scripts” 也可以完全忽略不影响任何功能。2. 快速识别是否真的踩中脚本拦截2.1 典型的错误日志特征很多时候你并不会正眼看到 “Ignored build scripts” 这句提示因为它只是安装过程里的一行警告混在一大堆 progress 日志中间。等到你启动项目或者执行构建时真正的异常才陆续冒出来。我自己遇到过一个最典型的情况用 Vite 建项目pnpm install顺利完成没有任何红色报错但一执行pnpm dev终端直接弹出来You installed esbuild for another platform than the one youre currently on. This wont work because esbuild is written with native code and needs to install a platform-specific binary executable.这句话是 esbuild 在运行时检测到自己的可执行文件不存在时给出的提示。根因就是 pnpm 默认拦截了 esbuild 的 postinstall 脚本导致平台二进制没下载下来。还有更隐蔽的情况。比如 better-sqlite3 装完没有报错等你执行数据库操作的时候Node.js 抛出一个模块找不到的错误Error: The module /path/to/node_modules/better-sqlite3/build/Release/better_sqlite3.node was compiled against a different Node.js version或者直接提示Cannot find module ./build/Release/better_sqlite3.node。这种情况十有八九是构建脚本被拦了根本没有生成这个文件。另外如果你安装 husky 之后发现git commit不触发 lint-staged也别急着怀疑配置写错。先看一眼安装输出里有没有 husky 那一行的 “Ignored build scripts”有的话你的.husky/目录可能压根没写入可执行钩子。2.2 确认当前工作区的拦截状态除了看报错pnpm 还提供一个直接查询状态的命令。在项目根目录执行pnpm ignored-builds这个命令会把当前工作区里所有被忽略构建脚本的依赖包列出来显示它们的名称、版本和对应依赖路径。我第一次跑这个命令时看到输出里躺着 esbuild、sharp、husky、prisma 一长串心里一下就有数了。如果你用的 pnpm 版本还没有这个命令也可以直接打开node_modules/.pnpm目录看有没有对应的包或者检查node_modules/.modules.yaml文件。.modules.yaml里会有类似这样的记录ignoredBuilds: - esbuild - husky看到这个列表基本就能确认是脚本拦截问题了。注意这个列表只是告诉你哪些包被拦了并不代表这些包一定需要放行。如果项目里某个包被列入拦截名单但实际运行完全正常那就不用动它多放行一个包就多一分供应链风险。3. 主流解决方案与配置解析3.1 白名单配置pnpm.onlyBuiltDependencies最推荐的方案是把需要执行构建脚本的包名写进白名单。pnpm 支持在package.json中声明以下字段{ pnpm: { onlyBuiltDependencies: [esbuild, sharp, prisma] } }这段配置放在项目的根package.json里即可。写完后重新执行一次pnpm installpnpm 会重新检查依赖树并对白名单里的包执行构建脚本。你会在安装日志里看到类似 “Running postinstall script for esbuild” 的字样说明脚本已经放行成功。需要注意一点onlyBuiltDependencies里填的是包名不是包路径。对于swc/core这种 scoped 包直接写swc/core就行不需要展开成路径。另外配置对 workspace 也生效在 monorepo 的根package.json里配置所有子包安装时都会按这个白名单执行。关于配置位置pnpm 也提供了对pnpm-workspace.yaml的支持。如果你用的是 pnpm 10可以在pnpm-workspace.yaml中增加onlyBuiltDependencies: - esbuild - sharp用哪个位置取决于项目风格。如果你习惯把工程化配置集中在package.json里就写在package.json。如果公司有多个项目共享同一套 pnpm 配置放pnpm-workspace.yaml也更方便统一管理。两个位置同时配置也可以pnpm 会合并取并集。3.2 交互式审批命令pnpm approve-buildspnpm 还提供一个略“偷懒”的命令适合你懒得手动整理白名单或者不确定项目里到底哪些包需要放行的情况。在项目根目录执行pnpm approve-buildspnpm 会列出当前被拦截的所有依赖包然后逐个询问你“是否允许某个包执行构建脚本”你按方向键和回车确认就行。确认完之后它会自动把允许的包写到package.json的pnpm.onlyBuiltDependencies字段里如果配置在pnpm-workspace.yaml则会写到那里。这个命令的好处是所见即所得避免遗漏。缺点是如果你工作区里依赖很多列出来的包可能几十个你得一个一个判断。而且万一你手滑把某个不认识的包也点成了 allow那跟直接关掉安全机制没区别。我的建议是这个命令适合第一次处理时用来快速“摸底”但最后还是要回到onlyBuiltDependencies手动确认一遍把不该放行的包剔除。3.3 忽略全部脚本的兜底策略有些项目是历史遗留下来的依赖了一堆需要构建脚本的旧包你压根没时间一个个甄别。这时你可能想的是能不能直接让 pnpm 忽略掉这个限制干脆全部放行可以但我不推荐尤其是生产项目。pnpm 提供两个相关配置项一个是把onlyBuiltDependencies换成allowBuilds的变体不同版本叫法不同还有一个是直接把安全策略降级{ pnpm: { allowBuilds: [*] } }在较新的 pnpm 版本里直接通过pnpm config set allow-builds true也能关闭这个拦截限制。但安全性影响很明显所有依赖包的任何安装脚本都会执行和 npm 的默认行为没有区别。你要知道pnpm 费这么大劲搞这个机制就是为了防供应链攻击你一手把它关掉等于主动放弃防护。我的实测经验是如果你只是在本地开发环境临时跑一下为了快速排除问题可以临时关掉但一定要记得改回来。放到 CI 或者生产构建里千万不要这么干。3.4 从 npm 或 yarn 迁移时的特殊处理从 npm 迁移到 pnpm或者从 yarn 迁移到 pnpm 的项目经常会遇到另一个迷惑现象同样一份依赖在 npm 下一切正常换到 pnpm 下就报错。这往往不只是脚本拦截的问题还牵涉到依赖提升机制的差异。npm 会把所有依赖平铺到node_modules根目录很多包虽然你 package.json 里没直接声明但代码里require了一个“漏网之鱼”也能跑。pnpm 采用符号链接 严格依赖隔离你没有声明的依赖模块解析时根本找不到。所以从 npm 迁移到 pnpm 后如果项目报一堆“Cannot find module”不要急着怀疑 pnpm 有 bug。先检查 package.json 里有没有声明这个依赖没声明就补上。这也是 pnpm 的隐形好处它逼你把依赖关系理清楚而不是靠 npm 的“依赖黑洞”碰运气。顺带说一句如果你迁移之后发现某个包的构建脚本被拦截但它实际上没有被任何代码使用那直接把这个包从 package.json 里删掉比放行它的构建脚本更干净。避免引入不必要的安全风险。4. 从报错到修复一次 esbuild 崩溃实战4.1 环境与报错现象讲一个我上周刚处理过的真实案例。一个 Vue 3 Vite 项目从 npm 迁移到 pnpmNode 版本 20pnpm 版本 10.4.1。迁移流程很简单rm -rf node_modules package-lock.json pnpm install安装日志滚动完一切显示成功。然后执行pnpm dev终端立刻弹出 esbuild 的平台不匹配错误。我当时第一反应是 esbuild 版本不对检查了 package.json锁定的版本是 0.21.5市面上已经稳定了不是版本问题。然后我检查了node_modules/.pnpm目录发现 esbuild 相关包确实存在但二进制文件缺失。这时我才意识到是 pnpm 的安全策略拦截了 esbuild 的 postinstall 脚本。4.2 一步步排查与修复我执行的排查流程如下首先运行pnpm ignored-builds输出结果里明确列出了 esbuild。确认问题后我当时没有直接改 package.json而是先跑了一次交互命令pnpm approve-builds在列表里选中 esbuild按回车确认。pnpm 自动写入了配置然后重新执行安装逻辑。之后我再跑pnpm dev项目正常启动问题解决。但这个方案并不是终点。因为我后来发现这个项目还依赖了sharp用于图片处理只是当时还没执行到图片处理相关的代码所以没暴露。我后来重新打开 package.json看到pnpm.onlyBuiltDependencies字段里只有 esbuild又把 sharp 加了进去{ pnpm: { onlyBuiltDependencies: [esbuild, sharp] } }然后重新跑一次pnpm install确认安装日志里 esbuild 和 sharp 的构建脚本都执行了这才算真正处理完。4.3 修复后的验证除了确认项目能正常启动我还建议验证一下原生模块是否真的可用。简单粗暴的方式就是直接在 Node 环境里加载一下node -e require(esbuild); console.log(esbuild ok)如果有具体使用场景比如 sharp可以这样验证node -e const sharp require(sharp); sharp(Buffer.from([1,2,3])).metadata().then(() console.log(sharp ok))加载不报错基本就是真的没问题了。这个步骤很多人会跳过但我觉得值得做特别是原生模块。有些包即使构建脚本执行了也可能会因为 Node 版本、系统架构不匹配而在运行时失败提前验证一下能少走弯路。5. 常见问题与排查技巧实录5.1 pnpm 不是内部或外部命令这个问题严格说不算脚本拦截但因为它和“用 pnpm 构建项目”这个场景绑定太紧密很多新手在遇到拦截报错前先被这个拦住了。Windows 上最常见的原因就是安装 pnpm 时没有把可执行文件目录加到 PATH 环境变量里。解决方法是重新安装 pnpm安装完重启终端或者手动把 pnpm 的全局安装路径添加到 PATH。通过 npm 全局安装时一般会提示全局 bin 目录在哪找到它加进 PATH。建议直接把 pnpm 的全局目录和 Node.js 的全局目录一起配置好一劳永逸。另外如果你是通过 corepack 启用的 pnpm可能还要看 corepack 本身是否正常。我在 Windows 上遇到过 corepack 安装的 pnpm 因为 Node.js 版本更新而失效的情况重装 Node 或重跑一次 corepack 命令就能解决。5.2 corepack 找不到 pnpm.cjs这个报错比较完整的样子是Cannot find module /root/.cache/node/corepack/v1/pnpm/12.4.2/bin/pnpm.cjs我在 Linux 服务器上遇到过一次。原因是 corepack 缓存目录下的 pnpm 文件损坏或者不完整可能是之前安装时网络中断或者磁盘空间不足导致的。解决方式很简单corepack disable corepack enable这会重置 corepack 的 shim 文件。如果还不行手动清理一下 corepack 缓存rm -rf /root/.cache/node/corepack corepack prepare pnpmlatest --activate之后再执行pnpm -v验证版本。这个文件路径里的版本号对应的是你要用的 pnpm 版本删缓存后记得重新用 corepack 指定一下项目需要的版本。5.3 清空 node_modules 后重装依然报错有些项目在遇到诡异问题后第一反应是删掉 node_modules 和 lockfile 重装结果问题依旧。这时候要反思一下你是不是在删掉之前根本没有处理脚本拦截的配置因为pnpm.onlyBuiltDependencies配置是存在 package.json 或 pnpm-workspace.yaml 里的只要你没改配置重装多少遍结果都一样。正确的姿势是先改配置清掉 node_modules再重新 install。另外注意pnpm 使用内容寻址存储全局 store 里可能已经有之前下载过的包重装时不一定重新跑去构建脚本。如果你改了白名单配置发现某些包还是不执行脚本可以试试pnpm rebuild esbuild强制执行某个包的构建脚本。或者更彻底一点pnpm store prune pnpm install把所有缓存清理干净再重来。5.4 只放行了白名单但 husky 钩子依然不生效husky 的原理是在安装时通过 postinstall 脚本自动执行husky init相关的命令把.husky/pre-commit之类文件里的 URL 重写成真实路径。如果你把 husky 加进onlyBuiltDependencies后重新安装发现 git 钩子还是不触发可能是因为你是在以前安装的旧项目里升级上来的.husky目录里的文件已经损坏或格式不对。这个时候不要只依赖安装脚本手动跑一次npx husky init或者按 husky 官方文档重新生成一次钩子文件。pnpm 放行脚本只是给你创造了执行条件原有的文件状态还是要自己确认。5.5 workspace 仓库里配置不生效怎么办monorepo 项目比单包项目更容易踩“配了白名单但没生效”的坑。常见原因是子包的 package.json 里也有pnpm字段覆盖了根目录配置。pnpm 的配置合并策略是子包只会继承根配置的一部分onlyBuiltDependencies这类字段在子包里定义时可能不会自动合并甚至可能被覆盖为空。解决方案统一把白名单配置放在pnpm-workspace.yaml里这是 pnpm 工作区层面的官方推荐位置。确认一下你的 pnpm 版本支持这个文件里的配置项新版 pnpm 在安装时会在日志里提示 “Ignored build scripts” 的同时给出一个运行pnpm approve-builds的建议那个命令会自动把你的选择写进pnpm-workspace.yaml。如果你用的还是老版本 pnpm升级到 10.x 基本能规避绝大多数配置合并的坑。我在升级到 pnpm 10 之后就再也没为 workspace 配置不生效的问题头疼过。6. 一点心得和默认配置建议我花了大篇幅讲原理和操作最后根据个人经验给一套默认配置建议。对于新项目我很建议保留 pnpm 的默认拦截策略只显式添加必要的白名单。哪怕装完报错了也不要第一反应去关闭全局拦截而是花几分钟查一下是哪个包的脚本被拦了确认它是真的需要构建才能用再把它加进白名单。这个过程其实很值它逼你重新审视一遍每个依赖是干什么的变相帮你做了一次依赖审计。我自己就是在一次次的排查里揪出过好几个实际上根本没用到、只是“顺手装上了”的包删掉之后项目体积小了一圈安全隐患也少了一个。对于老项目迁移建议先在本地环境跑一遍pnpm approve-builds把需要的包全部列出来然后人工确认一遍白名单再提交到代码仓库。因为onlyBuiltDependencies是跟着代码走的所有克隆你这个项目的同事都会自动应用同一份信任名单不用每个人都在各自机器上重新折腾一遍。如果项目部署到 CI记得确认 CI 环境使用的 pnpm 版本和本地一致不然安装构建脚本的行为可能有细微差别导致本地能跑、CI 上却崩这类问题排查起来非常费时间。如果实在碰到拿不准的情况先看官方文档优先按官方推荐方式处理别用网上流传的各种“黑魔法”绕开机制。pnpm 对脚本拦截的默认策略是在不断加强的你越能理解它的安全意图就越能在安全性和便利性之间找到平衡。
返回列表