bippy 0.6.1-dev.051a9b2 → 0.6.1-dev.3702582

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 +32 -48
  3. package/dist/core.cjs +1 -1
  4. package/dist/core.d.cts +45 -60
  5. package/dist/core.d.ts +45 -60
  6. package/dist/core.js +1 -1
  7. package/dist/core2.cjs +9 -0
  8. package/dist/core2.d.cts +11 -3
  9. package/dist/core2.d.ts +11 -3
  10. package/dist/core2.js +9 -0
  11. package/dist/errors.d.cts +410 -0
  12. package/dist/errors.d.ts +410 -0
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +11 -3
  15. package/dist/index.d.ts +11 -3
  16. package/dist/index.iife.js +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/install-hook-only.cjs +1 -1
  19. package/dist/install-hook-only.d.cts +9 -1
  20. package/dist/install-hook-only.d.ts +9 -1
  21. package/dist/install-hook-only.iife.js +1 -1
  22. package/dist/install-hook-only.js +1 -1
  23. package/dist/rdt-hook.cjs +1 -1
  24. package/dist/rdt-hook.js +1 -1
  25. package/dist/source.cjs +14 -5
  26. package/dist/source.d.cts +84 -69
  27. package/dist/source.d.ts +84 -69
  28. package/dist/source.js +14 -5
  29. package/package.json +22 -16
  30. package/src/core.ts +348 -344
  31. package/src/errors.ts +34 -0
  32. package/src/install-hook-only.ts +2 -2
  33. package/src/rdt-hook.ts +160 -161
  34. package/src/react-internals/generated/react-work-tags.ts +318 -0
  35. package/src/react-internals/index.ts +67 -0
  36. package/src/react-internals/semver.ts +80 -0
  37. package/src/react-internals/types.ts +186 -0
  38. package/src/source/constants.ts +1 -1
  39. package/src/source/error-stack.ts +11 -0
  40. package/src/source/get-display-name-from-source.ts +44 -41
  41. package/src/source/get-source.ts +10 -24
  42. package/src/source/index.ts +9 -1
  43. package/src/source/inspect-hooks.ts +220 -207
  44. package/src/source/owner-stack.ts +113 -172
  45. package/src/source/parse-debug-stack.ts +4 -4
  46. package/src/source/parse-hook-names.ts +14 -53
  47. package/src/source/parse-stack.ts +13 -30
  48. package/src/source/renderer-dispatchers.ts +30 -0
  49. package/src/source/symbolication.ts +517 -92
  50. package/dist/get-source.cjs +0 -19
  51. package/dist/get-source.js +0 -19
  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
16
  import { parseStack, StackFrame } from "./parse-stack.js";
29
- import { symbolicateStack } from "./symbolication.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
  );
@@ -338,14 +293,14 @@ const describeNativeComponentFrame = (
338
293
  } finally {
339
294
  reEntry = false;
340
295
 
341
- Error.prepareStackTrace = previousPrepareStackTrace;
296
+ setPrepareStackTrace(previousPrepareStackTrace);
342
297
 
343
- setCurrentDispatcher(previousDispatcher);
298
+ restoreRendererDispatchers(dispatcherSnapshots);
344
299
  console.error = previousConsoleError;
345
300
  console.warn = previousConsoleWarn;
346
301
  }
347
302
 
348
- const componentName = component ? getDisplayName(component) : "";
303
+ const componentName = getDisplayName(component);
349
304
  const syntheticFrame = componentName ? describeBuiltInComponentFrame(componentName) : "";
350
305
  componentFrameCache.set(component, syntheticFrame);
351
306
  return syntheticFrame;
