Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions e2e/cases/browser-logs/react-error/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { gotoPage, rspackTest } from '@e2e/helper';

rspackTest(
'should forward React runtime error logs to terminal',
async ({ devOnly, page }) => {
const rsbuild = await devOnly();

await gotoPage(page, rsbuild, '/undefinedError');
await rsbuild.expectLog(
`error [browser] Uncaught TypeError: Cannot read properties of undefined (reading 'name') (src/undefinedError.jsx:5:0)`,
{ posix: true },
);

await gotoPage(page, rsbuild, '/effectError');
await rsbuild.expectLog(
`error [browser] Uncaught SyntaxError: Unexpected token 'i', "invalid json" is not valid JSON (src/effectError.jsx:6:0)`,
{ posix: true },
);

await gotoPage(page, rsbuild, '/eventError');
await page.click('button');
await rsbuild.expectLog(
`error [browser] Uncaught TypeError: Cannot read properties of null (reading 'someMethod') (src/eventError.jsx:6:0)`,
{ posix: true },
);
},
);
13 changes: 13 additions & 0 deletions e2e/cases/browser-logs/react-error/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';

export default defineConfig({
plugins: [pluginReact()],
source: {
entry: {
eventError: './src/eventError.jsx',
effectError: './src/effectError.jsx',
undefinedError: './src/undefinedError.jsx',
},
},
});
14 changes: 14 additions & 0 deletions e2e/cases/browser-logs/react-error/src/effectError.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useEffect } from 'react';
import { createRoot } from 'react-dom/client';

function ComponentWithEffectError() {
useEffect(() => {
JSON.parse('invalid json');
}, []);

return <div>Component with effect error</div>;
}

const container = document.getElementById('root');
const root = createRoot(container);
root.render(<ComponentWithEffectError />);
18 changes: 18 additions & 0 deletions e2e/cases/browser-logs/react-error/src/eventError.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { createRoot } from 'react-dom/client';

function ComponentWithEventError() {
const handleClick = () => {
const obj = null;
obj.someMethod();
};

return (
<button type="button" onClick={handleClick}>
Click me to trigger error
</button>
);
}

const container = document.getElementById('root');
const root = createRoot(container);
root.render(<ComponentWithEventError />);
10 changes: 10 additions & 0 deletions e2e/cases/browser-logs/react-error/src/undefinedError.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createRoot } from 'react-dom/client';

function ComponentWithUndefinedError() {
const data = undefined;
return <div>{data.name}</div>;
}

const container = document.getElementById('root');
const root = createRoot(container);
root.render(<ComponentWithUndefinedError />);
31 changes: 21 additions & 10 deletions packages/core/src/server/browserLogs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'node:path';
import { promisify } from 'node:util';
import { parse as parseStack } from 'stacktrace-parser';
import { JS_REGEX } from '../constants';
import { parse as parseStack, type StackFrame } from 'stacktrace-parser';
import { SCRIPT_REGEX } from '../constants';
import { color } from '../helpers';
import { logger } from '../logger';
import type { EnvironmentContext, InternalContext, Rspack } from '../types';
Expand All @@ -27,6 +27,19 @@ async function mapSourceMapPosition(
return originalPosition;
}

/**
* Returns the first stack frame that looks like user code
*/
const findSourceFrame = (parsed: StackFrame[]) => {
return parsed.find(
(frame) =>
frame.file !== null &&
frame.column !== null &&
frame.lineNumber !== null &&
SCRIPT_REGEX.test(frame.file),
) as { file: string; column: number; lineNumber: number } | undefined;
};

/**
* Resolve source filename and original position from runtime stack trace
*/
Expand All @@ -41,16 +54,12 @@ const resolveSourceLocation = async (
}

// only parse JS files
const { file, column, lineNumber } = parsed[0];
if (
file === null ||
column === null ||
lineNumber === null ||
!JS_REGEX.test(file)
) {
const frame = findSourceFrame(parsed);
if (!frame) {
return;
}

const { file, column, lineNumber } = frame;
const sourceMapInfo = getFileFromUrl(
`${file}.map`,
fs as OutputFileSystem,
Expand Down Expand Up @@ -113,7 +122,9 @@ export const reportRuntimeError = async (

if (message.stack) {
const rawLocation = await formatErrorLocation(message.stack, context, fs);
log += color.dim(` (${rawLocation})`);
if (rawLocation) {
log += color.dim(` (${rawLocation})`);
}
}

logger.error(log);
Expand Down
Loading