【问题标题】:Compile a package that depends on ESM only library into a CommonJS package将仅依赖 ESM 库的包编译为 CommonJS 包
【发布时间】:2021-12-31 19:20:07
【问题描述】:

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

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

不支持 ES 模块 node_modules\unified\index.js 的 require()

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


参考资料:

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

【问题讨论】:

    标签: node.js typescript es6-modules commonjs


    【解决方案1】:

    总结

    你不能在 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 作为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
    

    现在安装的typescript的版本是"^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
    

    现在安装的typescript的版本是"^4.5.4"

    然后,将./tsconfig.json中的compilerOptions.module值修改回"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();
    

    【讨论】:

    • 非常感谢您的努力!我稍微阅读了dynamic import 上的帖子,了解到我们可以在 CJS 中导入 ESM 模块。但是,我还阅读了ESM vs CJS 上的帖子,更具体地说,是CJS Can import() ESM, but It’s Not Great 部分。就我而言,我确实想导出我的库,以便我的应用程序可以在同步上下文中使用它。如果我使用的第 3 方库仅支持 ESM,我想没有解决方法?
    • @HUIJINGHUANG 不幸的是,没有。如果您不介意分享,是什么阻止您将模块编写和发布为 ESM?
    • 我明白了。我可以将我的模块发布为 ESM。你有一个很好的教程供我学习吗?我尝试 [同时生成 esm 和 commonjs](sensedeep.com/blog/posts/2021/how-to-create-single-source-npm-module.html) 但在我现在使用 ESM 语法的应用程序中,不知何故指向 dist/cjs 文件夹而不是 dist/mjs 文件夹,这是超级有线的!然后我尝试了this tutorial,以便只生成 ESM 模块,但它不起作用。
    • 您可以将要导入的模块作为参考:unified:查看它的package.jsontsconfig.json等。
    • 谢谢@jsejcksn。我终于让它工作了,即使在不同的意义上。我仍然觉得处理这个 esm 与 commonjs 的事情非常痛苦。
    猜你喜欢
    • 2020-02-14
    • 2013-03-02
    • 2013-04-21
    • 2017-08-04
    • 2022-01-17
    • 2012-01-11
    • 1970-01-01
    • 2014-04-04
    • 2011-09-29
    相关资源
    最近更新 更多