@@ -353,35 +308,34 @@ const describeNativeComponentFrame = (
353
308
 
354
309
  // https://github.com/facebook/react/blob/ac3e705a18696168acfcaed39dce0cfaa6be8836/packages/react-reconciler/src/ReactFiberComponentStack.js#L180
355
310
  export const describeFiber = (fiber: Fiber, childFiber: Fiber | null): string => {
356
- const tag = fiber.tag as number;
357
311
  let stackFrame = "";
358
- switch (tag) {
359
- case ActivityComponentTag:
312
+ const workTags = getReactWorkTagsForFiber(fiber);
313
+ switch (fiber.tag) {
314
+ case workTags.ActivityComponent:
360
315
  stackFrame = describeBuiltInComponentFrame("Activity");
361
316
  break;
362
- case ClassComponentTag:
317
+ case workTags.ClassComponent:
363
318
  stackFrame = describeNativeComponentFrame(fiber.type, true);
364
319
  break;
365
- case ForwardRefTag:
366
- stackFrame = describeNativeComponentFrame(
367
- (fiber.type as { render: React.ComponentType<unknown> }).render,
368
- false,
369
- );
320
+ case workTags.ForwardRef:
321
+ stackFrame = describeNativeComponentFrame(getType(fiber.type) ?? fiber.type, false);
370
322
  break;
371
- case FunctionComponentTag:
372
- case SimpleMemoComponentTag:
323
+ case workTags.FunctionComponent:
324
+ case workTags.SimpleMemoComponent:
373
325
  stackFrame = describeNativeComponentFrame(fiber.type, false);
374
326
  break;
375
- case HostComponentTag:
376
- case HostHoistableTag:
377
- case HostSingletonTag:
378
- stackFrame = describeBuiltInComponentFrame(fiber.type as string);
327
+ case workTags.HostComponent:
328
+ case workTags.HostHoistable:
329
+ case workTags.HostSingleton:
330
+ if (typeof fiber.type === "string") {
331
+ stackFrame = describeBuiltInComponentFrame(fiber.type);
332
+ }
379
333
  break;
380
- case LazyComponentTag:
334
+ case workTags.LazyComponent:
381
335
  // TODO: When we support Thenables as component types we should rename this.
382
336
  stackFrame = describeBuiltInComponentFrame("Lazy");
383
337
  break;
384
- case SuspenseComponentTag:
338
+ case workTags.SuspenseComponent:
385
339
  if (fiber.child !== childFiber && childFiber !== null) {
386
340
  // If we came from the second Fiber then we're in the Suspense Fallback.
387
341
  stackFrame = describeBuiltInComponentFrame("Suspense Fallback");
@@ -389,10 +343,10 @@ export const describeFiber = (fiber: Fiber, childFiber: Fiber | null): string =>
389
343
  stackFrame = describeBuiltInComponentFrame("Suspense");
390
344
  }
391
345
  break;
392
- case SuspenseListComponentTag:
346
+ case workTags.SuspenseListComponent:
393
347
  stackFrame = describeBuiltInComponentFrame("SuspenseList");
394
348
  break;
395
- case ViewTransitionComponentTag:
349
+ case workTags.ViewTransitionComponent:
396
350
  // Note: enableViewTransition feature flag is not available in this codebase,
397
351
  // so we'll always include ViewTransition
398
352
  stackFrame = describeBuiltInComponentFrame("ViewTransition");
@@ -421,8 +375,8 @@ export const getFallbackParentStack = (thisFiber: Fiber): string => {
421
375
  // Since we don't have __DEV__ in this codebase, we'll check for _debugInfo
422
376
  const debugInfo = currentFiber._debugInfo;
423
377
  if (debugInfo && Array.isArray(debugInfo)) {
424
- for (let i = debugInfo.length - 1; i >= 0; i--) {
425
- const debugEntry = debugInfo[i];
378
+ for (let debugInfoIndex = debugInfo.length - 1; debugInfoIndex >= 0; debugInfoIndex--) {
379
+ const debugEntry = debugInfo[debugInfoIndex];
426
380
  if (typeof debugEntry.name === "string") {
427
381
  componentStack += describeDebugInfoFrame(debugEntry.name, debugEntry.env);
428
382
  }
@@ -435,7 +389,7 @@ export const getFallbackParentStack = (thisFiber: Fiber): string => {
435
389
  return componentStack;
436
390
  } catch (error) {
437
391
  if (error instanceof Error) {
438
- return `\nError generating stack: ${error.message}\n${error.stack}`;
392
+ return `\nBippy couldn’t generate the stack: ${error.message}\n${error.stack}`;
439
393
  }
440
394
  return "";
441
395
  }
@@ -460,14 +414,10 @@ export const getFallbackParentStack = (thisFiber: Fiber): string => {
460
414
  * @see https://github.com/facebook/react/blob/main/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js#L12
461
415
  */
462
416
  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
417
  let formattedStack = stack;
467
418
  if (!formattedStack) {
468
419
  return "";
469
420
  }
470
- Error.prepareStackTrace = prevPrepareStackTrace;
471
421
 
472
422
  if (formattedStack.startsWith("Error: react-stack-top-frame\n")) {
473
423
  // V8's default formatting prefixes with the error message which we
@@ -480,8 +430,7 @@ export const formatOwnerStack = (stack: string): string => {
480
430
  formattedStack = formattedStack.slice(firstNewlineIndex + 1);
481
431
  }
482
432
  let bottomFrameIndex = Math.max(
483
- formattedStack.indexOf("react_stack_bottom_frame"),
484
- formattedStack.indexOf("react-stack-bottom-frame"),
433
+ ...REACT_STACK_BOTTOM_FRAME_PATTERNS.map((pattern) => formattedStack.indexOf(pattern)),
485
434
  );
486
435
  if (bottomFrameIndex !== -1) {
487
436
  bottomFrameIndex = formattedStack.lastIndexOf("\n", bottomFrameIndex);
@@ -498,11 +447,6 @@ export const formatOwnerStack = (stack: string): string => {
498
447
  return formattedStack;
499
448
  };
500
449
 
501
- interface DebugStackEntry {
502
- componentName: string;
503
- stackFrames: StackFrame[];
504
- }
505
-
506
450
  const isReactServerComponentFrame = (stackFrame: StackFrame): boolean =>
507
451
  Boolean(
508
452
  stackFrame.functionName && stackFrame.fileName && isServerComponentUrl(stackFrame.fileName),
@@ -514,24 +458,22 @@ const areStackFramesEqual = (firstFrame: StackFrame, secondFrame: StackFrame): b
514
458
  firstFrame.columnNumber === secondFrame.columnNumber;
515
459
 
516
460
  const buildFunctionNameToRscFramesMap = (
517
- debugStackEntries: DebugStackEntry[],
461
+ debugStackFrames: StackFrame[],
518
462
  ): Map<string, StackFrame[]> => {
519
463
  const functionNameToRscFrames = new Map<string, StackFrame[]>();
520
464
 
521
- for (const debugStackEntry of debugStackEntries) {
522
- for (const stackFrame of debugStackEntry.stackFrames) {
523
- if (!isReactServerComponentFrame(stackFrame)) continue;
465
+ for (const stackFrame of debugStackFrames) {
466
+ if (!isReactServerComponentFrame(stackFrame)) continue;
524
467
 
525
- const functionName = stackFrame.functionName!;
526
- const framesForFunction = functionNameToRscFrames.get(functionName) ?? [];
527
- const isDuplicateFrame = framesForFunction.some((existingFrame) =>
528
- areStackFramesEqual(existingFrame, stackFrame),
529
- );
468
+ const functionName = stackFrame.functionName!;
469
+ const framesForFunction = functionNameToRscFrames.get(functionName) ?? [];
470
+ const isDuplicateFrame = framesForFunction.some((existingFrame) =>
471
+ areStackFramesEqual(existingFrame, stackFrame),
472
+ );
530
473
 
531
- if (!isDuplicateFrame) {
532
- framesForFunction.push(stackFrame);
533
- functionNameToRscFrames.set(functionName, framesForFunction);
534
- }
474
+ if (!isDuplicateFrame) {
475
+ framesForFunction.push(stackFrame);
476
+ functionNameToRscFrames.set(functionName, framesForFunction);
535
477
  }
536
478
  }
537
479
 
@@ -592,7 +534,7 @@ const getOwnerStackFromDebugStacks = (fiber: Fiber): StackFrame[] => {
592
534
  while (owner) {
593
535
  if (isFiberOwner(owner)) {
594
536
  const ownerFiber: Fiber = owner;
595
- owner = getDebugOwner(ownerFiber);
537
+ owner = ownerFiber._debugOwner;
596
538
  if (owner && hasDebugStack(ownerFiber)) {
597
539
  const { frames, isTrusted } = parseDebugStack(ownerFiber._debugStack);
598
540
  if (isTrusted) {
@@ -616,51 +558,39 @@ const getOwnerStackFromDebugStacks = (fiber: Fiber): StackFrame[] => {
616
558
  return ownerStackFrames;
617
559
  };
618
560
 
619
- const getDebugStackEntries = (rootFiber: Fiber): DebugStackEntry[] => {
620
- const debugStackEntries: DebugStackEntry[] = [];
561
+ const getDebugStackFrames = (rootFiber: Fiber): StackFrame[] => {
562
+ const debugStackFrames: StackFrame[] = [];
621
563
 
622
564
  traverseFiber(
623
565
  rootFiber,
624
566
  (currentFiber) => {
625
567
  if (!hasDebugStack(currentFiber)) return;
626
568
 
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
- });
569
+ const { frames, isTrusted } = parseDebugStack(currentFiber._debugStack);
570
+ if (isTrusted) {
571
+ debugStackFrames.push(...frames);
572
+ }
636
573
  },
637
574
  true,
638
575
  );
639
576
 
640
- return debugStackEntries;
577
+ return debugStackFrames;
641
578
  };
642
579
 
643
580
  /**
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.
581
+ * The unsymbolicated frames behind {@link getParentStack}: bundle-space
582
+ * locations from re-invoking each component with a throwing dispatcher,
583
+ * with server frames enriched from debug stacks by name matching.
649
584
  */
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);
585
+ export const getRawParentStack = (fiber: Fiber): StackFrame[] => {
586
+ const debugStackFrames = getDebugStackFrames(fiber);
656
587
  const fallbackStackFrames = parseStack(getFallbackParentStack(fiber));
657
- const functionNameToRscFrames = buildFunctionNameToRscFramesMap(debugStackEntries);
588
+ const functionNameToRscFrames = buildFunctionNameToRscFramesMap(debugStackFrames);
658
589
  const functionNameToUsageIndex = new Map<string, number>();
659
590
 
660
591
  const enrichedStackFrames = fallbackStackFrames.map((stackFrame): StackFrame => {
661
592
  const isServerFrame =
662
- (stackFrame.source?.includes(SERVER_FRAME_MARKER) ?? false) ||
663
- (stackFrame.source != null && SERVER_ENV_PATTERN.test(stackFrame.source));
593
+ stackFrame.source !== undefined && SERVER_ENV_PATTERN.test(stackFrame.source);
664
594
 
665
595
  if (isServerFrame) {
666
596
  return getEnrichedServerStackFrame(
@@ -673,15 +603,26 @@ export const getParentStack = async (
673
603
  return stackFrame;
674
604
  });
675
605
 
676
- const deduplicatedStackFrames = enrichedStackFrames.filter((stackFrame, index, frames) => {
606
+ return enrichedStackFrames.filter((stackFrame, index, frames) => {
677
607
  if (index === 0) return true;
678
608
  const previousFrame = frames[index - 1];
679
609
  return stackFrame.functionName !== previousFrame.functionName;
680
610
  });
681
-
682
- return symbolicateStack(deduplicatedStackFrames, shouldCache, fetchFunction);
683
611
  };
684
612
 
613
+ /**
614
+ * Returns a stack of ALL ancestor components in the render tree (the fiber's
615
+ * `return` chain), including wrappers that render `{children}` without having
616
+ * created this fiber's JSX. Locations come from re-invoking each component
617
+ * with a throwing dispatcher; server frames are enriched from debug stacks by
618
+ * name matching. Works on every React version.
619
+ */
620
+ export const getParentStack = async (
621
+ fiber: Fiber,
622
+ shouldCache = true,
623
+ fetchFunction?: SourceFetch,
624
+ ): Promise<StackFrame[]> => symbolicateStack(getRawParentStack(fiber), shouldCache, fetchFunction);
625
+
685
626
  // an owner frame is only actionable if it can point an editor somewhere:
686
627
  // it needs a file location and must not be ignore-listed bundler/framework code
687
628
  const isLocatableFrame = (stackFrame: StackFrame): boolean =>
@@ -700,7 +641,7 @@ const isLocatableFrame = (stackFrame: StackFrame): boolean =>
700
641
  export const getOwnerStack = async (
701
642
  fiber: Fiber,
702
643
  shouldCache = true,
703
- fetchFunction?: (url: string) => Promise<Response>,
644
+ fetchFunction?: SourceFetch,
704
645
  ): Promise<StackFrame[]> => {
705
646
  const debugStackFrames = getOwnerStackFromDebugStacks(fiber);
706
647
  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
  };