How to prevent typescript from transpiling dynamic imports into require()?
Asked Answered
F

8

35

I'm building a discord.js Discord bot. Now for some reason, discord.js doesn't work with ESM modules (a totally separate issue), so my bot app uses CommonJS modules. Now I have another project on my system called Lib, which has a lot of utility functions that I plan to use in several different projects so I don't have to rewrite them. This Lib project uses ESM modules. Since I have to import Lib from DiscordBot, I use the dynamic import syntax in typescript. Now, whenever I transpile my DiscordBot project, the dynamic imports get converted into some ugly javascript module code, and that ugly module code ultimately ends up using require(). Since require() can't import ESM modules, my bot ends up crashing.

I tried however to stop my ts compiler, copy the code from my ts file that imports Lib then pasting that code into the corresponding JS file manually (and removing TS-exclusive features like type annotations and interfaces). Then I ran my bot app, and it worked perfectly fine. But I don't want to have to do this every time. So it's tsc's compiling that's the problem. How do I fix this?

Frederick answered 12/12, 2020 at 13:22 Comment(3)
Have you explored the module option?Colyer
Well, yes of course. That's how I set Lib to be a ESM module and DiscordBot to be a CommonJS module.Frederick
Can you share your tsconfig.json here?Katabolism
C
11

So I understand the purpose is:

  1. Develop the code in TypeScript
  2. Run the compiled code in CommonJS package
  3. Import and use an ES Module

Option 1:

If "module" in tsconfig.json is set to "commonjs", currently there's no way to prevent TypeScript from transpiling dynamic import() into require() - except that you hide the code in a string and use eval to execute it. Like this:

async function body (pMap:any){
   // do something with module pMap here
}

eval ("import('p-map').then(body)");

No way TypeScript transpiles a string!


Option 2

Set "module" in tsconfig.json to "es2020". By doing this, dynamic import would not be transpiled into require(), and you can use dynamic import to import a CommonJS or ES Module. Or, you can use the const someModule = require("someModule") syntax to import a CommonJS module (would not be transpiled to ES6 import syntax). You cannot use the ES6 import syntax such as import * as someModule from "someModule" or import someModule from "someModule". These syntaxes will emit ES Module syntax imports ("module" is set to "es2020") and cannot be run in CommonJS package.


