feat: support dev.client.webSocketTransport option#7770
Conversation
Add a new `webSocketTransport` option to `dev.client` that allows users to provide a custom WebSocket transport module for HMR connections. This is useful in micro-frontend or reverse proxy scenarios where the page origin differs from the dev server origin. The module should default export a factory function with the signature `(url: string) => WebSocket`, which receives the WebSocket URL computed by Rsbuild and returns a standard WebSocket instance.
There was a problem hiding this comment.
Code Review
This pull request introduces a new configuration option, webSocketTransport, allowing users to provide a custom WebSocket transport module for HMR. This is highly beneficial for micro-frontend or reverse proxy setups where the page origin differs from the dev server origin. The changes include updates to client-side HMR logic, server-side middleware, configuration types, and documentation. The review feedback suggests two key improvements: resolving relative paths for webSocketTransport to absolute paths relative to the compiler context to prevent resolution failures, and adding a defensive runtime check to ensure the custom transport module exports a valid function before invoking it.
| const { webSocketTransport, ...clientConfigRest } = { ...config.dev.client }; | ||
| if (clientConfigRest.port === '<port>') { | ||
| clientConfigRest.port = resolvedPort; | ||
| } | ||
|
|
||
| const hmrEntry = `import { init } from '${toPosixPath(join(CLIENT_PATH, 'hmr.js'))}'; | ||
| ${isOverlayEnabled(config.dev.client.overlay) ? `import '${toPosixPath(join(CLIENT_PATH, 'overlay.js'))}';` : ''} | ||
| ${webSocketTransport ? `import __rsbuild_ws_transport from '${toPosixPath(webSocketTransport)}';` : ''} | ||
| init( | ||
| '${token}', | ||
| ${JSON.stringify(clientConfig)}, | ||
| ${JSON.stringify(clientConfigRest)}, | ||
| ${JSON.stringify(resolvedHost)}, | ||
| ${resolvedPort}, | ||
| ${JSON.stringify(config.server.base)}, | ||
| ${liveReloadEnabled}, | ||
| ${Boolean(config.dev.browserLogs)}, | ||
| ${JSON.stringify(config.dev.client.logLevel)} | ||
| ${JSON.stringify(config.dev.client.logLevel)}${webSocketTransport ? ',\n __rsbuild_ws_transport' : ''} | ||
| ) | ||
| `; |
There was a problem hiding this comment.
If webSocketTransport is configured as a relative path (e.g., starting with .), it may fail to resolve correctly depending on where the virtual HMR entry module is evaluated. Resolving it to an absolute path relative to the compiler context ensures robust resolution by Rspack while still allowing bare specifiers (like package names) to be resolved from node_modules.
const { webSocketTransport, ...clientConfigRest } = { ...config.dev.client };
if (clientConfigRest.port === '<port>') {
clientConfigRest.port = resolvedPort;
}
const resolvedTransport =
webSocketTransport && webSocketTransport.startsWith('.')
? join(compiler.context, webSocketTransport)
: webSocketTransport;
const hmrEntry = `import { init } from '${toPosixPath(join(CLIENT_PATH, 'hmr.js'))}';
${isOverlayEnabled(config.dev.client.overlay) ? `import '${toPosixPath(join(CLIENT_PATH, 'overlay.js'))}';` : ''}
${resolvedTransport ? `import __rsbuild_ws_transport from '${toPosixPath(resolvedTransport)}';` : ''}
init(
'${token}',
${JSON.stringify(clientConfigRest)},
${JSON.stringify(resolvedHost)},
${resolvedPort},
${JSON.stringify(config.server.base)},
${liveReloadEnabled},
${Boolean(config.dev.browserLogs)},
${JSON.stringify(config.dev.client.logLevel)}${resolvedTransport ? ',\\n __rsbuild_ws_transport' : ''}
)
`;| socket = createWebSocket | ||
| ? createWebSocket(socketUrl) | ||
| : new WebSocket(socketUrl); |
There was a problem hiding this comment.
Add a defensive check to ensure createWebSocket is a function before calling it. If the custom transport module is misconfigured (e.g., missing a default export or exporting an object instead of a function), this prevents a runtime crash and gracefully falls back to the standard WebSocket while logging an error.
if (typeof createWebSocket === 'function') {
socket = createWebSocket(socketUrl);
} else {
if (createWebSocket !== undefined) {
logger.error(
'[rsbuild] The custom webSocketTransport module must default export a function.',
);
}
socket = new WebSocket(socketUrl);
}
chenjiahan
left a comment
There was a problem hiding this comment.
Could you briefly explain when we need a custom webSocketTransport? Are there any cases where the default WebSocket doesn’t work well enough?
|
For micro-frontend or reverse proxy scenarios where the page origin differs from the dev server origin, would configuring the WebSocket URL be enough for your use case? |
@chenjiahan 配置 WebSocket URL 不满足我们的需求。 |
Summary
dev.client.webSocketTransportoption that allows users to provide a custom WebSocket transport module for HMR connections(url: string) => WebSocketthat receives the computed WebSocket URL and returns a standard WebSocket instanceChanges
packages/core/src/types/config.ts— AddwebSocketTransportfield toClientConfigpackages/core/src/client/hmr.ts— Accept optionalcreateWebSocketparameter ininit(), use it inconnect()instead ofnew WebSocket(url)packages/core/src/server/assets-middleware/index.ts— Import and inject the custom transport module in the virtual HMR entry when configuredwebsite/docs/{en,zh}/config/dev/client.mdx— Document the new option with usage examplesExample
Test plan
pnpm run build)e2e/cases/hmr/websocket-transport/— verifies custom transport derives URL fromdocument.currentScript.srcand HMR hot update works correctly