[DevTools] Add structure full stack parsing to DevTools (#34093)
We'll need complete parsing of stack traces for both owner stacks and async debug info so we need to expand the stack parsing capabilities a bit. This refactors the source location extraction to use some helpers we can use for other things too. This is a fork of `ReactFlightStackConfigV8` which also supports DevTools requirements like checking both `react_stack_bottom_frame` and `react-stack-bottom-frame` as well as supporting Firefox stacks. It also supports extracting the first frame of a component stack or the last frame of an owner stack for the source location.
Sebastian Markbåge committed
Aug 4, 2025 at 09:37 UTC
557745eb0b50c2c7b126813fdae9c1929afc87f9
5 files changed
+351
-204
packages/react-devtools-shared/src/__tests__/utils-test.js
+12
-12
@@ -19,8 +19,8 @@ import {
19
formatWithStyles,
20
gt,
21
gte,
22
- parseSourceFromComponentStack,
22
} from 'react-devtools-shared/src/backend/utils';
23
+import {extractLocationFromComponentStack} from 'react-devtools-shared/src/backend/utils/parseStackTrace';
24
import {
25
REACT_SUSPENSE_LIST_TYPE as SuspenseList,
26
REACT_STRICT_MODE_TYPE as StrictMode,
@@ -306,20 +306,20 @@ describe('utils', () => {
306
});
307
});
308
309
- describe('parseSourceFromComponentStack', () => {
309
+ describe('extractLocationFromComponentStack', () => {
310
it('should return null if passed empty string', () => {
311
- expect(parseSourceFromComponentStack('')).toEqual(null);
311
+ expect(extractLocationFromComponentStack('')).toEqual(null);
312
});
313
314
it('should construct the source from the first frame if available', () => {
315
expect(
316
- parseSourceFromComponentStack(
316
+ extractLocationFromComponentStack(
317
'at l (https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js:1:10389)\n' +
318
'at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)\n' +
319
'at r (https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:498)\n',
320
),
321
).toEqual([
322
- '',
322
+ 'l',
323
'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js',
324
1,
325
10389,
@@ -328,7 +328,7 @@ describe('utils', () => {
328
329
it('should construct the source from highest available frame', () => {
330
expect(
331
- parseSourceFromComponentStack(
331
+ extractLocationFromComponentStack(
332
' at Q\n' +
333
' at a\n' +
334
' at m (https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236)\n' +
@@ -342,7 +342,7 @@ describe('utils', () => {
342
' at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)',
343
),
344
).toEqual([
345
- '',
345
+ 'm',
346
'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js',
347
5,
348
9236,
@@ -351,7 +351,7 @@ describe('utils', () => {
351
352
it('should construct the source from frame, which has only url specified', () => {
353
expect(
354
- parseSourceFromComponentStack(
354
+ extractLocationFromComponentStack(
355
' at Q\n' +
356
' at a\n' +
357
' at https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236\n',
@@ -366,13 +366,13 @@ describe('utils', () => {
366
367
it('should parse sourceURL correctly if it includes parentheses', () => {
368
expect(
369
- parseSourceFromComponentStack(
369
+ extractLocationFromComponentStack(
370
'at HotReload (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:307:11)\n' +
371
' at Router (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js:181:11)\n' +
372
' at ErrorBoundaryHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:114:9)',
373
),
374
).toEqual([
375
- '',
375
+ 'HotReload',
376
'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js',
377
307,
378
11,
@@ -381,13 +381,13 @@ describe('utils', () => {
381
382
it('should support Firefox stack', () => {
383
expect(
384
- parseSourceFromComponentStack(
384
+ extractLocationFromComponentStack(
385
'tt@https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js:1:165558\n' +
386
'f@https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8535\n' +
387
'r@https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:513',
388
),
389
).toEqual([
390
- '',
390
+ 'tt',
391
'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js',
392
1,
393
165558,
packages/react-devtools-shared/src/backend/fiber/renderer.js
+7
-5
@@ -54,10 +54,12 @@ import {
54
formatDurationToMicrosecondsGranularity,
55
gt,
56
gte,
57
- parseSourceFromComponentStack,
58
- parseSourceFromOwnerStack,
57
serializeToString,
58
} from 'react-devtools-shared/src/backend/utils';
59
+import {
60
+ extractLocationFromComponentStack,
61
+ extractLocationFromOwnerStack,
62
+} from 'react-devtools-shared/src/backend/utils/parseStackTrace';
63
import {
64
cleanForBridge,
65
copyWithDelete,
@@ -6340,7 +6342,7 @@ export function attach(
6342
if (stackFrame === null) {
6343
return null;
6344
}
6343
- const source = parseSourceFromComponentStack(stackFrame);
6345
+ const source = extractLocationFromComponentStack(stackFrame);
6346
fiberInstance.source = source;
6347
return source;
6348
}
@@ -6369,7 +6371,7 @@ export function attach(
6371
// any intermediate utility functions. This won't point to the top of the component function
6372
// but it's at least somewhere within it.
6373
if (isError(unresolvedSource)) {
6372
- return (instance.source = parseSourceFromOwnerStack(
6374
+ return (instance.source = extractLocationFromOwnerStack(
6375
(unresolvedSource: any),
6376
));
6377
}
@@ -6377,7 +6379,7 @@ export function attach(
6379
const idx = unresolvedSource.lastIndexOf('\n');
6380
const lastLine =
6381
idx === -1 ? unresolvedSource : unresolvedSource.slice(idx + 1);
6380
- return (instance.source = parseSourceFromComponentStack(lastLine));
6382
+ return (instance.source = extractLocationFromComponentStack(lastLine));
6383
}
6384
6385
// $FlowFixMe: refined.
packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js
+1
-4
@@ -13,12 +13,9 @@ export function formatOwnerStack(error: Error): string {
13
const prevPrepareStackTrace = Error.prepareStackTrace;
14
// $FlowFixMe[incompatible-type] It does accept undefined.
15
Error.prepareStackTrace = undefined;
16
- const stack = error.stack;
16
+ let stack = error.stack;
17
Error.prepareStackTrace = prevPrepareStackTrace;
18
- return formatOwnerStackString(stack);
19
-}
18
21
-export function formatOwnerStackString(stack: string): string {
19
if (stack.startsWith('Error: react-stack-top-frame\n')) {
20
// V8's default formatting prefixes with the error message which we
21
// don't want/need.
packages/react-devtools-shared/src/backend/utils/index.js
-183
@@ -12,14 +12,11 @@ import {compareVersions} from 'compare-versions';
12
import {dehydrate} from 'react-devtools-shared/src/hydration';
13
import isArray from 'shared/isArray';
14
15
-import type {ReactFunctionLocation} from 'shared/ReactTypes';
15
import type {DehydratedData} from 'react-devtools-shared/src/frontend/types';
16
17
export {default as formatWithStyles} from './formatWithStyles';
18
export {default as formatConsoleArguments} from './formatConsoleArguments';
19
21
-import {formatOwnerStackString} from '../shared/DevToolsOwnerStack';
22
-
20
// TODO: update this to the first React version that has a corresponding DevTools backend
21
const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9';
22
export function hasAssignedBackend(version?: string): boolean {
@@ -258,186 +255,6 @@ export const isReactNativeEnvironment = (): boolean => {
255
return window.document == null;
256
};
257
261
-function extractLocation(url: string): null | {
262
- functionName?: string,
263
- sourceURL: string,
264
- line?: string,
265
- column?: string,
266
-} {
267
- if (url.indexOf(':') === -1) {
268
- return null;
269
- }
270
-
271
- // remove any parentheses from start and end
272
- const withoutParentheses = url.replace(/^\(+/, '').replace(/\)+$/, '');
273
- const locationParts = /(at )?(.+?)(?::(\d+))?(?::(\d+))?$/.exec(
274
- withoutParentheses,
275
- );
276
-
277
- if (locationParts == null) {
278
- return null;
279
- }
280
-
281
- const functionName = ''; // TODO: Parse this in the regexp.
282
- const [, , sourceURL, line, column] = locationParts;
283
- return {functionName, sourceURL, line, column};
284
-}
285
-
286
-const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
287
-function parseSourceFromChromeStack(
288
- stack: string,
289
-): ReactFunctionLocation | null {
290
- const frames = stack.split('\n');
291
- // eslint-disable-next-line no-for-of-loops/no-for-of-loops
292
- for (const frame of frames) {
293
- const sanitizedFrame = frame.trim();
294
-
295
- const locationInParenthesesMatch = sanitizedFrame.match(/ (\(.+\)$)/);
296
- const possibleLocation = locationInParenthesesMatch
297
- ? locationInParenthesesMatch[1]
298
- : sanitizedFrame;
299
-
300
- const location = extractLocation(possibleLocation);
301
- // Continue the search until at least sourceURL is found
302
- if (location == null) {
303
- continue;
304
- }
305
-
306
- const {functionName, sourceURL, line = '1', column = '1'} = location;
307
-
308
- return [
309
- functionName || '',
310
- sourceURL,
311
- parseInt(line, 10),
312
- parseInt(column, 10),
313
- ];
314
- }
315
-
316
- return null;
317
-}
318
-
319
-function parseSourceFromFirefoxStack(
320
- stack: string,
321
-): ReactFunctionLocation | null {
322
- const frames = stack.split('\n');
323
- // eslint-disable-next-line no-for-of-loops/no-for-of-loops
324
- for (const frame of frames) {
325
- const sanitizedFrame = frame.trim();
326
- const frameWithoutFunctionName = sanitizedFrame.replace(
327
- /((.*".+"[^@]*)?[^@]*)(?:@)/,
328
- '',
329
- );
330
-
331
- const location = extractLocation(frameWithoutFunctionName);
332
- // Continue the search until at least sourceURL is found
333
- if (location == null) {
334
- continue;
335
- }
336
-
337
- const {functionName, sourceURL, line = '1', column = '1'} = location;
338
-
339
- return [
340
- functionName || '',
341
- sourceURL,
342
- parseInt(line, 10),
343
- parseInt(column, 10),
344
- ];
345
- }
346
-
347
- return null;
348
-}
349
-
350
-export function parseSourceFromComponentStack(
351
- componentStack: string,
352
-): ReactFunctionLocation | null {
353
- if (componentStack.match(CHROME_STACK_REGEXP)) {
354
- return parseSourceFromChromeStack(componentStack);
355
- }
356
-
357
- return parseSourceFromFirefoxStack(componentStack);
358
-}
359
-
360
-let collectedLocation: ReactFunctionLocation | null = null;
361
-
362
-function collectStackTrace(
363
- error: Error,
364
- structuredStackTrace: CallSite[],
365
-): string {
366
- let result: null | ReactFunctionLocation = null;
367
- // Collect structured stack traces from the callsites.
368
- // We mirror how V8 serializes stack frames and how we later parse them.
369
- for (let i = 0; i < structuredStackTrace.length; i++) {
370
- const callSite = structuredStackTrace[i];
371
- const name = callSite.getFunctionName();
372
- if (
373
- name != null &&
374
- (name.includes('react_stack_bottom_frame') ||
375
- name.includes('react-stack-bottom-frame'))
376
- ) {
377
- // We pick the last frame that matches before the bottom frame since
378
- // that will be immediately inside the component as opposed to some helper.
379
- // If we don't find a bottom frame then we bail to string parsing.
380
- collectedLocation = result;
381
- // Skip everything after the bottom frame since it'll be internals.
382
- break;
383
- } else {
384
- const sourceURL = callSite.getScriptNameOrSourceURL();
385
- const line =
386
- // $FlowFixMe[prop-missing]
387
- typeof callSite.getEnclosingLineNumber === 'function'
388
- ? (callSite: any).getEnclosingLineNumber()
389
- : callSite.getLineNumber();
390
- const col =
391
- // $FlowFixMe[prop-missing]
392
- typeof callSite.getEnclosingColumnNumber === 'function'
393
- ? (callSite: any).getEnclosingColumnNumber()
394
- : callSite.getColumnNumber();
395
- if (!sourceURL || !line || !col) {
396
- // Skip eval etc. without source url. They don't have location.
397
- continue;
398
- }
399
- result = [name, sourceURL, line, col];
400
- }
401
- }
402
- // At the same time we generate a string stack trace just in case someone
403
- // else reads it.
404
- const name = error.name || 'Error';
405
- const message = error.message || '';
406
- let stack = name + ': ' + message;
407
- for (let i = 0; i < structuredStackTrace.length; i++) {
408
- stack += '\n at ' + structuredStackTrace[i].toString();
409
- }
410
- return stack;
411
-}
412
-
413
-export function parseSourceFromOwnerStack(
414
- error: Error,
415
-): ReactFunctionLocation | null {
416
- // First attempt to collected the structured data using prepareStackTrace.
417
- collectedLocation = null;
418
- const previousPrepare = Error.prepareStackTrace;
419
- Error.prepareStackTrace = collectStackTrace;
420
- let stack;
421
- try {
422
- stack = error.stack;
423
- } catch (e) {
424
- // $FlowFixMe[incompatible-type] It does accept undefined.
425
- Error.prepareStackTrace = undefined;
426
- stack = error.stack;
427
- } finally {
428
- Error.prepareStackTrace = previousPrepare;
429
- }
430
- if (collectedLocation !== null) {
431
- return collectedLocation;
432
- }
433
- if (stack == null) {
434
- return null;
435
- }
436
- // Fallback to parsing the string form.
437
- const componentStack = formatOwnerStackString(stack);
438
- return parseSourceFromComponentStack(componentStack);
439
-}
440
-
258
// 0.123456789 => 0.123
259
// Expects high-resolution timestamp in milliseconds, like from performance.now()
260
// Mainly used for optimizing the size of serialized profiling payload
packages/react-devtools-shared/src/backend/utils/parseStackTrace.js
new
+331
@@ -0,0 +1,331 @@
1
+/**
2
+/**
3
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ *
8
+ * @flow
9
+ */
10
+
11
+import type {ReactStackTrace, ReactFunctionLocation} from 'shared/ReactTypes';
12
+
13
+function parseStackTraceFromChromeStack(
14
+ stack: string,
15
+ skipFrames: number,
16
+): ReactStackTrace {
17
+ if (stack.startsWith('Error: react-stack-top-frame\n')) {
18
+ // V8's default formatting prefixes with the error message which we
19
+ // don't want/need.
20
+ stack = stack.slice(29);
21
+ }
22
+ let idx = stack.indexOf('react_stack_bottom_frame');
23
+ if (idx === -1) {
24
+ idx = stack.indexOf('react-stack-bottom-frame');
25
+ }
26
+ if (idx !== -1) {
27
+ idx = stack.lastIndexOf('\n', idx);
28
+ }
29
+ if (idx !== -1) {
30
+ // Cut off everything after the bottom frame since it'll be internals.
31
+ stack = stack.slice(0, idx);
32
+ }
33
+ const frames = stack.split('\n');
34
+ const parsedFrames: ReactStackTrace = [];
35
+ // We skip top frames here since they may or may not be parseable but we
36
+ // want to skip the same number of frames regardless. I.e. we can't do it
37
+ // in the caller.
38
+ for (let i = skipFrames; i < frames.length; i++) {
39
+ const parsed = chromeFrameRegExp.exec(frames[i]);
40
+ if (!parsed) {
41
+ continue;
42
+ }
43
+ let name = parsed[1] || '';
44
+ let isAsync = parsed[8] === 'async ';
45
+ if (name === '<anonymous>') {
46
+ name = '';
47
+ } else if (name.startsWith('async ')) {
48
+ name = name.slice(5);
49
+ isAsync = true;
50
+ }
51
+ let filename = parsed[2] || parsed[5] || '';
52
+ if (filename === '<anonymous>') {
53
+ filename = '';
54
+ }
55
+ const line = +(parsed[3] || parsed[6]);
56
+ const col = +(parsed[4] || parsed[7]);
57
+ parsedFrames.push([name, filename, line, col, 0, 0, isAsync]);
58
+ }
59
+ return parsedFrames;
60
+}
61
+
62
+const firefoxFrameRegExp = /^((?:.*".+")?[^@]*)@(.+):(\d+):(\d+)$/;
63
+function parseStackTraceFromFirefoxStack(
64
+ stack: string,
65
+ skipFrames: number,
66
+): ReactStackTrace {
67
+ let idx = stack.indexOf('react_stack_bottom_frame');
68
+ if (idx === -1) {
69
+ idx = stack.indexOf('react-stack-bottom-frame');
70
+ }
71
+ if (idx !== -1) {
72
+ idx = stack.lastIndexOf('\n', idx);
73
+ }
74
+ if (idx !== -1) {
75
+ // Cut off everything after the bottom frame since it'll be internals.
76
+ stack = stack.slice(0, idx);
77
+ }
78
+ const frames = stack.split('\n');
79
+ const parsedFrames: ReactStackTrace = [];
80
+ // We skip top frames here since they may or may not be parseable but we
81
+ // want to skip the same number of frames regardless. I.e. we can't do it
82
+ // in the caller.
83
+ for (let i = skipFrames; i < frames.length; i++) {
84
+ const parsed = firefoxFrameRegExp.exec(frames[i]);
85
+ if (!parsed) {
86
+ continue;
87
+ }
88
+ const name = parsed[1] || '';
89
+ const filename = parsed[2] || '';
90
+ const line = +parsed[3];
91
+ const col = +parsed[4];
92
+ parsedFrames.push([name, filename, line, col, 0, 0, false]);
93
+ }
94
+ return parsedFrames;
95
+}
96
+
97
+const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
98
+export function parseStackTraceFromString(
99
+ stack: string,
100
+ skipFrames: number,
101
+): ReactStackTrace {
102
+ if (stack.match(CHROME_STACK_REGEXP)) {
103
+ return parseStackTraceFromChromeStack(stack, skipFrames);
104
+ }
105
+ return parseStackTraceFromFirefoxStack(stack, skipFrames);
106
+}
107
+
108
+let framesToSkip: number = 0;
109
+let collectedStackTrace: null | ReactStackTrace = null;
110
+
111
+const identifierRegExp = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
112
+
113
+function getMethodCallName(callSite: CallSite): string {
114
+ const typeName = callSite.getTypeName();
115
+ const methodName = callSite.getMethodName();
116
+ const functionName = callSite.getFunctionName();
117
+ let result = '';
118
+ if (functionName) {
119
+ if (
120
+ typeName &&
121
+ identifierRegExp.test(functionName) &&
122
+ functionName !== typeName
123
+ ) {
124
+ result += typeName + '.';
125
+ }
126
+ result += functionName;
127
+ if (
128
+ methodName &&
129
+ functionName !== methodName &&
130
+ !functionName.endsWith('.' + methodName) &&
131
+ !functionName.endsWith(' ' + methodName)
132
+ ) {
133
+ result += ' [as ' + methodName + ']';
134
+ }
135
+ } else {
136
+ if (typeName) {
137
+ result += typeName + '.';
138
+ }
139
+ if (methodName) {
140
+ result += methodName;
141
+ } else {
142
+ result += '<anonymous>';
143
+ }
144
+ }
145
+ return result;
146
+}
147
+
148
+function collectStackTrace(
149
+ error: Error,
150
+ structuredStackTrace: CallSite[],
151
+): string {
152
+ const result: ReactStackTrace = [];
153
+ // Collect structured stack traces from the callsites.
154
+ // We mirror how V8 serializes stack frames and how we later parse them.
155
+ for (let i = framesToSkip; i < structuredStackTrace.length; i++) {
156
+ const callSite = structuredStackTrace[i];
157
+ let name = callSite.getFunctionName() || '<anonymous>';
158
+ if (
159
+ name.includes('react_stack_bottom_frame') ||
160
+ name.includes('react-stack-bottom-frame')
161
+ ) {
162
+ // Skip everything after the bottom frame since it'll be internals.
163
+ break;
164
+ } else if (callSite.isNative()) {
165
+ // $FlowFixMe[prop-missing]
166
+ const isAsync = callSite.isAsync();
167
+ result.push([name, '', 0, 0, 0, 0, isAsync]);
168
+ } else {
169
+ // We encode complex function calls as if they're part of the function
170
+ // name since we cannot simulate the complex ones and they look the same
171
+ // as function names in UIs on the client as well as stacks.
172
+ if (callSite.isConstructor()) {
173
+ name = 'new ' + name;
174
+ } else if (!callSite.isToplevel()) {
175
+ name = getMethodCallName(callSite);
176
+ }
177
+ if (name === '<anonymous>') {
178
+ name = '';
179
+ }
180
+ let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
181
+ if (filename === '<anonymous>') {
182
+ filename = '';
183
+ if (callSite.isEval()) {
184
+ const origin = callSite.getEvalOrigin();
185
+ if (origin) {
186
+ filename = origin.toString() + ', <anonymous>';
187
+ }
188
+ }
189
+ }
190
+ const line = callSite.getLineNumber() || 0;
191
+ const col = callSite.getColumnNumber() || 0;
192
+ const enclosingLine: number =
193
+ // $FlowFixMe[prop-missing]
194
+ typeof callSite.getEnclosingLineNumber === 'function'
195
+ ? (callSite: any).getEnclosingLineNumber() || 0
196
+ : 0;
197
+ const enclosingCol: number =
198
+ // $FlowFixMe[prop-missing]
199
+ typeof callSite.getEnclosingColumnNumber === 'function'
200
+ ? (callSite: any).getEnclosingColumnNumber() || 0
201
+ : 0;
202
+ // $FlowFixMe[prop-missing]
203
+ const isAsync = callSite.isAsync();
204
+ result.push([
205
+ name,
206
+ filename,
207
+ line,
208
+ col,
209
+ enclosingLine,
210
+ enclosingCol,
211
+ isAsync,
212
+ ]);
213
+ }
214
+ }
215
+ collectedStackTrace = result;
216
+
217
+ // At the same time we generate a string stack trace just in case someone
218
+ // else reads it. Ideally, we'd call the previous prepareStackTrace to
219
+ // ensure it's in the expected format but it's common for that to be
220
+ // source mapped and since we do a lot of eager parsing of errors, it
221
+ // would be slow in those environments. We could maybe just rely on those
222
+ // environments having to disable source mapping globally to speed things up.
223
+ // For now, we just generate a default V8 formatted stack trace without
224
+ // source mapping as a fallback.
225
+ const name = error.name || 'Error';
226
+ const message = error.message || '';
227
+ let stack = name + ': ' + message;
228
+ for (let i = 0; i < structuredStackTrace.length; i++) {
229
+ stack += '\n at ' + structuredStackTrace[i].toString();
230
+ }
231
+ return stack;
232
+}
233
+
234
+// This matches either of these V8 formats.
235
+// at name (filename:0:0)
236
+// at filename:0:0
237
+// at async filename:0:0
238
+const chromeFrameRegExp =
239
+ /^ *at (?:(.+) \((?:(.+):(\d+):(\d+)|\<anonymous\>)\)|(?:async )?(.+):(\d+):(\d+)|\<anonymous\>)$/;
240
+
241
+const stackTraceCache: WeakMap<Error, ReactStackTrace> = new WeakMap();
242
+
243
+export function parseStackTrace(
244
+ error: Error,
245
+ skipFrames: number,
246
+): ReactStackTrace {
247
+ // We can only get structured data out of error objects once. So we cache the information
248
+ // so we can get it again each time. It also helps performance when the same error is
249
+ // referenced more than once.
250
+ const existing = stackTraceCache.get(error);
251
+ if (existing !== undefined) {
252
+ return existing;
253
+ }
254
+ // We override Error.prepareStackTrace with our own version that collects
255
+ // the structured data. We need more information than the raw stack gives us
256
+ // and we need to ensure that we don't get the source mapped version.
257
+ collectedStackTrace = null;
258
+ framesToSkip = skipFrames;
259
+ const previousPrepare = Error.prepareStackTrace;
260
+ Error.prepareStackTrace = collectStackTrace;
261
+ let stack;
262
+ try {
263
+ stack = String(error.stack);
264
+ } finally {
265
+ Error.prepareStackTrace = previousPrepare;
266
+ }
267
+
268
+ if (collectedStackTrace !== null) {
269
+ const result = collectedStackTrace;
270
+ collectedStackTrace = null;
271
+ stackTraceCache.set(error, result);
272
+ return result;
273
+ }
274
+
275
+ // If the stack has already been read, or this is not actually a V8 compatible
276
+ // engine then we might not get a normalized stack and it might still have been
277
+ // source mapped. Regardless we try our best to parse it.
278
+
279
+ const parsedFrames = parseStackTraceFromString(stack, skipFrames);
280
+ stackTraceCache.set(error, parsedFrames);
281
+ return parsedFrames;
282
+}
283
+
284
+export function extractLocationFromOwnerStack(
285
+ error: Error,
286
+): ReactFunctionLocation | null {
287
+ const stackTrace = parseStackTrace(error, 0);
288
+ const stack = error.stack;
289
+ if (
290
+ !stack.includes('react_stack_bottom_frame') &&
291
+ !stack.includes('react-stack-bottom-frame')
292
+ ) {
293
+ // This didn't have a bottom to it, we can't trust it.
294
+ return null;
295
+ }
296
+ // We start from the bottom since that will have the best location for the owner itself.
297
+ for (let i = stackTrace.length - 1; i >= 0; i--) {
298
+ const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i];
299
+ // Take the first match with a colon in the file name.
300
+ if (fileName.indexOf(':') !== -1) {
301
+ return [
302
+ functionName,
303
+ fileName,
304
+ // Use enclosing line if available, since that points to the start of the function.
305
+ encLine || line,
306
+ encCol || col,
307
+ ];
308
+ }
309
+ }
310
+ return null;
311
+}
312
+
313
+export function extractLocationFromComponentStack(
314
+ stack: string,
315
+): ReactFunctionLocation | null {
316
+ const stackTrace = parseStackTraceFromString(stack, 0);
317
+ for (let i = 0; i < stackTrace.length; i++) {
318
+ const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i];
319
+ // Take the first match with a colon in the file name.
320
+ if (fileName.indexOf(':') !== -1) {
321
+ return [
322
+ functionName,
323
+ fileName,
324
+ // Use enclosing line if available. (Never the case here because we parse from string.)
325
+ encLine || line,
326
+ encCol || col,
327
+ ];
328
+ }
329
+ }
330
+ return null;
331
+}