bippy 0.7.2 → 0.7.3-dev.0ac832d

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 {
@@ -11,7 +14,7 @@ import {
11
14
  BippyUnsupportedHookError,
12
15
  } from "../errors.js";
13
16
  import { getReactWorkTagsForFiber } from "../react-internals/index.js";
14
- import { parseStack, type StackFrame } from "./parse-stack.js";
17
+ import { createStackParser, parseStack, type StackFrame } from "./parse-stack.js";
15
18
  import {
16
19
  getRendererDispatcherRefs,
17
20
  readDispatcher,
@@ -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") {
@@ -571,8 +604,9 @@ const findPrimitiveIndex = (hookStack: StackFrame[], hook: HookLogEntry): number
571
604
  const parseTrimmedStack = (
572
605
  rootStack: StackFrame[],
573
606
  hook: HookLogEntry,
607
+ parseHookStack: (stack: string) => StackFrame[],
574
608
  ): [StackFrame | null, StackFrame[] | null] => {
575
- const hookStack = parseErrorStack(hook.stackError);
609
+ const hookStack = parseHookStack(hook.stackError.stack || "");
576
610
  const rootIndex = findCommonAncestorIndex(rootStack, hookStack);
577
611
  const primitiveIndex = findPrimitiveIndex(hookStack, hook);
578
612
  if (rootIndex === -1 || primitiveIndex === -1 || rootIndex - primitiveIndex < 2) {
@@ -593,17 +627,20 @@ const NON_ID_HOOK_PRIMITIVES = new Set([
593
627
 
594
628
  const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): HooksTree => {
595
629
  const rootChildren: HooksNode[] = [];
630
+ const parseHookStack = createStackParser();
596
631
  let previousStack: StackFrame[] | null = null;
597
632
  let levelChildren = rootChildren;
598
633
  let nativeHookID = 0;
599
634
  const childrenStack: HooksNode[][] = [];
600
635
 
601
636
  for (const hook of capturedHookLog) {
602
- const [primitiveFrame, stack] = parseTrimmedStack(rootStack, hook);
637
+ const [primitiveFrame, stack] = parseTrimmedStack(rootStack, hook, parseHookStack);
603
638
  let displayName = hook.displayName;
604
639
  if (displayName === null && primitiveFrame !== null) {
605
- displayName =
606
- parseHookName(primitiveFrame.functionName) || parseHookName(hook.dispatcherHookName);
640
+ const primitiveName = parseHookName(primitiveFrame.functionName);
641
+ displayName = isInternalHookFrame(primitiveName)
642
+ ? null
643
+ : primitiveName || parseHookName(hook.dispatcherHookName);
607
644
  }
608
645
 
609
646
  if (stack !== null) {
@@ -628,6 +665,7 @@ const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): Ho
628
665
  name: parseHookName(stack[stackIndex - 1].functionName),
629
666
  value: undefined,
630
667
  subHooks: children,
668
+ debugInfo: null,
631
669
  hookSource: {
632
670
  lineNumber: stackFrame.lineNumber ?? null,
633
671
  columnNumber: stackFrame.columnNumber ?? null,
@@ -655,21 +693,52 @@ const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): Ho
655
693
  fileName: firstStackFrame?.fileName ?? null,
656
694
  };
657
695
 
658
- levelChildren.push({ id, isStateEditable, name, value: hook.value, subHooks: [], hookSource });
696
+ levelChildren.push({
697
+ id,
698
+ isStateEditable,
699
+ name,
700
+ value: hook.value,
701
+ subHooks: [],
702
+ debugInfo: hook.debugInfo,
703
+ hookSource,
704
+ });
659
705
  }
660
706
 
661
707
  processDebugValues(rootChildren, null);
708
+ removeInternalHookFrames(rootChildren, null);
662
709
  return rootChildren;
663
710
  };
664
711
 
712
+ const removeInternalHookFrames = (
713
+ hooksTree: HooksTree,
714
+ parentHooksNode: HooksNode | null,
715
+ ): void => {
716
+ for (let nodeIndex = 0; nodeIndex < hooksTree.length; nodeIndex++) {
717
+ const hooksNode = hooksTree[nodeIndex];
718
+ if (isInternalHookFrame(hooksNode.name)) {
719
+ if (parentHooksNode !== null && hooksNode.value !== undefined) {
720
+ if (parentHooksNode.value === undefined) parentHooksNode.value = hooksNode.value;
721
+ else if (Array.isArray(parentHooksNode.value)) {
722
+ parentHooksNode.value = [...parentHooksNode.value, hooksNode.value];
723
+ } else parentHooksNode.value = [parentHooksNode.value, hooksNode.value];
724
+ }
725
+ hooksTree.splice(nodeIndex, 1, ...hooksNode.subHooks);
726
+ nodeIndex--;
727
+ } else {
728
+ removeInternalHookFrames(hooksNode.subHooks, hooksNode);
729
+ }
730
+ }
731
+ };
732
+
665
733
  const processDebugValues = (hooksTree: HooksTree, parentHooksNode: HooksNode | null): void => {
666
734
  const debugValueNodes: HooksNode[] = [];
667
735
  for (let nodeIndex = 0; nodeIndex < hooksTree.length; nodeIndex++) {
668
736
  const hooksNode = hooksTree[nodeIndex];
669
- if (hooksNode.name === "DebugValue" && hooksNode.subHooks.length === 0) {
670
- hooksTree.splice(nodeIndex, 1);
737
+ if (hooksNode.name === "DebugValue") {
738
+ hooksTree.splice(nodeIndex, 1, ...hooksNode.subHooks);
671
739
  nodeIndex--;
672
- debugValueNodes.push(hooksNode);
740
+ // Internal frames surface as valueless DebugValue levels; only real labels bubble up.
741
+ if (hooksNode.value !== undefined) debugValueNodes.push(hooksNode);
673
742
  } else {
674
743
  processDebugValues(hooksNode.subHooks, hooksNode);
675
744
  }
@@ -723,8 +792,8 @@ const resolveDefaultProps = (
723
792
  baseProps: Record<string, unknown>,
724
793
  ): Record<string, unknown> => {
725
794
  if (
726
- component &&
727
- typeof component === "object" &&
795
+ component !== null &&
796
+ (typeof component === "object" || typeof component === "function") &&
728
797
  "defaultProps" in component &&
729
798
  typeof component.defaultProps === "object" &&
730
799
  component.defaultProps !== null
@@ -760,24 +829,29 @@ const restoreConsole = (originalMethods: Record<string, unknown>): void => {
760
829
  }
761
830
  };
762
831
 
763
- const performDispatcherInspection = (
832
+ // The render call must happen in the same frame that captures the ancestor stack,
833
+ // otherwise an extra frame lands in every hook's trimmed stack and becomes a bogus tree level.
834
+ const performDispatcherInspection = <TArgs extends unknown[]>(
764
835
  dispatcherRef: RendererDispatcherRef,
765
- renderFn: () => void,
836
+ renderFunction: (...args: TArgs) => unknown,
837
+ ...renderArgs: TArgs
766
838
  ): HooksTree => {
767
839
  const previousDispatcher = readDispatcher(dispatcherRef);
768
840
  writeDispatcher(dispatcherRef, dispatcherProxy);
841
+ isInspectingHooks = true;
769
842
 
770
843
  let capturedHookLog: HookLogEntry[] = [];
771
844
  let ancestorStackError: Error | undefined;
772
845
 
773
846
  try {
774
847
  ancestorStackError = new Error();
775
- renderFn();
848
+ renderFunction(...renderArgs);
776
849
  } catch (renderError) {
777
850
  handleRenderFunctionError(renderError);
778
851
  } finally {
779
852
  capturedHookLog = hookLog;
780
853
  hookLog = [];
854
+ isInspectingHooks = false;
781
855
  writeDispatcher(dispatcherRef, previousDispatcher);
782
856
  }
783
857
 
@@ -785,8 +859,12 @@ const performDispatcherInspection = (
785
859
  return buildTree(rootStack, capturedHookLog);
786
860
  };
787
861
 
788
- const requireDispatcherRef = (): RendererDispatcherRef => {
789
- const dispatcherRef = getRendererDispatcherRefs()[0];
862
+ const requireDispatcherRef = (
863
+ fiber?: Fiber,
864
+ target: ReactDevToolsTarget = globalThis,
865
+ ): RendererDispatcherRef => {
866
+ const rendererDispatcherRef = fiber ? getRenderer(fiber, target)?.currentDispatcherRef : null;
867
+ const dispatcherRef = rendererDispatcherRef ?? getRendererDispatcherRefs(target)[0];
790
868
  if (!dispatcherRef) {
791
869
  throw new BippyHookInspectionError(
792
870
  "Bippy couldn’t find a React renderer. Load React and install Bippy’s hook.",
@@ -823,8 +901,75 @@ const resolveContextDependency = (fiber: Fiber): void => {
823
901
  }
824
902
  };
825
903
 
904
+ const normalizeStandaloneHooks = (hooksTree: HooksTree): HooksTree => {
905
+ const normalizeNode = (hooksNode: HooksNode): HooksNode => {
906
+ const subHooks = hooksNode.subHooks.map(normalizeNode);
907
+ if (subHooks.length === 1) {
908
+ const childHook = subHooks[0];
909
+ if (hooksNode.name === "Context" && childHook.id === null) {
910
+ return { ...childHook, hookSource: hooksNode.hookSource };
911
+ }
912
+ if (hooksNode.name === "FormStatus" && childHook.name === "HostTransitionStatus") {
913
+ return { ...hooksNode, subHooks: [], value: childHook.value };
914
+ }
915
+ if (hooksNode.name === "Use") {
916
+ if (childHook.name === "Context") {
917
+ return { ...childHook, hookSource: hooksNode.hookSource };
918
+ }
919
+ if (childHook.name === "Promise" || childHook.name === "Unresolved") {
920
+ return {
921
+ ...hooksNode,
922
+ debugInfo: childHook.debugInfo,
923
+ subHooks: [],
924
+ value: childHook.value,
925
+ };
926
+ }
927
+ }
928
+ if (
929
+ hooksNode.id === null &&
930
+ hooksNode.value === undefined &&
931
+ (hooksNode.name === childHook.name || hooksNode.name === "<anonymous>")
932
+ ) {
933
+ return { ...childHook, hookSource: hooksNode.hookSource };
934
+ }
935
+ }
936
+ return { ...hooksNode, subHooks };
937
+ };
938
+
939
+ return hooksTree.map(normalizeNode);
940
+ };
941
+
942
+ export const inspectHooks = (
943
+ renderFunction: (props: Record<string, unknown>) => unknown,
944
+ props: Record<string, unknown>,
945
+ target: ReactDevToolsTarget = globalThis,
946
+ ): HooksTree => {
947
+ assertNotInspectingHooks();
948
+ const dispatcherRef = requireDispatcherRef(undefined, target);
949
+ getPrimitiveStackCache();
950
+ currentHook = null;
951
+ currentFiber = null;
952
+ currentContextDependency = null;
953
+ currentThenableState = null;
954
+ currentThenableIndex = 0;
955
+ const originalConsoleMethods = suppressConsole();
956
+ try {
957
+ return normalizeStandaloneHooks(
958
+ performDispatcherInspection(dispatcherRef, renderFunction, props),
959
+ );
960
+ } finally {
961
+ currentHook = null;
962
+ currentFiber = null;
963
+ currentContextDependency = null;
964
+ currentThenableState = null;
965
+ currentThenableIndex = 0;
966
+ restoreConsole(originalConsoleMethods);
967
+ }
968
+ };
969
+
826
970
  export const getFiberHooks = (fiber: Fiber): HooksTree => {
827
- const dispatcherRef = requireDispatcherRef();
971
+ assertNotInspectingHooks();
972
+ const dispatcherRef = requireDispatcherRef(fiber);
828
973
  const workTags = getReactWorkTagsForFiber(fiber);
829
974
 
830
975
  if (
@@ -839,6 +984,7 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
839
984
 
840
985
  currentHook = fiber.memoizedState;
841
986
  currentFiber = fiber;
987
+ currentMemoCacheIndex = 0;
842
988
 
843
989
  const debugThenableState = fiber.dependencies?._debugThenableState;
844
990
  const usedThenables = Array.isArray(debugThenableState)
@@ -847,18 +993,16 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
847
993
  currentThenableState = Array.isArray(usedThenables) ? usedThenables : null;
848
994
  currentThenableIndex = 0;
849
995
 
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
996
  const originalConsoleMethods = suppressConsole();
859
997
  const contextMap = new Map<ReactContext<unknown>, unknown>();
860
998
 
861
999
  try {
1000
+ resolveContextDependency(fiber);
1001
+ const type = fiber.type;
1002
+ const props =
1003
+ type !== fiber.elementType
1004
+ ? resolveDefaultProps(type, fiber.memoizedProps)
1005
+ : fiber.memoizedProps;
862
1006
  if (
863
1007
  currentContextDependency !== null &&
864
1008
  !Object.hasOwn(currentContextDependency, "memoizedValue")
@@ -870,9 +1014,15 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
870
1014
  if (!isForwardRefRenderType(type)) {
871
1015
  throw new BippyHookInspectionError("ForwardRef fiber is missing its render function.");
872
1016
  }
873
- return performDispatcherInspection(dispatcherRef, () => {
874
- type.render(props, fiber.ref);
875
- });
1017
+ const hooksTree = normalizeStandaloneHooks(
1018
+ performDispatcherInspection(dispatcherRef, type.render, props, fiber.ref),
1019
+ );
1020
+ for (const hooksNode of hooksTree) {
1021
+ if (hooksNode.hookSource?.functionName === "Object.render") {
1022
+ hooksNode.hookSource.functionName = null;
1023
+ }
1024
+ }
1025
+ return hooksTree;
876
1026
  }
877
1027
 
878
1028
  if (typeof type !== "function") {
@@ -880,15 +1030,14 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
880
1030
  "Function component fiber is missing its component function.",
881
1031
  );
882
1032
  }
883
- return performDispatcherInspection(dispatcherRef, () => {
884
- type(props);
885
- });
1033
+ return normalizeStandaloneHooks(performDispatcherInspection(dispatcherRef, type, props));
886
1034
  } finally {
887
1035
  currentFiber = null;
888
1036
  currentHook = null;
889
1037
  currentContextDependency = null;
890
1038
  currentThenableState = null;
891
1039
  currentThenableIndex = 0;
1040
+ currentMemoCacheIndex = 0;
892
1041
  restoreContexts(contextMap);
893
1042
  restoreConsole(originalConsoleMethods);
894
1043
  }
@@ -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
  };