问题描述:CommonJS 代码中引入 ESM 模块异常
在使用 TypeScript 开发的 Node.js Cli 工具项目中,tsconfig.json
中设置的输出结果为 CommonJS
。
当引入如 chalk
、boxen
等外部依赖时,由于这些包的最新版本都是纯 ES Module
的包,会导致类似如下的报错而不可用:
Error [ERR_REQUIRE_ESM]: require() of ES Module xxx\node_modules\boxen\index.js from abc.ts not supported. Instead change the require of index.js in xxx.ts to a dynamic import() which is available in all CommonJS modules. at Object.newLoader [as .js] (xxx\index.js:141:7) at Object.checkPkgUpdate (abc.ts:86:29) { code: 'ERR_REQUIRE_ESM' }
解决方案
ESM 模块中可以直接使用 import
引入 CommonJS 模块,但是 CommonJS 模块中只能以动态引入(import('xxx')
)的方式引入 ESM 模块。故可有如下几种解决方案。
1. 将自己的项目改为 ESM 方案
如果条件允许,可以将自己的项目也改为 ESM 方案。如对于 ts 类项目,可能将 tsconfig.json
中的 "module": "CommonJS"
改为 "module": "ESNext"
即可。
这是面向 ES 标准未来的最佳方案。前端轮子哥Sindre Sorhus为此专门写了一篇非常详细的迁移 ESM 的指引文档:esm-package.md
CJS 项目迁移至 ESM 后,代码逻辑上的变动主要有:
require
转向import
的写法,不能再使用require
,会报错__dirname
与__filename
修改为使用import.meta.url
兼容- ESM 模块加载相对路径文件时,需改为为包含后缀的完成路径
ESM 兼容 __dirname/__filename
示例:
import { dirname, basename } from 'node:path'; import { fileURLToPath } from 'node:url'; const _dirname = typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(import.meta.url)); const _filename = typeof __filename !== 'undefined' ? __filename : basename(fileURLToPath(import.meta.url));
CJS 项目迁移位 ESM 相关参考:
- https://nodejs.org/api/esm.html#esm_interoperability_with_commonjs
- esm-package.md
- https://blog.csdn.net/qq_21567385/article/details/121440227
- https://devblogs.microsoft.com/typescript/announcing-typescript-4-7-beta/#esm-nodejs
2. 保持使用依赖包最近一个支持 CommonJs 方案的旧版本
该方案原则上可以,许多开源库也是这么做的。但存在一个潜在的安全问题是这类包都不能得到更新,如果它们及其间接依赖的包被暴露出安全问题,则这些安全隐患则无法得到修复。
3. 使用 await import(…)
方式
NodeJs 的 CommonJS
方案支持使用 import(...)
动态导入方式。示例:
// ESM import import boxen from 'boxen'; // 修改为动态导入方式 const { default: boxen } = await import('boxen');
但是由于 tsc 编译输出为 CommonJS
的结果时,实际上会将动态导入全部编译为 require(...)
方式。
4. 使用 eval("import(...)")
方式
基于上面的动态导入方案,只需要避免 tsc 编译即可解决问题。利用 eval
的动态编译特性,可以使用如下示例方法实现。示例:
import type Boxen from 'boxen'; const tips = '...'; const { default: boxen } = await (eval(`import('boxen')`) as Promise<{ default: typeof Boxen }>); console.log(boxen(defaultTemplate));
实测基于 eval
的动态编译方案可解决此类问题,暂作为简单的临时解决方案。
当然,最符合社区标准发展的长期可选方案仍是方案1。