webpack-dev-middleware
Version:
A development middleware for webpack
776 lines (721 loc) • 29.6 kB
JavaScript
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
import ansiHTML from "ansi-html-community";
import theme from "./theme.js";
// eslint-disable-next-line jsdoc/reject-any-type
/** @typedef {any} EXPECTED_ANY */
/** @type {Record<string, string>} */
var characterReferences = {
"<": "<",
">": ">",
'"': """,
"'": "'",
"&": "&"
};
/**
* Encode the characters that are meaningful in HTML. Inlined (same as
* webpack-dev-server's overlay) so the client does not need `html-entities`.
* @param {string} text raw text
* @returns {string} entity-encoded text
*/
function encodeHtmlEntity(text) {
if (!text) {
return "";
}
return text.replace(/[<>'"&]/g, function (character) {
return characterReferences[character];
});
}
var OVERLAY_ID = "webpack-dev-middleware-hot-overlay";
var CARD_ID = "".concat(OVERLAY_ID, "-card");
// The overlay lives inside an `about:blank` iframe (same pattern as
// webpack-dev-server) so page styles cannot leak into it and its styles cannot
// leak out. Every style is applied through the CSSOM (`element.style`), which
// a strict `style-src` Content Security Policy allows, unlike inline `style`
// attributes.
/**
* The iframe acts as the backdrop: it covers the viewport and dims the page.
* @type {Record<string, string | number>}
*/
var backdropStyles = {
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
width: "100vw",
height: "100vh",
border: "none",
zIndex: 9999,
background: theme.backdrop
};
/** @type {Record<string, string | number>} */
var styles = {
// Dark panel; the top accent bar color is set per problem type in showProblems.
position: "relative",
background: theme.panel,
color: theme.text,
lineHeight: "1.6",
whiteSpace: "pre-wrap",
fontFamily: "Menlo, Consolas, 'Courier New', monospace",
fontSize: "14px",
width: "100%",
maxWidth: "960px",
maxHeight: "90vh",
margin: "auto",
padding: "28px 32px",
boxSizing: "border-box",
borderRadius: "8px",
borderTop: "3px solid #ff3348",
boxShadow: "0 8px 40px rgba(0,0,0,0.5)",
overflow: "auto",
direction: "ltr",
textAlign: "left"
};
/** @type {Record<string, string | number>} */
var bodyStyles = {
margin: 0,
padding: "32px",
boxSizing: "border-box",
minHeight: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "auto",
background: "transparent"
};
/** @type {Record<string, string | number>} */
var closeButtonStyles = {
position: "absolute",
top: "8px",
right: "12px",
border: "none",
background: "transparent",
color: theme.muted,
fontSize: "22px",
lineHeight: "1",
cursor: "pointer",
padding: "0"
};
/** @type {Record<string, string | string[]>} */
var colors = {
reset: ["transparent", "transparent"],
black: "181818",
red: "ff3348",
green: "3fff4f",
yellow: "ffd30e",
blue: "169be0",
magenta: "f840b7",
cyan: "0ad8e9",
lightgrey: "ebe7e3",
darkgrey: "6d7891"
};
/**
* @typedef {object} OverlayState
* @property {HTMLIFrameElement | null} frame overlay iframe
* @property {HTMLElement | null} card visible panel inside the iframe
* @property {boolean} runtimeListenersAttached whether the window listeners are attached
* @property {boolean} hostKeydownAttached whether the host document's Escape listener is attached
* @property {number} pageIndex page shown when paginating
* @property {Record<string, { type: "errors" | "warnings", lines: string[] }>} problemsBySource each reporting source's problems
* @property {{ type: "errors" | "warnings", lines: string[] } | null} currentProblems union of every source, as displayed
* @property {{ createHTML: (value: string) => EXPECTED_ANY } | undefined} trustedTypesPolicy trusted types policy
* @property {boolean | ((error: Error) => boolean)} catchRuntimeError whether (or which) runtime errors are shown — shared, so the copy that attached the window listeners honors every copy's configuration
*/
/** @returns {OverlayState} fresh overlay state */
function createOverlayState() {
return {
frame: null,
card: null,
runtimeListenersAttached: false,
hostKeydownAttached: false,
pageIndex: 0,
problemsBySource: {},
currentProblems: null,
trustedTypesPolicy: undefined,
catchRuntimeError: false
};
}
// The DOM/problem state is a per-page singleton shared through `window`, so
// every bundled copy of this module (e.g. the webpack-dev-server client and
// the webpack-dev-middleware client on the same page) renders into a single
// overlay instead of stacking duplicate iframes.
var OVERLAY_STATE_KEY = "__webpack_dev_middleware_hot_overlay_state__";
/** @type {OverlayState} */
var state = function () {
// The browser suite cannot produce a document-less environment; this is the
// guard for a server-side import of the bundle.
/* istanbul ignore next -- @preserve */
if (typeof window === "undefined") {
return createOverlayState();
}
var holder = /** @type {EXPECTED_ANY} */window;
if (!holder[OVERLAY_STATE_KEY]) {
holder[OVERLAY_STATE_KEY] = createOverlayState();
} else {
// A copy from another package version may have created the state with
// fewer fields — fill the gaps in place (replacing the object would
// orphan the references other copies already hold).
var defaults = createOverlayState();
for (var _i = 0, _Object$keys = Object.keys(defaults); _i < _Object$keys.length; _i++) {
var key = _Object$keys[_i];
if (!(key in holder[OVERLAY_STATE_KEY])) {
holder[OVERLAY_STATE_KEY][key] = defaults[(/** @type {keyof OverlayState} */key)];
}
}
}
return holder[OVERLAY_STATE_KEY];
}();
// Pagination (wdm extension, enabled by default): the card shows one problem
// at a time with prev/next navigation.
var paginate = true;
// When set, the file chips in error messages become clickable and issue
// `GET <endpoint>?fileName=<file:line:column>`. The endpoint itself is
// provided by the server integration (e.g. a route that calls launch-editor,
// like webpack-dev-server's `/webpack-dev-server/open-editor`).
/** @type {string} */
var openEditorEndpoint = "";
// Trusted Types support (same pattern as webpack-dev-server): when the page
// runs under `require-trusted-types-for 'script'`, every `innerHTML` write
// must go through a policy. The policy itself lives in the shared state —
// creating two policies with the same name throws under an enforced CSP.
/** @type {string | undefined} */
var trustedTypesPolicyName;
/**
* @param {HTMLElement} element element
* @param {string} html html to assign
*/
function setHTML(element, html) {
element.innerHTML = state.trustedTypesPolicy ? state.trustedTypesPolicy.createHTML(html) : html;
}
/**
* @param {EXPECTED_ANY} element element
* @param {Record<string, string | number>} style style map
*/
function applyStyle(element, style) {
for (var _i2 = 0, _Object$keys2 = Object.keys(style); _i2 < _Object$keys2.length; _i2++) {
var key = _Object$keys2[_i2];
element.style[key] = style[key];
}
}
/**
* Re-apply the inline `style` attributes produced by `ansi-html` (and our own
* highlight helpers) through the CSSOM. Under a strict `style-src` CSP the
* parser ignores `style` attributes, but CSSOM writes are always allowed.
* @param {HTMLElement} root subtree to normalize
*/
function normalizeInlineStyles(root) {
// Indexed rather than `for...of`: a `NodeList` is not iterable in an ES5
// browser, and the loop babel compiles it into throws on one.
var elements = root.querySelectorAll("[style]");
for (var index = 0; index < elements.length; index++) {
var element = elements[index];
/** @type {EXPECTED_ANY} */
element.style.cssText = element.getAttribute("style");
}
}
/**
* @param {"errors" | "warnings"} type problem type
* @returns {string | string[]} hex color (without `#`) for the given type
*/
function problemColor(type) {
/** @type {Record<string, string | string[]>} */
var problemColors = {
errors: colors.red,
warnings: colors.yellow
};
return problemColors[type] || colors.red;
}
/**
* @param {"errors" | "warnings"} type problem type
* @returns {string} HTML span with a colored badge
*/
function problemType(type) {
var color = problemColor(type);
return "<span style=\"background-color:#".concat(color, "; color:#000000; ") + 'padding:3px 6px; border-radius: 4px;">' + "".concat(type.slice(0, -1).toUpperCase(), "</span>");
}
/**
* Highlight the offending line of a code frame — the one webpack marks with a
* leading `>` gutter — so it stands out from the surrounding context lines.
* @param {string} html message HTML (already entity-encoded, so `>` is `>`)
* @returns {string} HTML with the error line wrapped in a colored span
*/
function highlightCodeFrame(html) {
return html.split("\n").map(function (line) {
return /^\s*>/.test(line) ? '<span style="display:inline-block; width:100%; margin:6px 0; ' + 'color:#ff6b6b; background-color:rgba(255,107,107,0.12);">' + "".concat(line, "</span>") : line;
}).join("\n");
}
/**
* Highlight the file references webpack reports. The header reference (the one
* with a `line:col` location, e.g. `./src/render.js 7:2`) is rendered as a file
* chip; bare paths elsewhere are just underlined.
* @param {string} html message HTML
* @returns {string} HTML with file references styled
*/
function highlightFilePath(html) {
return html.replace(/(\.{1,2}\/[\w./-]+\.\w+)(:\d+:\d+|\s\d+:\d+)?/g, function (match, filePath, location) {
if (!location) {
return "<span style=\"color:".concat(theme.accent, "; text-decoration:underline; ") + "text-underline-offset:2px;\">".concat(match, "</span>");
}
if (openEditorEndpoint) {
var position = location.trim().replace(/^:/, "");
return "<span style=\"color:".concat(theme.accent, "; cursor:pointer; ") + 'text-decoration:underline; text-underline-offset:2px;" ' + "data-open-file=\"".concat(filePath, ":").concat(position, "\" ") + 'title="Click to open in your editor">' + "".concat(filePath, "</span>").concat(location, "\n");
}
return "<span style=\"color:".concat(theme.accent, ";\">").concat(filePath, "</span>").concat(location, "\n");
});
}
/**
* Turn bare `http(s)` URLs in the message into clickable links.
* @param {string} html message HTML
* @returns {string} HTML with URLs wrapped in anchor tags
*/
function linkify(html) {
return html.replace(/https?:\/\/[^\s<>"]+/g, function (url) {
// Keep trailing punctuation (e.g. a sentence-ending dot) out of the href.
var trailing = url.match(/[.,;:!?)\]}]+$/);
var cut = trailing ? trailing[0] : "";
var href = url.slice(0, url.length - cut.length);
return "<a href=\"".concat(href, "\" target=\"_blank\" rel=\"noopener noreferrer\" ") + "style=\"color:".concat(theme.accent, ";\">").concat(href, "</a>").concat(cut);
});
}
/**
* Compute the union of every source's problems. Errors from any source take
* precedence over warnings, mirroring the reporter's per-bundle behavior.
* @returns {{ type: "errors" | "warnings", lines: string[] } | null} union
*/
function computeProblemsUnion() {
/** @type {{ type: "errors" | "warnings", lines: string[] }[]} */
var slots = [];
for (var _i3 = 0, _Object$keys3 = Object.keys(state.problemsBySource); _i3 < _Object$keys3.length; _i3++) {
var source = _Object$keys3[_i3];
slots.push(state.problemsBySource[source]);
}
if (slots.length === 0) {
return null;
}
/** @type {string[]} */
var errorLines = [];
/** @type {string[]} */
var allLines = [];
for (var _i4 = 0, _slots = slots; _i4 < _slots.length; _i4++) {
var slot = _slots[_i4];
if (slot.type === "errors") {
errorLines.push.apply(errorLines, _toConsumableArray(slot.lines));
}
allLines.push.apply(allLines, _toConsumableArray(slot.lines));
}
return errorLines.length > 0 ? {
type: "errors",
lines: errorLines
} : {
type: "warnings",
lines: allLines
};
}
/**
* Re-render the card. Late-bound to `renderProblems` below: the listeners
* `ensureOverlay` attaches re-render, and rendering needs the card
* `ensureOverlay` creates, so one of the two directions cannot be a direct
* call.
* @type {() => void}
*/
var render = function render() {};
/**
* Clamp the page index and re-render.
* @param {number} index requested page index
*/
function goToPage(index) {
// Paging is only reachable from a rendered overlay, which implies problems.
/* istanbul ignore next -- @preserve */
if (!state.currentProblems) {
return;
}
state.pageIndex = Math.min(state.currentProblems.lines.length - 1, Math.max(0, index));
render();
}
/**
* Remove one source's problems, or the whole overlay.
* @param {string=} source when given, only that source's problems are
* dropped and the overlay re-renders the remaining union; without it the
* overlay is dismissed entirely (Escape, backdrop, close button)
*/
export function clear(source) {
if (source !== undefined) {
// The reporter clears its slots on every clean build — when the source
// never reported anything there is nothing to drop, and re-rendering
// would needlessly rebuild the card another client is showing.
if (!Object.prototype.hasOwnProperty.call(state.problemsBySource, source)) {
return;
}
delete state.problemsBySource[source];
var union = computeProblemsUnion();
state.currentProblems = union;
if (union) {
state.pageIndex = Math.min(state.pageIndex, union.lines.length - 1);
render();
return;
}
}
if (state.frame && state.frame.parentNode) {
/** @type {ParentNode & Node} */
state.frame.parentNode.removeChild(state.frame);
}
state.frame = null;
state.card = null;
state.problemsBySource = {};
state.currentProblems = null;
state.pageIndex = 0;
}
/**
* Create (or return) the overlay iframe and the card inside it.
* @returns {HTMLElement | null} the card element, or null when the frame
* document is not available
*/
function ensureOverlay() {
if (state.frame && state.card && state.frame.parentNode) {
return state.card;
}
// Only reachable from a script running before <body> exists.
/* istanbul ignore next -- @preserve */
if (!document.body) {
return null;
}
// Dismiss the overlay when pressing Escape while the host page has focus —
// the frame's own keydown listener only fires when the frame is focused.
// Attached lazily on the first render (and once per page, the flag lives in
// the shared state) so importing the module stays free of DOM side effects.
if (!state.hostKeydownAttached) {
state.hostKeydownAttached = true;
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
clear();
}
});
}
// Enable Trusted Types if they are available in the current browser.
if (window.trustedTypes && !state.trustedTypesPolicy) {
state.trustedTypesPolicy = window.trustedTypes.createPolicy(trustedTypesPolicyName || "webpack-dev-middleware#overlay", {
createHTML: function createHTML(value) {
return value;
}
});
}
state.frame = document.createElement("iframe");
state.frame.id = OVERLAY_ID;
state.frame.src = "about:blank";
applyStyle(state.frame, backdropStyles);
document.body.appendChild(state.frame);
// A same-origin `about:blank` document is available synchronously.
var frameDocument = state.frame.contentDocument;
// A same-origin about:blank frame always has a document by this point.
/* istanbul ignore next -- @preserve */
if (!frameDocument || !frameDocument.body) {
document.body.removeChild(state.frame);
state.frame = null;
return null;
}
applyStyle(frameDocument.body, bodyStyles);
// Dismiss the overlay when pressing Escape while the frame has focus;
// navigate between problems with the arrow keys when paginating.
frameDocument.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
clear();
} else if (paginate && event.key === "ArrowLeft") {
goToPage(state.pageIndex - 1);
} else if (paginate && event.key === "ArrowRight") {
goToPage(state.pageIndex + 1);
}
});
// Dismiss the overlay when clicking the backdrop (but not the card itself).
frameDocument.addEventListener("click", function (event) {
var target = /** @type {EXPECTED_ANY} */event.target;
// Elements re-rendered away mid-dispatch (e.g. the pagination buttons)
// are no longer inside the card — do not treat them as backdrop clicks.
if (target && target.isConnected === false) {
return;
}
if (state.card && !state.card.contains(target)) {
clear();
}
});
// Open the clicked file reference through the configured endpoint.
frameDocument.addEventListener("click", function (event) {
var target = /** @type {EXPECTED_ANY} */event.target;
var opener = target && typeof target.closest === "function" ? target.closest("[data-open-file]") : null;
if (opener && openEditorEndpoint && typeof fetch === "function") {
fetch("".concat(openEditorEndpoint, "?fileName=").concat(encodeURIComponent(opener.getAttribute("data-open-file"))));
}
});
// The card is the visible panel that holds the problem messages.
state.card = frameDocument.createElement("div");
state.card.id = CARD_ID;
applyStyle(state.card, styles);
frameDocument.body.appendChild(state.card);
return state.card;
}
/**
* Render the current problem set into the card.
*/
function renderProblems() {
// Both guards cover callers that cannot occur in a browser: rendering is
// driven by a problem set, and `ensureOverlay` only fails without a body.
/* istanbul ignore next -- @preserve */
if (!state.currentProblems) {
return;
}
var card = ensureOverlay();
/* istanbul ignore next -- @preserve */
if (!card) {
return;
}
var frameDocument = /** @type {Document} */
/** @type {HTMLIFrameElement} */state.frame.contentDocument;
var _state$currentProblem = state.currentProblems,
type = _state$currentProblem.type,
lines = _state$currentProblem.lines;
var paginated = paginate && lines.length > 1;
// Accent the top bar with the problem color (red for errors, yellow for warnings).
card.style.borderTopColor = "#".concat(problemColor(type));
setHTML(card, "");
// A close (×) button pinned to the top-right corner of the card.
var closeButton = frameDocument.createElement("button");
closeButton.type = "button";
closeButton.textContent = "×";
closeButton.setAttribute("aria-label", "Close");
applyStyle(closeButton, closeButtonStyles);
closeButton.addEventListener("click", function () {
clear();
});
card.appendChild(closeButton);
var visible = paginated ? [lines[state.pageIndex]] : lines;
if (paginated) {
// Header row: badge and the problem's first line (usually the file
// reference) on the left, page navigation on the right (leaving room for
// the absolute-positioned close button).
var header = frameDocument.createElement("div");
applyStyle(header, {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "12px",
marginBottom: "16px",
paddingRight: "28px"
});
var line = /** @type {string} */visible[0];
var newlineIndex = line.indexOf("\n");
var title = newlineIndex === -1 ? line : line.slice(0, newlineIndex);
var badge = frameDocument.createElement("span");
applyStyle(badge, {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
});
setHTML(badge, "".concat(problemType(type), " in ").concat(linkify(highlightFilePath(ansiHTML(encodeHtmlEntity(title))))));
normalizeInlineStyles(badge);
var nav = frameDocument.createElement("div");
applyStyle(nav, {
display: "flex",
alignItems: "center",
gap: "12px",
fontSize: "13px",
color: "#999999"
});
/**
* @param {string} text button text
* @param {number} delta page delta
* @param {string} ariaLabel accessible label
* @returns {HTMLButtonElement} nav button
*/
var makeNavButton = function makeNavButton(text, delta, ariaLabel) {
var button = frameDocument.createElement("button");
button.type = "button";
button.textContent = text;
button.setAttribute("aria-label", ariaLabel);
applyStyle(button, {
border: "none",
background: "transparent",
// Follow the problem color (red for errors, yellow for warnings).
color: "#".concat(problemColor(type)),
cursor: "pointer",
fontSize: "16px",
lineHeight: "1",
padding: "0"
});
button.addEventListener("click", function () {
goToPage(state.pageIndex + delta);
});
return button;
};
var counter = frameDocument.createElement("span");
counter.textContent = "".concat(state.pageIndex + 1, " / ").concat(lines.length);
applyStyle(counter, {
color: "#f2f2f2"
});
nav.appendChild(makeNavButton("‹", -1, "Previous problem"));
nav.appendChild(counter);
nav.appendChild(makeNavButton("›", 1, "Next problem"));
header.appendChild(badge);
header.appendChild(nav);
card.appendChild(header);
}
var _iterator = _createForOfIteratorHelper(visible),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var _line = _step.value;
// When paginating, the first line (badge + file reference) already lives
// in the header; render only the remainder.
var _newlineIndex = _line.indexOf("\n");
var body = paginated ? _newlineIndex === -1 ? "" : _line.slice(_newlineIndex + 1) : _line;
if (paginated && !body) {
continue;
}
var msg = linkify(highlightFilePath(highlightCodeFrame(ansiHTML(encodeHtmlEntity(body)))));
var div = frameDocument.createElement("div");
div.style.marginBottom = "20px";
setHTML(div, paginated ? msg : "".concat(problemType(type), " in ").concat(msg));
normalizeInlineStyles(div);
card.appendChild(div);
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
var hint = frameDocument.createElement("div");
applyStyle(hint, {
marginTop: "4px",
paddingTop: "16px",
borderTop: "1px solid #465e69",
color: theme.muted,
fontSize: "13px"
});
hint.textContent = paginated ? "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.";
card.appendChild(hint);
}
render = renderProblems;
/**
* @param {"errors" | "warnings"} type problem type
* @param {string[]} lines messages to render
* @param {string=} source who reports them — each source (e.g. this client,
* the webpack-dev-server client, the runtime error capture) keeps its own
* slot and the overlay renders the union of every slot
*/
export function showProblems(type, lines) {
var source = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "";
state.problemsBySource[source] = {
type: type,
lines: _toConsumableArray(lines)
};
var union = /** @type {{ type: "errors" | "warnings", lines: string[] }} */
computeProblemsUnion();
// Re-publishing an identical set (e.g. another bundle of a multi-compiler
// synced) must not reset the page the user is reading.
var unchanged = state.currentProblems !== null && state.frame !== null &&
// The frame may have been detached without `clear()` (e.g. a framework
// wiping `document.body`) — an identical set must still re-mount it.
state.frame.parentNode !== null && state.currentProblems.type === union.type && state.currentProblems.lines.length === union.lines.length && state.currentProblems.lines.every(function (line, index) {
return line === union.lines[index];
});
state.currentProblems = union;
if (unchanged) {
return;
}
// Each new problem set starts at its first page.
state.pageIndex = 0;
renderProblems();
}
/**
* @param {EXPECTED_ANY} error thrown value
* @param {string} fallbackMessage fallback message
*/
function handleRuntimeError(error, fallbackMessage) {
// If the error stack indicates a React error boundary caught the error, do
// not show the overlay (same heuristic as webpack-dev-server).
if (error && error.stack && error.stack.indexOf("invokeGuardedCallbackDev") !== -1) {
return;
}
var errorObject = error instanceof Error ? error : new Error(error || fallbackMessage);
// `catchRuntimeError` may be a filter function, like in webpack-dev-server.
var shouldDisplay = typeof state.catchRuntimeError === "function" ? state.catchRuntimeError(errorObject) : true;
if (!shouldDisplay) {
return;
}
var stack = errorObject.stack ? "\n".concat(errorObject.stack) : "";
var message = "Uncaught runtime error: ".concat(errorObject.message).concat(stack);
// Runtime errors accumulate in their own slot, so they coexist with build
// problems and clearing the slot resets the accumulation.
var runtimeSlot = state.problemsBySource.runtime;
var messages = runtimeSlot ? [].concat(_toConsumableArray(runtimeSlot.lines), [message]) : [message];
showProblems("errors", messages, "runtime");
// When paginating, land on the newest runtime error.
if (state.currentProblems) {
goToPage(state.currentProblems.lines.lastIndexOf(message));
}
}
/**
* Listen for uncaught errors and unhandled rejections on the page.
*/
function attachRuntimeErrorListeners() {
if (state.runtimeListenersAttached) {
return;
}
state.runtimeListenersAttached = true;
window.addEventListener("error", function (event) {
if (!event.error && !event.message) {
return;
}
handleRuntimeError(event.error, event.message);
});
window.addEventListener("unhandledrejection", function (event) {
handleRuntimeError(event.reason, "Unknown promise rejection reason");
});
}
/**
* @param {{ ansiColors?: Record<string, string | string[]>, overlayStyles?: Record<string, string | number>, trustedTypesPolicyName?: string, catchRuntimeError?: boolean | ((error: Error) => boolean), openEditorEndpoint?: string, paginate?: boolean }} options options
* @returns {{ showProblems: typeof showProblems, clear: typeof clear }} overlay api
*/
export default function configureOverlay(options) {
if (options.trustedTypesPolicyName) {
trustedTypesPolicyName = options.trustedTypesPolicyName;
}
if (options.openEditorEndpoint !== undefined) {
openEditorEndpoint = options.openEditorEndpoint;
}
if (options.paginate !== undefined) {
paginate = Boolean(options.paginate);
}
if (options.catchRuntimeError !== undefined) {
state.catchRuntimeError = options.catchRuntimeError;
}
if (state.catchRuntimeError) {
attachRuntimeErrorListeners();
}
if (options.ansiColors) {
for (var _i5 = 0, _Object$keys4 = Object.keys(options.ansiColors); _i5 < _Object$keys4.length; _i5++) {
var color = _Object$keys4[_i5];
if (color in colors) {
colors[color] = options.ansiColors[color];
}
}
ansiHTML.setColors(colors);
}
if (options.overlayStyles) {
for (var _i6 = 0, _Object$keys5 = Object.keys(options.overlayStyles); _i6 < _Object$keys5.length; _i6++) {
var style = _Object$keys5[_i6];
styles[style] = options.overlayStyles[style];
}
}
if (state.card) {
applyStyle(state.card, styles);
}
return {
showProblems: showProblems,
clear: clear
};
}