DEV Community

Anton Golub
Anton Golub

Posted on

NestJS + esbuild workarounds

Q: Do we need ESM for backend apps?
A: Yes. More and more pkgs are moving to the ESM format, this is inevitable.

Q: Do we need bundles?
A: Probably, yes.

  • If your Nestjs app is distributed as a container with node_modules inside, bundling can greatly reduce its size.
du -hs ./node_modules
495M   ./node_modules
du -hs ./output.js   
10M    ./output.js
Enter fullscreen mode Exit fullscreen mode
  • Bundle is the best point for a thorough ISEC audit: to search for redos, suspicious API usages, protestware, etc.

  • The fewer fs I/O, the faster the application starts.

  • (TS) bundle absorbs the all required contents, so there is no tsc-esm transpilation issues

Q: esbuild, swc, rollup, parcel or babel?
A: esbuild.

Stack

:-(

const config =  {
  platform: 'node',
  target: ['node18', 'es2021'],
  format: 'esm',
  bundle: true,
}
Enter fullscreen mode Exit fullscreen mode

Problems

https://github.com/nestjs/nest-cli/issues/1157
https://github.com/nestjs/swagger/issues/1450
https://github.com/evanw/esbuild/pull/509
https://github.com/evanw/esbuild/issues/566

0. Some Nestjs internals are lazy-loaded, so sometimes they may be omitted, but in another cases they should be bundled. How to handle? https://esbuild.github.io/api/#external

const config =  {
  ...
  external: [
    'mqtt',
    'amqplib',
    'class-transformer/storage'
  ]
}
Enter fullscreen mode Exit fullscreen mode

1. openapi is not defined. https://github.com/nestjs/swagger/issues/1450

__decorate([
  Post('event-unsafe-batch'),
  HttpCode(200),
  openapi.ApiResponse({ status: 200, type: String }),
  __param(0, Body()),
  __param(1, Req()),
  __metadata("design:type", Function),
  __metadata("design:paramtypes", [Object, Object]),
  __metadata("design:returntype", Promise)
], EventUnsafeController.prototype, "logEventBatch", null);
Enter fullscreen mode Exit fullscreen mode

2. openapi / class-validator DTOs are referenced by require API. https://github.com/microsoft/TypeScript/issues/43329

export class CspReportDto {
  static _OPENAPI_METADATA_FACTORY() {
    return { timestamp: { required: false, type: () => Object }, 'csp-report': { required: true, type: () => require("./csp.dto.js").CspReport } };
  }
}
Enter fullscreen mode Exit fullscreen mode

3. NodeJS builtins are referenced via require API.

var require_async4 = __commonJS({
  "node_modules/resolve/lib/async.js"(exports, module2) {
    var fs2 = require("fs");
Enter fullscreen mode Exit fullscreen mode

4. esbuild-compiled ESM bundle cannot refer to views/redoc.handlebars

const redocFilePath = path_1.default.join(__dirname, "..", "views", "redoc.handlebars");
Enter fullscreen mode Exit fullscreen mode

5. _OPENAPI_METADATA_FACTORY class fields may be empty, so the swagger declaration cannot be properly rendered.

var Meta = class {
};
// β†’
var Meta = class {
  static _OPENAPI_METADATA_FACTORY() {
    return { appName: { required: false,  type: () =>  String }, appHost: { required: false,  type: () =>  String }, appVersion: { required: false,  type: () =>  String }, appNamespace: { required: false,  type: () =>  String }, appConfig: { required: false,  type: () =>  typeof (_a3 = typeof Record !== "undefined" && Record) === "function" ? _a3 : Object }, deviceInfo: { required: false,  type: () =>  typeof (_b3 = typeof Record !== "undefined" && Record) === "function" ? _b3 : Object }, userAgent: { required: false,  type: () =>  String }, envProfile: { required: false,  enum:  typeof (_c = typeof import_substrate2.EnvironmentProfile !== "undefined" && import_substrate2.EnvironmentProfile) === "function" ? _c : Object } }
  }
};
Enter fullscreen mode Exit fullscreen mode

6. Extra type wrappers cannot be processed by openapi / class-validator / class-transformer

  __metadata("design:type", typeof (_d = typeof Array !== "undefined" && Array) === "function" ? _d : Object)
  __metadata("design:type", typeof (_e = typeof import_substrate2.LogLevel !== "undefined" && import_substrate2.LogLevel) === "function" ? _e : Object)
  // β†’
  __metadata("design:type", Array)
  __metadata("design:type", import_substrate2.LogLevel)
Enter fullscreen mode Exit fullscreen mode

How to fix?

The right solution is certainly to improve the tools: create issues, discuss, suggest PRs.

How to fix it right here and right now?

@anatine/esbuild-decorators + old good monkey patching.
Fragile. Wrong. Terrible.

// build.js
import { build } from 'esbuild'
import path from 'node:path'
import { esbuildDecorators } from '@anatine/esbuild-decorators'

const cwd = process.cwd()
const outfile = path.resolve(cwd, 'output.js')
const tsconfig = path.resolve(cwd, 'tsconfig.json')
const entryPoints = [path.resolve(cwd, 'src/main/ts/index.ts')]
const config =  {
  platform: 'node',
  target: ['node18', 'es2021'],
  format: 'esm',
  bundle: true,
  keepNames: true,
  plugins: [
    esbuildDecorators({
      tsconfig,
      cwd
    }),
  ],
  tsconfig,
  entryPoints,
  outfile,
  external: [
    'kafkajs',
    'mqtt',
    'amqplib',
    'amqp-connection-manager',
    'nats',
    '@grpc/grpc-js',
    '@grpc/proto-loader',
    '@nestjs/websockets/socket-module',
    'class-transformer/storage'
  ]
}

await build(config)
Enter fullscreen mode Exit fullscreen mode
node build.js && nestjs-esm-fix --target=./output.js
Enter fullscreen mode Exit fullscreen mode

But it works.

Top comments (1)

Collapse
 
niraltmark profile image
Nir Altmark

What was the bundle size before and after?