From 6a1ae763643c728a93bb2e5b943fcebbe893ae7a Mon Sep 17 00:00:00 2001 From: Alexander Akait Date: Fri, 18 Sep 2026 11:54:01 +0000 Subject: [PATCH 1/4] fix: client, overlay, progress and server lifecycle defects Consolidates the actionable fixes from #5724, #5725, #5726, #5727, #5728, #5729, #5730 and #5732. Client: - honor `client.progress: "linear" | "circular"`; the resource query only recognized `"true"`, so both visual modes were silently disabled - parse the resource query with full `key=value` semantics (encoded keys, `+` as space, `=` inside values, malformed escapes ignored) - decode credentials taken from the current script tag so `formatURL` does not encode them twice - apply `client.overlay.warnings` / `.errors` filter functions to what the overlay renders, not only to the decision to render - apply the reconnect limit before the first connection attempt, so `client.reconnect: false` no longer retries when the socket never opens Overlay: - reuse the Trusted Types policy instead of re-creating it per open, which throws under a `trusted-types` CSP - keep only the newest queued render so messages are not duplicated when two batches arrive before the iframe loads - re-register the Escape handler on open; it was removed on first dismiss and never restored - encode the `open-editor` file name, render openable entries as buttons, and restore focus on dismiss Progress: - style the linear bar through `#progress`; the rules targeted `#bar`, which no template emits - clear the `disappear` class and the pending hide timer when a new build starts, so the indicator reappears - skip redundant `attributeChangedCallback` work and expose progressbar ARIA state and reduced-motion styles Server: - reject from `start()` on an occupied port or IPC path instead of throwing from an event handler, and release what setup allocated - fix `bonjour` protocol reporting (`||` bound tighter than the ternary) - only install the WebSocket `upgrade` listener in no-server mode, and remove it on close - skip incomplete interfaces and CIDRs in `findIp`, and hand the listening socket an unbracketed IPv6 address - wait for pending startup before shutting down in plugin mode - build the asset report from `toJson` with only the fields it prints, construct the `serve-index` middleware once, and serialize each broadcast once instead of per client - export `BaseServer` and type `webSocketServer.type` as its constructor Examples: - repair `api/plugin` (CommonJS in an ESM package), `ipc` (`http-proxy`), `proxy` and `general/proxy-simple` (options removed in v5) - serve the shared layout assets through `express.static`, which also works for the `hono` example, and read each README relative to its own directory - restore host and cross-origin checks in the `hono` example, whose `setupMiddlewares` replaces the built-in stack Co-authored-by: Oskar Eichler <62393985+OskarEichler@users.noreply.github.com> --- .changeset/fix-client-and-server-defects.md | 5 + client-src/index.js | 52 ++++-- client-src/overlay.js | 124 +++++++-------- client-src/progress.js | 65 +++++--- client-src/socket.js | 8 +- examples/.assets/layout.html | 11 +- examples/.assets/style.css | 24 ++- examples/api/middleware/README.md | 2 +- examples/api/middleware/webpack.config.js | 9 ++ examples/api/plugin/README.md | 2 +- examples/api/plugin/webpack.config.js | 29 ++-- examples/app/connect/README.md | 6 +- examples/app/hono/README.md | 53 ++++++- examples/app/hono/webpack.config.js | 37 +++++ examples/compression/false/README.md | 2 +- examples/default-cjs/webpack.config.cjs | 7 + examples/dev-middleware/webpack.config.js | 2 +- .../general/proxy-simple/webpack.config.js | 4 +- examples/general/universal-config/client.js | 2 +- examples/headers/array/README.md | 2 +- examples/ipc/webpack.config.js | 26 ++- examples/proxy/README.md | 43 ++++- examples/proxy/webpack.config.js | 29 ++-- examples/server/http2/README.md | 6 +- examples/util.js | 97 +++--------- lib/Server.js | 149 +++++++++++------- lib/servers/WebsocketServer.js | 18 ++- package.json | 6 + scripts/finalize-cjs-build.mjs | 15 +- test/client/index.test.js | 33 ++++ test/client/overlay-lifecycle.test.js | 97 ++++++++++++ test/client/socket-helper.test.js | 27 ++++ test/client/utils/createSocketURL.test.js | 15 ++ .../__snapshots__/api.test.js.snap.webpack5 | 1 - .../overlay.test.js.snap.webpack5 | 79 +++++++++- test/e2e/api.test.js | 60 +++++-- test/e2e/built-in-routes.test.js | 27 ++++ test/e2e/host.test.js | 107 ++++++------- test/server/find-ip.test.js | 46 ++++++ types/lib/Server.d.ts | 7 +- 40 files changed, 933 insertions(+), 401 deletions(-) create mode 100644 .changeset/fix-client-and-server-defects.md create mode 100644 test/client/overlay-lifecycle.test.js create mode 100644 test/server/find-ip.test.js diff --git a/.changeset/fix-client-and-server-defects.md b/.changeset/fix-client-and-server-defects.md new file mode 100644 index 0000000000..dbe1bdd06f --- /dev/null +++ b/.changeset/fix-client-and-server-defects.md @@ -0,0 +1,5 @@ +--- +"webpack-dev-server": patch +--- + +Fix client progress modes, socket URL credentials, overlay and progress lifecycles, server startup error handling and local IP lookup; export `BaseServer`. diff --git a/client-src/index.js b/client-src/index.js index e8a650ea3d..cf6f88fc3a 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -34,7 +34,7 @@ import sendMessage from "./utils/sendMessage.js"; * @typedef {object} Options * @property {boolean} hot true when hot enabled, otherwise false * @property {boolean} liveReload true when live reload enabled, otherwise false - * @property {boolean} progress true when need to show progress, otherwise false + * @property {boolean | "linear" | "circular"} progress progress display mode * @property {boolean | OverlayOptions} overlay overlay options * @property {LogLevel=} logging logging level * @property {number=} reconnect count of allowed reconnection @@ -129,10 +129,17 @@ const parseURL = (resourceQuery) => { const searchParams = resourceQuery.slice(1).split("&"); for (let i = 0; i < searchParams.length; i++) { - const pair = searchParams[i].split("="); - - /** @type {EXPECTED_ANY} */ - (result)[pair[0]] = decodeURIComponent(pair[1]); + const parameter = searchParams[i].replace(/\+/g, " "); + const separator = parameter.indexOf("="); + const key = separator === -1 ? parameter : parameter.slice(0, separator); + const value = separator === -1 ? "" : parameter.slice(separator + 1); + + try { + /** @type {EXPECTED_ANY} */ + (result)[decodeURIComponent(key)] = decodeURIComponent(value); + } catch { + // Ignore malformed percent escapes without preventing client startup. + } } } else { // Else, get the url from the