Repository: web-infra-dev/rsbuild (TypeScript)

Issue:
[Feature]: simplify rsbuild dev-related configurations
### What problem does this feature solve?

There are some duplications between `dev` and `tools.devServer` configurations, and some modern.js `tools.devServer` configurations are not yet supported by rsbuild.


in modern.js：
<div style="display:flex">
<img width="260" alt="image" src="https://github.com/web-infra-dev/rsbuild/assets/22373761/b8018b8c-e6b9-48ac-8b10-e818ac887078">

<img width="260" alt="image" src="https://github.com/web-infra-dev/rsbuild/assets/22373761/b01cfa36-db1d-4bf5-917e-13b822385bc0">
</div>

so, we will merge  some  `tools.devServer` into `dev`, and remove support for `tools.devServer`.


### What does the proposed API look like?

### configuration changes

- ~~tools.devServer.watch~~
- ~~tools.devServer.liveReload~~
- ~~tools.devServer.before~~  (use dev.setupMiddlewares.unshift instead)
- ~~tools.devServer.after~~  (use dev.setupMiddlewares.push instead)
- ~~tools.devServer.hot~~ (use dev.hmr instead)
- ~~tools.devServer.https~~  (use dev.https instead)
- tools.devServer.client => dev.client
- tools.devServer.setupMiddlewares => dev.setupMiddlewares
- tools.devServer.proxy => dev.proxy
- tools.devServer.compress => dev.compress
- tools.devServer.devMiddleware => dev.devMiddleware
- tools.devServer.headers => dev.headers
- tools.devServer.historyApiFallback => dev.historyApiFallback



Fix (patch):
diff --git a/.changeset/orange-scissors-bathe.md b/.changeset/orange-scissors-bathe.md
new file mode 100644
index 0000000000..737429f47c
--- /dev/null
+++ b/.changeset/orange-scissors-bathe.md
@@ -0,0 +1,6 @@
+---
+'@rsbuild/shared': patch
+'@rsbuild/core': patch
+---
+
+refactor: simplify rsbuild dev-related configurations #444
diff --git a/packages/core/src/server/constants.ts b/packages/core/src/server/constants.ts
deleted file mode 100644
index 40f12f11f1..0000000000
--- a/packages/core/src/server/constants.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import type { DevServerOptions } from '@rsbuild/shared';
-
-/**
- * hmr socket connect path
- */
-export const HMR_SOCK_PATH = '/webpack-hmr';
-
-export const getDefaultDevOptions = (): DevServerOptions => {
-  return {
-    client: {
-      path: HMR_SOCK_PATH,
-      // By default it is set to the port number of the dev server
-      port: '',
-      // By default it is set to "location.hostname"
-      host: '',
-      // By default it is set to "location.protocol === 'https:' ? 'wss' : 'ws'""
-      protocol: '',
-    },
-    https: false,
-    devMiddleware: { writeToDisk: true },
-    watch: true,
-    hot: true,
-    compress: true,
-    liveReload: true,
-  };
-};
diff --git a/packages/core/src/server/dev-middleware/index.ts b/packages/core/src/server/dev-middleware/index.ts
index 937a4e5243..b0c6b7b536 100644
--- a/packages/core/src/server/dev-middleware/index.ts
+++ b/packages/core/src/server/dev-middleware/index.ts
@@ -1,14 +1,14 @@
 import { Server } from 'http';
 import { EventEmitter } from 'events';
 import type {
-  DevServerOptions,
+  DevConfig,
   DevMiddlewareAPI,
   DevMiddleware as CustomDevMiddleware,
 } from '@rsbuild/shared';
 import SocketServer from './socketServer';
 
 type Options = {
-  dev: DevServerOptions;
+  dev: DevConfig;
   devMiddleware?: CustomDevMiddleware;
 };
 
@@ -16,7 +16,7 @@ const noop = () => {
   // noop
 };
 
