Skip to content

feat(client): post build events to the page, and guard reloads - #2425

Merged
alexander-akait merged 2 commits into
mainfrom
fix/client-gaps-vs-dev-server
Sep 26, 2026
Merged

alexander-akait merged 2 commits into
mainfrom
fix/client-gaps-vs-dev-server

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Summary

Before webpack-dev-server can delete its own client and re-export this one, everything that client does has to exist here. I compared the ~1140 lines of it that nothing had compared yet — client-src/index.js, progress.js and utils/ — and this PR closes three of the four gaps that turned up. (The fourth, progress: "linear" | "circular", is a separate PR since it is an option change rather than a fix.)

1. Nothing was posted to the page at all

webpack-dev-server's client posts every build event out through self.postMessage — its own comment says "so plugins can consume it". This client posted nothing. Anything listening for webpackOk and its siblings would have gone quiet the moment dev-server switched, with no error and nothing in a diff to notice, because none of it is documented.

All eight go out now, under the same names and payloads: webpackInvalid, webpackProgress, webpackOk, webpackStillOk, webpackWarnings, webpackErrors, webpackClose, and the bare webpackHotUpdate<hash> string — that last one is posted as a plain string rather than in the { type, data } shape, which is how it has always been sent.

Two mappings worth stating, since the two protocols are not identical:

  • built (a compilation that produced something) → webpackOk; sync (one with nothing to report) → webpackStillOk. Same distinction dev-server draws between ok and still-ok.
  • webpackClose announces an outage, once — not once per reconnection attempt. dev-server reports it behind a retries === 0 guard, and Server-Sent Events here retry for as long as the page is open, so counting attempts instead would have meant announcing a dead server every few seconds forever. Teeth-checked: dropping the guard gives three webpackClose messages where there should be one.

2. A reload could fire at the worst two moments

While the page is already navigating away — an update landing mid-navigation reloaded a page the browser was leaving. beforeunload now holds reloads off, released again after a grace period (a beforeunload can be cancelled, by another listener or by the user answering "stay"), with pagehide/pageshow settling the case it gets wrong: a page restored from the back/forward cache runs the same script again, and a flag left set by the navigation away would have blocked every later update.

And inside an about:blank iframe — srcdoc, or a document written into the frame — where reloading reloaded the blank document and lost the app. The nearest ancestor that has a url of its own is reloaded instead. Unlike dev-server's version this catches the cross-origin case rather than throwing on it, and reloads this frame as the most that is permitted.

3. A runtime error filter could not see what a rejection carried

Promise.reject({ status: 503 }) was wrapped in an Error and the object dropped, so a overlay.runtimeErrors filter deciding on a status code had nowhere to read it from. Kept as cause now, which is what dev-server does.

What kind of change does this PR introduce?

feature (the posted messages), plus two fixes

Did you add tests for your changes?

Yes, four end-to-end cases in a new test/e2e/messages.test.js: the order and names across a rebuild, the payload webpackErrors carries, webpackClose arriving exactly once across an outage, and a filter judging a rejected plain object through cause.

Both new behaviours are teeth-checked — removing the outage guard and removing cause each fail their case.

Verified locally: end-to-end 106/106, non-browser 6811 passed, lint and both typechecks clean. test/logging.test.js fails 74/74 on a clean main too, unrelated to this.

One thing to be aware of, unrelated but observed: turns an error overlay into a warning overlay on partial recovery failed once in a full run and passed on four subsequent runs, including two full suites, and on clean main. It is an existing intermittent, not something this branch introduces.

Does this PR introduce a breaking change?

No. Messages that were not posted before are now posted; a reload that used to fire at a moment it should not no longer does.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

The posted messages are worth documenting — they are the integration surface dev-server users rely on and neither package documents them today. Not included here.

Use of AI

AI-assisted (Claude Code). Used to audit this client against webpack-dev-server's, write the fixes and the tests, and verify them: each new behaviour was teeth-checked by reverting it and confirming the matching test fails.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Build progress, errors, warnings, and completion status are reported to the page during hot updates, including compatible hot-update events.
    • Socket disconnections are reported once per outage, including when the first connection attempt fails.
  • Bug Fixes
    • Reloads are deferred during navigation when appropriate and target the nearest ancestor with a valid URL when running in an about:blank iframe.
    • Runtime error filters can inspect rejected plain-object values through the error cause.

Three gaps against webpack-dev-server's client, found by comparing what
survives if dev-server deletes its own and re-exports this one.

Its client posts every build event out through `self.postMessage` "so
plugins can consume it", and this one posted nothing at all — so anything
listening for `webpackOk` and its siblings would have gone quiet without a
word. Undocumented, which makes it easy to drop and hard to notice. All
eight are posted now, under the same names and payloads, including the bare
`webpackHotUpdate<hash>` string. `webpackClose` announces an outage once
rather than once per retry, matching the `retries === 0` guard the other
client reports it behind.

A reload while the page is already navigating away reloads a page the
browser is leaving, so `beforeunload` now holds reloads off — released
again after a grace period, since a `beforeunload` can be cancelled, and
settled by `pagehide`/`pageshow` for a page that comes back out of the
back/forward cache. In an `about:blank` iframe a reload reloaded the blank
document and lost the app; the nearest ancestor with a url of its own is
reloaded instead, and a cross-origin one is left alone rather than thrown
on.

