index.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. const RuntimeErrorFooter = require('./components/RuntimeErrorFooter');
  2. const RuntimeErrorHeader = require('./components/RuntimeErrorHeader');
  3. const CompileErrorContainer = require('./containers/CompileErrorContainer');
  4. const RuntimeErrorContainer = require('./containers/RuntimeErrorContainer');
  5. const theme = require('./theme');
  6. const debounce = require('./utils/debounce');
  7. const removeAllChildren = require('./utils/removeAllChildren');
  8. /**
  9. * @callback RenderFn
  10. * @returns {void}
  11. */
  12. /* ===== Cached elements for DOM manipulations ===== */
  13. /**
  14. * The iframe that contains the overlay.
  15. * @type {HTMLIFrameElement}
  16. */
  17. let iframeRoot = null;
  18. /**
  19. * The document object from the iframe root, used to create and render elements.
  20. * @type {Document}
  21. */
  22. let rootDocument = null;
  23. /**
  24. * The root div elements will attach to.
  25. * @type {HTMLDivElement}
  26. */
  27. let root = null;
  28. /**
  29. * A Cached function to allow deferred render.
  30. * @type {RenderFn | null}
  31. */
  32. let scheduledRenderFn = null;
  33. /* ===== Overlay State ===== */
  34. /**
  35. * The latest error message from Webpack compilation.
  36. * @type {string}
  37. */
  38. let currentCompileErrorMessage = '';
  39. /**
  40. * Index of the error currently shown by the overlay.
  41. * @type {number}
  42. */
  43. let currentRuntimeErrorIndex = 0;
  44. /**
  45. * The latest runtime error objects.
  46. * @type {Error[]}
  47. */
  48. let currentRuntimeErrors = [];
  49. /**
  50. * The render mode the overlay is currently in.
  51. * @type {'compileError' | 'runtimeError' | null}
  52. */
  53. let currentMode = null;
  54. /**
  55. * @typedef {Object} IframeProps
  56. * @property {function(): void} onIframeLoad
  57. */
  58. /**
  59. * Creates the main `iframe` the overlay will attach to.
  60. * Accepts a callback to be ran after iframe is initialized.
  61. * @param {Document} document
  62. * @param {HTMLElement} root
  63. * @param {IframeProps} props
  64. * @returns {HTMLIFrameElement}
  65. */
  66. function IframeRoot(document, root, props) {
  67. const iframe = document.createElement('iframe');
  68. iframe.id = 'react-refresh-overlay';
  69. iframe.src = 'about:blank';
  70. iframe.style.border = 'none';
  71. iframe.style.height = '100vh';
  72. iframe.style.left = '0';
  73. iframe.style.position = 'fixed';
  74. iframe.style.top = '0';
  75. iframe.style.width = '100vw';
  76. iframe.style.zIndex = '2147483647';
  77. iframe.addEventListener('load', function onLoad() {
  78. // Reset margin of iframe body
  79. iframe.contentDocument.body.style.margin = '0';
  80. props.onIframeLoad();
  81. });
  82. // We skip mounting and returns as we need to ensure
  83. // the load event is fired after we setup the global variable
  84. return iframe;
  85. }
  86. /**
  87. * Creates the main `div` element for the overlay to render.
  88. * @param {Document} document
  89. * @param {HTMLElement} root
  90. * @returns {HTMLDivElement}
  91. */
  92. function OverlayRoot(document, root) {
  93. const div = document.createElement('div');
  94. div.id = 'react-refresh-overlay-error';
  95. // Style the contents container
  96. div.style.backgroundColor = '#' + theme.grey;
  97. div.style.boxSizing = 'border-box';
  98. div.style.color = '#' + theme.white;
  99. div.style.fontFamily = [
  100. '-apple-system',
  101. 'BlinkMacSystemFont',
  102. '"Segoe UI"',
  103. '"Helvetica Neue"',
  104. 'Helvetica',
  105. 'Arial',
  106. 'sans-serif',
  107. '"Apple Color Emoji"',
  108. '"Segoe UI Emoji"',
  109. 'Segoe UI Symbol',
  110. ].join(', ');
  111. div.style.fontSize = '0.875rem';
  112. div.style.height = '100vh';
  113. div.style.lineHeight = '1.3';
  114. div.style.overflow = 'auto';
  115. div.style.padding = '1rem 1.5rem 0';
  116. div.style.width = '100vw';
  117. root.appendChild(div);
  118. return div;
  119. }
  120. /**
  121. * Ensures the iframe root and the overlay root are both initialized before render.
  122. * If check fails, render will be deferred until both roots are initialized.
  123. * @param {RenderFn} renderFn A function that triggers a DOM render.
  124. * @returns {void}
  125. */
  126. function ensureRootExists(renderFn) {
  127. if (root) {
  128. // Overlay root is ready, we can render right away.
  129. renderFn();
  130. return;
  131. }
  132. // Creating an iframe may be asynchronous so we'll defer render.
  133. // In case of multiple calls, function from the last call will be used.
  134. scheduledRenderFn = renderFn;
  135. if (iframeRoot) {
  136. // Iframe is already ready, it will fire the load event.
  137. return;
  138. }
  139. // Create the iframe root, and, the overlay root inside it when it is ready.
  140. iframeRoot = IframeRoot(document, document.body, {
  141. onIframeLoad: function onIframeLoad() {
  142. rootDocument = iframeRoot.contentDocument;
  143. root = OverlayRoot(rootDocument, rootDocument.body);
  144. scheduledRenderFn();
  145. },
  146. });
  147. // We have to mount here to ensure `iframeRoot` is set when `onIframeLoad` fires.
  148. // This is because onIframeLoad() will be called synchronously
  149. // or asynchronously depending on the browser.
  150. document.body.appendChild(iframeRoot);
  151. }
  152. /**
  153. * Creates the main `div` element for the overlay to render.
  154. * @returns {void}
  155. */
  156. function render() {
  157. ensureRootExists(function () {
  158. const currentFocus = rootDocument.activeElement;
  159. let currentFocusId;
  160. if (currentFocus.localName === 'button' && currentFocus.id) {
  161. currentFocusId = currentFocus.id;
  162. }
  163. removeAllChildren(root);
  164. if (currentCompileErrorMessage) {
  165. currentMode = 'compileError';
  166. CompileErrorContainer(rootDocument, root, {
  167. errorMessage: currentCompileErrorMessage,
  168. });
  169. } else if (currentRuntimeErrors.length) {
  170. currentMode = 'runtimeError';
  171. RuntimeErrorHeader(rootDocument, root, {
  172. currentErrorIndex: currentRuntimeErrorIndex,
  173. totalErrors: currentRuntimeErrors.length,
  174. });
  175. RuntimeErrorContainer(rootDocument, root, {
  176. currentError: currentRuntimeErrors[currentRuntimeErrorIndex],
  177. });
  178. RuntimeErrorFooter(rootDocument, root, {
  179. initialFocus: currentFocusId,
  180. multiple: currentRuntimeErrors.length > 1,
  181. onClickCloseButton: function onClose() {
  182. clearRuntimeErrors();
  183. },
  184. onClickNextButton: function onNext() {
  185. if (currentRuntimeErrorIndex === currentRuntimeErrors.length - 1) {
  186. return;
  187. }
  188. currentRuntimeErrorIndex += 1;
  189. ensureRootExists(render);
  190. },
  191. onClickPrevButton: function onPrev() {
  192. if (currentRuntimeErrorIndex === 0) {
  193. return;
  194. }
  195. currentRuntimeErrorIndex -= 1;
  196. ensureRootExists(render);
  197. },
  198. });
  199. }
  200. });
  201. }
  202. /**
  203. * Destroys the state of the overlay.
  204. * @returns {void}
  205. */
  206. function cleanup() {
  207. // Clean up and reset all internal state.
  208. document.body.removeChild(iframeRoot);
  209. scheduledRenderFn = null;
  210. root = null;
  211. iframeRoot = null;
  212. }
  213. /**
  214. * Clears Webpack compilation errors and dismisses the compile error overlay.
  215. * @returns {void}
  216. */
  217. function clearCompileError() {
  218. if (!root || currentMode !== 'compileError') {
  219. return;
  220. }
  221. currentCompileErrorMessage = '';
  222. currentMode = null;
  223. cleanup();
  224. }
  225. /**
  226. * Clears runtime error records and dismisses the runtime error overlay.
  227. * @param {boolean} [dismissOverlay] Whether to dismiss the overlay or not.
  228. * @returns {void}
  229. */
  230. function clearRuntimeErrors(dismissOverlay) {
  231. if (!root || currentMode !== 'runtimeError') {
  232. return;
  233. }
  234. currentRuntimeErrorIndex = 0;
  235. currentRuntimeErrors = [];
  236. if (typeof dismissOverlay === 'undefined' || dismissOverlay) {
  237. currentMode = null;
  238. cleanup();
  239. }
  240. }
  241. /**
  242. * Shows the compile error overlay with the specific Webpack error message.
  243. * @param {string} message
  244. * @returns {void}
  245. */
  246. function showCompileError(message) {
  247. if (!message) {
  248. return;
  249. }
  250. currentCompileErrorMessage = message;
  251. render();
  252. }
  253. /**
  254. * Shows the runtime error overlay with the specific error records.
  255. * @param {Error[]} errors
  256. * @returns {void}
  257. */
  258. function showRuntimeErrors(errors) {
  259. if (!errors || !errors.length) {
  260. return;
  261. }
  262. currentRuntimeErrors = errors;
  263. render();
  264. }
  265. /**
  266. * The debounced version of `showRuntimeErrors` to prevent frequent renders
  267. * due to rapid firing listeners.
  268. * @param {Error[]} errors
  269. * @returns {void}
  270. */
  271. const debouncedShowRuntimeErrors = debounce(showRuntimeErrors, 30);
  272. /**
  273. * Detects if an error is a Webpack compilation error.
  274. * @param {Error} error The error of interest.
  275. * @returns {boolean} If the error is a Webpack compilation error.
  276. */
  277. function isWebpackCompileError(error) {
  278. return /Module [A-z ]+\(from/.test(error.message) || /Cannot find module/.test(error.message);
  279. }
  280. /**
  281. * Handles runtime error contexts captured with EventListeners.
  282. * Integrates with a runtime error overlay.
  283. * @param {Error} error A valid error object.
  284. * @returns {void}
  285. */
  286. function handleRuntimeError(error) {
  287. if (error && !isWebpackCompileError(error) && currentRuntimeErrors.indexOf(error) === -1) {
  288. currentRuntimeErrors = currentRuntimeErrors.concat(error);
  289. }
  290. debouncedShowRuntimeErrors(currentRuntimeErrors);
  291. }
  292. module.exports = Object.freeze({
  293. clearCompileError: clearCompileError,
  294. clearRuntimeErrors: clearRuntimeErrors,
  295. handleRuntimeError: handleRuntimeError,
  296. showCompileError: showCompileError,
  297. showRuntimeErrors: showRuntimeErrors,
  298. });