diff --git a/client-src/clients/WebSocketClient.js b/client-src/clients/WebSocketClient.js index b9d1711031..fdad02efd3 100644 --- a/client-src/clients/WebSocketClient.js +++ b/client-src/clients/WebSocketClient.js @@ -1,42 +1,9 @@ -import { log } from "../utils/log.js"; - -/** @typedef {import("../index.js").EXPECTED_ANY} EXPECTED_ANY */ - -/** - * @implements {CommunicationClient} - */ -export default class WebSocketClient { - /** - * @param {string} url url to connect - */ - constructor(url) { - this.client = new WebSocket(url); - this.client.onerror = (error) => { - log.error(error); - }; - } - - /** - * @param {(...args: EXPECTED_ANY[]) => void} fn function - */ - onOpen(fn) { - this.client.onopen = fn; - } - - /** - * @param {(...args: EXPECTED_ANY[]) => void} fn function - */ - onClose(fn) { - this.client.onclose = fn; - } - - // call f with the message string as the first argument - /** - * @param {(...args: EXPECTED_ANY[]) => void} fn function - */ - onMessage(fn) { - this.client.onmessage = (err) => { - fn(err.data); - }; - } -} +// Re-exported rather than implemented: webpack-dev-middleware ships the same +// transport, and one copy means one place for the things this one was missing — +// a `close()` (which the interface declares and this file never had), the guard +// that stops a queued event reporting after the caller closed, and resolving a +// relative or `http(s):` url for browsers whose `WebSocket` will not. +// +// `client.webSocketTransport` resolves to this path, so anything pointing at it +// keeps working. +export { default } from "webpack-dev-middleware/client/ws"; diff --git a/client-src/overlay.js b/client-src/overlay.js index 4dd3739814..2fb71480fe 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -1,738 +1,177 @@ -// The error overlay is inspired (and mostly copied) from Create React App (https://github.com/facebookincubator/create-react-app) -// They, in turn, got inspired by webpack-hot-middleware (https://github.com/glenjamin/webpack-hot-middleware). - -import ansiHTML from "ansi-html-community"; +// The overlay itself lives in webpack-dev-middleware — this is the adapter +// that keeps this package's shape on top of it: `createOverlay` returning +// something with `send`, and `formatProblem` for the console. +// +// Two things the two sides do differently, and how they meet here: +// +// * This package sends webpack's error objects over the wire and formats +// them in the browser; dev-middleware's server formats them first and its +// overlay takes strings, whose first line is the location. `problemLine` +// produces that shape from an object, so a problem renders the same way it +// does for the middleware. +// * That overlay is a pair of calls, `showProblems` and `clear`, rather than +// a state machine. The events this package sends map onto them directly, +// and the mapping is the whole of `send` below. +import configureOverlay, { + clear, + showProblems, +} from "webpack-dev-middleware/client/overlay"; /** @typedef {import("./index.js").EXPECTED_ANY} EXPECTED_ANY */ +/** @typedef {{ file?: string, moduleName?: string, loc?: string, message?: string, stack?: EXPECTED_ANY }} Message */ -const references = { - "<": "<", - ">": ">", - '"': """, - "'": "'", - "&": "&", -}; - -/** - * @param {string} text text - * @returns {string} encoded text - */ -function encode(text) { - if (!text) { - return ""; - } - - return text.replace( - /[<>'"&]/g, - (input) => references[/** @type {keyof typeof references} */ (input)], - ); -} - -/** - * @typedef {object} Context - * @property {"warning" | "error"} level level - * @property {(string | Message)[]} messages messages - * @property {"build" | "runtime"} messageSource message source - */ - -/** @typedef {{ type: string } & Record} Event */ - -/** - * @typedef {object} Options - * @property {{ [state: string]: { on: Record } }} states states - * @property {Context} context context - * @property {string} initial initial - */ - -/** - * @typedef {object} Implementation - * @property {{ [actionName: string]: (ctx: Context, event: Event) => Context | void }} actions actions - */ - -/** - * @typedef {{ send: (event: Event) => void }} StateMachine - */ - -/** - * 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 options - * @param {Implementation} implementation implementation - * @returns {StateMachine} state machine - */ -function createMachine({ states, context, initial }, { actions }) { - let currentState = initial; - let currentContext = context; - - return { - send: (event) => { - const currentStateOn = states[currentState].on; - const transitionConfig = currentStateOn && currentStateOn[event.type]; - - if (transitionConfig) { - currentState = transitionConfig.target; - if (transitionConfig.actions) { - transitionConfig.actions.forEach((actName) => { - const actionImpl = actions[actName]; - - const nextContextValue = - actionImpl && actionImpl(currentContext, event); +// What this package's overlay element has always been called. Kept through +// dev-middleware's `id` option so anything that queries it — a test, a +// screenshot tool, an integration that hides it — is unaffected by where the +// overlay now comes from. +const OVERLAY_ID = "webpack-dev-server-client-overlay"; - if (nextContextValue) { - currentContext = { - ...currentContext, - ...nextContextValue, - }; - } - }); - } - } - }, - }; -} +// Build problems live under one source so a clean build can drop them without +// touching runtime errors, which the overlay keeps in a slot of its own. +const BUILD_SOURCE = ""; -/** - * @typedef {object} ShowOverlayData - * @property {"warning" | "error"} level level - * @property {(string | Message)[]} messages messages - * @property {"build" | "runtime"} messageSource message source - */ +// The Trusted Types policy this package's overlay has always created. It has to +// stay this name: under an enforced `require-trusted-types-for 'script'` the +// page's CSP allowlists a policy by name, and dev-middleware's own default is a +// different one. +const TRUSTED_TYPES_POLICY_NAME = "webpack-dev-server#overlay"; -/** - * @typedef {object} CreateOverlayMachineOptions - * @property {(data: ShowOverlayData) => void} showOverlay show overlay - * @property {() => void} hideOverlay hide overlay - */ +// The route this package serves for opening a file in an editor, which makes +// the file references in a problem clickable. Off by default in +// dev-middleware, since it has no route of its own to point at. +const OPEN_EDITOR_ENDPOINT = "/webpack-dev-server/open-editor"; /** - * @param {CreateOverlayMachineOptions} options options - * @returns {StateMachine} state machine - */ -const createOverlayMachine = (options) => { - const { hideOverlay, showOverlay } = options; - - return createMachine( - { - initial: "hidden", - context: { - level: "error", - messages: [], - messageSource: "build", - }, - states: { - hidden: { - on: { - BUILD_ERROR: { - target: "displayBuildError", - actions: ["setMessages", "showOverlay"], - }, - RUNTIME_ERROR: { - target: "displayRuntimeError", - actions: ["setMessages", "showOverlay"], - }, - }, - }, - displayBuildError: { - on: { - DISMISS: { - target: "hidden", - actions: ["dismissMessages", "hideOverlay"], - }, - // WHY: A compilation reporting success says nothing about a runtime - // error, so it clears build errors only — no state but this one - // handles it, and an unhandled event is a no-op. Dismissing on the - // initial handshake's `ok` is what closed an overlay raised while - // the page was still loading (#5024). - BUILD_OK: { - target: "hidden", - actions: ["dismissMessages", "hideOverlay"], - }, - BUILD_ERROR: { - target: "displayBuildError", - actions: ["appendMessages", "showOverlay"], - }, - }, - }, - displayRuntimeError: { - on: { - DISMISS: { - target: "hidden", - actions: ["dismissMessages", "hideOverlay"], - }, - RUNTIME_ERROR: { - target: "displayRuntimeError", - actions: ["appendMessages", "showOverlay"], - }, - BUILD_ERROR: { - target: "displayBuildError", - actions: ["setMessages", "showOverlay"], - }, - }, - }, - }, - }, - { - actions: { - dismissMessages: () => { - 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, - showOverlay, - }, - }, - ); -}; - -/** - * @param {Error} error error - * @returns {undefined | string[]} stack + * Where a problem happened, in the one-line form dev-middleware's overlay + * renders as the heading. Empty when webpack did not say. + * @param {string | Message} item item + * @returns {string} location */ -const 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}`); +const problemLocation = (item) => { + if (typeof item === "string") { + return ""; } -}; -/** - * @callback ErrorCallback - * @param {ErrorEvent} error - * @returns {void} - */ + const file = item.file || ""; + // `indexOf`, not `includes`: this file is compiled to an ES5 baseline. + const moduleName = item.moduleName + ? item.moduleName.indexOf("!") !== -1 + ? `${item.moduleName.replace(/^(\s|\S)*!/, "")} (${item.moduleName})` + : `${item.moduleName}` + : ""; + const loc = item.loc; -/** - * @param {ErrorCallback} callback callback - * @returns {() => void} cleanup - */ -const listenToRuntimeError = (callback) => { - window.addEventListener("error", callback); + if (!moduleName && !file) { + return ""; + } - return function cleanup() { - window.removeEventListener("error", callback); - }; + return `${moduleName ? `${moduleName}${file ? ` (${file})` : ""}` : file}${ + loc ? ` ${loc}` : "" + }`; }; /** - * @callback UnhandledRejectionCallback - * @param {PromiseRejectionEvent} rejectionEvent - * @returns {void} - */ - -/** - * @param {UnhandledRejectionCallback} callback callback - * @returns {() => void} cleanup + * What a problem says, with any stack webpack attached. + * @param {string | Message} item item + * @returns {string} body */ -const listenToUnhandledRejection = (callback) => { - window.addEventListener("unhandledrejection", callback); +const problemBody = (item) => { + let body = typeof item === "string" ? item : item.message || ""; - return function cleanup() { - window.removeEventListener("unhandledrejection", callback); - }; -}; - -// Styles are inspired by `react-error-overlay` - -const msgStyles = { - error: { - backgroundColor: "rgba(206, 17, 38, 0.1)", - color: "#fccfcf", - }, - warning: { - backgroundColor: "rgba(251, 245, 180, 0.1)", - color: "#fbf5b4", - }, -}; -const iframeStyle = { - position: "fixed", - top: "0px", - left: "0px", - right: "0px", - bottom: "0px", - width: "100vw", - height: "100vh", - border: "none", - "z-index": 9999999999, -}; -const containerStyle = { - position: "fixed", - boxSizing: "border-box", - left: "0px", - top: "0px", - right: "0px", - bottom: "0px", - width: "100vw", - height: "100vh", - fontSize: "large", - padding: "2rem 2rem 4rem 2rem", - lineHeight: "1.2", - whiteSpace: "pre-wrap", - overflow: "auto", - backgroundColor: "rgba(0, 0, 0, 0.9)", - color: "white", -}; -const headerStyle = { - color: "#e83b46", - fontSize: "2em", - whiteSpace: "pre-wrap", - fontFamily: "sans-serif", - margin: "0 2rem 2rem 0", - flex: "0 0 auto", - maxHeight: "50%", - overflow: "auto", -}; -const dismissButtonStyle = { - color: "#ffffff", - lineHeight: "1rem", - fontSize: "1.5rem", - padding: "1rem", - cursor: "pointer", - position: "absolute", - right: "0px", - top: "0px", - backgroundColor: "transparent", - border: "none", -}; -const msgTypeStyle = { - color: "#e83b46", - fontSize: "1.2em", - marginBottom: "1rem", - fontFamily: "sans-serif", -}; -const msgTextStyle = { - lineHeight: "1.5", - fontSize: "1rem", - fontFamily: "Menlo, Consolas, monospace", -}; - -// ANSI HTML + if (typeof item !== "string" && Array.isArray(item.stack)) { + // `forEach`, not `for...of`: an ES5 target has no array iterator. + item.stack.forEach((frame) => { + if (typeof frame === "string") { + body += `\r\n${frame}`; + } + }); + } -const colors = { - reset: ["transparent", "transparent"], - black: "181818", - red: "E36049", - green: "B3CB74", - yellow: "FFD080", - blue: "7CAFC2", - magenta: "7FACCA", - cyan: "C3C2EF", - lightgrey: "EBE7E3", - darkgrey: "6D7891", + return body; }; -ansiHTML.setColors(colors); - -/** @typedef {Error & { file?: string, moduleName?: string, moduleIdentifier?: string, loc?: string, message?: string, stack?: string | string[] }} Message */ - /** * @param {string} type type * @param {string | Message} item item * @returns {{ header: string, body: string }} formatted problem */ const formatProblem = (type, item) => { - let header = type === "warning" ? "WARNING" : "ERROR"; - let body = ""; - - if (typeof item === "string") { - body += item; - } else { - const file = item.file || ""; - const moduleName = item.moduleName - ? item.moduleName.indexOf("!") !== -1 - ? `${item.moduleName.replace(/^(\s|\S)*!/, "")} (${item.moduleName})` - : `${item.moduleName}` - : ""; - const loc = item.loc; + const location = problemLocation(item); - header += `${ - moduleName || file - ? ` in ${ - moduleName ? `${moduleName}${file ? ` (${file})` : ""}` : file - }${loc ? ` ${loc}` : ""}` - : "" - }`; - body += item.message || ""; - } + return { + header: `${type === "warning" ? "WARNING" : "ERROR"}${location ? ` in ${location}` : ""}`, + body: problemBody(item), + }; +}; - if (typeof item !== "string" && Array.isArray(item.stack)) { - item.stack.forEach((stack) => { - if (typeof stack === "string") { - body += `\r\n${stack}`; - } - }); - } +/** + * One problem as dev-middleware's overlay wants it: location first, then what + * it says. It renders the first line as the heading and prefixes the level + * itself, which is why the level is not repeated here. + * @param {string | Message} item item + * @returns {string} the problem as one string + */ +const problemLine = (item) => { + const location = problemLocation(item); + const body = problemBody(item); - return { header, body }; + return location ? `${location}\n${body}` : body; }; /** * @typedef {object} CreateOverlayOptions * @property {(false | string)=} trustedTypesPolicyName trusted types policy name - * @property {(boolean | ((error: Error) => void))=} catchRuntimeError runtime error catcher + * @property {(boolean | ((error: Error) => boolean))=} catchRuntimeError whether, or which, runtime errors to show — the previous `=> void` was wrong, the returned value has always decided */ /** * @param {CreateOverlayOptions} options options - * @returns {StateMachine} overlay + * @returns {{ send: (event: EXPECTED_ANY) => void }} overlay */ const createOverlay = (options) => { - /** @type {HTMLIFrameElement | null | undefined} */ - let iframeContainerElement; - /** @type {HTMLDivElement | null | undefined} */ - let containerElement; - /** @type {HTMLDivElement | null | undefined} */ - let headerElement; - /** @type {((element: HTMLDivElement) => void) | undefined} */ - let onLoad; - /** @type {Element | null | undefined} */ - let previousActiveElement; - /** @type {Omit | undefined} */ - let overlayTrustedTypesPolicy; - - /** @typedef {Extract} CSSStyleDeclarationKeys */ - - /** - * @param {HTMLElement} element element - * @param {Partial} style style - */ - function applyStyle(element, style) { - Object.keys(style).forEach((prop) => { - element.style[/** @type {CSSStyleDeclarationKeys} */ (prop)] = - /** @type {string} */ - (style[/** @type {CSSStyleDeclarationKeys} */ (prop)]); - }); - } - - /** - * @param {string | false | undefined} trustedTypesPolicyName trusted types police name - */ - function createContainer(trustedTypesPolicyName) { - // Enable Trusted Types if they are available in the current browser. - if (window.trustedTypes && !overlayTrustedTypesPolicy) { - overlayTrustedTypesPolicy = window.trustedTypes.createPolicy( - trustedTypesPolicyName || "webpack-dev-server#overlay", - { - createHTML: (value) => value, - }, - ); - } - - iframeContainerElement = document.createElement("iframe"); - iframeContainerElement.id = "webpack-dev-server-client-overlay"; - iframeContainerElement.title = "Webpack development server errors"; - iframeContainerElement.src = "about:blank"; - applyStyle(iframeContainerElement, iframeStyle); - - previousActiveElement = document.activeElement; - // eslint-disable-next-line no-use-before-define - window.addEventListener("keydown", handleEscapeKey); - - 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); - - 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({ type: "DISMISS" }); - }); - - contentElement.appendChild(headerElement); - contentElement.appendChild(closeButtonElement); - contentElement.appendChild(containerElement); - - /** @type {Document} */ - ( - /** @type {HTMLIFrameElement} */ - (iframeContainerElement).contentDocument - ).body.appendChild(contentElement); - - const iframeDocument = /** @type {Document} */ ( - /** @type {HTMLIFrameElement} */ (iframeContainerElement) - .contentDocument - ); - iframeDocument.documentElement.lang = - document.documentElement.lang || "en"; - // eslint-disable-next-line no-use-before-define - iframeDocument.addEventListener("keydown", handleEscapeKey); - - if (onLoad) { - onLoad(contentElement); - onLoad = undefined; - } - - closeButtonElement.focus(); - - /** @type {HTMLIFrameElement} */ - (iframeContainerElement).onload = null; - }; - - document.body.appendChild(iframeContainerElement); - } - - /** - * @param {(element: HTMLDivElement) => void} callback callback - * @param {string | false | undefined} trustedTypesPolicyName trusted types policy name - */ - function ensureOverlayExists(callback, trustedTypesPolicyName) { - if (containerElement) { - // @ts-expect-error https://github.com/microsoft/TypeScript/issues/30024 - containerElement.innerHTML = overlayTrustedTypesPolicy - ? overlayTrustedTypesPolicy.createHTML("") - : ""; - // Everything is ready, call the callback right away. - callback(containerElement); - - return; - } - - // Each callback renders the complete current message list. - onLoad = callback; - - if (iframeContainerElement) { - return; - } - - createContainer(trustedTypesPolicyName); - } - - // Successful compilation. - /** - * @returns {void} - */ - function hide() { - if (!iframeContainerElement) { - return; - } - - // Clean up and reset internal state. - iframeContainerElement.onload = null; - iframeContainerElement.contentDocument?.removeEventListener( - "keydown", - // eslint-disable-next-line no-use-before-define - handleEscapeKey, - ); - const restoreFocus = document.activeElement === iframeContainerElement; - document.body.removeChild(iframeContainerElement); - - iframeContainerElement = null; - containerElement = null; - headerElement = null; - onLoad = undefined; - - if (restoreFocus && previousActiveElement instanceof HTMLElement) { - previousActiveElement.focus(); - } - previousActiveElement = null; - } - - // Compilation with errors (e.g. syntax error or missing modules). - /** - * @param {string} type type - * @param {(string | Message)[]} messages messages - * @param {undefined | false | string} trustedTypesPolicyName trusted types policy name - * @param {"build" | "runtime"} messageSource message source - */ - function show(type, messages, trustedTypesPolicyName, messageSource) { - ensureOverlayExists(() => { - /** @type {HTMLDivElement} */ - (headerElement).innerText = - messageSource === "runtime" - ? "Uncaught runtime errors:" - : "Compiled with problems:"; - - 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", - }); - - const canOpen = typeof message !== "string" && message.moduleIdentifier; - const typeElement = document.createElement(canOpen ? "button" : "div"); - const { header, body } = formatProblem(type, message); - - typeElement.innerText = header; - applyStyle(typeElement, msgTypeStyle); - - if (canOpen) { - typeElement.setAttribute("type", "button"); - applyStyle(typeElement, { - cursor: "pointer", - background: "none", - border: "none", - padding: "0", - textAlign: "left", - display: "block", - lineHeight: "inherit", - }); - // element.dataset not supported in IE - typeElement.setAttribute("data-can-open", "true"); - typeElement.addEventListener("click", () => { - fetch( - `/webpack-dev-server/open-editor?fileName=${encodeURIComponent(canOpen)}`, - ); - }); - } - - // Make it look similar to our terminal. - const text = ansiHTML(encode(body)); - const messageTextNode = document.createElement("div"); - applyStyle(messageTextNode, msgTextStyle); - - // @ts-expect-error https://github.com/microsoft/TypeScript/issues/30024 - messageTextNode.innerHTML = overlayTrustedTypesPolicy - ? overlayTrustedTypesPolicy.createHTML(text) - : text; - - entryElement.appendChild(typeElement); - entryElement.appendChild(messageTextNode); - - /** @type {HTMLDivElement} */ - (containerElement).appendChild(entryElement); - }); - }, trustedTypesPolicyName); - } - - /** @type {(event: KeyboardEvent) => void} */ - let handleEscapeKey; - - /** - * @returns {void} - */ - - const hideOverlayWithEscCleanup = () => { - window.removeEventListener("keydown", handleEscapeKey); - hide(); - }; - - const overlayService = createOverlayMachine({ - showOverlay: ({ level = "error", messages, messageSource }) => - show(level, messages, options.trustedTypesPolicyName, messageSource), - hideOverlay: hideOverlayWithEscCleanup, + configureOverlay({ + id: OVERLAY_ID, + catchRuntimeError: options.catchRuntimeError, + openEditorEndpoint: OPEN_EDITOR_ENDPOINT, + // `false` means "no name of my own", which is this package's default name + // rather than dev-middleware's. + trustedTypesPolicyName: + typeof options.trustedTypesPolicyName === "string" + ? options.trustedTypesPolicyName + : TRUSTED_TYPES_POLICY_NAME, }); - /** - * ESC key press to dismiss the overlay. - * @param {KeyboardEvent} event Keydown event - */ - handleEscapeKey = (event) => { - if (event.key === "Escape" || event.key === "Esc" || event.keyCode === 27) { - overlayService.send({ type: "DISMISS" }); - } - }; - - window.addEventListener("keydown", handleEscapeKey); - if (options.catchRuntimeError) { + return { /** - * @param {Error | undefined} error error - * @param {string} fallbackMessage fallback message + * @param {EXPECTED_ANY} event event */ - const handleError = (error, fallbackMessage) => { - const errorObject = - error instanceof Error - ? error - : new Error(error || fallbackMessage, { cause: error }); - - const shouldDisplay = - typeof options.catchRuntimeError === "function" - ? options.catchRuntimeError(errorObject) - : true; - - if (shouldDisplay) { - overlayService.send({ - type: "RUNTIME_ERROR", - messages: [ - { - message: errorObject.message, - stack: parseErrorToStacks(errorObject), - }, - ], - }); - } - }; - - listenToRuntimeError((errorEvent) => { - // error property may be empty in older browser like IE - const { error, message } = errorEvent; - - if (!error && !message) { - return; - } - - // if error stack indicates a React error boundary caught the error, do not show overlay. - if ( - error && - error.stack && - error.stack.includes("invokeGuardedCallbackDev") - ) { - return; + send(event) { + switch (event.type) { + case "BUILD_ERROR": + showProblems( + event.level === "warning" ? "warnings" : "errors", + /** @type {(string | Message)[]} */ (event.messages).map( + (message) => problemLine(message), + ), + BUILD_SOURCE, + ); + break; + // A build that succeeded says nothing about an error the page threw on + // its own, so only this package's build problems go. + case "BUILD_OK": + clear(BUILD_SOURCE); + break; + // Everything, runtime errors included: sent when a rebuild starts, or + // when the connection is gone, and neither leaves anything worth + // keeping on screen. + case "DISMISS": + clear(); + break; + default: + break; } - - handleError(error, message); - }); - - listenToUnhandledRejection((promiseRejectionEvent) => { - const { reason } = promiseRejectionEvent; - - handleError(reason, "Unknown promise rejection reason"); - }); - } - - return overlayService; + }, + }; }; export { createOverlay, formatProblem }; diff --git a/eslint.config.mjs b/eslint.config.mjs index fc6b35de37..5933bd719d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -49,4 +49,13 @@ export default defineConfig([ "import/no-unresolved": "off", }, }, + { + // `webpack-dev-middleware/client/ws` is a subpath of that package's + // `exports` map, which the import resolver cannot follow either. TypeScript + // does resolve it, so `lint:types-client` still covers the import. + files: ["client-src/clients/WebSocketClient.js", "client-src/overlay.js"], + rules: { + "import/no-unresolved": "off", + }, + }, ]); diff --git a/test/e2e/overlay.test.js b/test/e2e/overlay.test.js index 4c8e2531bc..d8535768e0 100644 --- a/test/e2e/overlay.test.js +++ b/test/e2e/overlay.test.js @@ -637,7 +637,9 @@ describe("overlay", () => { .frames() .find((item) => item.name() === "webpack-dev-server-client-overlay"); - const errorHandle = await frame.$("[data-can-open]"); + // The overlay comes from webpack-dev-middleware, which marks a + // clickable file reference with `data-open-file`. + const errorHandle = await frame.$("[data-open-file]"); await errorHandle.click(); @@ -2089,9 +2091,11 @@ describe("overlay", () => { const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); const overlayFrame = await overlayHandle.contentFrame(); + // The shared overlay heads a problem with its level and where it came + // from, rather than this package's old "Compiled with problems". expect( await overlayFrame.evaluate(() => document.body.textContent), - ).toContain("Compiled with problems"); + ).toContain("ERROR"); expect( pageErrors.filter((error) => /trusted type policy/i.test(error.message),