-function getHMRClientPath(client: DevServerOptions['client']) {
+function getHMRClientPath(client: DevConfig['client']) {
   const protocol = client?.protocol ? `&protocol=${client.protocol}` : '';
   const host = client?.host ? `&host=${client.host}` : '';
   const path = client?.path ? `&path=${client.path}` : '';
@@ -33,7 +33,7 @@ function getHMRClientPath(client: DevServerOptions['client']) {
 export default class DevMiddleware extends EventEmitter {
   public middleware?: DevMiddlewareAPI;
 
-  private devOptions: DevServerOptions;
+  private devOptions: DevConfig;
 
   private socketServer: SocketServer;
 
@@ -82,7 +82,7 @@ export default class DevMiddleware extends EventEmitter {
       },
     };
 
-    const enableHMR = this.devOptions.hot || this.devOptions.liveReload;
+    const enableHMR = this.devOptions.hmr;
 
     const middleware = devMiddleware({
       headers: devOptions.headers,
diff --git a/packages/core/src/server/dev-middleware/socketServer.ts b/packages/core/src/server/dev-middleware/socketServer.ts
index e88bddb01f..bddec251d9 100644
--- a/packages/core/src/server/dev-middleware/socketServer.ts
+++ b/packages/core/src/server/dev-middleware/socketServer.ts
@@ -1,7 +1,7 @@
 import { Server } from 'http';
 import { Socket } from 'net';
 import ws from 'ws';
-import { logger, Stats, DevServerOptions } from '@rsbuild/shared';
+import { logger, Stats, DevConfig } from '@rsbuild/shared';
 
 interface ExtWebSocket extends ws {
   isAlive: boolean;
@@ -12,7 +12,7 @@ export default class SocketServer {
 
   private readonly sockets: ws[] = [];
 
-  private readonly options: DevServerOptions;
+  private readonly options: DevConfig;
 
   private app?: Server;
 
@@ -20,7 +20,7 @@ export default class SocketServer {
 
   private timer: NodeJS.Timeout | null = null;
 
-  constructor(options: DevServerOptions) {
+  constructor(options: DevConfig) {
     this.options = options;
   }
 
@@ -124,14 +124,10 @@ export default class SocketServer {
       }
     });
 
-    if (this.options.hot || this.options.hot === 'only') {
+    if (this.options.hmr) {
       this.singleWrite(connection, 'hot');
     }
 
-    if (this.options.liveReload) {
-      this.singleWrite(connection, 'liveReload');
-    }
-
     // send first stats to active client sock if stats exist
     if (this.stats) {
       this.sendStats(true);
diff --git a/packages/core/src/server/devServer.ts b/packages/core/src/server/devServer.ts
index caf2850b9e..fdda2c8cf5 100644
--- a/packages/core/src/server/devServer.ts
+++ b/packages/core/src/server/devServer.ts
@@ -3,7 +3,7 @@ import { createServer as createHttpsServer } from 'https';
 import type { ListenOptions } from 'net';
 import url from 'url';
 import {
-  DevServerOptions,
+  DevConfig,
   RequestHandler,
   ExposeServerApis,
   RsbuildDevServerOptions,
@@ -18,15 +18,13 @@ import {
   StartServerResult,
   getDevOptions,
 } from '@rsbuild/shared';
-import { getDefaultDevOptions } from './constants';
 import DevMiddleware from './dev-middleware';
-import { deepmerge } from '@rsbuild/shared/deepmerge';
 import connect from 'connect';
 import { createProxyMiddleware } from './proxy';
 import { faviconFallbackMiddleware } from './middlewares';
 
 export class RsbuildDevServer {
-  private readonly dev: DevServerOptions;
+  private readonly dev: DevConfig;
   private readonly devMiddleware: DevMiddleware;
   private pwd: string;
   private app!: Server;
@@ -34,8 +32,7 @@ export class RsbuildDevServer {
 
   constructor(options: RsbuildDevServerOptions) {
     this.pwd = options.pwd;
-    // set dev server options, like webpack-dev-server
-    this.dev = deepmerge(getDefaultDevOptions(), options.dev);
+    this.dev = options.dev;
 
     // create dev middleware instance
     this.devMiddleware = new DevMiddleware({
diff --git a/packages/shared/src/devServer.ts b/packages/shared/src/devServer.ts
index 0139fe8641..8c4a390ecf 100644
--- a/packages/shared/src/devServer.ts
+++ b/packages/shared/src/devServer.ts
@@ -4,7 +4,7 @@ import type {
   OnAfterStartDevServerFn,
   OnBeforeStartDevServerFn,
   CompilerTapFn,
-  DevServerOptions,
+  DevConfig,
 } from './types';
 import { getPort } from './port';
 import deepmerge from 'deepmerge';
@@ -12,7 +12,6 @@ import { color } from './color';
 import { logger as defaultLogger, Logger } from './logger';
 import { DEFAULT_PORT, DEFAULT_DEV_HOST } from './constants';
 import { createAsyncHook } from './createHook';
-import { mergeChainedOptions } from './mergeChainedOptions';
 import type { Compiler } from '@rspack/core';
 
 export function printServerURLs(
@@ -26,13 +25,50 @@ export function printServerURLs(
   logger.log(message);
 }
 
+/**
+ * hmr socket connect path
+ */
+export const HMR_SOCK_PATH = '/webpack-hmr';
+
+export const mergeDevOptions = ({
+  rsbuildConfig,
+  port,
+}: {
+  rsbuildConfig: RsbuildConfig;
+  port: number;
+}) => {
+  const defaultDevConfig: DevConfig = {
+    client: {
+      path: HMR_SOCK_PATH,
+      port: port.toString(),
+      // By default it is set to "location.hostname"
+      host: '',
+      // By default it is set to "location.protocol === 'https:' ? 'wss' : 'ws'""
+      protocol: '',
+    },
+    compress: true,
+    port,
+    devMiddleware: {
+      writeToDisk: (file: string) => !file.includes('.hot-update.'),
+    },
+  };
+
+  const devServerConfig = rsbuildConfig.dev
+    ? deepmerge(defaultDevConfig, rsbuildConfig.dev)
+    : defaultDevConfig;
+
+  devServerConfig.port = port;
+
+  return devServerConfig;
+};
+
 export const getDevOptions = async ({
   rsbuildConfig,
   strictPort,
   getPortSilently,
 }: {
   rsbuildConfig: RsbuildConfig;
-  strictPort: boolean;
+  strictPort?: boolean;
   getPortSilently?: boolean;
 }) => {
   const port = await getPort(rsbuildConfig.dev?.port || DEFAULT_PORT, {
@@ -44,40 +80,9 @@ export const getDevOptions = async ({
 
   const https = Boolean(rsbuildConfig.dev?.https) || false;
 
-  const devServerConfig = await getDevServerOptions({
-    rsbuildConfig,
-    port,
-  });
+  const devServerConfig = mergeDevOptions({ rsbuildConfig, port });
 
-  return { port, host, https, devServerConfig };
-};
-
-export const getDevServerOptions = async ({
-  rsbuildConfig,
-  port,
-}: {
-  rsbuildConfig: RsbuildConfig;
-  port: number;
-}): Promise<DevServerOptions> => {
-  const defaultDevConfig = {
-    hot: rsbuildConfig.dev?.hmr ?? true,
-    watch: true,
-    client: {
-      port: port.toString(),
-    },
-    port,
-    liveReload: rsbuildConfig.dev?.hmr ?? true,
-    devMiddleware: {
-      writeToDisk: (file: string) => !file.includes('.hot-update.'),
-    },
-    https: rsbuildConfig.dev?.https,
-  };
-
-  return mergeChainedOptions({
-    defaults: defaultDevConfig,
-    options: rsbuildConfig.tools?.devServer,
-    mergeFn: deepmerge,
-  });
+  return { devServerConfig, port, host, https };
 };
 
 /** The context used by startDevServer. */
diff --git a/packages/shared/src/types/config/dev.ts b/packages/shared/src/types/config/dev.ts
index 05cfc96699..aa85e9ad89 100644
--- a/packages/shared/src/types/config/dev.ts
+++ b/packages/shared/src/types/config/dev.ts
@@ -1,4 +1,6 @@
 import type { ArrayOrNot } from '../utils';
+import type { IncomingMessage, ServerResponse } from 'http';
+import type { Options as ProxyOptions } from 'http-proxy-middleware';
 
 export type DevServerHttpsOptions = boolean | { key: string; cert: string };
 
@@ -6,6 +8,36 @@ export type ProgressBarConfig = {
   id?: string;
 };
 
+export type NextFunction = () => void;
+
+export type ProxyDetail = ProxyOptions & {
+  bypass?: (
+    req: IncomingMessage,
+    res: ServerResponse,
+    proxyOptions: RsbuildProxyOptions,
+  ) => string | undefined | null | false;
+  context?: string | string[];
+};
+
+export type RsbuildProxyOptions =
+  | Record<string, string>
+  | Record<string, ProxyDetail>
+  | ProxyDetail[]
+  | ProxyDetail;
+
+export type RequestHandler = (
+  req: IncomingMessage,
+  res: ServerResponse,
+  next: NextFunction,
+) => void;
+
+export type ExposeServerApis = {
+  sockWrite: (
+    type: string,
+    data?: string | boolean | Record<string, any>,
+  ) => void;
+};
+
 export interface DevConfig {
   /**
    * Whether to enable Hot Module Replacement.
@@ -42,7 +74,56 @@ export interface DevConfig {
    * Used to set the host of Dev Server.
    */
   host?: string;
+
+  /** config of hmr client. */
+  client?: {
+    path?: string;
+    port?: string;
+    host?: string;
+    protocol?: string;
+  };
+  /** Whether to enable gzip compression */
+  compress?: boolean;
+  /** see https://github.com/webpack/webpack-dev-middleware */
+  devMiddleware?: {
+    writeToDisk?: boolean | ((filename: string) => boolean);
+    outputFileSystem?: Record<string, any>;
+  };
+  proxy?: RsbuildProxyOptions;
+  headers?: Record<string, string | string[]>;
+  /** see https://github.com/bripkens/connect-history-api-fallback */
+  historyApiFallback?:
+    | boolean
+    | {
+        index?: string;
+        verbose?: boolean;
+        logger?: typeof console.log;
+        htmlAcceptHeaders?: string[];
+        disableDotRule?: true;
+        rewrites?: Array<{
+          from: RegExp;
+          to: string | RegExp | Function;
+        }>;
+      };
+  /** Provides the ability to execute a custom function and apply custom middlewares */
+  setupMiddlewares?: Array<
+    (
+      /** Order: `devServer.before` => `unshift` => internal middlewares => `push` => `devServer.after` */
+      middlewares: {
+        /** Use the `unshift` method if you want to run a middleware before all other middlewares */
+        unshift: (...handlers: RequestHandler[]) => void;
+        /** Use the `push` method if you want to run a middleware after all other middlewares */
+        push: (...handlers: RequestHandler[]) => void;
+      },
+      server: ExposeServerApis,
+    ) => void
+  >;
 }
 
 export type NormalizedDevConfig = DevConfig &
-  Required<Omit<DevConfig, 'beforeStartUrl' | 'progressBar'>>;
+  Required<
+    Pick<
+      DevConfig,
+      'hmr' | 'port' | 'https' | 'startUrl' | 'assetPrefix' | 'host'
+    >
+  >;
diff --git a/packages/shared/src/types/config/tools.ts b/packages/shared/src/types/config/tools.ts
index cead7df5e9..831a1bd8e0 100644
--- a/packages/shared/src/types/config/tools.ts
+++ b/packages/shared/src/types/config/tools.ts
@@ -15,101 +15,11 @@ import type {
 } from '../thirdParty';
 import type { BundlerChain } from '../bundlerConfig';
 import type { ModifyBundlerChainUtils, ModifyChainUtils } from '../hooks';
-import type { DevServerHttpsOptions } from './dev';
 import type { RspackConfig, RspackRule, RspackPluginInstance } from '../rspack';
 import type { Options as HTMLPluginOptions } from 'html-webpack-plugin';
-import type { IncomingMessage, ServerResponse } from 'http';
-import type { Options as ProxyOptions } from 'http-proxy-middleware';
-
-export type NextFunction = () => void;
-
-export type ProxyDetail = ProxyOptions & {
-  bypass?: (
-    req: IncomingMessage,
-    res: ServerResponse,
-    proxyOptions: RsbuildProxyOptions,
-  ) => string | undefined | null | false;
-  context?: string | string[];
-};
-
-export type RsbuildProxyOptions =
-  | Record<string, string>
-  | Record<string, ProxyDetail>
-  | ProxyDetail[]
-  | ProxyDetail;
-
-export type RequestHandler = (
-  req: IncomingMessage,
-  res: ServerResponse,
-  next: NextFunction,
-) => void;
-
-export type ExposeServerApis = {
-  sockWrite: (
-    type: string,
-    data?: string | boolean | Record<string, any>,
-  ) => void;
-};
 
 export type { HTMLPluginOptions };
 
-export type DevServerOptions = {
-  /** config of hmr client. */
-  client?: {
-    path?: string;
-    port?: string;
-    host?: string;
-    protocol?: string;
-  };
-  /** Whether to enable gzip compression */
-  compress?: boolean;
-  devMiddleware?: {
-    writeToDisk?: boolean | ((filename: string) => boolean);
-    outputFileSystem?: Record<string, any>;
-  };
-  proxy?: RsbuildProxyOptions;
-  headers?: Record<string, string | string[]>;
-  /** Provides the ability to execute a custom function and apply custom middlewares */
-  /** Whether to watch files change. */
-  watch?: boolean;
-  /** Whether to enable hot reload. */
-  hot?: boolean | string;
-  /** Whether to enable page reload. */
-  liveReload?: boolean;
-  /** Whether to enable https. */
-  https?: DevServerHttpsOptions;
-  /** see https://github.com/bripkens/connect-history-api-fallback */
-  historyApiFallback?:
-    | boolean
-    | {
-        index?: string;
-        verbose?: boolean;
-        logger?: typeof console.log;
-        htmlAcceptHeaders?: string[];
-        disableDotRule?: true;
-        rewrites?: Array<{
-          from: RegExp;
-          to: string | RegExp | Function;
-        }>;
-      };
-  /** Provides the ability to execute a custom function and apply custom middlewares */
-  setupMiddlewares?: Array<
-    (
-      /** Order: `devServer.before` => `unshift` => internal middlewares => `push` => `devServer.after` */
-      middlewares: {
-        /** Use the `unshift` method if you want to run a middleware before all other middlewares */
-        unshift: (...handlers: RequestHandler[]) => void;
-        /** Use the `push` method if you want to run a middleware after all other middlewares */
-        push: (...handlers: RequestHandler[]) => void;
-      },
-      server: ExposeServerApis,
-    ) => void
-  >;
-  [propName: string]: any;
-};
-
-export type ToolsDevServerConfig = ChainedConfig<DevServerOptions>;
-
 export type ToolsAutoprefixerConfig = ChainedConfig<AutoprefixerOptions>;
 
 export type ToolsSassConfig = ChainedConfigWithUtils<
@@ -178,10 +88,6 @@ export interface ToolsConfig {
    * Modify the config of [autoprefixer](https://github.com/postcss/autoprefixer)
    */
   autoprefixer?: ToolsAutoprefixerConfig;
-  /**
-   * Modify the options of DevServer.
-   */
-  devServer?: ToolsDevServerConfig;
   /**
    * Modify the options of [css-loader](https://github.com/webpack-contrib/css-loader).
    */
diff --git a/packages/shared/src/types/server.ts b/packages/shared/src/types/server.ts
index 5e102d9a6f..b3ecd3dde5 100644
--- a/packages/shared/src/types/server.ts
+++ b/packages/shared/src/types/server.ts
@@ -1,5 +1,5 @@
 import type { IncomingMessage, ServerResponse, Server } from 'http';
-import { DevServerOptions, NextFunction } from './config/tools';
+import { DevConfig, NextFunction } from './config/dev';
 import type { Logger } from '../logger';
 // import type Connect from 'connect';
 // import type { ListenOptions } from 'net';
@@ -45,7 +45,7 @@ export type DevMiddleware = (options: DevMiddlewareOptions) => DevMiddlewareAPI;
 
 export type RsbuildDevServerOptions = {
   pwd: string;
-  dev: Partial<DevServerOptions>;
+  dev: DevConfig;
   devMiddleware?: DevMiddleware;
 };