CJS 代码中引入 ESM 模块异常:Error [ERR_REQUIRE_ESM]: require() of ES Module

目录
[隐藏]

问题描述:CommonJS 代码中引入 ESM 模块异常

在使用 TypeScript 开发的 Node.js Cli 工具项目中,tsconfig.json 中设置的输出结果为 CommonJS
当引入如 chalkboxen 等外部依赖时,由于这些包的最新版本都是纯 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。

点赞 (0)
  1. kobe说道:
    Safari 5.0.2 Safari 5.0.2 iPhone iOS 4.3.2 iPhone iOS 4.3.2

    :biggrin:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Captcha Code