Skip to content

feat(hot): add apply and connect, replacing six browser options - #2458

Open
alexander-akait wants to merge 6 commits into
mainfrom
feat/hot-option-unions
Open

alexander-akait wants to merge 6 commits into
mainfrom
feat/hot-option-unions

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Two unions on the browser options, six names down to two. Not a breaking change — every old name keeps working and is folded into the one that replaced it, with removal left for a major.

Retargeted to main now that #2457 has merged.

apply replaces hot, liveReload and reload

Not a taste call — the control flow says they were one decision written three ways. liveReload is read only in the else branch of if (hot), and reload only inside applyUpdate, which the if branch calls. Of eight boolean combinations only four were reachable:

before now
hot: true, reload: true apply: "hmr"
hot: true, reload: false apply: "hmr-only"
hot: false, liveReload: true apply: "reload"
hot: false, liveReload: false apply: "nothing"

The page-url parameter gains from it

It could previously only turn something off, so -hot=false and -liveReload=false were two switches with a gap between them — a page could not ask for an outcome. Now it can:

?webpack-dev-middleware-apply=nothing
?webpack-dev-middleware-apply=reload
?webpack-dev-middleware-apply=false     // still taken as `nothing`

=false is kept because that is what the old pair meant together, so the habit survives.

connect replaces autoConnect, reconnect and timeout

middleware(compiler, {
  hot: { client: { connect: { retries: 3, timeout: 5000 } } },
});

false does not connect on load — setOptionsAndConnect() still does. The per-transport behaviour is unchanged and still lives in socket-options.js, so retries and timeout mean exactly what #2457 made them mean.

Compatibility

All six old names apply as before and are folded into the new ones. The new option wins when both are set: the other way round, a migration that sets it and leaves the old name behind would silently not apply.

Each old name warns in two places, which is forced rather than belt-and-braces: in node when it is set on hot.client, and in the browser when it arrives on the query. An entry somebody wired by hand never passes through the node side, so without the browser warning it would deprecate silently.

For the same reason the folding happens in the browser rather than in clientQuery — node cannot translate a query it never sees.

Why this shape

It is what the codebase already does everywhere: overlay, token, cors, progress and transport are each a scalar that may be an object. Neither of these reads as an exception.

Notes for review

  • The symmetry test needed teaching. The deprecated names are read from a list rather than written out one by one, so overrides.hot is not in the source for its regex to find. It collects them from LEGACY_OPTIONS now, with an assertion on that list's length so a regex that stops matching cannot quietly shrink the comparison.
  • It also could not see a type containing an object literal: /@property \{[^}]+\}/ stops at the brace inside { retries?: number }, so connect looked absent from the typedef. It matches one level of nesting now.
  • One e2e fixture had to be rethought rather than remapped. A live-reload case combined a recognised hot=false with an unrecognised live-reload=false; with one option that scenario cannot exist, so it now asserts the property it was really about — an unrecognised value leaves the default in force.
  • The rebase onto main cost more than it looked like it did. I resolved all eight conflicting hunks to the legacy side, which was right for the lines in conflict and wrong for the new lines that happened to sit in the same hunks: the README rows for apply and connect, the reworded urlPrefix row, and three tests covering the shapes connect takes. CodeRabbit caught two of the three; the tests were only found by auditing the branch against its pre-rebase commit. All restored in fc5195e, and the client options table now reads live options first with the six deprecated names together at the end.
  • The changeset entries from earlier PRs needed renaming too (1f37f40). They all land in one release, so the changelog is read by someone who only ever sees the final names — one entry even advertised a ?webpack-dev-middleware-liveReload=false parameter that no longer exists under any name.
  • And one I got wrong first: I mapped another fixture's -hot=false to -apply=nothing, but -hot=false left live reload on, so the build still reached the page, as a reload. nothing is the one mode where it does not arrive at all, and the test sat waiting for text that would never change. Mechanically renaming a query string is not safe when the options being replaced encoded a decision rather than a value.

Verified

On e92977b:

  • npm run lint — clean (eslint, prettier, cspell, tsc, client types, schema-check)
  • unit — 7151 passed, 22 suites
  • e2e — 175 passed, 14 suites
  • CI — 20 success, 1 skipped (dependabot-auto-merge): the full Test matrix on ubuntu/macos/windows × Node 20/22/24/25, Lint, Client, CodeQL, Analyze, dependency-review, Socket

Five browser tests cover the folding itself: each legacy combination, the precedence rule, and both warning channels.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

