AlexZeDim
AlexZeDim

Reputation: 4352

Unable to import ESM module in Nestjs

I am having a problem with importing ESM modules in my project based on Nest.js. As far as I understand, this problem is relevant not just to Nest.js but typescript as well.

I have tried various things and combinations of Node.js & typescript versions, adding "type":"module" to package.json & changes in the settings of my tsconfig.json file, so it has the following view, which is far from default values:

{
  "compilerOptions": {
    "lib": ["ES2020"],
    "esModuleInterop": true,
    "module": "NodeNext",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "Node",
    "target": "esnext",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false,
  }
}

My full environment is:

But it still gives me an error when I am trying to import any ESM module in any of my services. For example:

import random from `random`;

export class AppService implements OnApplicationBootstrap {
  async test() {
     const r = random.int(1, 5);
     console.log(r);
  }
}

Does anyone have a clue how to fix it?

Upvotes: 30

Views: 26791

Answers (4)

Marius CON
Marius CON

Reputation: 11

You could do a file utils.ts

import {DataTransformerOptions} from "@trpc/server";

const dynamicImport = async (packageName: string) =>
    new Function(`return import('${packageName}')`)();


let superjson: DataTransformerOptions;

export async function initDynamicImports() {
    if (!superjson) {
        superjson = (await dynamicImport('superjson')).default;
    }
}

export let getSuperJson = () => superjson;

And than in main.ts

import {NestFactory} from '@nestjs/core';
import {AppModule} from './app.module';
import {FastifyAdapter, NestFastifyApplication} from "@nestjs/platform-fastify";
import {ConfigService} from "@nestjs/config";
import {initDynamicImports} from "@server/utils";


async function bootstrap() {

    await initDynamicImports()
    const app = await NestFactory.create<NestFastifyApplication>(
        AppModule,
        new FastifyAdapter(),
    );
    app.enableCors();
    const configService = app.get(ConfigService);
    const PORT = configService.get<number>('PORT') || 4000;
    await app.listen(PORT, '0.0.0.0');
}

bootstrap();

Then just use the function to get the package .

Upvotes: 1

With Node v22 you can use --experimental-require-module flag

The package.json will look something similar to this:

{
  "scripts": {
    "start": "nest start -e 'node --experimental-require-module'",
    "start:dev": "nest start --watch -e 'node --experimental-require-module'",
    "start:prod": "node --experimental-require-module dist/main"
  }
}

Upvotes: 10

Usman Sabuwala
Usman Sabuwala

Reputation: 898

Someone in the Nest.js Discord Server suggested doing the below and it worked with no overhead

  1. Use node version 22.4.0
  2. Create an .npmrc file with the following content
node-options=--experimental-require-module

Upvotes: 7

Niko Hadouken
Niko Hadouken

Reputation: 781

This Problem seems to occur more frequently since more packages are switching over to be distributed as ES module.

Summary

  • ES Modules can import CommonJS modules
  • CommonJS modules cannot import ES Modules synchronously
  • NestJS does currently not support to compile to ESM - see this PR

There are two approaches for this problem.

Import package asynchronously using the dynamic import() function

The instruction to use import() for ES modules in CommonJS can be found everywhere. But when using typescript the additional hint is missing how to prevent the compiler to transform the import() call to a require(). I found two options for this:

  • set moduleResolution to nodenext or node16 in your tsconfig.json (Variant 1)
  • use the eval workaround - this is based on the section "Solution 2: Workaround using eval" from answer https://stackoverflow.com/a/70546326/13839825 (Variant 2 & 3)

Variant 1: await import() whenever needed

This solution is often suggested on the official NestJS Discord

  • add "moduleResolution": "nodenext" or "moduleResolution": "node16" to your tsconfig.json
  • use an await import() call when needed
  • simple and straight forward
  • package is not listed on top with other imports
  • you cannot import/use types from the imported ES module (at least I found no possibility so far)
  • only works in async context
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  async getHello(): Promise<string> {
    const random = (await import('random')).default;
    return 'Hello World! ' + random.int(1, 10);
  }
}

Variant 2: Helper Function

Note: Importing a type from a ES module will not result in an require call, since it's only used by the typescript compiler and not the runtime environment.

  • a bit messy because package is always hidden behind an extra function call
  • only works in async context
  • you can import/use types
import { Injectable } from '@nestjs/common';
import { type Random } from 'random';

async function getRandom(): Promise<Random> {
  const module = await (eval(`import('random')`) as Promise<any>);
  return module.default;
}

@Injectable()
export class AppService {
  async getHello(): Promise<string> {
    return 'Hello World! ' + (await getRandom()).int(1, 10);
  }
}

Variant 3: Import and save to local variable

  • no extra function call needed
  • imported module can be undefined at runtime (unlikely but still bad practice)
  • you can import/use types
import { Injectable } from '@nestjs/common';
import { type Random } from 'random';

let random: Random;
eval(`import('random')`).then((module) => {
  random = module.default;
});


@Injectable()
export class AppService {
  async getHello(): Promise<string> {
    return 'Hello World! ' + random.int(1, 10);
  }
}

Convert your NestJS project to ES modules (not recommended)

Although not supported, it seems possible to setup NestJS to compile to ESM. This Guide has good instructions how to do this for any typescript project.

I tested it with NestJS and found these steps sufficient:

  • add "type": "module" to your package.json
  • change module to NodeNextin your compilerOptions in tsconfig.json
  • add the .js extension to all of your relative imports

Now the import should work as expected.

import { Injectable } from '@nestjs/common';
import random from 'random';

@Injectable()
export class AppService {
  async getHello(): Promise<string> {
    return 'Hello World! ' + random.int(1, 10);
  }
}

What did not work so far were the unit tests with jest. I got other import errors there and I bet there are more problems down the road. I would avoid this approach and wait until NestJS officially supports ES modules.

Upvotes: 67

Related Questions