javascript - No se puede importar el módulo ESM en Nestjs
Estoy teniendo un problema con la importación de módulos ESM en mi proyecto basado en Nest.js. Según tengo entendido, este problema no solo es relevante para Nest.js, sino también para TypeScript.
He probado varias cosas y combinaciones de versiones de Node.js y TypeScript, agregando "type":"module" a package.json y cambios en la configuración de mi archivo tsconfig.json, por lo que tiene la siguiente apariencia, que está lejos de los valores predeterminados:
{
"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,
}
}
Mi entorno completo es:
- Node.js (19.2.1 LTS) gestionado mediante nvm
- Typescript (4.9.4, pero también probé 4.3.5)
- @nestjs/common: 9.2.1
- @nestjs/core: 9.2.1
- ts-loader: "9.4.2",
- ts-node: "10.9.1",
- tsconfig-paths: "4.1.0",
Pero todavía me da un error cuando trato de importar cualquier módulo ESM en cualquiera de mis servicios. Por ejemplo:
import random from `random`;
export class AppService implements OnApplicationBootstrap {
async test() {
const r = random.int(1, 5);
console.log(r);
}
}
¿Alguien tiene alguna idea de cómo arreglarlo?
Respuesta #1
Este problema parece ocurrir con más frecuencia desde que más paquetes están cambiando a ser distribuidos como módulo ES.
Resumen
- Los módulos ES pueden importar módulos CommonJS
- Los módulos CommonJS no pueden importar módulos ES de forma síncrona
- NestJS actualmente no soporta compilar a ESM - vea este PR
Hay dos enfoques para este problema.
Importar un paquete de forma asíncrona usando la función dinámica import()
La instrucción de usar import() para módulos ES en CommonJS se puede encontrar en todas partes. Pero al usar TypeScript falta la pista adicional sobre cómo evitar que el compilador transforme la llamada import() en un require(). Encontré dos opciones para esto:
- establece
moduleResolutionennodenextonode16en tutsconfig.json(Variante 1) - usa la solución alternativa
eval- esto se basa en la sección "Solución 2: Solución alternativa usando eval" de la respuesta https://stackoverflow.com/a/70546326/13839825 (Variante 2 y 3)
Variante 1: await import() siempre que sea necesario
Esta solución a menudo se sugiere en el Discord oficial de NestJS
- agrega
"moduleResolution": "nodenext"o"moduleResolution": "node16"a tutsconfig.json - usa una llamada
await import()cuando sea necesario - simple y directo
- el paquete no está listado en la parte superior con otras importaciones
- no puedes importar/usar tipos del módulo ES importado (al menos hasta ahora no he encontrado ninguna posibilidad)
- solo funciona en contexto
async
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);
}
}
Variante 2: Función auxiliar
Nota: Importar un tipo desde un módulo ES no resultará en una llamada a require, ya que solo es usado por el compilador de TypeScript y no por el entorno de ejecución.
- un poco desordenado porque el paquete siempre está oculto detrás de una llamada de función adicional
- solo funciona en contexto
async - puedes importar/usar tipos
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);
}
}
Variante 3: Importar y guardar en una variable local
- no se necesita llamada de función extra
- un módulo importado puede ser indefinido en tiempo de ejecución (poco probable pero aún así es una mala práctica)
- puedes importar/usar tipos
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);
}
}
Convierte tu proyecto NestJS a módulos ES (no recomendado)
Aunque no está soportado, parece posible configurar NestJS para compilar a ESM. Esta guía tiene buenas instrucciones sobre cómo hacer esto para cualquier proyecto de TypeScript.
Lo probé con NestJS y encontré estos pasos suficientes:
- agrega
"type": "module"a tupackage.json - cambia
moduleaNodeNexten tus compilerOptions entsconfig.json - agrega la extensión
.jsa todas tus importaciones relativas
Ahora la importación debería funcionar como se esperaba.
import { Injectable } from '@nestjs/common';
import random from 'random';
@Injectable()
export class AppService {
async getHello(): Promise<string> {
return 'Hello World! ' + random.int(1, 10);
}
}
Lo que no ha funcionado hasta ahora fueron las pruebas unitarias con jest. Obtuve otros errores de importación allí y apuesto a que habrá más problemas en el futuro. Evitaría este enfoque y esperaría hasta que NestJS soporte oficialmente los módulos ES.
Respuesta #2
Con Node v22 puedes usar la bandera --experimental-require-module
El package.json se verá algo similar a esto:
{
"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"
}
}
