Динамически загружать модуль ESM в пакете CJS в TypeScript

Я пытаюсь загрузить один модуль ESM в свой проект TypeScript CJS. Все примеры, которые я нахожу, предназначены для JavaScript.

// example.ts
export const example = async () => {
  const module = await import("esm-module");
  return module.func
}

Моя проблема в том, что TSC переносит функцию import в требование, которое все ломает.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.example = void 0;
const example = async () => {
    const module = await Promise.resolve().then(() => require("esm-module")); // Not desired
    return module.func;
};
exports.example = example

166
2

Ответы:

Решено

Я наткнулся на этот комментарий - Не уверен, что сейчас есть лучший способ. По крайней мере, eval используется с постоянной строкой, поэтому я не вижу проблем с линтингом.

// example.ts
type EsmModule = typeof import("esm-module");

export const example = async () => {
  const module = await (eval('import("esm-module")') as Promise<EsmModule>);
  return module.func
}

Большинство других решений усложняют основную предпосылку и либо пытаются испортить tsconfig.json или package.json, либо неправильно учитывают транспиляцию TypeScript.


Канонический подход

Скорее всего, вам придется использовать module: nodenext в своем tsconfig.json.

tsconfig.json

{
    "compilerOptions": {
        "target": "ESNext",
        "module": "NodeNext",
        "moduleResolution": "NodeNext",
        "outDir": "dist"
    },
    "include": ["src"]
}

пакет.json

"type": "commonjs"

Теперь примеры файлов:

источник/file.ts

export const example = async () => {
    // find-up is a popular ES module on NPM
    const module = await import("find-up");
    return module.findUp
}

src/other.ts

import { example } from './file.js'

async function run() {
    console.info('example', await example())
}

run()

Теперь бегите tsc. Вывод в dist будет выглядеть так:

dist/file.js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.example = void 0;
const example = async () => {
    const module = await import("find-up");
    return module.findUp;
};
exports.example = example;

dist/other.js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const file_js_1 = require("./file.js");
async function run() {
    console.info('example', await (0, file_js_1.example)());
}
run();

Альтернативный подход

Если вы можете использовать Node.js >= 22.0.0 и если модуль ES, который вы пытаетесь загрузить в CJS, соответствует этому критерию:

  • Модуль полностью синхронный (не содержит верхнего уровня await); и
  • Одно из этих условий соблюдено:
    • Файл имеет расширение .mjs.
    • Файл имеет расширение .js, а ближайший package.json содержит "type": "module".
    • Файл имеет расширение .js, ближайший package.json не содержит "type": "commonjs" и --experimental-detect-module включен.

Вы можете использовать --experimental-require-module.

tsconfig.json

{
    "compilerOptions": {
        "target": "ESNext",
        "module": "CommonJS",
        "moduleResolution": "Node",
        "outDir": "dist"
    },
    "include": ["src"]
}

пакет.json

"type": "commonjs"

источник/file.ts

export const example = () => {
    const module = require("find-up")
    
    return module.findUp
}

src/other.ts

import { example } from './file.js'

function run() {
    console.info('example', example())
}

run()

Теперь запустите tsc, чтобы получить обновленный результат в dist.

Теперь запустите node --experimental-require-module dist/other.js:

example [AsyncFunction: findUp]
(node:37823) ExperimentalWarning: Support for loading ES Module in require() is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)