bippy 0.7.2 → 0.7.3-dev.0c7e504

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.
@@ -1,8 +1,11 @@
1
+ import { getRenderer } from "../core.js";
2
+ import type { ReactDevToolsTarget } from "../rdt-hook.js";
1
3
  import type {
2
4
  Fiber,
3
5
  ContextDependency,
4
6
  MemoizedState,
5
7
  ReactContext,
8
+ ReactDebugInfo,
6
9
  RendererDispatcherRef,
7
10
  } from "../react-internals/index.js";
8
11
  import {
@@ -20,6 +23,7 @@ import {
20
23
 
21
24
  const REACT_CONTEXT_TYPE = Symbol.for("react.context");
22
25
  const REACT_MEMO_CACHE_SENTINEL = Symbol.for("react.memo_cache_sentinel");
26
+ const REACT_RECOVERABLE_TYPE = Symbol.for("react.recoverable");
23
27
 
24
28
  export interface HookSource {
25
29
  lineNumber: number | null;
@@ -34,6 +38,7 @@ export interface HooksNode {
34
38
  name: string;
35
39
  value: unknown;
36
40
  subHooks: HooksNode[];
41
+ debugInfo: ReactDebugInfo[] | null;
37
42
  hookSource: HookSource | null;
38
43
  }
39
44
 
@@ -44,6 +49,7 @@ interface HookLogEntry {
44
49
  primitive: string;
45
50
  stackError: Error;
46
51
  value: unknown;
52
+ debugInfo: ReactDebugInfo[] | null;
47
53
  dispatcherHookName: string;
48
54
  }
49
55
 
@@ -61,6 +67,7 @@ interface InspectableThenable {
61
67
  status?: unknown;
62
68
  then: (...arguments_: unknown[]) => unknown;
63
69
  value?: unknown;
70
+ _debugInfo?: ReactDebugInfo[];
64
71
  }
65
72
 
66
73
  interface ForwardRefRenderType {
@@ -68,6 +75,7 @@ interface ForwardRefRenderType {
68
75
  }
69
76
 
70
77
  interface InspectedActionState {
78
+ debugInfo: ReactDebugInfo[] | null;
71
79
  error: unknown;
72
80
  value: unknown;
73
81
  }
@@ -79,6 +87,16 @@ let currentHook: MemoizedState | null = null;
79
87
  let currentContextDependency: ContextDependency<unknown> | null = null;
80
88
  let currentThenableIndex = 0;
81
89
  let currentThenableState: unknown[] | null = null;
90
+ let currentMemoCacheIndex = 0;
91
+ let isInspectingHooks = false;
92
+
93
+ const assertNotInspectingHooks = (): void => {
94
+ if (isInspectingHooks) {
95
+ throw new BippyHookInspectionError(
96
+ "Hook inspection cannot be called during another inspection.",
97
+ );
98
+ }
99
+ };
82
100
 
83
101
  const SuspenseException: unknown = new Error(
84
102
  "Suspense interrupted this render. This error is an internal implementation detail of `use`.",
@@ -132,12 +150,14 @@ const pushHookLogEntry = (
132
150
  value: unknown,
133
151
  dispatcherHookName: string,
134
152
  displayName: string | null = null,
153
+ debugInfo: ReactDebugInfo[] | null = null,
135
154
  ): void => {
136
155
  hookLog.push({
137
156
  displayName,
138
157
  primitive,
139
158
  stackError: new Error(),
140
159
  value,
160
+ debugInfo,
141
161
  dispatcherHookName,
142
162
  });
143
163
  };
@@ -153,22 +173,26 @@ const dispatcherUse = (usable: unknown): unknown => {
153
173
 
154
174
  switch (thenable.status) {
155
175
  case "fulfilled": {
156
- pushHookLogEntry("Promise", thenable.value, "Use");
176
+ pushHookLogEntry("Promise", thenable.value, "Use", null, thenable._debugInfo ?? null);
157
177
  return thenable.value;
158
178
  }
159
179
  case "rejected":
160
180
  throw thenable.reason;
161
181
  }
162
- pushHookLogEntry("Unresolved", thenable, "Use");
182
+ pushHookLogEntry("Unresolved", thenable, "Use", null, thenable._debugInfo ?? null);
163
183
  throw SuspenseException;
164
184
  }
185
+ if ("$$typeof" in usable && usable.$$typeof === REACT_RECOVERABLE_TYPE) {
186
+ pushHookLogEntry("Recoverable", undefined, "Use");
187
+ return undefined;
188
+ }
165
189
  if ("$$typeof" in usable && usable.$$typeof === REACT_CONTEXT_TYPE && isReactContext(usable)) {
166
190
  const context: InspectableReactContext = {
167
191
  _currentValue: usable._currentValue,
168
192
  displayName: typeof usable.displayName === "string" ? usable.displayName : undefined,
169
193
  };
170
194
  const value = readContext(context);
171
- pushHookLogEntry("Context (use)", value, "Use", context.displayName ?? "Context");
195
+ pushHookLogEntry("Context (use)", value, "Use", context.displayName || "Context");
172
196
  return value;
173
197
  }
174
198
  }
@@ -177,7 +201,7 @@ const dispatcherUse = (usable: unknown): unknown => {
177
201
 
178
202
  const dispatcherUseContext = (context: InspectableReactContext): unknown => {
179
203
  const value = readContext(context);
180
- pushHookLogEntry("Context", value, "Context", context.displayName ?? null);
204
+ pushHookLogEntry("Context", value, "Context", context.displayName || null);
181
205
  return value;
182
206
  };
183
207
 
@@ -304,21 +328,13 @@ const dispatcherUseId = (): string => {
304
328
 
305
329
  const dispatcherUseMemoCache = (size: number): unknown[] => {
306
330
  const fiber = currentFiber;
307
- if (fiber === null || fiber === undefined) return [];
331
+ if (fiber === null) return [];
308
332
 
309
333
  const memoCache = fiber.updateQueue?.memoCache;
310
334
  if (memoCache === null || memoCache === undefined) return [];
311
335
 
312
- let memoCacheSlots = memoCache.data[memoCache.index];
313
- if (memoCacheSlots === undefined) {
314
- memoCacheSlots = memoCache.data[memoCache.index] = Array.from(
315
- { length: size },
316
- () => REACT_MEMO_CACHE_SENTINEL,
317
- );
318
- }
319
-
320
- memoCache.index++;
321
- return memoCacheSlots;
336
+ const memoCacheSlots = memoCache.data[currentMemoCacheIndex++];
337
+ return memoCacheSlots?.slice() ?? Array.from({ length: size }, () => REACT_MEMO_CACHE_SENTINEL);
322
338
  };
323
339
 
324
340
  const dispatcherUseOptimistic = (passthrough: unknown): [unknown, () => void] => {
@@ -333,6 +349,7 @@ const inspectActionStateHook = (
333
349
  initialState: unknown,
334
350
  ): InspectedActionState => {
335
351
  let value: unknown;
352
+ let debugInfo: ReactDebugInfo[] | null = null;
336
353
  let error: unknown = null;
337
354
  if (hook !== null) {
338
355
  const actionResult = hook.memoizedState;
@@ -340,6 +357,7 @@ const inspectActionStateHook = (
340
357
  switch (actionResult.status) {
341
358
  case "fulfilled":
342
359
  value = actionResult.value;
360
+ debugInfo = actionResult._debugInfo ?? null;
343
361
  break;
344
362
  case "rejected":
345
363
  error = actionResult.reason;
@@ -347,6 +365,7 @@ const inspectActionStateHook = (
347
365
  default:
348
366
  error = SuspenseException;
349
367
  value = actionResult;
368
+ debugInfo = actionResult._debugInfo ?? null;
350
369
  }
351
370
  } else {
352
371
  value = actionResult;
@@ -354,7 +373,7 @@ const inspectActionStateHook = (
354
373
  } else {
355
374
  value = initialState;
356
375
  }
357
- return { value, error };
376
+ return { value, debugInfo, error };
358
377
  };
359
378
 
360
379
  const createActionStateDispatcher =
@@ -363,8 +382,8 @@ const createActionStateDispatcher =
363
382
  const hook = nextHook();
364
383
  nextHook();
365
384
  nextHook();
366
- const { value, error } = inspectActionStateHook(hook, initialState);
367
- pushHookLogEntry(primitive, value, primitive);
385
+ const { value, debugInfo, error } = inspectActionStateHook(hook, initialState);
386
+ pushHookLogEntry(primitive, value, primitive, null, debugInfo);
368
387
  if (error !== null) throw error;
369
388
  return [value, () => {}, false];
370
389
  };
@@ -449,6 +468,7 @@ const getPrimitiveStackCache = (): Map<string, StackFrame[]> => {
449
468
  dispatcher.useActionState((state: unknown) => state, null);
450
469
  dispatcher.useHostTransitionStatus();
451
470
  dispatcher.use({ $$typeof: REACT_CONTEXT_TYPE, _currentValue: null });
471
+ dispatcher.use({ $$typeof: REACT_RECOVERABLE_TYPE });
452
472
  const fulfilledPromise = Promise.resolve(null);
453
473
  Reflect.set(fulfilledPromise, "status", "fulfilled");
454
474
  Reflect.set(fulfilledPromise, "value", null);
@@ -532,6 +552,19 @@ const parseHookName = (functionName: string | undefined): string => {
532
552
  return functionName.slice(startIndex);
533
553
  };
534
554
 
555
+ // Unlike upstream, bippy routes every dispatcher method through a shared log helper, so those
556
+ // frames appear in captured stacks and must not become tree levels. Names are read off the
557
+ // live functions rather than matched by prefix, so user hooks cannot collide and minified
558
+ // builds stay accurate.
559
+ const INTERNAL_HOOK_FRAME_NAMES = new Set(
560
+ [pushHookLogEntry, ...Object.values(dispatcher)].map((internalFunction) =>
561
+ parseHookName(internalFunction.name),
562
+ ),
563
+ );
564
+
565
+ const isInternalHookFrame = (hookName: string): boolean =>
566
+ hookName !== "" && INTERNAL_HOOK_FRAME_NAMES.has(hookName);
567
+
535
568
  const isReactWrapper = (functionName: string | undefined, wrapperName: string): boolean => {
536
569
  const hookName = parseHookName(functionName);
537
570
  if (wrapperName === "HostTransitionStatus") {
@@ -602,8 +635,10 @@ const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): Ho
602
635
  const [primitiveFrame, stack] = parseTrimmedStack(rootStack, hook);
603
636
  let displayName = hook.displayName;
604
637
  if (displayName === null && primitiveFrame !== null) {
605
- displayName =
606
- parseHookName(primitiveFrame.functionName) || parseHookName(hook.dispatcherHookName);
638
+ const primitiveName = parseHookName(primitiveFrame.functionName);
639
+ displayName = isInternalHookFrame(primitiveName)
640
+ ? null
641
+ : primitiveName || parseHookName(hook.dispatcherHookName);
607
642
  }
608
643
 
609
644
  if (stack !== null) {
@@ -628,6 +663,7 @@ const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): Ho
628
663
  name: parseHookName(stack[stackIndex - 1].functionName),
629
664
  value: undefined,
630
665
  subHooks: children,
666
+ debugInfo: null,
631
667
  hookSource: {
632
668
  lineNumber: stackFrame.lineNumber ?? null,
633
669
  columnNumber: stackFrame.columnNumber ?? null,
@@ -655,21 +691,52 @@ const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): Ho
655
691
  fileName: firstStackFrame?.fileName ?? null,
656
692
  };
657
693
 
658
- levelChildren.push({ id, isStateEditable, name, value: hook.value, subHooks: [], hookSource });
694
+ levelChildren.push({
695
+ id,
696
+ isStateEditable,
697
+ name,
698
+ value: hook.value,
699
+ subHooks: [],
700
+ debugInfo: hook.debugInfo,
701
+ hookSource,
702
+ });
659
703
  }
660
704
 
661
705
  processDebugValues(rootChildren, null);
706
+ removeInternalHookFrames(rootChildren, null);
662
707
  return rootChildren;
663
708
  };
664
709
 
710
+ const removeInternalHookFrames = (
711
+ hooksTree: HooksTree,
712
+ parentHooksNode: HooksNode | null,
713
+ ): void => {
714
+ for (let nodeIndex = 0; nodeIndex < hooksTree.length; nodeIndex++) {
715
+ const hooksNode = hooksTree[nodeIndex];
716
+ if (isInternalHookFrame(hooksNode.name)) {
717
+ if (parentHooksNode !== null && hooksNode.value !== undefined) {
718
+ if (parentHooksNode.value === undefined) parentHooksNode.value = hooksNode.value;
719
+ else if (Array.isArray(parentHooksNode.value)) {
720
+ parentHooksNode.value = [...parentHooksNode.value, hooksNode.value];
721
+ } else parentHooksNode.value = [parentHooksNode.value, hooksNode.value];
722
+ }
723
+ hooksTree.splice(nodeIndex, 1, ...hooksNode.subHooks);
724
+ nodeIndex--;
725
+ } else {
726
+ removeInternalHookFrames(hooksNode.subHooks, hooksNode);
727
+ }
728
+ }
729
+ };
730
+
665
731
  const processDebugValues = (hooksTree: HooksTree, parentHooksNode: HooksNode | null): void => {
666
732
  const debugValueNodes: HooksNode[] = [];
667
733
  for (let nodeIndex = 0; nodeIndex < hooksTree.length; nodeIndex++) {
668
734
  const hooksNode = hooksTree[nodeIndex];
669
- if (hooksNode.name === "DebugValue" && hooksNode.subHooks.length === 0) {
670
- hooksTree.splice(nodeIndex, 1);
735
+ if (hooksNode.name === "DebugValue") {
736
+ hooksTree.splice(nodeIndex, 1, ...hooksNode.subHooks);
671
737
  nodeIndex--;
672
- debugValueNodes.push(hooksNode);
738
+ // Internal frames surface as valueless DebugValue levels; only real labels bubble up.
739
+ if (hooksNode.value !== undefined) debugValueNodes.push(hooksNode);
673
740
  } else {
674
741
  processDebugValues(hooksNode.subHooks, hooksNode);
675
742
  }
@@ -723,8 +790,8 @@ const resolveDefaultProps = (
723
790
  baseProps: Record<string, unknown>,
724
791
  ): Record<string, unknown> => {
725
792
  if (
726
- component &&
727
- typeof component === "object" &&
793
+ component !== null &&
794
+ (typeof component === "object" || typeof component === "function") &&
728
795
  "defaultProps" in component &&
729
796
  typeof component.defaultProps === "object" &&
730
797
  component.defaultProps !== null
@@ -760,24 +827,29 @@ const restoreConsole = (originalMethods: Record<string, unknown>): void => {
760
827
  }
761
828
  };
762
829
 
763
- const performDispatcherInspection = (
830
+ // The render call must happen in the same frame that captures the ancestor stack,
831
+ // otherwise an extra frame lands in every hook's trimmed stack and becomes a bogus tree level.
832
+ const performDispatcherInspection = <TArgs extends unknown[]>(
764
833
  dispatcherRef: RendererDispatcherRef,
765
- renderFn: () => void,
834
+ renderFunction: (...args: TArgs) => unknown,
835
+ ...renderArgs: TArgs
766
836
  ): HooksTree => {
767
837
  const previousDispatcher = readDispatcher(dispatcherRef);
768
838
  writeDispatcher(dispatcherRef, dispatcherProxy);
839
+ isInspectingHooks = true;
769
840
 
770
841
  let capturedHookLog: HookLogEntry[] = [];
771
842
  let ancestorStackError: Error | undefined;
772
843
 
773
844
  try {
774
845
  ancestorStackError = new Error();
775
- renderFn();
846
+ renderFunction(...renderArgs);
776
847
  } catch (renderError) {
777
848
  handleRenderFunctionError(renderError);
778
849
  } finally {
779
850
  capturedHookLog = hookLog;
780
851
  hookLog = [];
852
+ isInspectingHooks = false;
781
853
  writeDispatcher(dispatcherRef, previousDispatcher);
782
854
  }
783
855
 
@@ -785,8 +857,12 @@ const performDispatcherInspection = (
785
857
  return buildTree(rootStack, capturedHookLog);
786
858
  };
787
859
 
788
- const requireDispatcherRef = (): RendererDispatcherRef => {
789
- const dispatcherRef = getRendererDispatcherRefs()[0];
860
+ const requireDispatcherRef = (
861
+ fiber?: Fiber,
862
+ target: ReactDevToolsTarget = globalThis,
863
+ ): RendererDispatcherRef => {
864
+ const rendererDispatcherRef = fiber ? getRenderer(fiber, target)?.currentDispatcherRef : null;
865
+ const dispatcherRef = rendererDispatcherRef ?? getRendererDispatcherRefs(target)[0];
790
866
  if (!dispatcherRef) {
791
867
  throw new BippyHookInspectionError(
792
868
  "Bippy couldn’t find a React renderer. Load React and install Bippy’s hook.",
@@ -823,8 +899,75 @@ const resolveContextDependency = (fiber: Fiber): void => {
823
899
  }
824
900
  };
825
901
 
902
+ const normalizeStandaloneHooks = (hooksTree: HooksTree): HooksTree => {
903
+ const normalizeNode = (hooksNode: HooksNode): HooksNode => {
904
+ const subHooks = hooksNode.subHooks.map(normalizeNode);
905
+ if (subHooks.length === 1) {
906
+ const childHook = subHooks[0];
907
+ if (hooksNode.name === "Context" && childHook.id === null) {
908
+ return { ...childHook, hookSource: hooksNode.hookSource };
909
+ }
910
+ if (hooksNode.name === "FormStatus" && childHook.name === "HostTransitionStatus") {
911
+ return { ...hooksNode, subHooks: [], value: childHook.value };
912
+ }
913
+ if (hooksNode.name === "Use") {
914
+ if (childHook.name === "Context") {
915
+ return { ...childHook, hookSource: hooksNode.hookSource };
916
+ }
917
+ if (childHook.name === "Promise" || childHook.name === "Unresolved") {
918
+ return {
919
+ ...hooksNode,
920
+ debugInfo: childHook.debugInfo,
921
+ subHooks: [],
922
+ value: childHook.value,
923
+ };
924
+ }
925
+ }
926
+ if (
927
+ hooksNode.id === null &&
928
+ hooksNode.value === undefined &&
929
+ (hooksNode.name === childHook.name || hooksNode.name === "<anonymous>")
930
+ ) {
931
+ return { ...childHook, hookSource: hooksNode.hookSource };
932
+ }
933
+ }
934
+ return { ...hooksNode, subHooks };
935
+ };
936
+
937
+ return hooksTree.map(normalizeNode);
938
+ };
939
+
940
+ export const inspectHooks = (
941
+ renderFunction: (props: Record<string, unknown>) => unknown,
942
+ props: Record<string, unknown>,
943
+ target: ReactDevToolsTarget = globalThis,
944
+ ): HooksTree => {
945
+ assertNotInspectingHooks();
946
+ const dispatcherRef = requireDispatcherRef(undefined, target);
947
+ getPrimitiveStackCache();
948
+ currentHook = null;
949
+ currentFiber = null;
950
+ currentContextDependency = null;
951
+ currentThenableState = null;
952
+ currentThenableIndex = 0;
953
+ const originalConsoleMethods = suppressConsole();
954
+ try {
955
+ return normalizeStandaloneHooks(
956
+ performDispatcherInspection(dispatcherRef, renderFunction, props),
957
+ );
958
+ } finally {
959
+ currentHook = null;
960
+ currentFiber = null;
961
+ currentContextDependency = null;
962
+ currentThenableState = null;
963
+ currentThenableIndex = 0;
964
+ restoreConsole(originalConsoleMethods);
965
+ }
966
+ };
967
+
826
968
  export const getFiberHooks = (fiber: Fiber): HooksTree => {
827
- const dispatcherRef = requireDispatcherRef();
969
+ assertNotInspectingHooks();
970
+ const dispatcherRef = requireDispatcherRef(fiber);
828
971
  const workTags = getReactWorkTagsForFiber(fiber);
829
972
 
830
973
  if (
@@ -839,6 +982,7 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
839
982
 
840
983
  currentHook = fiber.memoizedState;
841
984
  currentFiber = fiber;
985
+ currentMemoCacheIndex = 0;
842
986
 
843
987
  const debugThenableState = fiber.dependencies?._debugThenableState;
844
988
  const usedThenables = Array.isArray(debugThenableState)
@@ -847,18 +991,16 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
847
991
  currentThenableState = Array.isArray(usedThenables) ? usedThenables : null;
848
992
  currentThenableIndex = 0;
849
993
 
850
- resolveContextDependency(fiber);
851
-
852
- const type = fiber.type;
853
- let props = fiber.memoizedProps;
854
- if (type !== fiber.elementType) {
855
- props = resolveDefaultProps(type, props);
856
- }
857
-
858
994
  const originalConsoleMethods = suppressConsole();
859
995
  const contextMap = new Map<ReactContext<unknown>, unknown>();
860
996
 
861
997
  try {
998
+ resolveContextDependency(fiber);
999
+ const type = fiber.type;
1000
+ const props =
1001
+ type !== fiber.elementType
1002
+ ? resolveDefaultProps(type, fiber.memoizedProps)
1003
+ : fiber.memoizedProps;
862
1004
  if (
863
1005
  currentContextDependency !== null &&
864
1006
  !Object.hasOwn(currentContextDependency, "memoizedValue")
@@ -870,9 +1012,15 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
870
1012
  if (!isForwardRefRenderType(type)) {
871
1013
  throw new BippyHookInspectionError("ForwardRef fiber is missing its render function.");
872
1014
  }
873
- return performDispatcherInspection(dispatcherRef, () => {
874
- type.render(props, fiber.ref);
875
- });
1015
+ const hooksTree = normalizeStandaloneHooks(
1016
+ performDispatcherInspection(dispatcherRef, type.render, props, fiber.ref),
1017
+ );
1018
+ for (const hooksNode of hooksTree) {
1019
+ if (hooksNode.hookSource?.functionName === "Object.render") {
1020
+ hooksNode.hookSource.functionName = null;
1021
+ }
1022
+ }
1023
+ return hooksTree;
876
1024
  }
877
1025
 
878
1026
  if (typeof type !== "function") {
@@ -880,15 +1028,14 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
880
1028
  "Function component fiber is missing its component function.",
881
1029
  );
882
1030
  }
883
- return performDispatcherInspection(dispatcherRef, () => {
884
- type(props);
885
- });
1031
+ return normalizeStandaloneHooks(performDispatcherInspection(dispatcherRef, type, props));
886
1032
  } finally {
887
1033
  currentFiber = null;
888
1034
  currentHook = null;
889
1035
  currentContextDependency = null;
890
1036
  currentThenableState = null;
891
1037
  currentThenableIndex = 0;
1038
+ currentMemoCacheIndex = 0;
892
1039
  restoreContexts(contextMap);
893
1040
  restoreConsole(originalConsoleMethods);
894
1041
  }
@@ -7,7 +7,6 @@ import type {
7
7
  } from "../react-internals/index.js";
8
8
  import {
9
9
  REACT_STACK_BOTTOM_FRAME_PATTERNS,
10
- SERVER_FRAME_MARKER,
11
10
  SERVER_ENV_PATTERN,
12
11
  SERVER_COMPONENT_URL_PREFIXES,
13
12
  } from "./constants.js";
@@ -19,7 +18,11 @@ import {
19
18
  readDispatcher,
20
19
  writeDispatcher,
21
20
  } from "./renderer-dispatchers.js";
22
- import { symbolicateStack, type SourceFetch } from "./symbolication.js";
21
+ import {
22
+ symbolicateStack,
23
+ type SourceFetch,
24
+ type SourceMapRequestOptions,
25
+ } from "./symbolication.js";
23
26
 
24
27
  interface RendererDispatcherSnapshot {
25
28
  currentDispatcherRef: RendererDispatcherRef;
@@ -101,12 +104,18 @@ const describeBuiltInComponentFrame = (name: string): string => {
101
104
  return `\n in ${name}`;
102
105
  };
103
106
 
104
- export const describeDebugInfoFrame = (name: string, env?: string): string => {
105
- let frameDescription = describeBuiltInComponentFrame(name);
106
- if (env) {
107
- frameDescription += ` (at ${env})`;
107
+ export const describeDebugInfoFrame = (
108
+ name: string,
109
+ env?: string,
110
+ location?: Error | null,
111
+ ): string => {
112
+ if (location) {
113
+ const childStack = formatOwnerStack(location.stack ?? "");
114
+ const lastNewlineIndex = childStack.lastIndexOf("\n");
115
+ const lastLine = lastNewlineIndex === -1 ? childStack : childStack.slice(lastNewlineIndex + 1);
116
+ if (lastLine.includes(name)) return `\n${lastLine}`;
108
117
  }
109
- return frameDescription;
118
+ return describeBuiltInComponentFrame(`${name}${env ? ` [${env}]` : ""}`);
110
119
  };
111
120
 
112
121
  let reEntry = false;
@@ -115,7 +124,7 @@ let reEntry = false;
115
124
  // component type like React DevTools does.
116
125
  const componentFrameCache = new WeakMap<React.ComponentType<unknown>, string>();
117
126
 
118
- // https://github.com/facebook/react/blob/f739642745577a8e4dcb9753836ac3589b9c590a/packages/react-devtools-shared/src/backend/shared/DevToolsComponentStackFrame.js#L22
127
+ // https://github.com/facebook/react/blob/eafeac0/packages/shared/ReactComponentStackFrame.js#L64
119
128
  const describeNativeComponentFrame = (
120
129
  component: React.ComponentType<unknown>,
121
130
  construct: boolean,
@@ -153,21 +162,12 @@ const describeNativeComponentFrame = (
153
162
  throw Error();
154
163
  },
155
164
  });
156
- if (typeof Reflect === "object" && Reflect.construct) {
157
- try {
158
- Reflect.construct(ThrowingConstructor, []);
159
- } catch (caughtError) {
160
- control = caughtError;
161
- }
162
- Reflect.construct(component, [], ThrowingConstructor);
163
- } else {
164
- try {
165
- Function.prototype.apply.call(ThrowingConstructor, undefined, []);
166
- } catch (caughtError) {
167
- control = caughtError;
168
- }
169
- Function.prototype.apply.call(component, ThrowingConstructor.prototype, []);
165
+ try {
166
+ Reflect.construct(ThrowingConstructor, []);
167
+ } catch (caughtError) {
168
+ control = caughtError;
170
169
  }
170
+ Reflect.construct(component, [], ThrowingConstructor);
171
171
  } else {
172
172
  try {
173
173
  throw Error();
@@ -312,7 +312,7 @@ const describeNativeComponentFrame = (
312
312
  return syntheticFrame;
313
313
  };
314
314
 
315
- // https://github.com/facebook/react/blob/ac3e705a18696168acfcaed39dce0cfaa6be8836/packages/react-reconciler/src/ReactFiberComponentStack.js#L180
315
+ // https://github.com/facebook/react/blob/eafeac0/packages/react-reconciler/src/ReactFiberComponentStack.js#L37
316
316
  export const describeFiber = (fiber: Fiber, childFiber: Fiber | null): string => {
317
317
  let stackFrame = "";
318
318
  const workTags = getReactWorkTagsForFiber(fiber);
@@ -384,7 +384,11 @@ export const getFallbackParentStack = (thisFiber: Fiber): string => {
384
384
  for (let debugInfoIndex = debugInfo.length - 1; debugInfoIndex >= 0; debugInfoIndex--) {
385
385
  const debugEntry = debugInfo[debugInfoIndex];
386
386
  if (typeof debugEntry.name === "string") {
387
- componentStack += describeDebugInfoFrame(debugEntry.name, debugEntry.env);
387
+ componentStack += describeDebugInfoFrame(
388
+ debugEntry.name,
389
+ debugEntry.env,
390
+ debugEntry.debugLocation,
391
+ );
388
392
  }
389
393
  }
390
394
  }
@@ -511,7 +515,7 @@ const getEnrichedServerStackFrame = (
511
515
  lineNumber: resolvedRscFrame.lineNumber,
512
516
  columnNumber: resolvedRscFrame.columnNumber,
513
517
  source: serverFrame.source?.replace(
514
- SERVER_FRAME_MARKER,
518
+ SERVER_ENV_PATTERN,
515
519
  `(${resolvedRscFrame.fileName}:${resolvedRscFrame.lineNumber}:${resolvedRscFrame.columnNumber})`,
516
520
  ),
517
521
  };
@@ -534,7 +538,7 @@ const markFlightServerFrame = (stackFrame: StackFrame): StackFrame =>
534
538
  * carries `.debugStack`). This is exact - no re-invoking components, no
535
539
  * name-matching heuristics - but requires React 19.
536
540
  */
537
- const getOwnerStackFromDebugStacks = (fiber: Fiber): StackFrame[] => {
541
+ export const getRawOwnerStack = (fiber: Fiber): StackFrame[] => {
538
542
  const ownerStackFrames: StackFrame[] = [];
539
543
  let owner: Fiber | ServerComponentInfo | null | undefined = fiber;
540
544
  while (owner) {
@@ -625,9 +629,11 @@ export const getRawParentStack = (fiber: Fiber): StackFrame[] => {
625
629
  */
626
630
  export const getParentStack = async (
627
631
  fiber: Fiber,
628
- shouldCache = true,
629
- fetchFunction?: SourceFetch,
630
- ): Promise<StackFrame[]> => symbolicateStack(getRawParentStack(fiber), shouldCache, fetchFunction);
632
+ shouldUseCache = true,
633
+ sourceFetch?: SourceFetch,
634
+ requestOptions: SourceMapRequestOptions = {},
635
+ ): Promise<StackFrame[]> =>
636
+ symbolicateStack(getRawParentStack(fiber), shouldUseCache, sourceFetch, requestOptions);
631
637
 
632
638
  // an owner frame is only actionable if it can point an editor somewhere:
633
639
  // it needs a file location and must not be ignore-listed bundler/framework code
@@ -646,10 +652,11 @@ const isLocatableFrame = (stackFrame: StackFrame): boolean =>
646
652
  */
647
653
  export const getOwnerStack = async (
648
654
  fiber: Fiber,
649
- shouldCache = true,
650
- fetchFunction?: SourceFetch,
655
+ shouldUseCache = true,
656
+ sourceFetch?: SourceFetch,
657
+ requestOptions: SourceMapRequestOptions = {},
651
658
  ): Promise<StackFrame[]> => {
652
- const debugStackFrames = getOwnerStackFromDebugStacks(fiber);
659
+ const debugStackFrames = getRawOwnerStack(fiber);
653
660
  if (debugStackFrames.length > 0) {
654
661
  // the owner chain does not include the fiber itself, but bippy's stacks
655
662
  // always start with the fiber's own frame
@@ -657,8 +664,9 @@ export const getOwnerStack = async (
657
664
  selfFrame.functionName = getDisplayName(fiber.type) ?? selfFrame.functionName;
658
665
  const symbolicatedFrames = await symbolicateStack(
659
666
  [selfFrame, ...debugStackFrames],
660
- shouldCache,
661
- fetchFunction,
667
+ shouldUseCache,
668
+ sourceFetch,
669
+ requestOptions,
662
670
  );
663
671
  const hasLocatableOwnerFrame = symbolicatedFrames.some(
664
672
  (stackFrame, frameIndex) => frameIndex > 0 && isLocatableFrame(stackFrame),
@@ -668,5 +676,5 @@ export const getOwnerStack = async (
668
676
  }
669
677
  }
670
678
 
671
- return getParentStack(fiber, shouldCache, fetchFunction);
679
+ return getParentStack(fiber, shouldUseCache, sourceFetch, requestOptions);
672
680
  };
@@ -1,6 +1,6 @@
1
1
  import { JSX_FACTORY_FRAME_COUNT, REACT_STACK_BOTTOM_FRAME_PATTERNS } from "./constants.js";
2
2
  import { getPrepareStackTrace, setPrepareStackTrace } from "./error-stack.js";
3
- import { parseStack, StackFrame } from "./parse-stack.js";
3
+ import { parseStack, type StackFrame } from "./parse-stack.js";
4
4
 
5
5
  interface V8CallSite {
6
6
  getFunctionName?: () => string | null;