bippy 0.6.1 → 0.7.0-dev.697d535

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.
Files changed (67) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +223 -463
  3. package/dist/core.cjs +1 -1
  4. package/dist/core.js +1 -1
  5. package/dist/errors.d.cts +410 -0
  6. package/dist/errors.d.ts +410 -0
  7. package/dist/index.cjs +1 -1
  8. package/dist/index.d.cts +153 -3
  9. package/dist/index.d.ts +153 -3
  10. package/dist/index.js +1 -1
  11. package/dist/install-hook-only.cjs +1 -1
  12. package/dist/install-hook-only.d.cts +9 -1
  13. package/dist/install-hook-only.d.ts +9 -1
  14. package/dist/install-hook-only.js +1 -1
  15. package/dist/rdt-hook.cjs +1 -1
  16. package/dist/rdt-hook.js +1 -1
  17. package/dist/source.cjs +14 -5
  18. package/dist/source.d.cts +86 -69
  19. package/dist/source.d.ts +86 -69
  20. package/dist/source.js +14 -5
  21. package/package.json +26 -27
  22. package/src/core.ts +353 -685
  23. package/src/errors.ts +34 -0
  24. package/src/index.ts +1 -0
  25. package/src/install-hook-only.ts +2 -2
  26. package/src/rdt-hook.ts +160 -161
  27. package/src/react-internals/generated/react-work-tags.ts +318 -0
  28. package/src/react-internals/index.ts +67 -0
  29. package/src/react-internals/semver.ts +80 -0
  30. package/src/react-internals/types.ts +186 -0
  31. package/src/react.ts +76 -0
  32. package/src/source/constants.ts +1 -1
  33. package/src/source/error-stack.ts +11 -0
  34. package/src/source/get-display-name-from-source.ts +44 -40
  35. package/src/source/get-source.ts +43 -26
  36. package/src/source/index.ts +11 -1
  37. package/src/source/inspect-hooks.ts +220 -207
  38. package/src/source/owner-stack.ts +119 -174
  39. package/src/source/parse-debug-stack.ts +4 -4
  40. package/src/source/parse-hook-names.ts +14 -53
  41. package/src/source/parse-stack.ts +14 -31
  42. package/src/source/renderer-dispatchers.ts +30 -0
  43. package/src/source/symbolication.ts +709 -120
  44. package/dist/core.d.cts +0 -214
  45. package/dist/core.d.ts +0 -214
  46. package/dist/core2.d.cts +0 -3
  47. package/dist/core2.d.ts +0 -3
  48. package/dist/get-source.cjs +0 -19
  49. package/dist/get-source.js +0 -19
  50. package/dist/index.iife.js +0 -9
  51. package/dist/install-hook-only.iife.js +0 -9
  52. package/dist/react-refresh.cjs +0 -9
  53. package/dist/react-refresh.d.cts +0 -66
  54. package/dist/react-refresh.d.ts +0 -66
  55. package/dist/react-refresh.js +0 -9
  56. package/dist/unsubscribe.d.cts +0 -298
  57. package/dist/unsubscribe.d.ts +0 -298
  58. package/src/react-refresh/constants.ts +0 -9
  59. package/src/react-refresh/detect-hmr-transport.ts +0 -33
  60. package/src/react-refresh/index.ts +0 -173
  61. package/src/react-refresh/metro-hmr-transport.ts +0 -188
  62. package/src/react-refresh/next-webpack-hmr-transport.ts +0 -72
  63. package/src/react-refresh/normalize-hmr-file-path.ts +0 -24
  64. package/src/react-refresh/types.ts +0 -7
  65. package/src/react-refresh/vite-hmr-transport.ts +0 -116
  66. package/src/types.ts +0 -438
  67. package/src/unsubscribe.ts +0 -17
