feat: add hot option for hot module replacement - #2322
Conversation
🦋 Changeset detectedLatest commit: c4798c1 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.73% 94.19% +1.45%
==========================================
Files 3 4 +1
Lines 1005 1240 +235
Branches 312 380 +68
==========================================
+ Hits 932 1168 +236
+ Misses 65 64 -1
Partials 8 8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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.
3dd93ad to
52f4d31
Compare
a6de3aa to
593f91f
Compare
…ource (#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
…and remove cspell.config.json
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.
…tener 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.
…ate-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.
…shes 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.
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).
…igration 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.
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.
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.
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.
… 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`.
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).
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.
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.
11f24ab to
e1a5e29
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (9)
🚧 Files skipped from review as they are similar to previous changes (7)
Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review. WalkthroughThis change adds configurable SSE-based hot module replacement to the middleware. It adds a browser client with update handling, shared error overlays, progress indicators, subscriptions, and reconnection support. It publishes client build output and TypeScript declarations. It adds validation, server and browser tests, ES5 checks, examples for standard, multi-compiler, and shared-overlay HMR, plus migration documentation and repository configuration updates. Merge Risk: 🔵 Low · up to The new opt-in HMR endpoint can expose build filenames and diagnostics to cross-origin clients and retain unbounded persistent connections if reachable without upstream access controls; additionally, configured endpoints containing query or fragment components may not route correctly. The PR is mergeable with explicit owner awareness and follow-up on endpoint access limits and path handling. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 93.83% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 81 functions across 47 files. (3 skipped: 3 unsupported.) ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Some tools did not complete. Review the errors below. 🔧 ESLint
ESLint install timed out. The project may have too many dependencies for the sandbox. 🔧 ast-grep (0.45.2)test/middleware.test.jsast-grep timed out on this file Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Team
Run ID: a1330527-c6f4-47eb-98a2-86f26b3daa44
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (62)
.changeset/hot-middleware-migration.md.cspell.json.gitignoreREADME.mdbabel.config.jsclient-src/globals.d.tsclient-src/index.jsclient-src/indicator.jsclient-src/overlay.jsclient-src/process-update.jsclient-src/theme.jsclient-src/utils/get-hot.jsclient-src/utils/log.jsclient-src/utils/reload.jsclient-src/utils/strip-ansi.jseslint.config.mjsexamples/hot-multi-compiler/README.mdexamples/hot-multi-compiler/public/index.htmlexamples/hot-multi-compiler/server.jsexamples/hot-multi-compiler/src/admin-render.jsexamples/hot-multi-compiler/src/admin.jsexamples/hot-multi-compiler/src/index.jsexamples/hot-multi-compiler/src/render.jsexamples/hot-multi-compiler/webpack.config.jsexamples/hot-shared-overlay/README.mdexamples/hot-shared-overlay/public/index.htmlexamples/hot-shared-overlay/server.jsexamples/hot-shared-overlay/src/app.jsexamples/hot-shared-overlay/src/render.jsexamples/hot-shared-overlay/src/widget.jsexamples/hot-shared-overlay/webpack.config.jsexamples/hot/README.mdexamples/hot/public/index.htmlexamples/hot/server.jsexamples/hot/src/index.jsexamples/hot/src/render.jsexamples/hot/webpack.config.jslint-staged.config.jspackage.jsonsrc/hot.jssrc/index.jssrc/middleware.jssrc/options.jsontest/__snapshots__/client.test.js.snap.webpack5test/__snapshots__/validation-options.test.js.snap.webpack5test/client.test.jstest/es5.test.jstest/fixtures/webpack.array.one-error-one-warning-one-no.jstest/fixtures/webpack.array.one-error-one-warning-one-object.jstest/fixtures/webpack.array.one-error-one-warning-one-success.jstest/fixtures/webpack.array.warning.config.jstest/helpers/sse.jstest/hot.test.jstest/indicator.test.jstest/middleware.test.jstest/overlay.test.jstest/process-update.test.jstest/strip-ansi.test.jstest/validation-options.test.jstsconfig.client.jsontypes/hot.d.tstypes/index.d.ts
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.
| */ | ||
| function createHot(compiler, userOptions) { | ||
| const options = userOptions === true ? {} : userOptions; | ||
| const path = options.path || HOT_DEFAULT_PATH; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject query and fragment components in hot.path.
createHot preserves values such as "/__webpack_hmr?client=1". pathMatch parses the request URL and compares only its pathname with this full string. No request can match that configured endpoint, so the client receives a normal middleware response instead of SSE. Normalize this option to a pathname or reject ? and #.
- 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.
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
Summary by CodeRabbit
hotoption.