bippy 0.5.27 → 0.5.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/dist/{core-BDWE7M7e.d.ts → core-Cjoce0EW.d.ts} +2 -2
- package/dist/core-DBBh-FTl.js +9 -0
- package/dist/{core-CEUgwvkw.d.cts → core-Y1ecSyti.d.cts} +2 -2
- package/dist/core-xjGqMMEY.cjs +9 -0
- package/dist/core.cjs +1 -1
- package/dist/core.d.cts +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.iife.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{install-hook-only-BOBPiBkc.js → install-hook-only-CRye53Kz.cjs} +1 -1
- package/dist/{install-hook-only-aVNdwrnj.cjs → install-hook-only-DfpahZZO.js} +1 -1
- package/dist/install-hook-only.cjs +1 -1
- package/dist/install-hook-only.iife.js +1 -1
- package/dist/install-hook-only.js +1 -1
- package/dist/rdt-hook-Czn6qzdx.js +9 -0
- package/dist/rdt-hook-DnMMBqZs.cjs +9 -0
- package/dist/source.cjs +7 -7
- package/dist/source.d.cts +1 -1
- package/dist/source.d.ts +1 -1
- package/dist/source.js +12 -12
- package/package.json +3 -1
- package/src/core.ts +1325 -0
- package/src/index.ts +3 -0
- package/src/install-hook-only.ts +3 -0
- package/src/rdt-hook.ts +237 -0
- package/src/source/constants.ts +26 -0
- package/src/source/get-display-name-from-source.ts +103 -0
- package/src/source/get-source.ts +218 -0
- package/src/source/index.ts +6 -0
- package/src/source/owner-stack.ts +598 -0
- package/src/source/parse-stack.ts +295 -0
- package/src/source/symbolication.ts +410 -0
- package/src/source/types.ts +6 -0
- package/src/types.ts +244 -0
- package/dist/core-D8j-0_U5.cjs +0 -9
- package/dist/core-coQbWNwP.js +0 -9
- package/dist/rdt-hook-3SlCAu5p.cjs +0 -9
- package/dist/rdt-hook-BZMdLD7S.js +0 -9
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import {
|
|
2
|
+
_renderers,
|
|
3
|
+
ActivityComponentTag,
|
|
4
|
+
ClassComponentTag,
|
|
5
|
+
Fiber,
|
|
6
|
+
ForwardRefTag,
|
|
7
|
+
FunctionComponentTag,
|
|
8
|
+
getRDTHook,
|
|
9
|
+
HostComponentTag,
|
|
10
|
+
HostHoistableTag,
|
|
11
|
+
HostSingletonTag,
|
|
12
|
+
LazyComponentTag,
|
|
13
|
+
SimpleMemoComponentTag,
|
|
14
|
+
SuspenseComponentTag,
|
|
15
|
+
SuspenseListComponentTag,
|
|
16
|
+
ViewTransitionComponentTag,
|
|
17
|
+
getDisplayName,
|
|
18
|
+
traverseFiber,
|
|
19
|
+
} from '../core.js';
|
|
20
|
+
import { SERVER_FRAME_MARKER } from './constants.js';
|
|
21
|
+
|
|
22
|
+
import { parseStack, StackFrame } from './parse-stack.js';
|
|
23
|
+
import { symbolicateStack } from './symbolication.js';
|
|
24
|
+
|
|
25
|
+
export const hasDebugStack = (
|
|
26
|
+
fiber: Fiber,
|
|
27
|
+
): fiber is Fiber & {
|
|
28
|
+
_debugStack: NonNullable<Fiber['_debugStack']>;
|
|
29
|
+
} => {
|
|
30
|
+
return (
|
|
31
|
+
fiber._debugStack instanceof Error &&
|
|
32
|
+
typeof fiber._debugStack?.stack === 'string'
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getCurrentDispatcher = (): null | React.RefObject<unknown> => {
|
|
37
|
+
const rdtHook = getRDTHook();
|
|
38
|
+
for (const renderer of [
|
|
39
|
+
...Array.from(_renderers),
|
|
40
|
+
...Array.from(rdtHook.renderers.values()),
|
|
41
|
+
]) {
|
|
42
|
+
const currentDispatcherRef = renderer.currentDispatcherRef;
|
|
43
|
+
if (currentDispatcherRef && typeof currentDispatcherRef === 'object') {
|
|
44
|
+
return 'H' in currentDispatcherRef
|
|
45
|
+
? currentDispatcherRef.H
|
|
46
|
+
: currentDispatcherRef.current;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const setCurrentDispatcher = (value: null | React.RefObject<unknown>): void => {
|
|
53
|
+
for (const renderer of _renderers) {
|
|
54
|
+
const currentDispatcherRef = renderer.currentDispatcherRef;
|
|
55
|
+
if (currentDispatcherRef && typeof currentDispatcherRef === 'object') {
|
|
56
|
+
if ('H' in currentDispatcherRef) {
|
|
57
|
+
currentDispatcherRef.H = value;
|
|
58
|
+
} else {
|
|
59
|
+
currentDispatcherRef.current = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const describeBuiltInComponentFrame = (name: string): string => {
|
|
66
|
+
return `\n in ${name}`;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const describeDebugInfoFrame = (name: string, env?: string): string => {
|
|
70
|
+
let frameDescription = describeBuiltInComponentFrame(name);
|
|
71
|
+
if (env) {
|
|
72
|
+
frameDescription += ` (at ${env})`;
|
|
73
|
+
}
|
|
74
|
+
return frameDescription;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let reEntry = false;
|
|
78
|
+
|
|
79
|
+
// https://github.com/facebook/react/blob/f739642745577a8e4dcb9753836ac3589b9c590a/packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js#L22
|
|
80
|
+
const describeNativeComponentFrame = (
|
|
81
|
+
component: React.ComponentType<unknown>,
|
|
82
|
+
construct: boolean,
|
|
83
|
+
): string => {
|
|
84
|
+
if (!component || reEntry) {
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const previousPrepareStackTrace = Error.prepareStackTrace;
|
|
89
|
+
Error.prepareStackTrace = undefined;
|
|
90
|
+
reEntry = true;
|
|
91
|
+
|
|
92
|
+
const previousDispatcher = getCurrentDispatcher();
|
|
93
|
+
setCurrentDispatcher(null);
|
|
94
|
+
const previousConsoleError = console.error;
|
|
95
|
+
const previousConsoleWarn = console.warn;
|
|
96
|
+
console.error = () => {};
|
|
97
|
+
console.warn = () => {};
|
|
98
|
+
try {
|
|
99
|
+
/**
|
|
100
|
+
* Finding a common stack frame between sample and control errors can be
|
|
101
|
+
* tricky given the different types and levels of stack trace truncation from
|
|
102
|
+
* different JS VMs. So instead we'll attempt to control what that common
|
|
103
|
+
* frame should be through this object method:
|
|
104
|
+
* Having both the sample and control errors be in the function under the
|
|
105
|
+
* `DescribeNativeComponentFrameRoot` property, + setting the `name` and
|
|
106
|
+
* `displayName` properties of the function ensures that a stack
|
|
107
|
+
* frame exists that has the method name `DescribeNativeComponentFrameRoot` in
|
|
108
|
+
* it for both control and sample stacks.
|
|
109
|
+
*/
|
|
110
|
+
const RunInRootFrame = {
|
|
111
|
+
DetermineComponentFrameRoot() {
|
|
112
|
+
let control: unknown;
|
|
113
|
+
try {
|
|
114
|
+
// This should throw.
|
|
115
|
+
if (construct) {
|
|
116
|
+
// Something should be setting the props in the constructor.
|
|
117
|
+
const ThrowingConstructor = function () {
|
|
118
|
+
throw Error();
|
|
119
|
+
};
|
|
120
|
+
Object.defineProperty(ThrowingConstructor.prototype, 'props', {
|
|
121
|
+
set: function () {
|
|
122
|
+
// We use a throwing setter instead of frozen or non-writable props
|
|
123
|
+
// because that won't throw in a non-strict mode function.
|
|
124
|
+
throw Error();
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
if (typeof Reflect === 'object' && Reflect.construct) {
|
|
128
|
+
// We construct a different control for this case to include any extra
|
|
129
|
+
// frames added by the construct call.
|
|
130
|
+
try {
|
|
131
|
+
Reflect.construct(ThrowingConstructor, []);
|
|
132
|
+
} catch (caughtError) {
|
|
133
|
+
control = caughtError;
|
|
134
|
+
}
|
|
135
|
+
Reflect.construct(component, [], ThrowingConstructor);
|
|
136
|
+
} else {
|
|
137
|
+
try {
|
|
138
|
+
// @ts-expect-error -- ThrowingConstructor is a constructor function
|
|
139
|
+
ThrowingConstructor.call();
|
|
140
|
+
} catch (caughtError) {
|
|
141
|
+
control = caughtError;
|
|
142
|
+
}
|
|
143
|
+
// @ts-expect-error -- ThrowingConstructor is a constructor function
|
|
144
|
+
component.call(ThrowingConstructor.prototype);
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
try {
|
|
148
|
+
throw Error();
|
|
149
|
+
} catch (caughtError) {
|
|
150
|
+
control = caughtError;
|
|
151
|
+
}
|
|
152
|
+
// TODO(luna): This will currently only throw if the function component
|
|
153
|
+
// tries to access React/ReactDOM/props. We should probably make this throw
|
|
154
|
+
// in simple components too
|
|
155
|
+
const maybePromise = (component as () => Promise<unknown>)();
|
|
156
|
+
|
|
157
|
+
// If the function component returns a promise, it's likely an async
|
|
158
|
+
// component, which we don't yet support. Attach a noop catch handler to
|
|
159
|
+
// silence the error.
|
|
160
|
+
// TODO: Implement component stacks for async client components?
|
|
161
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- we literally check if this is a promise here
|
|
162
|
+
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
163
|
+
maybePromise.catch(() => {});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (sample: unknown) {
|
|
167
|
+
// This is inlined manually because closure doesn't do it for us.
|
|
168
|
+
if (
|
|
169
|
+
sample instanceof Error &&
|
|
170
|
+
control instanceof Error &&
|
|
171
|
+
typeof sample.stack === 'string'
|
|
172
|
+
) {
|
|
173
|
+
return [sample.stack, control.stack];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return [null, null];
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// @ts-expect-error --- displayName is not a property of the function
|
|
181
|
+
RunInRootFrame.DetermineComponentFrameRoot.displayName =
|
|
182
|
+
'DetermineComponentFrameRoot';
|
|
183
|
+
const namePropDescriptor = Object.getOwnPropertyDescriptor(
|
|
184
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
185
|
+
RunInRootFrame.DetermineComponentFrameRoot,
|
|
186
|
+
'name',
|
|
187
|
+
);
|
|
188
|
+
// Before ES6, the `name` property was not configurable.
|
|
189
|
+
if (namePropDescriptor?.configurable) {
|
|
190
|
+
// V8 utilizes a function's `name` property when generating a stack trace.
|
|
191
|
+
Object.defineProperty(
|
|
192
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
193
|
+
RunInRootFrame.DetermineComponentFrameRoot,
|
|
194
|
+
// Configurable properties can be updated even if its writable descriptor
|
|
195
|
+
// is set to `false`.
|
|
196
|
+
'name',
|
|
197
|
+
{ value: 'DetermineComponentFrameRoot' },
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const [sampleStack, controlStack] =
|
|
202
|
+
RunInRootFrame.DetermineComponentFrameRoot();
|
|
203
|
+
if (sampleStack && controlStack) {
|
|
204
|
+
// This extracts the first frame from the sample that isn't also in the control.
|
|
205
|
+
// Skipping one frame that we assume is the frame that calls the two.
|
|
206
|
+
const sampleLines = sampleStack.split('\n');
|
|
207
|
+
const controlLines = controlStack.split('\n');
|
|
208
|
+
let sampleIndex = 0;
|
|
209
|
+
let controlIndex = 0;
|
|
210
|
+
while (
|
|
211
|
+
sampleIndex < sampleLines.length &&
|
|
212
|
+
!sampleLines[sampleIndex].includes('DetermineComponentFrameRoot')
|
|
213
|
+
) {
|
|
214
|
+
sampleIndex++;
|
|
215
|
+
}
|
|
216
|
+
while (
|
|
217
|
+
controlIndex < controlLines.length &&
|
|
218
|
+
!controlLines[controlIndex].includes('DetermineComponentFrameRoot')
|
|
219
|
+
) {
|
|
220
|
+
controlIndex++;
|
|
221
|
+
}
|
|
222
|
+
// We couldn't find our intentionally injected common root frame, attempt
|
|
223
|
+
// to find another common root frame by search from the bottom of the
|
|
224
|
+
// control stack...
|
|
225
|
+
if (
|
|
226
|
+
sampleIndex === sampleLines.length ||
|
|
227
|
+
controlIndex === controlLines.length
|
|
228
|
+
) {
|
|
229
|
+
sampleIndex = sampleLines.length - 1;
|
|
230
|
+
controlIndex = controlLines.length - 1;
|
|
231
|
+
while (
|
|
232
|
+
sampleIndex >= 1 &&
|
|
233
|
+
controlIndex >= 0 &&
|
|
234
|
+
sampleLines[sampleIndex] !== controlLines[controlIndex]
|
|
235
|
+
) {
|
|
236
|
+
// We expect at least one stack frame to be shared.
|
|
237
|
+
// Typically this will be the root most one. However, stack frames may be
|
|
238
|
+
// cut off due to maximum stack limits. In this case, one maybe cut off
|
|
239
|
+
// earlier than the other. We assume that the sample is longer or the same
|
|
240
|
+
// and there for cut off earlier. So we should find the root most frame in
|
|
241
|
+
// the sample somewhere in the control.
|
|
242
|
+
controlIndex--;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
for (
|
|
246
|
+
;
|
|
247
|
+
sampleIndex >= 1 && controlIndex >= 0;
|
|
248
|
+
sampleIndex--, controlIndex--
|
|
249
|
+
) {
|
|
250
|
+
// Next we find the first one that isn't the same which should be the
|
|
251
|
+
// frame that called our sample function and the control.
|
|
252
|
+
if (sampleLines[sampleIndex] !== controlLines[controlIndex]) {
|
|
253
|
+
// In V8, the first line is describing the message but other VMs don't.
|
|
254
|
+
// If we're about to return the first line, and the control is also on the same
|
|
255
|
+
// line, that's a pretty good indicator that our sample threw at same line as
|
|
256
|
+
// the control. I.e. before we entered the sample frame. So we ignore this result.
|
|
257
|
+
// This can happen if you passed a class to function component, or non-function.
|
|
258
|
+
if (sampleIndex !== 1 || controlIndex !== 1) {
|
|
259
|
+
do {
|
|
260
|
+
sampleIndex--;
|
|
261
|
+
controlIndex--;
|
|
262
|
+
// We may still have similar intermediate frames from the construct call.
|
|
263
|
+
// The next one that isn't the same should be our match though.
|
|
264
|
+
if (
|
|
265
|
+
controlIndex < 0 ||
|
|
266
|
+
sampleLines[sampleIndex] !== controlLines[controlIndex]
|
|
267
|
+
) {
|
|
268
|
+
// V8 adds a "new" prefix for native classes. Let's remove it to make it prettier.
|
|
269
|
+
let stackFrame = `\n${sampleLines[sampleIndex].replace(
|
|
270
|
+
' at new ',
|
|
271
|
+
' at ',
|
|
272
|
+
)}`;
|
|
273
|
+
|
|
274
|
+
const displayName = getDisplayName(component);
|
|
275
|
+
// If our component frame is labeled "<anonymous>"
|
|
276
|
+
// but we have a user-provided "displayName"
|
|
277
|
+
// splice it in to make the stack more readable.
|
|
278
|
+
if (displayName && stackFrame.includes('<anonymous>')) {
|
|
279
|
+
stackFrame = stackFrame.replace('<anonymous>', displayName);
|
|
280
|
+
}
|
|
281
|
+
// Return the line we found.
|
|
282
|
+
return stackFrame;
|
|
283
|
+
}
|
|
284
|
+
} while (sampleIndex >= 1 && controlIndex >= 0);
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} finally {
|
|
291
|
+
reEntry = false;
|
|
292
|
+
|
|
293
|
+
Error.prepareStackTrace = previousPrepareStackTrace;
|
|
294
|
+
|
|
295
|
+
setCurrentDispatcher(previousDispatcher);
|
|
296
|
+
console.error = previousConsoleError;
|
|
297
|
+
console.warn = previousConsoleWarn;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const componentName = component ? getDisplayName(component) : '';
|
|
301
|
+
const syntheticFrame = componentName
|
|
302
|
+
? describeBuiltInComponentFrame(componentName)
|
|
303
|
+
: '';
|
|
304
|
+
return syntheticFrame;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// https://github.com/facebook/react/blob/ac3e705a18696168acfcaed39dce0cfaa6be8836/packages/react-reconciler/src/ReactFiberComponentStack.js#L180
|
|
308
|
+
export const describeFiber = (
|
|
309
|
+
fiber: Fiber,
|
|
310
|
+
childFiber: Fiber | null,
|
|
311
|
+
): string => {
|
|
312
|
+
const tag = fiber.tag as number;
|
|
313
|
+
let stackFrame = '';
|
|
314
|
+
switch (tag) {
|
|
315
|
+
case ActivityComponentTag:
|
|
316
|
+
stackFrame = describeBuiltInComponentFrame('Activity');
|
|
317
|
+
break;
|
|
318
|
+
case ClassComponentTag:
|
|
319
|
+
stackFrame = describeNativeComponentFrame(fiber.type, true);
|
|
320
|
+
break;
|
|
321
|
+
case ForwardRefTag:
|
|
322
|
+
stackFrame = describeNativeComponentFrame(
|
|
323
|
+
(fiber.type as { render: React.ComponentType<unknown> }).render,
|
|
324
|
+
false,
|
|
325
|
+
);
|
|
326
|
+
break;
|
|
327
|
+
case FunctionComponentTag:
|
|
328
|
+
case SimpleMemoComponentTag:
|
|
329
|
+
stackFrame = describeNativeComponentFrame(fiber.type, false);
|
|
330
|
+
break;
|
|
331
|
+
case HostComponentTag:
|
|
332
|
+
case HostHoistableTag:
|
|
333
|
+
case HostSingletonTag:
|
|
334
|
+
stackFrame = describeBuiltInComponentFrame(fiber.type as string);
|
|
335
|
+
break;
|
|
336
|
+
case LazyComponentTag:
|
|
337
|
+
// TODO: When we support Thenables as component types we should rename this.
|
|
338
|
+
stackFrame = describeBuiltInComponentFrame('Lazy');
|
|
339
|
+
break;
|
|
340
|
+
case SuspenseComponentTag:
|
|
341
|
+
if (fiber.child !== childFiber && childFiber !== null) {
|
|
342
|
+
// If we came from the second Fiber then we're in the Suspense Fallback.
|
|
343
|
+
stackFrame = describeBuiltInComponentFrame('Suspense Fallback');
|
|
344
|
+
} else {
|
|
345
|
+
stackFrame = describeBuiltInComponentFrame('Suspense');
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
case SuspenseListComponentTag:
|
|
349
|
+
stackFrame = describeBuiltInComponentFrame('SuspenseList');
|
|
350
|
+
break;
|
|
351
|
+
case ViewTransitionComponentTag:
|
|
352
|
+
// Note: enableViewTransition feature flag is not available in this codebase,
|
|
353
|
+
// so we'll always include ViewTransition
|
|
354
|
+
stackFrame = describeBuiltInComponentFrame('ViewTransition');
|
|
355
|
+
break;
|
|
356
|
+
default:
|
|
357
|
+
return '';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return stackFrame;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* react 19 introduces the _debugStack property, which we can use to grab the stack.
|
|
365
|
+
* however, for versions that don't have this property, we need to construct
|
|
366
|
+
* a "fake" version of the owner stack
|
|
367
|
+
*/
|
|
368
|
+
export const getFallbackOwnerStack = (thisFiber: Fiber): string => {
|
|
369
|
+
try {
|
|
370
|
+
let componentStack = '';
|
|
371
|
+
let currentFiber: Fiber | null = thisFiber;
|
|
372
|
+
let previousFiber: Fiber | null = null;
|
|
373
|
+
do {
|
|
374
|
+
componentStack += describeFiber(currentFiber, previousFiber);
|
|
375
|
+
|
|
376
|
+
// Add any Server Component stack frames in reverse order (dev only).
|
|
377
|
+
// Since we don't have __DEV__ in this codebase, we'll check for _debugInfo
|
|
378
|
+
const debugInfo = currentFiber._debugInfo;
|
|
379
|
+
if (debugInfo && Array.isArray(debugInfo)) {
|
|
380
|
+
for (let i = debugInfo.length - 1; i >= 0; i--) {
|
|
381
|
+
const debugEntry = debugInfo[i];
|
|
382
|
+
if (typeof debugEntry.name === 'string') {
|
|
383
|
+
componentStack += describeDebugInfoFrame(
|
|
384
|
+
debugEntry.name,
|
|
385
|
+
debugEntry.env,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
previousFiber = currentFiber;
|
|
392
|
+
currentFiber = currentFiber.return;
|
|
393
|
+
} while (currentFiber);
|
|
394
|
+
return componentStack;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error instanceof Error) {
|
|
397
|
+
return `\nError generating stack: ${error.message}\n${error.stack}`;
|
|
398
|
+
}
|
|
399
|
+
return '';
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* takes Error.stack and formats it to only the React owner stack
|
|
405
|
+
*
|
|
406
|
+
* before:
|
|
407
|
+
* ```
|
|
408
|
+
* Error: react-stack-top-frame
|
|
409
|
+
* at fakeJSXCallSite (http://localhost:3000/_next/static/chunks/<chunk-name>._.js:17665:16)
|
|
410
|
+
* at TodoItem (rsc://React/Server/file:///path/to/project/.next/server/chunks/ssr/<chunk-name>._.js)
|
|
411
|
+
* at react-stack-bottom-frame (http://localhost:3000/_next/static/chunks/<chunk-name>._.js:17984:89)
|
|
412
|
+
* ```
|
|
413
|
+
*
|
|
414
|
+
* after:
|
|
415
|
+
* ```
|
|
416
|
+
* at TodoItem (rsc://React/Server/file:///path/to/project/.next/server/chunks/ssr/<chunk-name>._.js)
|
|
417
|
+
* ```
|
|
418
|
+
*
|
|
419
|
+
* @see https://github.com/facebook/react/blob/main/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js#L12
|
|
420
|
+
*/
|
|
421
|
+
export const formatOwnerStack = (stack: string): string => {
|
|
422
|
+
const prevPrepareStackTrace = Error.prepareStackTrace;
|
|
423
|
+
Error.prepareStackTrace = undefined;
|
|
424
|
+
let formattedStack = stack;
|
|
425
|
+
if (!formattedStack) {
|
|
426
|
+
return '';
|
|
427
|
+
}
|
|
428
|
+
Error.prepareStackTrace = prevPrepareStackTrace;
|
|
429
|
+
|
|
430
|
+
if (formattedStack.startsWith('Error: react-stack-top-frame\n')) {
|
|
431
|
+
// V8's default formatting prefixes with the error message which we
|
|
432
|
+
// don't want/need
|
|
433
|
+
formattedStack = formattedStack.slice(29);
|
|
434
|
+
}
|
|
435
|
+
let idx = formattedStack.indexOf('\n');
|
|
436
|
+
if (idx !== -1) {
|
|
437
|
+
// pop the JSX frame
|
|
438
|
+
formattedStack = formattedStack.slice(idx + 1);
|
|
439
|
+
}
|
|
440
|
+
idx = Math.max(
|
|
441
|
+
formattedStack.indexOf('react_stack_bottom_frame'),
|
|
442
|
+
formattedStack.indexOf('react-stack-bottom-frame'),
|
|
443
|
+
);
|
|
444
|
+
if (idx !== -1) {
|
|
445
|
+
idx = formattedStack.lastIndexOf('\n', idx);
|
|
446
|
+
}
|
|
447
|
+
if (idx !== -1) {
|
|
448
|
+
// cut off everything after the bottom frame since it'll be internals.
|
|
449
|
+
formattedStack = formattedStack.slice(0, idx);
|
|
450
|
+
} else {
|
|
451
|
+
// we didn't find any internal callsite out to user space.
|
|
452
|
+
// This means that this was called outside an owner or the owner is fully internal.
|
|
453
|
+
// to keep things light we exclude the entire trace in this case.
|
|
454
|
+
return '';
|
|
455
|
+
}
|
|
456
|
+
return formattedStack;
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
interface OwnerStackEntry {
|
|
460
|
+
componentName: string;
|
|
461
|
+
stackFrames: StackFrame[];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const isReactServerComponentFrame = (stackFrame: StackFrame): boolean =>
|
|
465
|
+
Boolean(stackFrame.fileName?.startsWith('rsc://') && stackFrame.functionName);
|
|
466
|
+
|
|
467
|
+
const areStackFramesEqual = (
|
|
468
|
+
firstFrame: StackFrame,
|
|
469
|
+
secondFrame: StackFrame,
|
|
470
|
+
): boolean =>
|
|
471
|
+
firstFrame.fileName === secondFrame.fileName &&
|
|
472
|
+
firstFrame.lineNumber === secondFrame.lineNumber &&
|
|
473
|
+
firstFrame.columnNumber === secondFrame.columnNumber;
|
|
474
|
+
|
|
475
|
+
const buildFunctionNameToRscFramesMap = (
|
|
476
|
+
ownerStackEntries: OwnerStackEntry[],
|
|
477
|
+
): Map<string, StackFrame[]> => {
|
|
478
|
+
const functionNameToRscFrames = new Map<string, StackFrame[]>();
|
|
479
|
+
|
|
480
|
+
for (const ownerEntry of ownerStackEntries) {
|
|
481
|
+
for (const stackFrame of ownerEntry.stackFrames) {
|
|
482
|
+
if (!isReactServerComponentFrame(stackFrame)) continue;
|
|
483
|
+
|
|
484
|
+
const functionName = stackFrame.functionName!;
|
|
485
|
+
const framesForFunction = functionNameToRscFrames.get(functionName) ?? [];
|
|
486
|
+
const isDuplicateFrame = framesForFunction.some((existingFrame) =>
|
|
487
|
+
areStackFramesEqual(existingFrame, stackFrame),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
if (!isDuplicateFrame) {
|
|
491
|
+
framesForFunction.push(stackFrame);
|
|
492
|
+
functionNameToRscFrames.set(functionName, framesForFunction);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return functionNameToRscFrames;
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const getEnrichedServerStackFrame = (
|
|
501
|
+
serverFrame: StackFrame,
|
|
502
|
+
functionNameToRscFrames: Map<string, StackFrame[]>,
|
|
503
|
+
functionNameToUsageIndex: Map<string, number>,
|
|
504
|
+
): StackFrame => {
|
|
505
|
+
if (!serverFrame.functionName) {
|
|
506
|
+
return { ...serverFrame, isServer: true };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const availableRscFrames = functionNameToRscFrames.get(
|
|
510
|
+
serverFrame.functionName,
|
|
511
|
+
);
|
|
512
|
+
if (!availableRscFrames || availableRscFrames.length === 0) {
|
|
513
|
+
return { ...serverFrame, isServer: true };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const currentUsageIndex =
|
|
517
|
+
functionNameToUsageIndex.get(serverFrame.functionName) ?? 0;
|
|
518
|
+
const resolvedRscFrame =
|
|
519
|
+
availableRscFrames[currentUsageIndex % availableRscFrames.length];
|
|
520
|
+
functionNameToUsageIndex.set(serverFrame.functionName, currentUsageIndex + 1);
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
...serverFrame,
|
|
524
|
+
isServer: true,
|
|
525
|
+
fileName: resolvedRscFrame.fileName,
|
|
526
|
+
lineNumber: resolvedRscFrame.lineNumber,
|
|
527
|
+
columnNumber: resolvedRscFrame.columnNumber,
|
|
528
|
+
source: serverFrame.source?.replace(
|
|
529
|
+
SERVER_FRAME_MARKER,
|
|
530
|
+
`(${resolvedRscFrame.fileName}:${resolvedRscFrame.lineNumber}:${resolvedRscFrame.columnNumber})`,
|
|
531
|
+
),
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const getOwnerStackEntries = (rootFiber: Fiber): OwnerStackEntry[] => {
|
|
536
|
+
const ownerStackEntries: OwnerStackEntry[] = [];
|
|
537
|
+
|
|
538
|
+
traverseFiber(
|
|
539
|
+
rootFiber,
|
|
540
|
+
(currentFiber) => {
|
|
541
|
+
if (!hasDebugStack(currentFiber)) return;
|
|
542
|
+
|
|
543
|
+
const componentName =
|
|
544
|
+
typeof currentFiber.type !== 'string'
|
|
545
|
+
? getDisplayName(currentFiber.type) || '<anonymous>'
|
|
546
|
+
: currentFiber.type;
|
|
547
|
+
|
|
548
|
+
ownerStackEntries.push({
|
|
549
|
+
componentName,
|
|
550
|
+
stackFrames: parseStack(
|
|
551
|
+
formatOwnerStack(currentFiber._debugStack?.stack),
|
|
552
|
+
),
|
|
553
|
+
});
|
|
554
|
+
},
|
|
555
|
+
true,
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
return ownerStackEntries;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
export const getOwnerStack = async (
|
|
562
|
+
fiber: Fiber,
|
|
563
|
+
shouldCache = true,
|
|
564
|
+
fetchFunction?: (url: string) => Promise<Response>,
|
|
565
|
+
): Promise<StackFrame[]> => {
|
|
566
|
+
const ownerStackEntries = getOwnerStackEntries(fiber);
|
|
567
|
+
const fallbackStackFrames = parseStack(getFallbackOwnerStack(fiber));
|
|
568
|
+
const functionNameToRscFrames =
|
|
569
|
+
buildFunctionNameToRscFramesMap(ownerStackEntries);
|
|
570
|
+
const functionNameToUsageIndex = new Map<string, number>();
|
|
571
|
+
|
|
572
|
+
const enrichedStackFrames = fallbackStackFrames.map(
|
|
573
|
+
(stackFrame): StackFrame => {
|
|
574
|
+
const isServerFrame =
|
|
575
|
+
stackFrame.source?.includes(SERVER_FRAME_MARKER) ?? false;
|
|
576
|
+
|
|
577
|
+
if (isServerFrame) {
|
|
578
|
+
return getEnrichedServerStackFrame(
|
|
579
|
+
stackFrame,
|
|
580
|
+
functionNameToRscFrames,
|
|
581
|
+
functionNameToUsageIndex,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return stackFrame;
|
|
586
|
+
},
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
const deduplicatedStackFrames = enrichedStackFrames.filter(
|
|
590
|
+
(stackFrame, index, frames) => {
|
|
591
|
+
if (index === 0) return true;
|
|
592
|
+
const previousFrame = frames[index - 1];
|
|
593
|
+
return stackFrame.functionName !== previousFrame.functionName;
|
|
594
|
+
},
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
return symbolicateStack(deduplicatedStackFrames, shouldCache, fetchFunction);
|
|
598
|
+
};
|