TypeScript编译导致库导入失败(package.json exports配置问题)
我正在开发一个JavaScript库,目标支持script标签引入、ES6导入、Node.js require导入三种方式,同时兼容TypeScript和各类构建工具,但实际实现遇到了不少问题。
我的package.json中相关的exports配置如下:
"exports": { ".": { "require": "./dist/slim.copper.cjs", "import": "./dist/slim.copper.js", "types": "./dist/typed.copper.d.ts" } }
在TypeScript测试项目中,我用import Copper from "@jwrunge/copper";导入库,类型提示和智能感知正常,编译JavaScript也没报错,但运行Node时出现错误:
C:\Users\jrunge\Documents\CODE\copper_test\src\index.js:3 var copper_1 = require("@jwrunge/copper"); ^ Instead change the require of slim.copper.js in C:\Users\jrunge\Documents\CODE\copper_test\src\index.js to a dynamic import() which is available in all CommonJS modules. at Object.<anonymous> (C:\Users\jrunge\Documents\CODE\copper_test\src\index.js:3:16) { code: 'ERR_REQUIRE_ESM' }
TypeScript编译输出的JavaScript代码用了require,还保留了ES模块的风格:
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var copper_1 = require("@jwrunge/copper"); var store1 = copper_1.default.store("foo", { value: 12 });
我在输出的JS里打印copper_1,结果如下,没有default属性,看起来像是直接导入了.cjs版本:
{ store: [Function: store], ustore: [Function: ustore], getFunc: [Function: getFunc], addFuncs: [Function: addFuncs], config: [Function: config] }
另外,如果只提供ES模块(.js文件)不支持require,会触发以下错误:
Error [ERR_REQUIRE_ESM]: require() of ES Module C:\Users\jrunge\Documents\CODE\pubsub\dist\slim.copper.js from C:\Users\jrunge\Documents\CODE\copper_test\src\index.js not supported. Instead change the require of slim.copper.js in C:\Users\jrunge\Documents\CODE\copper_test\src\index.js to a dynamic import() which is available in all CommonJS modules. at Object.<anonymous> (C:\Users\jrunge\Documents\CODE\copper_test\src\index.js:3:16) { code: 'ERR_REQUIRE_ESM' }
我有两个疑问:
- 实际情况是不是JS输出用了库的.cjs版本,而这个版本没有ES模块那样的
default导出? - 在保留require支持的前提下,有哪些优雅/标准的解决方法?我可以接受给用户简单的使用说明,但更希望从库侧优化适配。
问题1的确认
是的,你的判断完全正确。核心原因是:
- TypeScript编译输出了CommonJS格式代码(使用
require),Node.js会匹配package.json exports里的require字段,加载.cjs版本的库。 - 你的
.cjs文件基于CommonJS规范编写,直接导出了方法集合(比如module.exports = { store, ustore... }),没有default属性;而ES模块版本的.js文件用export default导出,TypeScript编译后会尝试访问copper_1.default,但CommonJS版本中不存在该属性,因此报错。
问题2的解决方法
方案1:库侧统一导出格式(推荐)
让CommonJS和ES模块版本的导出结构对齐,同时支持默认导入和命名导入:
- 修改CommonJS版本代码:在直接导出对象的基础上,添加
default属性适配ES模块逻辑:// slim.copper.cjs const copper = { store: function() {}, ustore: function() {}, // ...其他方法 }; module.exports = copper; // 添加default属性,适配ES模块默认导入 module.exports.default = copper; - ES模块版本保持不变:继续使用
export default,也可按需添加命名导出:// slim.copper.js export const store = function() {}; export const ustore = function() {}; // ...其他命名导出 export default { store, ustore, /* ... */ };
这样无论用require还是import,都能正常访问到对应导出内容,TypeScript编译后的代码也不会出错。
方案2:优化package.json配置
在package.json中添加type字段,明确默认模块类型,同时配合exports字段的规则:
{ "type": "module", "exports": { ".": { "require": "./dist/slim.copper.cjs", "import": "./dist/slim.copper.js", "types": "./dist/typed.copper.d.ts" } } }
这个字段告诉Node.js默认文件是ES模块,但exports里的require字段会优先匹配CommonJS版本,确保使用require的用户能正常加载。
方案3:给用户的简单使用提示
如果不想修改库代码,可以告知用户两种环境下的正确导入写法:
- ES模块/TypeScript环境:可使用默认导入或命名导入:
// 方式1:默认导入 import Copper from "@jwrunge/copper"; // 方式2:命名导入(更直观) import { store, ustore } from "@jwrunge/copper"; - CommonJS环境:直接使用require结果,无需访问
default:const Copper = require("@jwrunge/copper"); Copper.store("foo", { value: 12 });
方案4:TypeScript编译配置优化
如果是用户侧的TypeScript项目,可让用户修改tsconfig.json,输出ES模块而非CommonJS:
{ "compilerOptions": { "module": "ESNext", "moduleResolution": "NodeNext", "target": "ES6" } }
编译后的代码会使用import而非require,Node.js会加载ES模块版本,避免default属性找不到的问题。
内容的提问来源于stack exchange,提问作者Jacob Runge

