我扫了 814 个依赖,97 个还敢要 Node 10

昨天在一个空目录里装 nuxt,终端刷出六行警告:

perl 复制代码
npm warn EBADENGINE Unsupported engine {
npm warn EBADENGINE   package: 'nuxt@4.6.4',
npm warn EBADENGINE   required: { node: '^22.22.3 || ^24.15.0 || >=26.0.0' },
npm warn EBADENGINE   current: { node: 'v24.14.1', npm: '11.14.1' }
npm warn EBADENGINE }

我机器上是 Node 24.14.1,差 0.0.9 个版本,不满足。然后 npm 说了句 up to date,装完了。

警告归警告,装归装。

我就想看看这个 engines.node 字段到底有多少人在认真写,于是把六个主流框架的依赖树整个拉了下来,814 个包,逐个查它们的声明。

814 个包里,375 个根本没写 Node 要求,46%。写了的 439 个里,218 个的声明下限低于 Node 18,将近一半。还有 97 个,声明连 Node 10 都接受。

先说口径

不是随便抓几个包。种子是 next、vite、@sveltejs/kit、nuxt、astro、@nestjs/core 六个包的最新版,用 npm 解析出真实依赖树:

bash 复制代码
npm install --package-lock-only

得到了 814 个包的真实版本(含各平台二进制包)。然后按名字+版本去 registry 查每个包的 engines.node,跟它自己的直接依赖比。

这里踩过一个坑,得说清楚。我第一版脚本图省事,拿依赖的 latest 跟父包比,跑出来 postcss 声明支持 Node 10、却「需要」Node 22。核对后发现是假的:postcss 实际锁定的 nanoid 是 3.x,而 latest 的 nanoid 5 才要 Node 22。换成真实的 lockfile 后重跑,那批结论全部作废。 下面所有数字都来自第二版。

「矛盾」的判定是:包自己声明的下限,严格低于它某个直接依赖声明的下限。双方都得有可解析的区间才算数,一共 160 个可比对,13 个对不上,8.1%。

声明和它自己的依赖打架

举几个真实例子:

  • unimport@7.0.2 声明 >=18.12.0,它依赖的 unplugin 要 20.19.0
  • @bruits/satteri-wasm32-wasi@0.11.7 声明 >=14.0.0,依赖的 @napi-rs/wasm-runtime 要 20.19.0
  • browserslist@4.29.5 声明 ^6 || ^7 || ... || >=13.7,依赖的 node-releases 要 18
  • vite-plugin-inspect@11.4.1 声明 >=14,依赖的 unplugin-utils 要 20.19.0

按自己的声明,Node 14 就能装。装上去,它自己的依赖先不答应。

这 13 个只是下限。我只比了直接依赖,传递依赖里的矛盾没算,所以 8.1% 是下限,不是全景。

不匹配会拦截吗?看是谁在装

我把「根包不匹配」和「依赖不匹配」两种情况,在四个包管理器上各跑了一遍:

位置 npm 默认 npm 加 engine-strict pnpm 10 yarn 1
根包不匹配 警告,退出码 0 失败 警告,退出码 0 失败
依赖不匹配 警告,退出码 0 失败 静默,退出码 0 失败

npm 默认相信「你比你依赖的作者更懂你的项目」,所以只劝不拦。加 engine-strict 才动真格:

bash 复制代码
npm install --engine-strict
# 或在 .npmrc 里写 engine-strict=true

pnpm 10.33 是三个里最松的,根包不匹配也只给一行 WARN,退出码 0;依赖不匹配连 WARN 都没有。yarn 1 最硬,两种位置都直接失败,非得加 --ignore-engines 才过得去。

还有一个东西叫 devEngines,是 npm 新推的写法:

json 复制代码
"devEngines": {
  "runtime": { "name": "node", "version": ">=22", "onFail": "error" }
}

同样是不匹配,加上这段之后 npm 默认就失败,EBADDEVENGINES,退出码 1,不需要任何开关。同一个 npm,旧字段当建议,新字段当命令。

97 个「Node 10 也行」

这是我扫之前没想到的数字。

439 个写了声明的包里,97 个的区间允许 Node 10。Node 10 是 2021 年 4 月 EOL 的,2026 年还允许它,这个声明等于没写。

其中 26 个是 @babel 家族,统一写 >=6.9.0------能理解,转译器得往下兼容。剩下 71 个不是。更离谱的是 41 个包,下限写的是 >=0.x:deepmerge 是 >=0.10.0,depd 是 >= 0.8,diff 是 >=0.3.1。Node 0.4 是 2011 年的版本。

再往上数,允许 Node 20 的有 264 个。Node 20 今年 4 月也 EOL 了。也就是说,439 个声明里,一大半在今天这个时间点已经过期。

这不是说这些包在 Node 10 上跑不起来------很多包确实还能跑,babel 那种纯 JS 的转译工具尤其能。问题是:当这个字段允许一切的时候,它就没有保护任何东西。

有人会说

「声明过期不代表包坏了。」对,我整篇都没说包会崩。我说的是另一件事:engines 被当成版本安全网在用,但它默认只警告,写的内容又大面积过期,这个网是漏的。

「我项目锁了 Node 版本,不需要它。」那是另一道防线,nvm、volta、CI 里的 node-version 都算。但依赖是有传递性的,你锁了根,管不住下游某个包偷偷要求更高的版本------我机器上那六行警告就是 nuxt 的传递依赖冒出来的。

「小项目装得上就行。」同意。这个字段真正的价值不在安装,在排查:线上挂了、本地好好的,先看一遍 engines 有没有人越界,比瞎猜快。

我现在怎么用

三个动作,五分钟:

bash 复制代码
# 1. 看自己的声明和 CI 里的 node-version 对不对得上
npm pkg get engines
bash 复制代码
# 2. 装的时候把警告当回事(CI 里加这一行)
npm install --engine-strict
bash 复制代码
# 3. 想知道哪些依赖越界了,重新装一遍,数 EBADENGINE
rm -rf node_modules package-lock.json && npm install 2>&1 | grep -c EBADENGINE

新项目别再用 engines 当开关了,直接上 devEngines + onFail: error,npm 默认就拦。

我的立场很简单:这个字段不是没用,是被用成了「写了就算做过」。写了 >=8,和写「Node 环境请自行保证」,效果一样。

你项目里的 engines 写的是多少?跑一下 npm pkg get engines,评论区报个数,我看看有多少人还停在 14 以下。


相关推荐
陈随易22 分钟前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员
IT_陈寒29 分钟前
Vue的这个响应式陷阱,差点让我加班到天亮
前端·人工智能·后端
程序员大阳1 小时前
HTML+CSS+JS对话式实战(9)-项目总结与问题分析
javascript·css·html·实战·项目总结
Flynt2 小时前
别写拖拽行为,只声明"能拖到哪":UIST 最佳论文的拖拽方案我实测了
前端·svg·preact
Moment2 小时前
代码都能让 Agent 写了,我们为什么还要学前端?
前端·后端·面试
Hilaku2 小时前
MCP 协议会成为前端 AI 工程的标准接口吗?
前端·javascript·程序员
Zan3 小时前
Tauri 2 实战:为什么一个可靠的桌面应用,应该由 Rust Host 管理生命周期?
前端·架构·rust
SameX3 小时前
鸿蒙 ArkTS 独立开发者的项目结构 model / dao / viewmodel / page / common 的分层
前端
阿星AI工作室3 小时前
WorkBuddy做数字人口播:动效包装+克隆声音+真实照片+对口型全流程
前端