NestJS + esbuild workarounds
Tags: nestjs, esbuild, javascript, node
Published: 2022-09-13 20:31:07

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
```

* Bundle is the best point for a thorough ISEC audit: to search for [redos](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_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](https://github.com/antongolub/tsc-esm-fix)

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

## Stack
* [Nestjs 9.x](https://github.com/nestjs/nest)
* [esbuild 0.15.x](https://github.com/evanw/esbuild)

## :-(
```js
const config =  {
  platform: 'node',
  target: ['node18', 'es2021'],
  format: 'esm',
  bundle: true,
}
```

## 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

```js
const config =  {
  ...
  external: [
    'mqtt',
    'amqplib',
    'class-transformer/storage'
  ]
}
```

1\. `openapi` is not defined. https://github.com/nestjs/swagger/issues/1450
```js
__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);
```

2\. openapi / class-validator DTOs are referenced by `require` API. https://github.com/microsoft/TypeScript/issues/43329
```js
export class CspReportDto {
  static _OPENAPI_METADATA_FACTORY() {
    return { timestamp: { required: false, type: () => Object }, 'csp-report': { required: true, type: () => require("./csp.dto.js").CspReport } };
  }
}
```

3\. NodeJS builtins are referenced via `require` API.
```js
var require_async4 = __commonJS({
  "node_modules/resolve/lib/async.js"(exports, module2) {
    var fs2 = require("fs");
```

4\. esbuild-compiled ESM bundle cannot refer to `views/redoc.handlebars`
```js
const redocFilePath = path_1.default.join(__dirname, "..", "views", "redoc.handlebars");
```

5\. `_OPENAPI_METADATA_FACTORY` class fields may be empty, so the swagger declaration cannot be properly rendered.
```js
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 } }
  }
};
```

6\. Extra type wrappers cannot be processed by openapi / `class-validator` / `class-transformer`
```js
  __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)
```

## 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](https://github.com/anatine/esbuildnx) + [old good monkey patching](https://github.com/antongolub/nestjs-esm-fix).
Fragile. Wrong. Terrible.

```js
// 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)
```
```bash
node build.js && nestjs-esm-fix --target=./output.js
```

But it works.