Node exports 字段的导入失败原因多样(exports缺失、条件不匹配、target为null等),作者开源了一个 exportwhy CLI 可自动诊断。
错误信息很短:Package subpath './internal' is not defined by "exports"。它只告诉你哪失败了,不说为什么,而修复方法取决于具体是哪种原因。
当 Node 拒绝一个包的子路径导入时,原因通常是以下几种之一:
import 的条目在 require 下会失败。null,这是故意阻塞该子路径。exports 字段,而 ESM 需要完整的文件名。要找出是哪种原因,需要打开 node_modules/<pkg>/package.json,然后手动在映射中层层查找,types、import、require、module-sync 和 default 条件嵌套好几层。
我写了一个小 CLI 来完成这个遍历。你在报错的项目中运行它:
npx github:Arthur031221/exportwhy tiny-lib/public
它会让你运行它的那个 Node 来解析当前目录下的路径指定符,分别用 import.meta.resolve 和 createRequire 走一遍,所以 npm 和 pnpm 的目录结构表现和运行时一致。然后它读取已安装包的 exports 映射并解释结果:
import OK node_modules/tiny-lib/dist/public.mjs
exports["./public"].import
require FAIL ERR_PACKAGE_PATH_NOT_EXPORTED
Why "./public" matches, but none of its conditions is active for require.
this entry is import only: load it with import(), or from an ES module.
对于缺失的键,它会列出最近的可公开访问的子路径;当文件在磁盘上存在但被 exports 隐藏时,它也会指出来。对于没有 exports 的包,它会显示 ESM 需要的文件名。两种模式都被拒绝时它以退出码 1 退出,--json 可以输出能直接粘贴到 issue 里的结果。
判断结果始终来自 Node。解释部分是我自己实现的 Node 文档中的查找规则,包括 * 模式匹配和条件数组。如果解释和 Node 的结论不一致,exportwhy 会打印 Node 的答案并说明解释未经确认。测试套件以 npm 和 pnpm 两种目录结构构建手写的包,同时也跑真实安装的包。
它只覆盖 Node 自身的解析。Vite、webpack、TypeScript 和 Bun 对同一个路径指定符可能有不同的接受或拒绝结果。目前是 v0.1,所以可以预期野外的 exports 映射中会有它解释得不好的情况。
如果你遇到了,<specifier> --json 的输出是最有用的 bug 报告:https://github.com/Arthur031221/exportwhy