Summary by CodeRabbit

  • New Features

    • Added apply modes to control page behavior: apply updates with reload fallback, apply without fallback, reload on changes, or do nothing.
    • Added a connect option to disable connections or configure retries and timeouts.
    • Page URL parameters can override the update behavior. Server-published reload events continue to reload pages.
  • Bug Fixes

    • Object-valued client options are now passed to the browser client correctly instead of being lost.
  • Compatibility

    • Previous client options remain supported with deprecation warnings; replacement options take precedence when both are set. The old options are planned for removal in the next major release.
  • Documentation

    • Updated documentation and types to describe the new options and behaviors.

Generated by Claude Code

@changeset-bot

changeset-bot Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e92977b

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

Base automatically changed from fix/reconnect-and-timeout-per-transport to main October 3, 2026 10:07
alexander-akait and others added 2 commits October 3, 2026 10:09
Two unions, both of options that shipped after 8.3.0 — so nothing released
has to keep working and no aliases are needed.

`apply` replaces `hot`, `liveReload` and `reload`. Only four of their eight
combinations ever differed, which the control flow says plainly:
`liveReload` is read only in the `else` branch of `if (hot)`, and `reload`
only inside `applyUpdate`, which the `if` branch calls. Three booleans for
one decision.

    hot: true,  reload: true      -> apply: "hmr"
    hot: true,  reload: false     -> apply: "hmr-only"
    hot: false, liveReload: true  -> apply: "reload"
    hot: false, liveReload: false -> apply: "nothing"

`connect` replaces `autoConnect`, `reconnect` and `timeout`: `false` does
not connect on load, and an object carries `retries` and `timeout`. The
per-transport semantics are unchanged, still in `socket-options.js`, which
is why `connect` could land without re-deriving them.

The page-url parameter follows `apply` and gains from it: it used to be
able only to turn something off, so `-hot=false` and `-liveReload=false`
were two switches with a gap between them. `-apply=<mode>` asks for an
outcome, and `=false` is still taken as `nothing` because that is what the
old pair meant together.

Both are the shape this codebase already uses everywhere — `overlay`,
`token`, `cors`, `progress` and `transport` are all a scalar that may be an
object — so neither reads as an exception.

Two things found on the way. The symmetry test could not see a type with an
object literal in it: `[^}]+` stopped at the brace inside
`{ retries?: number }`, so `connect` looked absent from the typedef. And
one live-reload case had combined a recognised `hot=false` with an
unrecognised `live-reload=false`; with one option that scenario cannot
exist, so it now asserts what it was really about — an unrecognised value
leaves the default in force.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
`-hot=false` turned Hot Module Replacement off and left live reload on, so
the build reached the page as a reload. I had mapped it to
`-apply=nothing`, which is the one mode where the build reaches the page
not at all — so the test waited thirty seconds for text that was never
going to change. `reload` is the mode that behaviour had a name for.

The equivalent fixture in `live-reload.test.js` was mapped correctly, which
is what made the difference visible.

Also drops the snapshot left behind by a renamed test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Walkthrough

The client now uses apply modes for page updates and connect settings for connection behavior. The six former option names remain supported with deprecation warnings, and the replacement option takes precedence when both forms are set. The schema, type declarations, documentation, and tests now describe and cover the new options, URL overrides, transport settings, and legacy compatibility.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to e9297

Readers may try a page-URL connection setting that has no effect. Clarify that only apply has a page-URL override; the remaining issue is bounded and does not prevent merging.

Security Architecture Review

Security architecture risk: 🔵 Low · up to e9297

The changes remain concentrated in development-client configuration and update behavior. Page URLs gain the ability to select an update strategy, but cannot use this mechanism to select an endpoint, transport, or credential. No concrete security exploit was established; deployment exposure remains uncertain.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — A party able to influence a visited page's query string can influence that client's update or reload strategy on successful build events. This path does not itself choose arbitrary executable content, redirect the connection, or grant server authority; it selects behavior over the client's configured build stream. Effective deployment and user exposure are not established.

Trust Boundaries and Controls

  • observed — Page URL mode selection uses parsed parameter names, a configured namespace, and an allowlist of modes; invalid modes preserve configured behavior. The namespace is input scoping, not authentication. Endpoint and credential configuration enter through the client override path and are used separately when creating the socket.

