refactor(client): reuse webpack-dev-middleware's transports and overlay, and add "sse" - #5750
Draft
alexander-akait wants to merge 4 commits into
Draft
alexander-akait wants to merge 4 commits into
alexander-akait wants to merge 4 commits into
Conversation
🦋 Changeset detectedLatest commit: 3328b32 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 |
First step of moving the hot clients into webpack-dev-middleware: this
package's `WebSocketClient` is a strict subset of the one that package now
ships, and worse in three ways. It has no `close()` at all, despite
declaring `@implements {CommunicationClient}`, which the interface requires.
It has no guard against an event the socket had already queued reporting
after the caller closed, so a close could schedule a reconnection nobody
asked for. And it hands the url to `new WebSocket` unresolved, which throws
on browsers before Chrome 125 / Firefox 124 / Safari 17.3 for a relative or
`http(s):` url — this package always builds an absolute `ws:` url, so that
one never bit here, but it is a trap for anyone reusing the class.
Re-exported rather than deleted: `client.webSocketTransport` resolves to
this path, so anything pointing at it keeps working.
`import/no-unresolved` is switched off for the file, following the exemption
already in place for `@changesets/get-github-info`: the import resolver
cannot follow an `exports` subpath. TypeScript does resolve it, so
`lint:types-client` still covers the import.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
692 lines of overlay deleted in favour of the one dev-middleware ships —
the state machine, the iframe, the rendering, the runtime-error listeners.
What is left is an adapter: this package's `createOverlay`/`send` shape on
top of that overlay's `showProblems`/`clear`, plus `formatProblem`, which
stays because the console uses it and because this package still receives
webpack's error objects rather than formatted strings.
Three things had to be carried across so nothing outward changes:
* the element id. It is what a test, a screenshot tool or an integration
finds the overlay by, so `webpack-dev-server-client-overlay` is passed
through dev-middleware's new `overlay.id` rather than renamed;
* the Trusted Types policy name. Under an enforced
`require-trusted-types-for 'script'` the page's CSP allowlists a policy
by name, and dev-middleware's default is a different one, so
`webpack-dev-server#overlay` is passed explicitly — including where
this package's option is `false`, which means "no name of my own";
* `/webpack-dev-server/open-editor`. dev-middleware leaves the endpoint
empty by default, having no route to point at, so the file references
in a problem would have stopped being clickable.
The first two came from the e2e suite failing, not from reading the code,
which is the argument for swapping under the tests rather than after.
What does change is the overlay's internal DOM, which is not an interface
this package documents: a clickable file reference is `[data-open-file]`
rather than `[data-can-open]`, and a problem is headed by its level and
origin rather than "Compiled with problems". Two tests assert on those and
move with the implementation.
Checked against a baseline taken first: the overlay suite fails exactly
the set it fails on a clean `main` in this container — no case newly
failing, none newly passing — and the client and web-socket-url suites are
unmoved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
`client.webSocketTransport: "sse"` connects the page with `EventSource` instead of a WebSocket. The endpoint is webpack-dev-middleware's hot endpoint, which is an ordinary response rather than an upgrade, so there is no second server to start and it sits in the middleware chain where everything this package puts in front of it can still see it. Naming it as the client's transport picks the endpoint that serves it, so `webSocketServer` does not have to be set as well; `webSocketServer: "sse"` is accepted too. `lib/servers/EventSourceServer.js` is the adapter between that endpoint and the transport shape the rest of this package is written against, so nothing above the wire can tell which one it is talking to — same protocol, same messages, same options. All of the protection stays here: the endpoint hands over the request each client connected with, and `allowedHosts` and the origin check decide as they do for a socket. One difference in how that check reads a request. A browser puts an `Origin` on a WebSocket handshake whether or not it is cross-origin, so one without it is refused; `EventSource` sends no `Origin` on a same-origin request, so for a stream an absent one is taken as a page of this server's own. A stream that does carry an `Origin` — which is what a cross-origin page sends — is checked as before, and the `Host` check runs either way. Both transports are the middleware's now, re-exported from `client-src/clients/`, and each has a test of its own: the contract both answer, plus the silence watchdog only a stream needs. `test/e2e/event-source.test.js` drives a real browser through a hot update over the stream, the handshake it is greeted with, and both sides of the host check. `socket.js` no longer throws on a frame it cannot parse. A stream's keep-alive has to be a `data:` frame rather than a comment, or the client's own watchdog would count a quiet connection as a dead one, so something that is not JSON now arrives on the wire by design. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
alexander-akait
force-pushed
the
feat/reuse-dev-middleware-client
branch
from
September 29, 2026 21:48
1e729d2 to
14fa95c
Compare
`client-src/overlay.js` was the last of it: an adapter whose substance was
turning webpack's error objects into the shape the shared overlay renders, and
a `send({ type })` state machine over `showProblems`/`clear`. Both belong
elsewhere. The formatting is webpack-dev-middleware's `client/problem` now,
and the state machine was standing in for per-source slots that overlay
already has, so the events map onto two calls.
What stays is this package's identity on top of a shared overlay, as options:
the `webpack-dev-server-client-overlay` element id, so anything querying it is
unaffected; the `webpack-dev-server#overlay` Trusted Types policy name, which
a page's CSP allowlists by name; and the `/webpack-dev-server/open-editor`
route that makes a file reference clickable.
`test/client/ReactErrorBoundary.test.js` goes with it — the heuristic it
covers lives in webpack-dev-middleware, which tests it.
Requires webpack/webpack-dev-middleware#2438.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebuilt on current
main, against webpack-dev-middlewaremain.What changed
Both transports come from webpack-dev-middleware now.
client-src/clients/WebSocketClient.jsandclient-src/clients/EventSourceClient.jsre-export them, and
client.webSocketTransportresolves to those paths as italways has. One copy means one place for the things this package's WebSocket
client was missing: a
close()(which the interface declares and it neverhad), the guard that stops a queued event reporting after the caller closed,
and resolving a relative or
http(s):url for browsers whoseWebSocketwillnot.
The overlay comes from there too, behind an adapter that keeps this
package's shape on top of it —
createOverlay().sendandformatProblem—its element id (
webpack-dev-server-client-overlay), its Trusted Types policyname, and its open-in-editor route. Its markup is the shared one now, so the
overlay snapshots are regenerated: the problem text and location survive, the
location is clickable (
data-open-file), and the heading is the level ratherthan "Compiled with problems".
"sse"is a second transport.The page connects with
EventSource. The endpoint is the middleware's hotendpoint — an ordinary response rather than an upgrade — so there is no second
server to start, and it sits in the chain where everything this package puts
in front of it can still see it.
lib/servers/EventSourceServer.jsis theadapter between that endpoint and the transport shape the rest of the package
is written against, so nothing above the wire can tell which one it is talking
to: same protocol, same messages, same options.
Naming it as the client's transport picks the endpoint that serves it, so
webSocketServerdoes not have to be set as well;webSocketServer: "sse"isaccepted too.
Protection stays here
The endpoint hands over the request each client connected with, so
allowedHostsand the origin check decide exactly as they do for a socket.There is one difference in how that check reads a request, and it is the
reason a browser could not connect at first. A browser puts an
Originon aWebSocket handshake whether or not it is cross-origin, so a handshake without
one is not a page and is refused.
EventSourcesends noOriginon asame-origin request, so the existing check refused every page this server
serves, in a reconnect loop. For a stream an absent
Originis now taken as apage of this server's own; a stream that does carry one — which is what a
cross-origin page sends — is checked as before, and the
Hostcheck, which isthe DNS-rebinding defence, runs either way. Nothing about the WebSocket path
changed.
Tests
The EventSource side had none, here or anywhere in this package. It does now:
test/client/clients/EventSourceClient.test.js— the contract bothtransports answer (connect, open, message as a string, close, and silence
after
close()), plus the one thing only a stream has: it reports a closewhen the connection falls silent without ever failing, and does not when
messages keep arriving.
test/client/clients/WebsocketClient.test.js— rewritten. It pokedclient.client.onerror, an internal that is webpack-dev-middleware's now, soit failed; it tests the public contract against a real server instead, and
gained the close-means-silence case.
test/e2e/event-source.test.js— a real browser over a real stream: itconnects and takes a rebuild as a hot update, it is greeted with this
package's handshake (
hot,liveReload,progress,overlay), adisallowed origin is refused, and a request with no origin at all is taken
as same-origin.
socket.jsno longer throws on a frame it cannot parse. A stream's keep-alivehas to be a
data:frame rather than a comment, or the client's own watchdogwould count a quiet connection as a dead one, so something that is not JSON
now arrives on the wire by design.
Worth knowing if you script a browser against the dev server: a stream is a
request that never ends, so a page with an open one never reaches "network
idle". Wait for
loadordomcontentloaded.State
Green locally: overlay 39, the two transport client suites, the new
event-sourcesuite,server-and-client-transport,web-socket-server,web-socket-communication,allowed-hosts,client,validate-options,normalize-options— 63/63 across the suites this touches, and lint, bothtypecheck passes, spelling, the precompiled schema check and the full build
clean. Two IPv6 cases in
test/serverand thetest/cli/basiccases failidentically on
mainin this environment, so they are not from this change.CI will be red until webpack-dev-middleware ships.
./client/wsand./client/sseare not in the published 8.3.0exports— this was verifiedagainst webpack-dev-middleware
main, installed from a local pack.🤖 Generated with Claude Code
https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA