Skip to content

Commit 3c0996c

Browse files
authored
feat: display browser runtime errors in the terminal (#6251)
1 parent e4b9252 commit 3c0996c

File tree

2 files changed

+69
-7
lines changed

2 files changed

+69
-7
lines changed

packages/core/src/client/hmr.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import type { SocketMessage } from '../server/socketServer';
1+
import type {
2+
ClientMessage,
3+
ClientMessageRuntimeError,
4+
SocketMessage,
5+
} from '../server/socketServer';
26
import type { NormalizedClientConfig } from '../types';
37

48
const config: NormalizedClientConfig = RSBUILD_CLIENT_CONFIG;
@@ -150,6 +154,13 @@ let socket: WebSocket | null = null;
150154
let reconnectCount = 0;
151155
let pingIntervalId: ReturnType<typeof setInterval>;
152156

157+
const isSocketReady = () => socket && socket.readyState === socket.OPEN;
158+
const socketSend = (data: ClientMessage) => {
159+
if (isSocketReady()) {
160+
socket!.send(JSON.stringify(data));
161+
}
162+
};
163+
153164
function onOpen() {
154165
// Notify users that the WebSocket has successfully connected.
155166
console.info('[rsbuild] WebSocket connected.');
@@ -160,10 +171,15 @@ function onOpen() {
160171
// To prevent WebSocket timeouts caused by proxies (e.g., nginx, docker),
161172
// send a periodic ping message to keep the connection alive.
162173
pingIntervalId = setInterval(() => {
163-
if (socket && socket.readyState === socket.OPEN) {
164-
socket.send(JSON.stringify({ type: 'ping' }));
165-
}
174+
socketSend({ type: 'ping' });
166175
}, 30000);
176+
177+
if (errorMessages.length) {
178+
errorMessages.forEach((message) => {
179+
socketSend(message);
180+
});
181+
errorMessages.length = 0;
182+
}
167183
}
168184

169185
function onMessage(e: MessageEvent<string>) {
@@ -214,7 +230,7 @@ function onClose() {
214230
setTimeout(connect, 1000 * 1.5 ** reconnectCount);
215231
}
216232

217-
function onError() {
233+
function onSocketError() {
218234
if (formatURL() !== formatURL(true)) {
219235
console.error(
220236
'[rsbuild] WebSocket connection failed. Trying direct connection fallback.',
@@ -225,6 +241,20 @@ function onError() {
225241
}
226242
}
227243

244+
const errorMessages: ClientMessageRuntimeError[] = [];
245+
246+
function onRuntimeError(event: ErrorEvent) {
247+
const message: ClientMessageRuntimeError = {
248+
type: 'runtime-error',
249+
message: event.message,
250+
};
251+
if (isSocketReady()) {
252+
socketSend(message);
253+
} else {
254+
errorMessages.push(message);
255+
}
256+
}
257+
228258
// Establishing a WebSocket connection with the server.
229259
function connect(fallback = false) {
230260
if (reconnectCount === 0) {
@@ -240,7 +270,7 @@ function connect(fallback = false) {
240270
socket.addEventListener('message', onMessage);
241271
// Handle errors
242272
if (!fallback) {
243-
socket.addEventListener('error', onError);
273+
socket.addEventListener('error', onSocketError);
244274
}
245275
}
246276

@@ -250,7 +280,7 @@ function removeListeners() {
250280
socket.removeEventListener('open', onOpen);
251281
socket.removeEventListener('close', onClose);
252282
socket.removeEventListener('message', onMessage);
253-
socket.removeEventListener('error', onError);
283+
socket.removeEventListener('error', onSocketError);
254284
}
255285
}
256286

@@ -260,4 +290,8 @@ function reloadPage() {
260290
}
261291
}
262292

293+
if (typeof window !== 'undefined') {
294+
window.addEventListener('error', onRuntimeError);
295+
}
296+
263297
connect();

packages/core/src/server/socketServer.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { IncomingMessage } from 'node:http';
22
import type { Socket } from 'node:net';
33
import type Ws from '../../compiled/ws/index.js';
44
import {
5+
color,
56
getAllStatsErrors,
67
getAllStatsWarnings,
78
getStatsOptions,
@@ -44,13 +45,24 @@ export type SocketMessageErrors = {
4445
data: { text: string[]; html: string };
4546
};
4647

48+
export type ClientMessageRuntimeError = {
49+
type: 'runtime-error';
50+
message: string;
51+
};
52+
53+
export type ClientMessagePing = {
54+
type: 'ping';
55+
};
56+
4757
export type SocketMessage =
4858
| SocketMessageOk
4959
| SocketMessageStaticChanged
5060
| SocketMessageHash
5161
| SocketMessageWarnings
5262
| SocketMessageErrors;
5363

64+
export type ClientMessage = ClientMessagePing | ClientMessageRuntimeError;
65+
5466
const parseQueryString = (req: IncomingMessage) => {
5567
const queryStr = req.url ? req.url.split('?')[1] : '';
5668
return queryStr ? Object.fromEntries(new URLSearchParams(queryStr)) : {};
@@ -247,6 +259,22 @@ export class SocketServer {
247259
socket.isAlive = true;
248260
});
249261

262+
socket.on('message', (data) => {
263+
try {
264+
const message: ClientMessage = JSON.parse(
265+
// eslint-disable-next-line @typescript-eslint/no-base-to-string
266+
typeof data === 'string' ? data : data.toString(),
267+
);
268+
269+
// Handle runtime errors from browser
270+
if (message.type === 'runtime-error') {
271+
logger.error(
272+
`${color.cyan('[browser]')} ${color.red(message.message)}`,
273+
);
274+
}
275+
} catch {}
276+
});
277+
250278
let sockets = this.socketsMap.get(token);
251279
if (!sockets) {
252280
sockets = new Set();

0 commit comments

Comments
 (0)