diff --git a/client-src/index.js b/client-src/index.js
index 4689552d9e..7cd441bf4b 100644
--- a/client-src/index.js
+++ b/client-src/index.js
@@ -10,12 +10,20 @@ import sendMessage from "./utils/sendMessage.js";
import reloadApp from "./utils/reloadApp.js";
import createSocketURL from "./utils/createSocketURL.js";
+/**
+ * @typedef {Object} OverlayOptions
+ * @property {boolean | (error: Error) => boolean} [warnings]
+ * @property {boolean | (error: Error) => boolean} [errors]
+ * @property {boolean | (error: Error) => boolean} [runtimeErrors]
+ * @property {string} [trustedTypesPolicyName]
+ */
+
/**
* @typedef {Object} Options
* @property {boolean} hot
* @property {boolean} liveReload
* @property {boolean} progress
- * @property {boolean | { warnings?: boolean, errors?: boolean, runtimeErrors?: boolean, trustedTypesPolicyName?: string }} overlay
+ * @property {boolean | OverlayOptions} overlay
* @property {string} [logging]
* @property {number} [reconnect]
*/
@@ -27,6 +35,30 @@ import createSocketURL from "./utils/createSocketURL.js";
* @property {string} [previousHash]
*/
+/**
+ * @param {boolean | { warnings?: boolean | string; errors?: boolean | string; runtimeErrors?: boolean | string; }} overlayOptions
+ */
+const decodeOverlayOptions = (overlayOptions) => {
+ if (typeof overlayOptions === "object") {
+ ["warnings", "errors", "runtimeErrors"].forEach((property) => {
+ if (typeof overlayOptions[property] === "string") {
+ const overlayFilterFunctionString = decodeURIComponent(
+ overlayOptions[property]
+ );
+
+ // eslint-disable-next-line no-new-func
+ const overlayFilterFunction = new Function(
+ "message",
+ `var callback = ${overlayFilterFunctionString}
+ return callback(message)`
+ );
+
+ overlayOptions[property] = overlayFilterFunction;
+ }
+ });
+ }
+};
+
/**
* @type {Status}
*/
@@ -83,6 +115,8 @@ if (parsedResourceQuery.overlay) {
runtimeErrors: true,
...options.overlay,
};
+
+ decodeOverlayOptions(options.overlay);
}
enabledFeatures.Overlay = true;
}
@@ -173,6 +207,7 @@ const onSocketMessage = {
}
options.overlay = value;
+ decodeOverlayOptions(options.overlay);
},
/**
* @param {number} value
@@ -266,17 +301,24 @@ const onSocketMessage = {
log.warn(printableWarnings[i]);
}
- const needShowOverlayForWarnings =
+ const overlayWarningsSetting =
typeof options.overlay === "boolean"
? options.overlay
: options.overlay && options.overlay.warnings;
- if (needShowOverlayForWarnings) {
- overlay.send({
- type: "BUILD_ERROR",
- level: "warning",
- messages: warnings,
- });
+ if (overlayWarningsSetting) {
+ const warningsToDisplay =
+ typeof overlayWarningsSetting === "function"
+ ? warnings.filter(overlayWarningsSetting)
+ : warnings;
+
+ if (warningsToDisplay.length) {
+ overlay.send({
+ type: "BUILD_ERROR",
+ level: "warning",
+ messages: warnings,
+ });
+ }
}
if (params && params.preventReloading) {
@@ -303,17 +345,24 @@ const onSocketMessage = {
log.error(printableErrors[i]);
}
- const needShowOverlayForErrors =
+ const overlayErrorsSettings =
typeof options.overlay === "boolean"
? options.overlay
: options.overlay && options.overlay.errors;
- if (needShowOverlayForErrors) {
- overlay.send({
- type: "BUILD_ERROR",
- level: "error",
- messages: errors,
- });
+ if (overlayErrorsSettings) {
+ const errorsToDisplay =
+ typeof overlayErrorsSettings === "function"
+ ? errors.filter(overlayErrorsSettings)
+ : errors;
+
+ if (errorsToDisplay.length) {
+ overlay.send({
+ type: "BUILD_ERROR",
+ level: "error",
+ messages: errors,
+ });
+ }
}
},
/**
diff --git a/client-src/overlay.js b/client-src/overlay.js
index 2887c28ad3..210b4996d1 100644
--- a/client-src/overlay.js
+++ b/client-src/overlay.js
@@ -78,7 +78,7 @@ function formatProblem(type, item) {
/**
* @typedef {Object} CreateOverlayOptions
* @property {string | null} trustedTypesPolicyName
- * @property {boolean} [catchRuntimeError]
+ * @property {boolean | (error: Error) => void} [catchRuntimeError]
*/
/**
@@ -90,6 +90,8 @@ const createOverlay = (options) => {
let iframeContainerElement;
/** @type {HTMLDivElement | null | undefined} */
let containerElement;
+ /** @type {HTMLDivElement | null | undefined} */
+ let headerElement;
/** @type {Array<(element: HTMLDivElement) => void>} */
let onLoadQueue = [];
/** @type {TrustedTypePolicy | undefined} */
@@ -124,6 +126,7 @@ const createOverlay = (options) => {
iframeContainerElement.id = "webpack-dev-server-client-overlay";
iframeContainerElement.src = "about:blank";
applyStyle(iframeContainerElement, iframeStyle);
+
iframeContainerElement.onload = () => {
const contentElement =
/** @type {Document} */
@@ -141,7 +144,7 @@ const createOverlay = (options) => {
contentElement.id = "webpack-dev-server-client-overlay-div";
applyStyle(contentElement, containerStyle);
- const headerElement = document.createElement("div");
+ headerElement = document.createElement("div");
headerElement.innerText = "Compiled with problems:";
applyStyle(headerElement, headerStyle);
@@ -219,9 +222,15 @@ const createOverlay = (options) => {
* @param {string} type
* @param {Array} messages
* @param {string | null} trustedTypesPolicyName
+ * @param {'build' | 'runtime'} messageSource
*/
- function show(type, messages, trustedTypesPolicyName) {
+ function show(type, messages, trustedTypesPolicyName, messageSource) {
ensureOverlayExists(() => {
+ headerElement.innerText =
+ messageSource === "runtime"
+ ? "Uncaught runtime errors:"
+ : "Compiled with problems:";
+
messages.forEach((message) => {
const entryElement = document.createElement("div");
const msgStyle =
@@ -267,8 +276,8 @@ const createOverlay = (options) => {
}
const overlayService = createOverlayMachine({
- showOverlay: ({ level = "error", messages }) =>
- show(level, messages, options.trustedTypesPolicyName),
+ showOverlay: ({ level = "error", messages, messageSource }) =>
+ show(level, messages, options.trustedTypesPolicyName, messageSource),
hideOverlay: hide,
});
@@ -284,15 +293,22 @@ const createOverlay = (options) => {
const errorObject =
error instanceof Error ? error : new Error(error || message);
- overlayService.send({
- type: "RUNTIME_ERROR",
- messages: [
- {
- message: errorObject.message,
- stack: parseErrorToStacks(errorObject),
- },
- ],
- });
+ const shouldDisplay =
+ typeof options.catchRuntimeError === "function"
+ ? options.catchRuntimeError(errorObject)
+ : true;
+
+ if (shouldDisplay) {
+ overlayService.send({
+ type: "RUNTIME_ERROR",
+ messages: [
+ {
+ message: errorObject.message,
+ stack: parseErrorToStacks(errorObject),
+ },
+ ],
+ });
+ }
});
}
diff --git a/client-src/overlay/state-machine.js b/client-src/overlay/state-machine.js
index d9ed764198..4c0444383c 100644
--- a/client-src/overlay/state-machine.js
+++ b/client-src/overlay/state-machine.js
@@ -4,6 +4,7 @@ import createMachine from "./fsm.js";
* @typedef {Object} ShowOverlayData
* @property {'warning' | 'error'} level
* @property {Array} messages
+ * @property {'build' | 'runtime'} messageSource
*/
/**
@@ -23,6 +24,7 @@ const createOverlayMachine = (options) => {
context: {
level: "error",
messages: [],
+ messageSource: "build",
},
states: {
hidden: {
@@ -73,18 +75,21 @@ const createOverlayMachine = (options) => {
return {
messages: [],
level: "error",
+ messageSource: "build",
};
},
appendMessages: (context, event) => {
return {
messages: context.messages.concat(event.messages),
level: event.level || context.level,
+ messageSource: event.type === "RUNTIME_ERROR" ? "runtime" : "build",
};
},
setMessages: (context, event) => {
return {
messages: event.messages,
level: event.level || context.level,
+ messageSource: event.type === "RUNTIME_ERROR" ? "runtime" : "build",
};
},
hideOverlay,
diff --git a/examples/client/overlay/app.js b/examples/client/overlay/app.js
index a4344aa340..5885cfaf68 100644
--- a/examples/client/overlay/app.js
+++ b/examples/client/overlay/app.js
@@ -5,7 +5,15 @@ const createErrorBtn = require("./error-button");
const target = document.querySelector("#target");
-target.insertAdjacentElement("afterend", createErrorBtn());
+target.insertAdjacentElement(
+ "afterend",
+ createErrorBtn("Click to throw error", "Error message thrown from JS")
+);
+
+target.insertAdjacentElement(
+ "afterend",
+ createErrorBtn("Click to throw ignored error", "something something")
+);
// eslint-disable-next-line import/no-unresolved, import/extensions
const invalid = require("./invalid.js");
diff --git a/examples/client/overlay/error-button.js b/examples/client/overlay/error-button.js
index 11fe606af0..2f0b87351e 100644
--- a/examples/client/overlay/error-button.js
+++ b/examples/client/overlay/error-button.js
@@ -1,18 +1,24 @@
"use strict";
-function unsafeOperation() {
- throw new Error("Error message thrown from JS");
-}
+/**
+ *
+ * @param {string} label
+ * @param {string} errorMessage
+ * @returns HTMLButtonElement
+ */
+module.exports = function createErrorButton(label, errorMessage) {
+ function unsafeOperation() {
+ throw new Error(errorMessage);
+ }
-function handleButtonClick() {
- unsafeOperation();
-}
+ function handleButtonClick() {
+ unsafeOperation();
+ }
-module.exports = function createErrorButton() {
const errorBtn = document.createElement("button");
errorBtn.addEventListener("click", handleButtonClick);
- errorBtn.innerHTML = "Click to throw error";
+ errorBtn.innerHTML = label;
return errorBtn;
};
diff --git a/examples/client/overlay/webpack.config.js b/examples/client/overlay/webpack.config.js
index 41d8ad543b..a2413a2434 100644
--- a/examples/client/overlay/webpack.config.js
+++ b/examples/client/overlay/webpack.config.js
@@ -10,7 +10,26 @@ module.exports = setup({
entry: "./app.js",
devServer: {
client: {
- overlay: true,
+ overlay: {
+ warnings: false,
+ runtimeErrors: (msg) => {
+ if (msg) {
+ let msgString;
+
+ if (msg instanceof Error) {
+ msgString = msg.message;
+ } else if (typeof msg === "string") {
+ msgString = msg;
+ }
+
+ if (msgString) {
+ return !/something/i.test(msgString);
+ }
+ }
+
+ return true;
+ },
+ },
},
},
// uncomment to test for IE
diff --git a/lib/Server.js b/lib/Server.js
index ec3453b6f2..0c093b2415 100644
--- a/lib/Server.js
+++ b/lib/Server.js
@@ -154,10 +154,14 @@ const schema = require("./options.json");
* @property {string} [username]
*/
+/**
+ * @typedef {boolean | ((error: Error) => void)} OverlayMessageOptions
+ */
+
/**
* @typedef {Object} ClientConfiguration
* @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging]
- * @property {boolean | { warnings?: boolean, errors?: boolean, runtimeErrors?: boolean }} [overlay]
+ * @property {boolean | { warnings?: OverlayMessageOptions, errors?: OverlayMessageOptions, runtimeErrors?: OverlayMessageOptions }} [overlay]
* @property {boolean} [progress]
* @property {boolean | number} [reconnect]
* @property {"ws" | "sockjs" | string} [webSocketTransport]
@@ -236,6 +240,16 @@ const memoize = (fn) => {
const getExpress = memoize(() => require("express"));
+/**
+ *
+ * @param {OverlayMessageOptions} [setting]
+ * @returns
+ */
+const encodeOverlaySettings = (setting) =>
+ typeof setting === "function"
+ ? encodeURIComponent(setting.toString())
+ : setting;
+
class Server {
/**
* @param {Configuration | Compiler | MultiCompiler} options
@@ -654,12 +668,19 @@ class Server {
}
if (typeof client.overlay !== "undefined") {
- searchParams.set(
- "overlay",
+ const overlayString =
typeof client.overlay === "boolean"
? String(client.overlay)
- : JSON.stringify(client.overlay)
- );
+ : JSON.stringify({
+ ...client.overlay,
+ errors: encodeOverlaySettings(client.overlay.errors),
+ warnings: encodeOverlaySettings(client.overlay.warnings),
+ runtimeErrors: encodeOverlaySettings(
+ client.overlay.runtimeErrors
+ ),
+ });
+
+ searchParams.set("overlay", overlayString);
}
if (typeof client.reconnect !== "undefined") {
@@ -2627,11 +2648,27 @@ class Server {
/** @type {ClientConfiguration} */
(this.options.client).overlay
) {
+ const overlayConfig = /** @type {ClientConfiguration} */ (
+ this.options.client
+ ).overlay;
+
this.sendMessage(
[client],
"overlay",
- /** @type {ClientConfiguration} */
- (this.options.client).overlay
+ typeof overlayConfig === "object"
+ ? {
+ ...overlayConfig,
+ errors:
+ overlayConfig.errors &&
+ encodeOverlaySettings(overlayConfig.errors),
+ warnings:
+ overlayConfig.warnings &&
+ encodeOverlaySettings(overlayConfig.warnings),
+ runtimeErrors:
+ overlayConfig.runtimeErrors &&
+ encodeOverlaySettings(overlayConfig.runtimeErrors),
+ }
+ : overlayConfig
);
}
diff --git a/lib/options.json b/lib/options.json
index 87ac7e1fb3..654a68a580 100644
--- a/lib/options.json
+++ b/lib/options.json
@@ -98,25 +98,49 @@
"additionalProperties": false,
"properties": {
"errors": {
- "description": "Enables a full-screen overlay in the browser when there are compiler errors.",
- "type": "boolean",
- "cli": {
- "negatedDescription": "Disables the full-screen overlay in the browser when there are compiler errors."
- }
+ "anyOf": [
+ {
+ "description": "Enables a full-screen overlay in the browser when there are compiler errors.",
+ "type": "boolean",
+ "cli": {
+ "negatedDescription": "Disables the full-screen overlay in the browser when there are compiler errors."
+ }
+ },
+ {
+ "instanceof": "Function",
+ "description": "Filter compiler errors. Return true to include and return false to exclude."
+ }
+ ]
},
"warnings": {
- "description": "Enables a full-screen overlay in the browser when there are compiler warnings.",
- "type": "boolean",
- "cli": {
- "negatedDescription": "Disables the full-screen overlay in the browser when there are compiler warnings."
- }
+ "anyOf": [
+ {
+ "description": "Enables a full-screen overlay in the browser when there are compiler warnings.",
+ "type": "boolean",
+ "cli": {
+ "negatedDescription": "Disables the full-screen overlay in the browser when there are compiler warnings."
+ }
+ },
+ {
+ "instanceof": "Function",
+ "description": "Filter compiler warnings. Return true to include and return false to exclude."
+ }
+ ]
},
"runtimeErrors": {
- "description": "Enables a full-screen overlay in the browser when there are uncaught runtime errors.",
- "type": "boolean",
- "cli": {
- "negatedDescription": "Disables the full-screen overlay in the browser when there are uncaught runtime errors."
- }
+ "anyOf": [
+ {
+ "description": "Enables a full-screen overlay in the browser when there are uncaught runtime errors.",
+ "type": "boolean",
+ "cli": {
+ "negatedDescription": "Disables the full-screen overlay in the browser when there are uncaught runtime errors."
+ }
+ },
+ {
+ "instanceof": "Function",
+ "description": "Filter uncaught runtime errors. Return true to include and return false to exclude."
+ }
+ ]
},
"trustedTypesPolicyName": {
"description": "The name of a Trusted Types policy for the overlay. Defaults to 'webpack-dev-server#overlay'.",
diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack4 b/test/__snapshots__/validate-options.test.js.snap.webpack4
index 2a5ed5e47d..887634ff91 100644
--- a/test/__snapshots__/validate-options.test.js.snap.webpack4
+++ b/test/__snapshots__/validate-options.test.js.snap.webpack4
@@ -107,14 +107,34 @@ exports[`options validate should throw an error on the "client" option with '{"o
exports[`options validate should throw an error on the "client" option with '{"overlay":{"errors":""}}' value 1`] = `
"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
- - options.client.overlay.errors should be a boolean.
- -> Enables a full-screen overlay in the browser when there are compiler errors."
+ - options.client should be one of these:
+ false | object { logging?, overlay?, progress?, reconnect?, webSocketTransport?, webSocketURL? }
+ -> Allows to specify options for client script in the browser or disable client script.
+ -> Read more at https://webpack.js.org/configuration/dev-server/#devserverclient
+ Details:
+ * options.client.overlay.errors should be one of these:
+ boolean | function
+ Details:
+ * options.client.overlay.errors should be a boolean.
+ -> Enables a full-screen overlay in the browser when there are compiler errors.
+ * options.client.overlay.errors should be an instance of function.
+ -> Filter compiler errors. Return true to include and return false to exclude."
`;
exports[`options validate should throw an error on the "client" option with '{"overlay":{"warnings":""}}' value 1`] = `
"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
- - options.client.overlay.warnings should be a boolean.
- -> Enables a full-screen overlay in the browser when there are compiler warnings."
+ - options.client should be one of these:
+ false | object { logging?, overlay?, progress?, reconnect?, webSocketTransport?, webSocketURL? }
+ -> Allows to specify options for client script in the browser or disable client script.
+ -> Read more at https://webpack.js.org/configuration/dev-server/#devserverclient
+ Details:
+ * options.client.overlay.warnings should be one of these:
+ boolean | function
+ Details:
+ * options.client.overlay.warnings should be a boolean.
+ -> Enables a full-screen overlay in the browser when there are compiler warnings.
+ * options.client.overlay.warnings should be an instance of function.
+ -> Filter compiler warnings. Return true to include and return false to exclude."
`;
exports[`options validate should throw an error on the "client" option with '{"progress":""}' value 1`] = `
diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack5 b/test/__snapshots__/validate-options.test.js.snap.webpack5
index 2a5ed5e47d..887634ff91 100644
--- a/test/__snapshots__/validate-options.test.js.snap.webpack5
+++ b/test/__snapshots__/validate-options.test.js.snap.webpack5
@@ -107,14 +107,34 @@ exports[`options validate should throw an error on the "client" option with '{"o
exports[`options validate should throw an error on the "client" option with '{"overlay":{"errors":""}}' value 1`] = `
"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
- - options.client.overlay.errors should be a boolean.
- -> Enables a full-screen overlay in the browser when there are compiler errors."
+ - options.client should be one of these:
+ false | object { logging?, overlay?, progress?, reconnect?, webSocketTransport?, webSocketURL? }
+ -> Allows to specify options for client script in the browser or disable client script.
+ -> Read more at https://webpack.js.org/configuration/dev-server/#devserverclient
+ Details:
+ * options.client.overlay.errors should be one of these:
+ boolean | function
+ Details:
+ * options.client.overlay.errors should be a boolean.
+ -> Enables a full-screen overlay in the browser when there are compiler errors.
+ * options.client.overlay.errors should be an instance of function.
+ -> Filter compiler errors. Return true to include and return false to exclude."
`;
exports[`options validate should throw an error on the "client" option with '{"overlay":{"warnings":""}}' value 1`] = `
"ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
- - options.client.overlay.warnings should be a boolean.
- -> Enables a full-screen overlay in the browser when there are compiler warnings."
+ - options.client should be one of these:
+ false | object { logging?, overlay?, progress?, reconnect?, webSocketTransport?, webSocketURL? }
+ -> Allows to specify options for client script in the browser or disable client script.
+ -> Read more at https://webpack.js.org/configuration/dev-server/#devserverclient
+ Details:
+ * options.client.overlay.warnings should be one of these:
+ boolean | function
+ Details:
+ * options.client.overlay.warnings should be a boolean.
+ -> Enables a full-screen overlay in the browser when there are compiler warnings.
+ * options.client.overlay.warnings should be an instance of function.
+ -> Filter compiler warnings. Return true to include and return false to exclude."
`;
exports[`options validate should throw an error on the "client" option with '{"progress":""}' value 1`] = `
diff --git a/test/e2e/__snapshots__/overlay.test.js.snap.webpack4 b/test/e2e/__snapshots__/overlay.test.js.snap.webpack4
index f989b527bb..44a1317cf9 100644
--- a/test/e2e/__snapshots__/overlay.test.js.snap.webpack4
+++ b/test/e2e/__snapshots__/overlay.test.js.snap.webpack4
@@ -2135,3 +2135,299 @@ exports[`overlay should show an error when "client.overlay.warnings" is "true":
+
+
+ Uncaught runtime errors:
+
+
+
+
+
+ ERROR
+
+
+ Injected error at throwError (<anonymous>:2:15) at
+ <anonymous>:3:9 at addScriptContent
+ (__puppeteer_evaluation_script__:9:27)
+
+
+
+
+
+
+
+ Compiled with problems:
+
+
+
+
+
+ ERROR
+
+
+ Unfiltered error
+
+
+
+
+
+ webpack-dev-server is running...
+
+
+
+
+
+
+ Compiled with problems:
+
+
+
+
+
+ WARNING
+
+
+ Unfiltered warning
+
+
+
+
+
+ webpack-dev-server is running...
+
+
+
+
+
+
+ Uncaught runtime errors:
+
+
+
+
+
+ ERROR
+
+
+ Injected error at throwError (<anonymous>:2:15) at
+ <anonymous>:3:9 at addScriptContent
+ (__puppeteer_evaluation_script__:9:27)
+
+
+
+
+
+
+
+ Compiled with problems:
+
+
+
+
+
+ ERROR
+
+
+ Unfiltered error
+
+
+
+
+
+ webpack-dev-server is running...
+
+
+
+
"
`;
+
+exports[`overlay should show warning when it is not filtered: overlay html 1`] = `
+"
+
+
+ Compiled with problems:
+
+
+
+
+
+ WARNING
+
+
+ Unfiltered warning
+
+
+
+
+
+"
+`;
+
+exports[`overlay should show warning when it is not filtered: page html 1`] = `
+"
+
webpack-dev-server is running...
+
+
+
+
+"
+`;
diff --git a/test/e2e/overlay.test.js b/test/e2e/overlay.test.js
index 86e31bbf16..6ea8cb60c5 100644
--- a/test/e2e/overlay.test.js
+++ b/test/e2e/overlay.test.js
@@ -596,6 +596,90 @@ describe("overlay", () => {
await server.stop();
});
+ it("should not show warning when it is filtered", async () => {
+ const compiler = webpack(config);
+
+ new WarningPlugin("My special warning").apply(compiler);
+
+ const server = new Server(
+ {
+ port,
+ client: {
+ overlay: {
+ warnings: (error) => {
+ // error is string in webpack 4
+ const message = typeof error === "string" ? error : error.message;
+ return message !== "My special warning";
+ },
+ },
+ },
+ },
+ compiler
+ );
+
+ await server.start();
+
+ const { page, browser } = await runBrowser();
+
+ await page.goto(`http://localhost:${port}/`, {
+ waitUntil: "networkidle0",
+ });
+
+ const overlayHandle = await page.$("#webpack-dev-server-client-overlay");
+
+ expect(overlayHandle).toBe(null);
+
+ await browser.close();
+ await server.stop();
+ });
+
+ it("should show warning when it is not filtered", async () => {
+ const compiler = webpack(config);
+
+ new WarningPlugin("Unfiltered warning").apply(compiler);
+
+ const server = new Server(
+ {
+ port,
+ client: {
+ overlay: {
+ warnings: () => true,
+ },
+ },
+ },
+ compiler
+ );
+
+ await server.start();
+
+ const { page, browser } = await runBrowser();
+
+ try {
+ await page.goto(`http://localhost:${port}/`, {
+ waitUntil: "networkidle0",
+ });
+
+ const pageHtml = await page.evaluate(() => document.body.outerHTML);
+ const overlayHandle = await page.$("#webpack-dev-server-client-overlay");
+ const overlayFrame = await overlayHandle.contentFrame();
+ const overlayHtml = await overlayFrame.evaluate(
+ () => document.body.outerHTML
+ );
+
+ expect(prettier.format(pageHtml, { parser: "html" })).toMatchSnapshot(
+ "page html"
+ );
+ expect(prettier.format(overlayHtml, { parser: "html" })).toMatchSnapshot(
+ "overlay html"
+ );
+ } catch (error) {
+ console.error(error);
+ }
+
+ await browser.close();
+ await server.stop();
+ });
+
it('should show a warning when "client.overlay" is "true"', async () => {
const compiler = webpack(config);
@@ -785,6 +869,95 @@ describe("overlay", () => {
await server.stop();
});
+ it("should not show error when it is filtered", async () => {
+ const compiler = webpack(config);
+
+ new ErrorPlugin("My special error").apply(compiler);
+
+ const server = new Server(
+ {
+ port,
+ client: {
+ overlay: {
+ errors: (error) => {
+ // error is string in webpack 4
+ const message = typeof error === "string" ? error : error.message;
+
+ return message !== "My special error";
+ },
+ },
+ },
+ },
+ compiler
+ );
+
+ await server.start();
+
+ const { page, browser } = await runBrowser();
+
+ try {
+ await page.goto(`http://localhost:${port}/`, {
+ waitUntil: "networkidle0",
+ });
+
+ const overlayHandle = await page.$("#webpack-dev-server-client-overlay");
+
+ expect(overlayHandle).toBe(null);
+ } catch (error) {
+ console.error(error);
+ }
+
+ await browser.close();
+ await server.stop();
+ });
+
+ it("should show error when it is not filtered", async () => {
+ const compiler = webpack(config);
+
+ new ErrorPlugin("Unfiltered error").apply(compiler);
+
+ const server = new Server(
+ {
+ port,
+ client: {
+ overlay: {
+ errors: () => true,
+ },
+ },
+ },
+ compiler
+ );
+
+ await server.start();
+
+ const { page, browser } = await runBrowser();
+
+ try {
+ await page.goto(`http://localhost:${port}/`, {
+ waitUntil: "networkidle0",
+ });
+
+ const pageHtml = await page.evaluate(() => document.body.outerHTML);
+ const overlayHandle = await page.$("#webpack-dev-server-client-overlay");
+ const overlayFrame = await overlayHandle.contentFrame();
+ const overlayHtml = await overlayFrame.evaluate(
+ () => document.body.outerHTML
+ );
+
+ expect(prettier.format(pageHtml, { parser: "html" })).toMatchSnapshot(
+ "page html"
+ );
+ expect(prettier.format(overlayHtml, { parser: "html" })).toMatchSnapshot(
+ "overlay html"
+ );
+ } catch (error) {
+ console.error(error);
+ }
+
+ await browser.close();
+ await server.stop();
+ });
+
it('should show an error when "client.overlay" is "true"', async () => {
const compiler = webpack(config);
@@ -1145,4 +1318,79 @@ describe("overlay", () => {
await browser.close();
await server.stop();
});
+
+ it("should show error for uncaught runtime error", async () => {
+ const compiler = webpack(config);
+
+ const server = new Server(
+ {
+ port,
+ },
+ compiler
+ );
+
+ await server.start();
+
+ const { page, browser } = await runBrowser();
+
+ await page.goto(`http://localhost:${port}/`, {
+ waitUntil: "networkidle0",
+ });
+
+ await page.addScriptTag({
+ content: `(function throwError() {
+ throw new Error('Injected error');
+ })();`,
+ });
+
+ const overlayHandle = await page.$("#webpack-dev-server-client-overlay");
+ const overlayFrame = await overlayHandle.contentFrame();
+ const overlayHtml = await overlayFrame.evaluate(
+ () => document.body.outerHTML
+ );
+
+ expect(prettier.format(overlayHtml, { parser: "html" })).toMatchSnapshot(
+ "overlay html"
+ );
+
+ await browser.close();
+ await server.stop();
+ });
+
+ it("should not show filtered runtime error", async () => {
+ const compiler = webpack(config);
+
+ const server = new Server(
+ {
+ port,
+ client: {
+ overlay: {
+ runtimeErrors: (error) => error && !/Injected/.test(error.message),
+ },
+ },
+ },
+ compiler
+ );
+
+ await server.start();
+
+ const { page, browser } = await runBrowser();
+
+ await page.goto(`http://localhost:${port}/`, {
+ waitUntil: "networkidle0",
+ });
+
+ await page.addScriptTag({
+ content: `(function throwError() {
+ throw new Error('Injected error');
+ })();`,
+ });
+
+ const overlayHandle = await page.$("#webpack-dev-server-client-overlay");
+
+ expect(overlayHandle).toBe(null);
+
+ await browser.close();
+ await server.stop();
+ });
});
diff --git a/types/lib/Server.d.ts b/types/lib/Server.d.ts
index e5ed287917..f8aa0d06bb 100644
--- a/types/lib/Server.d.ts
+++ b/types/lib/Server.d.ts
@@ -139,10 +139,13 @@ declare class Server {
* @property {string} [protocol]
* @property {string} [username]
*/
+ /**
+ * @typedef {boolean | ((error: Error) => void)} OverlayMessageOptions
+ */
/**
* @typedef {Object} ClientConfiguration
* @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging]
- * @property {boolean | { warnings?: boolean, errors?: boolean, runtimeErrors?: boolean }} [overlay]
+ * @property {boolean | { warnings?: OverlayMessageOptions, errors?: OverlayMessageOptions, runtimeErrors?: OverlayMessageOptions }} [overlay]
* @property {boolean} [progress]
* @property {boolean | number} [reconnect]
* @property {"ws" | "sockjs" | string} [webSocketTransport]
@@ -294,10 +297,13 @@ declare class Server {
* @property {string} [protocol]
* @property {string} [username]
*/
+ /**
+ * @typedef {boolean | ((error: Error) => void)} OverlayMessageOptions
+ */
/**
* @typedef {Object} ClientConfiguration
* @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging]
- * @property {boolean | { warnings?: boolean, errors?: boolean, runtimeErrors?: boolean }} [overlay]
+ * @property {boolean | { warnings?: OverlayMessageOptions, errors?: OverlayMessageOptions, runtimeErrors?: OverlayMessageOptions }} [overlay]
* @property {boolean} [progress]
* @property {boolean | number} [reconnect]
* @property {"ws" | "sockjs" | string} [webSocketTransport]
@@ -454,10 +460,13 @@ declare class Server {
* @property {string} [protocol]
* @property {string} [username]
*/
+ /**
+ * @typedef {boolean | ((error: Error) => void)} OverlayMessageOptions
+ */
/**
* @typedef {Object} ClientConfiguration
* @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging]
- * @property {boolean | { warnings?: boolean, errors?: boolean, runtimeErrors?: boolean }} [overlay]
+ * @property {boolean | { warnings?: OverlayMessageOptions, errors?: OverlayMessageOptions, runtimeErrors?: OverlayMessageOptions }} [overlay]
* @property {boolean} [progress]
* @property {boolean | number} [reconnect]
* @property {"ws" | "sockjs" | string} [webSocketTransport]
@@ -550,9 +559,6 @@ declare class Server {
simpleType: string;
multiple: boolean;
};
- /**
- * @typedef {Array<{ key: string; value: string }> | Record
} Headers
- */
"client-reconnect": {
configs: (
| {
@@ -613,7 +619,7 @@ declare class Server {
}[];
description: string;
simpleType: string;
- multiple: boolean;
+ /** @type {T} */ multiple: boolean;
};
"client-web-socket-url-password": {
configs: {
@@ -648,28 +654,27 @@ declare class Server {
simpleType: string;
multiple: boolean;
};
- /**
- * @private
- * @type {RequestHandler[]}
- */
"client-web-socket-url-protocol": {
configs: (
| {
description: string;
multiple: boolean;
path: string;
- /**
- * @private
- * @type {string | undefined}
- */
type: string;
values: string[];
}
| {
description: string;
+ /**
+ * @private
+ * @type {RequestHandler[]}
+ */
multiple: boolean;
path: string;
type: string;
+ /**
+ * @type {Socket[]}
+ */
}
)[];
description: string;
@@ -776,6 +781,9 @@ declare class Server {
simpleType: string;
multiple: boolean;
};
+ /**
+ * @type {string | undefined}
+ */
"https-ca": {
configs: {
type: string;
@@ -809,15 +817,12 @@ declare class Server {
simpleType: string;
multiple: boolean;
};
- /**
- * @type {string[]}
- */
"https-cacert-reset": {
configs: {
description: string;
multiple: boolean;
path: string;
- /** @type {WebSocketURL} */ type: string;
+ type: string;
}[];
description: string;
multiple: boolean;
@@ -827,7 +832,7 @@ declare class Server {
configs: {
type: string;
multiple: boolean;
- description: string;
+ /** @type {ClientConfiguration} */ description: string;
path: string;
}[];
description: string;
@@ -887,7 +892,7 @@ declare class Server {
}[];
description: string;
multiple: boolean;
- simpleType: string;
+ /** @type {string} */ simpleType: string;
};
"https-passphrase": {
configs: {
@@ -947,12 +952,6 @@ declare class Server {
values: boolean[];
multiple: boolean;
description: string;
- /**
- * prependEntry Method for webpack 4
- * @param {any} originalEntry
- * @param {any} newAdditionalEntries
- * @returns {any}
- */
path: string;
}
)[];
@@ -970,6 +969,12 @@ declare class Server {
}[];
description: string;
simpleType: string;
+ /**
+ * prependEntry Method for webpack 4
+ * @param {any} originalEntry
+ * @param {any} newAdditionalEntries
+ * @returns {any}
+ */
multiple: boolean;
};
"magic-html": {
@@ -993,9 +998,10 @@ declare class Server {
path: string;
}
| {
+ /** @type {any} */
type: string;
multiple: boolean;
- description: string;
+ /** @type {any} */ description: string;
negatedDescription: string;
path: string;
}
@@ -1253,7 +1259,7 @@ declare class Server {
type: string;
values: string[];
}[];
- /** @type {ServerConfiguration} */ description: string;
+ description: string;
multiple: boolean;
simpleType: string;
};
@@ -1293,7 +1299,7 @@ declare class Server {
type: string;
multiple: boolean;
description: string;
- path: string /** @type {any} */;
+ path: string;
}[];
description: string;
simpleType: string;
@@ -1319,8 +1325,9 @@ declare class Server {
}[];
description: string;
simpleType: string;
- multiple: boolean;
+ multiple: boolean /** @type {any} */;
};
+ /** @type {any} */
"static-serve-index": {
configs: {
type: string;
@@ -1610,10 +1617,13 @@ declare class Server {
* @property {string} [protocol]
* @property {string} [username]
*/
+ /**
+ * @typedef {boolean | ((error: Error) => void)} OverlayMessageOptions
+ */
/**
* @typedef {Object} ClientConfiguration
* @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging]
- * @property {boolean | { warnings?: boolean, errors?: boolean, runtimeErrors?: boolean }} [overlay]
+ * @property {boolean | { warnings?: OverlayMessageOptions, errors?: OverlayMessageOptions, runtimeErrors?: OverlayMessageOptions }} [overlay]
* @property {boolean} [progress]
* @property {boolean | number} [reconnect]
* @property {"ws" | "sockjs" | string} [webSocketTransport]
@@ -1759,10 +1769,13 @@ declare class Server {
* @property {string} [protocol]
* @property {string} [username]
*/
+ /**
+ * @typedef {boolean | ((error: Error) => void)} OverlayMessageOptions
+ */
/**
* @typedef {Object} ClientConfiguration
* @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging]
- * @property {boolean | { warnings?: boolean, errors?: boolean, runtimeErrors?: boolean }} [overlay]
+ * @property {boolean | { warnings?: OverlayMessageOptions, errors?: OverlayMessageOptions, runtimeErrors?: OverlayMessageOptions }} [overlay]
* @property {boolean} [progress]
* @property {boolean | number} [reconnect]
* @property {"ws" | "sockjs" | string} [webSocketTransport]
@@ -1824,25 +1837,158 @@ declare class Server {
additionalProperties: boolean;
properties: {
errors: {
- description: string;
- type: string;
- cli: {
- negatedDescription: string;
- };
+ anyOf: (
+ | {
+ description: string;
+ type: string;
+ cli: {
+ negatedDescription: string;
+ };
+ instanceof?: undefined;
+ }
+ | {
+ instanceof: string;
+ /**
+ * @typedef {Object} WebSocketServerConfiguration
+ * @property {"sockjs" | "ws" | string | Function} [type]
+ * @property {Record} [options]
+ */
+ /**
+ * @typedef {(import("ws").WebSocket | import("sockjs").Connection & { send: import("ws").WebSocket["send"], terminate: import("ws").WebSocket["terminate"], ping: import("ws").WebSocket["ping"] }) & { isAlive?: boolean }} ClientConnection
+ */
+ /**
+ * @typedef {import("ws").WebSocketServer | import("sockjs").Server & { close: import("ws").WebSocketServer["close"] }} WebSocketServer
+ */
+ /**
+ * @typedef {{ implementation: WebSocketServer, clients: ClientConnection[] }} WebSocketServerImplementation
+ */
+ /**
+ * @callback ByPass
+ * @param {Request} req
+ * @param {Response} res
+ * @param {ProxyConfigArrayItem} proxyConfig
+ */
+ /**
+ * @typedef {{ path?: HttpProxyMiddlewareOptionsFilter | undefined, context?: HttpProxyMiddlewareOptionsFilter | undefined } & { bypass?: ByPass } & HttpProxyMiddlewareOptions } ProxyConfigArrayItem
+ */
+ /**
+ * @typedef {(ProxyConfigArrayItem | ((req?: Request | undefined, res?: Response | undefined, next?: NextFunction | undefined) => ProxyConfigArrayItem))[]} ProxyConfigArray
+ */
+ /**
+ * @typedef {{ [url: string]: string | ProxyConfigArrayItem }} ProxyConfigMap
+ */
+ /**
+ * @typedef {Object} OpenApp
+ * @property {string} [name]
+ * @property {string[]} [arguments]
+ */
+ /**
+ * @typedef {Object} Open
+ * @property {string | string[] | OpenApp} [app]
+ * @property {string | string[]} [target]
+ */
+ /**
+ * @typedef {Object} NormalizedOpen
+ * @property {string} target
+ * @property {import("open").Options} options
+ */
+ /**
+ * @typedef {Object} WebSocketURL
+ * @property {string} [hostname]
+ * @property {string} [password]
+ * @property {string} [pathname]
+ * @property {number | string} [port]
+ * @property {string} [protocol]
+ * @property {string} [username]
+ */
+ /**
+ * @typedef {boolean | ((error: Error) => void)} OverlayMessageOptions
+ */
+ /**
+ * @typedef {Object} ClientConfiguration
+ * @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging]
+ * @property {boolean | { warnings?: OverlayMessageOptions, errors?: OverlayMessageOptions, runtimeErrors?: OverlayMessageOptions }} [overlay]
+ * @property {boolean} [progress]
+ * @property {boolean | number} [reconnect]
+ * @property {"ws" | "sockjs" | string} [webSocketTransport]
+ * @property {string | WebSocketURL} [webSocketURL]
+ */
+ /**
+ * @typedef {Array<{ key: string; value: string }> | Record} Headers
+ */
+ /**
+ * @typedef {{ name?: string, path?: string, middleware: ExpressRequestHandler | ExpressErrorRequestHandler } | ExpressRequestHandler | ExpressErrorRequestHandler} Middleware
+ */
+ /**
+ * @typedef {Object} Configuration
+ * @property {boolean | string} [ipc]
+ * @property {Host} [host]
+ * @property {Port} [port]
+ * @property {boolean | "only"} [hot]
+ * @property {boolean} [liveReload]
+ * @property {DevMiddlewareOptions} [devMiddleware]
+ * @property {boolean} [compress]
+ * @property {boolean} [magicHtml]
+ * @property {"auto" | "all" | string | string[]} [allowedHosts]
+ * @property {boolean | ConnectHistoryApiFallbackOptions} [historyApiFallback]
+ * @property {boolean | Record | BonjourOptions} [bonjour]
+ * @property {string | string[] | WatchFiles | Array} [watchFiles]
+ * @property {boolean | string | Static | Array} [static]
+ * @property {boolean | ServerOptions} [https]
+ * @property {boolean} [http2]
+ * @property {"http" | "https" | "spdy" | string | ServerConfiguration} [server]
+ * @property {boolean | "sockjs" | "ws" | string | WebSocketServerConfiguration} [webSocketServer]
+ * @property {ProxyConfigMap | ProxyConfigArrayItem | ProxyConfigArray} [proxy]
+ * @property {boolean | string | Open | Array} [open]
+ * @property {boolean} [setupExitSignals]
+ * @property {boolean | ClientConfiguration} [client]
+ * @property {Headers | ((req: Request, res: Response, context: DevMiddlewareContext) => Headers)} [headers]
+ * @property {(devServer: Server) => void} [onAfterSetupMiddleware]
+ * @property {(devServer: Server) => void} [onBeforeSetupMiddleware]
+ * @property {(devServer: Server) => void} [onListening]
+ * @property {(middlewares: Middleware[], devServer: Server) => Middleware[]} [setupMiddlewares]
+ */
+ description: string;
+ type?: undefined;
+ cli?: undefined;
+ }
+ )[];
};
warnings: {
- description: string;
- type: string;
- cli: {
- negatedDescription: string;
- };
+ anyOf: (
+ | {
+ description: string;
+ type: string;
+ cli: {
+ negatedDescription: string;
+ };
+ instanceof?: undefined;
+ }
+ | {
+ instanceof: string;
+ description: string;
+ type?: undefined;
+ cli?: undefined;
+ }
+ )[];
};
runtimeErrors: {
- description: string;
- type: string;
- cli: {
- negatedDescription: string;
- };
+ anyOf: (
+ | {
+ description: string;
+ type: string;
+ cli: {
+ negatedDescription: string;
+ };
+ instanceof?: undefined;
+ }
+ | {
+ instanceof: string;
+ description: string;
+ type?: undefined;
+ cli?: undefined;
+ }
+ )[];
};
trustedTypesPolicyName: {
description: string;
@@ -1869,69 +2015,6 @@ declare class Server {
anyOf: (
| {
type: string;
- /**
- * @typedef {Object} Open
- * @property {string | string[] | OpenApp} [app]
- * @property {string | string[]} [target]
- */
- /**
- * @typedef {Object} NormalizedOpen
- * @property {string} target
- * @property {import("open").Options} options
- */
- /**
- * @typedef {Object} WebSocketURL
- * @property {string} [hostname]
- * @property {string} [password]
- * @property {string} [pathname]
- * @property {number | string} [port]
- * @property {string} [protocol]
- * @property {string} [username]
- */
- /**
- * @typedef {Object} ClientConfiguration
- * @property {"log" | "info" | "warn" | "error" | "none" | "verbose"} [logging]
- * @property {boolean | { warnings?: boolean, errors?: boolean, runtimeErrors?: boolean }} [overlay]
- * @property {boolean} [progress]
- * @property {boolean | number} [reconnect]
- * @property {"ws" | "sockjs" | string} [webSocketTransport]
- * @property {string | WebSocketURL} [webSocketURL]
- */
- /**
- * @typedef {Array<{ key: string; value: string }> | Record} Headers
- */
- /**
- * @typedef {{ name?: string, path?: string, middleware: ExpressRequestHandler | ExpressErrorRequestHandler } | ExpressRequestHandler | ExpressErrorRequestHandler} Middleware
- */
- /**
- * @typedef {Object} Configuration
- * @property {boolean | string} [ipc]
- * @property {Host} [host]
- * @property {Port} [port]
- * @property {boolean | "only"} [hot]
- * @property {boolean} [liveReload]
- * @property {DevMiddlewareOptions} [devMiddleware]
- * @property {boolean} [compress]
- * @property {boolean} [magicHtml]
- * @property {"auto" | "all" | string | string[]} [allowedHosts]
- * @property {boolean | ConnectHistoryApiFallbackOptions} [historyApiFallback]
- * @property {boolean | Record | BonjourOptions} [bonjour]
- * @property {string | string[] | WatchFiles | Array} [watchFiles]
- * @property {boolean | string | Static | Array} [static]
- * @property {boolean | ServerOptions} [https]
- * @property {boolean} [http2]
- * @property {"http" | "https" | "spdy" | string | ServerConfiguration} [server]
- * @property {boolean | "sockjs" | "ws" | string | WebSocketServerConfiguration} [webSocketServer]
- * @property {ProxyConfigMap | ProxyConfigArrayItem | ProxyConfigArray} [proxy]
- * @property {boolean | string | Open | Array} [open]
- * @property {boolean} [setupExitSignals]
- * @property {boolean | ClientConfiguration} [client]
- * @property {Headers | ((req: Request, res: Response, context: DevMiddlewareContext) => Headers)} [headers]
- * @property {(devServer: Server) => void} [onAfterSetupMiddleware]
- * @property {(devServer: Server) => void} [onBeforeSetupMiddleware]
- * @property {(devServer: Server) => void} [onListening]
- * @property {(middlewares: Middleware[], devServer: Server) => Middleware[]} [setupMiddlewares]
- */
cli: {
negatedDescription: string;
};
@@ -2069,6 +2152,10 @@ declare class Server {
negatedDescription: string;
};
};
+ /**
+ * @private
+ * @type {string | undefined}
+ */
ca: {
anyOf: (
| {
@@ -2332,6 +2419,7 @@ declare class Server {
| {
type: string;
description: string;
+ /** @type {{ type: WebSocketServerConfiguration["type"], options: NonNullable }} */
link: string;
cli?: undefined;
}
@@ -2366,7 +2454,7 @@ declare class Server {
}
| {
enum: string[];
- type?: undefined;
+ /** @type {string} */ type?: undefined;
cli?: undefined;
}
)[];
@@ -2433,7 +2521,7 @@ declare class Server {
}
| {
$ref: string;
- /** @type {string} */ type?: undefined;
+ type?: undefined;
items?: undefined;
}
)[];
@@ -2570,7 +2658,7 @@ declare class Server {
}
)[];
description: string;
- /** @type {Object} */ link: string;
+ link: string;
};
Server: {
anyOf: {
@@ -2590,7 +2678,7 @@ declare class Server {
};
ServerString: {
type: string;
- minLength: number;
+ /** @type {string} */ minLength: number;
cli: {
exclude: boolean;
};
@@ -2786,6 +2874,7 @@ declare class Server {
)[];
description: string;
};
+ /** @type {NormalizedStatic} */
pfx: {
anyOf: (
| {
@@ -3059,7 +3148,6 @@ declare class Server {
};
};
additionalProperties: boolean;
- /** @type {ServerOptions} */
properties: {
allowedHosts: {
$ref: string;
@@ -3121,7 +3209,6 @@ declare class Server {
proxy: {
$ref: string;
};
- /** @type {any} */
server: {
$ref: string;
};
@@ -3502,6 +3589,7 @@ declare namespace Server {
Open,
NormalizedOpen,
WebSocketURL,
+ OverlayMessageOptions,
ClientConfiguration,
Headers,
Middleware,
@@ -3720,14 +3808,15 @@ type WebSocketURL = {
protocol?: string | undefined;
username?: string | undefined;
};
+type OverlayMessageOptions = boolean | ((error: Error) => void);
type ClientConfiguration = {
logging?: "none" | "error" | "warn" | "info" | "log" | "verbose" | undefined;
overlay?:
| boolean
| {
- warnings?: boolean | undefined;
- errors?: boolean | undefined;
- runtimeErrors?: boolean | undefined;
+ warnings?: OverlayMessageOptions | undefined;
+ errors?: OverlayMessageOptions | undefined;
+ runtimeErrors?: OverlayMessageOptions | undefined;
}
| undefined;
progress?: boolean | undefined;