A runtime error filter could not see what a rejection carried: a plain
object was wrapped in an `Error` and dropped. It is kept as `cause` now, so
a filter deciding on a status code has somewhere to read it from.

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

changeset-bot Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6ee3774

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

@coderabbitai

coderabbitai Bot commented Sep 26, 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 posts build-status and hot-update messages to the page, and reports socket disconnects. Reload handling tracks unload events, defers reload requests during navigation, and targets the nearest ancestor with a non-about URL. Runtime error handling preserves non-Error values in error.cause. End-to-end tests cover message ordering and payloads, socket closure, and runtime-error filtering.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 6ee37

The remaining concern is a narrow test gap in cross-origin reload fallback. It does not establish a current reload failure, so the change is mergeable with that coverage follow-up.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 6ee37

The changes appear confined to the development client and its page. No new privileged or cross-origin access path was demonstrated, but external message consumers and some embedding behavior remain unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated new exposure is to scripts receiving messages in the development client’s page, not to a newly addressed service, data store, or cross-origin frame. External page or plugin consumers were not identified.

Trust Boundaries and Controls

  • inferred — Build-event payloads originating at the development socket can become page-visible messages. The adapter supplies message shape, not authentication for consumers; the available evidence does not establish a consumer that treats these messages as authority.
  • observed — For about: frames, readable ancestor location access governs reload targeting; failed access results in a current-frame reload rather than navigation of the inaccessible ancestor.

Resilience and Maintainability Implications

  • inferred — The unloading transition coalesces deferred requests and clears them on navigation lifecycle events. Immediate calls outside that transition have no explicit duplicate guard; an unsafe security outcome from repeated calls is not demonstrated.

Hardening Proposals

  • proposed — Document the page-message shapes and identify their consumers before any consumer relies on these notifications for a security-sensitive decision.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the two primary changes: posting client build events to the page and guarding reloads during navigation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 7 files.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 9f8631f9-2bca-44b6-9003-ebe55a211101

📥 Commits

Reviewing files that changed from the base of the PR and between cc4946f and 54be7ee.

📒 Files selected for processing (9)
  • .changeset/client-postmessage-and-reload-guards.md
  • .changeset/reload-guards.md
  • .changeset/runtime-error-cause.md
  • client-src/clients/createSocket.js
  • client-src/index.js
  • client-src/overlay.js
  • client-src/utils/reload.js
  • client-src/utils/send-message.js
  • test/e2e/messages.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 client-src/clients/createSocket.js
Comment thread client-src/overlay.js Outdated
Comment thread client-src/utils/reload.js
Two review findings.

`new Error(message, { cause })` is ES2022, and this file is compiled to an
ES5 baseline for browsers that predate it — where the option is quietly
ignored and a `runtimeErrors` filter would find nothing to read. Defined
afterwards instead, and non-enumerable, which is the descriptor the option
would have given it.

A reload asked for while the page looked like it was leaving was dropped.
A `beforeunload` can be cancelled, and then the page is staying and still
wants the update, with nothing to ask again until the next rebuild — so it
is held for the grace period and performed when the navigation turns out
not to have happened. Dropped for good on `pagehide`, where the page
really is going, and on `pageshow`, where it came back from the cache
showing its own state and a reload from before the navigation is stale.

Covered by unit tests over a driven fake page, which is what makes the
timing and the three events testable at all.

A third finding, that `onDisconnect` reports a first attempt that never
opened, is accurate but deliberate: webpack-dev-server reports that case
too, from the same reset-on-open counter, and a page loaded while the
server is down has no connection either. Its wording said "a connection
that was open", which was wrong about its own behavior; the comment and
the type now say what it does.

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

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

🧹 Nitpick comments (1)
test/client-reload.test.js (1)

162-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the cross-origin test fail at the ancestor location read.

reloadPage reads target.parent before it reads the ancestor's location.protocol. The current mock throws too early, so a regression that fails to catch the ancestor location error can pass.

Suggested test change
-    Object.defineProperty(globalThis.window, "parent", {
+    const parent = { location: {} };
+    Object.defineProperty(parent.location, "protocol", {
       get() {
         throw new Error("cross-origin");
       },
     });
+    globalThis.window.parent = parent;

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: b1f684dc-ca2f-45f3-b716-fe20a166dca7

📥 Commits

Reviewing files that changed from the base of the PR and between 54be7ee and 6ee3774.

📒 Files selected for processing (4)
  • client-src/clients/createSocket.js
  • client-src/overlay.js
  • client-src/utils/reload.js
  • test/client-reload.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • client-src/overlay.js
  • client-src/clients/createSocket.js

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

@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.29630% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.13%. Comparing base (cc4946f) to head (6ee3774).

Files with missing lines Patch % Lines
client-src/utils/send-message.js 75.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2425      +/-   ##
==========================================
- Coverage   96.30%   96.13%   -0.17%     
==========================================
  Files          17       18       +1     
  Lines        1894     1943      +49     
==========================================
+ Hits         1824     1868      +44     
- Misses         70       75       +5     

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

@alexander-akait
alexander-akait merged commit 750edd7 into main Sep 26, 2026
20 of 22 checks passed
@alexander-akait
alexander-akait deleted the fix/client-gaps-vs-dev-server branch September 26, 2026 15:16
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