5.2 KB175 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8// Detect uncaught JavaScript errors and write them to the DOM in dev mode
9if (import.meta.hot) {
10 let errorContainer: HTMLDivElement | null = null;
11
12 const createErrorContainer = () => {
13 if (!errorContainer) {
14 errorContainer = document.createElement('div');
15 errorContainer.id = 'uncaught-error-display';
16 errorContainer.style.cssText = `
17 position: fixed;
18 bottom: 0;
19 left: 0;
20 right: 0;
21 max-height: 300px;
22 background-color: rgba(220, 38, 38, 0.95);
23 color: white;
24 font-family: monospace;
25 font-size: 12px;
26 z-index: 999999;
27 border-top: 3px solid #991b1b;
28 display: flex;
29 flex-direction: column;
30 `;
31
32 // Create header row
33 const header = document.createElement('div');
34 header.style.cssText = `
35 display: flex;
36 justify-content: space-between;
37 align-items: center;
38 padding: 12px 16px;
39 border-bottom: 1px solid rgba(255, 255, 255, 0.3);
40 font-weight: bold;
41 `;
42
43 const title = document.createElement('span');
44 title.textContent = 'Uncaught Webview Errors (development mode only)';
45 header.appendChild(title);
46
47 const closeButton = document.createElement('button');
48 closeButton.textContent = '×';
49 closeButton.style.cssText = `
50 background: none;
51 border: none;
52 color: white;
53 font-size: 24px;
54 line-height: 1;
55 cursor: pointer;
56 padding: 0;
57 width: 24px;
58 height: 24px;
59 display: flex;
60 align-items: center;
61 justify-content: center;
62 `;
63 closeButton.onclick = () => {
64 if (errorContainer) {
65 errorContainer.style.display = 'none';
66 }
67 };
68 header.appendChild(closeButton);
69
70 // Create errors content container
71 const errorsContent = document.createElement('div');
72 errorsContent.id = 'uncaught-error-display-content';
73 errorsContent.style.cssText = `
74 overflow-y: auto;
75 padding: 16px;
76 flex: 1;
77 `;
78
79 errorContainer.appendChild(header);
80 errorContainer.appendChild(errorsContent);
81 document.body.appendChild(errorContainer);
82 }
83 return errorContainer;
84 };
85
86 const displayError = (
87 message: string,
88 source?: string,
89 lineno?: number,
90 colno?: number,
91 error?: Error,
92 ) => {
93 const container = createErrorContainer();
94 const errorsContent = container.querySelector('#uncaught-error-display-content');
95 if (!errorsContent) {
96 return;
97 }
98
99 // Show the container if it was hidden
100 container.style.display = 'flex';
101
102 const errorEntry = document.createElement('div');
103 errorEntry.style.cssText = `
104 margin-bottom: 12px;
105 padding-bottom: 12px;
106 border-bottom: 1px solid rgba(255, 255, 255, 0.3);
107 `;
108
109 const timestamp = new Date().toLocaleTimeString();
110
111 // Create header line
112 const header = document.createElement('strong');
113 header.textContent = `[${timestamp}] Uncaught Error:`;
114 errorEntry.appendChild(header);
115 errorEntry.appendChild(document.createElement('br'));
116
117 // Add error message
118 const messageText = document.createTextNode(message);
119 errorEntry.appendChild(messageText);
120 errorEntry.appendChild(document.createElement('br'));
121
122 // Add source location if available
123 if (source) {
124 const sourceSpan = document.createElement('span');
125 sourceSpan.style.opacity = '0.8';
126 sourceSpan.textContent = `at ${source}:${lineno}:${colno}`;
127 errorEntry.appendChild(sourceSpan);
128 errorEntry.appendChild(document.createElement('br'));
129 }
130
131 // Add stack trace if available
132 if (error?.stack) {
133 const stackPre = document.createElement('pre');
134 stackPre.style.cssText =
135 'margin-top: 8px; opacity: 0.9; white-space: pre-wrap; word-break: break-all;';
136 stackPre.textContent = error.stack;
137 errorEntry.appendChild(stackPre);
138 }
139
140 errorsContent.insertBefore(errorEntry, errorsContent.firstChild);
141 };
142
143 const handleError = (event: ErrorEvent) => {
144 displayError(event.message, event.filename, event.lineno, event.colno, event.error);
145 };
146
147 const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
148 const error = event.reason;
149 let message = 'Unhandled Promise Rejection';
150 let errorObj: Error | undefined;
151
152 if (error instanceof Error) {
153 message = error.message;
154 errorObj = error;
155 } else if (typeof error === 'string') {
156 message = error;
157 } else if (error != null) {
158 message = String(error);
159 }
160
161 displayError(`Promise: ${message}`, undefined, undefined, undefined, errorObj);
162 };
163
164 window.addEventListener('error', handleError);
165 window.addEventListener('unhandledrejection', handleUnhandledRejection);
166
167 // TODO: we should probably call this on hot reload somehow
168 const _dispose = () => {
169 window.removeEventListener('error', handleError);
170 window.removeEventListener('unhandledrejection', handleUnhandledRejection);
171 errorContainer?.remove();
172 errorContainer = null;
173 };
174}
175