UNPKG

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