Skip to content
Merged
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
15 changes: 15 additions & 0 deletions .changeset/client-logger-name.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"webpack-dev-middleware": minor
---

`hot.client.logging` takes an object as well as a level, so a package embedding this runtime can label the console with its own name:

```js
middleware(compiler, {
hot: { client: { logging: { level: "warn", name: "my-dev-server" } } },
});
```

Messages read `[my-dev-server] …` rather than `[webpack-dev-middleware] …`. Unset, it is this package's name as before.

The reason is the one that made the overlay's element id an option: the package a developer installed is the one they would report a problem to, and a console labelled with a dependency's name sends them to the wrong repository. It matters most where this runtime is the whole of another package's client — webpack-dev-server, whose users have read `[webpack-dev-server]` for years.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -825,7 +825,7 @@ the rest cannot — see [opting one page out](#opting-one-page-out).
| `connect` | `boolean\|{ retries, timeout }` | `true` | Whether to connect when the entry runs, and how the connection is held open. `false` does not connect — call `setOptionsAndConnect()` yourself. `retries` is how many times to reconnect before giving up; unset, `"sse"` keeps trying for as long as the page is open while `"ws"` gives up after `10`. `timeout` is how long silence is tolerated before reconnecting, in milliseconds, and the interval between reconnections — `"sse"` only, since a `"ws"` heartbeat is a protocol ping the browser answers without telling JavaScript. |
| `overlay` | `boolean\|Object` | `true` | In-page overlay for problems: a boolean, or a JSON object — see [overlay options](#client-overlay-options). Same value shape as webpack-dev-server's [`client.overlay`](https://webpack.js.org/configuration/dev-server/#overlay), plus a few webpack-dev-middleware extensions. |
| `urlPrefix` | `string` | `"webpack-dev-middleware"` | Names the page-url parameter (`<prefix>-apply`) that overrides [`apply`](#client-options) for a single page — see [opting one page out](#opting-one-page-out). Change it if you are building a server of your own and want parameters named after it. |
| `logging` | `string` | `"info"` | Logger level — one of `"none"`, `"error"`, `"warn"`, `"info"`, `"log"`, `"verbose"`. Uses webpack's runtime logger. |
| `logging` | `string\|Object` | `"info"` | Logger level — one of `"none"`, `"error"`, `"warn"`, `"info"`, `"log"`, `"verbose"`. Uses webpack's runtime logger. An object takes the same value as `level` plus a `name`, which is what every message is labelled with in the console: a package embedding this runtime is the one a developer installed and would report a problem to, so it can say its own name — `{ level: "warn", name: "my-dev-server" }` logs `[my-dev-server] …`. |
| `name` | `string` | `""` | Restrict updates to a specific compilation name (useful with multi-compiler). |
| `progress` | `boolean` | `true` | Show a small badge in the page while a rebuild is in progress (with the compilation percentage when the server enables `hot.progress`). Set to `false` to disable. |
| `dynamicPublicPath` | `boolean` | `false` | Prefix `path` with `__webpack_public_path__` at runtime. The leading slash of `path` is stripped and no other normalization is applied, so the public path should end with `/`. |
Expand Down
32 changes: 30 additions & 2 deletions client-src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import createSocket from "./clients/createSocket.js";
import * as indicator from "./indicator.js";
import configureOverlay from "./overlay.js";
import applyUpdate from "./process-update.js";
import { log, setLogLevel } from "./utils/log.js";
import { log, setLogLevel, setLogName } from "./utils/log.js";
import reloadPage from "./utils/reload.js";
import sendMessage from "./utils/send-message.js";
import socketOptions from "./utils/socket-options.js";
Expand Down Expand Up @@ -63,6 +63,7 @@ import withToken from "./utils/with-token.js";
* @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 {LogLevel} logging logger level
* @property {string=} loggerName what to label messages with in the console
* @property {string} name limit updates to this compilation name
* @property {string} token the secret the endpoint requires, when it requires one, put on the connection url — empty when it requires none
* @property {boolean | "circular" | "linear"} progress show an indicator while a rebuild is in progress — `true` and `"circular"` a small badge, `"linear"` a thin bar across the top of the viewport
Expand All @@ -77,6 +78,7 @@ const options = {
overlay: true,
urlPrefix: "webpack-dev-middleware",
logging: "info",
loggerName: "",
name: "",
// The secret the endpoint requires, when it requires one. Put on the url
// rather than sent as a header: neither `EventSource` nor `WebSocket` lets a
Expand Down Expand Up @@ -351,7 +353,32 @@ function setOverrides(overrides) {
}
if (overrides.urlPrefix) options.urlPrefix = overrides.urlPrefix;
if (overrides.logging) {
options.logging = /** @type {LogLevel} */ (overrides.logging);
// A level, or a json object carrying the level and the name to label
// messages with — the same two shapes the other options take.
let logging = overrides.logging;
let parsed;

try {
parsed = JSON.parse(logging);
} catch {
// Not json, so it is the level it looks like.
}

// Only an object is the second shape. `JSON.parse` also accepts a bare
// number, boolean or quoted string, and a level is none of those — asking
// what came back rather than what the text started with also means
// leading whitespace does not hide it.
if (parsed && typeof parsed === "object") {
logging = parsed.level;

if (parsed.name) {
options.loggerName = parsed.name;
}
}

if (logging) {
options.logging = /** @type {LogLevel} */ (logging);
}
}
if (overrides.name) {
options.name = overrides.name;
Expand All @@ -372,6 +399,7 @@ function setOverrides(overrides) {
options.path = __webpack_public_path__ + options.path.replace(/^\//, "");
}

setLogName(options.loggerName);
setLogLevel(options.logging);
}

Expand Down
15 changes: 13 additions & 2 deletions client-src/utils/log.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-expect-error -- no published types for this entry point
import logger from "webpack/lib/logging/runtime.js";

const LOGGER_NAME = "webpack-dev-middleware";
const DEFAULT_NAME = "webpack-dev-middleware";
const DEFAULT_LEVEL = "info";

/** @typedef {false | true | "none" | "error" | "warn" | "info" | "log" | "verbose"} LogLevel */
Expand All @@ -15,7 +15,18 @@ export function setLogLevel(level) {

setLogLevel(DEFAULT_LEVEL);

const rawLog = logger.getLogger(LOGGER_NAME);
// What every message is labelled with in the console. A package embedding this
// runtime is the package the developer installed and the one they would report
// a problem to, so it says its own name rather than this one — the same reason
// the overlay's element id is settable.
let rawLog = logger.getLogger(DEFAULT_NAME);

/**
* @param {string=} name what to label messages with
*/
export function setLogName(name) {
rawLog = logger.getLogger(name || DEFAULT_NAME);
}

/**
* Guard a logger method: under a `require-trusted-types-for 'script'`
Expand Down
4 changes: 3 additions & 1 deletion src/hot.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
/** @typedef {import("webpack").StatsOptions} StatsOptions */
/** @typedef {import("webpack").Configuration["stats"]} MiddlewareStatsOption */

/** @typedef {("none" | "error" | "warn" | "info" | "log" | "verbose")} LogLevel */

/**
* Everything the browser runtime reads, as it is set in node. One for one with
* what the entry query carries, so every option has both spellings: set it
Expand All @@ -38,7 +40,7 @@
* @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 {("none" | "error" | "warn" | "info" | "log" | "verbose")=} logging how much the runtime logs to the browser console
* @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
Expand Down
2 changes: 1 addition & 1 deletion src/options.check.js

Large diffs are not rendered by default.

36 changes: 34 additions & 2 deletions src/options.json
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,40 @@
"minLength": 1
},
"logging": {
"description": "How much the runtime logs to the browser console.",
"enum": ["none", "error", "warn", "info", "log", "verbose"]
"description": "Logger level in the browser, or an object carrying the level and the name every message is labelled with in the console \u2014 a package embedding this runtime is the one a developer would report a problem to, so it can say its own name.",
"anyOf": [
{
"enum": [
"none",
"error",
"warn",
"info",
"log",
"verbose"
]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"level": {
"enum": [
"none",
"error",
"warn",
"info",
"log",
"verbose"
]
},
"name": {
"description": "What every message is labelled with in the console. Defaults to this package's name.",
"type": "string",
"minLength": 1
}
}
}
]
},
"dynamicPublicPath": {
"description": "Prefix the endpoint path with the bundle's public path at runtime.",
Expand Down
67 changes: 67 additions & 0 deletions test/e2e/messages.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,73 @@ describe("messages posted to the page (browser)", () => {
);
});

// A package embedding this runtime is the one the developer installed and
// the one they would report a problem to, so it labels the console with its
// own name — the same reason the overlay's element id is settable.
it("labels the console with the name it was given", async () => {
hotApp = await createHotApp({
bare: true,
hot: { client: { logging: { level: "info", name: "my-dev-server" } } },
code: recordingApp("v1"),
});
({ page, browser } = await runBrowser());
const console_ = collectConsole(page);

await page.goto(hotApp.url);
await waitForAppText(page, "v1");
await console_.waitFor("connected");

const said = console_.messages.join("\n");

expect(said).toContain("[my-dev-server]");
expect(said).not.toContain("[webpack-dev-middleware]");
});

// Whitespace before the json, which a query can carry and a check on the
// first character would miss — the object would be read as the level, and
// the name silently lost.
it("reads the name whatever the json is padded with", async () => {
hotApp = await createHotApp({
query: `?logging=${encodeURIComponent(' {"level":"info","name":"padded-server"} ')}`,
code: recordingApp("v1"),
});
({ page, browser } = await runBrowser());
const console_ = collectConsole(page);

await page.goto(hotApp.url);
await waitForAppText(page, "v1");
await console_.waitFor("connected");

expect(console_.messages.join("\n")).toContain("[padded-server]");
});

// Both cases above use `"info"`, the default — so neither would notice the
// object's `name` being applied while its `level` was dropped. `"warn"`
// silences `connected`, which is logged at info, and leaves a warning
// through: the level has to be read from inside the object for this to hold.
it("applies the level from inside the object too", async () => {
hotApp = await createHotApp({
bare: true,
hot: { client: { logging: { level: "warn", name: "quiet-server" } } },
code: recordingApp("v1"),
});
({ page, browser } = await runBrowser());
const console_ = collectConsole(page);

await page.goto(hotApp.url);
await waitForAppText(page, "v1");

// Logged at the error level, which `"warn"` lets through.
hotApp.instance.publish({ action: "error", message: "a warning level" });
await console_.waitFor("a warning level");

const said = console_.messages.join("\n");

expect(said).toContain("[quiet-server]");
// Logged at info, so `"warn"` has to have come from inside the object.
expect(said).not.toContain("connected");
});

it("says when the connection went away, once per outage", async () => {
hotApp = await createHotApp({
query: "?timeout=1000",
Expand Down
4 changes: 4 additions & 0 deletions types/client/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ export type ClientOptions = {
* logger level
*/
logging: LogLevel;
/**
* what to label messages with in the console
*/
loggerName?: string | undefined;
/**
* limit updates to this compilation name
*/
Expand Down
4 changes: 4 additions & 0 deletions types/client/utils/log.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
* @param {LogLevel} level log level (or `false` for off, `true` for default)
*/
export function setLogLevel(level: LogLevel): void;
/**
* @param {string=} name what to label messages with
*/
export function setLogName(name?: string | undefined): void;
export namespace log {
let error: (...args: unknown[]) => void;
let warn: (...args: unknown[]) => void;
Expand Down
13 changes: 11 additions & 2 deletions types/hot.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ declare namespace createHot {
Duplex,
StatsOptions,
MiddlewareStatsOption,
LogLevel,
HotClientOptions,
HotOptions,
CorsOrigin,
Expand Down Expand Up @@ -166,6 +167,7 @@ type HttpServer = import("node:http").Server;
type Duplex = import("node:stream").Duplex;
type StatsOptions = import("webpack").StatsOptions;
type MiddlewareStatsOption = import("webpack").Configuration["stats"];
type LogLevel = "none" | "error" | "warn" | "info" | "log" | "verbose";
/**
* Everything the browser runtime reads, as it is set in node. One for one with
* what the entry query carries, so every option has both spellings: set it
Expand Down Expand Up @@ -235,10 +237,17 @@ type HotClientOptions = {
*/
urlPrefix?: string | undefined;
/**
* how much the runtime logs to the browser console
* how much the runtime logs to the browser console, and the name every message is labelled with
*/
logging?:
("none" | "error" | "warn" | "info" | "log" | "verbose") | undefined;
| (
| LogLevel
| {
level?: LogLevel;
name?: string;
}
)
| undefined;
/**
* 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
*/
Expand Down
Loading