Skip to content

Commit 81586f6

Browse files
committed
Move parseStackTrace to ReactFlightServerConfig
All servers currently use the V8 format. Including Bun, which overrides the JSC format to V8 format it seems. However, we might need to configure this for the FB build. Conceptually it's also just something that is environment specific. I now apply filtering on the result which needs to update some hacks.
1 parent bc730ac commit 81586f6

17 files changed

+140
-79
lines changed

packages/react-reconciler/src/ReactFiberOwnerStack.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
// TODO: Make this configurable on the root.
11-
const externalRegExp = /\/node\_modules\/|\(\<anonymous\>\)/;
11+
const externalRegExp = /\/node\_modules\/|\(\<anonymous\>/;
1212

1313
function isNotExternal(stackFrame: string): boolean {
1414
return !externalRegExp.test(stackFrame);

packages/react-server/src/ReactFizzOwnerStack.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
// TODO: Make this configurable on the root.
11-
const externalRegExp = /\/node\_modules\/|\(\<anonymous\>\)/;
11+
const externalRegExp = /\/node\_modules\/|\(\<anonymous\>/;
1212

1313
function isNotExternal(stackFrame: string): boolean {
1414
return !externalRegExp.test(stackFrame);

packages/react-server/src/ReactFlightOwnerStack.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
// TODO: Make this configurable on the Request.
11-
const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\<anonymous\>\)/;
11+
const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\<anonymous\>/;
1212

1313
function isNotExternal(stackFrame: string): boolean {
1414
return !externalRegExp.test(stackFrame);

packages/react-server/src/ReactFlightServer.js

Lines changed: 18 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import type {
6363
ReactComponentInfo,
6464
ReactAsyncInfo,
6565
ReactStackTrace,
66+
ReactCallSite,
6667
} from 'shared/ReactTypes';
6768
import type {ReactElement} from 'shared/ReactElementType';
6869
import type {LazyComponent} from 'react/src/ReactLazy';
@@ -78,6 +79,7 @@ import {
7879
requestStorage,
7980
createHints,
8081
initAsyncDebugInfo,
82+
parseStackTrace,
8183
} from './ReactFlightServerConfig';
8284

8385
import {
@@ -132,83 +134,19 @@ import binaryToComparableString from 'shared/binaryToComparableString';
132134
import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
133135

134136
// TODO: Make this configurable on the Request.
135-
const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\<anonymous\>\)/;
137+
const externalRegExp = /\/node\_modules\/|^node\:|^$/;
136138

137-
function isNotExternal(stackFrame: string): boolean {
138-
return !externalRegExp.test(stackFrame);
139+
function isNotExternal(stackFrame: ReactCallSite): boolean {
140+
const filename = stackFrame[1];
141+
return !externalRegExp.test(filename);
139142
}
140143

141-
function prepareStackTrace(
142-
error: Error,
143-
structuredStackTrace: CallSite[],
144-
): string {
145-
const name = error.name || 'Error';
146-
const message = error.message || '';
147-
let stack = name + ': ' + message;
148-
for (let i = 0; i < structuredStackTrace.length; i++) {
149-
stack += '\n at ' + structuredStackTrace[i].toString();
150-
}
151-
return stack;
152-
}
153-
154-
function getStack(error: Error): string {
155-
// We override Error.prepareStackTrace with our own version that normalizes
156-
// the stack to V8 formatting even if the server uses other formatting.
157-
// It also ensures that source maps are NOT applied to this since that can
158-
// be slow we're better off doing that lazily from the client instead of
159-
// eagerly on the server. If the stack has already been read, then we might
160-
// not get a normalized stack and it might still have been source mapped.
161-
// So the client still needs to be resilient to this.
162-
const previousPrepare = Error.prepareStackTrace;
163-
Error.prepareStackTrace = prepareStackTrace;
164-
try {
165-
// eslint-disable-next-line react-internal/safe-string-coercion
166-
return String(error.stack);
167-
} finally {
168-
Error.prepareStackTrace = previousPrepare;
169-
}
170-
}
171-
172-
// This matches either of these V8 formats.
173-
// at name (filename:0:0)
174-
// at filename:0:0
175-
// at async filename:0:0
176-
const frameRegExp =
177-
/^ {3} at (?:(.+) \(([^\)]+):(\d+):(\d+)\)|(?:async )?([^\)]+):(\d+):(\d+))$/;
178-
179-
function parseStackTrace(error: Error, skipFrames: number): ReactStackTrace {
144+
function filterStackTrace(error: Error, skipFrames: number): ReactStackTrace {
180145
// Since stacks can be quite large and we pass a lot of them, we filter them out eagerly
181146
// to save bandwidth even in DEV. We'll also replay these stacks on the client so by
182147
// stripping them early we avoid that overhead. Otherwise we'd normally just rely on
183148
// the DevTools or framework's ignore lists to filter them out.
184-
let stack = getStack(error);
185-
if (stack.startsWith('Error: react-stack-top-frame\n')) {
186-
// V8's default formatting prefixes with the error message which we
187-
// don't want/need.
188-
stack = stack.slice(29);
189-
}
190-
let idx = stack.indexOf('react-stack-bottom-frame');
191-
if (idx !== -1) {
192-
idx = stack.lastIndexOf('\n', idx);
193-
}
194-
if (idx !== -1) {
195-
// Cut off everything after the bottom frame since it'll be internals.
196-
stack = stack.slice(0, idx);
197-
}
198-
const frames = stack.split('\n').slice(skipFrames).filter(isNotExternal);
199-
const parsedFrames: ReactStackTrace = [];
200-
for (let i = 0; i < frames.length; i++) {
201-
const parsed = frameRegExp.exec(frames[i]);
202-
if (!parsed) {
203-
continue;
204-
}
205-
const name = parsed[1] || '';
206-
const filename = parsed[2] || parsed[5] || '';
207-
const line = +(parsed[3] || parsed[6]);
208-
const col = +(parsed[4] || parsed[7]);
209-
parsedFrames.push([name, filename, line, col]);
210-
}
211-
return parsedFrames;
149+
return parseStackTrace(error, skipFrames).filter(isNotExternal);
212150
}
213151

214152
initAsyncDebugInfo();
@@ -234,7 +172,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
234172
// Extract the stack. Not all console logs print the full stack but they have at
235173
// least the line it was called from. We could optimize transfer by keeping just
236174
// one stack frame but keeping it simple for now and include all frames.
237-
const stack = parseStackTrace(new Error('react-stack-top-frame'), 1);
175+
const stack = filterStackTrace(new Error('react-stack-top-frame'), 1);
238176
request.pendingChunks++;
239177
// We don't currently use this id for anything but we emit it so that we can later
240178
// refer to previous logs in debug info to associate them with a component.
@@ -993,7 +931,7 @@ function callWithDebugContextInDEV<A, T>(
993931
if (enableOwnerStacks) {
994932
// $FlowFixMe[cannot-write]
995933
componentDebugInfo.stack =
996-
task.debugStack === null ? null : parseStackTrace(task.debugStack, 1);
934+
task.debugStack === null ? null : filterStackTrace(task.debugStack, 1);
997935
// $FlowFixMe[cannot-write]
998936
componentDebugInfo.debugStack = task.debugStack;
999937
// $FlowFixMe[cannot-write]
@@ -1055,7 +993,9 @@ function renderFunctionComponent<Props>(
1055993
if (enableOwnerStacks) {
1056994
// $FlowFixMe[cannot-write]
1057995
componentDebugInfo.stack =
1058-
task.debugStack === null ? null : parseStackTrace(task.debugStack, 1);
996+
task.debugStack === null
997+
? null
998+
: filterStackTrace(task.debugStack, 1);
1059999
// $FlowFixMe[cannot-write]
10601000
componentDebugInfo.debugStack = task.debugStack;
10611001
// $FlowFixMe[cannot-write]
@@ -1449,7 +1389,9 @@ function renderClientElement(
14491389
key,
14501390
props,
14511391
task.debugOwner,
1452-
task.debugStack === null ? null : parseStackTrace(task.debugStack, 1),
1392+
task.debugStack === null
1393+
? null
1394+
: filterStackTrace(task.debugStack, 1),
14531395
validated,
14541396
]
14551397
: [REACT_ELEMENT_TYPE, type, key, props, task.debugOwner]
@@ -2848,7 +2790,7 @@ function emitPostponeChunk(
28482790
try {
28492791
// eslint-disable-next-line react-internal/safe-string-coercion
28502792
reason = String(postponeInstance.message);
2851-
stack = parseStackTrace(postponeInstance, 0);
2793+
stack = filterStackTrace(postponeInstance, 0);
28522794
} catch (x) {
28532795
stack = [];
28542796
}
@@ -2876,7 +2818,7 @@ function emitErrorChunk(
28762818
if (error instanceof Error) {
28772819
// eslint-disable-next-line react-internal/safe-string-coercion
28782820
message = String(error.message);
2879-
stack = parseStackTrace(error, 0);
2821+
stack = filterStackTrace(error, 0);
28802822
const errorEnv = (error: any).environmentName;
28812823
if (typeof errorEnv === 'string') {
28822824
// This probably came from another FlightClient as a pass through.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
* @flow
8+
*/
9+
10+
import type {ReactStackTrace} from 'shared/ReactTypes';
11+
12+
function prepareStackTrace(
13+
error: Error,
14+
structuredStackTrace: CallSite[],
15+
): string {
16+
const name = error.name || 'Error';
17+
const message = error.message || '';
18+
let stack = name + ': ' + message;
19+
for (let i = 0; i < structuredStackTrace.length; i++) {
20+
stack += '\n at ' + structuredStackTrace[i].toString();
21+
}
22+
return stack;
23+
}
24+
25+
function getStack(error: Error): string {
26+
// We override Error.prepareStackTrace with our own version that normalizes
27+
// the stack to V8 formatting even if the server uses other formatting.
28+
// It also ensures that source maps are NOT applied to this since that can
29+
// be slow we're better off doing that lazily from the client instead of
30+
// eagerly on the server. If the stack has already been read, then we might
31+
// not get a normalized stack and it might still have been source mapped.
32+
const previousPrepare = Error.prepareStackTrace;
33+
Error.prepareStackTrace = prepareStackTrace;
34+
try {
35+
// eslint-disable-next-line react-internal/safe-string-coercion
36+
return String(error.stack);
37+
} finally {
38+
Error.prepareStackTrace = previousPrepare;
39+
}
40+
}
41+
42+
// This matches either of these V8 formats.
43+
// at name (filename:0:0)
44+
// at filename:0:0
45+
// at async filename:0:0
46+
const frameRegExp =
47+
/^ {3} at (?:(.+) \(([^\)]+):(\d+):(\d+)\)|(?:async )?([^\)]+):(\d+):(\d+))$/;
48+
49+
export function parseStackTrace(
50+
error: Error,
51+
skipFrames: number,
52+
): ReactStackTrace {
53+
let stack = getStack(error);
54+
if (stack.startsWith('Error: react-stack-top-frame\n')) {
55+
// V8's default formatting prefixes with the error message which we
56+
// don't want/need.
57+
stack = stack.slice(29);
58+
}
59+
let idx = stack.indexOf('react-stack-bottom-frame');
60+
if (idx !== -1) {
61+
idx = stack.lastIndexOf('\n', idx);
62+
}
63+
if (idx !== -1) {
64+
// Cut off everything after the bottom frame since it'll be internals.
65+
stack = stack.slice(0, idx);
66+
}
67+
const frames = stack.split('\n');
68+
const parsedFrames: ReactStackTrace = [];
69+
// We skip top frames here since they may or may not be parseable but we
70+
// want to skip the same number of frames regardless. I.e. we can't do it
71+
// in the caller.
72+
for (let i = skipFrames; i < frames.length; i++) {
73+
const parsed = frameRegExp.exec(frames[i]);
74+
if (!parsed) {
75+
continue;
76+
}
77+
let name = parsed[1] || '';
78+
if (name === '<anonymous>') {
79+
name = '';
80+
}
81+
let filename = parsed[2] || parsed[5] || '';
82+
if (filename === '<anonymous>') {
83+
filename = '';
84+
}
85+
const line = +(parsed[3] || parsed[6]);
86+
const col = +(parsed[4] || parsed[7]);
87+
parsedFrames.push([name, filename, line, col]);
88+
}
89+
return parsedFrames;
90+
}

packages/react-server/src/forks/ReactFlightServerConfig.custom.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export * from '../ReactFlightServerConfigBundlerCustom';
1414

1515
export * from '../ReactFlightServerConfigDebugNoop';
1616

17+
export * from '../ReactFlightStackConfigV8';
18+
1719
export type Hints = any;
1820
export type HintCode = any;
1921
// eslint-disable-next-line no-unused-vars

packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-esm.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
2121
(null: any);
2222

2323
export * from '../ReactFlightServerConfigDebugNoop';
24+
25+
export * from '../ReactFlightStackConfigV8';

packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
2121
(null: any);
2222

2323
export * from '../ReactFlightServerConfigDebugNoop';
24+
25+
export * from '../ReactFlightStackConfigV8';

packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
2121
(null: any);
2222

2323
export * from '../ReactFlightServerConfigDebugNoop';
24+
25+
export * from '../ReactFlightStackConfigV8';

packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
2121
(null: any);
2222

2323
export * from '../ReactFlightServerConfigDebugNoop';
24+
25+
export * from '../ReactFlightStackConfigV8';

0 commit comments

Comments
 (0)