feat: add hot option for hot module replacement#2322
Conversation
🦋 Changeset detectedLatest commit: 3dd93ad The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
* 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
7a77601 to
18d9f51
Compare
There was a problem hiding this comment.
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
hotoption (with schema + TypeScript types). - Add a browser client runtime (overlay + update application) under
client-src/, built toclient/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.
| 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(); | ||
| }, |
| const path = options.path || HOT_DEFAULT_PATH; | ||
| const heartbeat = options.heartbeat || HOT_DEFAULT_HEARTBEAT; | ||
| const { statsOptions } = options; |
| const resultStatsOptions = { | ||
| all: false, | ||
| hash: true, | ||
| timings: true, | ||
| errors: true, | ||
| warnings: true, | ||
| ...(statsOptions && typeof statsOptions === "object" ? statsOptions : {}), | ||
| }; |
| 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. |
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 atwebpack-dev-middleware(compiler, { hot: true })with no separateapp.use().PR — Browser client runtime (
client-runtime)Ports the browser client into
client-src/, mirroring the layoutwebpack-dev-serveruses (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