Description
From time to time people ask on our Discord community about it even tho this isn't exactly a nestjs issue. So I think that it would be good to have this mentioned somewhere in the docs site until we address the PR nestjs/nest#8736
If we conclude that we won't add this to the docs, I'll publish an article at https://dev.to
typescript users may face several issues when trying to use the import()
expression to load ESM-only packages in a CommonJS project.
example
In a default standard nestjs app (typescript) we just can't do import superjson from 'superjson';
:
const superjson_1 = __importDefault(require("superjson"));
^
Error [ERR_REQUIRE_ESM]: require() of ES Module /tmp/nestjs-app/node_modules/superjson/dist/index.js from /tmp/nestjs-app/dist/src/app.module.js not supported.
Instead change the require of index.js in /tmp/nestjs-app/dist/src/app.module.js to a dynamic import() which is available in all CommonJS modules.
at Object.<anonymous> (/tmp/nestjs-app/dist/src/app.module.js:17:37) {
code: 'ERR_REQUIRE_ESM'
}
the reason is well explained in this StackOverflow answer: https://stackoverflow.com/a/75287028
solution
As mentioned in SO, we can address this is three ways:
- changing the
moduleResolution
andmodule
in our tsconfig file so thatimport()
won't be translated torequire
- use a helper eval-like function to prevents typescript from translating
import
intorequire
- use the
--experimental-require-module
NodeJS's flag
And the docs we must show both of them because sometimes we can't change the module
entry.
Another issue is that the docs should clarify is that depending on the package being loaded we must access the default
property (more on this bellow).
We should also double-check if those solutions works for Jest/Vitest -- specially regarding mocking features
code snippets
I'm not sure about the best way to show-off the solutions to this yet but I think that we should avoid abstractions so the dev can understand the bare bones and write their own.
Working examples of the approach (1):
- without NestJS stuff:
import { Module } from '@nestjs/common';
@Module({})
export class AppModule {
async onModuleInit() {
const superjson = await import('superjson');
const jsonString = superjson.stringify({ date: new Date(0) });
console.log(jsonString)
const delay = await import('delay').then(loadedModule => loadedModule.default);
await delay(1000)
}
}
- Using providers
import { Inject, Module } from '@nestjs/common';
@Module({
providers: [
{
provide: 'package:delay',
useFactory: () => import('delay').then(loadedModule => loadedModule.default),
},
{
provide: 'package:superjson',
useFactory: () => import('superjson'),
},
]
})
export class AppModule {
@Inject('package:delay')
private readonly delay: typeof import('delay', { with: { 'resolution-mode': 'import' } }).default
@Inject('package:superjson')
private readonly superjson: typeof import('superjson', { with: {'resolution-mode': 'import'} })
async onModuleInit() {
const jsonString = this.superjson.stringify({ date: new Date(0) });
console.log(jsonString)
console.time('>')
await this.delay(1000)
console.timeEnd('>')
}
}
Working example of the approach (2):
import { Inject, Module } from '@nestjs/common';
export const importPackage = async (packageName: string) =>
new Function(`return import('${packageName}')`)()
.then(loadedModule => loadedModule['default'] ?? loadedModule);
@Module({
providers: [
{
provide: 'package:delay',
useFactory: () => importPackage('delay'),
},
{
provide: 'package:superjson',
useFactory: () => importPackage('superjson'),
},
]
})
export class AppModule {
@Inject('package:delay')
private readonly delay: typeof import('delay').default;
@Inject('package:superjson')
private readonly superjson: typeof import('superjson');
async onModuleInit() {
const jsonString = this.superjson.stringify({ date: new Date(0) });
console.log(jsonString)
console.time('>')
await this.delay(1000)
console.timeEnd('>')
}
}