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, 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,
}
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'
]
}
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);
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 } };
}
}
3. NodeJS builtins are referenced via require
API.
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
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.
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
__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 + 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)
node build.js && nestjs-esm-fix --target=./output.js
But it works.
Top comments (2)
how do you know what external modules can be excluded? do you have a script to detect that?
What was the bundle size before and after?