Skip to content

feat: add hot option for hot module replacement#2322

Open
bjohansebas wants to merge 13 commits into
mainfrom
hot-middleware
Open

feat: add hot option for hot module replacement#2322
bjohansebas wants to merge 13 commits into
mainfrom
hot-middleware

Conversation

@bjohansebas

@bjohansebas bjohansebas commented May 16, 2026

Copy link
Copy Markdown
Member

Summary

PR — Server-side HMR core (serve-logic)

Integrates the SSE endpoint into the existing middleware behind a new hot: true | { path, heartbeat, log, statsOptions } option. Available at webpack-dev-middleware(compiler, { hot: true }) with no separate app.use().

PR — Browser client runtime (client-runtime)

Ports the browser client into client-src/, mirroring the layout webpack-dev-server uses (client-src/client/).

PR — End-to-end integration tests

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

@changeset-bot

changeset-bot Bot commented May 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 593f91f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack-dev-middleware Minor

Not sure what this means? Click here to learn what changesets are.

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

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.61017% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.29%. Comparing base (af30757) to head (593f91f).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/index.js 89.13% 5 Missing ⚠️
src/hot.js 99.21% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2322      +/-   ##
==========================================
+ Coverage   92.70%   93.29%   +0.59%     
==========================================
  Files           3        4       +1     
  Lines        1001     1179     +178     
  Branches      311      354      +43     
==========================================
+ Hits          928     1100     +172     
- Misses         65       71       +6     
  Partials        8        8              

☔ View full report in Codecov by Harness.
📢 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.

@bjohansebas
bjohansebas marked this pull request as ready for review June 16, 2026 22:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class Hot Module Replacement support to webpack-dev-middleware by introducing a hot option that serves an SSE endpoint and shipping a bundled browser client runtime (webpack-dev-middleware/client) to consume it.

Changes:

  • Add server-side HMR SSE endpoint and wire it into the middleware via a new hot option (with schema + TypeScript types).
  • Add a browser client runtime (overlay + update application) under client-src/, built to client/ and exported as ./client.
  • Add unit + integration tests (SSE helpers, overlay/client tests, middleware E2E), plus a runnable example.

Reviewed changes

Copilot reviewed 32 out of 37 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
types/index.d.ts Exposes hot option + context.hot typings.
types/hot.d.ts Declares public typings for the hot/SSE helper module.
tsconfig.client.json Adds type-checking config for client-src/.
test/validation-options.test.js Adds validation cases for hot options.
test/overlay.test.js Adds jsdom tests for the error overlay UI.
test/middleware.test.js Adds end-to-end tests for SSE endpoint behavior.
test/hot.test.js Adds unit tests for hot server primitives and payload logic.
test/helpers/sse.js Adds helper utilities for SSE handshake/event collection in tests.
test/fixtures/webpack.array.warning.config.js Fixes output path for a fixture used by tests.
test/client.test.js Adds jsdom tests for the browser HMR client runtime.
test/snapshots/validation-options.test.js.snap.webpack5 Updates snapshots for new hot option validation errors.
test/snapshots/client.test.js.snap.webpack5 Adds snapshots for client logging behavior.
src/options.json Adds schema definition for hot option.
src/middleware.js Intercepts SSE requests and routes them to the hot handler.
src/index.js Creates/closes hot instance and adds Hono-specific SSE shims.
src/hot.js Implements SSE event stream + compiler hook publishing logic.
README.md Documents hot server option and the bundled client runtime + options.
package.json Adds ./client export, client build script, new deps, and tooling tweaks.
lint-staged.config.js Updates cspell config usage and eslint invocation flags.
examples/hot/webpack.config.js Adds minimal webpack config demonstrating client entry + HMR plugin.
examples/hot/src/render.js Adds demo module to show HMR updates.
examples/hot/src/index.js Adds demo entry accepting HMR updates.
examples/hot/server.js Adds Express example server mounting middleware with hot: true.
examples/hot/README.md Documents how to run the HMR example.
examples/hot/public/index.html Adds example HTML page loading the in-memory bundle.
eslint.config.mjs Adds ignores for built/example dirs and config for client-src.
cspell.config.json Adds centralized cspell config and ignore paths.
client-src/utils/log.js Adds client-side logger wrapper using webpack runtime logging.
client-src/process-update.js Adds client-side HMR update application logic.
client-src/overlay.js Adds client-side error/warning overlay implementation.
client-src/index.js Adds client runtime entry (EventSource, parsing, overlay, subscriptions).
client-src/globals.d.ts Adds local typing shims for client-src TS checking.
babel.config.js Updates Babel preset-env targeting and test env config.
.gitignore Ignores generated client/ output.
.github/workflows/nodejs.yml Enables workflow triggers on hot-middleware branch.
.changeset/hot-middleware-migration.md Adds a changeset entry describing the new feature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/hot.js
Comment thread src/hot.js
Comment thread src/hot.js
Comment thread babel.config.js
Comment thread .changeset/hot-middleware-migration.md
* 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!
* 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
Copilot AI review requested due to automatic review settings July 14, 2026 02:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…dency (#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
Copilot AI review requested due to automatic review settings July 20, 2026 17:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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
Copilot AI review requested due to automatic review settings July 20, 2026 17:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

* 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!
Copilot AI review requested due to automatic review settings July 20, 2026 20:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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
Copilot AI review requested due to automatic review settings July 20, 2026 20:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…er (#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>
Copilot AI review requested due to automatic review settings July 20, 2026 22:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 21, 2026 01:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.
Copilot AI review requested due to automatic review settings July 21, 2026 02:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

This was referenced Jul 21, 2026
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