编译依赖 ESM only 库的包成 CommonJS 包

Compile a package that depends on ESM only library into a CommonJS package

我正在开发一个依赖于仅 ESM 库的包:unified 并且我将我的 npm 包公开为 CommonJS 库。

当我在应用程序中调用我的包时,node 给我这个错误消息:

require() of ES Module node_modules\unified\index.js not supported

错误消息很明显,因为我们不允许 require ESM 模块,但我不是已经告诉 Typescript 将源代码编译成 CommonJS 格式吗?


参考文献:

  1. ESM vs CommonJS
  2. How to Create a Hybrid NPM Module for ESM and CommonJS

总结

你不能在 CJS 中使用 static import statements:没有办法绕过它。

然而,如果您只需要在异步上下文中使用模块,则可以通过 dynamic import statements 使用 ES 模块。但是,TypeScript 的当前状态在这种方法方面引入了一些复杂性。


操作方法

考虑这个示例,其中我使用您提到的模块设置了一个 CJS TS 存储库,并且我已经配置了 npm test 脚本来编译和 运行 输出。我已将以下文件放入一个空目录(我在这个 Stack Overflow 问题的 ID 之后将其命名为 so-70545129):

文件

./package.json

{
  "name": "so-70545129",
  "version": "1.0.0",
  "description": "",
  "type": "commonjs",
  "main": "dist/index.js",
  "scripts": {
    "compile": "tsc",
    "test": "npm run compile && node dist/index.js"
  },
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "@types/node": "^17.0.5",
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "unified": "^10.1.1"
  }
}

./tsconfig.json

{
  "compilerOptions": {
    "exactOptionalPropertyTypes": true,
    "isolatedModules": true,
    "lib": [
      "ESNext"
    ],
    "module": "CommonJS",
    "moduleResolution": "Node",
    "noUncheckedIndexedAccess": true,
    "outDir": "dist",
    "strict": true,
    "target": "ESNext",
  },
  "include": [
    "./src/**/*"
  ]
}

./src/index.ts

import {unified} from 'unified';

function logUnified (): void {
  console.log('This is unified:', unified);
}

logUnified();


现在,运行 npm install 和 运行 test 脚本:

$ npm install
--- snip ---

$ npm run test

> so-70545129@1.0.0 test
> npm run compile && node dist/index.js


> so-70545129@1.0.0 compile
> tsc

/so-70545129/dist/index.js:3
const unified_1 = require("unified");
                  ^

Error [ERR_REQUIRE_ESM]: require() of ES Module /so-70545129/node_modules/unified/index.js from /so-70545129/dist/index.js not supported.
Instead change the require of /so-70545129/node_modules/unified/index.js in /so-70545129/dist/index.js to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (/so-70545129/dist/index.js:3:19) {
  code: 'ERR_REQUIRE_ESM'
}

作为参考,输出如下:./dist/index.js:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const unified_1 = require("unified");
function logUnified() {
    console.log('This is unified:', unified_1.unified);
}
logUnified();

上面的错误解释了问题(我在这个答案的顶部总结了)。 TypeScript 已将静态 import 语句转换为 require 的调用,因为模块类型是“CommonJS”。让我们修改 ./src/index.ts 以使用动态导入:

import {type Processor} from 'unified';

/**
 * `unified` does not export the type of its main function,
 * but you can easily recreate it:
 *
 * Ref: https://github.com/unifiedjs/unified/blob/10.1.1/index.d.ts#L863
 */
type Unified = () => Processor;

/**
 * AFAIK, all envs which support Node cache modules,
 * but, just in case, you can memoize it:
 */
let unified: Unified | undefined;
async function getUnified (): Promise<Unified> {
  if (typeof unified !== 'undefined') return unified;
  const mod = await import('unified');
  ({unified} = mod);
  return unified;
}

async function logUnified (): Promise<void> {
  const unified = await getUnified();
  console.log('This is unified:', unified);
}

logUnified();

运行 test 脚本再次:

$ npm run test

> so-70545129@1.0.0 test
> npm run compile && node dist/index.js


> so-70545129@1.0.0 compile
> tsc

node:internal/process/promises:246
          triggerUncaughtException(err, true /* fromPromise */);
          ^

Error [ERR_REQUIRE_ESM]: require() of ES Module /so-70545129/node_modules/unified/index.js from /so-70545129/dist/index.js not supported.
Instead change the require of /so-70545129/node_modules/unified/index.js in /so-70545129/dist/index.js to a dynamic import() which is available in all CommonJS modules.
    at /so-70545129/dist/index.js:11:52
    at async getUnified (/so-70545129/dist/index.js:11:17)
    at async logUnified (/so-70545129/dist/index.js:16:21) {
  code: 'ERR_REQUIRE_ESM'
}

障碍

嗯,我们不是刚刚解决了这个问题吗?让我们看一下输出:./dist/index.js:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
 * AFAIK, all envs which support Node cache modules,
 * but, just in case, you can memoize it:
 */
let unified;
async function getUnified() {
    if (typeof unified !== 'undefined')
        return unified;
    const mod = await Promise.resolve().then(() => require('unified'));
    ({ unified } = mod);
    return unified;
}
async function logUnified() {
    const unified = await getUnified();
    console.log('This is unified:', unified);
}
logUnified();

解决方案

为什么对 require 的调用仍然存在?这个 GitHub issue ms/TS#43329 解释了为什么 TS 仍然这样编译,并提供了两个解决方案:

  1. 在您的 TSConfig 中,set compilerOptions.module to "node12"(或 nodenext)。

  2. 如果#1 不是一个选项(你没有在你的问题中说),使用 eval as a workaround

