340 lines
9.1 KiB
JavaScript
340 lines
9.1 KiB
JavaScript
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,
|
|
});
|