Skip to content

fix(ws,microservices): load packages where bundlers can see them - #18285

Merged
kamilmysliwiec merged 3 commits into
masterfrom
fix/bundler-visible-package-loads
Oct 26, 2026
Merged

kamilmysliwiec merged 3 commits into
masterfrom
fix/bundler-visible-package-loads

Conversation

@kamilmysliwiec

Copy link
Copy Markdown
Member

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

WsAdapter and several microservices transports load their packages with createRequire(import.meta.url)('pkg') in their constructors. webpack 5 follows that, but esbuild and rolldown don't (evanw/esbuild#1828): the package is left out of the bundle without a warning, and a bundled app that runs without node_modules exits with The "ws" package is missing. ClientMqtt, ClientNats, ClientRMQ and ServerGrpc fail the same way, even when the package is in the bundle through another import. Under --format=cjs, import.meta.url is empty, so these loads fail even when the package is installed. This came with the ESM migration in v12; v11 used () => require('pkg'), which esbuild could see.

Issue Number: Closes #18284

Thanks to @sumanthratna for the report and the esbuild reproduction.

What is the new behavior?

  • @nestjs/platform-ws imports WebSocketServer from ws statically. ws is a regular dependency of the package, so it's always bundled.
  • ClientMqtt, ClientRMQ and ClientNats load their packages on connect(), and ServerGrpc and ServerRMQ when they listen, with a literal import(), as ServerKafka and ServerRedis already do. A close() during the first load cancels the connection attempt instead of leaving a client open. createClient() of ClientMqtt and ClientRMQ still loads the package if it's called before connect().
  • KafkaReplyPartitionAssigner reuses the kafkajs that ClientKafka has already loaded.
  • The loadPackageSync docstring now says which bundlers follow createRequire loaders.

Checked with esbuild 0.28.2 (ESM output, each bundle run from a folder without node_modules): before, new WsAdapter(server) exited with "ws package is missing" and ServerGrpc bundled 0 grpc-js modules; after, ws is in the bundle, ServerGrpc bundles grpc-js and binds, and ClientMqtt and ClientNats connect to local brokers.

Does this PR introduce a breaking change?

  • Yes
  • No

A missing transport package is now reported (same message, same exit) when the client connects or the server starts listening, instead of when it is constructed.

Other information

  • ClientGrpcProxy still loads @grpc/grpc-js synchronously, because getService() is synchronous. Like the protoLoader option (whose package name is only known at runtime), it can't be bundled by esbuild or rolldown: mark these packages as external (e.g. --packages=external) and keep them in node_modules.
  • With esbuild, @nestjs/microservices now also needs @grpc/grpc-js to be installed or marked external, as already applies to kafkajs, ioredis, mqtt, @nats-io/transport-node and amqp-connection-manager.
  • ws is CommonJS, so bundling it into ESM output with esbuild needs a require banner, e.g. --banner:js="import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);".
  • Tests: unit tests for platform-ws, microservices and common pass; the ws, gRPC, MQTT and NATS integration suites pass locally. RabbitMQ and Kafka integration suites weren't run locally (no broker).

ws is a dependency of @nestjs/platform-ws, but the adapter loaded it
with createRequire(import.meta.url)('ws') in its constructor, which
esbuild and rolldown don't follow: a bundled app left ws out and exited
with "ws package is missing". A static import is bundled by all of them.
ClientMqtt, ClientRMQ, ClientNats, ServerGrpc and ServerRMQ loaded their
packages with createRequire() in the constructor, which esbuild and
rolldown don't follow. They now load them with a literal import() when
the client connects or the server listens, as ServerKafka and
ServerRedis already do. createClient() of ClientMqtt and ClientRMQ still
loads the package when called before connect().

ClientGrpcProxy keeps its synchronous load, since getService() is
synchronous; bundled applications have to mark its packages external.
webpack 5 follows createRequire(import.meta.url)('pkg'), esbuild and
rolldown don't. Point to a literal import() loader or a static import.
@kamilmysliwiec
kamilmysliwiec merged commit 6aafca1 into master Oct 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WsAdapter's createRequire(import.meta.url)('ws') loader is not statically analysable by esbuild, so a bundled app exits with "ws package is missing"

1 participant