123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- const RuntimeErrorFooter = require('./components/RuntimeErrorFooter');
- const RuntimeErrorHeader = require('./components/RuntimeErrorHeader');
- const CompileErrorContainer = require('./containers/CompileErrorContainer');
- const RuntimeErrorContainer = require('./containers/RuntimeErrorContainer');
- const theme = require('./theme');
- const debounce = require('./utils/debounce');
- const removeAllChildren = require('./utils/removeAllChildren');
- /**
- * @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 = '100vh';
- iframe.style.left = '0';
- 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 = '100vh';
- div.style.lineHeight = '1.3';
- div.style.overflow = 'auto';
- div.style.padding = '1rem 1.5rem 0';
- 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;
- }
- 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 = 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,
- });
|