Skip to content

feat: support dev.client.webSocketTransport option#7770

Open
zhouxinyong wants to merge 2 commits into
web-infra-dev:mainfrom
zhouxinyong:feat/websocket-transport
Open

feat: support dev.client.webSocketTransport option#7770
zhouxinyong wants to merge 2 commits into
web-infra-dev:mainfrom
zhouxinyong:feat/websocket-transport

Conversation

@zhouxinyong
Copy link
Copy Markdown

Summary

  • Add a new dev.client.webSocketTransport option that allows users to provide a custom WebSocket transport module for HMR connections
  • 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 (url: string) => WebSocket that receives the computed WebSocket URL and returns a standard WebSocket instance

Changes

  • packages/core/src/types/config.ts — Add webSocketTransport field to ClientConfig
  • packages/core/src/client/hmr.ts — Accept optional createWebSocket parameter in init(), use it in connect() instead of new WebSocket(url)
  • packages/core/src/server/assets-middleware/index.ts — Import and inject the custom transport module in the virtual HMR entry when configured
  • website/docs/{en,zh}/config/dev/client.mdx — Document the new option with usage examples

Example

// rsbuild.config.ts
export default {
  dev: {
    client: {
      webSocketTransport: require.resolve('./client/customTransport.js'),
    },
  },
};
// client/customTransport.js
let cachedBaseUrl = null;

export default function createWebSocket(url) {
  if (!cachedBaseUrl) {
    const scriptUrl = new URL(document.currentScript.src);
    const wsProtocol = scriptUrl.protocol === 'https:' ? 'wss:' : 'ws:';
    cachedBaseUrl = `${wsProtocol}//${scriptUrl.host}/rsbuild-hmr`;
  }
  const token = new URL(url).searchParams.get('token');
  return new WebSocket(`${cachedBaseUrl}?token=${token}`);
}

Test plan

  • Build passes (pnpm run build)
  • Lint and type check pass (pre-commit hooks)
  • E2E test added: e2e/cases/hmr/websocket-transport/ — verifies custom transport derives URL from document.currentScript.src and HMR hot update works correctly

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.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +204 to 222
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' : ''}
)
`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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' : ''}
)
`;

Comment on lines +420 to +422
socket = createWebSocket
? createWebSocket(socketUrl)
: new WebSocket(socketUrl);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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);
    }

Copy link
Copy Markdown
Member

@chenjiahan chenjiahan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you briefly explain when we need a custom webSocketTransport? Are there any cases where the default WebSocket doesn’t work well enough?

@chenjiahan
Copy link
Copy Markdown
Member

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?

@zhouxinyong
Copy link
Copy Markdown
Author

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 不满足我们的需求。
我们原来用的是webpack的 rspack也有这个配置项
devServer: { client: { webSocketTransport: require.resolve('./client/RelativeWebSocketClient'), } }
但rsbuild不支持这个配置。
我们开发环境有个代理路径:
根据分支和ip动态生成的,所以这个wss路径根据路径动态生成。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants