exports 字段必须是对象,不能是数组或字符串;顶层键必须以.开头,值为字符串、对象或null,如".": "./dist/index.cjs";需用conditions(import/require/browser/types等)区分模块类型与环境,路径须用正斜杠且注意大小写。

exports 字段必须是对象,不能是数组或字符串
很多人在第一次写 exports 时直接复制旧版 main 或 module 的写法,结果 npm install 后报错 Invalid "exports" main export。这是因为 exports 是一个严格结构的映射对象,顶层只能是键值对,键必须以 . 开头(如 "./index.js"),值必须是字符串、对象或 null。
常见错误写法:
"exports": "./dist/index.cjs"
这会直接导致 Node.js 加载失败——即使你只用一个入口,也得包一层
".":
"exports": {
".": "./dist/index.cjs"
}
- 顶层
"."是默认入口,等价于以前的main - 任何其他导出路径(如
"./esm"、"./package.json")都必须显式声明 - 不声明的路径一律被禁止访问(这是设计意图,不是 bug)
用条件导出(conditions)区分 ESM/CJS/Node/Browser
跨平台的核心不是“写一堆路径”,而是用 import / require / browser 等条件关键字控制解析逻辑。Node.js 和现代 bundler(Vite、Webpack 5+)都支持这些字段,但行为有差异。
典型配置示例:
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"browser": "./dist/index.browser.js",
"node": "./dist/index.node.js"
}
}
-
import条件匹配 ES 模块导入(import x from "pkg"),优先返回.mjs或带"type": "module"的.js -
require条件匹配require("pkg"),必须返回 CommonJS(.cjs或.js且无type: module) -
browser在 Vite/Webpack 中会被识别;但注意:Node.js 完全忽略 它,只看import/require - 多个条件可共存,运行时按顺序匹配第一个满足的(顺序不重要,语义优先)
动态条件(如 types、development)需要显式声明
TypeScript 类型提示、开发环境专用代码这些不是默认条件,必须手动加进 exports 并确保工具链支持。比如没配 "types",VS Code 就找不到类型定义,即使 index.d.ts 文件存在。
安全写法:
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"development": "./dist/index.dev.js",
"production": "./dist/index.prod.js"
}
}
-
types不影响运行时,但 tsc、IDE 依赖它定位类型 —— 键名必须是"types",拼错(如"typings")无效 -
development/production被 Vite、Webpack、esbuild 支持,但 Node.js 原生不识别;若想在纯 Node 环境也生效,得靠process.env.NODE_ENV+ 动态require回退,exports本身做不到 - 条件之间无继承关系:写了
"import"不代表自动 fallback 到"require",未匹配条件会报ERR_PACKAGE_PATH_NOT_EXPORTED
Windows 路径分隔符和大小写敏感性容易引发本地测试误判
开发时在 macOS/Linux 下一切正常,CI(尤其是 Windows runners)却报 Cannot find module 'xxx',大概率是 exports 里用了反斜杠 \ 或大小写不一致的文件名。
- 所有路径必须用正斜杠
/,Node.js 的exports解析器不处理\(即使你在 Windows 上生成路径) - 文件系统大小写敏感性:macOS 默认不敏感,Linux/CI 通常敏感。确保
"./dist/Index.mjs"对应的文件名真是大写I,否则 Linux 下直接 404 - 验证方式:在目标平台跑
node -e "console.log(require('your-pkg'))"和node -e "import('your-pkg')",别只信本地 import 语法高亮
最麻烦的点其实是嵌套导出和条件组合:比如同时要支持 import "pkg/subpath" + types + browser,就得为每个子路径重复写一遍条件对象。一不留神就漏掉某个组合,而错误信息又极其笼统 —— 此时别猜,直接用 node --trace-module-resolutions 看它到底卡在哪一层解析。











