chore(release): new release - #2406
Open
github-actions[bot] wants to merge 1 commit into
Open
github-actions[bot] wants to merge 1 commit into
github-actions[bot] wants to merge 1 commit into
Conversation
github-actions
Bot
force-pushed
the
changeset-release/main
branch
6 times, most recently
from
September 23, 2026 10:38
6c779e5 to
4ee071a
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
9 times, most recently
from
September 26, 2026 15:28
8edb030 to
c4f9ffd
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
14 times, most recently
from
September 30, 2026 11:39
2c25530 to
a702738
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
September 30, 2026 12:38
a702738 to
5dfd2aa
Compare
|
ok |
github-actions
Bot
force-pushed
the
changeset-release/main
branch
12 times, most recently
from
October 3, 2026 10:07
c72796f to
fad3035
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
October 3, 2026 10:08
fad3035 to
7042804
Compare
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.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
webpack-dev-middleware@8.4.0
Minor Changes
The hot client now runs in a web worker. It connected only where there was a (by @alexander-akait in #2430)
window, which a worker has none of — but it hasEventSource,WebSocketand webpack's runtime, which is all an update needs, so the client reads
selfinstead. In a page that is the same object, so nothing changes there.
The overlay and the building indicator stay with the page, since a worker has
no document. A reload cannot happen from inside one either — there is no
location.reloadin a worker — so when an update cannot be applied the clientsays so once and leaves the page that started the worker to reload it.
target: "webworker"compilations get a client fromhotlike any otherbrowser target, so a worker is hot without a line of configuration.
The client can now keep a page up to date without Hot Module Replacement.
hot(by @alexander-akait in #2433)(default
true) says whether a build is applied as an update, andliveReload(default
true) reloads the page on a build that changed something when it isnot — so a project with no
HotModuleReplacementPluginstill sees its changes.A build that changed nothing is left alone either way.
Added a
reloadaction, for a change no compilation knows about: publish{ action: "reload" }and every page loads itself again, whateverhotandliveReloadare set to. It does what thesubscribe()example in the READMEused to have you write by hand.
Added
urlPrefix(default"webpack-dev-middleware"), which names the page-urlparameters that turn
hotandliveReloadoff for a single page —?webpack-dev-middleware-liveReload=false.live-reloadused to be accepted as another spelling ofreload. The two aredifferent —
reloadis the fallback for an update that was tried and could notbe applied — so the one that reloads on a build is its own option,
liveReload.live-reloadis not accepted under either meaning. It was never released.every browser option can be set on the middleware as well as on the query (by @alexander-akait in #2436)
hot.clientand the injected entry's query are now one option set rather thantwo overlapping ones.
hot,liveReloadandurlPrefixwere readable from thequery alone and can now be set in node:
transport,pathandnamego the other way: they were the middleware's toset, and can now be overridden, which is what a page reaching the endpoint
through a proxy or on another origin needs.
The middleware's own values — the resolved
hot.transport, the resolvedhot.pathand the compilation's name — remain the defaults, so nothing changesfor anyone not setting them.
A
hot.transportof your own now gets a client too, whenhot.client.transportsays which of the two built-in protocols yours carries. Without it the client is
still yours to add, as before.
Every option has one name and no aliases, in
hot.client, in the entry queryand in the page-url parameters alike. The two second spellings the query had
picked up from webpack-dev-server —
webSocketURLforpathandlive-reloadfor
liveReload— are gone, and so is the-live-reloadpage parameter, whichis now
-liveReload. Neither alias was ever released.Post build events to the page the way webpack-dev-server's client does —
webpackInvalid,webpackProgress,webpackOk,webpackStillOk,webpackWarnings,webpackErrors,webpackCloseandwebpackHotUpdate<hash>— so a plugin or a framework's dev tooling can follow a build without reaching into the client (by @alexander-akait in #2425)Carry the browser runtime's events over a WebSocket with the client
transport=wsoption, or over a transport of your own, and reuse the built-in two fromwebpack-dev-middleware/client/sseandwebpack-dev-middleware/client/ws(by @alexander-akait in #2421)The overlay now takes one of webpack's errors or warnings as it comes, not (by @alexander-akait in #2438)
only a formatted string:
The middleware formats its own payloads on the server, so its client never
needed this. A server that sends webpack's error objects to the browser and
formats them there — which is what webpack-dev-server does — had to write that
formatting itself. It is
webpack-dev-middleware/client/problemnow, andformatProblemis there for a console as well as an overlay:The server's own formatting reads the same way as a result, which fixes two
things it got wrong. An error webpack names no module for sent a first line
holding a single space — and the overlay reads the first line as the heading,
so it drew a heading with nothing in it; it now sends the message alone. And a
module built by loaders reports its whole request (
babel-loader!./app.js),which read as noise where the file is what matters; the file comes first now,
with the request after it, and
fileis used when webpack sets one.Grouped six flat options into
cacheandmime: (by @alexander-akait in #2455)etagcache.etaglastModifiedcache.lastModifiedcacheControlcache.controlcacheImmutablecache.immutablemimeTypesmime.typesmimeTypeDefaultmime.defaultFour of the sixteen top-level options were the same topic and two more were another, so the list read as an inbox rather than a design.
cacheControlandcacheImmutablealso lose their stutter inside the group.Both spellings work. A legacy name warns and names its replacement, and will be removed in the next major release; when a name is set both ways the grouped one applies, so a migration that sets the new name and forgets to delete the old is not silently ignored. The legacy keys stay on
instance.context.optionsfor anything reading them.instance.context.optionsis now a copy of the object you passed rather than that object itself, so two middlewares built from one options object stay independent — the same reason themime.typestable is copied per instance. Adding or replacing a key on your object after building the middleware no longer reaches it. An object nested inside it is still shared, as it has always been —cache.controlin its object form, andheaders, behave the same way here as before.Added
hot.client, so the browser runtime's options are set on the middleware (by @alexander-akait in #2432)along with the rest of the hot configuration instead of in a query string on an
entry. It is read when the client is injected;
overlay,progress,reload,logging,reconnect,timeout,autoConnectanddynamicPublicPathareaccepted, and
overlaytakes the same object (filter functions included) as thequery does.
transport,pathandnameare not accepted: the middlewareknows those and sets them itself, so the runtime cannot be pointed somewhere the
server is not listening.
Added
handleUpgrade(req, socket, head)andonConnect(fn), so a server can (by @alexander-akait in #2431)decide for itself who may listen to the hot endpoint.
handleUpgradeanswersone WebSocket upgrade for a server that keeps its own
upgradelistener,instead of handing the whole server over with
attach, and says whether therequest was the endpoint's.
onConnectis called with each client and therequest it joined with, before anything is published to it, so a client closed
there is sent nothing at all.
onConnectis now given the request in both transports; it used to be dropped,which left nothing to judge a client by.
Added
hot.cors, which says which origins may reach the hot endpoint from a page on another one, over either transport. (by @alexander-akait in #2444)The Server-Sent Events endpoint answers every request with
Access-Control-Allow-Origin: *, inherited fromwebpack-hot-middleware, and nothing could turn it off. A payload carries a build's module paths and the source frames webpack puts in a failed build's errors, so that grant lets any site loaded in the same browser read part of the developer's source.hot.corsis how to narrow it: an origin, a list of origins and patterns, a regular expression, a predicate,{ origin }as Vite andexpressjs/corsare configured,falsefor nothing but the endpoint's own origin, ortruefor every one of them. An allowed origin is echoed back withVary: Originrather than wildcarded.The two transports default differently, and deliberately. Server-Sent Events keeps granting every origin, because narrowing it would stop a page served from another origin reading its own build — a breaking change, which waits for a major release. The WebSocket transport is new here, so it starts where the other one is going: local origins only (
localhostand anything under it,127.0.0.1,[::1], any port, either scheme), the same set and the same reasoning as Vite'sserver.corsdefault. Setcorsto narrow the event stream today; the next major release will do it for you.Each transport enforces it where its wire allows. The event stream carries the grant or withholds it, and refuses nothing. A WebSocket handshake is not subject to CORS — a browser sends
Originand pays no attention to what comes back — so there the upgrade is refused with403before the handshake completes, through bothattach(server)andhandleUpgrade(req, socket, head). A request carrying noOriginat all, and one whoseOriginis the one it was addressed to, are allowed either way.A server that already decides for itself who may connect should set
cors: true, so its own rule is the only one.Added
hot.token: a secret the injected client carries and the hot endpoint (by @alexander-akait in #2451)requires, so reaching the stream takes something a page has to have been given
rather than a header the browser may or may not send.
hot.corsis answered byOrigin, and that is its weakness. A browser omitsOriginand the wholeSec-Fetch-*family when the destination is notpotentially trustworthy — plain
httpto anything butlocalhost, whichhost: "0.0.0.0"gives you. webpack-dev-server shipped two fixes built onthose headers and both were bypassed exactly that way, CVE-2026-6402 and then
CVE-2026-14620. A token asks the browser to volunteer nothing.
Off by default on both transports, and
truein the next major release. Atoken only reaches the browser on the entry the middleware adds, and
injectbeing on does not mean an entry was added: it is skipped when every entry point
already pulls the client in, when
hot.transportis a function, and for anon-web target. Requiring one by default would turn each of those into a
403on every client. Set
token: trueto turn it on, and if you do so where noclient was injected the middleware warns rather than leaving you with an
unexplained refusal.
With the client injected, that is all it takes: it is handed the token and puts
it on its connection url.
hot.inject: falseturns the requirement off — thetoken travels in the entry the middleware adds, so with nothing injected there
is no way to hand one over. A configuration that lists the client entry itself
is built before the middleware exists and cannot carry a minted token, so give
it a fixed one both sides know, or read the minted one from
instance.token.What it does not protect: the client reads the token from its entry query, so
it is a string in the bundle. Anything that can already read the bundle
cross-origin reads the token with it, and over plain
httpto a non-localhostaddress nothing stops that unless your server sends
Cross-Origin-Resource-Policy. This hardens every case where the bundle is notreadable, and is defence in depth in the case where it is.
Choose how hot module replacement events reach the clients with
hot.transport: Server-Sent Events (the default), a WebSocket, or a transport of your own (by @alexander-akait in #2420)hotnow puts the client runtime andHotModuleReplacementPlugininto the compilation itself, so enabling it is the whole of what a webpack configuration needs — no entry to add, no plugin to apply. The client is told the endpoint and the transport the middleware resolved, so the two cannot drift apart. A configuration that already has the client as an entry is left alone, nothing is injected into a compilation that does not target the browser, andhot.inject: falseturns it off for anyone who would rather wire it themselves (by @alexander-akait in #2430)Added
publish(payload)to the instance, which puts a payload of your own on the hot stream. The middleware publishes what it knows about — a build starting, finishing, failing — and anything else a server measures is its own;ProgressPluginis the example. The bundled client already renders{ action: "progress" }, so a server that applies the plugin itself now has somewhere to put what it reports. Nothing is sent when no client is connected, and it does nothing whenhotis off. (by @alexander-akait in #2451)Extensions resolve through
mime-dbdirectly, rather than through (by @alexander-akait in #2449)mime-types.mime-dbis the data, and webpack itself already depends on itand scores it the same way, so a webpack project now installs one package here
instead of two over the same table. Every extension in
mime-dbresolves tothe byte-identical media type,
Content-Typeand charset it did before.The table is also built per middleware instance now. The
mimeTypesoptionused to be applied by writing into the table
mime-typesexports, which isone object shared by everything in the process that requires it: two
middlewares accumulated into a single map rather than each keeping its own,
and anything else using
mime-typesinherited whatever a middleware hadregistered. The option is read ahead of the known extensions instead, so it
belongs to the instance it was given to and the database is left alone.
overlay.idsets the id of the overlay element, so a package embedding this overlay can keep the id its own users already query instead of making them all chase a rename. The card follows as<id>-card, and the default is unchanged (by @alexander-akait in #2429)progressnow takes"circular"and"linear"as well as a boolean, the same values as webpack-dev-server'sclient.progress."circular"is the badge this package has always shown, and whattruestill selects;"linear"renders a thin bar across the top of the viewport (by @alexander-akait in #2426)A custom
hot.transportnow needs only four methods —onConnect,publish,publishToandclose.handlerandhasClientsbecame optional: without ahandlera request on the endpoint's path is answered426 Upgrade Required, and withouthasClientsa payload is built and the transport decides for itself inpublish. A transport that implements all six keeps working unchanged (by @alexander-akait in #2427)Patch Changes
Fixed
autoConnecton the client's query being read differently from every other boolean there. It tested=== "true"while the rest test!== "false", so a value it did not recognise —?autoConnect=1— turned the client off rather than leaving it on, and since the default is already on, turning it off is the only thing anyone writes it for. (by @alexander-akait in #2456)Bound the internal url and
Rangeheader caches, which grew for the life of the process and were never released, even byclose(). (by @alexander-akait in #2405)Ship type declarations for the
./client,./client/sse,./client/ws,./client/indicatorand./client/overlayexports, and mark the client as the ES modules it has always been. A TypeScript consumer importing one of them gotany— or, undernode16/nodenextresolution, the module namespace instead of the default export, because the files are ES modules inside a CommonJS package with nothing saying so (by @alexander-akait in #2428)Say
connectedwhichever transport the client used, rather than only Server-Sent Events (by @alexander-akait in #2423)Deprecated the
hot.progressoption; it will be removed in the next major release and keeps working until then. It appliedProgressPluginto your compiler, which leaves a server that applies one itself — webpack-dev-server does — with two of them on one compiler. Apply it yourself and hand the result topublish, rounding the percent and dropping a tick that repeats one as the option did for you. The browser end of this,hot.client.progress, is unaffected and stays. (by @alexander-akait in #2451)Fixed an empty overlay that covered the page and could only be dismissed by (by @alexander-akait in #2437)
hand. A source reporting an empty list of problems — which is how a source
says it has nothing — was kept as a slot, so the union of every source's
problems was non-empty and the card mounted with nothing in it. Worse when
another source then cleared: the overlay stayed, showing only its dismiss
hint, on page
-1. An empty list is now the source having nothing, and theoverlay closes when no source has anything left.
Fixed the building indicator staying on the page for good after a
multi-compiler build. A progress payload carries no compilation name, so the
client attributed it to whichever compilation most recently started building —
and a payload from a still-running compilation could re-mark a sibling that
had already finished, and would never report again, as building. Progress now
reports on the build that is running instead of starting one, so the badge
goes away once every compilation that started has reported back.
The overlay recognizes a file reference in every shape a stack frame carries (by @alexander-akait in #2439)
it, not just webpack's own relative paths. An absolute path
(
/home/me/src/app.js:3:1), a Windows one (C:\src\app.js:4:2) and afile://url are all clickable now; before, only./and../were, so aruntime error's stack offered nothing to open.
A frame in webpack's generated runtime is left alone. It has no file behind it,
and now that an absolute path is recognized it would otherwise be offered for
opening and the endpoint asked for something it cannot do.
A file reference inside a url is left to the url.
https://example.test/app.jsis a link, and its path is not somewhere an editor can go.
An editor that does not open says so, rather than nothing happening: the
reference gets a title explaining it, and the console gets the reason and which
endpoint was asked.
Move focus into the error overlay when it opens, keep it on the navigation while paging through problems, and give it back to whatever the page had focused — a control inside an open shadow root included — when it closes, and give its frame an accessible name (by @alexander-akait in #2423)
Validate options with a precompiled schema to cut ~155ms from startup. (by @alexander-akait in #2413)
Fixed two broken links in the README: the
etagrow of the options table and the two references to theattachmethod pointed at headings that do not exist. (by @alexander-akait in #2451)hot.client.reconnectnow applies to Server-Sent Events as well. It was overridden toInfinitythere, so asking for a bounded number of attempts over the default transport did nothing. Unset still means "keep trying for as long as the page is open", since a dev server is expected to come back and a tab left open across a restart has to find it again. (by @alexander-akait in #2457)hot.client.timeoutis documented as Server-Sent Events only, which is what it always was: that transport sends its heartbeat as data the client can see, while a WebSocket sends a protocol ping the browser answers without telling JavaScript — a silence watchdog there would fire on a healthy idle connection, and the half-open case it would catch is handled by the server, which terminates a socket that stops answering. It is no longer handed to a WebSocket client whose constructor takes no options.Both decisions now live in one place,
client-src/utils/socket-options.js, with tests for each transport. The documented default forreconnectwas wrong for the default transport while it was being ignored there: unset, Server-Sent Events keep trying for as long as the page is open and a WebSocket gives up after 10.Dropped the
ansi-html-communitydependency. The overlay's ANSI-to-HTML (by @alexander-akait in #2442)conversion is
client-src/utils/ansi-html.jsnow, which is the third of thatpackage this project used — the rest was surface it never touched, and it
shipped into every consumer's browser bundle. Four production dependencies
instead of five, and one fewer unmaintained package in the supply chain (its
last release was 0.0.8 in April 2022, itself a fork of the abandoned
ansi-html).Output is byte-identical for the sequences a build actually produces. Four
things it got wrong are fixed:
"transparent", which is how the overlay says to leavethe page's own colour alone, became
color:#transparent. That is not acolour, so the reset worked only because browsers drop an invalid
declaration, and the inverse sequence did nothing at all.
\u001b[1;31m) matched nothing,so the escape stayed in the output as text for the reader to see.
\u001b[m, which is\u001b[0mwritten short, was left in the output thesame way.
</span>. Thehighlighters wrap their own spans around this output, so a stray close could
end one of theirs early.
</span>, whatever was open.\u001b[3mopens an<i>, so an unclosed italic came out as<i>x</span>, and an interleavedsequence crossed its tags:
<i><span>x</i></span>. Each open element nowcarries its own closing tag, so the markup nests whatever the sequences do.
The conversion had no test of its own while it was a dependency. It has 25 now,
one of which walks every three-sequence combination and checks the result
nests.
Do not reload a page that is already navigating away, and reload the nearest ancestor that has a url of its own when the app runs in an
about:blankiframe (by @alexander-akait in #2425)Give an
overlay.runtimeErrorsfilter the rejected value througherror.cause, so a rejection carrying a plain object rather than anErrorcan still be judged on what it carries (by @alexander-akait in #2425)Keep an uncaught runtime error in the overlay when a build succeeds — a successful compilation says nothing about an error the page threw on its own, and it used to dismiss one raised moments earlier by an entry that threw while it was still evaluating. A rebuild still clears it, since that replaces the code the error came from (by @alexander-akait in #2424)
Make the two client transports behave alike: neither logs a raw connection error, and neither reports anything after being closed (by @alexander-akait in #2423)
Hardened the path-traversal guards in
getFilenameFromUrl. The remainder left after thepublicPathprefix is stripped is now checked for..on its own, before it is joined onto the output root. (by @alexander-akait in #2445)The existing
..guard tests the whole request path normalized, which catches far less than it appears to:/public/../secretnormalizes tosecretwith no..segment left to match, and/assets../secretnever had a..segment to begin with — it is a sibling sharing the prefix, which apublicPathwith no trailing slash makes possible. Both of those were left to the containment check on the final resolved path, so a single check stood between two classes of traversal and the output root. There are now two independent guards: one on the shape of the request, one on where it resolved.A
..that walks out of the output root and back into it —/assets../dist/file.js, which resolves to a file that is inside — is now refused rather than served. It was only ever a second spelling of a path reachable directly, and one whose shape cannot be told apart from an escape.