Skip to content

feat: add browser client runtime for HMR - #2323

Merged
bjohansebas merged 13 commits into
hot-middlewarefrom
client-runtime
Jun 16, 2026
Merged

feat: add browser client runtime for HMR#2323
bjohansebas merged 13 commits into
hot-middlewarefrom
client-runtime

Conversation

@bjohansebas

Copy link
Copy Markdown
Member

Summary

What kind of change does this PR introduce?

Did you add tests for your changes?

Does this PR introduce a breaking change?

If relevant, what needs to be documented once your changes are merged or what have you already documented?

Use of AI

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.
Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.
Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.
@changeset-bot

changeset-bot Bot commented May 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 4a68e44

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codecov

codecov Bot commented May 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.70%. Comparing base (92b2820) to head (4a68e44).

Additional details and impacted files
@@               Coverage Diff               @@
##           hot-middleware    #2323   +/-   ##
===============================================
  Coverage           92.70%   92.70%           
===============================================
  Files                   3        3           
  Lines                1001     1001           
  Branches              311      311           
===============================================
  Hits                  928      928           
  Misses                 65       65           
  Partials                8        8           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.
Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.
Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.
Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.
Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix
Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.
`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.
@bjohansebas
bjohansebas merged commit 1a72cc9 into hot-middleware Jun 16, 2026
30 of 31 checks passed
@bjohansebas
bjohansebas deleted the client-runtime branch June 16, 2026 14:23
bjohansebas added a commit that referenced this pull request Jun 16, 2026
* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!
@alexander-akait

Copy link
Copy Markdown
Member

@bjohansebas Please don't merge such things in future without approving, at least 1 approve, I don't make it strict to be more flexibility, but it doesn't mean we should ignore it, I don't merge then because we need to change to architecture problems on webpack and dev server side

@bjohansebas

Copy link
Copy Markdown
Member Author

this isn't merged into main, it's only in another branch (#2322), so any changes can still be made there.

@alexander-akait

Copy link
Copy Markdown
Member

Yeah, I see it, just for future, with other branches will be good to make the right architecture too, but it was already merged, so we will work with branch together, I don't like it because it makes a big diff and often unreadable

bjohansebas added a commit that referenced this pull request Jul 14, 2026
* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!
bjohansebas added a commit that referenced this pull request Jul 24, 2026
* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!
bjohansebas added a commit that referenced this pull request Jul 25, 2026
* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!
bjohansebas added a commit that referenced this pull request Jul 25, 2026
* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!
alexander-akait pushed a commit that referenced this pull request Sep 1, 2026
* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!
alexander-akait added a commit that referenced this pull request Sep 1, 2026
* feat: add hot option for hot module replacement

* feat: add browser client runtime for HMR (#2323)

* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!

* feat: implement hot module replacement middleware (#2321)

* fix(test): correct output path typo in webpack.array.warning fixture

The first compiler entry used `../../outputs/...` which escaped the test
directory and wrote artifacts to the repository root, outside of the
`/test/outputs` paths covered by `.gitignore` and `.prettierignore`.

* feat: implement hot module replacement middleware

Adds a `hot: true | { path, heartbeat, log, statsOptions }` option that
turns the dev middleware into a Server-Sent Events endpoint publishing
`building`, `built` and `sync` payloads from the webpack compiler.

The hot endpoint defaults to `/__webpack_hmr` and is served by the
existing middleware - no separate `app.use()` call is required.
`close()` tears down clients and the heartbeat timer.

* test: add tests for hot middleware

Covers schema validation of the `hot` option (success and failure cases
with snapshots), unit tests for `pathMatch`, `formatErrors`,
`buildModuleMap` and `createEventStream`, and integration tests that
verify SSE headers, the default and custom hot paths, MultiCompiler
support, `close()` teardown, and the `log` option (custom function and
`log: false`).

* test: add unit and integration tests for hot middleware functionality

* feat: enhance honoWrapper to support Web ReadableStream for hot middleware responses

* feat: add TypeScript definitions for hot module replacement functionality

* test(hot): cover publish, sync-on-connect, headers and close behavior

Ports the remaining unit-level cases from webpack-hot-middleware that
were not already covered by the framework matrix in middleware.test.js:

- the public `publish()` API broadcasts custom payloads
- a client connecting after a build receives a `sync` event
  initialised from the last stats
- HTTP/1 clients get `Connection: keep-alive`, HTTP/2 clients do not
- when `stats.name` is empty the published payload falls back to
  `compilation.name`
- a single broadcast reaches every attached client
- after `close()` further compiler events do not produce writes

* docs: document the hot option in README

* docs: list the hot option in the README options table

* refactor: replace EXPECTED_ANY with specific types in hot module definitions

* docs: update README to clarify default stats options for SSE payload

* refactor: remove log option from hot middleware and update related documentation

* docs: update default value for hot option in README to false

* docs: add hot module replacement example with Express server and client setup

* feat: enhance error overlay with close button and improved styling

* feat: enable reload option for HMR by default and update tests

* feat: add SSE helper functions and tests for event streaming

* refactor(client): inline ansi stripping and drop the strip-ansi dependency (#2350)

The client runtime runs in the browser, so Node's
util.stripVTControlCharacters is not an option. Inline the ansi-regex
pattern (verified against strip-ansi@6 output) in a small util instead
of shipping the dependency.

Ref webpack/webpack-hot-middleware#465
Ref webpack/webpack-hot-middleware#474

* refactor(hot): drop the module map from the SSE payload (#2349)

webpack-dev-server does not send module names over the wire: the HMR
runtime logs module ids on apply. Align the SSE payload with that
(name, action, time, hash, errors, warnings) instead of serializing a
module id → name map that was only used for log cosmetics and was
empty with the default stats options anyway.

Ref webpack/webpack-hot-middleware#452
Ref webpack/webpack-hot-middleware#306

* feat(client): add disconnect() to close the SSE connection (#2351)

* feat(client): add disconnect() to close the SSE connection

Expose a way to close the EventSource for the current path and stop
the reconnection watchdog (e.g. before tearing the page down). The
cached wrapper is dropped so a later setOptionsAndConnect() opens a
fresh connection.

Ref webpack/webpack-hot-middleware#367

* fixup!

* feat(hot): include the changed file in the building event (#2352)

The compiler's invalid hook reports which file invalidated the
compilation. Forward it as an optional `file` field on the `building`
payload and log it in the client, so users can see what triggered a
rebuild.

Ref webpack/webpack-hot-middleware#173

* feat(client): bring the error overlay to parity with webpack-dev-server (#2353)

* feat(client): bring the error overlay to parity with webpack-dev-server

- Render inside an about:blank iframe and style exclusively through
  the CSSOM so the overlay works under a strict style-src CSP; inline
  styles from ansi-html are re-applied via style.cssText.
- Support Trusted Types: innerHTML writes go through a policy
  (configurable via overlayTrustedTypesPolicyName).
- Capture uncaught runtime errors and unhandled rejections in the
  overlay (overlayRuntimeErrors, default true), with the same React
  error boundary heuristic as dev-server.
- Inline the HTML entity encoder and drop the html-entities dependency.
- Expose the overlay as a standalone subpath export
  (webpack-dev-middleware/client/overlay) so webpack-dev-server can
  reuse it.

Ref webpack/webpack-hot-middleware#457

* feat(client): support opening file references in the editor

When `overlayOpenEditorEndpoint` is set, file chips in the overlay
become clickable and issue GET <endpoint>?fileName=<file:line:column>
— the same contract as webpack-dev-server's open-editor route. The
endpoint implementation is left to the server integration, so no
launch-editor dependency is added.

* feat(client): bring the error overlay to parity with webpack-dev-server

- Render inside an about:blank iframe and style exclusively through
  the CSSOM so the overlay works under a strict style-src CSP; inline
  styles from ansi-html are re-applied via style.cssText.
- Support Trusted Types: innerHTML writes go through a policy
  (configurable via the overlay's trustedTypesPolicyName).
- Capture uncaught runtime errors and unhandled rejections in the
  overlay, with the same React error boundary heuristic as dev-server.
- Adopt dev-server's client.overlay option shape: a boolean or a JSON
  object with errors/warnings/runtimeErrors (booleans or filter
  functions) and trustedTypesPolicyName, so dev-server configs migrate
  as-is. Warnings are shown by default and no longer block updates,
  matching dev-server.
- Support opening file references in the editor: when
  overlayOpenEditorEndpoint is set, file chips issue
  GET <endpoint>?fileName=<file:line:column> (same contract as
  dev-server's open-editor route); the endpoint implementation is left
  to the server integration.
- Inline the HTML entity encoder and drop the html-entities dependency.
- Expose the overlay as a standalone subpath export
  (webpack-dev-middleware/client/overlay) so webpack-dev-server can
  reuse it.

Ref webpack/webpack-hot-middleware#457
Ref webpack/webpack-hot-middleware#184

* fix(client): track overlay problems per compilation

With a MultiCompiler the client receives one event per bundle, and a
successful build from one bundle used to wipe another bundle's
still-valid errors from the overlay. Keep the live problems keyed by
compilation name, render the union, and only clear the overlay when
every compilation is clean. The console de-duplication cache is also
keyed per bundle so interleaved payloads do not defeat it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(examples): add a MultiCompiler hot example

Keep examples/hot as the minimal single-compiler setup (now with
runtime-error buttons and an opt-in DEMO_WARNING build warning), and
add examples/hot-multi-compiler: two compilers ("app" and "admin")
sharing one middleware instance and a single SSE connection, with
per-bundle `?name=` client scoping and recipes for the overlay's
per-compilation error tracking.

Each multi config sets a distinct output.uniqueName (and prefixed
hot-update filenames): with both bundles on one page, a shared
webpackHotUpdate global would make each bundle's updates land in the
wrong runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(client): nest overlay extensions inside the overlay option

Move styles, ansiColors and openEditorEndpoint into the overlay object
as webpack-dev-middleware extensions of dev-server's client.overlay
shape, replacing the flat overlayStyles/ansiColors/
overlayOpenEditorEndpoint options.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* test(client): cover process-update

Isolate the HMR runtime lookup behind a small util so process-update
is loadable under jest, and add coverage for the updated-modules
console output.

* feat(client): collapse the updated-modules list in the console (#2361)

The success path now logs a one-line summary with the module count at
the default "info" level, and the per-module detail as a collapsed
console group visible from the "log" level up (webpack's runtime
logger gates groups below that level). The unaccepted-modules warning
list stays flat: it is diagnostic output for the failing case.

Ref webpack/webpack-hot-middleware#311

* feat(client): paginate the overlay problems (#2359)

The overlay now shows one problem at a time, with a header row holding
the problem badge and prev/next navigation (colored after the problem
type), a page counter and arrow-key navigation. Each new problem set
starts at its first page, accumulated runtime errors land on the
newest one, and identical re-published sets (e.g. another bundle of a
multi-compiler syncing) keep the current page. Enabled by default;
disable with the overlay option's `paginate` extension
(`?overlay={"paginate":false}`) to render the full list.

Also ignore click targets re-rendered away mid-dispatch in the
backdrop-dismiss listener, which otherwise closed the overlay when
clicking the navigation buttons.

* fix(client): avoid double slash when joining dynamicPublicPath (#2348)

* fix(client): avoid double slash when joining dynamicPublicPath

When output.publicPath ends with a slash (the common case) and the SSE
path starts with one, the naive concatenation produced URLs like
`https://host//__webpack_hmr`, which never match the server-side
pathname check.

