构建过了不代表 Nitro 能启动:给 server bundle 补运行时 import 检查

有些依赖升级,最麻烦的地方在于 TypeScript 保持沉默。

之前处理 next-auth 安全升级时就遇到过这个问题:next-auth 升上去了,@sidebase/nuxt-auth 里还残留旧入口 next-auth/core。当时那篇文章主要讲的是 package exports 变化,以及为什么要做一个很窄的兼容入口。这个补充工具解决的是另一个问题:后续怎么避免类似问题又悄悄混进 Nitro server bundle。

构建通过不等于 Node 能解析

Nuxt / Nitro 的构建链路会做很多事,但它不等于真的在生产目录下启动一次 Node server。某些裸包 import 可能会被保留到 .output/server 里,直到运行 node .output/server/index.mjs 时,Node resolver 才按 package exports 和当前目录的 node_modules 去解析。

这时常见失败是:

ERR_PACKAGE_PATH_NOT_EXPORTED

它的意思很直接:代码 import 了某个包的子路径,但这个子路径没有被包的 exports 声明公开。安全升级、minor 升级、wrapper 包滞后升级,都可能把这种问题带出来。

TypeScript 没报错也不奇怪。类型声明可能还在,构建器可能只是把 import 留在产物里,或者 dev server 的 alias / optimize 行为和 Nitro server bundle 不完全一样。真正有权判定运行时能不能解析的,是 Node 在产物目录下的 resolver。

检查产物,而不是猜源码

这个工具不从源码里猜风险,而是等构建完成后扫 .output/server。它会读取 .js.mjs.cjs 文件,提取几类运行时 import:

  • import x from 'pkg/subpath'
  • import 'pkg/subpath'
  • export { x } from 'pkg/subpath'
  • await import('pkg/subpath')
  • require('pkg/subpath')

然后它只保留运行时会交给 Node 解析的裸包 specifier。相对路径、绝对路径、node:file:virtual:nitro:#internal 都会跳过,因为它们不属于 package exports 这一类问题。

最后一步直接在 .output/server 目录下启动一个 Node 子进程,逐个执行 import.meta.resolve(specifier)。这样检查条件更接近生产启动时的 Node ESM 解析,也避开了开发目录里的偶然结果。

失败信息要能直接定位

这类检查如果只报一句「有包解析失败」,基本没法用。源码包里把失败格式化成三段:

  • 失败的 specifier。
  • Node resolver 给出的错误码和错误信息。
  • 产物里引用它的文件列表。

这样处理起来会更直接:如果是 wrapper 包继续引用旧入口,就看能不能升级 wrapper;如果上游短期没有修复,再决定是否用 alias 或兼容入口兜住;如果是项目自己 import 了私有子路径,就改回公开 API。

这条检查也不应该替代真实 smoke test。它只回答一个窄问题:server bundle 里保留下来的裸包 import,Node 能不能解析。

源码包可以直接迁移

请把 Nitro Runtime Import Guardrail 接入当前项目。
工具包根路径:https://shengsheng.fun/files/nitro-runtime-import-bundle-check/kits/nitro-runtime-import-guardrail/
先读 README.md、MANIFEST.json、FILES.json、CHANGELOG.md、AGENT_PROMPT.md,再按 FILES.json 读取源码;迁移 copy/scripts/ 和 copy/tests/,在构建后运行 verify:nitro-runtime-imports。
Nitro 运行时 import 检查源码正在加载代码工作区...

迁移后建议把它接到构建验证链路里,而不是只作为手工命令保留。比如先跑 pnpm build 生成 .output/server,再跑 pnpm verify:nitro-runtime-imports。如果项目有轻量的测试构建命令,也可以挂在那里,避免每次都走完整发布构建。

总结

  • build / typecheck 通过,不代表 Nitro server bundle 里的裸包 import 都能被 Node 解析。
  • 检查源码不如检查产物,因为真正运行的是 .output/server
  • 让 Node 在产物目录下执行 import.meta.resolve,比自己模拟 package exports 更稳。
  • 这类工具要保持窄边界:只检查运行时 import 解析,不假装覆盖所有 server 启动问题。