[Flight] Parse Stack on the Server and Transfer Structured Stack (#30410)
Stacked on #30401. Previously we were transferring the original V8 stack trace string to the client and then parsing it there. However, really the server is the one that knows what format it is and it should be able to vary by server environment. We also don't use the raw string anymore (at least not in enableOwnerStacks). We always create the native Error stacks. The string also made it unclear which environment it is and it was tempting to just use it as is. Instead I parse it on the server and make it a structured stack in the transfer format. It also makes it clear that it needs to be formatted in the current environment before presented.
Sebastian Markbåge committed
Jul 22, 2024 at 11:18 UTC
06763852ded5c85d3f6b7a47f77a59d9336d45d7
22 files changed
+267
-123
packages/react-client/src/ReactFlightClient.js
+73
-37
@@ -12,6 +12,7 @@ import type {
12
ReactDebugInfo,
13
ReactComponentInfo,
14
ReactAsyncInfo,
15
+ ReactStackTrace,
16
} from 'shared/ReactTypes';
17
import type {LazyComponent} from 'react/src/ReactLazy';
18
@@ -624,7 +625,7 @@ function createElement(
625
key: mixed,
626
props: mixed,
627
owner: null | ReactComponentInfo, // DEV-only
627
- stack: null | string, // DEV-only
628
+ stack: null | ReactStackTrace, // DEV-only
629
validated: number, // DEV-only
630
):
631
| React$Element<any>
@@ -1738,6 +1739,27 @@ function stopStream(
1739
controller.close(row === '' ? '"$undefined"' : row);
1740
}
1741
1742
+function formatV8Stack(
1743
+ errorName: string,
1744
+ errorMessage: string,
1745
+ stack: null | ReactStackTrace,
1746
+): string {
1747
+ let v8StyleStack = errorName + ': ' + errorMessage;
1748
+ if (stack) {
1749
+ for (let i = 0; i < stack.length; i++) {
1750
+ const frame = stack[i];
1751
+ const [name, filename, line, col] = frame;
1752
+ if (!name) {
1753
+ v8StyleStack += '\n at ' + filename + ':' + line + ':' + col;
1754
+ } else {
1755
+ v8StyleStack +=
1756
+ '\n at ' + name + ' (' + filename + ':' + line + ':' + col + ')';
1757
+ }
1758
+ }
1759
+ }
1760
+ return v8StyleStack;
1761
+}
1762
+
1763
type ErrorWithDigest = Error & {digest?: string};
1764
function resolveErrorProd(
1765
response: Response,
@@ -1773,7 +1795,7 @@ function resolveErrorDev(
1795
id: number,
1796
digest: string,
1797
message: string,
1776
- stack: string,
1798
+ stack: ReactStackTrace,
1799
env: string,
1800
): void {
1801
if (!__DEV__) {
@@ -1793,7 +1815,8 @@ function resolveErrorDev(
1815
message ||
1816
'An error occurred in the Server Components render but no message was provided',
1817
);
1796
- error.stack = stack;
1818
+ // For backwards compat we use the V8 formatting when the flag is off.
1819
+ error.stack = formatV8Stack(error.name, error.message, stack);
1820
} else {
1821
const callStack = buildFakeCallStack(
1822
response,
@@ -1853,7 +1876,7 @@ function resolvePostponeDev(
1876
response: Response,
1877
id: number,
1878
reason: string,
1856
- stack: string,
1879
+ stack: ReactStackTrace,
1880
): void {
1881
if (!__DEV__) {
1882
// These errors should never make it into a build so we don't need to encode them in codes.json
@@ -1862,11 +1885,34 @@ function resolvePostponeDev(
1885
'resolvePostponeDev should never be called in production mode. Use resolvePostponeProd instead. This is a bug in React.',
1886
);
1887
}
1865
- // eslint-disable-next-line react-internal/prod-error-codes
1866
- const error = new Error(reason || '');
1867
- const postponeInstance: Postpone = (error: any);
1868
- postponeInstance.$$typeof = REACT_POSTPONE_TYPE;
1869
- postponeInstance.stack = stack;
1888
+ let postponeInstance: Postpone;
1889
+ if (!enableOwnerStacks) {
1890
+ // Executing Error within a native stack isn't really limited to owner stacks
1891
+ // but we gate it behind the same flag for now while iterating.
1892
+ // eslint-disable-next-line react-internal/prod-error-codes
1893
+ postponeInstance = (Error(reason || ''): any);
1894
+ postponeInstance.$$typeof = REACT_POSTPONE_TYPE;
1895
+ // For backwards compat we use the V8 formatting when the flag is off.
1896
+ postponeInstance.stack = formatV8Stack(
1897
+ postponeInstance.name,
1898
+ postponeInstance.message,
1899
+ stack,
1900
+ );
1901
+ } else {
1902
+ const callStack = buildFakeCallStack(
1903
+ response,
1904
+ stack,
1905
+ // $FlowFixMe[incompatible-use]
1906
+ Error.bind(null, reason || ''),
1907
+ );
1908
+ const rootTask = response._debugRootTask;
1909
+ if (rootTask != null) {
1910
+ postponeInstance = rootTask.run(callStack);
1911
+ } else {
1912
+ postponeInstance = callStack();
1913
+ }
1914
+ postponeInstance.$$typeof = REACT_POSTPONE_TYPE;
1915
+ }
1916
const chunks = response._chunks;
1917
const chunk = chunks.get(id);
1918
if (!chunk) {
@@ -1973,40 +2019,25 @@ function createFakeFunction<T>(
2019
return fn;
2020
}
2021
1976
-// This matches either of these V8 formats.
1977
-// at name (filename:0:0)
1978
-// at filename:0:0
1979
-// at async filename:0:0
1980
-const frameRegExp =
1981
- /^ {3} at (?:(.+) \(([^\)]+):(\d+):(\d+)\)|(?:async )?([^\)]+):(\d+):(\d+))$/;
1982
-
2022
function buildFakeCallStack<T>(
2023
response: Response,
1985
- stack: string,
2024
+ stack: ReactStackTrace,
2025
innerCall: () => T,
2026
): () => T {
1988
- const frames = stack.split('\n');
2027
let callStack = innerCall;
1990
- for (let i = 0; i < frames.length; i++) {
1991
- const frame = frames[i];
1992
- let fn = fakeFunctionCache.get(frame);
2028
+ for (let i = 0; i < stack.length; i++) {
2029
+ const frame = stack[i];
2030
+ const frameKey = frame.join('-');
2031
+ let fn = fakeFunctionCache.get(frameKey);
2032
if (fn === undefined) {
1994
- const parsed = frameRegExp.exec(frame);
1995
- if (!parsed) {
1996
- // We assume the server returns a V8 compatible stack trace.
1997
- continue;
1998
- }
1999
- const name = parsed[1] || '';
2000
- const filename = parsed[2] || parsed[5] || '';
2001
- const line = +(parsed[3] || parsed[6]);
2002
- const col = +(parsed[4] || parsed[7]);
2033
+ const [name, filename, line, col] = frame;
2034
const sourceMap = response._debugFindSourceMapURL
2035
? response._debugFindSourceMapURL(filename)
2036
: null;
2037
fn = createFakeFunction(name, filename, sourceMap, line, col);
2038
// TODO: This cache should technically live on the response since the _debugFindSourceMapURL
2039
// function is an input and can vary by response.
2009
- fakeFunctionCache.set(frame, fn);
2040
+ fakeFunctionCache.set(frameKey, fn);
2041
}
2042
callStack = fn.bind(null, callStack);
2043
}
@@ -2026,7 +2057,7 @@ function initializeFakeTask(
2057
return cachedEntry;
2058
}
2059
2029
- if (typeof debugInfo.stack !== 'string') {
2060
+ if (debugInfo.stack == null) {
2061
// If this is an error, we should've really already initialized the task.
2062
// If it's null, we can't initialize a task.
2063
return null;
@@ -2064,7 +2095,7 @@ function initializeFakeTask(
2095
const createFakeJSXCallStack = {
2096
'react-stack-bottom-frame': function (
2097
response: Response,
2067
- stack: string,
2098
+ stack: ReactStackTrace,
2099
): Error {
2100
const callStackForError = buildFakeCallStack(
2101
response,
@@ -2077,7 +2108,7 @@ const createFakeJSXCallStack = {
2108
2109
const createFakeJSXCallStackInDEV: (
2110
response: Response,
2080
- stack: string,
2111
+ stack: ReactStackTrace,
2112
) => Error = __DEV__
2113
? // We use this technique to trick minifiers to preserve the function name.
2114
(createFakeJSXCallStack['react-stack-bottom-frame'].bind(
@@ -2100,7 +2131,7 @@ function initializeFakeStack(
2131
if (cachedEntry !== undefined) {
2132
return;
2133
}
2103
- if (typeof debugInfo.stack === 'string') {
2134
+ if (debugInfo.stack != null) {
2135
// $FlowFixMe[cannot-write]
2136
// $FlowFixMe[prop-missing]
2137
debugInfo.debugStack = createFakeJSXCallStackInDEV(
@@ -2154,8 +2185,13 @@ function resolveConsoleEntry(
2185
return;
2186
}
2187
2157
- const payload: [string, string, null | ReactComponentInfo, string, mixed] =
2158
- parseModel(response, value);
2188
+ const payload: [
2189
+ string,
2190
+ ReactStackTrace,
2191
+ null | ReactComponentInfo,
2192
+ string,
2193
+ mixed,
2194
+ ] = parseModel(response, value);
2195
const methodName = payload[0];
2196
const stackTrace = payload[1];
2197
const owner = payload[2];
packages/react-client/src/__tests__/ReactFlight-test.js
+16
-2
@@ -27,10 +27,24 @@ function normalizeCodeLocInfo(str) {
27
);
28
}
29
30
+function formatV8Stack(stack) {
31
+ let v8StyleStack = '';
32
+ if (stack) {
33
+ for (let i = 0; i < stack.length; i++) {
34
+ const [name] = stack[i];
35
+ if (v8StyleStack !== '') {
36
+ v8StyleStack += '\n';
37
+ }
38
+ v8StyleStack += ' in ' + name + ' (at **)';
39
+ }
40
+ }
41
+ return v8StyleStack;
42
+}
43
+
44
function normalizeComponentInfo(debugInfo) {
31
- if (typeof debugInfo.stack === 'string') {
45
+ if (Array.isArray(debugInfo.stack)) {
46
const {debugTask, debugStack, ...copy} = debugInfo;
33
- copy.stack = normalizeCodeLocInfo(debugInfo.stack);
47
+ copy.stack = formatV8Stack(debugInfo.stack);
48
if (debugInfo.owner) {
49
copy.owner = normalizeComponentInfo(debugInfo.owner);
50
}
packages/react-reconciler/src/ReactFiberOwnerStack.js
+1
-1
@@ -8,7 +8,7 @@
8
*/
9
10
// TODO: Make this configurable on the root.
11
-const externalRegExp = /\/node\_modules\/|\(\<anonymous\>\)/;
11
+const externalRegExp = /\/node\_modules\/|\(\<anonymous\>/;
12
13
function isNotExternal(stackFrame: string): boolean {
14
return !externalRegExp.test(stackFrame);
packages/react-server/src/ReactFizzComponentStack.js
+7
-7
@@ -165,17 +165,17 @@ export function getOwnerStackByComponentStackNodeInDev(
165
// TODO: Should we stash this somewhere for caching purposes?
166
ownerStack = formatOwnerStack(owner.debugStack);
167
owner = owner.owner;
168
- } else if (owner.stack != null) {
168
+ } else {
169
// Client Component
170
const node: ComponentStackNode = (owner: any);
171
- if (typeof owner.stack !== 'string') {
172
- ownerStack = node.stack = formatOwnerStack(owner.stack);
173
- } else {
174
- ownerStack = owner.stack;
171
+ if (node.stack != null) {
172
+ if (typeof node.stack !== 'string') {
173
+ ownerStack = node.stack = formatOwnerStack(node.stack);
174
+ } else {
175
+ ownerStack = node.stack;
176
+ }
177
}
178
owner = owner.owner;
177
- } else {
178
- owner = owner.owner;
179
}
180
// If we don't actually print the stack if there is no owner of this JSX element.
181
// In a real app it's typically not useful since the root app is always controlled
packages/react-server/src/ReactFizzOwnerStack.js
+1
-1
@@ -8,7 +8,7 @@
8
*/
9
10
// TODO: Make this configurable on the root.
11
-const externalRegExp = /\/node\_modules\/|\(\<anonymous\>\)/;
11
+const externalRegExp = /\/node\_modules\/|\(\<anonymous\>/;
12
13
function isNotExternal(stackFrame: string): boolean {
14
return !externalRegExp.test(stackFrame);
packages/react-server/src/ReactFlightOwnerStack.js
+1
-1
@@ -8,7 +8,7 @@
8
*/
9
10
// TODO: Make this configurable on the Request.
11
-const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\<anonymous\>\)/;
11
+const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\<anonymous\>/;
12
13
function isNotExternal(stackFrame: string): boolean {
14
return !externalRegExp.test(stackFrame);
packages/react-server/src/ReactFlightServer.js
+31
-65
@@ -62,6 +62,8 @@ import type {
62
ReactDebugInfo,
63
ReactComponentInfo,
64
ReactAsyncInfo,
65
+ ReactStackTrace,
66
+ ReactCallSite,
67
} from 'shared/ReactTypes';
68
import type {ReactElement} from 'shared/ReactElementType';
69
import type {LazyComponent} from 'react/src/ReactLazy';
@@ -77,6 +79,7 @@ import {
79
requestStorage,
80
createHints,
81
initAsyncDebugInfo,
82
+ parseStackTrace,
83
} from './ReactFlightServerConfig';
84
85
import {
@@ -131,64 +134,19 @@ import binaryToComparableString from 'shared/binaryToComparableString';
134
import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable';
135
136
// TODO: Make this configurable on the Request.
134
-const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\<anonymous\>\)/;
137
+const externalRegExp = /\/node\_modules\/|^node\:|^$/;
138
136
-function isNotExternal(stackFrame: string): boolean {
137
- return !externalRegExp.test(stackFrame);
139
+function isNotExternal(stackFrame: ReactCallSite): boolean {
140
+ const filename = stackFrame[1];
141
+ return !externalRegExp.test(filename);
142
}
143
140
-function prepareStackTrace(
141
- error: Error,
142
- structuredStackTrace: CallSite[],
143
-): string {
144
- const name = error.name || 'Error';
145
- const message = error.message || '';
146
- let stack = name + ': ' + message;
147
- for (let i = 0; i < structuredStackTrace.length; i++) {
148
- stack += '\n at ' + structuredStackTrace[i].toString();
149
- }
150
- return stack;
151
-}
152
-
153
-function getStack(error: Error): string {
154
- // We override Error.prepareStackTrace with our own version that normalizes
155
- // the stack to V8 formatting even if the server uses other formatting.
156
- // It also ensures that source maps are NOT applied to this since that can
157
- // be slow we're better off doing that lazily from the client instead of
158
- // eagerly on the server. If the stack has already been read, then we might
159
- // not get a normalized stack and it might still have been source mapped.
160
- // So the client still needs to be resilient to this.
161
- const previousPrepare = Error.prepareStackTrace;
162
- Error.prepareStackTrace = prepareStackTrace;
163
- try {
164
- // eslint-disable-next-line react-internal/safe-string-coercion
165
- return String(error.stack);
166
- } finally {
167
- Error.prepareStackTrace = previousPrepare;
168
- }
169
-}
170
-
171
-function filterDebugStack(error: Error): string {
144
+function filterStackTrace(error: Error, skipFrames: number): ReactStackTrace {
145
// Since stacks can be quite large and we pass a lot of them, we filter them out eagerly
146
// to save bandwidth even in DEV. We'll also replay these stacks on the client so by
147
// stripping them early we avoid that overhead. Otherwise we'd normally just rely on
148
// the DevTools or framework's ignore lists to filter them out.
176
- let stack = getStack(error);
177
- if (stack.startsWith('Error: react-stack-top-frame\n')) {
178
- // V8's default formatting prefixes with the error message which we
179
- // don't want/need.
180
- stack = stack.slice(29);
181
- }
182
- let idx = stack.indexOf('react-stack-bottom-frame');
183
- if (idx !== -1) {
184
- idx = stack.lastIndexOf('\n', idx);
185
- }
186
- if (idx !== -1) {
187
- // Cut off everything after the bottom frame since it'll be internals.
188
- stack = stack.slice(0, idx);
189
- }
190
- const frames = stack.split('\n').slice(1);
191
- return frames.filter(isNotExternal).join('\n');
149
+ return parseStackTrace(error, skipFrames).filter(isNotExternal);
150
}
151
152
initAsyncDebugInfo();
@@ -214,7 +172,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
172
// Extract the stack. Not all console logs print the full stack but they have at
173
// least the line it was called from. We could optimize transfer by keeping just
174
// one stack frame but keeping it simple for now and include all frames.
217
- const stack = filterDebugStack(new Error('react-stack-top-frame'));
175
+ const stack = filterStackTrace(new Error('react-stack-top-frame'), 1);
176
request.pendingChunks++;
177
// We don't currently use this id for anything but we emit it so that we can later
178
// refer to previous logs in debug info to associate them with a component.
@@ -973,7 +931,7 @@ function callWithDebugContextInDEV<A, T>(
931
if (enableOwnerStacks) {
932
// $FlowFixMe[cannot-write]
933
componentDebugInfo.stack =
976
- task.debugStack === null ? null : filterDebugStack(task.debugStack);
934
+ task.debugStack === null ? null : filterStackTrace(task.debugStack, 1);
935
// $FlowFixMe[cannot-write]
936
componentDebugInfo.debugStack = task.debugStack;
937
// $FlowFixMe[cannot-write]
@@ -1035,7 +993,9 @@ function renderFunctionComponent<Props>(
993
if (enableOwnerStacks) {
994
// $FlowFixMe[cannot-write]
995
componentDebugInfo.stack =
1038
- task.debugStack === null ? null : filterDebugStack(task.debugStack);
996
+ task.debugStack === null
997
+ ? null
998
+ : filterStackTrace(task.debugStack, 1);
999
// $FlowFixMe[cannot-write]
1000
componentDebugInfo.debugStack = task.debugStack;
1001
// $FlowFixMe[cannot-write]
@@ -1429,7 +1389,9 @@ function renderClientElement(
1389
key,
1390
props,
1391
task.debugOwner,
1432
- task.debugStack === null ? null : filterDebugStack(task.debugStack),
1392
+ task.debugStack === null
1393
+ ? null
1394
+ : filterStackTrace(task.debugStack, 1),
1395
validated,
1396
]
1397
: [REACT_ELEMENT_TYPE, type, key, props, task.debugOwner]
@@ -2519,12 +2481,12 @@ function renderModelDestructive(
2481
// $FlowFixMe[method-unbinding]
2482
typeof value.debugTask.run === 'function') ||
2483
value.debugStack instanceof Error) &&
2484
+ (enableOwnerStacks
2485
+ ? isArray((value: any).stack)
2486
+ : typeof (value: any).stack === 'undefined') &&
2487
typeof value.name === 'string' &&
2488
typeof value.env === 'string' &&
2524
- value.owner !== undefined &&
2525
- (enableOwnerStacks
2526
- ? typeof (value: any).stack === 'string'
2527
- : typeof (value: any).stack === 'undefined')
2489
+ value.owner !== undefined
2490
) {
2491
// This looks like a ReactComponentInfo. We can't serialize the ConsoleTask object so we
2492
// need to omit it before serializing.
@@ -2824,12 +2786,14 @@ function emitPostponeChunk(
2786
let row;
2787
if (__DEV__) {
2788
let reason = '';
2827
- let stack = '';
2789
+ let stack: ReactStackTrace;
2790
try {
2791
// eslint-disable-next-line react-internal/safe-string-coercion
2792
reason = String(postponeInstance.message);
2831
- stack = getStack(postponeInstance);
2832
- } catch (x) {}
2793
+ stack = filterStackTrace(postponeInstance, 0);
2794
+ } catch (x) {
2795
+ stack = [];
2796
+ }
2797
row = serializeRowHeader('P', id) + stringify({reason, stack}) + '\n';
2798
} else {
2799
// No reason included in prod.
@@ -2848,13 +2812,13 @@ function emitErrorChunk(
2812
let errorInfo: any;
2813
if (__DEV__) {
2814
let message;
2851
- let stack = '';
2815
+ let stack: ReactStackTrace;
2816
let env = request.environmentName();
2817
try {
2818
if (error instanceof Error) {
2819
// eslint-disable-next-line react-internal/safe-string-coercion
2820
message = String(error.message);
2857
- stack = getStack(error);
2821
+ stack = filterStackTrace(error, 0);
2822
const errorEnv = (error: any).environmentName;
2823
if (typeof errorEnv === 'string') {
2824
// This probably came from another FlightClient as a pass through.
@@ -2863,9 +2827,11 @@ function emitErrorChunk(
2827
}
2828
} else if (typeof error === 'object' && error !== null) {
2829
message = describeObjectForErrorMessage(error);
2830
+ stack = [];
2831
} else {
2832
// eslint-disable-next-line react-internal/safe-string-coercion
2833
message = String(error);
2834
+ stack = [];
2835
}
2836
} catch (x) {
2837
message = 'An error occurred but serializing the error message failed.';
@@ -3316,7 +3282,7 @@ function emitConsoleChunk(
3282
id: number,
3283
methodName: string,
3284
owner: null | ReactComponentInfo,
3319
- stackTrace: string,
3285
+ stackTrace: ReactStackTrace,
3286
args: Array<any>,
3287
): void {
3288
if (!__DEV__) {
packages/react-server/src/ReactFlightStackConfigV8.js
new
+90
@@ -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
+2
@@ -14,6 +14,8 @@ export * from '../ReactFlightServerConfigBundlerCustom';
14
15
export * from '../ReactFlightServerConfigDebugNoop';
16
17
+export * from '../ReactFlightStackConfigV8';
18
+
19
export type Hints = any;
20
export type HintCode = any;
21
// eslint-disable-next-line no-unused-vars
packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-esm.js
+2
@@ -21,3 +21,5 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
21
(null: any);
22
23
export * from '../ReactFlightServerConfigDebugNoop';
24
+
25
+export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js
+2
@@ -21,3 +21,5 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
21
(null: any);
22
23
export * from '../ReactFlightServerConfigDebugNoop';
24
+
25
+export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js
+2
@@ -21,3 +21,5 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
21
(null: any);
22
23
export * from '../ReactFlightServerConfigDebugNoop';
24
+
25
+export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js
+2
@@ -21,3 +21,5 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
21
(null: any);
22
23
export * from '../ReactFlightServerConfigDebugNoop';
24
+
25
+export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js
+3
@@ -35,4 +35,7 @@ export const createAsyncHook: HookCallbacks => AsyncHook =
35
};
36
export const executionAsyncId: () => number =
37
typeof async_hooks === 'object' ? async_hooks.executionAsyncId : (null: any);
38
+
39
export * from '../ReactFlightServerConfigDebugNode';
40
+
41
+export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-edge.js
+3
@@ -36,4 +36,7 @@ export const createAsyncHook: HookCallbacks => AsyncHook =
36
};
37
export const executionAsyncId: () => number =
38
typeof async_hooks === 'object' ? async_hooks.executionAsyncId : (null: any);
39
+
40
export * from '../ReactFlightServerConfigDebugNode';
41
+
42
+export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js
+2
@@ -14,6 +14,8 @@ export * from '../ReactFlightServerConfigBundlerCustom';
14
15
export * from '../ReactFlightServerConfigDebugNoop';
16
17
+export * from '../ReactFlightStackConfigV8';
18
+
19
export type Hints = any;
20
export type HintCode = any;
21
// eslint-disable-next-line no-unused-vars
packages/react-server/src/forks/ReactFlightServerConfig.dom-node-esm.js
+3
@@ -24,4 +24,7 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
24
supportsComponentStorage ? new AsyncLocalStorage() : (null: any);
25
26
export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
27
+
28
export * from '../ReactFlightServerConfigDebugNode';
29
+
30
+export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js
+3
@@ -24,4 +24,7 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
24
supportsComponentStorage ? new AsyncLocalStorage() : (null: any);
25
26
export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
27
+
28
export * from '../ReactFlightServerConfigDebugNode';
29
+
30
+export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js
+3
@@ -24,4 +24,7 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
24
supportsComponentStorage ? new AsyncLocalStorage() : (null: any);
25
26
export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks';
27
+
28
export * from '../ReactFlightServerConfigDebugNode';
29
+
30
+export * from '../ReactFlightStackConfigV8';
packages/react-server/src/forks/ReactFlightServerConfig.markup.js
+2
@@ -28,6 +28,8 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
28
29
export * from '../ReactFlightServerConfigDebugNoop';
30
31
+export * from '../ReactFlightStackConfigV8';
32
+
33
export type ClientManifest = null;
34
export opaque type ClientReference<T> = null; // eslint-disable-line no-unused-vars
35
export opaque type ServerReference<T> = null; // eslint-disable-line no-unused-vars
packages/shared/ReactTypes.js
+11
-2
@@ -178,11 +178,20 @@ export type Awaited<T> = T extends null | void
178
: T // argument was not an object
179
: T; // non-thenable
180
181
+export type ReactCallSite = [
182
+ string, // function name
183
+ string, // file name TODO: model nested eval locations as nested arrays
184
+ number, // line number
185
+ number, // column number
186
+];
187
+
188
+export type ReactStackTrace = Array<ReactCallSite>;
189
+
190
export type ReactComponentInfo = {
191
+name?: string,
192
+env?: string,
193
+owner?: null | ReactComponentInfo,
185
- +stack?: null | string,
194
+ +stack?: null | ReactStackTrace,
195
// Stashed Data for the Specific Execution Environment. Not part of the transport protocol
196
+debugStack?: null | Error,
197
+debugTask?: null | ConsoleTask,
@@ -191,7 +200,7 @@ export type ReactComponentInfo = {
200
export type ReactAsyncInfo = {
201
+started?: number,
202
+completed?: number,
194
- +stack?: string,
203
+ +stack?: null | ReactStackTrace,
204
};
205
206
export type ReactDebugInfo = Array<ReactComponentInfo | ReactAsyncInfo>;
scripts/jest/setupTests.js
+7
-7
@@ -292,13 +292,13 @@ function lazyRequireFunctionExports(moduleName) {
292
// If this export is a function, return a wrapper function that lazily
293
// requires the implementation from the current module cache.
294
if (typeof originalModule[prop] === 'function') {
295
- const wrapper = function () {
296
- return jest.requireActual(moduleName)[prop].apply(this, arguments);
297
- };
298
- // We use this to trick the filtering of Flight to exclude this frame.
299
- Object.defineProperty(wrapper, 'name', {
300
- value: '(<anonymous>)',
301
- });
295
+ // eslint-disable-next-line no-eval
296
+ const wrapper = eval(`
297
+ (function () {
298
+ return jest.requireActual(moduleName)[prop].apply(this, arguments);
299
+ })
300
+ // We use this to trick the filtering of Flight to exclude this frame.
301
+ //# sourceURL=<anonymous>`);
302
return wrapper;
303
} else {
304
return originalModule[prop];