Skip to content

refactor(client): reuse webpack-dev-middleware's hot client - #5750

Draft
alexander-akait wants to merge 1 commit into
mainfrom
feat/reuse-dev-middleware-client
Draft

alexander-akait wants to merge 1 commit into
mainfrom
feat/reuse-dev-middleware-client

Conversation

@alexander-akait

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

Copy link
Copy Markdown
Member

Draft, and it has to stay one until webpack-dev-middleware is released. CI is red here for a reason outside the diff — see Why CI cannot pass yet below. Opened now so the plan and the findings are reviewable before the large part lands.

Summary

First step of moving the hot clients into webpack-dev-middleware, so this package stops carrying its own. This commit takes the WebSocket transport class.

This package's WebSocketClient is a strict subset of the one dev-middleware now ships, and worse in three ways:

  • no close() at all, despite declaring @implements {CommunicationClient} — the interface requires it;
  • 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;
  • the url is handed 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 it never bit here, but it is a trap for anyone reusing the class.

Re-exported rather than deleted, because client.webSocketTransport resolves to this path.

Why CI cannot pass yet

webpack-dev-middleware@8.3.0 on npm exports only ./client, ./client/indicator and ./client/overlay. There is no ./client/ws — that export landed in webpack/webpack-dev-middleware#2421, which is merged to main but unreleased. So the specifier this commit imports does not resolve against the published dependency at all: it is not merely a types problem, webpack cannot resolve it either.

Two dev-middleware changes therefore gate this PR:

  1. the release itself — chore(release): new release webpack-dev-middleware#2406, which carries the changesets for ./client/ws;
  2. fix(client): ship types for the client exports, and say it is ESM webpack-dev-middleware#2428 — none of the ./client/* exports could be imported by a TypeScript consumer at all, because no declarations were emitted for them and the client is ESM inside a CommonJS package. lint:types-client here only passes with that in.

Once dev-middleware releases, this needs a dependency bump and nothing else.

Tested against dev-middleware's main, not a guess: built and packed from main, installed here, and the suites run against it. Baselines taken first, so the comparison means something:

baseline with this change
test/client/index.test.js 21/21 21/21
test/e2e/web-socket-server-url.test.js 29 pass / 5 fail 29 pass / 5 fail
test/e2e/overlay.test.js 15 pass / 24 fail 15 pass / 24 fail

The failures are identical with and without the change and reproduce on a clean main in that container — the 5 are ("ws") cases needing local-ip/IPv4 host resolution, and the overlay suite fails there for environmental reasons. Not introduced here, and not claimed as passing.

What the rest of the move needs, and what blocks it

Worth stating before the big commit, because the plan said "delete client-src/ and re-export dev-middleware's client" and that turns out to understate it. The two packages do not speak the same protocol. This package sends { type, data } with ok/still-ok/invalid/hash/static-changed/progress-update; dev-middleware sends { action } with building/progress/built/sync. Swapping the client means adopting its protocol on the server side too.

Three things make that tractable and one blocks it:

  • ✅ The connect-time config push is redundant. This package sends hot, liveReload, progress, reconnect, overlay and logging per client on connect — and already puts all of them in the client entry query. dev-middleware's client reads them from the query, including this package's spellings (webSocketURL, reconnect, live-reload). So the push can go.
  • ✅ static-changed has a home: published through hot.publish and picked up by the client's subscribe() hook for actions it does not recognise.
  • ✅ hash / still-ok are covered by the built/sync payloads, which carry the hash, errors and warnings.
  • ❌ Blocker: there is no way to reject a connecting client. This package validates Host/Origin on connection and sends error then closes. dev-middleware's WebSocket server has no hook for that, so delegating the server would silently drop an origin check — security-relevant, not cosmetic. It needs a small addition there first.
  • ⚠️ preventReloading on warnings has no dev-middleware equivalent.

What kind of change does this PR introduce?

refactor

Did you add tests for your changes?

No new tests in this commit — it swaps an implementation for a superset of itself, and the existing client and e2e suites cover it. The behaviour that is new (a working close(), the closed guard, url resolution) is already tested in dev-middleware.

Does this PR introduce a breaking change?

No. client.webSocketTransport keeps resolving to the same path, and the class answers the same interface — plus the close() it was supposed to have.

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

Nothing for this commit. The full move will want a note that the client is dev-middleware's, and the client.overlay keys that gains (styles, ansiColors, openEditorEndpoint, paginate).

Use of AI

AI-assisted (Claude Code). Used to compare the two transports, make the swap, and verify it against a pack of dev-middleware's main with baselines taken before and after; also to work out what the remaining steps need, which is where the protocol divergence and the missing origin-check hook came from.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

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
@changeset-bot

changeset-bot Bot commented Sep 26, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 5a7ec65

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

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