Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new FAQ page: How to use ESM-only packages in Nestjs CJS #3093

Open
micalevisk opened this issue Sep 22, 2024 · 6 comments
Open

new FAQ page: How to use ESM-only packages in Nestjs CJS #3093

micalevisk opened this issue Sep 22, 2024 · 6 comments

Comments

@micalevisk
Copy link
Member

micalevisk commented Sep 22, 2024

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:

  1. changing the moduleResolution and module in our tsconfig file so that import() won't be translated to require
  2. use a helper eval-like function to prevents typescript from translating import into require
  3. 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):
  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)
  }
}
  1. 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('>')
  }
}
@kamilmysliwiec
Copy link
Member

Would you like to create a PR for this?

@micalevisk micalevisk moved this from 🤔 I'll investigate later to 💭 Maybe I'll take this in My contributions to NestJS framework 😻 Sep 23, 2024
@Kimblis
Copy link

Kimblis commented Sep 24, 2024

Hey, I tried to implement this, but still get an error:
image
image

SUPERJSON_PROVIDER is just a constant (a name)

@micalevisk
Copy link
Member Author

@Kimblis looks like your tsconfig doesn't have the expected moduleResolution value. Check out the StackOverflow answer

@codiophile
Copy link

I recently spent a lot of effort trying to use an ES module in a NestJS application. This would have saved me a bunch of time. One thing I would add is that Jest doesn't seem to work with dynamic imports. In my case, I didn't want to test that dependency anyway, so I'm using a mock in its place, but I'm sure others will face situations where their code is working, but they can't test it, because the dynamic import crashes Jest. I don't know what the solution is, but if possible, I think it would be good to add advice on how to deal with this in Jest.

@kamilmysliwiec
Copy link
Member

Should we mention that https://x.com/JoyeeCheung/status/1839312043490578562 as well?

@Duske
Copy link

Duske commented Oct 7, 2024

We faced the same issue and decided to upgrade the entire toolchain using swc, esm and vitest like in https://github.com/AlbertHernandez/nestjs-service-template. Surely not a silver bullet, but that options also exists and is still using nestjs tooling (swc, vitest).

@micalevisk micalevisk moved this from 💭 Maybe I'll take this to 🕒 I'm trying... in My contributions to NestJS framework 😻 Oct 12, 2024
@micalevisk micalevisk moved this from 🕒 I'm trying... to ✅ LGTM in My contributions to NestJS framework 😻 Oct 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants