const RuntimeErrorFooter = require('./components/RuntimeErrorFooter.js'); const RuntimeErrorHeader = require('./components/RuntimeErrorHeader.js'); const CompileErrorContainer = require('./containers/CompileErrorContainer.js'); const RuntimeErrorContainer = require('./containers/RuntimeErrorContainer.js'); const theme = require('./theme.js'); const utils = require('./utils.js'); /** * @callback RenderFn * @returns {void} */ /* ===== Cached elements for DOM manipulations ===== */ /** * The iframe that contains the overlay. * @type {HTMLIFrameElement} */ let iframeRoot = null; /** * The document object from the iframe root, used to create and render elements. * @type {Document} */ let rootDocument = null; /** * The root div elements will attach to. * @type {HTMLDivElement} */ let root = null; /** * A Cached function to allow deferred render. * @type {RenderFn | null} */ let scheduledRenderFn = null; /* ===== Overlay State ===== */ /** * The latest error message from Webpack compilation. * @type {string} */ let currentCompileErrorMessage = ''; /** * Index of the error currently shown by the overlay. * @type {number} */ let currentRuntimeErrorIndex = 0; /** * The latest runtime error objects. * @type {Error[]} */ let currentRuntimeErrors = []; /** * The render mode the overlay is currently in. * @type {'compileError' | 'runtimeError' | null} */ let currentMode = null; /** * @typedef {Object} IframeProps * @property {function(): void} onIframeLoad */ /** * Creates the main `iframe` the overlay will attach to. * Accepts a callback to be ran after iframe is initialized. * @param {Document} document * @param {HTMLElement} root * @param {IframeProps} props * @returns {HTMLIFrameElement} */ function IframeRoot(document, root, props) { const iframe = document.createElement('iframe'); iframe.id = 'react-refresh-overlay'; iframe.src = 'about:blank'; iframe.style.border = 'none'; iframe.style.height = '100%'; iframe.style.left = '0'; iframe.style.minHeight = '100vh'; iframe.style.minHeight = '-webkit-fill-available'; iframe.style.position = 'fixed'; iframe.style.top = '0'; iframe.style.width = '100vw'; iframe.style.zIndex = '2147483647'; iframe.addEventListener('load', function onLoad() { // Reset margin of iframe body iframe.contentDocument.body.style.margin = '0'; props.onIframeLoad(); }); // We skip mounting and returns as we need to ensure // the load event is fired after we setup the global variable return iframe; } /** * Creates the main `div` element for the overlay to render. * @param {Document} document * @param {HTMLElement} root * @returns {HTMLDivElement} */ function OverlayRoot(document, root) { const div = document.createElement('div'); div.id = 'react-refresh-overlay-error'; // Style the contents container div.style.backgroundColor = '#' + theme.grey; div.style.boxSizing = 'border-box'; div.style.color = '#' + theme.white; div.style.fontFamily = [ '-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', '"Helvetica Neue"', 'Helvetica', 'Arial', 'sans-serif', '"Apple Color Emoji"', '"Segoe UI Emoji"', 'Segoe UI Symbol', ].join(', '); div.style.fontSize = '0.875rem'; div.style.height = '100%'; div.style.lineHeight = '1.3'; div.style.overflow = 'auto'; div.style.padding = '1rem 1.5rem 0'; div.style.paddingTop = 'max(1rem, env(safe-area-inset-top))'; div.style.paddingRight = 'max(1.5rem, env(safe-area-inset-right))'; div.style.paddingBottom = 'env(safe-area-inset-bottom)'; div.style.paddingLeft = 'max(1.5rem, env(safe-area-inset-left))'; div.style.width = '100vw'; root.appendChild(div); return div; } /** * Ensures the iframe root and the overlay root are both initialized before render. * If check fails, render will be deferred until both roots are initialized. * @param {RenderFn} renderFn A function that triggers a DOM render. * @returns {void} */ function ensureRootExists(renderFn) { if (root) { // Overlay root is ready, we can render right away. renderFn(); return; } // Creating an iframe may be asynchronous so we'll defer render. // In case of multiple calls, function from the last call will be used. scheduledRenderFn = renderFn; if (iframeRoot) { // Iframe is already ready, it will fire the load event. return; } // Create the iframe root, and, the overlay root inside it when it is ready. iframeRoot = IframeRoot(document, document.body, { onIframeLoad: function onIframeLoad() { rootDocument = iframeRoot.contentDocument; root = OverlayRoot(rootDocument, rootDocument.body); scheduledRenderFn(); }, }); // We have to mount here to ensure `iframeRoot` is set when `onIframeLoad` fires. // This is because onIframeLoad() will be called synchronously // or asynchronously depending on the browser. document.body.appendChild(iframeRoot); } /** * Creates the main `div` element for the overlay to render. * @returns {void} */ function render() { ensureRootExists(function () { const currentFocus = rootDocument.activeElement; let currentFocusId; if (currentFocus.localName === 'button' && currentFocus.id) { currentFocusId = currentFocus.id; } utils.removeAllChildren(root); if (currentCompileErrorMessage) { currentMode = 'compileError'; CompileErrorContainer(rootDocument, root, { errorMessage: currentCompileErrorMessage, }); } else if (currentRuntimeErrors.length) { currentMode = 'runtimeError'; RuntimeErrorHeader(rootDocument, root, { currentErrorIndex: currentRuntimeErrorIndex, totalErrors: currentRuntimeErrors.length, }); RuntimeErrorContainer(rootDocument, root, { currentError: currentRuntimeErrors[currentRuntimeErrorIndex], }); RuntimeErrorFooter(rootDocument, root, { initialFocus: currentFocusId, multiple: currentRuntimeErrors.length > 1, onClickCloseButton: function onClose() { clearRuntimeErrors(); }, onClickNextButton: function onNext() { if (currentRuntimeErrorIndex === currentRuntimeErrors.length - 1) { return; } currentRuntimeErrorIndex += 1; ensureRootExists(render); }, onClickPrevButton: function onPrev() { if (currentRuntimeErrorIndex === 0) { return; } currentRuntimeErrorIndex -= 1; ensureRootExists(render); }, }); } }); } /** * Destroys the state of the overlay. * @returns {void} */ function cleanup() { // Clean up and reset all internal state. document.body.removeChild(iframeRoot); scheduledRenderFn = null; root = null; iframeRoot = null; } /** * Clears Webpack compilation errors and dismisses the compile error overlay. * @returns {void} */ function clearCompileError() { if (!root || currentMode !== 'compileError') { return; } currentCompileErrorMessage = ''; currentMode = null; cleanup(); } /** * Clears runtime error records and dismisses the runtime error overlay. * @param {boolean} [dismissOverlay] Whether to dismiss the overlay or not. * @returns {void} */ function clearRuntimeErrors(dismissOverlay) { if (!root || currentMode !== 'runtimeError') { return; } currentRuntimeErrorIndex = 0; currentRuntimeErrors = []; if (typeof dismissOverlay === 'undefined' || dismissOverlay) { currentMode = null; cleanup(); } } /** * Shows the compile error overlay with the specific Webpack error message. * @param {string} message * @returns {void} */ function showCompileError(message) { if (!message) { return; } currentCompileErrorMessage = message; render(); } /** * Shows the runtime error overlay with the specific error records. * @param {Error[]} errors * @returns {void} */ function showRuntimeErrors(errors) { if (!errors || !errors.length) { return; } currentRuntimeErrors = errors; render(); } /** * The debounced version of `showRuntimeErrors` to prevent frequent renders * due to rapid firing listeners. * @param {Error[]} errors * @returns {void} */ const debouncedShowRuntimeErrors = utils.debounce(showRuntimeErrors, 30); /** * Detects if an error is a Webpack compilation error. * @param {Error} error The error of interest. * @returns {boolean} If the error is a Webpack compilation error. */ function isWebpackCompileError(error) { return /Module [A-z ]+\(from/.test(error.message) || /Cannot find module/.test(error.message); } /** * Handles runtime error contexts captured with EventListeners. * Integrates with a runtime error overlay. * @param {Error} error A valid error object. * @returns {void} */ function handleRuntimeError(error) { if (error && !isWebpackCompileError(error) && currentRuntimeErrors.indexOf(error) === -1) { currentRuntimeErrors = currentRuntimeErrors.concat(error); } debouncedShowRuntimeErrors(currentRuntimeErrors); } module.exports = Object.freeze({ clearCompileError: clearCompileError, clearRuntimeErrors: clearRuntimeErrors, handleRuntimeError: handleRuntimeError, showCompileError: showCompileError, showRuntimeErrors: showRuntimeErrors, });