index.js 9.1 KB

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