From 9a60e88afd00571a800428a61312d0595564599a Mon Sep 17 00:00:00 2001 From: Malcolm Kee Date: Mon, 17 Oct 2022 00:45:00 +1100 Subject: [PATCH 1/8] feat: catch runtime error --- client-src/index.js | 35 ++- client-src/overlay.js | 356 +++++++++++++----------- client-src/overlay/runtime-error.js | 20 ++ client-src/overlay/state-machine.js | 89 ++++++ examples/client/overlay/app.js | 5 + examples/client/overlay/error-button.js | 12 + package-lock.json | 11 + package.json | 1 + 8 files changed, 359 insertions(+), 170 deletions(-) create mode 100644 client-src/overlay/runtime-error.js create mode 100644 client-src/overlay/state-machine.js create mode 100644 examples/client/overlay/error-button.js diff --git a/client-src/index.js b/client-src/index.js index 7ea51c0c63..706c77cfe1 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -4,7 +4,7 @@ import webpackHotLog from "webpack/hot/log.js"; import stripAnsi from "./utils/stripAnsi.js"; import parseURL from "./utils/parseURL.js"; import socket from "./socket.js"; -import { formatProblem, show, hide } from "./overlay.js"; +import { formatProblem, createOverlay } from "./overlay.js"; import { log, logEnabledFeatures, setLogLevel } from "./utils/log.js"; import sendMessage from "./utils/sendMessage.js"; import reloadApp from "./utils/reloadApp.js"; @@ -115,6 +115,13 @@ self.addEventListener("beforeunload", () => { status.isUnloading = true; }); +const trustedTypesPolicyName = + typeof options.overlay === "object" && options.overlay.trustedTypesPolicyName; + +const overlay = createOverlay({ + trustedTypesPolicyName, +}); + const onSocketMessage = { hot() { if (parsedResourceQuery.hot === "false") { @@ -135,7 +142,7 @@ const onSocketMessage = { // Fixes #1042. overlay doesn't clear if errors are fixed but warnings remain. if (options.overlay) { - hide(); + overlay.send("DISMISS"); } sendMessage("Invalid"); @@ -192,7 +199,7 @@ const onSocketMessage = { log.info("Nothing changed."); if (options.overlay) { - hide(); + overlay.send("DISMISS"); } sendMessage("StillOk"); @@ -201,7 +208,7 @@ const onSocketMessage = { sendMessage("Ok"); if (options.overlay) { - hide(); + overlay.send("DISMISS"); } reloadApp(options, status); @@ -256,10 +263,11 @@ const onSocketMessage = { : options.overlay && options.overlay.warnings; if (needShowOverlayForWarnings) { - const trustedTypesPolicyName = - typeof options.overlay === "object" && - options.overlay.trustedTypesPolicyName; - show("warning", warnings, trustedTypesPolicyName || null); + overlay.send({ + type: "BUILD_ERROR", + level: "warning", + messages: warnings, + }); } if (params && params.preventReloading) { @@ -292,10 +300,11 @@ const onSocketMessage = { : options.overlay && options.overlay.errors; if (needShowOverlayForErrors) { - const trustedTypesPolicyName = - typeof options.overlay === "object" && - options.overlay.trustedTypesPolicyName; - show("error", errors, trustedTypesPolicyName || null); + overlay.send({ + type: "BUILD_ERROR", + level: "error", + messages: errors, + }); } }, /** @@ -308,7 +317,7 @@ const onSocketMessage = { log.info("Disconnected!"); if (options.overlay) { - hide(); + overlay.send("DISMISS"); } sendMessage("Close"); diff --git a/client-src/overlay.js b/client-src/overlay.js index 4e7909b370..7e2ac75e96 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -3,6 +3,7 @@ import ansiHTML from "ansi-html-community"; import { encode } from "html-entities"; +import { interpret } from "@xstate/fsm"; import { containerStyle, dismissButtonStyle, @@ -12,6 +13,8 @@ import { msgTextStyle, msgTypeStyle, } from "./overlay/styles.js"; +import { listenToRuntimeError } from "./overlay/runtime-error.js"; +import createOverlayMachine from "./overlay/state-machine.js"; const colors = { reset: ["transparent", "transparent"], @@ -26,127 +29,8 @@ const colors = { darkgrey: "6D7891", }; -/** @type {HTMLIFrameElement | null | undefined} */ -let iframeContainerElement; -/** @type {HTMLDivElement | null | undefined} */ -let containerElement; -/** @type {Array<(element: HTMLDivElement) => void>} */ -let onLoadQueue = []; -/** @type {TrustedTypePolicy | undefined} */ -let overlayTrustedTypesPolicy; - ansiHTML.setColors(colors); -/** - * - * @param {HTMLElement} element - * @param {CSSStyleDeclaration} style - */ -function applyStyle(element, style) { - Object.keys(style).forEach((prop) => { - element.style[prop] = style[prop]; - }); -} - -/** - * @param {string | null} trustedTypesPolicyName - */ -function createContainer(trustedTypesPolicyName) { - // Enable Trusted Types if they are available in the current browser. - if (window.trustedTypes) { - overlayTrustedTypesPolicy = window.trustedTypes.createPolicy( - trustedTypesPolicyName || "webpack-dev-server#overlay", - { - createHTML: (value) => value, - } - ); - } - - iframeContainerElement = document.createElement("iframe"); - iframeContainerElement.id = "webpack-dev-server-client-overlay"; - iframeContainerElement.src = "about:blank"; - applyStyle(iframeContainerElement, iframeStyle); - iframeContainerElement.onload = () => { - containerElement = - /** @type {Document} */ - ( - /** @type {HTMLIFrameElement} */ - (iframeContainerElement).contentDocument - ).createElement("div"); - - containerElement.id = "webpack-dev-server-client-overlay-div"; - applyStyle(containerElement, containerStyle); - - const headerElement = document.createElement("div"); - - headerElement.innerText = "Compiled with problems:"; - applyStyle(headerElement, headerStyle); - - const closeButtonElement = document.createElement("button"); - - applyStyle(closeButtonElement, dismissButtonStyle); - - closeButtonElement.innerText = "×"; - closeButtonElement.ariaLabel = "Dismiss"; - closeButtonElement.addEventListener("click", () => { - hide(); - }); - - containerElement.appendChild(headerElement); - containerElement.appendChild(closeButtonElement); - - /** @type {Document} */ - ( - /** @type {HTMLIFrameElement} */ - (iframeContainerElement).contentDocument - ).body.appendChild(containerElement); - - onLoadQueue.forEach((onLoad) => { - onLoad(/** @type {HTMLDivElement} */ (containerElement)); - }); - onLoadQueue = []; - - /** @type {HTMLIFrameElement} */ - (iframeContainerElement).onload = null; - }; - - document.body.appendChild(iframeContainerElement); -} - -/** - * @param {(element: HTMLDivElement) => void} callback - * @param {string | null} trustedTypesPolicyName - */ -function ensureOverlayExists(callback, trustedTypesPolicyName) { - if (containerElement) { - // Everything is ready, call the callback right away. - callback(containerElement); - - return; - } - - onLoadQueue.push(callback); - - if (iframeContainerElement) { - return; - } - - createContainer(trustedTypesPolicyName); -} - -// Successful compilation. -function hide() { - if (!iframeContainerElement) { - return; - } - - // Clean up and reset internal state. - document.body.removeChild(iframeContainerElement); - - iframeContainerElement = null; - containerElement = null; -} - /** * @param {string} type * @param {string | { file?: string, moduleName?: string, loc?: string, message?: string }} item @@ -181,54 +65,212 @@ function formatProblem(type, item) { return { header, body }; } -// Compilation with errors (e.g. syntax error or missing modules). /** - * @param {string} type - * @param {Array} messages - * @param {string | null} trustedTypesPolicyName + * @typedef {Object} CreateOverlayOptions + * @property {string | null} trustedTypesPolicyName */ -function show(type, messages, trustedTypesPolicyName) { - ensureOverlayExists(() => { - messages.forEach((message) => { - const entryElement = document.createElement("div"); - const msgStyle = type === "warning" ? msgStyles.warning : msgStyles.error; - applyStyle(entryElement, { - ...msgStyle, - padding: "1rem 1rem 1.5rem 1rem", + +/** + * + * @param {CreateOverlayOptions} options + */ +const createOverlay = (options) => { + /** @type {HTMLIFrameElement | null | undefined} */ + let iframeContainerElement; + /** @type {HTMLDivElement | null | undefined} */ + let containerElement; + /** @type {Array<(element: HTMLDivElement) => void>} */ + let onLoadQueue = []; + /** @type {TrustedTypePolicy | undefined} */ + let overlayTrustedTypesPolicy; + + /** + * + * @param {HTMLElement} element + * @param {CSSStyleDeclaration} style + */ + function applyStyle(element, style) { + Object.keys(style).forEach((prop) => { + element.style[prop] = style[prop]; + }); + } + + /** + * @param {string | null} trustedTypesPolicyName + */ + function createContainer(trustedTypesPolicyName) { + // Enable Trusted Types if they are available in the current browser. + if (window.trustedTypes) { + overlayTrustedTypesPolicy = window.trustedTypes.createPolicy( + trustedTypesPolicyName || "webpack-dev-server#overlay", + { + createHTML: (value) => value, + } + ); + } + + iframeContainerElement = document.createElement("iframe"); + iframeContainerElement.id = "webpack-dev-server-client-overlay"; + iframeContainerElement.src = "about:blank"; + applyStyle(iframeContainerElement, iframeStyle); + iframeContainerElement.onload = () => { + const contentElement = + /** @type {Document} */ + ( + /** @type {HTMLIFrameElement} */ + (iframeContainerElement).contentDocument + ).createElement("div"); + containerElement = + /** @type {Document} */ + ( + /** @type {HTMLIFrameElement} */ + (iframeContainerElement).contentDocument + ).createElement("div"); + + contentElement.id = "webpack-dev-server-client-overlay-div"; + applyStyle(contentElement, containerStyle); + + const headerElement = document.createElement("div"); + + headerElement.innerText = "Compiled with problems:"; + applyStyle(headerElement, headerStyle); + + const closeButtonElement = document.createElement("button"); + + applyStyle(closeButtonElement, dismissButtonStyle); + + closeButtonElement.innerText = "×"; + closeButtonElement.ariaLabel = "Dismiss"; + closeButtonElement.addEventListener("click", () => { + // eslint-disable-next-line no-use-before-define + overlayService.send("DISMISS"); + }); + + contentElement.appendChild(headerElement); + contentElement.appendChild(closeButtonElement); + contentElement.appendChild(containerElement); + + /** @type {Document} */ + ( + /** @type {HTMLIFrameElement} */ + (iframeContainerElement).contentDocument + ).body.appendChild(contentElement); + + onLoadQueue.forEach((onLoad) => { + onLoad(/** @type {HTMLDivElement} */ (contentElement)); }); + onLoadQueue = []; + + /** @type {HTMLIFrameElement} */ + (iframeContainerElement).onload = null; + }; + + document.body.appendChild(iframeContainerElement); + } - const typeElement = document.createElement("div"); - const { header, body } = formatProblem(type, message); + /** + * @param {(element: HTMLDivElement) => void} callback + * @param {string | null} trustedTypesPolicyName + */ + function ensureOverlayExists(callback, trustedTypesPolicyName) { + if (containerElement) { + containerElement.innerHTML = ""; + // Everything is ready, call the callback right away. + callback(containerElement); - typeElement.innerText = header; - applyStyle(typeElement, msgTypeStyle); + return; + } - if (message.moduleIdentifier) { - applyStyle(typeElement, { cursor: "pointer" }); - typeElement.dataset.canOpen = true; - typeElement.addEventListener("click", () => { - fetch( - `/webpack-dev-server/open-editor?fileName=${message.moduleIdentifier}` - ); + onLoadQueue.push(callback); + + if (iframeContainerElement) { + return; + } + + createContainer(trustedTypesPolicyName); + } + + // Successful compilation. + function hide() { + if (!iframeContainerElement) { + return; + } + + // Clean up and reset internal state. + document.body.removeChild(iframeContainerElement); + + iframeContainerElement = null; + containerElement = null; + } + + // Compilation with errors (e.g. syntax error or missing modules). + /** + * @param {string} type + * @param {Array} messages + * @param {string | null} trustedTypesPolicyName + */ + function show(type, messages, trustedTypesPolicyName) { + ensureOverlayExists(() => { + messages.forEach((message) => { + const entryElement = document.createElement("div"); + const msgStyle = + type === "warning" ? msgStyles.warning : msgStyles.error; + applyStyle(entryElement, { + ...msgStyle, + padding: "1rem 1rem 1.5rem 1rem", }); - } - // Make it look similar to our terminal. - const text = ansiHTML(encode(body)); - const messageTextNode = document.createElement("div"); - applyStyle(messageTextNode, msgTextStyle); + const typeElement = document.createElement("div"); + const { header, body } = formatProblem(type, message); + + typeElement.innerText = header; + applyStyle(typeElement, msgTypeStyle); + + if (message.moduleIdentifier) { + applyStyle(typeElement, { cursor: "pointer" }); + typeElement.dataset.canOpen = true; + typeElement.addEventListener("click", () => { + fetch( + `/webpack-dev-server/open-editor?fileName=${message.moduleIdentifier}` + ); + }); + } + + // Make it look similar to our terminal. + const text = ansiHTML(encode(body)); + const messageTextNode = document.createElement("div"); + applyStyle(messageTextNode, msgTextStyle); - messageTextNode.innerHTML = overlayTrustedTypesPolicy - ? overlayTrustedTypesPolicy.createHTML(text) - : text; + messageTextNode.innerHTML = overlayTrustedTypesPolicy + ? overlayTrustedTypesPolicy.createHTML(text) + : text; - entryElement.appendChild(typeElement); - entryElement.appendChild(messageTextNode); + entryElement.appendChild(typeElement); + entryElement.appendChild(messageTextNode); - /** @type {HTMLDivElement} */ - (containerElement).appendChild(entryElement); + /** @type {HTMLDivElement} */ + (containerElement).appendChild(entryElement); + }); + }, trustedTypesPolicyName); + } + + const overlayMachine = createOverlayMachine({ + showOverlay: ({ level = "error", messages }) => + show(level, messages, options.trustedTypesPolicyName), + hideOverlay: hide, + }); + + const overlayService = interpret(overlayMachine).start(); + + listenToRuntimeError((err) => { + console.log(err); + overlayService.send({ + type: "RUNTIME_ERROR", + messages: [err.message], }); - }, trustedTypesPolicyName); -} + }); + + return overlayService; +}; -export { formatProblem, show, hide }; +export { formatProblem, createOverlay }; diff --git a/client-src/overlay/runtime-error.js b/client-src/overlay/runtime-error.js new file mode 100644 index 0000000000..3c0210d6d3 --- /dev/null +++ b/client-src/overlay/runtime-error.js @@ -0,0 +1,20 @@ +/** + * @callback ErrorCallback + * @param {ErrorEvent} error + * @returns {void} + */ + +/** + * @param {ErrorCallback} callback + */ +function listenToRuntimeError(callback) { + window.addEventListener("error", callback); + + return function cleanup() { + window.removeEventListener("error", callback); + }; +} + +function listenToSomething() {} + +export { listenToRuntimeError, listenToSomething }; diff --git a/client-src/overlay/state-machine.js b/client-src/overlay/state-machine.js new file mode 100644 index 0000000000..5f322daa3f --- /dev/null +++ b/client-src/overlay/state-machine.js @@ -0,0 +1,89 @@ +import { createMachine, assign } from "@xstate/fsm"; + +/** + * @typedef {Object} ShowOverlayData + * @property {'warning' | 'error'} level + * @property {Array} messages + */ + +/** + * @typedef {Object} CreateOverlayMachineOptions + * @property {(data: ShowOverlayData) => void} showOverlay + * @property {() => void} hideOverlay + */ + +/** + * @param {CreateOverlayMachineOptions} options + */ +const createOverlayMachine = (options) => { + const { hideOverlay, showOverlay } = options; + const overlayMachine = createMachine( + { + id: "overlay", + initial: "hidden", + context: { + level: "error", + messages: [], + }, + states: { + hidden: { + entry: "hideOverlay", + on: { + BUILD_ERROR: { + target: "displayBuildError", + actions: ["setMessages", "showOverlay"], + }, + RUNTIME_ERROR: { + target: "displayRuntimeError", + actions: ["setMessages", "showOverlay"], + }, + }, + }, + displayBuildError: { + on: { + DISMISS: { target: "hidden", actions: "dismissMessages" }, + BUILD_ERROR: { + target: "displayBuildError", + actions: ["appendMessages", "showOverlay"], + }, + }, + }, + displayRuntimeError: { + on: { + DISMISS: { target: "hidden", actions: "dismissMessages" }, + RUNTIME_ERROR: { + target: "displayRuntimeError", + actions: ["appendMessages", "showOverlay"], + }, + BUILD_ERROR: { + target: "displayBuildError", + actions: ["setMessages", "showOverlay"], + }, + }, + }, + }, + }, + { + actions: { + dismissMessages: assign({ + messages: [], + level: "error", + }), + appendMessages: assign({ + messages: (context, event) => context.messages.concat(event.messages), + level: (context, event) => event.level || context.level, + }), + setMessages: assign({ + messages: (_, event) => event.messages, + level: (context, event) => event.level || context.level, + }), + hideOverlay, + showOverlay, + }, + } + ); + + return overlayMachine; +}; + +export default createOverlayMachine; diff --git a/examples/client/overlay/app.js b/examples/client/overlay/app.js index 77142167f0..a4344aa340 100644 --- a/examples/client/overlay/app.js +++ b/examples/client/overlay/app.js @@ -1,7 +1,12 @@ "use strict"; +// eslint-disable-next-line import/order +const createErrorBtn = require("./error-button"); + const target = document.querySelector("#target"); +target.insertAdjacentElement("afterend", createErrorBtn()); + // 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 new file mode 100644 index 0000000000..dcd695b67e --- /dev/null +++ b/examples/client/overlay/error-button.js @@ -0,0 +1,12 @@ +"use strict"; + +module.exports = function createErrorButton() { + const errorBtn = document.createElement("button"); + + errorBtn.addEventListener("click", () => { + throw new Error("runtime error!"); + }); + errorBtn.innerHTML = "Click to throw error"; + + return errorBtn; +}; diff --git a/package-lock.json b/package-lock.json index 3d98071ead..27b85a4e32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@types/serve-static": "^1.13.10", "@types/sockjs": "^0.3.33", "@types/ws": "^8.5.1", + "@xstate/fsm": "^2.0.0", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.0.11", "chokidar": "^3.5.3", @@ -3998,6 +3999,11 @@ } } }, + "node_modules/@xstate/fsm": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-2.0.0.tgz", + "integrity": "sha512-p/zcvBMoU2ap5byMefLkR+AM+Eh99CU/SDEQeccgKlmFNOMDwphaRGqdk+emvel/SaGZ7Rf9sDvzAplLzLdEVQ==" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -18764,6 +18770,11 @@ "dev": true, "requires": {} }, + "@xstate/fsm": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-2.0.0.tgz", + "integrity": "sha512-p/zcvBMoU2ap5byMefLkR+AM+Eh99CU/SDEQeccgKlmFNOMDwphaRGqdk+emvel/SaGZ7Rf9sDvzAplLzLdEVQ==" + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", diff --git a/package.json b/package.json index 802a42f333..a2fb0226d4 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@types/serve-static": "^1.13.10", "@types/sockjs": "^0.3.33", "@types/ws": "^8.5.1", + "@xstate/fsm": "^2.0.0", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.0.11", "chokidar": "^3.5.3", From 178066d73296ade39e2154a33c2ffe6f00827494 Mon Sep 17 00:00:00 2001 From: Malcolm Kee Date: Sun, 27 Nov 2022 12:25:38 +1100 Subject: [PATCH 2/8] fix: remove xstate --- client-src/index.js | 8 +- client-src/overlay.js | 8 +- client-src/overlay/fsm.js | 57 + client-src/overlay/state-machine.js | 44 +- package-lock.json | 11 - package.json | 1 - test/client/index.test.js | 63 +- .../multi-compiler.test.js.snap.webpack5 | 12 +- .../overlay.test.js.snap.webpack5 | 1110 +++++++++-------- 9 files changed, 717 insertions(+), 597 deletions(-) create mode 100644 client-src/overlay/fsm.js diff --git a/client-src/index.js b/client-src/index.js index 706c77cfe1..981bb99ee8 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -142,7 +142,7 @@ const onSocketMessage = { // Fixes #1042. overlay doesn't clear if errors are fixed but warnings remain. if (options.overlay) { - overlay.send("DISMISS"); + overlay.send({ type: "DISMISS" }); } sendMessage("Invalid"); @@ -199,7 +199,7 @@ const onSocketMessage = { log.info("Nothing changed."); if (options.overlay) { - overlay.send("DISMISS"); + overlay.send({ type: "DISMISS" }); } sendMessage("StillOk"); @@ -208,7 +208,7 @@ const onSocketMessage = { sendMessage("Ok"); if (options.overlay) { - overlay.send("DISMISS"); + overlay.send({ type: "DISMISS" }); } reloadApp(options, status); @@ -317,7 +317,7 @@ const onSocketMessage = { log.info("Disconnected!"); if (options.overlay) { - overlay.send("DISMISS"); + overlay.send({ type: "DISMISS" }); } sendMessage("Close"); diff --git a/client-src/overlay.js b/client-src/overlay.js index 7e2ac75e96..d475a37500 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -3,7 +3,6 @@ import ansiHTML from "ansi-html-community"; import { encode } from "html-entities"; -import { interpret } from "@xstate/fsm"; import { containerStyle, dismissButtonStyle, @@ -143,7 +142,7 @@ const createOverlay = (options) => { closeButtonElement.ariaLabel = "Dismiss"; closeButtonElement.addEventListener("click", () => { // eslint-disable-next-line no-use-before-define - overlayService.send("DISMISS"); + overlayService.send({ type: "DISMISS" }); }); contentElement.appendChild(headerElement); @@ -254,16 +253,13 @@ const createOverlay = (options) => { }, trustedTypesPolicyName); } - const overlayMachine = createOverlayMachine({ + const overlayService = createOverlayMachine({ showOverlay: ({ level = "error", messages }) => show(level, messages, options.trustedTypesPolicyName), hideOverlay: hide, }); - const overlayService = interpret(overlayMachine).start(); - listenToRuntimeError((err) => { - console.log(err); overlayService.send({ type: "RUNTIME_ERROR", messages: [err.message], diff --git a/client-src/overlay/fsm.js b/client-src/overlay/fsm.js new file mode 100644 index 0000000000..95abd60f58 --- /dev/null +++ b/client-src/overlay/fsm.js @@ -0,0 +1,57 @@ +/** + * @typedef {Object} StateDefinitions + * @property {{[event: string]: { target: string; actions?: Array }}} [on] + */ + +/** + * @typedef {Object} Options + * @property {{[state: string]: StateDefinitions}} states + * @property {object} context; + * @property {string} initial + */ + +/** + * @typedef {Object} Implementation + * @property {{[actionName: string]: (ctx: object, event: any) => object}} actions + */ + +/** + * A simplified `createMachine` from `@xstate/fsm` with the following differences: + * + * - the returned machine is technically a "service". No `interpret(machine).start()` is needed. + * - the state definition only support `on` and target must be declared with { target: 'nextState', actions: [] } explicitly. + * - event passed to `send` must be an object with `type` property. + * - actions implementation will be [assign action](https://xstate.js.org/docs/guides/context.html#assign-action) if you return any value. + * Do not return anything if you just want to invoke side effect. + * + * The goal of this custom function is to avoid installing the entire `'xstate/fsm'` package, while enabling modeling using + * state machine. You can copy the first parameter into the editor at https://stately.ai/viz to visualize the state machine. + * + * @param {Options} options + * @param {Implementation} implementation + */ +function createMachine({ states, context, initial }, { actions }) { + let currentState = initial; + let currentContext = context; + + return { + send: (event) => { + const transitionConfig = states[currentState].on?.[event.type]; + + if (transitionConfig) { + currentState = transitionConfig.target; + transitionConfig.actions?.forEach((actName) => { + const nextContextValue = actions[actName]?.(currentContext, event); + if (nextContextValue) { + currentContext = { + ...currentContext, + ...nextContextValue, + }; + } + }); + } + }, + }; +} + +export default createMachine; diff --git a/client-src/overlay/state-machine.js b/client-src/overlay/state-machine.js index 5f322daa3f..d9ed764198 100644 --- a/client-src/overlay/state-machine.js +++ b/client-src/overlay/state-machine.js @@ -1,4 +1,4 @@ -import { createMachine, assign } from "@xstate/fsm"; +import createMachine from "./fsm.js"; /** * @typedef {Object} ShowOverlayData @@ -19,7 +19,6 @@ const createOverlayMachine = (options) => { const { hideOverlay, showOverlay } = options; const overlayMachine = createMachine( { - id: "overlay", initial: "hidden", context: { level: "error", @@ -27,7 +26,6 @@ const createOverlayMachine = (options) => { }, states: { hidden: { - entry: "hideOverlay", on: { BUILD_ERROR: { target: "displayBuildError", @@ -41,7 +39,10 @@ const createOverlayMachine = (options) => { }, displayBuildError: { on: { - DISMISS: { target: "hidden", actions: "dismissMessages" }, + DISMISS: { + target: "hidden", + actions: ["dismissMessages", "hideOverlay"], + }, BUILD_ERROR: { target: "displayBuildError", actions: ["appendMessages", "showOverlay"], @@ -50,7 +51,10 @@ const createOverlayMachine = (options) => { }, displayRuntimeError: { on: { - DISMISS: { target: "hidden", actions: "dismissMessages" }, + DISMISS: { + target: "hidden", + actions: ["dismissMessages", "hideOverlay"], + }, RUNTIME_ERROR: { target: "displayRuntimeError", actions: ["appendMessages", "showOverlay"], @@ -65,18 +69,24 @@ const createOverlayMachine = (options) => { }, { actions: { - dismissMessages: assign({ - messages: [], - level: "error", - }), - appendMessages: assign({ - messages: (context, event) => context.messages.concat(event.messages), - level: (context, event) => event.level || context.level, - }), - setMessages: assign({ - messages: (_, event) => event.messages, - level: (context, event) => event.level || context.level, - }), + dismissMessages: () => { + return { + messages: [], + level: "error", + }; + }, + appendMessages: (context, event) => { + return { + messages: context.messages.concat(event.messages), + level: event.level || context.level, + }; + }, + setMessages: (context, event) => { + return { + messages: event.messages, + level: event.level || context.level, + }; + }, hideOverlay, showOverlay, }, diff --git a/package-lock.json b/package-lock.json index 27b85a4e32..3d98071ead 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "@types/serve-static": "^1.13.10", "@types/sockjs": "^0.3.33", "@types/ws": "^8.5.1", - "@xstate/fsm": "^2.0.0", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.0.11", "chokidar": "^3.5.3", @@ -3999,11 +3998,6 @@ } } }, - "node_modules/@xstate/fsm": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-2.0.0.tgz", - "integrity": "sha512-p/zcvBMoU2ap5byMefLkR+AM+Eh99CU/SDEQeccgKlmFNOMDwphaRGqdk+emvel/SaGZ7Rf9sDvzAplLzLdEVQ==" - }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -18770,11 +18764,6 @@ "dev": true, "requires": {} }, - "@xstate/fsm": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-2.0.0.tgz", - "integrity": "sha512-p/zcvBMoU2ap5byMefLkR+AM+Eh99CU/SDEQeccgKlmFNOMDwphaRGqdk+emvel/SaGZ7Rf9sDvzAplLzLdEVQ==" - }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", diff --git a/package.json b/package.json index a2fb0226d4..802a42f333 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "@types/serve-static": "^1.13.10", "@types/sockjs": "^0.3.33", "@types/ws": "^8.5.1", - "@xstate/fsm": "^2.0.0", "ansi-html-community": "^0.0.8", "bonjour-service": "^1.0.11", "chokidar": "^3.5.3", diff --git a/test/client/index.test.js b/test/client/index.test.js index a1ed197be5..c3883e5fd4 100644 --- a/test/client/index.test.js +++ b/test/client/index.test.js @@ -35,15 +35,21 @@ describe("index", () => { jest.setMock("../../client-src/socket.js", jest.fn()); socket = require("../../client-src/socket"); + const send = jest.fn(); + // overlay jest.setMock("../../client-src/overlay.js", { - hide: jest.fn(), - show: jest.fn(), + createOverlay: () => { + return { + send, + }; + }, formatProblem: (item) => { return { header: "HEADER warning", body: `BODY: ${item}` }; }, }); - overlay = require("../../client-src/overlay"); + const { createOverlay } = require("../../client-src/overlay"); + overlay = createOverlay(); // reloadApp jest.setMock("../../client-src/utils/reloadApp.js", jest.fn()); @@ -89,13 +95,13 @@ describe("index", () => { expect(log.log.info.mock.calls[0][0]).toMatchSnapshot(); expect(sendMessage.mock.calls[0][0]).toMatchSnapshot(); - expect(overlay.hide).not.toBeCalled(); + expect(overlay.send).not.toBeCalledWith({ type: "DISMISS" }); // change flags onSocketMessage.overlay(true); onSocketMessage["still-ok"](); - expect(overlay.hide).toBeCalled(); + expect(overlay.send).toHaveBeenCalledWith({ type: "DISMISS" }); }); test("should run onSocketMessage.progress and onSocketMessage['progress-update']", () => { @@ -191,9 +197,14 @@ describe("index", () => { // change flags onSocketMessage.overlay({ warnings: true }); - onSocketMessage.warnings([]); + onSocketMessage.warnings(["warning message"]); - expect(overlay.show).toBeCalled(); + expect(overlay.send).toHaveBeenCalledTimes(1); + expect(overlay.send).toHaveBeenCalledWith({ + type: "BUILD_ERROR", + level: "warning", + messages: ["warning message"], + }); }); test("should parse overlay options from resource query", () => { @@ -202,17 +213,22 @@ describe("index", () => { global.__resourceQuery = `?overlay=${encodeURIComponent( `{"warnings": false}` )}`; - overlay.show.mockReset(); + overlay.send.mockReset(); socket.mockReset(); jest.unmock("../../client-src/utils/parseURL.js"); require("../../client-src"); onSocketMessage = socket.mock.calls[0][1]; onSocketMessage.warnings(["warn1"]); - expect(overlay.show).not.toBeCalled(); + expect(overlay.send).not.toBeCalled(); onSocketMessage.errors(["error1"]); - expect(overlay.show).toBeCalledTimes(1); + expect(overlay.send).toBeCalledTimes(1); + expect(overlay.send).toHaveBeenCalledWith({ + type: "BUILD_ERROR", + level: "error", + messages: ["error1"], + }); }); jest.isolateModules(() => { @@ -220,17 +236,22 @@ describe("index", () => { global.__resourceQuery = `?overlay=${encodeURIComponent( `{"errors": false}` )}`; - overlay.show.mockReset(); + overlay.send.mockReset(); socket.mockReset(); jest.unmock("../../client-src/utils/parseURL.js"); require("../../client-src"); onSocketMessage = socket.mock.calls[0][1]; onSocketMessage.errors(["error1"]); - expect(overlay.show).not.toBeCalled(); + expect(overlay.send).not.toBeCalled(); onSocketMessage.warnings(["warn1"]); - expect(overlay.show).toBeCalledTimes(1); + expect(overlay.send).toBeCalledTimes(1); + expect(overlay.send).toHaveBeenCalledWith({ + type: "BUILD_ERROR", + level: "warning", + messages: ["warn1"], + }); }); jest.isolateModules(() => { @@ -238,15 +259,25 @@ describe("index", () => { global.__resourceQuery = "?overlay=true"; jest.unmock("../../client-src/utils/parseURL.js"); socket.mockReset(); - overlay.show.mockReset(); + overlay.send.mockReset(); require("../../client-src"); onSocketMessage = socket.mock.calls[0][1]; onSocketMessage.warnings(["warn2"]); - expect(overlay.show).toBeCalledTimes(1); + expect(overlay.send).toBeCalledTimes(1); + expect(overlay.send).toHaveBeenLastCalledWith({ + type: "BUILD_ERROR", + level: "warning", + messages: ["warn2"], + }); onSocketMessage.errors(["error2"]); - expect(overlay.show).toBeCalledTimes(2); + expect(overlay.send).toBeCalledTimes(2); + expect(overlay.send).toHaveBeenLastCalledWith({ + type: "BUILD_ERROR", + level: "error", + messages: ["error2"], + }); }); }); diff --git a/test/e2e/__snapshots__/multi-compiler.test.js.snap.webpack5 b/test/e2e/__snapshots__/multi-compiler.test.js.snap.webpack5 index 764980bfe3..5da317a992 100644 --- a/test/e2e/__snapshots__/multi-compiler.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/multi-compiler.test.js.snap.webpack5 @@ -51,13 +51,13 @@ Array [ "[HMR] Cannot apply update. Need to do a full reload!", "[HMR] Error: Aborted because ./browser.js is not accepted Update propagation: ./browser.js - at applyHandler (http://127.0.0.1:8103/browser.js:1044:31) - at http://127.0.0.1:8103/browser.js:743:21 + at applyHandler (http://127.0.0.1:8103/browser.js:1077:31) + at http://127.0.0.1:8103/browser.js:776:21 at Array.map () - at internalApply (http://127.0.0.1:8103/browser.js:742:54) - at http://127.0.0.1:8103/browser.js:712:26 - at waitForBlockingPromises (http://127.0.0.1:8103/browser.js:666:48) - at http://127.0.0.1:8103/browser.js:710:24", + at internalApply (http://127.0.0.1:8103/browser.js:775:54) + at http://127.0.0.1:8103/browser.js:745:26 + at waitForBlockingPromises (http://127.0.0.1:8103/browser.js:699:48) + at http://127.0.0.1:8103/browser.js:743:24", "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading disabled, Progress disabled, Overlay enabled.", "[HMR] Waiting for update signal from WDS...", "Hello from the browser", diff --git a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 index c85c4cf4cc..33fc1dbc70 100644 --- a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 @@ -82,36 +82,38 @@ exports[`overlay should not show initially, then show on an error and allow to c > × -
-
- ERROR in ./foo.js 1:1 -
+
- Module parse failed: Unterminated template (1:1) You may need an - appropriate loader to handle this file type, currently no loaders are - configured to process this file. See - https://webpack.js.org/concepts#loaders > \`; +
+ ERROR in ./foo.js 1:1 +
+
+ Module parse failed: Unterminated template (1:1) You may need an + appropriate loader to handle this file type, currently no loaders are + configured to process this file. See + https://webpack.js.org/concepts#loaders > \`; +
@@ -206,36 +208,38 @@ exports[`overlay should not show initially, then show on an error, then hide on > × -
-
- ERROR in ./foo.js 1:1 -
+
- Module parse failed: Unterminated template (1:1) You may need an - appropriate loader to handle this file type, currently no loaders are - configured to process this file. See - https://webpack.js.org/concepts#loaders > \`; +
+ ERROR in ./foo.js 1:1 +
+
+ Module parse failed: Unterminated template (1:1) You may need an + appropriate loader to handle this file type, currently no loaders are + configured to process this file. See + https://webpack.js.org/concepts#loaders > \`; +
@@ -330,36 +334,38 @@ exports[`overlay should not show initially, then show on an error, then show oth > × -
+
- ERROR in ./foo.js 1:1 -
-
- Module parse failed: Unterminated template (1:1) You may need an - appropriate loader to handle this file type, currently no loaders are - configured to process this file. See - https://webpack.js.org/concepts#loaders > \`; +
+ ERROR in ./foo.js 1:1 +
+
+ Module parse failed: Unterminated template (1:1) You may need an + appropriate loader to handle this file type, currently no loaders are + configured to process this file. See + https://webpack.js.org/concepts#loaders > \`; +
@@ -417,36 +423,38 @@ exports[`overlay should not show initially, then show on an error, then show oth > × -
-
- ERROR in ./foo.js 1:1 -
+
- Module parse failed: Unterminated template (1:1) You may need an - appropriate loader to handle this file type, currently no loaders are - configured to process this file. See - https://webpack.js.org/concepts#loaders > \`;a +
+ ERROR in ./foo.js 1:1 +
+
+ Module parse failed: Unterminated template (1:1) You may need an + appropriate loader to handle this file type, currently no loaders are + configured to process this file. See + https://webpack.js.org/concepts#loaders > \`;a +
@@ -570,31 +578,33 @@ exports[`overlay should show a warning after invalidation: overlay html 1`] = ` > × -
-
- WARNING -
+
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
@@ -673,58 +683,60 @@ exports[`overlay should show a warning and error for initial compilation and pro > × -
-
- WARNING -
+
- <strong>strong</strong> -
-
-
-
- ERROR +
+ ERROR +
+
+ <strong>strong</strong> +
- <strong>strong</strong> +
+ ERROR +
+
+ <strong>strong</strong> +
@@ -803,139 +815,141 @@ exports[`overlay should show a warning and error for initial compilation: overla > × -
-
- WARNING -
-
- Warning from compilation -
-
-
-
- WARNING -
-
- Warning from compilation -
-
-
-
- ERROR -
+
- Error from compilation. Can't find 'test' module. +
+ ERROR +
+
+ Warning from compilation +
-
-
- ERROR +
+ ERROR +
+
+ Warning from compilation +
- Error from compilation. Can't find 'test' module. +
+ ERROR +
+
+ Error from compilation. Can't find 'test' module. +
-
-
- ERROR +
+ ERROR +
+
+ Error from compilation. Can't find 'test' module. +
- Error from compilation. Can't find 'test' module. +
+ ERROR +
+
+ Error from compilation. Can't find 'test' module. +
@@ -1014,31 +1028,33 @@ exports[`overlay should show a warning and hide them after closing connection: o > × -
+
- WARNING -
-
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
@@ -1125,31 +1141,33 @@ exports[`overlay should show a warning for initial compilation: overlay html 1`] > × -
+
- WARNING -
-
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
@@ -1228,31 +1246,33 @@ exports[`overlay should show a warning when "client.overlay" is "true": overlay > × -
+
- WARNING -
-
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
@@ -1331,31 +1351,33 @@ exports[`overlay should show a warning when "client.overlay.errors" is "true": o > × -
+
- WARNING -
-
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
@@ -1434,31 +1456,33 @@ exports[`overlay should show a warning when "client.overlay.warnings" is "true": > × -
+
- WARNING -
-
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
@@ -1537,42 +1561,44 @@ exports[`overlay should show an ansi formatted error for initial compilation: ov > × -
+
- ERROR -
-
- - 18 | - Render - ansi formatted text +
+ + 18 | + Render + ansi formatted text +
@@ -1651,31 +1677,33 @@ exports[`overlay should show an error after invalidation: overlay html 1`] = ` > × -
-
- ERROR -
+
- Error from compilation +
+ ERROR +
+
+ Error from compilation +
@@ -1754,31 +1782,33 @@ exports[`overlay should show an error for initial compilation: overlay html 1`] > × -
+
- ERROR -
-
- Error from compilation. Can't find 'test' module. +
+ ERROR +
+
+ Error from compilation. Can't find 'test' module. +
@@ -1857,31 +1887,33 @@ exports[`overlay should show an error when "client.overlay" is "true": overlay h > × -
+
- ERROR -
-
- Error from compilation. Can't find 'test' module. +
+ ERROR +
+
+ Error from compilation. Can't find 'test' module. +
@@ -1960,31 +1992,33 @@ exports[`overlay should show an error when "client.overlay.errors" is "true": ov > × -
-
- ERROR -
+
- Error from compilation. Can't find 'test' module. +
+ ERROR +
+
+ Error from compilation. Can't find 'test' module. +
@@ -2063,31 +2097,33 @@ exports[`overlay should show an error when "client.overlay.warnings" is "true": > × -
+
- WARNING -
-
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
@@ -2166,31 +2202,33 @@ exports[`overlay should show overlay when Trusted Types are enabled: overlay htm > × -
+
- ERROR -
-
- Error from compilation. Can't find 'test' module. +
+ ERROR +
+
+ Error from compilation. Can't find 'test' module. +
From d14aaf43a3eb801999a6098a101c96ca71631913 Mon Sep 17 00:00:00 2001 From: Malcolm Kee Date: Sun, 27 Nov 2022 14:23:48 +1100 Subject: [PATCH 3/8] feat: display runtime error stack --- client-src/overlay.js | 31 ++++++++++++++++--- client-src/overlay/runtime-error.js | 19 ++++++++++-- examples/client/overlay/error-button.js | 12 +++++-- .../multi-compiler.test.js.snap.webpack5 | 6 ---- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/client-src/overlay.js b/client-src/overlay.js index d475a37500..e241ada988 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -3,6 +3,11 @@ import ansiHTML from "ansi-html-community"; import { encode } from "html-entities"; +import { + listenToRuntimeError, + parseErrorToStacks, +} from "./overlay/runtime-error.js"; +import createOverlayMachine from "./overlay/state-machine.js"; import { containerStyle, dismissButtonStyle, @@ -12,8 +17,6 @@ import { msgTextStyle, msgTypeStyle, } from "./overlay/styles.js"; -import { listenToRuntimeError } from "./overlay/runtime-error.js"; -import createOverlayMachine from "./overlay/state-machine.js"; const colors = { reset: ["transparent", "transparent"], @@ -32,7 +35,7 @@ ansiHTML.setColors(colors); /** * @param {string} type - * @param {string | { file?: string, moduleName?: string, loc?: string, message?: string }} item + * @param {string | { file?: string, moduleName?: string, loc?: string, message?: string; stack?: string[] }} item * @returns {{ header: string, body: string }} */ function formatProblem(type, item) { @@ -61,6 +64,14 @@ function formatProblem(type, item) { body += item.message || ""; } + if (Array.isArray(item.stack)) { + item.stack.forEach((stack) => { + if (typeof stack === "string") { + body += `\r\n${stack}`; + } + }); + } + return { header, body }; } @@ -259,10 +270,20 @@ const createOverlay = (options) => { hideOverlay: hide, }); - listenToRuntimeError((err) => { + listenToRuntimeError((errorEvent) => { + const { error } = errorEvent; + if (!error) { + return; + } + const errorObject = error instanceof Error ? error : new Error(error); overlayService.send({ type: "RUNTIME_ERROR", - messages: [err.message], + messages: [ + { + message: errorObject.message, + stack: parseErrorToStacks(errorObject), + }, + ], }); }); diff --git a/client-src/overlay/runtime-error.js b/client-src/overlay/runtime-error.js index 3c0210d6d3..a04fd82353 100644 --- a/client-src/overlay/runtime-error.js +++ b/client-src/overlay/runtime-error.js @@ -1,3 +1,18 @@ +/** + * + * @param {Error} error + */ +function parseErrorToStacks(error) { + if (!error || !(error instanceof Error)) { + throw new Error(`parseErrorToStacks expects Error object`); + } + if (typeof error.stack === "string") { + return error.stack + .split("\n") + .filter((stack) => stack !== `Error: ${error.message}`); + } +} + /** * @callback ErrorCallback * @param {ErrorEvent} error @@ -15,6 +30,4 @@ function listenToRuntimeError(callback) { }; } -function listenToSomething() {} - -export { listenToRuntimeError, listenToSomething }; +export { listenToRuntimeError, parseErrorToStacks }; diff --git a/examples/client/overlay/error-button.js b/examples/client/overlay/error-button.js index dcd695b67e..77457d5ea4 100644 --- a/examples/client/overlay/error-button.js +++ b/examples/client/overlay/error-button.js @@ -1,11 +1,17 @@ "use strict"; +const unsafeOperation = () => { + throw new Error("Error message thrown from JS"); +}; + +const handleButtonClick = () => { + unsafeOperation(); +}; + module.exports = function createErrorButton() { const errorBtn = document.createElement("button"); - errorBtn.addEventListener("click", () => { - throw new Error("runtime error!"); - }); + errorBtn.addEventListener("click", handleButtonClick); errorBtn.innerHTML = "Click to throw error"; return errorBtn; diff --git a/test/e2e/__snapshots__/multi-compiler.test.js.snap.webpack5 b/test/e2e/__snapshots__/multi-compiler.test.js.snap.webpack5 index 5da317a992..c7643a4446 100644 --- a/test/e2e/__snapshots__/multi-compiler.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/multi-compiler.test.js.snap.webpack5 @@ -51,13 +51,7 @@ Array [ "[HMR] Cannot apply update. Need to do a full reload!", "[HMR] Error: Aborted because ./browser.js is not accepted Update propagation: ./browser.js - at applyHandler (http://127.0.0.1:8103/browser.js:1077:31) - at http://127.0.0.1:8103/browser.js:776:21 at Array.map () - at internalApply (http://127.0.0.1:8103/browser.js:775:54) - at http://127.0.0.1:8103/browser.js:745:26 - at waitForBlockingPromises (http://127.0.0.1:8103/browser.js:699:48) - at http://127.0.0.1:8103/browser.js:743:24", "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading disabled, Progress disabled, Overlay enabled.", "[HMR] Waiting for update signal from WDS...", "Hello from the browser", From 86c6c46debddf06a444b5ac44b11f8d159c06d66 Mon Sep 17 00:00:00 2001 From: Malcolm Kee Date: Sun, 27 Nov 2022 14:28:39 +1100 Subject: [PATCH 4/8] fix: snapshot --- test/e2e/__snapshots__/multi-compiler.test.js.snap.webpack5 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/e2e/__snapshots__/multi-compiler.test.js.snap.webpack5 b/test/e2e/__snapshots__/multi-compiler.test.js.snap.webpack5 index c7643a4446..5da317a992 100644 --- a/test/e2e/__snapshots__/multi-compiler.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/multi-compiler.test.js.snap.webpack5 @@ -51,7 +51,13 @@ Array [ "[HMR] Cannot apply update. Need to do a full reload!", "[HMR] Error: Aborted because ./browser.js is not accepted Update propagation: ./browser.js + at applyHandler (http://127.0.0.1:8103/browser.js:1077:31) + at http://127.0.0.1:8103/browser.js:776:21 at Array.map () + at internalApply (http://127.0.0.1:8103/browser.js:775:54) + at http://127.0.0.1:8103/browser.js:745:26 + at waitForBlockingPromises (http://127.0.0.1:8103/browser.js:699:48) + at http://127.0.0.1:8103/browser.js:743:24", "[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading disabled, Progress disabled, Overlay enabled.", "[HMR] Waiting for update signal from WDS...", "Hello from the browser", From 5298888190ce4b79ddd0d992717f465ffec93530 Mon Sep 17 00:00:00 2001 From: Malcolm Kee Date: Sat, 3 Dec 2022 04:05:10 +1100 Subject: [PATCH 5/8] fix: update snapshot --- .../overlay.test.js.snap.webpack4 | 1050 +++++++++-------- 1 file changed, 543 insertions(+), 507 deletions(-) diff --git a/test/e2e/__snapshots__/overlay.test.js.snap.webpack4 b/test/e2e/__snapshots__/overlay.test.js.snap.webpack4 index e80e804422..f989b527bb 100644 --- a/test/e2e/__snapshots__/overlay.test.js.snap.webpack4 +++ b/test/e2e/__snapshots__/overlay.test.js.snap.webpack4 @@ -82,34 +82,36 @@ exports[`overlay should not show initially, then show on an error and allow to c > × -
+
- ERROR -
-
- ./foo.js 1:1 Module parse failed: Unterminated template (1:1) You may - need an appropriate loader to handle this file type, currently no - loaders are configured to process this file. See - https://webpack.js.org/concepts#loaders > \`; +
+ ERROR +
+
+ ./foo.js 1:1 Module parse failed: Unterminated template (1:1) You may + need an appropriate loader to handle this file type, currently no + loaders are configured to process this file. See + https://webpack.js.org/concepts#loaders > \`; +
@@ -204,34 +206,36 @@ exports[`overlay should not show initially, then show on an error, then hide on > × -
-
- ERROR -
+
- ./foo.js 1:1 Module parse failed: Unterminated template (1:1) You may - need an appropriate loader to handle this file type, currently no - loaders are configured to process this file. See - https://webpack.js.org/concepts#loaders > \`; +
+ ERROR +
+
+ ./foo.js 1:1 Module parse failed: Unterminated template (1:1) You may + need an appropriate loader to handle this file type, currently no + loaders are configured to process this file. See + https://webpack.js.org/concepts#loaders > \`; +
@@ -326,34 +330,36 @@ exports[`overlay should not show initially, then show on an error, then show oth > × -
-
- ERROR -
+
- ./foo.js 1:1 Module parse failed: Unterminated template (1:1) You may - need an appropriate loader to handle this file type, currently no - loaders are configured to process this file. See - https://webpack.js.org/concepts#loaders > \`; +
+ ERROR +
+
+ ./foo.js 1:1 Module parse failed: Unterminated template (1:1) You may + need an appropriate loader to handle this file type, currently no + loaders are configured to process this file. See + https://webpack.js.org/concepts#loaders > \`; +
@@ -411,34 +417,36 @@ exports[`overlay should not show initially, then show on an error, then show oth > × -
+
- ERROR -
-
- ./foo.js 1:1 Module parse failed: Unterminated template (1:1) You may - need an appropriate loader to handle this file type, currently no - loaders are configured to process this file. See - https://webpack.js.org/concepts#loaders > \`;a +
+ ERROR +
+
+ ./foo.js 1:1 Module parse failed: Unterminated template (1:1) You may + need an appropriate loader to handle this file type, currently no + loaders are configured to process this file. See + https://webpack.js.org/concepts#loaders > \`;a +
@@ -554,31 +562,33 @@ exports[`overlay should show a warning after invalidation: overlay html 1`] = ` > × -
+
- WARNING -
-
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
@@ -657,58 +667,60 @@ exports[`overlay should show a warning and error for initial compilation and pro > × -
-
- WARNING -
+
- <strong>strong</strong> -
-
-
-
- ERROR +
+ ERROR +
+
+ <strong>strong</strong> +
- <strong>strong</strong> +
+ ERROR +
+
+ <strong>strong</strong> +
@@ -787,139 +799,141 @@ exports[`overlay should show a warning and error for initial compilation: overla > × -
-
- WARNING -
-
- Warning from compilation -
-
-
-
- WARNING -
+
- Warning from compilation -
-
-
-
- ERROR +
+ ERROR +
+
+ Warning from compilation +
- Error from compilation. Can't find 'test' module. -
-
-
-
- ERROR +
+ ERROR +
+
+ Warning from compilation +
- Error from compilation. Can't find 'test' module. +
+ ERROR +
+
+ Error from compilation. Can't find 'test' module. +
-
-
- ERROR +
+ ERROR +
+
+ Error from compilation. Can't find 'test' module. +
- Error from compilation. Can't find 'test' module. +
+ ERROR +
+
+ Error from compilation. Can't find 'test' module. +
@@ -998,31 +1012,33 @@ exports[`overlay should show a warning and hide them after closing connection: o > × -
+
- WARNING -
-
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
@@ -1109,31 +1125,33 @@ exports[`overlay should show a warning for initial compilation: overlay html 1`] > × -
-
- WARNING -
+
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
@@ -1212,31 +1230,33 @@ exports[`overlay should show a warning when "client.overlay" is "true": overlay > × -
+
- WARNING -
-
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
@@ -1315,31 +1335,33 @@ exports[`overlay should show a warning when "client.overlay.errors" is "true": o > × -
-
- WARNING -
+
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
@@ -1418,31 +1440,33 @@ exports[`overlay should show a warning when "client.overlay.warnings" is "true": > × -
+
- WARNING -
-
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
@@ -1521,42 +1545,44 @@ exports[`overlay should show an ansi formatted error for initial compilation: ov > × -
+
- ERROR -
-
- - 18 | - Render - ansi formatted text +
+ + 18 | + Render + ansi formatted text +
@@ -1635,31 +1661,33 @@ exports[`overlay should show an error after invalidation: overlay html 1`] = ` > × -
+
- ERROR -
-
- Error from compilation +
+ ERROR +
+
+ Error from compilation +
@@ -1738,31 +1766,33 @@ exports[`overlay should show an error for initial compilation: overlay html 1`] > × -
+
- ERROR -
-
- Error from compilation. Can't find 'test' module. +
+ ERROR +
+
+ Error from compilation. Can't find 'test' module. +
@@ -1841,31 +1871,33 @@ exports[`overlay should show an error when "client.overlay" is "true": overlay h > × -
+
- ERROR -
-
- Error from compilation. Can't find 'test' module. +
+ ERROR +
+
+ Error from compilation. Can't find 'test' module. +
@@ -1944,31 +1976,33 @@ exports[`overlay should show an error when "client.overlay.errors" is "true": ov > × -
-
- ERROR -
+
- Error from compilation. Can't find 'test' module. +
+ ERROR +
+
+ Error from compilation. Can't find 'test' module. +
@@ -2047,31 +2081,33 @@ exports[`overlay should show an error when "client.overlay.warnings" is "true": > × -
+
- WARNING -
-
- Warning from compilation +
+ WARNING +
+
+ Warning from compilation +
From 10f69a10abc1fe4a8e87880604a479cf337553ec Mon Sep 17 00:00:00 2001 From: Malcolm Kee Date: Mon, 5 Dec 2022 23:45:39 +1100 Subject: [PATCH 6/8] fix: remove optional chaining syntax --- client-src/overlay/fsm.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/client-src/overlay/fsm.js b/client-src/overlay/fsm.js index 95abd60f58..78020e97aa 100644 --- a/client-src/overlay/fsm.js +++ b/client-src/overlay/fsm.js @@ -36,19 +36,26 @@ function createMachine({ states, context, initial }, { actions }) { return { send: (event) => { - const transitionConfig = states[currentState].on?.[event.type]; + const currentStateOn = states[currentState].on; + const transitionConfig = currentStateOn && currentStateOn[event.type]; if (transitionConfig) { currentState = transitionConfig.target; - transitionConfig.actions?.forEach((actName) => { - const nextContextValue = actions[actName]?.(currentContext, event); - if (nextContextValue) { - currentContext = { - ...currentContext, - ...nextContextValue, - }; - } - }); + if (transitionConfig.actions) { + transitionConfig.actions.forEach((actName) => { + const actionImpl = actions[actName]; + + const nextContextValue = + actionImpl && actionImpl(currentContext, event); + + if (nextContextValue) { + currentContext = { + ...currentContext, + ...nextContextValue, + }; + } + }); + } } }, }; From d450375f5689beabfce3e6861868c473b1458d76 Mon Sep 17 00:00:00 2001 From: Malcolm Kee Date: Tue, 6 Dec 2022 22:12:36 +1100 Subject: [PATCH 7/8] fix: remove non IE compatible feature --- client-src/overlay.js | 3 ++- examples/client/overlay/error-button.js | 8 ++++---- examples/client/overlay/webpack.config.js | 2 ++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/client-src/overlay.js b/client-src/overlay.js index e241ada988..61cc37d678 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -238,7 +238,8 @@ const createOverlay = (options) => { if (message.moduleIdentifier) { applyStyle(typeElement, { cursor: "pointer" }); - typeElement.dataset.canOpen = true; + // element.dataset not supported in IE + typeElement.setAttribute("data-can-open", true); typeElement.addEventListener("click", () => { fetch( `/webpack-dev-server/open-editor?fileName=${message.moduleIdentifier}` diff --git a/examples/client/overlay/error-button.js b/examples/client/overlay/error-button.js index 77457d5ea4..11fe606af0 100644 --- a/examples/client/overlay/error-button.js +++ b/examples/client/overlay/error-button.js @@ -1,12 +1,12 @@ "use strict"; -const unsafeOperation = () => { +function unsafeOperation() { throw new Error("Error message thrown from JS"); -}; +} -const handleButtonClick = () => { +function handleButtonClick() { unsafeOperation(); -}; +} module.exports = function createErrorButton() { const errorBtn = document.createElement("button"); diff --git a/examples/client/overlay/webpack.config.js b/examples/client/overlay/webpack.config.js index 43e883e183..41d8ad543b 100644 --- a/examples/client/overlay/webpack.config.js +++ b/examples/client/overlay/webpack.config.js @@ -13,4 +13,6 @@ module.exports = setup({ overlay: true, }, }, + // uncomment to test for IE + // target: ["web", "es5"], }); From cb17c06e1c686ebf04608b53849a6438ae5b1d9a Mon Sep 17 00:00:00 2001 From: Malcolm Kee Date: Mon, 19 Dec 2022 16:36:03 +1100 Subject: [PATCH 8/8] fix: handle error event in IE --- client-src/overlay.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client-src/overlay.js b/client-src/overlay.js index 61cc37d678..a46300a61b 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -272,11 +272,13 @@ const createOverlay = (options) => { }); listenToRuntimeError((errorEvent) => { - const { error } = errorEvent; - if (!error) { + // error property may be empty in older browser like IE + const { error, message } = errorEvent; + if (!error && !message) { return; } - const errorObject = error instanceof Error ? error : new Error(error); + const errorObject = + error instanceof Error ? error : new Error(error || message); overlayService.send({ type: "RUNTIME_ERROR", messages: [