Skip to content

refactor(hot): give each transport its own file, load only the one chosen - #2452

Merged
alexander-akait merged 3 commits into
feat/hot-tokenfrom
refactor/hot-transport-files
Oct 2, 2026
Merged

alexander-akait merged 3 commits into
feat/hot-tokenfrom
refactor/hot-transport-files

Conversation

@alexander-akait

Copy link
Copy Markdown
Member

What

hot.js had one transport in a file of its own and the other inline, which made the structure read as if Server-Sent Events were the special case rather than the default. Both are files now, and hot.js is the lifecycle and the payloads.

src/servers/
  EventSourceServer.js   <- extracted from hot.js
  WebSocketServer.js

hot.js drops from 231 lines of stream plumbing to the part that is actually about hot: the webpack hooks, normalizeStats, the built/sync/building payloads, and createHot.

Each transport owns what is specific to it

The CORS default differed per transport and was decided in hot.js, away from the code that applies it. Now each file holds its own:

// servers/EventSourceServer.js
const HOT_DEFAULT_CORS_SSE = true;

// servers/WebSocketServer.js
const HOT_DEFAULT_CORS_WS = CORS_LOCAL_ORIGINS;

What stays in hot.js is the mint that hands one token to whichever transport was built — that is genuinely shared.

pathMatch moves to utils.js: middleware.js was reaching through hot.js to get at it, which is not a hot concern.

Lazily required, both of them

function requireServer(name) {
  return name === "WebSocketServer"
    ? require("./servers/WebSocketServer.js")
    : require("./servers/EventSourceServer.js");
}

#2450 made the WebSocket server lazy. This finishes the thought: a project on 'ws' no longer parses the SSE server either, and a custom transport parses neither.

Notes for review

  • Pure movement plus the two default constants; no behaviour changes. The CORS and token semantics are the same code, relocated to the file that applies them.
  • types/hot.d.ts loses the declarations that moved, and types/servers/EventSourceServer.d.ts is generated — regenerated with npm run build:types, not written by hand.
  • Two tests follow their subject: pathMatch is imported from ../src/utils, and the custom-transport assertion now expects the options object the transport is handed rather than one assembled in hot.js.

Verified

  • npm run lint — clean (eslint, prettier, cspell, tsc, client types, schema-check)
  • unit — 7084 passed, 19 suites
  • e2e — 167 passed, 14 suites, 20 snapshots

Stacked on #2451 — review that one first; this PR's diff is only the refactor.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA


Generated by Claude Code

@changeset-bot

changeset-bot Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9959f95

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

alexander-akait and others added 2 commits October 2, 2026 00:40
`createEventStream` moves out of `hot.js` into `src/servers/EventSourceServer.js`,
beside the WebSocket one, and both are required where the transport is picked
rather than at the top of the module. Neither is reached until a `createHot`
call asks for it:

  transport           EventSourceServer   WebSocketServer   ws
  "sse" (default)     loaded              —                 —
  "ws"                —                   loaded            loaded
  your own            —                   —                 —

Previously the event stream was parsed by every consumer, including one on a
WebSocket or on a transport of its own, because it lived in `hot.js`.

`src/servers/` now holds both, which is the shape webpack-dev-server's
`lib/servers/` has, and the CORS and token rules sit with the transport that
enforces them — `hot.js` keeps only the default each one starts from and the
mint that hands a token to both.

`createEventStream` is still exported from `hot.js`, as a wrapper that loads
the module on the first call, so an importer sees no change.

`requireServer` spells each path out rather than building one from its
argument: a bundler has to be able to see both statically.

Also fixes two fixtures that hand-wire a client and so have to carry a token of
their own, which is what a developer wiring their own entry has to do: the
worker app, and the cross-origin test that builds its own WebSocket url — that
one reads `instance.token` rather than hardcoding one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
Follow-up to extracting the event stream: the module boundaries were right but
three things were still on the wrong side of them.

Each transport's CORS default moves to the transport that applies it.
`HOT_DEFAULT_CORS_SSE` and `HOT_DEFAULT_CORS_WS` lived in `utils.js`, which
`hot.js` then imported the SSE one from purely to re-export it — it never used
it. `resolveCors` is called inside each server, so the default each one starts
from belongs there too, with the comment explaining why they differ.

`pathMatch` moves to `utils.js`. `middleware.js` was reaching through `hot.js`
for a url helper, which is the wrong direction: the middleware does not
otherwise depend on the hot module, and every other request helper it uses is
already in `utils.js`. `hot.js` did not use `pathMatch` itself either — that
import was another re-export.

And a comment that the lazy-loading change had stranded: "what a transport has
to do for itself" describes `CLIENT_STREAM_METHODS`, and `requireServer` had
been inserted between the two.

What is left in `hot.js` is one thing: the hot lifecycle and the payloads it
publishes — its own defaults, the contract a custom transport has to meet, and
`createHot`. Transport mechanics are in `./servers`, request helpers in
`./utils`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
Measuring a build is the server's call rather than the middleware's, and
`hot.progress` made it the middleware's: it applied `ProgressPlugin` to
your compiler, so a server that applies one itself — webpack-dev-server
does — ends up with two of them on one compiler.

What only the middleware can do is carry the result, and the bundled
client already renders `{ action: "progress" }`. So the option becomes one
public method: `instance.publish(payload)` puts a payload of your own on
the stream, and a server hands over what its own plugin reports. It is a
no-op when `hot` is off, so a caller does not have to ask first, and
nothing is sent when no client is connected — a `ProgressPlugin` tick
fires far more often than anyone is listening.

`hot.progress` keeps working until the next major release. Its browser
end, `hot.client.progress`, is unaffected and stays: a server publishing
its own progress payload gets it drawn with no further configuration.

Two things the option did for you become the caller's: rounding the
percent, and dropping a tick that rounds to the same whole number as the
last one.

Also fixes two README links that pointed at headings which do not exist,
for `etag` and for the `attach` method.


Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@alexander-akait
alexander-akait merged commit 621e013 into feat/hot-token Oct 2, 2026
5 checks passed
@alexander-akait
alexander-akait deleted the refactor/hot-transport-files branch October 2, 2026 05: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