Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/hot-client-apply-and-connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

pr: #2458

`hot.client.apply` (`"hmr"`, `"hmr-only"`, `"reload"` or `"nothing"`) says what a build does to the page, so a project without HMR still reloads on a change; a single page can choose its own mode with `?webpack-dev-middleware-apply=`, and publishing `{ action: "reload" }` reloads every page. `hot.client.connect` (`false`, or `{ retries, timeout }`) controls connecting and reconnecting, with `retries` honoured on both transports. The `hot`, `liveReload`, `reload`, `autoConnect`, `reconnect` and `timeout` options they replace still work with a deprecation warning until the next major release.
`hot.client.apply` (`"hmr"`, `"hmr-only"`, `"reload"` or `"nothing"`) says what a build does to the page, so a project without HMR still reloads on a change; a single page can choose its own mode with `?webpack-dev-middleware-apply=`, and publishing `{ action: "reload" }` reloads every page. `hot.client.connect` (`false`, or `{ retries, timeout }`) controls connecting and reconnecting, with `retries` honoured on both transports. The `hot`, `liveReload`, `reload`, `autoConnect`, `reconnect` and `timeout` query parameters they replace are still read until the next major release, with a deprecation warning for this package's own three.
2 changes: 1 addition & 1 deletion .changeset/hot-client-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

pr: #2436

Added `hot.client`, which sets the browser runtime's options on the middleware under the same names the entry query takes, so a configuration no longer needs a hand-written query string. `path` accepts a url or its parts (`{ port: 8080 }`) and resolves the rest in the page, and `logging` accepts `{ level, name }` so an embedding package can label the console with its own name.
Added `hot.client`, which sets the browser runtime's options on the middleware under the same names the entry query takes, so a configuration no longer needs a hand-written query string. `url` accepts a url or its parts (`{ port: 8080 }`) and resolves the rest in the page, `pageParamPrefix` renames the per-page `?webpack-dev-middleware-apply=` parameter, and `logging` accepts `{ level, name }` so an embedding package can label the console with its own name.
49 changes: 19 additions & 30 deletions README.md

Large diffs are not rendered by default.