让我们探讨一下这两个选项:

方案一:修改TSConfig

让我们修改./tsconfig.json中的compilerOptions.module值:

{
  "compilerOptions": {
    ...
    "module": "node12",
    ...
  },
...
}

又是运行:

$ npm run test

> so-70545129@1.0.0 test
> npm run compile && node dist/index.js


> so-70545129@1.0.0 compile
> tsc

tsconfig.json:8:15 - error TS4124: Compiler option 'module' of value 'node12' is unstable. Use nightly TypeScript to silence this error. Try updating with 'npm install -D typescript@next'.

8     "module": "node12",
                ~~~~~~~~


Found 1 error.

另一个编译器错误!让我们按照诊断消息中的建议来解决它:将 TS 更新到不稳定版本 typescript@next:

$ npm uninstall typescript && npm install --save-dev typescript@next
--- snip ---

$ npm ls
so-70545129@1.0.0 /so-70545129
├── @types/node@17.0.5
├── typescript@4.6.0-dev.20211231
└── unified@10.1.1

The version of typescript now installed is "^4.6.0-dev.20211231"

让我们再运行:

$ npm run test

> so-70545129@1.0.0 test
> npm run compile && node dist/index.js


> so-70545129@1.0.0 compile
> tsc

node:internal/process/promises:246
          triggerUncaughtException(err, true /* fromPromise */);
          ^

Error [ERR_REQUIRE_ESM]: require() of ES Module /so-70545129/node_modules/unified/index.js from /so-70545129/dist/index.js not supported.
Instead change the require of /so-70545129/node_modules/unified/index.js in /so-70545129/dist/index.js to a dynamic import() which is available in all CommonJS modules.
    at /so-70545129/dist/index.js:30:65
    at async getUnified (/so-70545129/dist/index.js:30:17)
    at async logUnified (/so-70545129/dist/index.js:35:21) {
  code: 'ERR_REQUIRE_ESM'
}

还是一样的错误。这是检查的输出:./dist/index.js:

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
/**
 * AFAIK, all envs which support Node cache modules,
 * but, just in case, you can memoize it:
 */
let unified;
async function getUnified() {
    if (typeof unified !== 'undefined')
        return unified;
    const mod = await Promise.resolve().then(() => __importStar(require('unified')));
    ({ unified } = mod);
    return unified;
}
async function logUnified() {
    const unified = await getUnified();
    console.log('This is unified:', unified);
}
logUnified();

TS 仍在将动态 import 转换为对 require 的调用,即使我们已遵循所有诊断消息建议并正确配置了项目。在这一点上这似乎是一个错误。

让我们尝试解决方法,但首先,让我们撤消刚刚所做的更改:

首先卸载不稳定版typescript,重新安装稳定版:

$ npm uninstall typescript && npm install --save-dev typescript
--- snip ---

$ npm ls
so-70545129@1.0.0 /so-70545129
├── @types/node@17.0.5
├── typescript@4.5.4
└── unified@10.1.1

The version of typescript now installed is "^4.5.4"

然后,将compilerOptions.module的值修改回./tsconfig.json中的"CommonJS"

{
  "compilerOptions": {
    ...
    "module": "CommonJS",
    ...
  },
...
}

解决方案 2:使用 eval

的解决方法

让我们修改 ./src/index.ts,特别是函数 getUnified(第 16-21 行):

目前看起来是这样的:

async function getUnified (): Promise<Unified> {
  if (typeof unified !== 'undefined') return unified;
  const mod = await import('unified');
  ({unified} = mod);
  return unified;
}

而TS拒绝停止转换的问题语句在第18行:

const mod = await import('unified');

让我们将其移动到字符串文字中并在 运行 时使用 eval 对其进行评估,以便 TS 不会对其进行转换:

// before:
const mod = await import('unified');

// after:
const mod = await (eval(`import('unified')`) as Promise<typeof import('unified')>);

所以整个函数现在看起来像这样:

async function getUnified (): Promise<Unified> {
  if (typeof unified !== 'undefined') return unified;
  const mod = await (eval(`import('unified')`) as Promise<typeof import('unified')>);
  ({unified} = mod);
  return unified;
}

保存文件并再次运行:

$ npm run test

> so-70545129@1.0.0 test
> npm run compile && node dist/index.js


> so-70545129@1.0.0 compile
> tsc

This is unified: [Function: processor] {
  data: [Function: data],
  Parser: undefined,
  Compiler: undefined,
  freeze: [Function: freeze],
  attachers: [],
  use: [Function: use],
  parse: [Function: parse],
  stringify: [Function: stringify],
  run: [Function: run],
  runSync: [Function: runSync],
  process: [Function: process],
  processSync: [Function: processSync]
}

终于!达到了预期的结果。让我们最后一次比较输出:./dist/index.js:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
 * AFAIK, all envs which support Node cache modules,
 * but, just in case, you can memoize it:
 */
let unified;
async function getUnified() {
    if (typeof unified !== 'undefined')
        return unified;
    const mod = await eval(`import('unified')`);
    ({ unified } = mod);
    return unified;
}
async function logUnified() {
    const unified = await getUnified();
    console.log('This is unified:', unified);
}
logUnified();

这就是我们想要的:动态 import 语句未转换为 require 调用。

现在,当您需要使用unified函数时,只需在您的程序中使用这个语法:

const unified = await getUnified();