Ref webpack/webpack-hot-middleware#366

* fix(client): append path to the public path like a filename in dynamicPublicPath

The default `path` starts with "/", so the naive concatenation with a
public path ending in "/" produced URLs like
`https://host//__webpack_hmr`, which never match the server-side
pathname check.

Strip the leading slash from `path` and concatenate without any other
normalization: the public path itself is left untouched (intentional
double slashes, e.g. nginx rewrites, are preserved) and is expected to
end with "/".

Ref webpack/webpack-hot-middleware#366

* feat(hot): add a building indicator with optional compilation progress (#2358)

The client shows a small badge (shadow DOM, CSSOM-only styles) while a
rebuild is in progress, enabled by default (disable with
`?progress=false` on the client entry). When the server enables the new
`hot.progress` option, webpack's ProgressPlugin publishes throttled
`progress` events over SSE (deduplicated by rounded percent, reset on
each new build) and the badge renders a CSP-safe SVG progress ring with
the compilation percentage.

The palette is shared with the error overlay through a common theme
module, and the indicator is exposed as a standalone subpath export
(webpack-dev-middleware/client/indicator) so other tooling can reuse
it. Both the indicator and the overlay now bail out safely when
`document.body` does not exist yet.

Ref webpack/webpack-hot-middleware#167

* docs: add HMR notes and troubleshooting section (#2362)

* docs: add HMR notes and troubleshooting section

Covers the recurring questions from the webpack-hot-middleware
backlog: browser connection limits and HTTP/2
(webpack/webpack-hot-middleware#423,
webpack/webpack-hot-middleware#298), warning filtering layers
(webpack/webpack-hot-middleware#319,
webpack/webpack-hot-middleware#228), absolute paths and public paths
(webpack/webpack-hot-middleware#281,
webpack/webpack-hot-middleware#203,
webpack/webpack-hot-middleware#193), and a custom-events recipe
(webpack/webpack-hot-middleware#118,
webpack/webpack-hot-middleware#226). The client `reload` option now
also notes its default differs from webpack-hot-middleware
(webpack/webpack-hot-middleware#370).

* fixup!

* fix(hot): publish sync instead of built for unchanged bundles (#2357)

* fix(hot): publish sync instead of built for unchanged bundles

With a MultiCompiler, rebuilding one child re-emits done for every
child. Clients of the unchanged bundles then try to fetch a hot-update
manifest that was never emitted (404 / "Cannot find update", or an
unwanted full reload with reload=true).

Compare each bundle's hash against the previous build and announce
unchanged bundles as `sync` instead of `built`. The first build still
publishes `built`, and new clients are still caught up with `sync` —
but no longer while a rebuild is in progress.

Ref webpack/webpack-hot-middleware#312

* fixup!

* fix(client): reload when an accept handler errors during apply (#2360)

* fix(client): reload when an accept handler errors during apply

An error thrown inside a module.hot.accept handler was logged and
ignored, leaving the page running stale code with no recovery. When the
`reload` option is enabled (the default), onErrored now falls back to a
full page reload, like the other unrecoverable update paths.

The HMR runtime and page-reload calls are isolated behind small utils
so process-update finally has direct test coverage. A missing HMR
runtime is now reported with a single actionable error instead of
throwing at bundle evaluation.

Ref webpack/webpack-hot-middleware#334

* fixup!

* fixup!

* fix(hot): scope the catch-up sync and pair bundles by name

- A client connecting after a build now receives the catch-up `sync`
  alone instead of it being broadcast to every connected client, which
  re-triggered their reporters on each new tab.
- `publishBundles` pairs the previous build's bundles by name instead of
  array index, so a changing set of compilations cannot compare a bundle
  against a sibling's hash. Unnamed bundles keep the positional pairing.
- The client's console dedup cache is cleared per bundle, so a sibling's
  clean build no longer re-logs another bundle's unchanged problems.

* fix(hot): end SSE requests that arrive after close()

instance.close() left `context.hot` set while `handle()` silently
returned, so a request to the hot path after close received no
response, no `next()`, and hung until the socket timed out.

Detach the SSE intercept on close so requests fall through to the
regular middleware, and answer 404 from `handle()` for requests that
raced the intercept.

* feat(client): share the building indicator and track builds by source (#2369)

The badge state lives in a window singleton (same pattern as the
overlay), so a second bundled copy of the module drives the same badge
instead of stacking a duplicate, and fields missing from another
version's state are filled in place.

show() and hide() take an optional source: each concurrent build keeps
the badge alive until every source finished, so in a MultiCompiler one
bundle's `built` no longer hides the badge while a sibling is still
compiling — and with two clients sharing the badge, one client's hide()
cannot drop the other's indication. hide() without a source still
removes the badge unconditionally. Progress payloads carry no name, so
the client attributes them to the most recent `building` event.

* feat(client): share the overlay across bundled copies and report by source (#2368)

* feat(client): share the overlay across bundled copies and report by source

The overlay DOM and problem state now live in a window singleton, so a
second bundled copy of the module (e.g. the webpack-dev-server client
once it adopts this overlay) renders into the same iframe instead of
stacking a duplicate. Fields missing from a state created by an older
copy are filled in place, and the Trusted Types policy moves into the
shared state too — creating two policies with the same name throws
under an enforced CSP.

* test(overlay): prevent re-rendering when clearing a source that reported nothing

* refactor(cspell): consolidate cspell configuration into .cspell.json and remove cspell.config.json

* fix(hot): name building events and repair the client reconnect lifecycle

The building payload now carries the name of the compilation that
invalidated (tapped per child compiler, since the MultiCompiler hook
does not say which one fired), so clients can pair it with the built or
sync that follows — without it the building indicator registered every
build under "" and could never be hidden for named compilations.

On the client, the inactivity watchdog is restarted inside init(), so it
survives a reconnect instead of dying with the first clearInterval, and
the reconnect timeout handle is now stored and cleared by close(), so
disconnect() during the reconnect window no longer resurrects an
orphaned, uncloseable connection.

All three defects were inherited from webpack-hot-middleware.

* fix(hot): harden the SSE handshake and defer the overlay's Escape listener

The handshake now ends a response whose headers were already sent
instead of crashing on writeHead, and the middleware routes handshake
errors to next() — an exception there previously became an unhandled
rejection that killed the process.

The overlay's Escape listener on the host document is now attached
lazily inside ensureOverlay (once per page, through the shared state),
matching how webpack-dev-server registers it inside createOverlay, so
importing the client in a non-DOM environment (SSR bundle, worker) no
longer throws at evaluation time.

* fix(hot): align statsOptions and heartbeat validation and pair duplicate-name bundles by occurrence

statsOptions now accepts only the object form everywhere: the schema
rejected string presets the types allowed, and booleans validated but
were silently ignored since toBundles only merges objects over the
middleware's base stats options. Schema, JSDoc, and generated types all
agree now, so invalid forms fail validation instead of at runtime.

heartbeat: 0 was schema-valid but silently replaced with the 10s
default by a falsy check — the schema now requires >= 1 and the code
uses ?? so the option and its validation tell the same story.

* fix(hot): require a leading slash in the hot path and serialize publishes once

A path without a leading slash validated but could never match a
request, since URL pathnames always start with one — the schema now
enforces it. publish() also serializes the payload once per event
instead of once per connected client.

* fix(hono): set headers in the SSE writeHead shim instead of appending

writeHead semantics replace a header an earlier middleware may have
left on the response — appending merged both values into one
comma-joined header (e.g. "max-age=100, no-cache, no-transform" for
Cache-Control, which intermediaries may treat as cacheable).

* docs: fix the hot option reference and add a webpack-hot-middleware migration guide

The changeset advertised a `log` option that fails validation (it is
`progress`), `hot.statsOptions` documented the boolean form the schema
no longer accepts, `hot.path` now notes the required leading slash, and
the launch-editor sentence that had landed inside the `paginate`
parenthetical is back with `openEditorEndpoint`, where it belongs.

Also adds a "Migrating from webpack-hot-middleware" section: server and
webpack-config before/after, an option mapping table, and the
programmatic API equivalents.

* docs: make the migration guide's entry snippet valid standalone JS

eslint parses the README's fenced code blocks, and the bare
`entry: [...]` object fragment failed with "Unexpected token :" — the
before/after entries are now wrapped in module.exports.

* build: compile the client to ES5 and keep dist on the node target

The shared preset-env targets (`esmodules` + node 0.12) also applied to
`src`, so `dist` was downlevelled to ES5 with inlined regenerator helpers.
Target node 20.9 by default and override only `client-src`, whose output has
to be parsable by an old browser. `lint:types-client` now checks the client
without node types and against an ES5 `lib`, so a post-ES5 built-in is an
error instead of a runtime failure.

* fix(package): do not add an exports field yet

Adding `exports` hides every path the package does not list (deep imports like
`webpack-dev-middleware/dist/...`), which is a breaking change. The
`./client` subpath resolves through the directory anyway, so the field is
dropped and a TODO records it for the next major. Also drops the now unused
`@babel/plugin-transform-runtime` dev dependency.

* refactor(client): define everything before it is used and stay on ES5 built-ins

Drops the `no-use-before-define: { functions: false }` exception: the client
is reordered so every function is defined before its first reference, which
also removes the `@ts-expect-error`s that hoisting forced on process-update.
Replaces the built-ins an ES5 browser does not have (`Map`,
`URLSearchParams`, `Object.values`, `flatMap`, `includes`, `append`,
`remove`) and guards `fetch`. A non-numeric `timeout` is now ignored (`NaN`
never compares greater, so the watchdog could never report a dead connection)
and `dynamicPublicPath=false` no longer behaves like `true`.

* fix(hot): never write to a response that already ended

The heartbeat and every publish wrote to each registered client, so a
response ended between the socket dying and its `close` event threw a
write-after-end. Clients are skipped once they end, and a request that was
already destroyed when the handshake finished is dropped instead of staying
in the client map forever (`close` never fires for it again).

* ci: do not add a branch to the workflow triggers

* test: keep the js3 array fixture output under test/outputs

Its siblings write to `../outputs/one-error-one-warning-one-success`; `js3`
wrote into `test/fixtures` and left an untracked directory behind after every
run.

* docs: list the hot options and note what the client needs in old browsers

* test(client): assert the compiled client is always ES5

Compiles every `client-src` file through the real build config (`envName:
"production"`, since jest runs under the test env) and walks the acorn AST
against an allowlist of ES5.1 node types, so a syntax nobody thought of fails
rather than slips through. `let`, generators, computed/shorthand properties,
bigint literals and post-ES5 regular expression flags are checked on the node
types ES5 already had. Only the module syntax webpack consumes (and the
`import.meta.webpackHot` it replaces) is allowed through.

* docs: list the client overlay options in their own table

* test(hot): cover the remaining lines of the SSE stream

Codecov reported three uncovered lines in src/hot.js. Two are branches the
review added (the catch-up write to an ended response, and the idempotent
disconnect), one is the child-compilation path of extractBundles. hot.js is
at 100% lines from the unit test alone now.

* fix(hot): address the automated review findings

- Attach the SSE stream only for GET: a HEAD request to the hot path was
  handed a body and left hanging until it timed out.
- Reject a query string or fragment in `hot.path`: `pathMatch` compares
  pathnames, so such an endpoint could never match a request.
- Do not subscribe the same client copy twice: with `autoConnect` on, a
  `setOptionsAndConnect()` call added a second listener and every message
  was processed twice. Keyed by path, so a call that changes `path` still
  subscribes.
- Keep delivering to `subscribeAll` when the `name` filter rejects an
  event, as its documentation promises.
- Walk `querySelectorAll` by index: a NodeList is not iterable in an ES5
  browser, so the compiled for...of threw there.
- Say in the changeset that the client ships with the package rather than
  being served by the middleware.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: alexander-akait <sheo13666q@gmail.com>
alexander-akait added a commit that referenced this pull request Sep 2, 2026
* feat: add hot option for hot module replacement

* feat: add browser client runtime for HMR (#2323)

* feat: add browser client runtime for HMR

Ports the browser client into `client-src/`, mirroring the layout used
in `webpack-dev-server` (source in `client-src/`, built to `client/`).

The client connects to the SSE endpoint via `EventSource`, parses
query-string options from `__resourceQuery`, dispatches `building`,
`built` and `sync` payloads, applies HMR through `process-update.js`
and renders compile-time errors and warnings through an in-page overlay
(`overlay.js`).

Exposed via the `./client` subpath export so users can wire it as a
webpack entry: `require('webpack-dev-middleware/client')`. The source
is transpiled with a browser-targeted babel override and the resulting
files are shipped under `/client`.

* test: add browser client runtime tests

Covers the public client API and key SSE handling paths in jsdom:
EventSource connection on default and custom paths, ignored heartbeat
messages, dispatch of building/built/sync to subscribers, custom
handler for unknown actions, warnings on invalid JSON, EventSource
wrapper caching across multiple entries, and timeout-driven reconnect.

* test: expand browser client coverage to mirror webpack-hot-middleware

Adds tests covering the original webpack-hot-middleware client suite
(processUpdate invocations on built/sync, errored/warning behavior,
overlay show/hide transitions, the overlayWarnings option, the name
filter), while keeping the new coverage for heartbeat handling,
invalid JSON warnings, EventSource wrapper caching across entries and
timeout-driven reconnects.

* ci: run on push and PRs against the hot-middleware umbrella branch

* refactor(client): switch process-update to promise-only HMR API

The ported logic called `module.hot.check`/`apply` with both a callback
and a Promise-handling branch to support webpack < 2. In webpack 5 both
paths fire, so the callback ran twice, triggering a redundant
`module.hot.apply` on every update.

Drop the legacy callback path and use the Promise API exclusively, which
is the canonical webpack 5 contract and matches our peer dependency.

* chore(client): type-check client-src with a dedicated tsconfig

Mirrors the layout webpack-dev-server uses: a separate
`tsconfig.client.json` (`noEmit`, browser-targeted libs,
`webpack/module` augmentation) runs over `client-src/` via a new
`lint:types-client` script, with a small `client-src/globals.d.ts`
declaring `ansi-html-community` and the per-page singletons the client
stores on `window`.

Refines the JSDoc annotations in `client-src/index.js` and
`client-src/process-update.js` so `module.hot`, `window` extensions
and the HMR `ApplyOptions` type-check cleanly.

* docs: document the browser client runtime in README

Adds a 'Hot Module Replacement client' section explaining how to wire
`webpack-dev-middleware/client` as a webpack entry, the query-string
options that the runtime understands, and the programmatic
`subscribe` / `subscribeAll` / `useCustomOverlay` /
`setOptionsAndConnect` exports.

* chore(client): adopt browser-outdated-recommended-commonjs eslint preset

Switches the client-src lint config to the dedicated preset
`eslint-config-webpack` ships for browser-targeted CommonJS code, the
same family of preset webpack-dev-server uses for its own client.

Brings the per-directory rule overrides down to two:
`no-console` (legitimately used for HMR status messages) and
`no-use-before-define` (relaxed for hoisted function declarations).

Adjusts the source to satisfy the rest of the preset directly: adds
`use strict` headers, fills in JSDoc for every function, renames
`EventSourceWrapper` to `createEventSourceWrapper` (per `new-cap`),
names the anonymous module exports, and reorders `performReload`
before `handleError` so it is declared before use.

* refactor(client): route logging through webpack's runtime logger

Wraps `webpack/lib/logging/runtime` in a small `utils/log.js` module
that exposes a level-based logger registered under the
`webpack-dev-middleware` name (matching the infrastructure logger the
server side already uses). Replaces every `console.log`/
`console.warn` call in the client and HMR update path with the
equivalent `log.info`/`log.warn`/`log.error` calls so output is
prefixed and gated by a single `logging` level.

User-facing API:

- `logging` query-string option accepts `none|error|warn|info|log|verbose`
- The previous `log`, `warn`, `noInfo` and `quiet` flags are dropped
  in favour of `logging`

Other cleanups enabled by this:

- Drop the `no-console: off` exception from the client-src ESLint config
- Update README's client option table accordingly
- Add tests covering the new `logging` levels and the logger prefix

* test(client): use snapshots for logger output assertions

Replaces regex-based `some(([msg]) => /.../).toBe(true)` checks on the
mocked console with `toMatchSnapshot()` over `mock.calls`. The
snapshots capture the exact log lines including the
`[webpack-dev-middleware]` prefix and per-call argument count, so any
change to the log format surfaces in the test output instead of silently
passing.

Adds explicit assertions to the existing error / warning flow tests so
`console.error` / `console.warn` mocks are not only silenced but also
verified to be called with the expected output.

* chore: document why client-src needs the ecmaVersion override

`eslint-config-webpack@4.9.6` still ships `browser-outdated-recommended-commonjs`
with `configs["javascript/es5"]` and no parser override, so `const`
is rejected. The module variant of the same preset patches this
upstream — we replicate the patch locally until the commonjs variant
does the same.

* refactor(client): migrate to ES modules and update Babel configuration

* fixup!

* feat: implement hot module replacement middleware (#2321)

* fix(test): correct output path typo in webpack.array.warning fixture

The first compiler entry used `../../outputs/...` which escaped the test
directory and wrote artifacts to the repository root, outside of the
`/test/outputs` paths covered by `.gitignore` and `.prettierignore`.

* feat: implement hot module replacement middleware

Adds a `hot: true | { path, heartbeat, log, statsOptions }` option that
turns the dev middleware into a Server-Sent Events endpoint publishing
`building`, `built` and `sync` payloads from the webpack compiler.

The hot endpoint defaults to `/__webpack_hmr` and is served by the
existing middleware - no separate `app.use()` call is required.
`close()` tears down clients and the heartbeat timer.

* test: add tests for hot middleware

Covers schema validation of the `hot` option (success and failure cases
with snapshots), unit tests for `pathMatch`, `formatErrors`,
`buildModuleMap` and `createEventStream`, and integration tests that
verify SSE headers, the default and custom hot paths, MultiCompiler
support, `close()` teardown, and the `log` option (custom function and
`log: false`).

* test: add unit and integration tests for hot middleware functionality

* feat: enhance honoWrapper to support Web ReadableStream for hot middleware responses

* feat: add TypeScript definitions for hot module replacement functionality

* test(hot): cover publish, sync-on-connect, headers and close behavior

Ports the remaining unit-level cases from webpack-hot-middleware that
were not already covered by the framework matrix in middleware.test.js:

- the public `publish()` API broadcasts custom payloads
- a client connecting after a build receives a `sync` event
  initialised from the last stats
- HTTP/1 clients get `Connection: keep-alive`, HTTP/2 clients do not
- when `stats.name` is empty the published payload falls back to
  `compilation.name`
- a single broadcast reaches every attached client
- after `close()` further compiler events do not produce writes

* docs: document the hot option in README

* docs: list the hot option in the README options table

* refactor: replace EXPECTED_ANY with specific types in hot module definitions

* docs: update README to clarify default stats options for SSE payload

* refactor: remove log option from hot middleware and update related documentation

* docs: update default value for hot option in README to false

* docs: add hot module replacement example with Express server and client setup

* feat: enhance error overlay with close button and improved styling

* feat: enable reload option for HMR by default and update tests

* feat: add SSE helper functions and tests for event streaming

* refactor(client): inline ansi stripping and drop the strip-ansi dependency (#2350)

The client runtime runs in the browser, so Node's
util.stripVTControlCharacters is not an option. Inline the ansi-regex
pattern (verified against strip-ansi@6 output) in a small util instead
of shipping the dependency.

Ref webpack/webpack-hot-middleware#465
Ref webpack/webpack-hot-middleware#474

* refactor(hot): drop the module map from the SSE payload (#2349)

webpack-dev-server does not send module names over the wire: the HMR
runtime logs module ids on apply. Align the SSE payload with that
(name, action, time, hash, errors, warnings) instead of serializing a
module id → name map that was only used for log cosmetics and was
empty with the default stats options anyway.

Ref webpack/webpack-hot-middleware#452
Ref webpack/webpack-hot-middleware#306

* feat(client): add disconnect() to close the SSE connection (#2351)

* feat(client): add disconnect() to close the SSE connection

Expose a way to close the EventSource for the current path and stop
the reconnection watchdog (e.g. before tearing the page down). The
cached wrapper is dropped so a later setOptionsAndConnect() opens a
fresh connection.

Ref webpack/webpack-hot-middleware#367

* fixup!

* feat(hot): include the changed file in the building event (#2352)

The compiler's invalid hook reports which file invalidated the
compilation. Forward it as an optional `file` field on the `building`
payload and log it in the client, so users can see what triggered a
rebuild.

Ref webpack/webpack-hot-middleware#173

* feat(client): bring the error overlay to parity with webpack-dev-server (#2353)

* feat(client): bring the error overlay to parity with webpack-dev-server

- Render inside an about:blank iframe and style exclusively through
  the CSSOM so the overlay works under a strict style-src CSP; inline
  styles from ansi-html are re-applied via style.cssText.
- Support Trusted Types: innerHTML writes go through a policy
  (configurable via overlayTrustedTypesPolicyName).
- Capture uncaught runtime errors and unhandled rejections in the
  overlay (overlayRuntimeErrors, default true), with the same React
  error boundary heuristic as dev-server.
- Inline the HTML entity encoder and drop the html-entities dependency.
- Expose the overlay as a standalone subpath export
  (webpack-dev-middleware/client/overlay) so webpack-dev-server can
  reuse it.

Ref webpack/webpack-hot-middleware#457

* feat(client): support opening file references in the editor

When `overlayOpenEditorEndpoint` is set, file chips in the overlay
become clickable and issue GET <endpoint>?fileName=<file:line:column>
— the same contract as webpack-dev-server's open-editor route. The
endpoint implementation is left to the server integration, so no
launch-editor dependency is added.

* feat(client): bring the error overlay to parity with webpack-dev-server

- Render inside an about:blank iframe and style exclusively through
  the CSSOM so the overlay works under a strict style-src CSP; inline
  styles from ansi-html are re-applied via style.cssText.
- Support Trusted Types: innerHTML writes go through a policy
  (configurable via the overlay's trustedTypesPolicyName).
- Capture uncaught runtime errors and unhandled rejections in the
  overlay, with the same React error boundary heuristic as dev-server.
- Adopt dev-server's client.overlay option shape: a boolean or a JSON
  object with errors/warnings/runtimeErrors (booleans or filter
  functions) and trustedTypesPolicyName, so dev-server configs migrate
  as-is. Warnings are shown by default and no longer block updates,
  matching dev-server.
- Support opening file references in the editor: when
  overlayOpenEditorEndpoint is set, file chips issue
  GET <endpoint>?fileName=<file:line:column> (same contract as
  dev-server's open-editor route); the endpoint implementation is left
  to the server integration.
- Inline the HTML entity encoder and drop the html-entities dependency.
- Expose the overlay as a standalone subpath export
  (webpack-dev-middleware/client/overlay) so webpack-dev-server can
  reuse it.

Ref webpack/webpack-hot-middleware#457
Ref webpack/webpack-hot-middleware#184

* fix(client): track overlay problems per compilation

With a MultiCompiler the client receives one event per bundle, and a
successful build from one bundle used to wipe another bundle's
still-valid errors from the overlay. Keep the live problems keyed by
compilation name, render the union, and only clear the overlay when
every compilation is clean. The console de-duplication cache is also
keyed per bundle so interleaved payloads do not defeat it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(examples): add a MultiCompiler hot example

Keep examples/hot as the minimal single-compiler setup (now with
runtime-error buttons and an opt-in DEMO_WARNING build warning), and
add examples/hot-multi-compiler: two compilers ("app" and "admin")
sharing one middleware instance and a single SSE connection, with
per-bundle `?name=` client scoping and recipes for the overlay's
per-compilation error tracking.

Each multi config sets a distinct output.uniqueName (and prefixed
hot-update filenames): with both bundles on one page, a shared
webpackHotUpdate global would make each bundle's updates land in the
wrong runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(client): nest overlay extensions inside the overlay option

Move styles, ansiColors and openEditorEndpoint into the overlay object
as webpack-dev-middleware extensions of dev-server's client.overlay
shape, replacing the flat overlayStyles/ansiColors/
overlayOpenEditorEndpoint options.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* test(client): cover process-update

Isolate the HMR runtime lookup behind a small util so process-update
is loadable under jest, and add coverage for the updated-modules
console output.

* feat(client): collapse the updated-modules list in the console (#2361)

The success path now logs a one-line summary with the module count at
the default "info" level, and the per-module detail as a collapsed
console group visible from the "log" level up (webpack's runtime
logger gates groups below that level). The unaccepted-modules warning
list stays flat: it is diagnostic output for the failing case.

Ref webpack/webpack-hot-middleware#311

* feat(client): paginate the overlay problems (#2359)

The overlay now shows one problem at a time, with a header row holding
the problem badge and prev/next navigation (colored after the problem
type), a page counter and arrow-key navigation. Each new problem set
starts at its first page, accumulated runtime errors land on the
newest one, and identical re-published sets (e.g. another bundle of a
multi-compiler syncing) keep the current page. Enabled by default;
disable with the overlay option's `paginate` extension
(`?overlay={"paginate":false}`) to render the full list.

Also ignore click targets re-rendered away mid-dispatch in the
backdrop-dismiss listener, which otherwise closed the overlay when
clicking the navigation buttons.

* fix(client): avoid double slash when joining dynamicPublicPath (#2348)

* fix(client): avoid double slash when joining dynamicPublicPath

When output.publicPath ends with a slash (the common case) and the SSE
path starts with one, the naive concatenation produced URLs like
`https://host//__webpack_hmr`, which never match the server-side
pathname check.

Ref webpack/webpack-hot-middleware#366

* fix(client): append path to the public path like a filename in dynamicPublicPath

The default `path` starts with "/", so the naive concatenation with a
public path ending in "/" produced URLs like
`https://host//__webpack_hmr`, which never match the server-side
pathname check.

Strip the leading slash from `path` and concatenate without any other
normalization: the public path itself is left untouched (intentional
double slashes, e.g. nginx rewrites, are preserved) and is expected to
end with "/".

Ref webpack/webpack-hot-middleware#366

* feat(hot): add a building indicator with optional compilation progress (#2358)

The client shows a small badge (shadow DOM, CSSOM-only styles) while a
rebuild is in progress, enabled by default (disable with
`?progress=false` on the client entry). When the server enables the new
`hot.progress` option, webpack's ProgressPlugin publishes throttled
`progress` events over SSE (deduplicated by rounded percent, reset on
each new build) and the badge renders a CSP-safe SVG progress ring with
the compilation percentage.

The palette is shared with the error overlay through a common theme
module, and the indicator is exposed as a standalone subpath export
(webpack-dev-middleware/client/indicator) so other tooling can reuse
it. Both the indicator and the overlay now bail out safely when
`document.body` does not exist yet.

Ref webpack/webpack-hot-middleware#167

* docs: add HMR notes and troubleshooting section (#2362)

* docs: add HMR notes and troubleshooting section

Covers the recurring questions from the webpack-hot-middleware
backlog: browser connection limits and HTTP/2
(webpack/webpack-hot-middleware#423,
webpack/webpack-hot-middleware#298), warning filtering layers
(webpack/webpack-hot-middleware#319,
webpack/webpack-hot-middleware#228), absolute paths and public paths
(webpack/webpack-hot-middleware#281,
webpack/webpack-hot-middleware#203,
webpack/webpack-hot-middleware#193), and a custom-events recipe
(webpack/webpack-hot-middleware#118,
webpack/webpack-hot-middleware#226). The client `reload` option now
also notes its default differs from webpack-hot-middleware
(webpack/webpack-hot-middleware#370).

* fixup!

* fix(hot): publish sync instead of built for unchanged bundles (#2357)

* fix(hot): publish sync instead of built for unchanged bundles

With a MultiCompiler, rebuilding one child re-emits done for every
child. Clients of the unchanged bundles then try to fetch a hot-update
manifest that was never emitted (404 / "Cannot find update", or an
unwanted full reload with reload=true).

Compare each bundle's hash against the previous build and announce
unchanged bundles as `sync` instead of `built`. The first build still
publishes `built`, and new clients are still caught up with `sync` —
but no longer while a rebuild is in progress.

Ref webpack/webpack-hot-middleware#312

* fixup!

* fix(client): reload when an accept handler errors during apply (#2360)

* fix(client): reload when an accept handler errors during apply

An error thrown inside a module.hot.accept handler was logged and
ignored, leaving the page running stale code with no recovery. When the
`reload` option is enabled (the default), onErrored now falls back to a
full page reload, like the other unrecoverable update paths.

The HMR runtime and page-reload calls are isolated behind small utils
so process-update finally has direct test coverage. A missing HMR
runtime is now reported with a single actionable error instead of
throwing at bundle evaluation.

Ref webpack/webpack-hot-middleware#334

* fixup!

* fixup!

* fix(hot): scope the catch-up sync and pair bundles by name

- A client connecting after a build now receives the catch-up `sync`
  alone instead of it being broadcast to every connected client, which
  re-triggered their reporters on each new tab.
- `publishBundles` pairs the previous build's bundles by name instead of
  array index, so a changing set of compilations cannot compare a bundle
  against a sibling's hash. Unnamed bundles keep the positional pairing.
- The client's console dedup cache is cleared per bundle, so a sibling's
  clean build no longer re-logs another bundle's unchanged problems.

* fix(hot): end SSE requests that arrive after close()

instance.close() left `context.hot` set while `handle()` silently
returned, so a request to the hot path after close received no
response, no `next()`, and hung until the socket timed out.

Detach the SSE intercept on close so requests fall through to the
regular middleware, and answer 404 from `handle()` for requests that
raced the intercept.

* feat(client): share the building indicator and track builds by source (#2369)

The badge state lives in a window singleton (same pattern as the
overlay), so a second bundled copy of the module drives the same badge
instead of stacking a duplicate, and fields missing from another
version's state are filled in place.

show() and hide() take an optional source: each concurrent build keeps
the badge alive until every source finished, so in a MultiCompiler one
bundle's `built` no longer hides the badge while a sibling is still
compiling — and with two clients sharing the badge, one client's hide()
cannot drop the other's indication. hide() without a source still
removes the badge unconditionally. Progress payloads carry no name, so
the client attributes them to the most recent `building` event.

* feat(client): share the overlay across bundled copies and report by source (#2368)

* feat(client): share the overlay across bundled copies and report by source

The overlay DOM and problem state now live in a window singleton, so a
second bundled copy of the module (e.g. the webpack-dev-server client
once it adopts this overlay) renders into the same iframe instead of
stacking a duplicate. Fields missing from a state created by an older
copy are filled in place, and the Trusted Types policy moves into the
shared state too — creating two policies with the same name throws
under an enforced CSP.

* test(overlay): prevent re-rendering when clearing a source that reported nothing

* refactor(cspell): consolidate cspell configuration into .cspell.json and remove cspell.config.json

* fix(hot): name building events and repair the client reconnect lifecycle

The building payload now carries the name of the compilation that
invalidated (tapped per child compiler, since the MultiCompiler hook
does not say which one fired), so clients can pair it with the built or
sync that follows — without it the building indicator registered every
build under "" and could never be hidden for named compilations.

On the client, the inactivity watchdog is restarted inside init(), so it
survives a reconnect instead of dying with the first clearInterval, and
the reconnect timeout handle is now stored and cleared by close(), so
disconnect() during the reconnect window no longer resurrects an
orphaned, uncloseable connection.

All three defects were inherited from webpack-hot-middleware.

* fix(hot): harden the SSE handshake and defer the overlay's Escape listener

The handshake now ends a response whose headers were already sent
instead of crashing on writeHead, and the middleware routes handshake
errors to next() — an exception there previously became an unhandled
rejection that killed the process.

The overlay's Escape listener on the host document is now attached
lazily inside ensureOverlay (once per page, through the shared state),
matching how webpack-dev-server registers it inside createOverlay, so
importing the client in a non-DOM environment (SSR bundle, worker) no
longer throws at evaluation time.

* fix(hot): align statsOptions and heartbeat validation and pair duplicate-name bundles by occurrence

statsOptions now accepts only the object form everywhere: the schema
rejected string presets the types allowed, and booleans validated but
were silently ignored since toBundles only merges objects over the
middleware's base stats options. Schema, JSDoc, and generated types all
agree now, so invalid forms fail validation instead of at runtime.

heartbeat: 0 was schema-valid but silently replaced with the 10s
default by a falsy check — the schema now requires >= 1 and the code
uses ?? so the option and its validation tell the same story.

* fix(hot): require a leading slash in the hot path and serialize publishes once

A path without a leading slash validated but could never match a
request, since URL pathnames always start with one — the schema now
enforces it. publish() also serializes the payload once per event
instead of once per connected client.

* fix(hono): set headers in the SSE writeHead shim instead of appending

writeHead semantics replace a header an earlier middleware may have
left on the response — appending merged both values into one
comma-joined header (e.g. "max-age=100, no-cache, no-transform" for
Cache-Control, which intermediaries may treat as cacheable).

* docs: fix the hot option reference and add a webpack-hot-middleware migration guide

The changeset advertised a `log` option that fails validation (it is
`progress`), `hot.statsOptions` documented the boolean form the schema
no longer accepts, `hot.path` now notes the required leading slash, and
the launch-editor sentence that had landed inside the `paginate`
parenthetical is back with `openEditorEndpoint`, where it belongs.

Also adds a "Migrating from webpack-hot-middleware" section: server and
webpack-config before/after, an option mapping table, and the
programmatic API equivalents.

* docs: make the migration guide's entry snippet valid standalone JS

eslint parses the README's fenced code blocks, and the bare
`entry: [...]` object fragment failed with "Unexpected token :" — the
before/after entries are now wrapped in module.exports.

* test: adopt a hybrid unit + browser e2e model, shard CI, and snapshot the console

The hot client, overlay, and building indicator are now exercised end
to end in headless Chrome (puppeteer 22, the last CJS release jest can
require) against a real webpack watcher: updates applied without
reload, the full-reload fallback, reconnect with catch-up sync after a
server restart, disconnect() semantics, overlay lifecycle (Escape on
the host page, HTML-in-error-message escaping, warnings, pagination),
the building badge, multi-compiler ?name= filtering with per-bundle
overlay slots, and the custom publish/subscribe API.

Console behavior is snapshotted from the real browser console: the full
info-level update cycle, logging=none/warn/error gates, and the
per-bundle dedup where a sibling's clean rebuild does not re-log
another bundle's unchanged warning while its own rebuild does. Each
multi-compiler compilation gets its own context dir so editing one
entry cannot invalidate the sibling's watcher and shuffle event order.

The jsdom suites stay, slimmed to what a browser cannot exercise
deterministically: process-update internals, shared state between
bundled copies, option parsing, and protocol edges. The review of the
new tests also surfaced a real client bug, now fixed: an EventSource
error event queued behind disconnect() re-armed the reconnect timer on
the orphaned wrapper — close() is now final.

e2e runs serially through the new test:e2e script (four concurrent
Chrome instances plus watchers starved the rest of the suite), and CI
shards every test job 4x with jest --shard, uploading per-shard
coverage that a final job aggregates for codecov, mirroring
webpack-dev-server's setup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): snapshot the logging=log module detail and drop the covered unit tests

A logging=log e2e run now snapshots the collapsed "Updated modules:"
group and its per-module entries straight from the browser console
(puppeteer surfaces group frames as console events). The jsdom
equivalents — the process-update logging describe and the
building-message log test, whose output already appears verbatim in the
info-level e2e snapshot — are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): move the reconnection tests to the browser and snapshot their console

The inactivity watchdog is now exercised against a genuinely silent SSE
connection (a server heartbeat far beyond the client timeout): the
second connect proves it fires on pure silence and the third that it
re-arms after a reconnect. Manual recovery gets its own test —
disconnect() followed by setOptionsAndConnect() opens a fresh
connection that still delivers updates. Both, plus the server-restart
catch-up test, snapshot their console sequence; browser network-error
noise is filtered out of the snapshots since its volume depends on
reconnect timing.

The fake-timer equivalents in the jsdom suite are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): exercise the indicator's shared state with two real bundled copies

The multi-compiler helper already produces what the jsdom suite could
only simulate: two genuinely separate bundled copies of the indicator
module on one page. Each bundle now exposes its copy as a global and
the tests drive both from the browser — a second copy adopts the same
badge instead of stacking one, per-source counting spans copies, hiding
an unknown source is ignored, and a leaner state shape left by an older
package version gets its missing fields filled (planted via
evaluateOnNewDocument before the bundles load).

The jsdom indicator suite is removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): cover the client option surface in the browser and slim the jsdom suite

The subscribe/subscribeAll contract, the ?name= filter, per-source badge
counting, the shared SSE connection between bundled clients (asserted
explicitly in the multi-compiler console snapshot: one "connected" for
two clients), the dev-server-shaped overlay option, and the warnings
filter function — parsed from the entry query and applied against two
real warning-producing modules — now all run in Chrome. The warnings
overlay test also covers clearing on a recovered build, and the
pagination test snapshots the two-error console output that the deleted
jsdom snapshot used to document.

client.test.js keeps only what the browser cannot pin deterministically:
problem-type transitions, the multi-bundle problem union, option
forwarding to the overlay factory, dynamicPublicPath URL building, and
protocol edges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): assert the multi-bundle problem union in the browser

Breaking both bundles shows the union of their problems in the shared
overlay — paginated one per page, in whichever order the compilers
finished. The two jsdom equivalents are removed: the union test, and
no-apply-on-errored-builds, which the multi-compiler console snapshot
already documents (no "Checking for updates" follows a bundle's error
report).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): collapse the rebuilding path in console snapshots

webpack's invalid hook sometimes reports the watched directory instead
of the changed file under the polling watcher, so the "bundle
rebuilding (…)" line alternated between the file and the fixture dir
and flaked the snapshots. The normalizer now collapses both forms to
"(<fixture> changed)".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): isolate the indicator shared-state tests from the hot client

The bundles' real hot clients call indicator.hide("a")/hide("b") when
their connect-time syncs arrive — the same source names these tests
register — so a late sync could wipe a source mid-test. progress=false
keeps the client away from the indicator entirely.

* test: drop a stray build artifact committed into fixtures

test/fixtures/js3 was compiled output (a misdirected copy of the
dev-server-false test's dist, whose real destination under test/outputs
is gitignored) swept up by a bulk git add.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: let test:only take file arguments without losing the e2e exclusion

--testPathIgnorePatterns is variadic, so a positional file after
`npm run test:only --` was consumed as another ignore pattern — silently
excluding the very file being targeted (the = form appends all the
same). The exclusion now lives in jest.config.js and lifts itself when a
test/e2e path is requested explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): exercise the overlay's shared state with two real bundled copies

Same pattern as the indicator: each compilation exposes its own bundled
copy of the overlay module and the tests drive both from the browser —
a second copy adopts the shared iframe instead of stacking one, both
sources paginate together in the union, either copy can dismiss what
the other rendered, errors from one copy outrank another's warnings
until the erroring source recovers, clearing an unreported source does
not rebuild the card another copy is showing, and a leaner state shape
left by an older package version gets its missing fields filled.

The jsdom equivalents are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): dismiss the overlay with real clicks

Backdrop click, close (×) button, and click-inside-stays-open now run
against real mouse events in the overlay iframe — the backdrop case
clicks the frame's top-left corner away from the centered card, and the
close-button pass rides the catch-up sync that re-shows the overlay
after a reload. The jsdom dismiss describe is removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: gitignore the js3 fixture output

The one-error-one-warning-one-* fixture configs build into
test/fixtures/js3 by design, so every logging run recreates it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): accumulate runtime errors in the browser and settle on webpack 5.109

Runtime-error accumulation (two errors paging "1 / 2" ↔ "2 / 2") and
unhandled promise rejections now run against real page errors — the
rejection fixture rejects from the page's own script so the event
carries the real reason. Their jsdom equivalents are removed.

The branch also settles on webpack 5.109: the lockfile catches up with
the already-bumped range, and the assertions and console snapshots
adopt 5.109's parse-error format (numbered code-frame gutter, caret
line, and the "File was parsed as module type" note) instead of
normalizing it away.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(client): replace webpack's logging runtime with a self-contained logger

webpack/lib/logging/runtime.js pulls tapable into the client bundle,
and tapable compiles its hooks with new Function — an EvalError under a
require-trusted-types-for 'script' CSP that killed the client's logging
and surfaced spurious runtime errors in the overlay. The replacement is
a level-gated console logger with byte-identical output (same prefix
merging, same level gates, groups from the "log" level up), verified
against the existing console snapshots. It also drops tapable and
webpack's logging machinery from the bundle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): enforce the Trusted Types CSP for real

The page is served with require-trusted-types-for 'script' and a
trusted-types allowlist holding only the configured policy name — under
real Chrome enforcement, which jsdom cannot do, the overlay only
renders if every HTML write went through that policy (including inside
the about:blank iframe, which inherits the page's CSP). The jsdom
trusted-types test is removed; the helper gained a pageHeaders option.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(client): keep HMR alive when Trusted Types blocks the logging runtime

Under a require-trusted-types-for 'script' CSP, tapable — bundled
through webpack's logging runtime — cannot compile its hooks: the first
log call throws an EvalError from new Function, killing whatever
listener happened to log (and with it the update flow). The logger
methods are now guarded, so webpack's logging runtime stays the engine
and enforcement just turns logging off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: prove SSE broadcast with two real pages and a mid-connection publish

Two browser pages connected to the same server both apply a single
edit — broadcast proven end to end. The unit variant also stops passing
vacuously: it now publishes while both SSE clients are attached instead
of only observing the per-client connect-time sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): poll app text on an interval so hidden pages resolve

waitForFunction's default requestAnimationFrame polling never fires on
a hidden document, so the backgrounded first tab of the broadcast test
hung on a wait for text it had already rendered. Also settle the
connections before editing so both pages receive the broadcast rather
than a catch-up sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): drive instance.invalidate() and instance.close() against a browser

invalidate() with unchanged sources rebuilds and reaches the page as a
building event followed by a no-op sync — the client neither re-renders
nor reloads (a built here would 404 on the never-emitted manifest).
close() ends the SSE stream under a connected page: the client falls
into its reconnect loop against an endpoint that no longer speaks SSE
and the page keeps running untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): keyboard pagination, paginate=false, and the cross-copy runtime filter

Arrow keys page through problems with real keyboard events (the frame
holds focus after the pager click), paginate=false shows the full
problem list with no counter, and a runtime filter configured by a
later bundled copy is honored by the window listeners the first copy
attached — proven non-vacuously by first catching an error through
copy A, then watching copy B's rejecting filter keep the overlay away.
The three jsdom equivalents are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: drop two overlay unit tests the e2e suite already covers

The accent-bar colors are asserted (both hues, including the
error-to-warning flip) by the shared-state e2e, and the default
pagination counter by the pagination e2e.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): cover pagination clamping and page retention, drop the jsdom describe

The pagination e2e now clamps at the last page under real clicks and
key presses, and a shared-state test drives a bundled copy through the
page-index semantics: a re-publish of the same problems keeps the page
the user navigated to, a different set starts back at page one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: drop two more overlay unit tests the e2e suite covers

Problem replacement is asserted by the page-retention e2e (a new set
swaps the rendered content), and the overlay API shape is exercised by
every shared-state test driving real bundled copies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: assert the dismiss hint where it is acted on

The Escape e2e now checks the card advertises the dismissal it is about
to perform; the jsdom hint test is removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): assert linkification on real error messages

The build-error e2e verifies webpack's own "See https://…" becomes a
safe new-tab link (href, target, rel), and the runtime-error e2e keeps
the sentence-ending dot out of the href. The two jsdom linkify tests
are removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: drop the hot unit tests the browser suites already prove

The building payload's file and compiler name are pinned by the e2e
console snapshots, sync-vs-built by the invalidate and multi-compiler
runs, custom publishes by the subscribe round-trip, and the event
stream's delivery, broadcast, and close behavior by the two-page and
instance.close() tests. The invalidate e2e now snapshots its console —
a file-less building line and a sync silent enough not to log. What
stays in hot.test.js is the transport level: headers, heartbeat frames,
the headersSent guard, duplicate-name pairing, and progress throttling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): snapshot the console wherever it is asserted

hot.statsOptions warnings-filtering runs in the browser — its snapshot
is a lone "connected", the warning never entering the payload, while
the overlay={"warnings":false} snapshot shows the warning reaching the
console and stopping there. Three hot unit tests fall away: the
late-connect sync and post-close handling are covered by the e2e and
framework suites, and statsOptions forwarding by the new run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): problem-type transitions, runtimeErrors=false, warning updates, and dynamicPublicPath

A warning overlay escalates to an error overlay when the build breaks,
an error overlay becomes a warning overlay on partial recovery (read
through the page realm so the reload fallback cannot detach it),
overlay={"runtimeErrors":false} leaves runtime errors uncaught, updates
carrying warnings apply without a reload, and the client connects and
updates through a dynamic public path served under /assets/ (the
helper gained a publicPath option). Six jsdom equivalents are removed —
client.test.js keeps only the slash-edge parsing and the protocol
guards nothing can reach end to end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): read the SSE protocol raw off a real server

A plain http reader against the running middleware covers what the
fake-response units simulated: the keep-alive handshake headers, real
heartbeat frames on real timers, and the catch-up sync reaching only
the newly connecting client while already-attached ones stay quiet.
The three fake-timer/fake-response equivalents leave hot.test.js; what
remains there has no end-to-end path (pure parsing, HTTP/2 headers,
forced pairing edges, progress throttling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): make the warning-dedup sequence independent of the catch-up race

The shared SSE connection writes its catch-up sync once — a sibling
bundle still evaluating when it arrives misses it, so a warning carried
by that sync reached the console on some runs and not others. The
widget now starts clean (an empty catch-up cannot vary the sequence)
and picks its warning up through a build the page observes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): adopt webpack-dev-server's 400s test cap for cold CI runners

A multi-step browser test spent its whole 120s budget on a cold
Windows runner without any single wait hanging — first build, rebuild,
and reload each run several times slower there. dev-server runs all
its tests under a flat --test-timeout=400000; the e2e suites now use
the same cap, while the per-wait 30s timeouts keep catching real hangs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): share the waits, fixtures, and teardown across the browser suites

One helper now owns the text/overlay waits (all on interval polling —
only one of the five copies had received the hidden-page fix), the app
fixtures (accepted, unaccepted, warning-carrying, boom), the overlay
and indicator element ids, and the browser/app teardown that every
describe had cloned. Two snapshots move a warning column, following
the shared fixture's indentation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): click the backdrop by raw mouse coordinates

ElementHandle.click's scrollIntoViewIfNeeded pre-check stalled CDP on a
slow Windows runner (Runtime.callFunctionOn timing out). The overlay
iframe is fixed at the viewport origin, so a raw mouse click at (5,5)
lands on the backdrop without any evaluate round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): open the clicked file reference through a real endpoint

The overlay's file chip carries webpack's real file:line:column
reference, and clicking it lands a GET on an express route the test
mounts through the helper's new setup hook — the full documented
open-in-editor round trip, not just a fetch spy. Without an endpoint
configured no chip is rendered. The jsdom describe is removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): feed the client real heartbeats and a malformed frame

With a 100ms server heartbeat, several real 💓 frames cross the wire
and reach neither subscribers (only the catch-up sync does) nor the
console — snapshotted as a lone "connected". A rogue SSE endpoint
mounted through the helper's setup hook feeds the real EventSource a
non-JSON frame: the client warns (snapshotted, V8's wording is pinned
by the puppeteer-locked Chrome) and the page keeps running. The two
jsdom equivalents leave; client.test.js is down to the slash-edge
parsing and the no-EventSource guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): finish the client migration — runtime public path and no-EventSource

The dynamic public path is now asserted on the URLs the browser
actually requests: a bundled setter swaps __webpack_public_path__ at
runtime (the free variable only exists inside the bundle) and manual
reconnects show intentional double slashes surviving and trailing
slashes not doubling. Deleting EventSource before the bundle loads
(evaluateOnNewDocument) snapshots the polyfill warning with the page
still rendering. The jsdom client suite is gone — nothing was left
that a real browser could not cover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): pin only the first three connects of the watchdog cycle

The silent watchdog keeps reconnecting by design, so a fourth
"connected" can land in the same poll window as the awaited third and
flake the full-list snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): process the update paths against the real HMR runtime

publish() turns out to be the injection point the fake runtimes
simulated: a built announcing a hash the server never produced walks
the real check → manifest 404 → reload fallback; a sync carrying the
bundle's own hash (read through a bundled getter — __webpack_hash__
only exists inside the bundle) locks the compilation name so a
sibling's impossible hash is ignored, snapshotted as pure silence; and
without HotModuleReplacementPlugin the disabled-runtime error logs
exactly once across two builds. The accept-handler error path runs on
a genuinely throwing accept callback: reload by default, a warning and
the broken state kept with reload=false. Six fake-runtime units leave;
what stays needs forced runtime states no real flow reaches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): click frames through DOM-domain coordinates

The close-button click stalled on a Windows runner with the same
Runtime.callFunctionOn timeout as the backdrop before it — main-world
evaluates go unresponsive after navigations there. clickInFrame resolves
the element through the isolated-world selector wait, takes its
clickable point from the DOM domain, and clicks with raw mouse input;
the in-card and close-button clicks use it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): the pre-lock catch-up race and the missing-update-chunk path

The sibling-sync race runs deterministically against the real runtime:
a slowed-down manifest route holds the sibling's check open while the
own sync locks the compilation name, and the failed check is discarded
in silence — snapshotted as a connect and a single "Checking". Trying
to force the runtime's failure status through a missing update chunk
surfaced the real behavior instead: check rejects with a
ChunkLoadError, no failure status is reached, and the client logs
"Update check failed" and stays put — now pinned as its own test. The
abort-status unit stays: no real flow reaches that state on demand.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: drop the last process-update unit

The failure-status branch it exercised is unreachable through the real
runtime: the client's ignoreUnaccepted/ignoreDeclined/ignoreErrored
options close every path webpack 5.109 has to the abort and fail
statuses. The branch itself stays as a cheap guard for upstream
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): the React-boundary heuristic and the runtime slot reset, for real

A fixture function literally named invokeGuardedCallbackDev throws, so
the genuine stack carries the marker React's dev build leaves on
boundary-handled errors — the overlay ignores it while a plain error
right after proves the listeners live. The runtime slot resets through
its real trigger: a clean rebuild's reporter clears the accumulated
errors, and the next one starts a fresh slot with no pager. The jsdom
runtime-errors describe is removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): assert the badge and file-path rendering on real errors

The ERROR/WARNING badges (text and background) and the highlighted file
path are checked where Chrome actually renders them — on webpack's own
parse error and critical-dependency warning. Three jsdom rendering
tests leave.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): retire the last jsdom suite

The final overlay rendering details run in Chrome: the code-frame
highlight on webpack's real parse error, the re-mount after something
wiped the iframe (re-triggered by an unchanged rebuild, exercising the
identical-set guard), clear() as a no-op with nothing shown, and the
query-configured custom card styles and ansi colors driving the badge
and border. With no jsdom consumer left, jest-environment-jsdom leaves
the dev dependencies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: collect server-side coverage from the e2e phase

Migrating server behavior to the browser suites moved its execution out
of the coverage run — the heartbeat write, the SSE handshake, and every
hot.js path only e2e exercises showed as uncovered. The e2e step now
instruments src/ too (the middleware runs inside the jest process even
under a browser), writes to its own directory, and the artifact carries
both lcov files for codecov to merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(e2e): let the watcher settle before frame-level assertions

macOS fired a spurious startup rebuild whose unchanged hash broadcast a
legitimate sync to every client — indistinguishable from a re-sent
catch-up at the frame level. The helper's settle() drains builds until
the watcher goes quiet, and the catch-up test runs after it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(client): use a Set for renewed module ids in logUpdates (#2375)

Replaces Array#includes inside the filter over updated modules
(O(n·m) → O(n+m)) — noticeable on updates touching hundreds of modules.

* perf(hot): avoid idle work on the SSE stream (#2376)

- skip payload serialization in publish() when no client is connected,
  and cut the ProgressPlugin callback early via eventStream.hasClients()
- start the heartbeat interval with the first client and stop it with
  the last one, instead of ticking forever
- pair bundles against the previous build through a name-grouped Map,
  replacing the per-bundle filter (O(n²) → O(n))

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* test: measure the hot client from the browser

The e2e suite already exercised client-src, but nothing counted it: only
jest-instrumented modules were measured, so the browser-only paths read 0%
and the number came from the jsdom tests alone.

The e2e bundle now instruments client-src as webpack builds it, and the
counters are read off each page before its browser closes. Two details
matter. Istanbul's default way of reaching the global is
new Function("return this"), which a page enforcing
require-trusted-types-for 'script' refuses to compile, so the scope is
named directly -- instrumenting must not change what the client can run
under. And a reload takes window.__coverage__ with it, which is exactly
what reloadPage and the unaccepted-update branches would have reported,
so the counters are parked in sessionStorage on pagehide and picked up
afterwards.

client-src goes from 94.58% to 96.71% of statements and 85.35% to 90.07%
of branches; theme.js, utils/get-hot.js and utils/reload.js were each
reported at 0% and are now fully covered. Two e2e cases cover what was
left reachable: the overlay query override, an empty pair in the client's
own query string, and the non-JSON overlay value that falls back to on.

* test: retire the jsdom client suites, run the browser ones on Linux only

The four jsdom suites duplicated what test/e2e now drives through a real
browser, and since the client is measured from the browser they were no
longer carrying coverage either. Removing them takes jest-environment-jsdom
with them; the remaining jest suites are node-only.

client-src is at 97.82% of statements from the browser suite alone. Four
cases cover what the deletions left reachable: a second connect for an
already-subscribed path, a host-supplied custom overlay, an update to a
declined module, and an error event carrying neither an error nor a
message. The environment guards a browser cannot produce -- a
document-less import, a script running before <body>, a frame without a
document -- are marked rather than faked, since contriving them is how
these suites would turn flaky.

The browser suite says the same thing on every OS and node version while
costing the slowest job in the matrix, so it runs on ubuntu/22.x alone.
All four shards still run there, so the suite is covered exactly once.

* test: address the review findings on the e2e suite

Two of my own tests were green for the wrong reason. The custom-overlay
one stubbed show/clear, but the client calls showProblems/clear, so only
clear ever ran and a length check passed on it; it now asserts
showProblems with the errors. The declined-update one asserted a log
count while the page was in fact reloading underneath it -- a decline
cannot be applied, so the client falls back to a full reload, which the
test now waits for and asserts. The bare-error-event test checked the DOM
only after triggering a real error, which would have reused a wrongly
created overlay.

The runtime-error tests raced the client: attachRuntimeErrorListeners
runs during the client's own setup, while the tests only waited for the
app to render, so an error thrown first landed nowhere and the overlay
never appeared. Two different tests failed on two consecutive runs from
this. They now wait for the listeners through the state the client
already exposes -- including after a reload, and excluding the test whose
configuration deliberately attaches none.

Also: the merge left an unreachable env.test block in babel.config.js
behind the early return, naming a plugin the same merge removed; a failed
coverage read could leave Chromium running; the SSE reader dropped frames
split across TCP chunks; snapshots pinned V8 and puppeteer wording that a
toolchain bump would change; a multi-compiler comment claimed a re-log
that the dedup cache makes impossible; and upload-coverage skipped
entirely when a shard failed.

* ci: run the browser suite in one job instead of across the matrix

The browser suite was running in all 48 matrix jobs, and the 4-way shard
axis this branch added existed to absorb that cost. It drives a real
Chromium, so every OS and node version reports the same thing.

It moves to a single ubuntu job. The shard axis goes with it, which puts
the test matrix back to what main has: 3 OS x 4 node versions, 12 jobs
running the node suite unsharded. Together with lint, the client job and
the coverage upload, a pull request goes from 48 test jobs to 12 plus
one.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: alexander-akait <sheo13666q@gmail.com>
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