Resilience and Maintainability Implications

  • observed — Connection initiation remains distinct from automatic startup: manual connection explicitly invokes connect, repeated subscription to the same path is suppressed, and disconnect closes and removes the cached wrapper. Socket close marks the instance terminal and clears its retry timer before closing the transport, preventing queued close events from scheduling recovery after explicit shutdown.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 14 files. (5 skipped: 5…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding apply and connect to replace six browser options.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Update the entry-query example and the setOptionsAndConnect comment to the new… · README.md:795

README.md:795
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update the entry-query example and the setOptionsAndConnect comment to the new option names.

The example at Line 795 uses reload=false. The comment at Line 925 says "when autoConnect=false". This PR removes both options. setOverrides reads only apply and connect, so the client ignores reload=false. As a result, a user who copies the example keeps the default "hmr" mode with reload fallback.

Proposed fix
-  "webpack-dev-middleware/client?reload=false&overlay=false",
+  "webpack-dev-middleware/client?apply=hmr-only&overlay=false",
-// Connect manually when `autoConnect=false`. Accepts the same option keys as
+// Connect manually when `connect=false`. Accepts the same option keys as

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ec9d3156-784a-4916-b1db-0359ac5bc391
📥 Commits

Reviewing files that changed from the base of the PR and between 792bb61 and 8abbfa0.

📒 Files selected for processing (18)
  • .changeset/hot-client-apply-and-connect.md
  • .changeset/reconnect-and-timeout-per-transport.md
  • README.md
  • client-src/index.js
  • client-src/utils/socket-options.js
  • src/hot.js
  • src/options.check.js
  • src/options.json
  • test/client-socket-options.test.js
  • test/e2e/__snapshots__/process-update.test.js.snap.webpack5
  • test/e2e/client.test.js
  • test/e2e/inject.test.js
  • test/e2e/live-reload.test.js
  • test/e2e/process-update.test.js
  • test/inject-client.test.js
  • types/client/index.d.ts
  • types/client/utils/socket-options.d.ts
  • types/hot.d.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

Comment thread .changeset/reconnect-and-timeout-per-transport.md Outdated
Not a breaking change any more: `hot`, `liveReload`, `reload`,
`autoConnect`, `reconnect` and `timeout` all still apply, and are folded
into the two options that replaced them.

    hot: true,  reload: true      -> apply: "hmr"
    hot: true,  reload: false     -> apply: "hmr-only"
    hot: false, liveReload: true  -> apply: "reload"
    hot: false, liveReload: false -> apply: "nothing"
    autoConnect: false            -> connect: false
    reconnect, timeout            -> connect: { retries, timeout }

The new option wins when both are set: the other way round, a migration
that sets it and leaves the old name behind would silently not apply. Each
old name warns, and it has to warn in two places — node, when it is set on
`hot.client`, and the browser, when it arrives on the query, since an entry
somebody wired by hand never passes through the node side at all.

The folding happens in the browser rather than in `clientQuery` for the
same reason: a hand-written query has to work, and node cannot translate
one it never sees.

The symmetry test needed to learn about this. The deprecated names are read
from a list rather than written out one by one, so `overrides.hot` does not
appear in the source for it to find; it now collects them from
`LEGACY_OPTIONS`, with an assertion on the length of that list so a
regex that stops matching cannot quietly shrink the comparison.

Five browser tests cover the folding, including the precedence rule and
both warning channels.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
@alexander-akait alexander-akait changed the title feat(hot)!: replace six browser options with apply and connect feat(hot): add apply and connect, replacing six browser options Oct 3, 2026
@codecov

codecov Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.49123% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.36%. Comparing base (792bb61) to head (e92977b).

Files with missing lines Patch % Lines
client-src/index.js 94.87% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2458      +/-   ##
==========================================
+ Coverage   96.28%   96.36%   +0.08%     
==========================================
  Files          23       23              
  Lines        2449     2476      +27     
==========================================
+ Hits         2358     2386      +28     
+ Misses         91       90       -1     

☔ 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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟠 Major · Serialize hot.client.connect objects as JSON, or the object form is lost. · utils.js:1489-1491

src/utils.js:1489-1491
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Serialize hot.client.connect objects as JSON, or the object form is lost.

clientQuery runs String(value) on every key except overlay.

  1. The schema accepts hot.client.connect: { retries: 3, timeout: 5000 }.
  2. clientQuery turns that object into the query value "[object Object]".
  3. In client-src/index.js, setOverrides calls JSON.parse on that value. The parse throws.
  4. The fallback sets options.connect = "[object Object]" !== "false", which is true.

As a result, retries and timeout set from Node are dropped without a warning. The e2e tests only pass connect through a hand-written entry query, so they do not cover this path.

🐛 Proposed fix
-    if (key !== "overlay") {
-      query[key] = String(value);
-      continue;
-    }
+    if (key === "connect" && typeof value === "object" && value !== null) {
+      query.connect = JSON.stringify(value);
+      continue;
+    }
+
+    if (key !== "overlay") {
+      query[key] = String(value);
+      continue;
+    }

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9d239294-69bc-478b-a744-2d0beb9a1f25
📥 Commits

Reviewing files that changed from the base of the PR and between 8abbfa0 and 8dd2841.

📒 Files selected for processing (14)
  • .changeset/hot-client-apply-and-connect.md
  • README.md
  • client-src/index.js
  • client-src/utils/socket-options.js
  • src/hot.js
  • src/options.check.js
  • src/options.json
  • src/utils.js
  • test/client-socket-options.test.js
  • test/e2e/client.test.js
  • test/e2e/live-reload.test.js
  • test/inject-client.test.js
  • types/client/utils/socket-options.d.ts
  • types/hot.d.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/hot-client-apply-and-connect.md

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment thread README.md Outdated
alexander-akait and others added 3 commits October 3, 2026 10:57
Both are listed as the replacement the six deprecated rows point to, but the
table had no row for either, so following those links left you without their
values, default or object shape. The `urlPrefix` row still described the
parameter as turning `hot` and `liveReload` off, which is the shape it had
before `apply` replaced them.

Rebasing onto main resolved every conflicting hunk to the legacy side, which
also dropped content that happened to share those hunks: these rows, the
`urlPrefix` wording, and three tests covering the shapes `connect` takes.
Restored, and the table now reads live options first with the six deprecated
names together at the end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
All of these land in one release, so the changelog is read by someone who only
ever sees the final names. Three entries described options and page-url
parameters as they were partway through: the retry count and silence timeout
under their replaced names, a `-liveReload=false` parameter that no longer
exists under any name, and a claim that every option has exactly one spelling,
which the six kept for a release contradict. The replaced names are noted where
they were the subject, since they still work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
`clientQuery` special-cased `overlay` and ran `String(value)` over everything
else, which was true of every other option until `connect` took an object too.
`{ retries: 3, timeout: 5000 }` became the text `"[object Object]"`; the client
parses `connect` as JSON, the parse threw, and the fallback read the text as a
boolean — so both fields were dropped in silence and the connection ran on the
defaults.

The rule is now the value's shape rather than the option's name: an object goes
over as JSON, `overlay` keeping the encoding its filter functions need. The e2e
tests set `connect` through a hand-written entry query, which skips this path
entirely, so the new one sets it on the middleware and watches for the
shortened watchdog interval actually arriving — it fails on `String(value)`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

Copy link
Copy Markdown
Member Author

Both out-of-diff findings handled.

src/utils.js — connect objects lost to String(value) (🟠 Major)

Real, and the diagnosis was exact. Confirmed before fixing:

clientQuery({ connect: { retries: 3, timeout: 5000 } })
→ { connect: '[object Object]' }

Fixed in e92977b, but keyed on the value's shape rather than the option's name, so the next option to grow an object form needs no change here:

query[key] = isObject ? JSON.stringify(value) : String(value);

overlay keeps its own branch, since its filter functions need encoding before they are serialized.

The observation that no test covered this path was the more useful half of the finding, so there are now two:

  • a unit test on clientQuery directly, and
  • an e2e that sets connect: { timeout: 1000 } on the middleware rather than in a hand-written query, with the heartbeat pushed out to an hour so only the watchdog can reconnect, and waits for three connects. On String(value) the client silently runs the 20000ms default and the third connect never comes.

I checked that second one both ways — reverting the one-line fix fails it at waitForCount, restoring it passes.

README.md — the entry-query example and the setOptionsAndConnect comment

Both already corrected in fc5195e, which landed before this review: ?apply=hmr-only&overlay=false and // Connect manually when \connect=false``, the same two edits proposed.

Verified on e92977b

  • npm run lint — clean
  • unit — 7151 passed, 22 suites
  • e2e — 174 passed on the previous commit; rerunning on this one

Generated by Claude Code

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bcba2f18-e7dc-45df-a2f3-5449ed6507d6
📥 Commits

Reviewing files that changed from the base of the PR and between 8dd2841 and e92977b.

📒 Files selected for processing (9)
  • .changeset/client-live-reload.md
  • .changeset/client-options-both-ways.md
  • .changeset/connect-object-from-node.md
  • .changeset/reconnect-and-timeout-per-transport.md
  • README.md
  • src/utils.js
  • test/client-socket-options.test.js
  • test/e2e/client.test.js
  • test/inject-client.test.js

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment thread README.md
Comment on lines +810 to +811
entry's query as `<name>=<value>`, with the same effect, in both places and in
the page-url parameters below. The last six are the names `apply` and `connect`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

State that the page-URL override applies only to apply. Both passages imply that every client option can be set through a page-URL parameter. The client reads a page-URL override for apply; ?webpack-dev-middleware-connect=false, for example, will not disable the connection. (raw.githubusercontent.com)

  • README.md#L810-L811: limit “the same effect” to hot.client and the entry query, then describe the apply page-URL override separately.
  • .changeset/client-options-both-ways.md#L39-L41: make the same distinction between entry-query options and the apply page-URL parameter.
📍 Affects 2 files
  • README.md#L810-L811 (this comment)
  • .changeset/client-options-both-ways.md#L39-L41

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.

1 participant