构建过了不代表 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。迁移后建议把它接到构建验证链路里,而不是只作为手工命令保留。比如先跑 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 启动问题。