Below is a bit information:

  • If "module" is set to "es2020": dynamic import import() is not transpiled.

  • If "module" is set to `"es2015": there's an error:

    TS1323: Dynamic imports are only supported when the '--module' flag is set to 'es2020', 'esnext', 'commonjs', 'amd', 'system', or 'umd'.

  • If "module" is set to "commonjs": dynamic imports are transpiled.

Quote tsconfig.json reference for "module" field:

If you are wondering about the difference between ES2015 and ES2020, ES2020 adds support for dynamic imports, and import.meta.

Catkin answered 5/6, 2021 at 11:53 Comment(7)
I asked OP for a tsconfig.json but I now realise how old the question is and it only came up for me because you put a bounty on it. Would you be able to share a tsconfig.json? Even better would be a minimal test case. Perhaps in a paste or maybe even a new question.Katabolism
My answer has nothing to do with tsconfig.json. If a setting in tsconfig.json can prevent typescript from transpiring dynamic import into require(), that would be a beautify answer. My answer is ugly - it hides the dynamic import syntax in a string, and uses 'eval' to execute it. Typescript won't transpire a string.Catkin
I followed this and had no success; the dynamically imported code still shows up in the compiled javascript. Anything missing? Note, I also had to add "moduleResolution": "node",Unify
@AndreiS are you using option 2? Also note the original comment: "By doing this, dynamic import would not be transpiled into require(), and you can use dynamic import to import a CommonJS or ES Module." So the purpose is to keep the original dynamic import code, NOT to transpile it.Catkin
Thanks @BingRen! Yes, I was using option 2. The code still transpiled (meaning the "required" code was brought into the built package)Unify
Probably give javascript.plainenglish.io/… a readCatkin
Although this works (also with new Function(...)), for some reason any module imported this way has its source code logged to console by Node.js (at least in my project).Waver
M
9

The node12 setting others are talking about did not work for me, but these compilerOptions did, using Typescript 4.7.2:

"module": "CommonJS",
"moduleResolution": "Node16",

This saved my backside, I did not have to migrate all import requires to imports to be able to use an ESM npm lib.

Typescript input source:

import Redis = require('redis');
import * as _ from 'lodash';

export async function main() {
    const fileType = await import('file-type');
    console.log(fileType, _.get, Redis);
}

CommonJS output:

...
const Redis = require("redis");
const _ = __importStar(require("lodash"));
async function main() {
    const fileType = await import('file-type');
    console.log(fileType, _.get, Redis);
}
exports.main = main;
Mayhew answered 4/7, 2022 at 13:57 Comment(2)
Unfortunately 'Node16' apparently breaks if you try to import a hybrid module with a static import...Curzon
This works well in my case. I have an NPM package that compiles to both CJS and ESM, and I use a library that only supports ESM. So, I have two separate TSConfig files, and in tsconfig.cjs.json, I have this "moduleResolution": "Node16", option set. Unfortunately, I do have to have the wrapped async function for importing any third-party modules that do not have their own CJS option set up, but that's only Chalk in my library.Yamamoto
S
6

This is currently not possible. There is a very new issue at GitHub (https://github.com/microsoft/TypeScript/issues/43329), but that is not implemented yet. So everything you can do now is to switch from ESM to CommonJS with your Lib project.


Update 2022

The issue has been closed and there is now a new option for "module" called node12. That should fix the problem

Subclavian answered 6/6, 2021 at 8:37 Comment(1)
It is possible. See my answer below. It's ugly, but it works.Catkin
C
5

I'm using a variant of the already mentioned eval-based hack to overcome this issue.

So for example, parse-domain is distributed as an ESM module, so importing it like this breaks in a CJS-based node app:

import { fromUrl, parseDomain } from 'parse-domain';

const parseDomainFromUrl = (url: string) => {
    return parseDomain(fromUrl(url));
}

And this is how I have managed to get it working:

const dynamicImport = new Function('specifier', 'return import(specifier)');

const parseDomainFromUrl = (url: string) => {
  return dynamicImport('parse-domain').then((module: any) => {
    const { fromUrl, parseDomain } = module;
    return parseDomain(fromUrl(url));
  })
};

(Note that parseDomainFromUrl became asynchronous in the process, so it would need to be awaited by the caller.)

Cicisbeo answered 30/9, 2022 at 11:40 Comment(0)
S
3

I'm late for party but I want to leave here my sollution too. Using js eval (no package required, this is a native js function) I've created an async typed esm module importer:

export async function importEsmModule<T>(
  name: string
): Promise<T> {
  const module = eval(
    `(async () => {return await import("${ name }")})()`
  )
  return module as T
}

//
// usage
//

import type MyModuleType from 'myesmmodule'

const MyModule = await importEsmModule<typeof MyModuleType>('myesmmodule')

For me this works like a charm :)

If you prefer, I have also created this npm package.

Hope this helps someone!

Shelton answered 24/10, 2023 at 18:52 Comment(1)
This is the BEST ANSWER by far. Thanks my man, you the boss.Spermaceti
P
2

Adding my answer because this is apparently still an issue in 2023, and none of the solutions, save the eval hack is foolproof. Using anything other than commonjs as the module setting will likely break compilation for other modules. In our case, loads of firebase modules have issues on setting anything other than commonjs.

Specifically, we needed to stop typescript from transpiling dynamic imports down to require

The option that worked for us was to set the module option to "NodeNext" and then skipLibCheck set to true to suppress the error that was cause by one of the dependency that we imported. Its not like we can really change anything with that dependency (since it was used by a googlecloud module).

Depending on your view, this is not ideal, but neither is the js module system. Pasting our tsconfig.json config below:

{
  "compilerOptions": {
    "module": "NodeNext",
    "skipLibCheck": true,
    "allowSyntheticDefaultImports": true,
    "noImplicitReturns": true,
    "noUnusedLocals": false,
    "outDir": "lib",
    "sourceMap": true,
    "strict": true,
    "target": "ES2020",
    "esModuleInterop": true
  },
  "compileOnSave": true,
  "include": ["src"],
  "exclude": ["debug_scripts"]
}
Precept answered 21/9, 2023 at 9:54 Comment(0)
P
1

This has been fixed with the addition of the node12 option for the module setting. From the docs:

Available in nightly builds, the experimental node12 and nodenext modes integrate with Node’s native ECMAScript Module support. The emitted JavaScript uses either CommonJS or ES2020 output depending on the file extension and the value of the type setting in the nearest package.json. Module resolution also works differently. You can learn more in the handbook.

If you use this setting without a nightly build, however, it currently produces the following error:

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'.

Prong answered 19/2, 2022 at 15:11 Comment(0)
T
0

What compiler/bundler are you using? I am assuming tsc based on context.

I recommend using esbuild to compile and bundle your TS. You can also use it simply to transform it after using tsc. It has an option called "format" that can remove any module-style imports. See https://esbuild.github.io/api/#format.

Here is a simple example of using.

build.js

const esbuild = require("esbuild");

esbuild.build({
   allowOverwrite: true,
   write: true,
   entryPoints: ["my-main-file.ts"],
   outfile: "some-file.bundle.js",
   format: "cjs", //format option set to cjs makes all imports common-js style
   bundle: true,
}).then(() => {
   console.log("Done!");
});

You can then add something like this to your package.json

"scripts": {
        "build": "node build.js",
...rest of scripts

Here is an additional link about some caveats using esbuild with typescript. None of these should really be a problem for you. https://esbuild.github.io/content-types/#typescript-caveats

Transliterate answered 11/6, 2021 at 23:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.