Spaces:
Sleeping
Sleeping
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, | |
}); | |