diff --git a/.changeset/autoconnect-query-parsing.md b/.changeset/autoconnect-query-parsing.md deleted file mode 100644 index 348e50de2..000000000 --- a/.changeset/autoconnect-query-parsing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Fixed `autoConnect` on 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. diff --git a/.changeset/bound-memoize-caches.md b/.changeset/bound-memoize-caches.md deleted file mode 100644 index fec7c17bd..000000000 --- a/.changeset/bound-memoize-caches.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Bound the internal url and `Range` header caches, which grew for the life of the process and were never released, even by `close()`. diff --git a/.changeset/client-in-a-worker.md b/.changeset/client-in-a-worker.md deleted file mode 100644 index bcd44558d..000000000 --- a/.changeset/client-in-a-worker.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -The hot client now runs in a web worker. It connected only where there was a -`window`, which a worker has none of — but it has `EventSource`, `WebSocket` -and webpack's runtime, which is all an update needs, so the client reads `self` -instead. 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.reload` in a worker — so when an update cannot be applied the client -says so once and leaves the page that started the worker to reload it. - -`target: "webworker"` compilations get a client from `hot` like any other -browser target, so a worker is hot without a line of configuration. diff --git a/.changeset/client-live-reload.md b/.changeset/client-live-reload.md deleted file mode 100644 index c6e127ae7..000000000 --- a/.changeset/client-live-reload.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -The client can now keep a page up to date without Hot Module Replacement. `hot` -(default `true`) says whether a build is applied as an update, and `liveReload` -(default `true`) reloads the page on a build that changed something when it is -not — so a project with no `HotModuleReplacementPlugin` still sees its changes. -A build that changed nothing is left alone either way. - -Added a `reload` action, for a change no compilation knows about: publish -`{ action: "reload" }` and every page loads itself again, whatever `hot` and -`liveReload` are set to. It does what the `subscribe()` example in the README -used to have you write by hand. - -Added `urlPrefix` (default `"webpack-dev-middleware"`), which names the page-url -parameter that overrides what a build does to a single page — -`?webpack-dev-middleware-apply=nothing`. - -`live-reload` used to be accepted as another spelling of `reload`. The two are -different — `reload` is the fallback for an update that was tried and could not -be applied — so the one that reloads on a build is its own option, `liveReload`. -`live-reload` is not accepted under either meaning. It was never released. diff --git a/.changeset/client-options-both-ways.md b/.changeset/client-options-both-ways.md deleted file mode 100644 index c39bea0ab..000000000 --- a/.changeset/client-options-both-ways.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -every browser option can be set on the middleware as well as on the query - -`hot.client` and the injected entry's query are now one option set rather than -two overlapping ones. `hot`, `liveReload` and `urlPrefix` were readable from the -query alone and can now be set in node: - -```js -app.use( - middleware(compiler, { - hot: { client: { hot: false, liveReload: true, urlPrefix: "my-server" } }, - }), -); -``` - -`transport`, `path` and `name` go the other way: they were the middleware's to -set, and can now be overridden, which is what a page reaching the endpoint -through a proxy or on another origin needs. - -```js -app.use( - middleware(compiler, { - hot: { client: { path: "wss://dev.example.com/__webpack_hmr" } }, - }), -); -``` - -The middleware's own values — the resolved `hot.transport`, the resolved -`hot.path` and the compilation's name — remain the defaults, so nothing changes -for anyone not setting them. - -A `hot.transport` of your own now gets a client too, when `hot.client.transport` -says which of the two built-in protocols yours carries. Without it the client is -still yours to add, as before. - -Every option is spelled one way in `hot.client` and in the entry query alike, -apart from the six names kept working for a release after `apply` and `connect` -replaced them. One option, `apply`, can also be set for a single page through a -url parameter (`?webpack-dev-middleware-apply=nothing`); the rest are the -middleware's and the entry's to set. The two second spellings the query had -picked up from webpack-dev-server — `webSocketURL` for `path` and `live-reload` -for `liveReload` — are gone. Neither alias was ever released. diff --git a/.changeset/client-postmessage-and-reload-guards.md b/.changeset/client-postmessage-and-reload-guards.md deleted file mode 100644 index 410d0ce2a..000000000 --- a/.changeset/client-postmessage-and-reload-guards.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -Post build events to the page the way webpack-dev-server's client does — `webpackInvalid`, `webpackProgress`, `webpackOk`, `webpackStillOk`, `webpackWarnings`, `webpackErrors`, `webpackClose` and `webpackHotUpdate` — so a plugin or a framework's dev tooling can follow a build without reaching into the client diff --git a/.changeset/client-transports.md b/.changeset/client-transports.md deleted file mode 100644 index b35b662ae..000000000 --- a/.changeset/client-transports.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -Carry the browser runtime's events over a WebSocket with the client `transport=ws` option, or over a transport of your own, and reuse the built-in two from `webpack-dev-middleware/client/sse` and `webpack-dev-middleware/client/ws` diff --git a/.changeset/client-type-declarations.md b/.changeset/client-type-declarations.md deleted file mode 100644 index a642cbac8..000000000 --- a/.changeset/client-type-declarations.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Ship type declarations for the `./client`, `./client/sse`, `./client/ws`, `./client/indicator` and `./client/overlay` exports, and mark the client as the ES modules it has always been. A TypeScript consumer importing one of them got `any` — or, under `node16`/`nodenext` resolution, the module namespace instead of the default export, because the files are ES modules inside a CommonJS package with nothing saying so diff --git a/.changeset/connect-object-from-node.md b/.changeset/connect-object-from-node.md deleted file mode 100644 index 2bb2ddb64..000000000 --- a/.changeset/connect-object-from-node.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Fixed `hot.client.connect` set as an object on the middleware being dropped. The browser options travel to the runtime as its entry's query, where every value but `overlay` went through `String(value)` — so `{ retries: 3, timeout: 5000 }` arrived as the text `"[object Object]"`, which the client failed to parse and then read as the boolean `true`. Both fields were lost without a warning, and the connection used the defaults. Any value that is an object is now serialized as JSON, which is the shape the client parses it out of, so an option that grows an object form later needs no change here. diff --git a/.changeset/connected-log-both-transports.md b/.changeset/connected-log-both-transports.md deleted file mode 100644 index d298e8e86..000000000 --- a/.changeset/connected-log-both-transports.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Say `connected` whichever transport the client used, rather than only Server-Sent Events diff --git a/.changeset/deprecate-hot-progress.md b/.changeset/deprecate-hot-progress.md deleted file mode 100644 index 932bd297a..000000000 --- a/.changeset/deprecate-hot-progress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Deprecated the `hot.progress` option; it will be removed in the next major release and keeps working until then. It applied `ProgressPlugin` to 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 to [`publish`](https://github.com/webpack/webpack-dev-middleware#publishpayload), 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. diff --git a/.changeset/feat-problem-formatter.md b/.changeset/feat-problem-formatter.md deleted file mode 100644 index 783247970..000000000 --- a/.changeset/feat-problem-formatter.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -The overlay now takes one of webpack's errors or warnings as it comes, not -only a formatted string: - -```js -import { showProblems } from "webpack-dev-middleware/client/overlay"; - -showProblems("errors", stats.errors, "build"); -``` - -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/problem` now, and -`formatProblem` is there for a console as well as an overlay: - -```js -import { formatProblem } from "webpack-dev-middleware/client/problem"; - -const { header, body } = formatProblem("error", error); -// "ERROR in ./src/app.js 3:0", "Module parse failed: ..." -``` - -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 `file` is used when webpack sets one. diff --git a/.changeset/fix-empty-overlay-and-stuck-indicator.md b/.changeset/fix-empty-overlay-and-stuck-indicator.md deleted file mode 100644 index 6681beec1..000000000 --- a/.changeset/fix-empty-overlay-and-stuck-indicator.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Fixed an empty overlay that covered the page and could only be dismissed by -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 the -overlay 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. diff --git a/.changeset/fix-overlay-file-references.md b/.changeset/fix-overlay-file-references.md deleted file mode 100644 index 9c06f759c..000000000 --- a/.changeset/fix-overlay-file-references.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -The overlay recognizes a file reference in every shape a stack frame carries -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 a -`file://` url are all clickable now; before, only `./` and `../` were, so a -runtime 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.js` -is 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. diff --git a/.changeset/group-cache-and-mime.md b/.changeset/group-cache-and-mime.md deleted file mode 100644 index d0cc19134..000000000 --- a/.changeset/group-cache-and-mime.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -Grouped six flat options into `cache` and `mime`: - -| before | now | -| ----------------- | -------------------- | -| `etag` | `cache.etag` | -| `lastModified` | `cache.lastModified` | -| `cacheControl` | `cache.control` | -| `cacheImmutable` | `cache.immutable` | -| `mimeTypes` | `mime.types` | -| `mimeTypeDefault` | `mime.default` | - -Four 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. `cacheControl` and `cacheImmutable` also 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.options` for anything reading them. - -`instance.context.options` is 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 the `mime.types` table 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.control` in its object form, and `headers`, behave the same way here as before. diff --git a/.changeset/hot-client-apply-and-connect.md b/.changeset/hot-client-apply-and-connect.md deleted file mode 100644 index 7c54341d9..000000000 --- a/.changeset/hot-client-apply-and-connect.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -Two unions on the browser options, replacing six with two. - -`hot.client.apply` replaces `hot`, `liveReload` and `reload`. Only four of their eight combinations ever differed — `liveReload` was read only when HMR was off, `reload` only when it was on — so the three booleans were one decision written three ways: - -| before | now | -| ------------------------------- | ------------------- | -| `hot: true, reload: true` | `apply: "hmr"` | -| `hot: true, reload: false` | `apply: "hmr-only"` | -| `hot: false, liveReload: true` | `apply: "reload"` | -| `hot: false, liveReload: false` | `apply: "nothing"` | - -The page-url parameter follows it: `?webpack-dev-middleware-apply=nothing` in place of `-hot=false` and `-liveReload=false`, with `=false` still accepted as `nothing`. It is more expressive than before, since a page can now ask for a mode rather than only turn something off. - -`hot.client.connect` replaces `autoConnect`, `reconnect` and `timeout`. `false` does not connect on load; an object carries `retries` and `timeout`: - -```js -middleware(compiler, { - hot: { client: { connect: { retries: 3, timeout: 5000 } } }, -}); -``` - -All six old names keep working. Each warns, in node when it is set on `hot.client` and in the browser when it arrives on the query — a hand-written entry has only the second channel — and the option that replaced it wins when both are given, so a migration that sets the new name and leaves the old one behind is not silently ignored. They are removed in the next major release. diff --git a/.changeset/hot-client-options.md b/.changeset/hot-client-options.md deleted file mode 100644 index 9583561c6..000000000 --- a/.changeset/hot-client-options.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -Added `hot.client`, so the browser runtime's options are set on the middleware -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`, `autoConnect` and `dynamicPublicPath` are -accepted, and `overlay` takes the same object (filter functions included) as the -query does. `transport`, `path` and `name` are not accepted: the middleware -knows those and sets them itself, so the runtime cannot be pointed somewhere the -server is not listening. diff --git a/.changeset/hot-connect-request.md b/.changeset/hot-connect-request.md deleted file mode 100644 index 220a993b0..000000000 --- a/.changeset/hot-connect-request.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -Added `handleUpgrade(req, socket, head)` and `onConnect(fn)`, so a server can -decide for itself who may listen to the hot endpoint. `handleUpgrade` answers -one WebSocket upgrade for a server that keeps its own `upgrade` listener, -instead of handing the whole server over with `attach`, and says whether the -request was the endpoint's. `onConnect` is called with each client and the -request it joined with, before anything is published to it, so a client closed -there is sent nothing at all. - -`onConnect` is now given the request in both transports; it used to be dropped, -which left nothing to judge a client by. diff --git a/.changeset/hot-cors.md b/.changeset/hot-cors.md deleted file mode 100644 index 66cbc3255..000000000 --- a/.changeset/hot-cors.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -Added `hot.cors`, which says which origins may reach the hot endpoint from a page on another one, over either transport. - -The Server-Sent Events endpoint answers every request with `Access-Control-Allow-Origin: *`, inherited from `webpack-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.cors` is how to narrow it: an origin, a list of origins and patterns, a regular expression, a predicate, `{ origin }` as Vite and `expressjs/cors` are configured, `false` for nothing but the endpoint's own origin, or `true` for every one of them. An allowed origin is echoed back with `Vary: Origin` rather 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 (`localhost` and anything under it, `127.0.0.1`, `[::1]`, any port, either scheme), the same set and the same reasoning as Vite's `server.cors` default. Set `cors` to 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 `Origin` and pays no attention to what comes back — so there the upgrade is refused with `403` before the handshake completes, through both `attach(server)` and `handleUpgrade(req, socket, head)`. A request carrying no `Origin` at all, and one whose `Origin` is 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. diff --git a/.changeset/hot-token.md b/.changeset/hot-token.md deleted file mode 100644 index ac170a599..000000000 --- a/.changeset/hot-token.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -Added `hot.token`: a secret the injected client carries and the hot endpoint -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.cors` is answered by `Origin`, and that is its weakness. A browser omits -`Origin` and the whole `Sec-Fetch-*` family when the destination is not -potentially trustworthy — plain `http` to anything but `localhost`, which -`host: "0.0.0.0"` gives you. webpack-dev-server shipped two fixes built on -those 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 `true` in the next major release. A -token only reaches the browser on the entry the middleware adds, and `inject` -being on does not mean an entry was added: it is skipped when every entry point -already pulls the client in, when `hot.transport` is a function, and for a -non-web target. Requiring one by default would turn each of those into a `403` -on every client. Set `token: true` to turn it on, and if you do so where no -client 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: false` turns the requirement off — the -token 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 `http` to a non-localhost -address nothing stops that unless your server sends -`Cross-Origin-Resource-Policy`. This hardens every case where the bundle is not -readable, and is defence in depth in the case where it is. diff --git a/.changeset/hot-transport-ws.md b/.changeset/hot-transport-ws.md deleted file mode 100644 index 9b7d908cc..000000000 --- a/.changeset/hot-transport-ws.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -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 diff --git a/.changeset/inject-hot-client.md b/.changeset/inject-hot-client.md deleted file mode 100644 index fa6f63214..000000000 --- a/.changeset/inject-hot-client.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -`hot` now puts the client runtime and `HotModuleReplacementPlugin` into 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, and `hot.inject: false` turns it off for anyone who would rather wire it themselves diff --git a/.changeset/instance-publish.md b/.changeset/instance-publish.md deleted file mode 100644 index 3dfb2499a..000000000 --- a/.changeset/instance-publish.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -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; `ProgressPlugin` is 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 when `hot` is off. diff --git a/.changeset/mime-db-directly.md b/.changeset/mime-db-directly.md deleted file mode 100644 index b08dcfbcd..000000000 --- a/.changeset/mime-db-directly.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -Extensions resolve through `mime-db` directly, rather than through -`mime-types`. `mime-db` is the data, and webpack itself already depends on it -and 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-db` resolves to -the byte-identical media type, `Content-Type` and charset it did before. - -The table is also built per middleware instance now. The `mimeTypes` option -used to be applied by writing into the table `mime-types` exports, which is -one 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-types` inherited whatever a middleware had -registered. 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. diff --git a/.changeset/overlay-focus.md b/.changeset/overlay-focus.md deleted file mode 100644 index a143a467d..000000000 --- a/.changeset/overlay-focus.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -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 diff --git a/.changeset/overlay-id-option.md b/.changeset/overlay-id-option.md deleted file mode 100644 index fbcb01cbf..000000000 --- a/.changeset/overlay-id-option.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -`overlay.id` sets 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 `-card`, and the default is unchanged diff --git a/.changeset/perf-precompiled-options-schema.md b/.changeset/perf-precompiled-options-schema.md deleted file mode 100644 index 5f0a205b3..000000000 --- a/.changeset/perf-precompiled-options-schema.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Validate options with a precompiled schema to cut ~155ms from startup. diff --git a/.changeset/progress-linear.md b/.changeset/progress-linear.md deleted file mode 100644 index 81f7a5181..000000000 --- a/.changeset/progress-linear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -`progress` now takes `"circular"` and `"linear"` as well as a boolean, the same values as webpack-dev-server's `client.progress`. `"circular"` is the badge this package has always shown, and what `true` still selects; `"linear"` renders a thin bar across the top of the viewport diff --git a/.changeset/publish-payload-extras.md b/.changeset/publish-payload-extras.md deleted file mode 100644 index 604f6cde8..000000000 --- a/.changeset/publish-payload-extras.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Typed `publish(payload)` as taking a payload of your own, rather than only the keys this middleware itself publishes. `action` is still required — it is what a client tells one payload from another by — and everything beyond it now travels untouched. - -`publish` exists for what a server measures that the middleware does not, and such a payload carries whatever that server has to say. webpack-dev-server's own `ProgressPlugin` reports which plugin a tick came from, which is more than `percent` and `message`; a `subscribe` handler is what reads it. Typing it shut made the shape of a published payload this middleware's to approve, which is the opposite of what the method is for — and it was a type error at the call site for a key that worked perfectly well at runtime. diff --git a/.changeset/readme-anchors.md b/.changeset/readme-anchors.md deleted file mode 100644 index 741f6676d..000000000 --- a/.changeset/readme-anchors.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Fixed two broken links in the README: the `etag` row of the options table and the two references to the `attach` method pointed at headings that do not exist. diff --git a/.changeset/reconnect-and-timeout-per-transport.md b/.changeset/reconnect-and-timeout-per-transport.md deleted file mode 100644 index 420a32be3..000000000 --- a/.changeset/reconnect-and-timeout-per-transport.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -`hot.client.connect.retries` (`hot.client.reconnect` before it was replaced, still accepted) now applies to Server-Sent Events as well. It was overridden to `Infinity` there, 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. - -`hot.client.connect.timeout` (`hot.client.timeout` before it was replaced, still accepted) is 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 for the retry count was 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. diff --git a/.changeset/refactor-inline-ansi-html.md b/.changeset/refactor-inline-ansi-html.md deleted file mode 100644 index caca5f376..000000000 --- a/.changeset/refactor-inline-ansi-html.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Dropped the `ansi-html-community` dependency. The overlay's ANSI-to-HTML -conversion is `client-src/utils/ansi-html.js` now, which is the third of that -package 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: - -- A palette entry of `"transparent"`, which is how the overlay says to leave - the page's own colour alone, became `color:#transparent`. That is not a - colour, so the reset worked only because browsers drop an invalid - declaration, and the inverse sequence did nothing at all. -- A sequence carrying more than one parameter (`\u001b[1;31m`) matched nothing, - so the escape stayed in the output as text for the reader to see. -- `\u001b[m`, which is `\u001b[0m` written short, was left in the output the - same way. -- A closing sequence with nothing open emitted an unmatched ``. The - highlighters wrap their own spans around this output, so a stray close could - end one of theirs early. -- Every closing tag was a ``, whatever was open. `\u001b[3m` opens an - ``, so an unclosed italic came out as `x`, and an interleaved - sequence crossed its tags: `x`. Each open element now - carries 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. diff --git a/.changeset/reload-guards.md b/.changeset/reload-guards.md deleted file mode 100644 index 02182d46e..000000000 --- a/.changeset/reload-guards.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -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:blank` iframe diff --git a/.changeset/runtime-error-cause.md b/.changeset/runtime-error-cause.md deleted file mode 100644 index ae4315e16..000000000 --- a/.changeset/runtime-error-cause.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Give an `overlay.runtimeErrors` filter the rejected value through `error.cause`, so a rejection carrying a plain object rather than an `Error` can still be judged on what it carries diff --git a/.changeset/runtime-error-survives-build.md b/.changeset/runtime-error-survives-build.md deleted file mode 100644 index 2b21f7d92..000000000 --- a/.changeset/runtime-error-survives-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -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 diff --git a/.changeset/simpler-transport-contract.md b/.changeset/simpler-transport-contract.md deleted file mode 100644 index 3f6a5c893..000000000 --- a/.changeset/simpler-transport-contract.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": minor ---- - -A custom `hot.transport` now needs only four methods — `onConnect`, `publish`, `publishTo` and `close`. `handler` and `hasClients` became optional: without a `handler` a request on the endpoint's path is answered `426 Upgrade Required`, and without `hasClients` a payload is built and the transport decides for itself in `publish`. A transport that implements all six keeps working unchanged diff --git a/.changeset/transport-parity.md b/.changeset/transport-parity.md deleted file mode 100644 index d15e56568..000000000 --- a/.changeset/transport-parity.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Make the two client transports behave alike: neither logs a raw connection error, and neither reports anything after being closed diff --git a/.changeset/traversal-remainder-guard.md b/.changeset/traversal-remainder-guard.md deleted file mode 100644 index 203a76bd7..000000000 --- a/.changeset/traversal-remainder-guard.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"webpack-dev-middleware": patch ---- - -Hardened the path-traversal guards in `getFilenameFromUrl`. The remainder left after the `publicPath` prefix is stripped is now checked for `..` on its own, before it is joined onto the output root. - -The existing `..` guard tests the whole request path _normalized_, which catches far less than it appears to: `/public/../secret` normalizes to `secret` with no `..` segment left to match, and `/assets../secret` never had a `..` segment to begin with — it is a sibling sharing the prefix, which a `publicPath` with 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index c2908a774..821d95975 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,356 @@ # Changelog +## 8.4.0 + +### Minor Changes + +- The hot client now runs in a web worker. It connected only where there was a (by [@alexander-akait](https://github.com/alexander-akait) in [#2430](https://github.com/webpack/webpack-dev-middleware/pull/2430)) + `window`, which a worker has none of — but it has `EventSource`, `WebSocket` + and webpack's runtime, which is all an update needs, so the client reads `self` + instead. 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.reload` in a worker — so when an update cannot be applied the client + says so once and leaves the page that started the worker to reload it. + + `target: "webworker"` compilations get a client from `hot` like any other + browser 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](https://github.com/alexander-akait) in [#2433](https://github.com/webpack/webpack-dev-middleware/pull/2433)) + (default `true`) says whether a build is applied as an update, and `liveReload` + (default `true`) reloads the page on a build that changed something when it is + not — so a project with no `HotModuleReplacementPlugin` still sees its changes. + A build that changed nothing is left alone either way. + + Added a `reload` action, for a change no compilation knows about: publish + `{ action: "reload" }` and every page loads itself again, whatever `hot` and + `liveReload` are set to. It does what the `subscribe()` example in the README + used to have you write by hand. + + Added `urlPrefix` (default `"webpack-dev-middleware"`), which names the page-url + parameter that overrides what a build does to a single page — + `?webpack-dev-middleware-apply=nothing`. + + `live-reload` used to be accepted as another spelling of `reload`. The two are + different — `reload` is the fallback for an update that was tried and could not + be applied — so the one that reloads on a build is its own option, `liveReload`. + `live-reload` is 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](https://github.com/alexander-akait) in [#2436](https://github.com/webpack/webpack-dev-middleware/pull/2436)) + + `hot.client` and the injected entry's query are now one option set rather than + two overlapping ones. `hot`, `liveReload` and `urlPrefix` were readable from the + query alone and can now be set in node: + + ```js + app.use( + middleware(compiler, { + hot: { client: { hot: false, liveReload: true, urlPrefix: "my-server" } }, + }), + ); + ``` + + `transport`, `path` and `name` go the other way: they were the middleware's to + set, and can now be overridden, which is what a page reaching the endpoint + through a proxy or on another origin needs. + + ```js + app.use( + middleware(compiler, { + hot: { client: { path: "wss://dev.example.com/__webpack_hmr" } }, + }), + ); + ``` + + The middleware's own values — the resolved `hot.transport`, the resolved + `hot.path` and the compilation's name — remain the defaults, so nothing changes + for anyone not setting them. + + A `hot.transport` of your own now gets a client too, when `hot.client.transport` + says which of the two built-in protocols yours carries. Without it the client is + still yours to add, as before. + + Every option is spelled one way in `hot.client` and in the entry query alike, + apart from the six names kept working for a release after `apply` and `connect` + replaced them. One option, `apply`, can also be set for a single page through a + url parameter (`?webpack-dev-middleware-apply=nothing`); the rest are the + middleware's and the entry's to set. The two second spellings the query had + picked up from webpack-dev-server — `webSocketURL` for `path` and `live-reload` + for `liveReload` — are gone. 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`, `webpackClose` and `webpackHotUpdate` — so a plugin or a framework's dev tooling can follow a build without reaching into the client (by [@alexander-akait](https://github.com/alexander-akait) in [#2425](https://github.com/webpack/webpack-dev-middleware/pull/2425)) + +- Carry the browser runtime's events over a WebSocket with the client `transport=ws` option, or over a transport of your own, and reuse the built-in two from `webpack-dev-middleware/client/sse` and `webpack-dev-middleware/client/ws` (by [@alexander-akait](https://github.com/alexander-akait) in [#2421](https://github.com/webpack/webpack-dev-middleware/pull/2421)) + +- The overlay now takes one of webpack's errors or warnings as it comes, not (by [@alexander-akait](https://github.com/alexander-akait) in [#2438](https://github.com/webpack/webpack-dev-middleware/pull/2438)) + only a formatted string: + + ```js + import { showProblems } from "webpack-dev-middleware/client/overlay"; + + showProblems("errors", stats.errors, "build"); + ``` + + 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/problem` now, and + `formatProblem` is there for a console as well as an overlay: + + ```js + import { formatProblem } from "webpack-dev-middleware/client/problem"; + + const { header, body } = formatProblem("error", error); + // "ERROR in ./src/app.js 3:0", "Module parse failed: ..." + ``` + + 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 `file` is used when webpack sets one. + +- Grouped six flat options into `cache` and `mime`: (by [@alexander-akait](https://github.com/alexander-akait) in [#2455](https://github.com/webpack/webpack-dev-middleware/pull/2455)) + + | before | now | + | ----------------- | -------------------- | + | `etag` | `cache.etag` | + | `lastModified` | `cache.lastModified` | + | `cacheControl` | `cache.control` | + | `cacheImmutable` | `cache.immutable` | + | `mimeTypes` | `mime.types` | + | `mimeTypeDefault` | `mime.default` | + + Four 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. `cacheControl` and `cacheImmutable` also 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.options` for anything reading them. + + `instance.context.options` is 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 the `mime.types` table 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.control` in its object form, and `headers`, behave the same way here as before. + +- Two unions on the browser options, replacing six with two. (by [@alexander-akait](https://github.com/alexander-akait) in [#2458](https://github.com/webpack/webpack-dev-middleware/pull/2458)) + + `hot.client.apply` replaces `hot`, `liveReload` and `reload`. Only four of their eight combinations ever differed — `liveReload` was read only when HMR was off, `reload` only when it was on — so the three booleans were one decision written three ways: + + | before | now | + | ------------------------------- | ------------------- | + | `hot: true, reload: true` | `apply: "hmr"` | + | `hot: true, reload: false` | `apply: "hmr-only"` | + | `hot: false, liveReload: true` | `apply: "reload"` | + | `hot: false, liveReload: false` | `apply: "nothing"` | + + The page-url parameter follows it: `?webpack-dev-middleware-apply=nothing` in place of `-hot=false` and `-liveReload=false`, with `=false` still accepted as `nothing`. It is more expressive than before, since a page can now ask for a mode rather than only turn something off. + + `hot.client.connect` replaces `autoConnect`, `reconnect` and `timeout`. `false` does not connect on load; an object carries `retries` and `timeout`: + + ```js + middleware(compiler, { + hot: { client: { connect: { retries: 3, timeout: 5000 } } }, + }); + ``` + + All six old names keep working. Each warns, in node when it is set on `hot.client` and in the browser when it arrives on the query — a hand-written entry has only the second channel — and the option that replaced it wins when both are given, so a migration that sets the new name and leaves the old one behind is not silently ignored. They are removed in the next major release. + +- Added `hot.client`, so the browser runtime's options are set on the middleware (by [@alexander-akait](https://github.com/alexander-akait) in [#2432](https://github.com/webpack/webpack-dev-middleware/pull/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`, `autoConnect` and `dynamicPublicPath` are + accepted, and `overlay` takes the same object (filter functions included) as the + query does. `transport`, `path` and `name` are not accepted: the middleware + knows those and sets them itself, so the runtime cannot be pointed somewhere the + server is not listening. + +- Added `handleUpgrade(req, socket, head)` and `onConnect(fn)`, so a server can (by [@alexander-akait](https://github.com/alexander-akait) in [#2431](https://github.com/webpack/webpack-dev-middleware/pull/2431)) + decide for itself who may listen to the hot endpoint. `handleUpgrade` answers + one WebSocket upgrade for a server that keeps its own `upgrade` listener, + instead of handing the whole server over with `attach`, and says whether the + request was the endpoint's. `onConnect` is called with each client and the + request it joined with, before anything is published to it, so a client closed + there is sent nothing at all. + + `onConnect` is 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](https://github.com/alexander-akait) in [#2444](https://github.com/webpack/webpack-dev-middleware/pull/2444)) + + The Server-Sent Events endpoint answers every request with `Access-Control-Allow-Origin: *`, inherited from `webpack-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.cors` is how to narrow it: an origin, a list of origins and patterns, a regular expression, a predicate, `{ origin }` as Vite and `expressjs/cors` are configured, `false` for nothing but the endpoint's own origin, or `true` for every one of them. An allowed origin is echoed back with `Vary: Origin` rather 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 (`localhost` and anything under it, `127.0.0.1`, `[::1]`, any port, either scheme), the same set and the same reasoning as Vite's `server.cors` default. Set `cors` to 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 `Origin` and pays no attention to what comes back — so there the upgrade is refused with `403` before the handshake completes, through both `attach(server)` and `handleUpgrade(req, socket, head)`. A request carrying no `Origin` at all, and one whose `Origin` is 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](https://github.com/alexander-akait) in [#2451](https://github.com/webpack/webpack-dev-middleware/pull/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.cors` is answered by `Origin`, and that is its weakness. A browser omits + `Origin` and the whole `Sec-Fetch-*` family when the destination is not + potentially trustworthy — plain `http` to anything but `localhost`, which + `host: "0.0.0.0"` gives you. webpack-dev-server shipped two fixes built on + those 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 `true` in the next major release. A + token only reaches the browser on the entry the middleware adds, and `inject` + being on does not mean an entry was added: it is skipped when every entry point + already pulls the client in, when `hot.transport` is a function, and for a + non-web target. Requiring one by default would turn each of those into a `403` + on every client. Set `token: true` to turn it on, and if you do so where no + client 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: false` turns the requirement off — the + token 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 `http` to a non-localhost + address nothing stops that unless your server sends + `Cross-Origin-Resource-Policy`. This hardens every case where the bundle is not + readable, 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](https://github.com/alexander-akait) in [#2420](https://github.com/webpack/webpack-dev-middleware/pull/2420)) + +- `hot` now puts the client runtime and `HotModuleReplacementPlugin` into 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, and `hot.inject: false` turns it off for anyone who would rather wire it themselves (by [@alexander-akait](https://github.com/alexander-akait) in [#2430](https://github.com/webpack/webpack-dev-middleware/pull/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; `ProgressPlugin` is 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 when `hot` is off. (by [@alexander-akait](https://github.com/alexander-akait) in [#2451](https://github.com/webpack/webpack-dev-middleware/pull/2451)) + +- Extensions resolve through `mime-db` directly, rather than through (by [@alexander-akait](https://github.com/alexander-akait) in [#2449](https://github.com/webpack/webpack-dev-middleware/pull/2449)) + `mime-types`. `mime-db` is the data, and webpack itself already depends on it + and 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-db` resolves to + the byte-identical media type, `Content-Type` and charset it did before. + + The table is also built per middleware instance now. The `mimeTypes` option + used to be applied by writing into the table `mime-types` exports, which is + one 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-types` inherited whatever a middleware had + registered. 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.id` sets 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 `-card`, and the default is unchanged (by [@alexander-akait](https://github.com/alexander-akait) in [#2429](https://github.com/webpack/webpack-dev-middleware/pull/2429)) + +- `progress` now takes `"circular"` and `"linear"` as well as a boolean, the same values as webpack-dev-server's `client.progress`. `"circular"` is the badge this package has always shown, and what `true` still selects; `"linear"` renders a thin bar across the top of the viewport (by [@alexander-akait](https://github.com/alexander-akait) in [#2426](https://github.com/webpack/webpack-dev-middleware/pull/2426)) + +- A custom `hot.transport` now needs only four methods — `onConnect`, `publish`, `publishTo` and `close`. `handler` and `hasClients` became optional: without a `handler` a request on the endpoint's path is answered `426 Upgrade Required`, and without `hasClients` a payload is built and the transport decides for itself in `publish`. A transport that implements all six keeps working unchanged (by [@alexander-akait](https://github.com/alexander-akait) in [#2427](https://github.com/webpack/webpack-dev-middleware/pull/2427)) + +### Patch Changes + +- Fixed `autoConnect` on 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](https://github.com/alexander-akait) in [#2456](https://github.com/webpack/webpack-dev-middleware/pull/2456)) + +- Bound the internal url and `Range` header caches, which grew for the life of the process and were never released, even by `close()`. (by [@alexander-akait](https://github.com/alexander-akait) in [#2405](https://github.com/webpack/webpack-dev-middleware/pull/2405)) + +- Ship type declarations for the `./client`, `./client/sse`, `./client/ws`, `./client/indicator` and `./client/overlay` exports, and mark the client as the ES modules it has always been. A TypeScript consumer importing one of them got `any` — or, under `node16`/`nodenext` resolution, 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](https://github.com/alexander-akait) in [#2428](https://github.com/webpack/webpack-dev-middleware/pull/2428)) + +- Fixed `hot.client.connect` set as an object on the middleware being dropped. The browser options travel to the runtime as its entry's query, where every value but `overlay` went through `String(value)` — so `{ retries: 3, timeout: 5000 }` arrived as the text `"[object Object]"`, which the client failed to parse and then read as the boolean `true`. Both fields were lost without a warning, and the connection used the defaults. Any value that is an object is now serialized as JSON, which is the shape the client parses it out of, so an option that grows an object form later needs no change here. (by [@alexander-akait](https://github.com/alexander-akait) in [#2458](https://github.com/webpack/webpack-dev-middleware/pull/2458)) + +- Say `connected` whichever transport the client used, rather than only Server-Sent Events (by [@alexander-akait](https://github.com/alexander-akait) in [#2423](https://github.com/webpack/webpack-dev-middleware/pull/2423)) + +- Deprecated the `hot.progress` option; it will be removed in the next major release and keeps working until then. It applied `ProgressPlugin` to 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 to [`publish`](https://github.com/webpack/webpack-dev-middleware#publishpayload), 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](https://github.com/alexander-akait) in [#2451](https://github.com/webpack/webpack-dev-middleware/pull/2451)) + +- Fixed an empty overlay that covered the page and could only be dismissed by (by [@alexander-akait](https://github.com/alexander-akait) in [#2437](https://github.com/webpack/webpack-dev-middleware/pull/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 the + overlay 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](https://github.com/alexander-akait) in [#2439](https://github.com/webpack/webpack-dev-middleware/pull/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 a + `file://` url are all clickable now; before, only `./` and `../` were, so a + runtime 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.js` + is 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](https://github.com/alexander-akait) in [#2423](https://github.com/webpack/webpack-dev-middleware/pull/2423)) + +- Validate options with a precompiled schema to cut ~155ms from startup. (by [@alexander-akait](https://github.com/alexander-akait) in [#2413](https://github.com/webpack/webpack-dev-middleware/pull/2413)) + +- Typed `publish(payload)` as taking a payload of your own, rather than only the keys this middleware itself publishes. `action` is still required — it is what a client tells one payload from another by — and everything beyond it now travels untouched. (by [@alexander-akait](https://github.com/alexander-akait) in [#2460](https://github.com/webpack/webpack-dev-middleware/pull/2460)) + + `publish` exists for what a server measures that the middleware does not, and such a payload carries whatever that server has to say. webpack-dev-server's own `ProgressPlugin` reports which plugin a tick came from, which is more than `percent` and `message`; a `subscribe` handler is what reads it. Typing it shut made the shape of a published payload this middleware's to approve, which is the opposite of what the method is for — and it was a type error at the call site for a key that worked perfectly well at runtime. + +- Fixed two broken links in the README: the `etag` row of the options table and the two references to the `attach` method pointed at headings that do not exist. (by [@alexander-akait](https://github.com/alexander-akait) in [#2451](https://github.com/webpack/webpack-dev-middleware/pull/2451)) + +- `hot.client.connect.retries` (`hot.client.reconnect` before it was replaced, still accepted) now applies to Server-Sent Events as well. It was overridden to `Infinity` there, 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](https://github.com/alexander-akait) in [#2457](https://github.com/webpack/webpack-dev-middleware/pull/2457)) + + `hot.client.connect.timeout` (`hot.client.timeout` before it was replaced, still accepted) is 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 for the retry count was 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-community` dependency. The overlay's ANSI-to-HTML (by [@alexander-akait](https://github.com/alexander-akait) in [#2442](https://github.com/webpack/webpack-dev-middleware/pull/2442)) + conversion is `client-src/utils/ansi-html.js` now, which is the third of that + package 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: + + - A palette entry of `"transparent"`, which is how the overlay says to leave + the page's own colour alone, became `color:#transparent`. That is not a + colour, so the reset worked only because browsers drop an invalid + declaration, and the inverse sequence did nothing at all. + - A sequence carrying more than one parameter (`\u001b[1;31m`) matched nothing, + so the escape stayed in the output as text for the reader to see. + - `\u001b[m`, which is `\u001b[0m` written short, was left in the output the + same way. + - A closing sequence with nothing open emitted an unmatched ``. The + highlighters wrap their own spans around this output, so a stray close could + end one of theirs early. + - Every closing tag was a ``, whatever was open. `\u001b[3m` opens an + ``, so an unclosed italic came out as `x`, and an interleaved + sequence crossed its tags: `x`. Each open element now + carries 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:blank` iframe (by [@alexander-akait](https://github.com/alexander-akait) in [#2425](https://github.com/webpack/webpack-dev-middleware/pull/2425)) + +- Give an `overlay.runtimeErrors` filter the rejected value through `error.cause`, so a rejection carrying a plain object rather than an `Error` can still be judged on what it carries (by [@alexander-akait](https://github.com/alexander-akait) in [#2425](https://github.com/webpack/webpack-dev-middleware/pull/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](https://github.com/alexander-akait) in [#2424](https://github.com/webpack/webpack-dev-middleware/pull/2424)) + +- Make the two client transports behave alike: neither logs a raw connection error, and neither reports anything after being closed (by [@alexander-akait](https://github.com/alexander-akait) in [#2423](https://github.com/webpack/webpack-dev-middleware/pull/2423)) + +- Hardened the path-traversal guards in `getFilenameFromUrl`. The remainder left after the `publicPath` prefix is stripped is now checked for `..` on its own, before it is joined onto the output root. (by [@alexander-akait](https://github.com/alexander-akait) in [#2445](https://github.com/webpack/webpack-dev-middleware/pull/2445)) + + The existing `..` guard tests the whole request path _normalized_, which catches far less than it appears to: `/public/../secret` normalizes to `secret` with no `..` segment left to match, and `/assets../secret` never had a `..` segment to begin with — it is a sibling sharing the prefix, which a `publicPath` with 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. + ## 8.3.0 ### Minor Changes diff --git a/package.json b/package.json index 25e469b9d..bf593fa08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webpack-dev-middleware", - "version": "8.3.0", + "version": "8.4.0", "description": "A development middleware for webpack", "keywords": [ "webpack",