Dynamically load ESM module in CJS package in TypeScript
Asked Answered
B

2

0

I am trying to load a single ESM module in my TypeScript CJS project. All examples I find are for JavaScript.

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

My issue is that TSC transpiles the import function to a require which breaks everything.

"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
Barone answered 8/5 at 9:45 Comment(0)
B
0

I stumbled across this comment - Not sure if there is currently a better way. At least the eval is used with a constant string so I don't see any linting issues.

// 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
}

Most other solutions complicate the basic premise and either try to mess with the tsconfig.json or package.json, or don't correctly account for TypeScript transpilation.

Barone answered 8/5 at 9:51 Comment(3)
You'd rather use eval than a properly configured TS project? Your linked comment refers to this approach as quick and dirty.Jaala
The project is a legacy project stuck with moduleResolution as Node10 but not able to use Node 22 until it enters LTS status in October. I already tried a combination of ts-config settings, but I still encounter an ESM error deeper within the ESM package itself. This is the only solution that works in our current setup. Ideally however, yes, I would like to migrate the project to ESNext and eventually to ESM once the ecosystem is a little more stableBarone
You should update your question to mention what moduleResolution and/or node version you must use. If you’re able to use node 20 which is LTS then you should be able to update your moduleResolution to avoid the eval. Of course that depends on whatever error you saw from loading the package which should have also been mentioned.Jaala
J
1

Canonical Approach

You most likely need to use module: nodenext in your tsconfig.json.

tsconfig.json

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

package.json

"type": "commonjs"

Now example files:

src/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.log('example', await example())
}

run()

Now run tsc. The output in dist will look like this:

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.log('example', await (0, file_js_1.example)());
}
run();

Alternative Approach

If you can use Node.js >= 22.0.0 and if the ES module you are trying to load into CJS meets this criteria:

  • The module is fully synchronous (contains no top-level await); and
  • One of these conditions are met:
    • The file has a .mjs extension.
    • The file has a .js extension, and the closest package.json contains "type": "module"
    • The file has a .js extension, the closest package.json does not contain "type": "commonjs", and --experimental-detect-module is enabled.

You can use --experimental-require-module.

tsconfig.json

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

package.json

"type": "commonjs"

src/file.ts

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

src/other.ts

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

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

run()

Now run tsc to get the updated output in dist.

Now run 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)
Jaala answered 9/5 at 15:30 Comment(0)
B
0

I stumbled across this comment - Not sure if there is currently a better way. At least the eval is used with a constant string so I don't see any linting issues.

// 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
}

Most other solutions complicate the basic premise and either try to mess with the tsconfig.json or package.json, or don't correctly account for TypeScript transpilation.

Barone answered 8/5 at 9:51 Comment(3)
You'd rather use eval than a properly configured TS project? Your linked comment refers to this approach as quick and dirty.Jaala
The project is a legacy project stuck with moduleResolution as Node10 but not able to use Node 22 until it enters LTS status in October. I already tried a combination of ts-config settings, but I still encounter an ESM error deeper within the ESM package itself. This is the only solution that works in our current setup. Ideally however, yes, I would like to migrate the project to ESNext and eventually to ESM once the ecosystem is a little more stableBarone
You should update your question to mention what moduleResolution and/or node version you must use. If you’re able to use node 20 which is LTS then you should be able to update your moduleResolution to avoid the eval. Of course that depends on whatever error you saw from loading the package which should have also been mentioned.Jaala

© 2022 - 2024 — McMap. All rights reserved.