javascript - Predvolená inštancia triedy vedie k TypeError (ESM/CJS interoperabilita)


Zhrnutie problému

Ahoj,

Mám projekt v TypeScripte, kde sa snažím vytvoriť inštanciu triedy, ktorá bola predvoleným exportom z iného balíka. Píšem svoj projekt v syntaxi ESM, zatiaľ čo balík, na ktorom závisí, má výstup CJS. Problém, na ktorý narážam, je, že počas behu programu, keď tok programu dosiahne bod inštancie triedy, dostávam nasledujúcu chybu -

new TestClass({ arg1: "Hello, World!" });
^

TypeError: TestClass is not a constructor

Kód

//My package.json
{
  "name": "myproject",
  "version": "1.0.0",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "type": "module",
  "dependencies": {
    "testpackage": "^1.0.0",
    "typescript": "^5.0.4"
  },
  "devDependencies": {
    "@types/node": "^20.2.5"
  }
}

//My index.ts
import TestClass from "testpackage";

new TestClass({ arg1: "Hello, World!" });
//My tsconfig.json
{
    "include": ["src"],
    "compilerOptions": {
        "outDir": "dist",
        "lib": ["es2023"],
        "target": "es2022",
        "moduleResolution": "node"
    }
}

//Dependency's package.json
{
    "name": "testpackage",
    "version": "1.0.0",
    "description": "TestPackage",
    "main": "./dist/testFile.js",
    "exports": "./dist/testFile.js",
    "scripts": {
        "build": "tsc"
    },
    "files": ["dist"],
    "devDependencies": {
        "@types/node": "^20.2.5",
        "typescript": "^5.0.4"
    }
}
//Dependency's testFile.ts
export default class TestClass {
    constructor({ arg1 }: { arg1: string }) {
        console.log(arg1);
    }
}
//Dependency's tsconfig.json
{
    "include": ["src"],
    "compilerOptions": {
        "declaration": true,
        "lib": ["es2023"],
        "target": "es6",
        "module": "CommonJS",
        "outDir": "dist"
    }
}
//Dependency's testFile.js output
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class TestClass {
    constructor({ arg1 }) {
        console.log(arg1);
    }
}
exports.default = TestClass;

Všetko funguje dobre, ak odstránim "type": "module" z mojho package.json. Funguje to aj vtedy, ak je trieda pomenovaný export namiesto predvoleného exportu v kóde závislosti. Je toto známa nekompatibilita pri pokuse importovať CJS do ESM alebo tu robím niečo nesprávne?


Poznámka - Ak nastavím "moduleResolution": "nodenext" v mojom tsconfig.json, chyba sa vygeneruje už počas kompilácie -

src/index.ts:3:5 - error TS2351: This expression is not constructable.
  Type 'typeof import("<project_dir>/node_modules/testpackage/dist/testFile")' has no construct signatures.

3 new TestClass({ arg1: "Hello, World!" });
      ~~~~~~~~~


Found 1 error in src/index.ts:3

Odpoveď č. 1

Existujú známe problémy s kompatibilitou medzi CommonJS (CJS) a modulmi ECMAScript (ESM). V ESM sú predvolené exporty modulov CJS zabalené do vlastností default namiesto toho, aby boli priamo prístupné. Na druhej strane, pomenované exporty nie sú ovplyvnené a môžu byť importované priamo.

Ak špecifikujete „type“: Špecifikovanie „module“ v súbore package.json spôsobí, že Node.js bude považovať súbor .js za ESM. Preto musíte modul importovať pomocou ESM import príkazu. Ak však modul, ktorý sa pokúšate importovať, je vo formáte CJS, narazíte na problémy s kompatibilitou.

Existuje niekoľko možností, ako to opraviť.

  1. Pristupujte ku triede cez predvolenú vlastnosť, ako je popísané vyššie.
import Test from 'package-name';
const TestClass = Test.default;

  1. Aby ste sa vyhli problémom spôsobeným miešaním dvoch formátov modulov, preveďte celý kód na používanie buď ESM, alebo CJS.

  2. Načítajte modul CJS pomocou funkcie createRequire v Node.js.

import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const TestClass = require('package-name');