@@ -1,32 +1,30 @@
1
- import {
2
- _renderers,
3
- ActivityComponentTag,
4
- ClassComponentTag,
1
+ import { getDisplayName, getType, traverseFiber } from "../core.js";
2
+ import { getReactWorkTagsForFiber } from "../react-internals/index.js";
3
+ import type {
5
4
  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 { ServerComponentInfo } from "../types.js";
5
+ RendererDispatcherRef,
6
+ ServerComponentInfo,
7
+ } from "../react-internals/index.js";
21
8
  import {
9
+ REACT_STACK_BOTTOM_FRAME_PATTERNS,
22
10
  SERVER_FRAME_MARKER,
23
11
  SERVER_ENV_PATTERN,
24
12
  SERVER_COMPONENT_URL_PREFIXES,
25
13
  } from "./constants.js";
26
-
14
+ import { getPrepareStackTrace, setPrepareStackTrace } from "./error-stack.js";
27
15
  import { parseDebugStack } from "./parse-debug-stack.js";
28
- import { parseStack, StackFrame } from "./parse-stack.js";
29
- import { symbolicateStack } from "./symbolication.js";
16
+ import { parseStack, type StackFrame } from "./parse-stack.js";
17
+ import {
18
+ getRendererDispatcherRefs,
19
+ readDispatcher,
20
+ writeDispatcher,
21
+ } from "./renderer-dispatchers.js";
22
+ import { symbolicateStack, type SourceFetch } from "./symbolication.js";
23
+
24
+ interface RendererDispatcherSnapshot {
25
+ currentDispatcherRef: RendererDispatcherRef;
26
+ value: unknown;
27
+ }
30
28
 
31
29
  export const hasDebugStack = (
32
30
  fiber: Fiber,
@@ -37,13 +35,7 @@ export const hasDebugStack = (
37
35
  };
38
36
 
39
37
  const isFiberOwner = (owner: Fiber | ServerComponentInfo): owner is Fiber =>
40
- typeof (owner as Fiber).tag === "number";
41
-
42
- // react's typings (and bippy's Fiber, which mirrors them) declare _debugOwner
43
- // as a Fiber, but react 19 flight sets a ReactComponentInfo object for server
44
- // component owners
45
- const getDebugOwner = (fiber: Fiber): Fiber | ServerComponentInfo | undefined =>
46
- fiber._debugOwner as Fiber | ServerComponentInfo | undefined;
38
+ "tag" in owner && typeof owner.tag === "number";
47
39
 
48
40
  /**
49
41
  * Locates a frame inside the fiber's own function body without invoking it:
@@ -90,27 +82,18 @@ export const getDefinitionFrameFromOwnedChild = (fiber: Fiber): StackFrame | nul
90
82
  return null;
91
83
  };
92
84
 
93
- const getCurrentDispatcher = (): null | React.RefObject<unknown> => {
94
- const rdtHook = getRDTHook();
95
- for (const renderer of [...Array.from(_renderers), ...Array.from(rdtHook.renderers.values())]) {
96
- const currentDispatcherRef = renderer.currentDispatcherRef;
97
- if (currentDispatcherRef && typeof currentDispatcherRef === "object") {
98
- return "H" in currentDispatcherRef ? currentDispatcherRef.H : currentDispatcherRef.current;
99
- }
85
+ const clearRendererDispatchers = (): RendererDispatcherSnapshot[] => {
86
+ const dispatcherSnapshots: RendererDispatcherSnapshot[] = [];
87
+ for (const currentDispatcherRef of getRendererDispatcherRefs()) {
88
+ dispatcherSnapshots.push({ currentDispatcherRef, value: readDispatcher(currentDispatcherRef) });
89
+ writeDispatcher(currentDispatcherRef, null);
100
90
  }
101
- return null;
91
+ return dispatcherSnapshots;
102
92
  };
103
93
 
104
- const setCurrentDispatcher = (value: null | React.RefObject<unknown>): void => {
105
- for (const renderer of _renderers) {
106
- const currentDispatcherRef = renderer.currentDispatcherRef;
107
- if (currentDispatcherRef && typeof currentDispatcherRef === "object") {
108
- if ("H" in currentDispatcherRef) {
109
- currentDispatcherRef.H = value;
110
- } else {
111
- currentDispatcherRef.current = value;
112
- }
113
- }
94
+ const restoreRendererDispatchers = (dispatcherSnapshots: RendererDispatcherSnapshot[]): void => {
95
+ for (const { currentDispatcherRef, value } of dispatcherSnapshots) {
96
+ writeDispatcher(currentDispatcherRef, value);
114
97
  }
115
98
  };
116
99
 
@@ -146,49 +129,31 @@ const describeNativeComponentFrame = (
146
129
  return cachedFrame;
147
130
  }
148
131
 
149
- const previousPrepareStackTrace = Error.prepareStackTrace;
150
- // HACK: V8 API allows undefined but bun-types declares it as non-optional
151
- (Error as { prepareStackTrace?: typeof Error.prepareStackTrace }).prepareStackTrace = undefined;
132
+ const previousPrepareStackTrace = getPrepareStackTrace();
133
+ setPrepareStackTrace(undefined);
152
134
  reEntry = true;
153
135
 
154
- const previousDispatcher = getCurrentDispatcher();
155
- setCurrentDispatcher(null);
136
+ const dispatcherSnapshots = clearRendererDispatchers();
156
137
  const previousConsoleError = console.error;
157
138
  const previousConsoleWarn = console.warn;
158
139
  console.error = () => {};
159
140
  console.warn = () => {};
160
141
  try {
161
- /**
162
- * Finding a common stack frame between sample and control errors can be
163
- * tricky given the different types and levels of stack trace truncation from
164
- * different JS VMs. So instead we'll attempt to control what that common
165
- * frame should be through this object method:
166
- * Having both the sample and control errors be in the function under the
167
- * `DescribeNativeComponentFrameRoot` property, + setting the `name` and
168
- * `displayName` properties of the function ensures that a stack
169
- * frame exists that has the method name `DescribeNativeComponentFrameRoot` in
170
- * it for both control and sample stacks.
171
- */
142
+ // HACK: Run the control and sample under the same property name so their stacks share a frame.
172
143
  const RunInRootFrame = {
173
- DetermineComponentFrameRoot() {
144
+ DetermineComponentFrameRoot(this: void) {
174
145
  let control: unknown;
175
146
  try {
176
- // This should throw.
177
147
  if (construct) {
178
- // Something should be setting the props in the constructor.
179
148
  const ThrowingConstructor = function () {
180
149
  throw Error();
181
150
  };
182
151
  Object.defineProperty(ThrowingConstructor.prototype, "props", {
183
152
  set: function () {
184
- // We use a throwing setter instead of frozen or non-writable props
185
- // because that won't throw in a non-strict mode function.
186
153
  throw Error();
187
154
  },
188
155
  });
189
156
  if (typeof Reflect === "object" && Reflect.construct) {
190
- // We construct a different control for this case to include any extra
191
- // frames added by the construct call.
192
157
  try {
193
158
  Reflect.construct(ThrowingConstructor, []);
194
159
  } catch (caughtError) {
@@ -197,13 +162,11 @@ const describeNativeComponentFrame = (
197
162
  Reflect.construct(component, [], ThrowingConstructor);
198
163
  } else {
199
164
  try {
200
- // @ts-expect-error -- ThrowingConstructor is a constructor function
201
- ThrowingConstructor.call();
165
+ Function.prototype.apply.call(ThrowingConstructor, undefined, []);
202
166
  } catch (caughtError) {
203
167
  control = caughtError;
204
168
  }
205
- // @ts-expect-error -- ThrowingConstructor is a constructor function
206
- component.call(ThrowingConstructor.prototype);
169
+ Function.prototype.apply.call(component, ThrowingConstructor.prototype, []);
207
170
  }
208
171
  } else {
209
172
  try {
@@ -211,22 +174,17 @@ const describeNativeComponentFrame = (
211
174
  } catch (caughtError) {
212
175
  control = caughtError;
213
176
  }
214
- // TODO(luna): This will currently only throw if the function component
215
- // tries to access React/ReactDOM/props. We should probably make this throw
216
- // in simple components too
217
- const maybePromise = (component as () => Promise<unknown>)();
218
-
219
- // If the function component returns a promise, it's likely an async
220
- // component, which we don't yet support. Attach a noop catch handler to
221
- // silence the error.
222
- // TODO: Implement component stacks for async client components?
223
- // eslint-disable-next-line @typescript-eslint/no-misused-promises -- we literally check if this is a promise here
224
- if (maybePromise && typeof maybePromise.catch === "function") {
225
- maybePromise.catch(() => {});
177
+ const maybePromise = Function.prototype.apply.call(component, undefined, []);
178
+ if (
179
+ typeof maybePromise === "object" &&
180
+ maybePromise !== null &&
181
+ "catch" in maybePromise &&
182
+ typeof maybePromise.catch === "function"
183
+ ) {
184
+ maybePromise.catch(() => undefined);
226
185
  }
227
186
  }
228
187
  } catch (sample: unknown) {
229
- // This is inlined manually because closure doesn't do it for us.
230
188
  if (
231
189
  sample instanceof Error &&
232
190
  control instanceof Error &&
@@ -239,21 +197,18 @@ const describeNativeComponentFrame = (
239
197
  },
240
198
  };
241
199
 
242
- // @ts-expect-error --- displayName is not a property of the function
243
- RunInRootFrame.DetermineComponentFrameRoot.displayName = "DetermineComponentFrameRoot";
200
+ Object.defineProperty(RunInRootFrame.DetermineComponentFrameRoot, "displayName", {
201
+ value: "DetermineComponentFrameRoot",
202
+ });
244
203
  const namePropDescriptor = Object.getOwnPropertyDescriptor(
245
204
  // eslint-disable-next-line @typescript-eslint/unbound-method
246
205
  RunInRootFrame.DetermineComponentFrameRoot,
247
206
  "name",
248
207
  );
249
- // Before ES6, the `name` property was not configurable.
250
208
  if (namePropDescriptor?.configurable) {
251
- // V8 utilizes a function's `name` property when generating a stack trace.
252
209
  Object.defineProperty(
253
210
  // eslint-disable-next-line @typescript-eslint/unbound-method
254
211
  RunInRootFrame.DetermineComponentFrameRoot,
255
- // Configurable properties can be updated even if its writable descriptor
256
- // is set to `false`.
257
212
  "name",
258
213
  { value: "DetermineComponentFrameRoot" },
259
214
  );
@@ -317,6 +272,10 @@ const describeNativeComponentFrame = (
317
272
  if (controlIndex < 0 || sampleLines[sampleIndex] !== controlLines[controlIndex]) {
318
273
  // V8 adds a "new" prefix for native classes. Let's remove it to make it prettier.
319
274
  let stackFrame = `\n${sampleLines[sampleIndex].replace(" at new ", " at ")}`;
275
+ const [parsedStackFrame] = parseStack(stackFrame);
276
+ if (!parsedStackFrame?.fileName) {
277
+ continue;
278
+ }
320
279
 
321
280
  const displayName = getDisplayName(component);
322
281
  // If our component frame is labeled "<anonymous>"
@@ -338,14 +297,14 @@ const describeNativeComponentFrame = (
338
297
  } finally {
339
298
  reEntry = false;
340
299
 
341
- Error.prepareStackTrace = previousPrepareStackTrace;
300
+ setPrepareStackTrace(previousPrepareStackTrace);
342
301
 
343
- setCurrentDispatcher(previousDispatcher);
302
+ restoreRendererDispatchers(dispatcherSnapshots);
344
303
  console.error = previousConsoleError;
345
304
  console.warn = previousConsoleWarn;
346
305
  }
347
306
 
348
- const componentName = component ? getDisplayName(component) : "";
307
+ const componentName = getDisplayName(component);
349
308
  const syntheticFrame = componentName ? describeBuiltInComponentFrame(componentName) : "";
350
309
  componentFrameCache.set(component, syntheticFrame);
351
310
  return syntheticFrame;
@@ -353,35 +312,34 @@ const describeNativeComponentFrame = (
353
312
 
354
313
  // https://github.com/facebook/react/blob/ac3e705a18696168acfcaed39dce0cfaa6be8836/packages/react-reconciler/src/ReactFiberComponentStack.js#L180
355
314
  export const describeFiber = (fiber: Fiber, childFiber: Fiber | null): string => {
356
- const tag = fiber.tag as number;
357
315
  let stackFrame = "";
358
- switch (tag) {
359
- case ActivityComponentTag:
316
+ const workTags = getReactWorkTagsForFiber(fiber);
317
+ switch (fiber.tag) {
318
+ case workTags.ActivityComponent:
360
319
  stackFrame = describeBuiltInComponentFrame("Activity");
361
320
  break;
362
- case ClassComponentTag:
321
+ case workTags.ClassComponent:
363
322
  stackFrame = describeNativeComponentFrame(fiber.type, true);
364
323
  break;
365
- case ForwardRefTag:
366
- stackFrame = describeNativeComponentFrame(
367
- (fiber.type as { render: React.ComponentType<unknown> }).render,
368
- false,
369
- );
324
+ case workTags.ForwardRef:
325
+ stackFrame = describeNativeComponentFrame(getType(fiber.type) ?? fiber.type, false);
370
326
  break;
371
- case FunctionComponentTag:
372
- case SimpleMemoComponentTag:
373
- stackFrame = describeNativeComponentFrame(fiber.type, false);
327
+ case workTags.FunctionComponent:
328
+ case workTags.SimpleMemoComponent:
329
+ stackFrame = describeNativeComponentFrame(getType(fiber.type) ?? fiber.type, false);
374
330
  break;
375
- case HostComponentTag:
376
- case HostHoistableTag:
377
- case HostSingletonTag:
378
- stackFrame = describeBuiltInComponentFrame(fiber.type as string);
331
+ case workTags.HostComponent:
332
+ case workTags.HostHoistable:
333
+ case workTags.HostSingleton:
334
+ if (typeof fiber.type === "string") {
335
+ stackFrame = describeBuiltInComponentFrame(fiber.type);
336
+ }
379
337
  break;
380
- case LazyComponentTag:
338
+ case workTags.LazyComponent:
381
339
  // TODO: When we support Thenables as component types we should rename this.
382
340
  stackFrame = describeBuiltInComponentFrame("Lazy");
383
341
  break;
384
- case SuspenseComponentTag:
342
+ case workTags.SuspenseComponent:
385
343
  if (fiber.child !== childFiber && childFiber !== null) {
386
344
  // If we came from the second Fiber then we're in the Suspense Fallback.
387
345
  stackFrame = describeBuiltInComponentFrame("Suspense Fallback");
@@ -389,10 +347,10 @@ export const describeFiber = (fiber: Fiber, childFiber: Fiber | null): string =>
389
347
  stackFrame = describeBuiltInComponentFrame("Suspense");
390
348
  }
391
349
  break;
392
- case SuspenseListComponentTag:
350
+ case workTags.SuspenseListComponent:
393
351
  stackFrame = describeBuiltInComponentFrame("SuspenseList");
394
352
  break;
395
- case ViewTransitionComponentTag:
353
+ case workTags.ViewTransitionComponent:
396
354
  // Note: enableViewTransition feature flag is not available in this codebase,
397
355
  // so we'll always include ViewTransition
398
356
  stackFrame = describeBuiltInComponentFrame("ViewTransition");
@@ -421,8 +379,8 @@ export const getFallbackParentStack = (thisFiber: Fiber): string => {
421
379
  // Since we don't have __DEV__ in this codebase, we'll check for _debugInfo
422
380
  const debugInfo = currentFiber._debugInfo;
423
381
  if (debugInfo && Array.isArray(debugInfo)) {
424
- for (let i = debugInfo.length - 1; i >= 0; i--) {
425
- const debugEntry = debugInfo[i];
382
+ for (let debugInfoIndex = debugInfo.length - 1; debugInfoIndex >= 0; debugInfoIndex--) {
383
+ const debugEntry = debugInfo[debugInfoIndex];
426
384
  if (typeof debugEntry.name === "string") {
427
385
  componentStack += describeDebugInfoFrame(debugEntry.name, debugEntry.env);
428
386
  }
@@ -435,7 +393,7 @@ export const getFallbackParentStack = (thisFiber: Fiber): string => {
435
393
  return componentStack;
436
394
  } catch (error) {
437
395
  if (error instanceof Error) {
438
- return `\nError generating stack: ${error.message}\n${error.stack}`;
396
+ return `\nBippy couldn’t generate the stack: ${error.message}\n${error.stack}`;
439
397
  }
440
398
  return "";
441
399
  }
@@ -460,14 +418,10 @@ export const getFallbackParentStack = (thisFiber: Fiber): string => {
460
418
  * @see https://github.com/facebook/react/blob/main/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js#L12
461
419
  */
462
420
  export const formatOwnerStack = (stack: string): string => {
463
- const prevPrepareStackTrace = Error.prepareStackTrace;
464
- // HACK: V8 API allows undefined but bun-types declares it as non-optional
465
- (Error as { prepareStackTrace?: typeof Error.prepareStackTrace }).prepareStackTrace = undefined;
466
421
  let formattedStack = stack;
467
422
  if (!formattedStack) {
468
423
  return "";
469
424
  }
470
- Error.prepareStackTrace = prevPrepareStackTrace;
471
425
 
472
426
  if (formattedStack.startsWith("Error: react-stack-top-frame\n")) {
473
427
  // V8's default formatting prefixes with the error message which we
@@ -480,8 +434,7 @@ export const formatOwnerStack = (stack: string): string => {
480
434
  formattedStack = formattedStack.slice(firstNewlineIndex + 1);
481
435
  }
482
436
  let bottomFrameIndex = Math.max(
483
- formattedStack.indexOf("react_stack_bottom_frame"),
484
- formattedStack.indexOf("react-stack-bottom-frame"),
437
+ ...REACT_STACK_BOTTOM_FRAME_PATTERNS.map((pattern) => formattedStack.indexOf(pattern)),
485
438
  );
486
439
  if (bottomFrameIndex !== -1) {
487
440
  bottomFrameIndex = formattedStack.lastIndexOf("\n", bottomFrameIndex);
@@ -498,11 +451,6 @@ export const formatOwnerStack = (stack: string): string => {
498
451
  return formattedStack;
499
452
  };
500
453
 
501
- interface DebugStackEntry {
502
- componentName: string;
503
- stackFrames: StackFrame[];
504
- }
505
-
506
454
  const isReactServerComponentFrame = (stackFrame: StackFrame): boolean =>
507
455
  Boolean(
508
456
  stackFrame.functionName && stackFrame.fileName && isServerComponentUrl(stackFrame.fileName),
@@ -514,24 +462,22 @@ const areStackFramesEqual = (firstFrame: StackFrame, secondFrame: StackFrame): b
514
462
  firstFrame.columnNumber === secondFrame.columnNumber;
515
463
 
516
464
  const buildFunctionNameToRscFramesMap = (
517
- debugStackEntries: DebugStackEntry[],
465
+ debugStackFrames: StackFrame[],
518
466
  ): Map<string, StackFrame[]> => {
519
467
  const functionNameToRscFrames = new Map<string, StackFrame[]>();
520
468
 
521
- for (const debugStackEntry of debugStackEntries) {
522
- for (const stackFrame of debugStackEntry.stackFrames) {
523
- if (!isReactServerComponentFrame(stackFrame)) continue;
469
+ for (const stackFrame of debugStackFrames) {
470
+ if (!isReactServerComponentFrame(stackFrame)) continue;
524
471
 
525
- const functionName = stackFrame.functionName!;
526
- const framesForFunction = functionNameToRscFrames.get(functionName) ?? [];
527
- const isDuplicateFrame = framesForFunction.some((existingFrame) =>
528
- areStackFramesEqual(existingFrame, stackFrame),
529
- );
472
+ const functionName = stackFrame.functionName!;
473
+ const framesForFunction = functionNameToRscFrames.get(functionName) ?? [];
474
+ const isDuplicateFrame = framesForFunction.some((existingFrame) =>
475
+ areStackFramesEqual(existingFrame, stackFrame),
476
+ );
530
477
 
531
- if (!isDuplicateFrame) {
532
- framesForFunction.push(stackFrame);
533
- functionNameToRscFrames.set(functionName, framesForFunction);
534
- }
478
+ if (!isDuplicateFrame) {
479
+ framesForFunction.push(stackFrame);
480
+ functionNameToRscFrames.set(functionName, framesForFunction);
535
481
  }
536
482
  }
537
483
 
@@ -592,7 +538,7 @@ const getOwnerStackFromDebugStacks = (fiber: Fiber): StackFrame[] => {
592
538
  while (owner) {
593
539
  if (isFiberOwner(owner)) {
594
540
  const ownerFiber: Fiber = owner;
595
- owner = getDebugOwner(ownerFiber);
541
+ owner = ownerFiber._debugOwner;
596
542
  if (owner && hasDebugStack(ownerFiber)) {
597
543
  const { frames, isTrusted } = parseDebugStack(ownerFiber._debugStack);
598
544
  if (isTrusted) {
@@ -616,51 +562,39 @@ const getOwnerStackFromDebugStacks = (fiber: Fiber): StackFrame[] => {
616
562
  return ownerStackFrames;
617
563
  };
618
564
 
619
- const getDebugStackEntries = (rootFiber: Fiber): DebugStackEntry[] => {
620
- const debugStackEntries: DebugStackEntry[] = [];
565
+ const getDebugStackFrames = (rootFiber: Fiber): StackFrame[] => {
566
+ const debugStackFrames: StackFrame[] = [];
621
567
 
622
568
  traverseFiber(
623
569
  rootFiber,
624
570
  (currentFiber) => {
625
571
  if (!hasDebugStack(currentFiber)) return;
626
572
 
627
- const componentName =
628
- typeof currentFiber.type !== "string"
629
- ? getDisplayName(currentFiber.type) || "<anonymous>"
630
- : currentFiber.type;
631
-
632
- debugStackEntries.push({
633
- componentName,
634
- stackFrames: parseStack(formatOwnerStack(currentFiber._debugStack?.stack)),
635
- });
573
+ const { frames, isTrusted } = parseDebugStack(currentFiber._debugStack);
574
+ if (isTrusted) {
575
+ debugStackFrames.push(...frames);
576
+ }
636
577
  },
637
578
  true,
638
579
  );
639
580
 
640
- return debugStackEntries;
581
+ return debugStackFrames;
641
582
  };
642
583
 
643
584
  /**
644
- * Returns a stack of ALL ancestor components in the render tree (the fiber's
645
- * `return` chain), including wrappers that render `{children}` without having
646
- * created this fiber's JSX. Locations come from re-invoking each component
647
- * with a throwing dispatcher; server frames are enriched from debug stacks by
648
- * name matching. Works on every React version.
585
+ * The unsymbolicated frames behind {@link getParentStack}: bundle-space
586
+ * locations from re-invoking each component with a throwing dispatcher,
587
+ * with server frames enriched from debug stacks by name matching.
649
588
  */
650
- export const getParentStack = async (
651
- fiber: Fiber,
652
- shouldCache = true,
653
- fetchFunction?: (url: string) => Promise<Response>,
654
- ): Promise<StackFrame[]> => {
655
- const debugStackEntries = getDebugStackEntries(fiber);
589
+ export const getRawParentStack = (fiber: Fiber): StackFrame[] => {
590
+ const debugStackFrames = getDebugStackFrames(fiber);
656
591
  const fallbackStackFrames = parseStack(getFallbackParentStack(fiber));
657
- const functionNameToRscFrames = buildFunctionNameToRscFramesMap(debugStackEntries);
592
+ const functionNameToRscFrames = buildFunctionNameToRscFramesMap(debugStackFrames);
658
593
  const functionNameToUsageIndex = new Map<string, number>();
659
594
 
660
595
  const enrichedStackFrames = fallbackStackFrames.map((stackFrame): StackFrame => {
661
596
  const isServerFrame =
662
- (stackFrame.source?.includes(SERVER_FRAME_MARKER) ?? false) ||
663
- (stackFrame.source != null && SERVER_ENV_PATTERN.test(stackFrame.source));
597
+ stackFrame.source !== undefined && SERVER_ENV_PATTERN.test(stackFrame.source);
664
598
 
665
599
  if (isServerFrame) {
666
600
  return getEnrichedServerStackFrame(
@@ -673,15 +607,26 @@ export const getParentStack = async (
673
607
  return stackFrame;
674
608
  });
675
609
 
676
- const deduplicatedStackFrames = enrichedStackFrames.filter((stackFrame, index, frames) => {
610
+ return enrichedStackFrames.filter((stackFrame, index, frames) => {
677
611
  if (index === 0) return true;
678
612
  const previousFrame = frames[index - 1];
679
613
  return stackFrame.functionName !== previousFrame.functionName;
680
614
  });
681
-
682
- return symbolicateStack(deduplicatedStackFrames, shouldCache, fetchFunction);
683
615
  };
684
616
 
617
+ /**
618
+ * Returns a stack of ALL ancestor components in the render tree (the fiber's
619
+ * `return` chain), including wrappers that render `{children}` without having
620
+ * created this fiber's JSX. Locations come from re-invoking each component
621
+ * with a throwing dispatcher; server frames are enriched from debug stacks by
622
+ * name matching. Works on every React version.
623
+ */
624
+ export const getParentStack = async (
625
+ fiber: Fiber,
626
+ shouldCache = true,
627
+ fetchFunction?: SourceFetch,
628
+ ): Promise<StackFrame[]> => symbolicateStack(getRawParentStack(fiber), shouldCache, fetchFunction);
629
+
685
630
  // an owner frame is only actionable if it can point an editor somewhere:
686
631
  // it needs a file location and must not be ignore-listed bundler/framework code
687
632
  const isLocatableFrame = (stackFrame: StackFrame): boolean =>
@@ -700,7 +645,7 @@ const isLocatableFrame = (stackFrame: StackFrame): boolean =>
700
645
  export const getOwnerStack = async (
701
646
  fiber: Fiber,
702
647
  shouldCache = true,
703
- fetchFunction?: (url: string) => Promise<Response>,
648
+ fetchFunction?: SourceFetch,
704
649
  ): Promise<StackFrame[]> => {
705
650
  const debugStackFrames = getOwnerStackFromDebugStacks(fiber);
706
651
  if (debugStackFrames.length > 0) {
@@ -1,4 +1,5 @@
1
1
  import { JSX_FACTORY_FRAME_COUNT, REACT_STACK_BOTTOM_FRAME_PATTERNS } from "./constants.js";
2
+ import { getPrepareStackTrace, setPrepareStackTrace } from "./error-stack.js";
2
3
  import { parseStack, StackFrame } from "./parse-stack.js";
3
4
 
4
5
  interface V8CallSite {
@@ -117,14 +118,13 @@ export const parseDebugStack = (debugStack: Error): ParsedDebugStack => {
117
118
  }
118
119
  return stackString;
119
120
  };
120
- const previousPrepareStackTrace = Error.prepareStackTrace;
121
- // node's CallSite typings disagree with browser-safe optional methods
122
- Error.prepareStackTrace = collectFramesAndFormatStack as typeof Error.prepareStackTrace;
121
+ const previousPrepareStackTrace = getPrepareStackTrace();
122
+ setPrepareStackTrace(collectFramesAndFormatStack);
123
123
  let stackString: string;
124
124
  try {
125
125
  stackString = String(debugStack.stack);
126
126
  } finally {
127
- Error.prepareStackTrace = previousPrepareStackTrace;
127
+ setPrepareStackTrace(previousPrepareStackTrace);
128
128
  }
129
129
 
130
130
  const result = structuredResult ?? parseMaterializedStack(stackString);
@@ -1,5 +1,10 @@
1
1
  import type { HooksNode, HooksTree, HookSource } from "./inspect-hooks.js";
2
- import { getSourceMap, getSourceFromSourceMap, type SourceMap } from "./symbolication.js";
2
+ import {
3
+ getSourceContentFromSourceMap,
4
+ getSourceFromSourceMap,
5
+ getSourceMap,
6
+ type SourceFetch,
7
+ } from "./symbolication.js";
3
8
 
4
9
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
5
10
  export interface HookNames extends Map<string, string> {}
@@ -33,40 +38,6 @@ const flattenHooksTree = (hooksTree: HooksTree): HooksNode[] => {
33
38
  return hooksList;
34
39
  };
35
40
 
36
- const findSourceContentByFileName = (
37
- sources: string[],
38
- sourcesContent: string[] | undefined,
39
- fileName: string,
40
- ): string | null => {
41
- if (!sourcesContent) return null;
42
- const sourceIndex = sources.indexOf(fileName);
43
- return sourceIndex !== -1 ? (sourcesContent[sourceIndex] ?? null) : null;
44
- };
45
-
46
- const getSourceContentFromSourceMap = (
47
- sourceMap: SourceMap,
48
- originalFileName: string,
49
- ): string | null => {
50
- const directResult = findSourceContentByFileName(
51
- sourceMap.sources,
52
- sourceMap.sourcesContent,
53
- originalFileName,
54
- );
55
- if (directResult) return directResult;
56
-
57
- if (sourceMap.sections) {
58
- for (const section of sourceMap.sections) {
59
- const sectionResult = findSourceContentByFileName(
60
- section.map.sources,
61
- section.map.sourcesContent,
62
- originalFileName,
63
- );
64
- if (sectionResult) return sectionResult;
65
- }
66
- }
67
- return null;
68
- };
69
-
70
41
  const extractVariableNameFromBinding = (binding: string): string | null => {
71
42
  const trimmed = binding.trim();
72
43
  if (trimmed.startsWith("[")) {
@@ -109,9 +80,8 @@ interface ResolvedSource {
109
80
  }
110
81
 
111
82
  interface SourceResolutionContext {
112
- sourceMapsByFile: Map<string, SourceMap | null>;
113
83
  sourceContentCache: Map<string, string | null>;
114
- fetchFn?: (url: string) => Promise<Response>;
84
+ fetchFn?: SourceFetch;
115
85
  }
116
86
 
117
87
  const resolveOriginalSource = async (
@@ -120,25 +90,17 @@ const resolveOriginalSource = async (
120
90
  runtimeColumn: number,
121
91
  context: SourceResolutionContext,
122
92
  ): Promise<ResolvedSource | null> => {
123
- const { sourceMapsByFile, sourceContentCache, fetchFn } = context;
93
+ const { sourceContentCache, fetchFn } = context;
124
94
 
125
- if (!sourceMapsByFile.has(runtimeFileName)) {
126
- sourceMapsByFile.set(runtimeFileName, await getSourceMap(runtimeFileName, true, fetchFn));
127
- }
128
-
129
- const sourceMap = sourceMapsByFile.get(runtimeFileName) ?? null;
95
+ const sourceMap = await getSourceMap(runtimeFileName, true, fetchFn);
130
96
 
131
97
  if (sourceMap) {
132
98
  const originalLocation = getSourceFromSourceMap(sourceMap, runtimeLine, runtimeColumn);
133
99
  if (originalLocation?.fileName && originalLocation.lineNumber !== undefined) {
134
- const cacheKey = `sourcemap:${runtimeFileName}:${originalLocation.fileName}`;
135
- if (!sourceContentCache.has(cacheKey)) {
136
- sourceContentCache.set(
137
- cacheKey,
138
- getSourceContentFromSourceMap(sourceMap, originalLocation.fileName),
139
- );
140
- }
141
- const originalSourceCode = sourceContentCache.get(cacheKey) ?? null;
100
+ const originalSourceCode = getSourceContentFromSourceMap(
101
+ sourceMap,
102
+ originalLocation.fileName,
103
+ );
142
104
  if (originalSourceCode) {
143
105
  return {
144
106
  sourceCode: originalSourceCode,
@@ -173,7 +135,7 @@ const resolveOriginalSource = async (
173
135
 
174
136
  export const parseHookNames = async (
175
137
  hooksTree: HooksTree,
176
- fetchFn?: (url: string) => Promise<Response>,
138
+ fetchFn?: SourceFetch,
177
139
  ): Promise<HookNames> => {
178
140
  const hookNames: HookNames = new Map();
179
141
  const hooksList = flattenHooksTree(hooksTree);
@@ -181,7 +143,6 @@ export const parseHookNames = async (
181
143
  if (hooksList.length === 0) return hookNames;
182
144
 
183
145
  const resolutionContext: SourceResolutionContext = {
184
- sourceMapsByFile: new Map(),
185
146
  sourceContentCache: new Map(),
186
147
  fetchFn,
187
148
  };