You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
339 lines
9.1 KiB
339 lines
9.1 KiB
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, |
|
});
|
|
|