27 changes: 17 additions & 10 deletions client-src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ import withToken from "./utils/with-token.js";
* @property {ApplyMode} apply what a build does to the page
* @property {boolean | ConnectOptions} connect whether to connect when the entry runs, and how the connection is held open
* @property {boolean | OverlayOptions} overlay enable the in-page error overlay (same value shape as webpack-dev-server's `client.overlay`)
* @property {string} urlPrefix prefix of the page-url parameters that override `apply` for one page
* @property {string} pageParamPrefix prefix of the page-url parameters that override `apply` for one page
* @property {LogLevel} logging logger level
* @property {string=} loggerName what to label messages with in the console
* @property {string} name limit updates to this compilation name
Expand All @@ -87,7 +87,7 @@ const options = {
apply: "hmr",
connect: true,
overlay: true,
urlPrefix: "webpack-dev-middleware",
pageParamPrefix: "webpack-dev-middleware",
logging: "info",
loggerName: "",
name: "",
Expand Down Expand Up @@ -301,8 +301,8 @@ function readApplyMode(value) {
* tab opts out of what the rest of the project is configured for —
* `?webpack-dev-middleware-apply=nothing` to stop a page reloading under you
* while you work in it, for instance, or `=false` for the same thing.
* `urlPrefix` names them, so a server built on this middleware can name them
* after itself.
* `pageParamPrefix` names them, so a server built on this middleware can name
* them after itself.
*
* The parameter is the option, spelled the one way the option is spelled.
* @param {string} setting which option the page may have something to say about
Expand All @@ -311,9 +311,9 @@ function readApplyMode(value) {
function urlOverride(setting) {
// Parsed rather than searched for as text: `?note=…-apply=false` carries
// the words without being the parameter. The name is compared
// case-insensitively on both sides, so a `urlPrefix` with capitals in it
// works as written.
const wanted = `${options.urlPrefix}-${setting}`.toLowerCase();
// case-insensitively on both sides, so a `pageParamPrefix` with capitals in
// it works as written.
const wanted = `${options.pageParamPrefix}-${setting}`.toLowerCase();
// Nowhere this runs is without a url, but nothing here needs one either: an
// empty query asks for nothing.
const search =
Expand Down Expand Up @@ -417,11 +417,16 @@ function setOverrides(overrides) {
// differ and leaves the rest to be resolved against the page, which is the
// only place the rest is known — behind a proxy, on another host, or on a
// socket listening on a port of its own.
// `url` is what the option is called in node, and wins; `path` is what this
// query called it first, and still reads.
if (overrides.url) {
overrides.path = overrides.url;
}
// The parts of the url as parameters of their own, the way
// webpack-dev-server's query has always carried them — so an entry written
// by hand for that server still connects where it says. Read as a path in
// parts, which resolves what they leave out against the page; a `path` given
// as well says it all and wins.
// parts, which resolves what they leave out against the page; a `url` or
// `path` given as well says it all and wins.
if (!overrides.path) {
/** @type {Record<string, string>} */
const parts = {};
Expand Down Expand Up @@ -514,7 +519,9 @@ function setOverrides(overrides) {
options.apply = mode;
}
}
if (overrides.urlPrefix) options.urlPrefix = overrides.urlPrefix;
if (overrides.pageParamPrefix) {
options.pageParamPrefix = overrides.pageParamPrefix;
}
if (overrides.name) {
options.name = overrides.name;
}
Expand Down
20 changes: 3 additions & 17 deletions src/hot.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,27 +35,20 @@
* here and the injected entry carries it, or write it on the query of a client
* entry of your own.
*
* `transport`, `path` and `name` are the exception only in having a default
* `transport`, `url` and `name` are the exception only in having a default
* the middleware knows — the resolved `hot.transport`, the resolved `hot.path`
* and the compilation's name. Setting one here replaces that, which is what a
* page reaching the endpoint through a proxy or another origin needs.
* @typedef {object} HotClientOptions
* @property {("sse" | "ws" | string)=} transport which transport the runtime speaks, `hot.transport` by default; any other string is a module exporting a client class of your own, used in place of the built-in one
* @property {(string | PathSpec)=} path where the runtime connects, `hot.path` by default; may be an absolute url for an endpoint on another origin, or the parts that differ with the rest resolved in the page
* @property {(string | PathSpec)=} url where the runtime connects, `hot.path` by default: a path, an absolute url for an endpoint on another origin, or the parts that differ with the rest resolved in the page
* @property {string=} name limit the runtime to one compilation's builds, the compilation's own name by default
* @property {string=} token the secret the runtime puts on its connection url, `hot.token` by default
* @property {(boolean | Record<string, EXPECTED_ANY>)=} overlay show build problems and uncaught runtime errors in an overlay
* @property {(boolean | "circular" | "linear")=} progress show an indicator while a rebuild is in progress
* @property {boolean=} hot deprecated, removed in the next major release — use `apply`
* @property {boolean=} liveReload deprecated, removed in the next major release — use `apply`
* @property {boolean=} reload deprecated, removed in the next major release — use `apply`
* @property {("hmr" | "hmr-only" | "reload" | "nothing")=} apply what a build does to the page — apply the update and reload if it cannot be applied, apply it and stop with a message if it cannot, load the page again on any build that changed something, or leave the page alone
* @property {(boolean | { retries?: number, timeout?: number })=} connect whether to connect when the entry runs, and how the connection is held open
* @property {string=} urlPrefix prefix of the page-url parameter that overrides `apply` for a single page
* @property {string=} pageParamPrefix prefix of the page-url parameters that override `apply` for a single page
* @property {(LogLevel | { level?: LogLevel, name?: string })=} logging how much the runtime logs to the browser console, and the name every message is labelled with
* @property {number=} reconnect how many times to reconnect before giving up; unset, Server-Sent Events keep trying for as long as the page is open while a WebSocket gives up after 10
* @property {number=} timeout how long the runtime tolerates silence before reconnecting, in milliseconds — Server-Sent Events only, since a WebSocket's heartbeat is a protocol ping JavaScript cannot see
* @property {boolean=} autoConnect connect as soon as the entry runs
* @property {boolean=} dynamicPublicPath prefix the path with the bundle's public path at runtime
*/

Expand All @@ -64,7 +57,6 @@
* @property {("sse" | "ws" | ClientStreamFactory<EXPECTED_ANY>)=} transport how events reach the clients, Server-Sent Events by default
* @property {string=} path the path the endpoint is served at
* @property {number=} heartbeat heartbeat interval in milliseconds
* @property {HttpServer=} server HTTP server the `"ws"` transport answers upgrades on, when it is already built
* @property {Record<string, EXPECTED_ANY>=} ws options for the `ws` server behind the `"ws"` transport — compression, payload limits, `verifyClient`, or a `port` or a `server` of its own to listen on; `path`, `noServer` and `clientTracking` are the middleware's
* @property {StatsOptions=} statsOptions deprecated, removed in the next major release — webpack stats options used when serializing compilation results
* @property {boolean=} progress publish compilation progress events to the clients
Expand Down Expand Up @@ -578,12 +570,6 @@ function createHot(compiler, userOptions, statsOption) {
}
});

// A WebSocket is upgraded by the HTTP server rather than answered by the
// middleware, so the transport needs the server itself.
if (options.server && eventStream.attach) {
eventStream.attach(options.server);
}

// TODO in the next major release remove `progress` and this warning
if (options.progress) {
logger.warn(
Expand Down
2 changes: 1 addition & 1 deletion src/options.check.js

Large diffs are not rendered by default.

45 changes: 5 additions & 40 deletions src/options.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
}
]
},
"path": {
"description": "Where the runtime connects. Defaults to the resolved `hot.path`; a string may be an absolute url for an endpoint reached on another origin or through a proxy, and an object says only the parts that differ and leaves the rest to be resolved in the page, which is the only place the rest is known.",
"url": {
"description": "Where the runtime connects: a path, an absolute url for an endpoint reached on another origin or through a proxy, or the parts of one that differ, with the rest resolved in the page, which is the only place the rest is known. Defaults to the resolved `hot.path`.",
"anyOf": [
{
"type": "string",
Expand Down Expand Up @@ -77,10 +77,6 @@
"description": "Limit the runtime to one compilation's builds. Defaults to the compilation's own name.",
"type": "string"
},
"token": {
"description": "The secret the runtime puts on its connection url. The middleware sets this to whatever 'hot.token' resolved to, so it only needs setting for a client pointed at another endpoint that requires a different one.",
"type": "string"
},
"overlay": {
"description": "Show build problems and uncaught runtime errors in an overlay.",
"anyOf": [
Expand Down Expand Up @@ -131,8 +127,8 @@
}
]
},
"urlPrefix": {
"description": "Prefix of the page-url parameter that overrides `apply` for a single page.",
"pageParamPrefix": {
"description": "Prefix of the page-url parameters that override `apply` for a single page, as in `?webpack-dev-middleware-hot=false`.",
"type": "string",
"minLength": 1
},
Expand Down Expand Up @@ -161,32 +157,6 @@
"dynamicPublicPath": {
"description": "Prefix the endpoint path with the bundle's public path at runtime.",
"type": "boolean"
},
"hot": {
"description": "Apply a build through Hot Module Replacement. Deprecated, and removed in the next major release: use `apply`.",
"type": "boolean"
},
"liveReload": {
"description": "Reload the page on a build that changed something, when `hot` is off. Deprecated, and removed in the next major release: use `apply`.",
"type": "boolean"
},
"reload": {
"description": "Reload the page when an update cannot be applied. Deprecated, and removed in the next major release: use `apply`.",
"type": "boolean"
},
"autoConnect": {
"description": "Connect as soon as the entry runs. Deprecated, and removed in the next major release: use `connect`.",
"type": "boolean"
},
"reconnect": {
"description": "How many times to reconnect before giving up. Unset, Server-Sent Events keep trying for as long as the page is open — a dev server is expected to come back — while a WebSocket gives up after 10. Setting it applies to both. Deprecated, and removed in the next major release: use `connect`.",
"type": "number",
"minimum": 0
},
"timeout": {
"description": "How long the runtime tolerates silence before reconnecting, in milliseconds. Server-Sent Events only: their heartbeat arrives as data the client can see, whereas a WebSocket's is a protocol ping the browser answers without telling JavaScript — a half-open socket there is terminated by the server instead. Deprecated, and removed in the next major release: use `connect`.",
"type": "number",
"exclusiveMinimum": 0
}
}
},
Expand Down Expand Up @@ -475,7 +445,7 @@
"additionalProperties": false,
"properties": {
"transport": {
"description": "How events reach the clients: `sse`, `ws` (needs the optional `ws` dependency and an HTTP server to answer upgrades on, given as `server` or through the middleware's `attach` method), or a function building a transport of your own.",
"description": "How events reach the clients: `sse`, `ws` (needs the optional `ws` dependency and an HTTP server to answer upgrades on, handed over through the middleware's `attach` or `handleUpgrade` method), or a function building a transport of your own.",
"anyOf": [
{
"enum": ["sse", "ws"]
Expand All @@ -495,11 +465,6 @@
"type": "number",
"minimum": 1
},
"server": {
"description": "HTTP server the `ws` transport answers upgrades on, when it is already built. Otherwise hand it over later with the middleware's `attach` method.",
"type": "object",
"additionalProperties": true
},
"ws": {
"description": "Options for the `ws` server behind the `ws` transport: compression, payload limits, `verifyClient`, or a `port` or a `server` of its own to listen on instead of answering the upgrades it is handed. `path`, `noServer` and `clientTracking` are the middleware's and are ignored here.",
"link": "https://github.com/webpack/webpack-dev-middleware#hot",
Expand Down
32 changes: 5 additions & 27 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1457,17 +1457,6 @@ function filterSource(option, filter) {
);
}

// The six browser options `apply` and `connect` replaced. Accepted and folded
// in by the client until the next major release.
const LEGACY_CLIENT_OPTIONS = [
"hot",
"liveReload",
"reload",
"autoConnect",
"reconnect",
"timeout",
];

/**
* The browser options, as the client reads them from its resource query.
* @param {EXPECTED_ANY} client the `hot.client` option
Expand All @@ -1483,12 +1472,12 @@ function clientQuery(client, resolvedPath) {
}

for (let [key, value] of Object.entries(client)) {
// A path given in parts replaces the resolved path in the query outright,
// A url given in parts replaces the resolved path in the query outright,
// so the part it leaves out has to be carried over rather than left to the
// client's own default — which is the same path only until someone sets
// `hot.path`, and then quietly is not.
if (
key === "path" &&
key === "url" &&
resolvedPath &&
typeof value === "object" &&
value !== null &&
Expand Down Expand Up @@ -1602,17 +1591,6 @@ function injectHotClient(compilers, options, logger) {
/** @type {HotClientOptions | undefined} */
const clientOptions = options.client || undefined;

// TODO in the next major release remove this warning and `LEGACY_CLIENT_OPTIONS`
const deprecated = LEGACY_CLIENT_OPTIONS.filter((name) =>
Object.hasOwn(clientOptions || {}, name),
);

if (deprecated.length > 0) {
logger.warn(
`${deprecated.map((name) => `'hot.client.${name}'`).join(", ")} ${deprecated.length === 1 ? "is" : "are"} deprecated and will be removed in the next major release. 'hot.client.apply' replaces 'hot', 'liveReload' and 'reload'; 'hot.client.connect' replaces 'autoConnect', 'reconnect' and 'timeout'. Until then these still apply, and the option that replaced them wins when both are set.`,
);
}

let warned = false;
// A token only reaches the browser on the entry added below, so a required
// one with nothing added would refuse every client.
Expand Down Expand Up @@ -1660,10 +1638,10 @@ function injectHotClient(compilers, options, logger) {
!customClientTransport &&
client.transport &&
client.transport !== options.transport &&
!client.path
!client.url
) {
logger.warn(
`'hot.client.transport' is '${client.transport}' while the endpoint serves '${options.transport}', so the client will not connect. Set them to the same thing, or give 'hot.client.path' the endpoint that does speak '${client.transport}'.`,
`'hot.client.transport' is '${client.transport}' while the endpoint serves '${options.transport}', so the client will not connect. Set them to the same thing, or give 'hot.client.url' the endpoint that does speak '${client.transport}'.`,
);
}

Expand Down Expand Up @@ -1703,7 +1681,7 @@ function injectHotClient(compilers, options, logger) {
// carries the same options and is spread over them.
const { name: compilation } = compiler.options;
/** @type {Record<string, string>} */
const query = { path: options.path, transport };
const query = { url: options.path, transport };

if (options.token) {
query.token = options.token;
Expand Down
Loading
Loading