From 47f39185a7e67875f7b1431015305ae37e65def8 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 5 Sep 2026 23:16:11 -0500 Subject: [PATCH 1/4] refactor: reuse dev-middleware overlay and progress indicator --- client-src/globals.d.ts | 24 +- client-src/index.js | 28 +- client-src/overlay.js | 414 +- client-src/progress.js | 249 +- package-lock.json | 137 +- package.json | 3 +- test/client/overlay.test.js | 55 + test/client/progress.test.js | 38 + .../overlay.test.js.snap.webpack5 | 3449 +++++++++-------- test/e2e/overlay.test.js | 306 +- 10 files changed, 2192 insertions(+), 2511 deletions(-) create mode 100644 test/client/overlay.test.js create mode 100644 test/client/progress.test.js diff --git a/client-src/globals.d.ts b/client-src/globals.d.ts index bdf8a44f49..4a454b86c2 100644 --- a/client-src/globals.d.ts +++ b/client-src/globals.d.ts @@ -13,12 +13,22 @@ declare const __webpack_dev_server_client__: | { default: CommunicationClientConstructor } | undefined; -declare module "ansi-html-community" { - function ansiHtmlCommunity(str: string): string; - - namespace ansiHtmlCommunity { - function setColors(colors: Record): void; - } +declare module "webpack-dev-middleware/client/overlay" { + export default function configureOverlay(options: { + trustedTypesPolicyName?: string; + openEditorEndpoint?: string; + paginate?: boolean; + }): { + showProblems( + type: "errors" | "warnings", + lines: string[], + source?: string, + ): void; + clear(source?: string): void; + }; +} - export default ansiHtmlCommunity; +declare module "webpack-dev-middleware/client/indicator" { + export function show(text?: string, percent?: number, source?: string): void; + export function hide(source?: string): void; } diff --git a/client-src/index.js b/client-src/index.js index e8a650ea3d..c779a7b715 100644 --- a/client-src/index.js +++ b/client-src/index.js @@ -4,7 +4,7 @@ import hotEmitter from "webpack/hot/emitter.js"; // @ts-expect-error import webpackHotLog from "webpack/hot/log.js"; import { createOverlay, formatProblem } from "./overlay.js"; -import { defineProgressElement, isProgressSupported } from "./progress.js"; +import { hideProgress, showProgress } from "./progress.js"; import socket from "./socket.js"; import { log, setLogLevel } from "./utils/log.js"; import sendMessage from "./utils/sendMessage.js"; @@ -189,7 +189,11 @@ if (parsedResourceQuery["live-reload"] === "true") { enabledFeatures["Live Reloading"] = true; } -if (parsedResourceQuery.progress === "true") { +if ( + parsedResourceQuery.progress === "true" || + parsedResourceQuery.progress === "linear" || + parsedResourceQuery.progress === "circular" +) { options.progress = true; enabledFeatures.Progress = true; } @@ -444,6 +448,10 @@ const onSocketMessage = { */ progress(value) { options.progress = value; + + if (!value) { + hideProgress(); + } }, /** * @param {{ pluginName?: string, percent: string, msg: string }} data date with progress @@ -457,20 +465,14 @@ const onSocketMessage = { ); } - if (isProgressSupported() && typeof options.progress === "string") { - let progress = document.querySelector("wds-progress"); - if (!progress) { - defineProgressElement(); - progress = document.createElement("wds-progress"); - document.body.appendChild(progress); - } - progress.setAttribute("progress", data.percent); - progress.setAttribute("type", options.progress); + if (options.progress) { + showProgress(Number(data.percent), data.msg); } sendMessage("Progress", data); }, "still-ok": function stillOk() { + hideProgress(); log.info("Nothing changed."); if (options.overlay) { @@ -480,6 +482,7 @@ const onSocketMessage = { sendMessage("StillOk"); }, ok() { + hideProgress(); sendMessage("Ok"); if (options.overlay) { @@ -505,6 +508,7 @@ const onSocketMessage = { * @param {{ preventReloading: boolean }=} params extra params */ warnings(warnings, params) { + hideProgress(); log.warn("Warnings while compiling."); const printableWarnings = warnings.map((error) => { @@ -549,6 +553,7 @@ const onSocketMessage = { * @param {Error[]} errors errors */ errors(errors) { + hideProgress(); log.error("Errors while compiling. Reload prevented."); const printableErrors = errors.map((error) => { @@ -590,6 +595,7 @@ const onSocketMessage = { log.error(error); }, close() { + hideProgress(); log.info("Disconnected!"); if (options.overlay) { diff --git a/client-src/overlay.js b/client-src/overlay.js index 6003b6194b..710379014e 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -1,82 +1,7 @@ -// 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"; +import configureOverlay from "webpack-dev-middleware/client/overlay"; /** @typedef {import("./index.js").EXPECTED_ANY} EXPECTED_ANY */ -/** - * @type {(input: string, position: number) => number | undefined} - */ -// @ts-expect-error -const getCodePoint = String.prototype.codePointAt - ? // @ts-expect-error - (input, position) => input.codePointAt(position) - : (input, position) => - (input.charCodeAt(position) - 0xd800) * 0x400 + - input.charCodeAt(position + 1) - - 0xdc00 + - 0x10000; - -/** - * @param {string} macroText macro text - * @param {RegExp} macroRegExp macro reg exp - * @param {(input: string) => string} macroReplacer macro replacer - * @returns {string} result - */ -const replaceUsingRegExp = (macroText, macroRegExp, macroReplacer) => { - macroRegExp.lastIndex = 0; - let replaceMatch = macroRegExp.exec(macroText); - let replaceResult; - if (replaceMatch) { - replaceResult = ""; - let replaceLastIndex = 0; - do { - if (replaceLastIndex !== replaceMatch.index) { - replaceResult += macroText.slice(replaceLastIndex, replaceMatch.index); - } - const replaceInput = replaceMatch[0]; - replaceResult += macroReplacer(replaceInput); - replaceLastIndex = replaceMatch.index + replaceInput.length; - } while ((replaceMatch = macroRegExp.exec(macroText))); - - if (replaceLastIndex !== macroText.length) { - replaceResult += macroText.slice(replaceLastIndex); - } - } else { - replaceResult = macroText; - } - return replaceResult; -}; - -const references = { - "<": "<", - ">": ">", - '"': """, - "'": "'", - "&": "&", -}; - -/** - * @param {string} text text - * @returns {string} encoded text - */ -function encode(text) { - if (!text) { - return ""; - } - - return replaceUsingRegExp(text, /[<>'"&]/g, (input) => { - let result = references[/** @type {keyof typeof references} */ (input)]; - if (!result) { - const code = - input.length > 1 ? getCodePoint(input, 0) : input.charCodeAt(0); - result = `&#${code};`; - } - return result; - }); -} - /** * @typedef {object} Context * @property {"warning" | "error"} level level @@ -299,97 +224,6 @@ const listenToUnhandledRejection = (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 - -const colors = { - reset: ["transparent", "transparent"], - black: "181818", - red: "E36049", - green: "B3CB74", - yellow: "FFD080", - blue: "7CAFC2", - magenta: "7FACCA", - cyan: "C3C2EF", - lightgrey: "EBE7E3", - darkgrey: "6D7891", -}; - -ansiHTML.setColors(colors); - /** @typedef {Error & { file?: string, moduleName?: string, moduleIdentifier?: string, loc?: string, message?: string, stack?: string | string[] }} Message */ /** @@ -444,205 +278,12 @@ const formatProblem = (type, item) => { * @returns {StateMachine} 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)[]} */ - let onLoadQueue = []; - /** @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 = 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); - - 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); - - onLoadQueue.forEach((onLoad) => { - onLoad(/** @type {HTMLDivElement} */ (contentElement)); - }); - onLoadQueue = []; - - /** @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; - } - - onLoadQueue.push(callback); - - if (iframeContainerElement) { - return; - } - - createContainer(trustedTypesPolicyName); - } - - // Successful compilation. - /** - * @returns {void} - */ - 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 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 typeElement = document.createElement("div"); - const { header, body } = formatProblem(type, message); - - typeElement.innerText = header; - applyStyle(typeElement, msgTypeStyle); - - if (typeof message !== "string" && message.moduleIdentifier) { - applyStyle(typeElement, { cursor: "pointer" }); - // element.dataset not supported in IE - typeElement.setAttribute("data-can-open", "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); - - // @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); - } + const sharedOverlay = configureOverlay({ + trustedTypesPolicyName: + options.trustedTypesPolicyName || "webpack-dev-server#overlay", + openEditorEndpoint: "/webpack-dev-server/open-editor", + paginate: false, + }); /** @type {(event: KeyboardEvent) => void} */ let handleEscapeKey; @@ -653,12 +294,47 @@ const createOverlay = (options) => { const hideOverlayWithEscCleanup = () => { window.removeEventListener("keydown", handleEscapeKey); - hide(); + sharedOverlay.clear("webpack-dev-server"); }; const overlayService = createOverlayMachine({ - showOverlay: ({ level = "error", messages, messageSource }) => - show(level, messages, options.trustedTypesPolicyName, messageSource), + showOverlay: ({ level = "error", messages }) => { + sharedOverlay.showProblems( + level === "warning" ? "warnings" : "errors", + messages.map((message) => { + const { header, body } = formatProblem(level, message); + const location = header.replace(/^(ERROR|WARNING)( in )?/, ""); + return location ? `${location}\n${body}` : body; + }), + "webpack-dev-server", + ); + + // The shared renderer links display paths. Resolve those links to the + // compiler's module identifiers, which can be outside the server cwd. + const frame = /** @type {HTMLIFrameElement | null} */ ( + document.getElementById("webpack-dev-middleware-hot-overlay") + ); + const links = + frame?.contentDocument?.querySelectorAll("[data-open-file]"); + links?.forEach((link) => { + const file = link.getAttribute("data-open-file") || ""; + messages.forEach((message) => { + if (typeof message === "string" || !message.moduleIdentifier) { + return; + } + const name = (message.moduleName || message.file || "").replace( + /^(\s|\S)*!/, + "", + ); + if (name && file.indexOf(`${name}:`) === 0) { + link.setAttribute( + "data-open-file", + `${message.moduleIdentifier.replace(/^[^|]*\|/, "").replace(/^(\s|\S)*!/, "")}${file.slice(name.length)}`, + ); + } + }); + }); + }, hideOverlay: hideOverlayWithEscCleanup, }); /** diff --git a/client-src/progress.js b/client-src/progress.js index acac9ed77a..7eb9ca1861 100644 --- a/client-src/progress.js +++ b/client-src/progress.js @@ -1,238 +1,31 @@ -/** - * @returns {boolean} true when custom elements supported, otherwise false - */ -export function isProgressSupported() { - return ( - "customElements" in self && Boolean(HTMLElement.prototype.attachShadow) - ); -} +import { hide, show } from "webpack-dev-middleware/client/indicator"; + +const source = "webpack-dev-server"; /** + * @param {number} percent compilation progress + * @param {string} message progress message * @returns {void} */ -export function defineProgressElement() { - if (customElements.get("wds-progress")) { +export function showProgress(percent, message) { + if ( + typeof document === "undefined" || + typeof HTMLElement === "undefined" || + !HTMLElement.prototype.attachShadow + ) { return; } - class WebpackDevServerProgress extends HTMLElement { - constructor() { - super(); - this.attachShadow({ mode: "open" }); - this.maxDashOffset = -219.99078369140625; - this.animationTimer = null; - } - - #reset() { - clearTimeout(this.animationTimer); - this.animationTimer = null; - - const typeAttr = this.getAttribute("type")?.toLowerCase(); - this.type = typeAttr === "circular" ? "circular" : "linear"; - - const innerHTML = - this.type === "circular" - ? WebpackDevServerProgress.#circularTemplate() - : WebpackDevServerProgress.#linearTemplate(); - /** @type {ShadowRoot} */ - (this.shadowRoot).innerHTML = innerHTML; - - const progressValue = this.getAttribute("progress"); - this.initialProgress = progressValue ? Number(progressValue) : 0; - - this.#update(this.initialProgress); - } - - static #circularTemplate() { - return ` - - - `; - } - - static #linearTemplate() { - return ` - -
- `; - } - - connectedCallback() { - this.#reset(); - } - - static get observedAttributes() { - return ["progress", "type"]; - } - - /** - * @param {string} name name - * @param {string} oldValue old value - * @param {string} newValue new value - */ - attributeChangedCallback(name, oldValue, newValue) { - if (name === "progress") { - this.#update(Number(newValue)); - } else if (name === "type") { - this.#reset(); - } - } - - /** - * @param {number} percent percent - */ - #update(percent) { - const shadowRoot = /** @type {ShadowRoot} */ (this.shadowRoot); - const element = - /** @type {HTMLElement} */ - (shadowRoot.querySelector("#progress")); - if (this.type === "circular") { - const path = - /** @type {SVGPathElement} */ - (shadowRoot.querySelector("path")); - const value = - /** @type {HTMLElement} */ - (shadowRoot.querySelector("#percent-value")); - const offset = ((100 - percent) / 100) * this.maxDashOffset; - - path.style.strokeDashoffset = String(offset); - value.textContent = String(percent); - } else { - element.style.width = `${percent}%`; - } - - if (percent >= 100) { - this.#hide(); - } else if (percent > 0) { - this.#show(); - } - } - - #show() { - const shadowRoot = /** @type {ShadowRoot} */ (this.shadowRoot); - const element = - /** @type {HTMLElement} */ - (shadowRoot.querySelector("#progress")); - element.classList.remove("hidden"); - } - - #hide() { - const shadowRoot = /** @type {ShadowRoot} */ (this.shadowRoot); - const element = - /** @type {HTMLElement} */ - (shadowRoot.querySelector("#progress")); - if (this.type === "circular") { - element.classList.add("disappear"); - element.addEventListener( - "animationend", - () => { - element.classList.add("hidden"); - this.#update(0); - }, - { once: true }, - ); - } else if (this.type === "linear") { - element.classList.add("disappear"); - this.animationTimer = setTimeout(() => { - element.classList.remove("disappear"); - element.classList.add("hidden"); - element.style.width = "0%"; - this.animationTimer = null; - }, 800); - } - } + if (percent >= 100) { + hide(source); + } else { + show(`${percent}% - ${message}`, percent, source); } +} - customElements.define("wds-progress", WebpackDevServerProgress); +/** + * @returns {void} + */ +export function hideProgress() { + hide(source); } diff --git a/package-lock.json b/package-lock.json index 67f7f2df12..8923ab5046 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "@types/serve-index": "^1.9.4", "@types/serve-static": "^2.2.0", "@types/ws": "^8.18.1", - "ansi-html-community": "^0.0.8", "bonjour-service": "^1.3.0", "chokidar": "^5.0.0", "compression": "^1.8.1", @@ -32,7 +31,7 @@ "selfsigned": "^5.5.0", "serve-index": "^1.9.2", "tinyglobby": "^0.2.15", - "webpack-dev-middleware": "^8.0.3", + "webpack-dev-middleware": "^8.3.0", "ws": "^8.20.0" }, "bin": { @@ -3780,13 +3779,13 @@ } }, "node_modules/@jsonjoy.com/fs-core": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.1.tgz", - "integrity": "sha512-YrEi/ZPmgc+GfdO0esBF04qv8boK9Dg9WpRQw/+vM8Qt3nnVIJWIa8HwZ/LXVZ0DB11XUROM8El/7yYTJX+WtA==", + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.71.0.tgz", + "integrity": "sha512-9DFR/j+bm+tig1abs1CWS1/r0IVjNUdTp/+q6R/PXayXpO1rgbUh7tkanGYP40I0zdxbOreN3tmzBpVHgfMKzg==", "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.1", - "@jsonjoy.com/fs-node-utils": "4.57.1", + "@jsonjoy.com/fs-node-builtins": "4.71.0", + "@jsonjoy.com/fs-node-utils": "4.71.0", "thingies": "^2.5.0" }, "engines": { @@ -3801,14 +3800,14 @@ } }, "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.1.tgz", - "integrity": "sha512-ooEPvSW/HQDivPDPZMibHGKZf/QS4WRir1czGZmXmp3MsQqLECZEpN0JobrD8iV9BzsuwdIv+PxtWX9WpPLsIA==", + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.71.0.tgz", + "integrity": "sha512-dRw5ojxzep3lntVGVBLzQUMYj1hXLH+DAz9sK0vKU+594x/zA2nYPOiitlyhYtOJ2nv1FXNq7c55rgwANknIWw==", "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.57.1", - "@jsonjoy.com/fs-node-builtins": "4.57.1", - "@jsonjoy.com/fs-node-utils": "4.57.1", + "@jsonjoy.com/fs-core": "4.71.0", + "@jsonjoy.com/fs-node-builtins": "4.71.0", + "@jsonjoy.com/fs-node-utils": "4.71.0", "thingies": "^2.5.0" }, "engines": { @@ -3823,16 +3822,16 @@ } }, "node_modules/@jsonjoy.com/fs-node": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.1.tgz", - "integrity": "sha512-3YaKhP8gXEKN+2O49GLNfNb5l2gbnCFHyAaybbA2JkkbQP3dpdef7WcUaHAulg/c5Dg4VncHsA3NWAUSZMR5KQ==", + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.71.0.tgz", + "integrity": "sha512-yt5Ak0otPdHPIlmMvF16aLG2qSZL52EYJ7HOkYpBcIPWw+kmT1FtTSj4oiV2OUkJbG8pLzA+RhMgrlRl85QuBw==", "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.57.1", - "@jsonjoy.com/fs-node-builtins": "4.57.1", - "@jsonjoy.com/fs-node-utils": "4.57.1", - "@jsonjoy.com/fs-print": "4.57.1", - "@jsonjoy.com/fs-snapshot": "4.57.1", + "@jsonjoy.com/fs-core": "4.71.0", + "@jsonjoy.com/fs-node-builtins": "4.71.0", + "@jsonjoy.com/fs-node-utils": "4.71.0", + "@jsonjoy.com/fs-print": "4.71.0", + "@jsonjoy.com/fs-snapshot": "4.71.0", "glob-to-regex.js": "^1.0.0", "thingies": "^2.5.0" }, @@ -3848,9 +3847,9 @@ } }, "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.1.tgz", - "integrity": "sha512-XHkFKQ5GSH3uxm8c3ZYXVrexGdscpWKIcMWKFQpMpMJc8gA3AwOMBJXJlgpdJqmrhPyQXxaY9nbkNeYpacC0Og==", + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.71.0.tgz", + "integrity": "sha512-BSzl+QFSxZF58BxGjV1wqCJ+qSn3b2IZnb+z3hDQq6goqOO5RuxF8gRihjsFx17AfpEpa7XjYcP8XpQtDU8RrQ==", "license": "Apache-2.0", "engines": { "node": ">=10.0" @@ -3864,14 +3863,14 @@ } }, "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.1.tgz", - "integrity": "sha512-pqGHyWWzNck4jRfaGV39hkqpY5QjRUQ/nRbNT7FYbBa0xf4bDG+TE1Gt2KWZrSkrkZZDE3qZUjYMbjwSliX6pg==", + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.71.0.tgz", + "integrity": "sha512-OlXBZKIeDx5bIGcQmn0w+nVkLheCiuQSepKiBAiWSWimSdn8Q6Yc9ih2y8zStcJk31fieqnov9V+o8XTWn/5Rw==", "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-fsa": "4.57.1", - "@jsonjoy.com/fs-node-builtins": "4.57.1", - "@jsonjoy.com/fs-node-utils": "4.57.1" + "@jsonjoy.com/fs-fsa": "4.71.0", + "@jsonjoy.com/fs-node-builtins": "4.71.0", + "@jsonjoy.com/fs-node-utils": "4.71.0" }, "engines": { "node": ">=10.0" @@ -3885,12 +3884,13 @@ } }, "node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.1.tgz", - "integrity": "sha512-vp+7ZzIB8v43G+GLXTS4oDUSQmhAsRz532QmmWBbdYA20s465JvwhkSFvX9cVTqRRAQg+vZ7zWDaIEh0lFe2gw==", + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.71.0.tgz", + "integrity": "sha512-YtzCL3jbKYx6LHxqS9ymJ9Ob7SO9cucI+kpNO3LijGNFEjFbvNsTlHkvFgocAMAjUk96TpQTCsYkzFQi1CdlAA==", "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.1" + "@jsonjoy.com/fs-node-builtins": "4.71.0", + "glob-to-regex.js": "^1.0.1" }, "engines": { "node": ">=10.0" @@ -3904,12 +3904,12 @@ } }, "node_modules/@jsonjoy.com/fs-print": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.1.tgz", - "integrity": "sha512-Ynct7ZJmfk6qoXDOKfpovNA36ITUx8rChLmRQtW08J73VOiuNsU8PB6d/Xs7fxJC2ohWR3a5AqyjmLojfrw5yw==", + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.71.0.tgz", + "integrity": "sha512-OhfDSdvyO8uGV0U11OP+mOeVCPgjuP1HqCGR/TdBQLxHoDEWJAK3KOP5fPXo57H7i3G0x0Vmv8xPRHDle4XGzA==", "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.57.1", + "@jsonjoy.com/fs-node-utils": "4.71.0", "tree-dump": "^1.1.0" }, "engines": { @@ -3924,13 +3924,13 @@ } }, "node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.1.tgz", - "integrity": "sha512-/oG8xBNFMbDXTq9J7vepSA1kerS5vpgd3p5QZSPd+nX59uwodGJftI51gDYyHRpP57P3WCQf7LHtBYPqwUg2Bg==", + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.71.0.tgz", + "integrity": "sha512-md+Wov365xa9A3nfW9YQTHLcweHHAee/e/0UoVRbldEHxboPJknuO3sEkNVVECO0dTqKra/ERaQylAMRrGWd0Q==", "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.57.1", + "@jsonjoy.com/fs-node-utils": "4.71.0", "@jsonjoy.com/json-pack": "^17.65.0", "@jsonjoy.com/util": "^17.65.0" }, @@ -10420,9 +10420,9 @@ } }, "node_modules/glob-to-regex.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz", - "integrity": "sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", "license": "Apache-2.0", "engines": { "node": ">=10.0" @@ -13250,19 +13250,19 @@ } }, "node_modules/memfs": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.1.tgz", - "integrity": "sha512-WvzrWPwMQT+PtbX2Et64R4qXKK0fj/8pO85MrUCzymX3twwCiJCdvntW3HdhG1teLJcHDDLIKx5+c3HckWYZtQ==", + "version": "4.71.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.71.0.tgz", + "integrity": "sha512-Zwrk7TpTXBkic7taZL+2QeppbauG0QOufRsT1zuXDYLW8jCajH0W1VSZ17+I7ODqiNbH9J1Vf1QJCqFlCfurDg==", "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.57.1", - "@jsonjoy.com/fs-fsa": "4.57.1", - "@jsonjoy.com/fs-node": "4.57.1", - "@jsonjoy.com/fs-node-builtins": "4.57.1", - "@jsonjoy.com/fs-node-to-fsa": "4.57.1", - "@jsonjoy.com/fs-node-utils": "4.57.1", - "@jsonjoy.com/fs-print": "4.57.1", - "@jsonjoy.com/fs-snapshot": "4.57.1", + "@jsonjoy.com/fs-core": "4.71.0", + "@jsonjoy.com/fs-fsa": "4.71.0", + "@jsonjoy.com/fs-node": "4.71.0", + "@jsonjoy.com/fs-node-builtins": "4.71.0", + "@jsonjoy.com/fs-node-to-fsa": "4.71.0", + "@jsonjoy.com/fs-node-utils": "4.71.0", + "@jsonjoy.com/fs-print": "4.71.0", + "@jsonjoy.com/fs-snapshot": "4.71.0", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", @@ -13273,9 +13273,6 @@ "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" } }, "node_modules/memorystream": { @@ -15691,12 +15688,16 @@ "license": "MIT" }, "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "license": "MIT", "engines": { "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/raw-body": { @@ -18415,15 +18416,15 @@ } }, "node_modules/webpack-dev-middleware": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-8.0.3.tgz", - "integrity": "sha512-zWrde9VZDiRaFuWsjHO40wm9LxxtXEk8DdzFXdU7eU5ZpiANnZZDBbZgN3guxbEoKqUHd9YupBmynyioz42nkA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-8.3.0.tgz", + "integrity": "sha512-jFNNMB29ugmkEUFW9cpWNqcWFQNwuMTMVoRpKVAiz/5nh3gA4x9x6DRPhNHGc/SLre7pEP9yrAPDAmCvdCblKg==", "license": "MIT", "dependencies": { - "memfs": "^4.56.10", + "ansi-html-community": "^0.0.8", + "memfs": "^4.68.2", "mime-types": "^3.0.2", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", + "range-parser": "^1.3.0", "schema-utils": "^4.3.3" }, "engines": { diff --git a/package.json b/package.json index 38fbb1dd59..5971216253 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,6 @@ "@types/serve-index": "^1.9.4", "@types/serve-static": "^2.2.0", "@types/ws": "^8.18.1", - "ansi-html-community": "^0.0.8", "bonjour-service": "^1.3.0", "chokidar": "^5.0.0", "compression": "^1.8.1", @@ -81,7 +80,7 @@ "selfsigned": "^5.5.0", "serve-index": "^1.9.2", "tinyglobby": "^0.2.15", - "webpack-dev-middleware": "^8.0.3", + "webpack-dev-middleware": "^8.3.0", "ws": "^8.20.0" }, "devDependencies": { diff --git a/test/client/overlay.test.js b/test/client/overlay.test.js new file mode 100644 index 0000000000..b40e0c6994 --- /dev/null +++ b/test/client/overlay.test.js @@ -0,0 +1,55 @@ +import "../helpers/jsdom-setup.js"; + +import { afterEach, describe, it } from "node:test"; +import { expect } from "expect"; +import { clear, showProblems } from "webpack-dev-middleware/client/overlay"; +import { createOverlay } from "../../client-src/overlay.js"; + +const selector = "#webpack-dev-middleware-hot-overlay"; + +describe("shared overlay", () => { + afterEach(() => clear()); + + it("should render compiler messages safely and link files to the editor", () => { + const overlay = createOverlay({ catchRuntimeError: false }); + overlay.send({ + type: "BUILD_ERROR", + level: "error", + messages: [ + { + moduleName: "./src/app.js", + moduleIdentifier: "javascript/esm|/project/src/app.js", + loc: "2:3", + message: "", + }, + ], + }); + const frame = globalThis.document.querySelector(selector); + const document = frame.contentDocument; + expect(document.querySelector("script")).toBeNull(); + expect(document.querySelector("[data-open-file]").dataset.openFile).toBe( + "/project/src/app.js:2:3", + ); + expect(document.body.textContent).toContain(""); + overlay.send({ type: "DISMISS" }); + expect(globalThis.document.querySelector(selector)).toBeNull(); + }); + + it("should preserve problems reported by another client on a clean build", () => { + showProblems("errors", ["Other client error"], "other-client"); + const overlay = createOverlay({ catchRuntimeError: false }); + overlay.send({ + type: "BUILD_ERROR", + level: "error", + messages: ["Server error"], + }); + expect(document.querySelectorAll(selector)).toHaveLength(1); + overlay.send({ type: "DISMISS" }); + expect( + document.querySelector(selector).contentDocument.body.textContent, + ).toContain("Other client error"); + expect( + document.querySelector(selector).contentDocument.body.textContent, + ).not.toContain("Server error"); + }); +}); diff --git a/test/client/progress.test.js b/test/client/progress.test.js new file mode 100644 index 0000000000..d0514176a7 --- /dev/null +++ b/test/client/progress.test.js @@ -0,0 +1,38 @@ +import "../helpers/jsdom-setup.js"; + +import { afterEach, describe, it } from "node:test"; +import { expect } from "expect"; +import { hide, show } from "webpack-dev-middleware/client/indicator"; +import { hideProgress, showProgress } from "../../client-src/progress.js"; + +const selector = "#webpack-dev-middleware-building-indicator"; + +describe("progress", () => { + afterEach(() => hide()); + + it("should display progress and remove the indicator on completion", () => { + showProgress(25, "building"); + const indicator = document.querySelector(selector); + expect(indicator.shadowRoot.textContent).toContain("25% - building"); + expect(indicator.shadowRoot.querySelector("svg").style.display).toBe( + "block", + ); + + showProgress(100, "completed"); + expect(document.querySelector(selector)).toBeNull(); + + showProgress(10, "rebuilding"); + expect(document.querySelector(selector).shadowRoot.textContent).toContain( + "10% - rebuilding", + ); + }); + + it("should preserve another client's indicator when this client finishes", () => { + show("Other build", 10, "other-client"); + showProgress(50, "building"); + hideProgress(); + expect(document.querySelector(selector)).not.toBeNull(); + hide("other-client"); + expect(document.querySelector(selector)).toBeNull(); + }); +}); diff --git a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 index cc27c7c873..cbe57e8a71 100644 --- a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 @@ -44,7 +44,7 @@ exports[`overlay > should not show initially, then show on an error and allow to @@ -63,91 +64,117 @@ exports[`overlay > should not show initially, then show on an error and allow to `; exports[`overlay > should not show initially, then show on an error and allow to close 3`] = ` -" +"
-
- Compiled with problems: -
-
-
+ 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 > \`; -
-
+
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -176,7 +203,7 @@ exports[`overlay > should not show initially, then show on an error, then hide o @@ -195,91 +223,117 @@ exports[`overlay > should not show initially, then show on an error, then hide o `; exports[`overlay > should not show initially, then show on an error, then hide on fix 3`] = ` -" +"
-
- Compiled with problems: -
-
-
+ ERROR -
- 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 > \`; -
-
+ 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 + > \`; +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -308,7 +362,7 @@ exports[`overlay > should not show initially, then show on an error, then show o @@ -327,91 +382,117 @@ exports[`overlay > should not show initially, then show on an error, then show o `; exports[`overlay > should not show initially, then show on an error, then show other error, then hide on fix 3`] = ` -" +"
-
- Compiled with problems: -
-
-
+ 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 > \`; -
-
+
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -424,7 +505,7 @@ exports[`overlay > should not show initially, then show on an error, then show o @@ -443,91 +525,117 @@ exports[`overlay > should not show initially, then show on an error, then show o `; exports[`overlay > should not show initially, then show on an error, then show other error, then hide on fix 5`] = ` -" +"
-
- Compiled with problems: -
-
-
+ 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 -
-
+
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -556,7 +664,7 @@ exports[`overlay > should show a warning after invalidation 1`] = ` @@ -575,86 +684,86 @@ exports[`overlay > should show a warning after invalidation 1`] = ` `; exports[`overlay > should show a warning after invalidation 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ WARNING -
- WARNING -
-
- Warning from compilation -
-
+ in Warning from compilation +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -667,7 +776,7 @@ exports[`overlay > should show a warning and error for initial compilation 1`] = @@ -686,194 +796,134 @@ exports[`overlay > should show a warning and error for initial compilation 1`] = `; exports[`overlay > should show a warning and error for initial compilation 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ ERROR -
- ERROR -
-
- Warning from compilation -
-
-
+
+ ERROR -
- ERROR -
-
- Warning from compilation -
-
-
+
+ ERROR -
- ERROR -
-
- Error from compilation. Can't find 'test' module. -
-
-
+
+ ERROR -
- ERROR -
-
- Error from compilation. Can't find 'test' module. -
-
-
+
+ ERROR -
- ERROR -
-
- Error from compilation. Can't find 'test' module. -
-
+ in Error from compilation. Can't find 'test' module. +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -886,7 +936,7 @@ exports[`overlay > should show a warning and error for initial compilation and p @@ -905,113 +956,98 @@ exports[`overlay > should show a warning and error for initial compilation and p `; exports[`overlay > should show a warning and error for initial compilation and protects against xss 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ ERROR -
- ERROR -
-
- <strong>strong</strong> -
-
-
+
+ ERROR -
- ERROR -
-
- <strong>strong</strong> -
-
+ in <strong>strong</strong> +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -1024,7 +1060,7 @@ exports[`overlay > should show a warning and hide them after closing connection @@ -1043,86 +1080,86 @@ exports[`overlay > should show a warning and hide them after closing connection `; exports[`overlay > should show a warning and hide them after closing connection 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ WARNING -
- WARNING -
-
- Warning from compilation -
-
+ in Warning from compilation +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -1143,7 +1180,7 @@ exports[`overlay > should show a warning for initial compilation 1`] = ` @@ -1162,86 +1200,86 @@ exports[`overlay > should show a warning for initial compilation 1`] = ` `; exports[`overlay > should show a warning for initial compilation 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ WARNING -
- WARNING -
-
- Warning from compilation -
-
+ in Warning from compilation +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -1254,7 +1292,7 @@ exports[`overlay > should show a warning when \"client.overlay.errors\" is \"tru @@ -1273,86 +1312,86 @@ exports[`overlay > should show a warning when \"client.overlay.errors\" is \"tru `; exports[`overlay > should show a warning when \"client.overlay.errors\" is \"true\" 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ WARNING -
- WARNING -
-
- Warning from compilation -
-
+ in Warning from compilation +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -1365,7 +1404,7 @@ exports[`overlay > should show a warning when \"client.overlay.warnings\" is \"t @@ -1384,86 +1424,86 @@ exports[`overlay > should show a warning when \"client.overlay.warnings\" is \"t `; exports[`overlay > should show a warning when \"client.overlay.warnings\" is \"true\" 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ WARNING -
- WARNING -
-
- Warning from compilation -
-
+ in Warning from compilation +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -1476,7 +1516,7 @@ exports[`overlay > should show a warning when \"client.overlay\" is \"true\" 1`] @@ -1495,86 +1536,86 @@ exports[`overlay > should show a warning when \"client.overlay\" is \"true\" 1`] `; exports[`overlay > should show a warning when \"client.overlay\" is \"true\" 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ WARNING -
- WARNING -
-
- Warning from compilation -
-
+ in Warning from compilation +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -1587,7 +1628,7 @@ exports[`overlay > should show an ansi formatted error for initial compilation 1 @@ -1606,97 +1648,98 @@ exports[`overlay > should show an ansi formatted error for initial compilation 1 `; exports[`overlay > should show an ansi formatted error for initial compilation 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ ERROR + in + -
- ERROR -
-
- - 18 | - Render - ansi formatted text -
-
+ 18 | + Render + ansi formatted text +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -1709,7 +1752,7 @@ exports[`overlay > should show an error after invalidation 1`] = ` @@ -1728,86 +1772,86 @@ exports[`overlay > should show an error after invalidation 1`] = ` `; exports[`overlay > should show an error after invalidation 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ ERROR -
- ERROR -
-
- Error from compilation -
-
+ in Error from compilation +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -1820,7 +1864,7 @@ exports[`overlay > should show an error for initial compilation 1`] = ` @@ -1839,86 +1884,86 @@ exports[`overlay > should show an error for initial compilation 1`] = ` `; exports[`overlay > should show an error for initial compilation 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ ERROR -
- ERROR -
-
- Error from compilation. Can't find 'test' module. -
-
+ in Error from compilation. Can't find 'test' module. +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -1931,7 +1976,7 @@ exports[`overlay > should show an error when \"client.overlay.errors\" is \"true @@ -1950,86 +1996,86 @@ exports[`overlay > should show an error when \"client.overlay.errors\" is \"true `; exports[`overlay > should show an error when \"client.overlay.errors\" is \"true\" 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ ERROR -
- ERROR -
-
- Error from compilation. Can't find 'test' module. -
-
+ in Error from compilation. Can't find 'test' module. +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -2042,7 +2088,7 @@ exports[`overlay > should show an error when \"client.overlay.warnings\" is \"tr @@ -2061,86 +2108,86 @@ exports[`overlay > should show an error when \"client.overlay.warnings\" is \"tr `; exports[`overlay > should show an error when \"client.overlay.warnings\" is \"true\" 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ WARNING -
- WARNING -
-
- Warning from compilation -
-
+ in Warning from compilation +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -2153,7 +2200,7 @@ exports[`overlay > should show an error when \"client.overlay\" is \"true\" 1`] @@ -2172,86 +2220,86 @@ exports[`overlay > should show an error when \"client.overlay\" is \"true\" 1`] `; exports[`overlay > should show an error when \"client.overlay\" is \"true\" 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ ERROR -
- ERROR -
-
- Error from compilation. Can't find 'test' module. -
-
+ in Error from compilation. Can't find 'test' module. +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -2259,86 +2307,86 @@ exports[`overlay > should show an error when \"client.overlay\" is \"true\" 2`] `; exports[`overlay > should show error for uncaught promise rejection 1`] = ` -" +"
-
- Uncaught runtime errors: -
-
-
+ ERROR -
- ERROR -
-
- Async error at <anonymous>:3:26 -
-
+ in Async error at <anonymous>:3:26 +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -2346,87 +2394,87 @@ exports[`overlay > should show error for uncaught promise rejection 1`] = ` `; exports[`overlay > should show error for uncaught runtime error 1`] = ` -" +"
-
- Uncaught runtime errors: -
-
-
+ ERROR -
- ERROR -
-
- Injected error at throwError (<anonymous>:2:15) at - <anonymous>:3:9 -
-
+ in Injected error at throwError (<anonymous>:2:15) at + <anonymous>:3:9 +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -2439,7 +2487,7 @@ exports[`overlay > should show error when it is not filtered 1`] = ` @@ -2458,86 +2507,86 @@ exports[`overlay > should show error when it is not filtered 1`] = ` `; exports[`overlay > should show error when it is not filtered 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ ERROR -
- ERROR -
-
- Unfiltered error -
-
+ in Unfiltered error +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -2550,7 +2599,7 @@ exports[`overlay > should show overlay when Trusted Types are enabled 1`] = ` @@ -2569,86 +2619,86 @@ exports[`overlay > should show overlay when Trusted Types are enabled 1`] = ` `; exports[`overlay > should show overlay when Trusted Types are enabled 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ ERROR -
- ERROR -
-
- Error from compilation. Can't find 'test' module. -
-
+ in Error from compilation. Can't find 'test' module. +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -2661,7 +2711,7 @@ exports[`overlay > should show overlay when Trusted Types are enabled and the \" @@ -2680,86 +2731,86 @@ exports[`overlay > should show overlay when Trusted Types are enabled and the \" `; exports[`overlay > should show overlay when Trusted Types are enabled and the \"require-trusted-types-for 'script'\" header was used 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ ERROR -
- ERROR -
-
- Error from compilation. Can't find 'test' module. -
-
+ in Error from compilation. Can't find 'test' module. +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -2772,7 +2823,7 @@ exports[`overlay > should show overlay when \"Content-Security-Policy\" is \"def @@ -2791,86 +2843,86 @@ exports[`overlay > should show overlay when \"Content-Security-Policy\" is \"def `; exports[`overlay > should show overlay when \"Content-Security-Policy\" is \"default-src 'self'\" was used 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ ERROR -
- ERROR -
-
- Error from compilation. Can't find 'test' module. -
-
+ in Error from compilation. Can't find 'test' module. +
+
+ Click outside, press Esc, or fix the code to dismiss.
@@ -2883,7 +2935,7 @@ exports[`overlay > should show warning when it is not filtered 1`] = ` @@ -2902,86 +2955,86 @@ exports[`overlay > should show warning when it is not filtered 1`] = ` `; exports[`overlay > should show warning when it is not filtered 2`] = ` -" +"
-
- Compiled with problems: -
-
-
+ WARNING -
- WARNING -
-
- Unfiltered warning -
-
+ in Unfiltered warning +
+
+ Click outside, press Esc, or fix the code to dismiss.
diff --git a/test/e2e/overlay.test.js b/test/e2e/overlay.test.js index 0861c62771..b9d7ab8d94 100644 --- a/test/e2e/overlay.test.js +++ b/test/e2e/overlay.test.js @@ -114,11 +114,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -159,11 +161,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -208,11 +212,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -255,11 +261,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -301,11 +309,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -340,7 +350,7 @@ describe("overlay", () => { }); let pageHtml = await page.evaluate(() => document.body.outerHTML); - let overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + let overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); t.assert.snapshot( @@ -351,15 +361,17 @@ describe("overlay", () => { fs.writeFileSync(pathToOverlayFixture, "`;"); - await page.waitForSelector("#webpack-dev-server-client-overlay"); + await page.waitForSelector("#webpack-dev-middleware-hot-overlay"); - overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); pageHtml = await page.evaluate(() => document.body.outerHTML); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -374,12 +386,12 @@ describe("overlay", () => { fs.writeFileSync(pathToOverlayFixture, overlayFixtureCode); - await page.waitForSelector("#webpack-dev-server-client-overlay", { + await page.waitForSelector("#webpack-dev-middleware-hot-overlay", { hidden: true, }); pageHtml = await page.evaluate(() => document.body.outerHTML); - overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); t.assert.snapshot( @@ -410,7 +422,7 @@ describe("overlay", () => { }); let pageHtml = await page.evaluate(() => document.body.outerHTML); - let overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + let overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); t.assert.snapshot( @@ -421,15 +433,17 @@ describe("overlay", () => { fs.writeFileSync(pathToOverlayFixture, "`;"); - await page.waitForSelector("#webpack-dev-server-client-overlay"); + await page.waitForSelector("#webpack-dev-middleware-hot-overlay"); - overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); pageHtml = await page.evaluate(() => document.body.outerHTML); let overlayFrame = await overlayHandle.contentFrame(); - let overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + let overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -444,16 +458,18 @@ describe("overlay", () => { fs.writeFileSync(pathToOverlayFixture, "`;a"); - await page.waitForSelector("#webpack-dev-server-client-overlay", { + await page.waitForSelector("#webpack-dev-middleware-hot-overlay", { hidden: true, }); - await page.waitForSelector("#webpack-dev-server-client-overlay"); + await page.waitForSelector("#webpack-dev-middleware-hot-overlay"); - overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); pageHtml = await page.evaluate(() => document.body.outerHTML); overlayFrame = await overlayHandle.contentFrame(); - overlayHtml = await overlayFrame.evaluate(() => document.body.outerHTML); + overlayHtml = (await overlayFrame.evaluate(() => document.body.outerHTML)) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -468,12 +484,12 @@ describe("overlay", () => { fs.writeFileSync(pathToOverlayFixture, overlayFixtureCode); - await page.waitForSelector("#webpack-dev-server-client-overlay", { + await page.waitForSelector("#webpack-dev-middleware-hot-overlay", { hidden: true, }); pageHtml = await page.evaluate(() => document.body.outerHTML); - overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); t.assert.snapshot( @@ -504,7 +520,7 @@ describe("overlay", () => { }); let pageHtml = await page.evaluate(() => document.body.outerHTML); - let overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + let overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); t.assert.snapshot( @@ -515,15 +531,17 @@ describe("overlay", () => { fs.writeFileSync(pathToOverlayFixture, "`;"); - await page.waitForSelector("#webpack-dev-server-client-overlay"); + await page.waitForSelector("#webpack-dev-middleware-hot-overlay"); - overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); pageHtml = await page.evaluate(() => document.body.outerHTML); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -538,18 +556,18 @@ describe("overlay", () => { const frame = await page .frames() - .find((item) => item.name() === "webpack-dev-server-client-overlay"); + .find((item) => item.name() === "webpack-dev-middleware-hot-overlay"); const buttonHandle = await frame.$("button"); await buttonHandle.click(); - await page.waitForSelector("#webpack-dev-server-client-overlay", { + await page.waitForSelector("#webpack-dev-middleware-hot-overlay", { hidden: true, }); pageHtml = await page.evaluate(() => document.body.outerHTML); - overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); t.assert.snapshot( @@ -588,13 +606,13 @@ describe("overlay", () => { fs.writeFileSync(pathToOverlayFixture, "`;"); - await page.waitForSelector("#webpack-dev-server-client-overlay"); + await page.waitForSelector("#webpack-dev-middleware-hot-overlay"); const frame = page .frames() - .find((item) => item.name() === "webpack-dev-server-client-overlay"); + .find((item) => item.name() === "webpack-dev-middleware-hot-overlay"); - const errorHandle = await frame.$("[data-can-open]"); + const errorHandle = await frame.$("[data-open-file]"); await errorHandle.click(); @@ -636,7 +654,7 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); t.assert.snapshot( @@ -678,7 +696,7 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); t.assert.snapshot( @@ -725,7 +743,7 @@ describe("overlay", () => { // Delay for the overlay to appear await delay(1000); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); } finally { @@ -764,11 +782,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -812,11 +832,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -862,11 +884,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -912,11 +936,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -960,7 +986,7 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); t.assert.snapshot( @@ -1002,7 +1028,7 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); t.assert.snapshot( @@ -1050,7 +1076,7 @@ describe("overlay", () => { // Delay for the overlay to appear await delay(1000); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); } finally { @@ -1089,11 +1115,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -1137,11 +1165,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -1193,11 +1223,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); expect( consoleMessages.filter((item) => @@ -1260,11 +1292,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); await page.goto(`http://localhost:${port}/`, { waitUntil: "networkidle0", @@ -1319,7 +1353,7 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); t.assert.snapshot( await format(pageHtml, { @@ -1360,11 +1394,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -1410,11 +1446,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -1459,11 +1497,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -1537,14 +1577,16 @@ describe("overlay", () => { // Delay for the overlay to appear await delay(1000); - await page.waitForSelector("#webpack-dev-server-client-overlay"); + await page.waitForSelector("#webpack-dev-middleware-hot-overlay"); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -1596,14 +1638,16 @@ describe("overlay", () => { // Delay for the overlay to appear await delay(1000); - await page.waitForSelector("#webpack-dev-server-client-overlay"); + await page.waitForSelector("#webpack-dev-middleware-hot-overlay"); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { @@ -1649,11 +1693,13 @@ describe("overlay", () => { // Delay for the overlay to appear await delay(1000); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(overlayHtml, { @@ -1699,7 +1745,7 @@ describe("overlay", () => { // Delay for the overlay to appear await delay(1000); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); } finally { @@ -1738,11 +1784,13 @@ describe("overlay", () => { // Delay for the overlay to appear await delay(1000); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(overlayHtml, { @@ -1790,7 +1838,7 @@ describe("overlay", () => { // Delay for the overlay to appear await delay(1000); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); } finally { @@ -1835,7 +1883,7 @@ describe("overlay", () => { // Delay for the overlay to appear await delay(1000); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); expect(overlayHandle).toBeNull(); } finally { @@ -1879,11 +1927,13 @@ describe("overlay", () => { await delay(1000); const pageHtml = await page.evaluate(() => document.body.outerHTML); - const overlayHandle = await page.$("#webpack-dev-server-client-overlay"); + const overlayHandle = await page.$("#webpack-dev-middleware-hot-overlay"); const overlayFrame = await overlayHandle.contentFrame(); - const overlayHtml = await overlayFrame.evaluate( - () => document.body.outerHTML, - ); + const overlayHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); t.assert.snapshot( await format(pageHtml, { From 6f2bb16b697046db1486dbd9907e7a11c5938d58 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 5 Sep 2026 23:26:23 -0500 Subject: [PATCH 2/4] fix: enable pagination in the shared overlay --- client-src/overlay.js | 60 ++-- test/client/overlay.test.js | 43 +++ .../overlay.test.js.snap.webpack5 | 338 ++++++++++++++---- test/e2e/overlay.test.js | 12 + 4 files changed, 363 insertions(+), 90 deletions(-) diff --git a/client-src/overlay.js b/client-src/overlay.js index 710379014e..b7d74d6b92 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -282,7 +282,7 @@ const createOverlay = (options) => { trustedTypesPolicyName: options.trustedTypesPolicyName || "webpack-dev-server#overlay", openEditorEndpoint: "/webpack-dev-server/open-editor", - paginate: false, + paginate: true, }); /** @type {(event: KeyboardEvent) => void} */ @@ -297,6 +297,35 @@ const createOverlay = (options) => { sharedOverlay.clear("webpack-dev-server"); }; + /** @type {Document | null | undefined} */ + let overlayDocument; + /** @type {(string | Message)[]} */ + let overlayMessages = []; + + // Pagination renders new links. Resolve them after mouse or keyboard + // navigation as well as when compiler messages first arrive. + const updateEditorLinks = () => { + const links = overlayDocument?.querySelectorAll("[data-open-file]"); + links?.forEach((link) => { + const file = link.getAttribute("data-open-file") || ""; + overlayMessages.forEach((message) => { + if (typeof message === "string" || !message.moduleIdentifier) { + return; + } + const name = (message.moduleName || message.file || "").replace( + /^(\s|\S)*!/, + "", + ); + if (name && file.indexOf(`${name}:`) === 0) { + link.setAttribute( + "data-open-file", + `${message.moduleIdentifier.replace(/^[^|]*\|/, "").replace(/^(\s|\S)*!/, "")}${file.slice(name.length)}`, + ); + } + }); + }); + }; + const overlayService = createOverlayMachine({ showOverlay: ({ level = "error", messages }) => { sharedOverlay.showProblems( @@ -309,31 +338,16 @@ const createOverlay = (options) => { "webpack-dev-server", ); - // The shared renderer links display paths. Resolve those links to the - // compiler's module identifiers, which can be outside the server cwd. + overlayMessages = messages; const frame = /** @type {HTMLIFrameElement | null} */ ( document.getElementById("webpack-dev-middleware-hot-overlay") ); - const links = - frame?.contentDocument?.querySelectorAll("[data-open-file]"); - links?.forEach((link) => { - const file = link.getAttribute("data-open-file") || ""; - messages.forEach((message) => { - if (typeof message === "string" || !message.moduleIdentifier) { - return; - } - const name = (message.moduleName || message.file || "").replace( - /^(\s|\S)*!/, - "", - ); - if (name && file.indexOf(`${name}:`) === 0) { - link.setAttribute( - "data-open-file", - `${message.moduleIdentifier.replace(/^[^|]*\|/, "").replace(/^(\s|\S)*!/, "")}${file.slice(name.length)}`, - ); - } - }); - }); + if (frame?.contentDocument !== overlayDocument) { + overlayDocument = frame?.contentDocument; + overlayDocument?.addEventListener("click", updateEditorLinks); + overlayDocument?.addEventListener("keydown", updateEditorLinks); + } + updateEditorLinks(); }, hideOverlay: hideOverlayWithEscCleanup, }); diff --git a/test/client/overlay.test.js b/test/client/overlay.test.js index b40e0c6994..1bef1a28d0 100644 --- a/test/client/overlay.test.js +++ b/test/client/overlay.test.js @@ -35,6 +35,49 @@ describe("shared overlay", () => { expect(globalThis.document.querySelector(selector)).toBeNull(); }); + it("should paginate problems with buttons and arrow keys and resolve each page's editor links", () => { + const overlay = createOverlay({ catchRuntimeError: false }); + overlay.send({ + type: "BUILD_ERROR", + level: "error", + messages: [ + { + moduleName: "./first.js", + moduleIdentifier: "/project/first.js", + loc: "1:1", + message: "First problem", + }, + { + moduleName: "./second.js", + moduleIdentifier: "/project/second.js", + loc: "2:3", + message: "Second problem", + }, + ], + }); + const frameDocument = document.querySelector(selector).contentDocument; + expect(frameDocument.body.textContent).toContain("1 / 2"); + expect(frameDocument.body.textContent).toContain("First problem"); + expect(frameDocument.body.textContent).not.toContain("Second problem"); + + frameDocument.querySelector('[aria-label="Next problem"]').click(); + expect(frameDocument.body.textContent).toContain("2 / 2"); + expect(frameDocument.body.textContent).toContain("Second problem"); + expect(frameDocument.body.textContent).not.toContain("First problem"); + expect( + frameDocument.querySelector("[data-open-file]").dataset.openFile, + ).toBe("/project/second.js:2:3"); + + frameDocument.dispatchEvent( + new KeyboardEvent("keydown", { key: "ArrowLeft" }), + ); + expect(frameDocument.body.textContent).toContain("1 / 2"); + expect( + frameDocument.querySelector("[data-open-file]").dataset.openFile, + ).toBe("/project/first.js:1:1"); + overlay.send({ type: "DISMISS" }); + }); + it("should preserve problems reported by another client on a clean build", () => { showProblems("errors", ["Other client error"], "other-client"); const overlay = createOverlay({ catchRuntimeError: false }); diff --git a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 index cbe57e8a71..db26b505b4 100644 --- a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 @@ -854,65 +854,221 @@ exports[`overlay > should show a warning and error for initial compilation 2`] = > × -
+
ERRORERROR + in Warning from compilation - in Warning from compilation -
-
- ERROR - in Warning from compilation + 1 / 5 +
-
- ERROR - in Error from compilation. Can't find 'test' module. +
+ Use ‹ › or the arrow keys to navigate. Click outside, press Esc, or fix + the code to dismiss.
-
+
+ +" +`; + +exports[`overlay > should show a warning and error for initial compilation 3`] = ` +" +
+ +
ERRORERROR + in Warning from compilation - in Error from compilation. Can't find 'test' module. -
-
- ERROR - in Error from compilation. Can't find 'test' module. + 2 / 5 +
- Click outside, press Esc, or fix the code to dismiss. + Use ‹ › or the arrow keys to navigate. Click outside, press Esc, or fix + the code to dismiss.
@@ -1014,29 +1171,75 @@ exports[`overlay > should show a warning and error for initial compilation and p > × -
+
ERRORERROR + in <strong>strong</strong> - in <strong>strong</strong> -
-
- ERROR - in <strong>strong</strong> + 1 / 2 +
- Click outside, press Esc, or fix the code to dismiss. + Use ‹ › or the arrow keys to navigate. Click outside, press Esc, or fix + the code to dismiss.
diff --git a/test/e2e/overlay.test.js b/test/e2e/overlay.test.js index b9d7ab8d94..250143a39e 100644 --- a/test/e2e/overlay.test.js +++ b/test/e2e/overlay.test.js @@ -230,6 +230,18 @@ describe("overlay", () => { parser: "html", }), ); + await overlayFrame.click('[aria-label="Next problem"]'); + const nextPageHtml = ( + await overlayFrame.evaluate(() => document.body.outerHTML) + ) + .replaceAll(config.context.replaceAll("\\", "/"), "") + .replaceAll(config.context, ""); + t.assert.snapshot(await format(nextPageHtml, { parser: "html" })); + + await overlayFrame.click('[aria-label="Previous problem"]'); + expect(await overlayFrame.evaluate(() => document.body.outerHTML)).toBe( + overlayHtml, + ); } finally { await browser.close(); await server.stop(); From d4fc4fd6b1a8aac96f09963ae5b02cc56792af8d Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 5 Sep 2026 23:41:04 -0500 Subject: [PATCH 3/4] refactor: delegate overlay state and runtime errors to dev-middleware --- client-src/globals.d.ts | 1 + client-src/overlay.js | 365 +++--------------- test/client/ReactErrorBoundary.test.js | 191 ++++----- test/client/overlay.test.js | 28 ++ .../overlay.test.js.snap.webpack5 | 90 +---- test/e2e/overlay.test.js | 6 +- 6 files changed, 179 insertions(+), 502 deletions(-) diff --git a/client-src/globals.d.ts b/client-src/globals.d.ts index 4a454b86c2..ed72e003ba 100644 --- a/client-src/globals.d.ts +++ b/client-src/globals.d.ts @@ -18,6 +18,7 @@ declare module "webpack-dev-middleware/client/overlay" { trustedTypesPolicyName?: string; openEditorEndpoint?: string; paginate?: boolean; + catchRuntimeError?: boolean | ((error: Error) => boolean); }): { showProblems( type: "errors" | "warnings", diff --git a/client-src/overlay.js b/client-src/overlay.js index b7d74d6b92..4c4d02b1aa 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -1,229 +1,5 @@ import configureOverlay from "webpack-dev-middleware/client/overlay"; -/** @typedef {import("./index.js").EXPECTED_ANY} EXPECTED_ANY */ - -/** - * @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); - - if (nextContextValue) { - currentContext = { - ...currentContext, - ...nextContextValue, - }; - } - }); - } - } - }, - }; -} - -/** - * @typedef {object} ShowOverlayData - * @property {"warning" | "error"} level level - * @property {(string | Message)[]} messages messages - * @property {"build" | "runtime"} messageSource message source - */ - -/** - * @typedef {object} CreateOverlayMachineOptions - * @property {(data: ShowOverlayData) => void} showOverlay show overlay - * @property {() => void} hideOverlay hide overlay - */ - -/** - * @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"], - }, - 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 - */ -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}`); - } -}; - -/** - * @callback ErrorCallback - * @param {ErrorEvent} error - * @returns {void} - */ - -/** - * @param {ErrorCallback} callback callback - * @returns {() => void} cleanup - */ -const listenToRuntimeError = (callback) => { - window.addEventListener("error", callback); - - return function cleanup() { - window.removeEventListener("error", callback); - }; -}; - -/** - * @callback UnhandledRejectionCallback - * @param {PromiseRejectionEvent} rejectionEvent - * @returns {void} - */ - -/** - * @param {UnhandledRejectionCallback} callback callback - * @returns {() => void} cleanup - */ -const listenToUnhandledRejection = (callback) => { - window.addEventListener("unhandledrejection", callback); - - return function cleanup() { - window.removeEventListener("unhandledrejection", callback); - }; -}; - /** @typedef {Error & { file?: string, moduleName?: string, moduleIdentifier?: string, loc?: string, message?: string, stack?: string | string[] }} Message */ /** @@ -267,36 +43,64 @@ const formatProblem = (type, item) => { return { header, body }; }; +/** @typedef {{ type: "DISMISS" } | { type: "BUILD_ERROR", level: "warning" | "error", messages: (string | Message)[] }} OverlayEvent */ + +// The middleware wraps non-Error throws without `cause`. Preserve the original +// value for dev-server's runtimeErrors filters, including rejected objects. +/** @type {unknown} */ +let runtimeErrorCause; +let captureRuntimeErrorCause = false; + /** * @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 runtime error catcher */ /** * @param {CreateOverlayOptions} options options - * @returns {StateMachine} overlay + * @returns {{ send: (event: OverlayEvent) => void }} overlay */ const createOverlay = (options) => { + if ( + !captureRuntimeErrorCause && + typeof options.catchRuntimeError === "function" + ) { + window.addEventListener( + "error", + (event) => { + runtimeErrorCause = event.error; + }, + true, + ); + window.addEventListener( + "unhandledrejection", + (event) => { + runtimeErrorCause = event.reason; + }, + true, + ); + captureRuntimeErrorCause = true; + } + const sharedOverlay = configureOverlay({ trustedTypesPolicyName: options.trustedTypesPolicyName || "webpack-dev-server#overlay", openEditorEndpoint: "/webpack-dev-server/open-editor", paginate: true, + catchRuntimeError: (error) => { + const cause = runtimeErrorCause; + runtimeErrorCause = undefined; + return typeof options.catchRuntimeError === "function" + ? options.catchRuntimeError( + cause instanceof Error + ? error + : new Error(error.message, { cause }), + ) + : Boolean(options.catchRuntimeError); + }, }); - /** @type {(event: KeyboardEvent) => void} */ - let handleEscapeKey; - - /** - * @returns {void} - */ - - const hideOverlayWithEscCleanup = () => { - window.removeEventListener("keydown", handleEscapeKey); - sharedOverlay.clear("webpack-dev-server"); - }; - /** @type {Document | null | undefined} */ let overlayDocument; /** @type {(string | Message)[]} */ @@ -326,8 +130,17 @@ const createOverlay = (options) => { }); }; - const overlayService = createOverlayMachine({ - showOverlay: ({ level = "error", messages }) => { + return { + send(event) { + if (event.type === "DISMISS") { + sharedOverlay.clear("webpack-dev-server:warning"); + sharedOverlay.clear("webpack-dev-server:error"); + sharedOverlay.clear("runtime"); + overlayMessages = []; + return; + } + + const { level, messages } = event; sharedOverlay.showProblems( level === "warning" ? "warnings" : "errors", messages.map((message) => { @@ -335,7 +148,7 @@ const createOverlay = (options) => { const location = header.replace(/^(ERROR|WARNING)( in )?/, ""); return location ? `${location}\n${body}` : body; }), - "webpack-dev-server", + `webpack-dev-server:${level}`, ); overlayMessages = messages; @@ -349,77 +162,7 @@ const createOverlay = (options) => { } updateEditorLinks(); }, - hideOverlay: hideOverlayWithEscCleanup, - }); - /** - * 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) { - /** - * @param {Error | undefined} error error - * @param {string} fallbackMessage fallback message - */ - 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; - } - - handleError(error, message); - }); - - listenToUnhandledRejection((promiseRejectionEvent) => { - const { reason } = promiseRejectionEvent; - - handleError(reason, "Unknown promise rejection reason"); - }); - } - - return overlayService; }; export { createOverlay, formatProblem }; diff --git a/test/client/ReactErrorBoundary.test.js b/test/client/ReactErrorBoundary.test.js index bcbd8552bf..c91a49db5b 100644 --- a/test/client/ReactErrorBoundary.test.js +++ b/test/client/ReactErrorBoundary.test.js @@ -1,24 +1,17 @@ import "../helpers/jsdom-setup.js"; -import { afterEach, beforeEach, describe, it, mock } from "node:test"; +import { afterEach, describe, it } from "node:test"; import { expect } from "expect"; -import { spyOn } from "jest-mock"; +import { clear } from "webpack-dev-middleware/client/overlay"; import { createOverlay } from "../../client-src/overlay.js"; describe("createOverlay", () => { - beforeEach(() => { - mock.timers.enable(); - }); - - afterEach(() => { - mock.timers.reset(); - mock.reset(); - }); + const selector = "#webpack-dev-middleware-hot-overlay"; + afterEach(() => clear()); it("should not show overlay for errors caught by React error boundaries", () => { const options = { trustedTypesPolicyName: null, catchRuntimeError: true }; - const overlay = createOverlay(options); - const showOverlayMock = spyOn(overlay, "send"); + createOverlay(options); const reactError = new Error( "Error inside React render\n" + @@ -41,14 +34,12 @@ describe("createOverlay", () => { }); globalThis.dispatchEvent(errorEvent); - expect(showOverlayMock).not.toHaveBeenCalled(); - showOverlayMock.mockRestore(); + expect(document.querySelector(selector)).toBeNull(); }); it("should show overlay for normal uncaught errors", () => { const options = { trustedTypesPolicyName: null, catchRuntimeError: true }; - const overlay = createOverlay(options); - const showOverlayMock = spyOn(overlay, "send"); + createOverlay(options); const regularError = new Error( "Error inside React render\n" + @@ -65,22 +56,14 @@ describe("createOverlay", () => { }); globalThis.dispatchEvent(errorEvent); - expect(showOverlayMock).toHaveBeenCalledWith({ - type: "RUNTIME_ERROR", - messages: [ - { - message: regularError.message, - stack: expect.anything(), - }, - ], - }); - showOverlayMock.mockRestore(); + expect( + document.querySelector(selector).contentDocument.body.textContent, + ).toContain(regularError.message); }); it("should show overlay for normal uncaught errors (when null is thrown)", () => { const options = { trustedTypesPolicyName: null, catchRuntimeError: true }; - const overlay = createOverlay(options); - const showOverlayMock = spyOn(overlay, "send"); + createOverlay(options); const errorEvent = new ErrorEvent("error", { error: null, @@ -88,16 +71,9 @@ describe("createOverlay", () => { }); globalThis.dispatchEvent(errorEvent); - expect(showOverlayMock).toHaveBeenCalledWith({ - type: "RUNTIME_ERROR", - messages: [ - { - message: "error", - stack: expect.anything(), - }, - ], - }); - showOverlayMock.mockRestore(); + expect( + document.querySelector(selector).contentDocument.body.textContent, + ).toContain("error"); }); it("should show overlay for normal uncaught errors when catchRuntimeError is a function that return true", () => { @@ -105,8 +81,7 @@ describe("createOverlay", () => { trustedTypesPolicyName: null, catchRuntimeError: () => true, }; - const overlay = createOverlay(options); - const showOverlayMock = spyOn(overlay, "send"); + createOverlay(options); const regularError = new Error("Regular test error"); const errorEvent = new ErrorEvent("error", { @@ -115,16 +90,9 @@ describe("createOverlay", () => { }); globalThis.dispatchEvent(errorEvent); - expect(showOverlayMock).toHaveBeenCalledWith({ - type: "RUNTIME_ERROR", - messages: [ - { - message: regularError.message, - stack: expect.anything(), - }, - ], - }); - showOverlayMock.mockRestore(); + expect( + document.querySelector(selector).contentDocument.body.textContent, + ).toContain(regularError.message); }); it("should not show overlay for normal uncaught errors when catchRuntimeError is a function that return false", () => { @@ -132,8 +100,7 @@ describe("createOverlay", () => { trustedTypesPolicyName: null, catchRuntimeError: () => false, }; - const overlay = createOverlay(options); - const showOverlayMock = spyOn(overlay, "send"); + createOverlay(options); const regularError = new Error("Regular test error"); const errorEvent = new ErrorEvent("error", { @@ -142,14 +109,12 @@ describe("createOverlay", () => { }); globalThis.dispatchEvent(errorEvent); - expect(showOverlayMock).not.toHaveBeenCalled(); - showOverlayMock.mockRestore(); + expect(document.querySelector(selector)).toBeNull(); }); it("should not show the overlay for errors with stack containing 'invokeGuardedCallbackDev'", () => { const options = { trustedTypesPolicyName: null, catchRuntimeError: true }; - const overlay = createOverlay(options); - const showOverlayMock = spyOn(overlay, "send"); + createOverlay(options); const reactInternalError = new Error("React internal error"); reactInternalError.stack = "invokeGuardedCallbackDev\n at somefile.js"; @@ -159,14 +124,12 @@ describe("createOverlay", () => { }); globalThis.dispatchEvent(errorEvent); - expect(showOverlayMock).not.toHaveBeenCalled(); - showOverlayMock.mockRestore(); + expect(document.querySelector(selector)).toBeNull(); }); it("should show overlay for unhandled rejections", () => { const options = { trustedTypesPolicyName: null, catchRuntimeError: true }; - const overlay = createOverlay(options); - const showOverlayMock = spyOn(overlay, "send"); + createOverlay(options); const rejectionReason = new Error("Promise rejection reason"); const rejectionEvent = new Event("unhandledrejection"); @@ -174,72 +137,72 @@ describe("createOverlay", () => { globalThis.dispatchEvent(rejectionEvent); - expect(showOverlayMock).toHaveBeenCalledWith({ - type: "RUNTIME_ERROR", - messages: [ - { - message: rejectionReason.message, - stack: expect.anything(), - }, - ], - }); - showOverlayMock.mockRestore(); + expect( + document.querySelector(selector).contentDocument.body.textContent, + ).toContain(rejectionReason.message); }); it("should show overlay for unhandled rejections with string reason", () => { const options = { trustedTypesPolicyName: null, catchRuntimeError: true }; - const overlay = createOverlay(options); - const showOverlayMock = spyOn(overlay, "send"); + createOverlay(options); const rejectionEvent = new Event("unhandledrejection"); rejectionEvent.reason = "some reason"; globalThis.dispatchEvent(rejectionEvent); - expect(showOverlayMock).toHaveBeenCalledWith({ - type: "RUNTIME_ERROR", - messages: [ - { - message: "some reason", - stack: expect.anything(), - }, - ], - }); - showOverlayMock.mockRestore(); + expect( + document.querySelector(selector).contentDocument.body.textContent, + ).toContain("some reason"); }); - // ESC key test cases - - it("should dismiss overlay when ESC key is pressed", () => { - const options = { trustedTypesPolicyName: null, catchRuntimeError: true }; - const overlay = createOverlay(options); - const showOverlayMock = spyOn(overlay, "send"); - - const escEvent = new KeyboardEvent("keydown", { key: "Escape" }); - globalThis.window.dispatchEvent(escEvent); - - expect(showOverlayMock).toHaveBeenCalledWith({ type: "DISMISS" }); - showOverlayMock.mockRestore(); + for (const target of ["page", "frame"]) { + it(`should dismiss the overlay with Escape from the ${target}`, () => { + const overlay = createOverlay({ catchRuntimeError: true }); + overlay.send({ + type: "BUILD_ERROR", + level: "error", + messages: ["Build failed"], + }); + const frame = document.querySelector(selector); + const targetDocument = + target === "page" ? document : frame.contentDocument; + targetDocument.dispatchEvent( + new KeyboardEvent("keydown", { key: "Escape" }), + ); + expect(document.querySelector(selector)).toBeNull(); + }); + } + + it("should preserve the cause of a rejected value for runtime error filters", () => { + const reason = { error: new Error("Rejected object") }; + let received; + createOverlay({ + catchRuntimeError: (error) => { + received = error; + return false; + }, + }); + const event = new Event("unhandledrejection"); + event.reason = reason; + globalThis.dispatchEvent(event); + expect(received.cause).toBe(reason); + expect(document.querySelector(selector)).toBeNull(); }); - it("should dismiss overlay when 'Esc' key is pressed (older browsers)", () => { - const options = { trustedTypesPolicyName: null, catchRuntimeError: true }; - const overlay = createOverlay(options); - const showOverlayMock = spyOn(overlay, "send"); - - const escEvent = new KeyboardEvent("keydown", { key: "Esc" }); - globalThis.window.dispatchEvent(escEvent); - - expect(showOverlayMock).toHaveBeenCalledWith({ type: "DISMISS" }); - showOverlayMock.mockRestore(); + it("should stop displaying runtime errors when capture is disabled", () => { + createOverlay({ catchRuntimeError: true }); + createOverlay({ catchRuntimeError: false }); + globalThis.dispatchEvent( + new ErrorEvent("error", { error: new Error("Ignored") }), + ); + expect(document.querySelector(selector)).toBeNull(); }); - it("should not dismiss overlay for other keys", () => { - const options = { trustedTypesPolicyName: null, catchRuntimeError: true }; - const overlay = createOverlay(options); - const showOverlayMock = spyOn(overlay, "send"); - - const otherKeyEvent = new KeyboardEvent("keydown", { key: "Enter" }); - globalThis.window.dispatchEvent(otherKeyEvent); - - expect(showOverlayMock).not.toHaveBeenCalled(); - showOverlayMock.mockRestore(); + it("should clear runtime problems on the next build", () => { + const overlay = createOverlay({ catchRuntimeError: true }); + globalThis.dispatchEvent( + new ErrorEvent("error", { error: new Error("Runtime failure") }), + ); + expect(document.querySelector(selector)).not.toBeNull(); + overlay.send({ type: "DISMISS" }); + expect(document.querySelector(selector)).toBeNull(); }); }); diff --git a/test/client/overlay.test.js b/test/client/overlay.test.js index 1bef1a28d0..16471a0628 100644 --- a/test/client/overlay.test.js +++ b/test/client/overlay.test.js @@ -78,6 +78,34 @@ describe("shared overlay", () => { overlay.send({ type: "DISMISS" }); }); + it("should replace build problems without retaining dismissed messages", () => { + const overlay = createOverlay({ catchRuntimeError: false }); + overlay.send({ + type: "BUILD_ERROR", + level: "error", + messages: ["First failure"], + }); + overlay.send({ + type: "BUILD_ERROR", + level: "error", + messages: ["Latest failure"], + }); + const frameDocument = document.querySelector(selector).contentDocument; + expect(frameDocument.body.textContent).toContain("Latest failure"); + expect(frameDocument.body.textContent).not.toContain("First failure"); + + frameDocument.querySelector('[aria-label="Close"]').click(); + overlay.send({ + type: "BUILD_ERROR", + level: "error", + messages: ["New failure"], + }); + expect( + document.querySelector(selector).contentDocument.body.textContent, + ).not.toContain("Latest failure"); + overlay.send({ type: "DISMISS" }); + }); + it("should preserve problems reported by another client on a clean build", () => { showProblems("errors", ["Other client error"], "other-client"); const overlay = createOverlay({ catchRuntimeError: false }); diff --git a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 index db26b505b4..182e34a545 100644 --- a/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 +++ b/test/e2e/__snapshots__/overlay.test.js.snap.webpack5 @@ -875,7 +875,7 @@ exports[`overlay > should show a warning and error for initial compilation 2`] = " >ERROR - in Warning from compilation
1 / 51 / 32 / 52 / 3 -
+
ERROR - in <strong>strong</strong> -
ERROR - 1 / 2 -
+ in <strong>strong</strong>
- Use ‹ › or the arrow keys to navigate. Click outside, press Esc, or fix - the code to dismiss. + Click outside, press Esc, or fix the code to dismiss.
@@ -2579,7 +2520,8 @@ exports[`overlay > should show error for uncaught promise rejection 1`] = ` " >ERROR - in Async error at <anonymous>:3:26 + in Uncaught runtime error: Async error Error: Async error at + <anonymous>:3:26
ERROR - in Injected error at throwError (<anonymous>:2:15) at - <anonymous>:3:9 + in Uncaught runtime error: Injected error Error: Injected error at + throwError (<anonymous>:2:15) at <anonymous>:3:9
{ new WarningPlugin().apply(compiler); new WarningPlugin().apply(compiler); - new ErrorPlugin().apply(compiler); - new ErrorPlugin().apply(compiler); - new ErrorPlugin().apply(compiler); + new ErrorPlugin("First compilation error").apply(compiler); + new ErrorPlugin("Second compilation error").apply(compiler); + new ErrorPlugin("Third compilation error").apply(compiler); const devServerOptions = { port, From 66da6ab5a0c2ce2dacaa033a79eea2d9f9f5928e Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 5 Sep 2026 23:46:10 -0500 Subject: [PATCH 4/4] refactor: remove duplicate runtime error listeners --- client-src/overlay.js | 39 +------------------------- test/client/ReactErrorBoundary.test.js | 8 ++++-- test/e2e/overlay.test.js | 6 ++-- 3 files changed, 10 insertions(+), 43 deletions(-) diff --git a/client-src/overlay.js b/client-src/overlay.js index 4c4d02b1aa..4f4655d590 100644 --- a/client-src/overlay.js +++ b/client-src/overlay.js @@ -45,12 +45,6 @@ const formatProblem = (type, item) => { /** @typedef {{ type: "DISMISS" } | { type: "BUILD_ERROR", level: "warning" | "error", messages: (string | Message)[] }} OverlayEvent */ -// The middleware wraps non-Error throws without `cause`. Preserve the original -// value for dev-server's runtimeErrors filters, including rejected objects. -/** @type {unknown} */ -let runtimeErrorCause; -let captureRuntimeErrorCause = false; - /** * @typedef {object} CreateOverlayOptions * @property {(false | string)=} trustedTypesPolicyName trusted types policy name @@ -62,43 +56,12 @@ let captureRuntimeErrorCause = false; * @returns {{ send: (event: OverlayEvent) => void }} overlay */ const createOverlay = (options) => { - if ( - !captureRuntimeErrorCause && - typeof options.catchRuntimeError === "function" - ) { - window.addEventListener( - "error", - (event) => { - runtimeErrorCause = event.error; - }, - true, - ); - window.addEventListener( - "unhandledrejection", - (event) => { - runtimeErrorCause = event.reason; - }, - true, - ); - captureRuntimeErrorCause = true; - } - const sharedOverlay = configureOverlay({ trustedTypesPolicyName: options.trustedTypesPolicyName || "webpack-dev-server#overlay", openEditorEndpoint: "/webpack-dev-server/open-editor", paginate: true, - catchRuntimeError: (error) => { - const cause = runtimeErrorCause; - runtimeErrorCause = undefined; - return typeof options.catchRuntimeError === "function" - ? options.catchRuntimeError( - cause instanceof Error - ? error - : new Error(error.message, { cause }), - ) - : Boolean(options.catchRuntimeError); - }, + catchRuntimeError: options.catchRuntimeError || (() => false), }); /** @type {Document | null | undefined} */ diff --git a/test/client/ReactErrorBoundary.test.js b/test/client/ReactErrorBoundary.test.js index c91a49db5b..31dec37286 100644 --- a/test/client/ReactErrorBoundary.test.js +++ b/test/client/ReactErrorBoundary.test.js @@ -171,8 +171,9 @@ describe("createOverlay", () => { }); } - it("should preserve the cause of a rejected value for runtime error filters", () => { - const reason = { error: new Error("Rejected object") }; + it("should pass a rejected Error and its cause to runtime error filters", () => { + const cause = { error: new Error("Rejected object") }; + const reason = new Error("Rejected promise", { cause }); let received; createOverlay({ catchRuntimeError: (error) => { @@ -183,7 +184,8 @@ describe("createOverlay", () => { const event = new Event("unhandledrejection"); event.reason = reason; globalThis.dispatchEvent(event); - expect(received.cause).toBe(reason); + expect(received).toBe(reason); + expect(received.cause).toBe(cause); expect(document.querySelector(selector)).toBeNull(); }); diff --git a/test/e2e/overlay.test.js b/test/e2e/overlay.test.js index e9af6806e1..3eefa5c44c 100644 --- a/test/e2e/overlay.test.js +++ b/test/e2e/overlay.test.js @@ -1859,7 +1859,7 @@ describe("overlay", () => { } }); - it("should not show filtered promise rejection with specific error cause", async () => { + it("should not show filtered promise rejection with an explicit error cause", async () => { const compiler = webpack(config); const server = new Server( @@ -1887,7 +1887,9 @@ describe("overlay", () => { await page.addScriptTag({ content: `(function throwError() { setTimeout(function () { - Promise.reject({ error: new Error('Injected async error') }); + Promise.reject(new Error('Rejected promise', { + cause: { error: new Error('Injected async error') } + })); }, 0); })();`, });