Skip to content

feat: add hot option for hot module replacement#2322

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

feat: add hot option for hot module replacement#2322
bjohansebas wants to merge 7 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: 3dd93ad

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.68508% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.31%. Comparing base (1a24bbd) to head (3dd93ad).

Files with missing lines Patch % Lines
src/index.js 89.13% 5 Missing ⚠️
src/hot.js 99.23% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2322      +/-   ##
==========================================
+ Coverage   92.60%   93.31%   +0.70%     
==========================================
  Files           3        4       +1     
  Lines        1001     1182     +181     
  Branches      311      354      +43     
==========================================
+ Hits          927     1103     +176     
- Misses         66       71       +5     
  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.

* 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
@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 on lines +77 to +97
const interval = setInterval(() => {
everyClient((client) => {
client.write("data: 💓\n\n");
});
}, heartbeat);

// Don't block process exit on the heartbeat timer.
if (typeof interval.unref === "function") {
interval.unref();
}

return {
close() {
clearInterval(interval);
everyClient((client) => {
if (!client.writableEnded) {
client.end();
}
});
clients = new Map();
},
Comment thread src/hot.js
Comment on lines +274 to +276
const path = options.path || HOT_DEFAULT_PATH;
const heartbeat = options.heartbeat || HOT_DEFAULT_HEARTBEAT;
const { statsOptions } = options;
Comment thread src/hot.js
Comment on lines +218 to +225
const resultStatsOptions = {
all: false,
hash: true,
timings: true,
errors: true,
warnings: true,
...(statsOptions && typeof statsOptions === "object" ? statsOptions : {}),
};
Comment thread babel.config.js
Comment on lines 10 to 13
targets: {
node: "20.9.0",
esmodules: true,
node: "0.12",
},
"webpack-dev-middleware": minor
---

Added a `hot` option that enables hot module replacement, replacing the need for `webpack-hot-middleware`. Pass `hot: true` to enable with defaults, or `hot: { path, heartbeat, log, statsOptions }` to customize. The client runtime is served by the middleware itself.
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