bippy 0.6.1 → 0.7.0-dev.07e6032

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