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": " `; + +exports[`overlay should show error for uncaught runtime error: overlay html 1`] = ` +" +
+
+ Uncaught runtime errors: +
+ +
+
+
+ ERROR +
+
+ Injected error at throwError (<anonymous>:2:15) at + <anonymous>:3:9 at addScriptContent + (__puppeteer_evaluation_script__:9:27) +
+
+
+
+ +" +`; + +exports[`overlay should show error when it is not filtered: overlay html 1`] = ` +" +
+
+ Compiled with problems: +
+ +
+
+
+ ERROR +
+
+ Unfiltered error +
+
+
+
+ +" +`; + +exports[`overlay should show error when it is not filtered: page html 1`] = ` +" +

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/__snapshots__/overlay.test.js.snap.webpack5 b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 index 33fc1dbc70..49990c3d43 100644 --- a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 @@ -2152,6 +2152,197 @@ exports[`overlay should show an error when "client.overlay.warnings" is "true": " `; +exports[`overlay should show error for uncaught runtime error: overlay html 1`] = ` +" +
+
+ Uncaught runtime errors: +
+ +
+
+
+ ERROR +
+
+ Injected error at throwError (<anonymous>:2:15) at + <anonymous>:3:9 at addScriptContent + (__puppeteer_evaluation_script__:9:27) +
+
+
+
+ +" +`; + +exports[`overlay should show error when it is not filtered: overlay html 1`] = ` +" +
+
+ Compiled with problems: +
+ +
+
+
+ ERROR +
+
+ Unfiltered error +
+
+
+
+ +" +`; + +exports[`overlay should show error when it is not filtered: page html 1`] = ` +" +

webpack-dev-server is running...

+ + + + +" +`; + exports[`overlay should show overlay when Trusted Types are enabled: overlay html 1`] = ` "
" `; + +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;