bippy 0.7.2-dev.eecabd1 → 0.7.3-dev.1d4f35b

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
  }
@@ -132,12 +140,14 @@ const pushHookLogEntry = (
132
140
  value: unknown,
133
141
  dispatcherHookName: string,
134
142
  displayName: string | null = null,
143
+ debugInfo: ReactDebugInfo[] | null = null,
135
144
  ): void => {
136
145
  hookLog.push({
137
146
  displayName,
138
147
  primitive,
139
148
  stackError: new Error(),
140
149
  value,
150
+ debugInfo,
141
151
  dispatcherHookName,
142
152
  });
143
153
  };
@@ -153,22 +163,26 @@ const dispatcherUse = (usable: unknown): unknown => {
153
163
 
154
164
  switch (thenable.status) {
155
165
  case "fulfilled": {
156
- pushHookLogEntry("Promise", thenable.value, "Use");
166
+ pushHookLogEntry("Promise", thenable.value, "Use", null, thenable._debugInfo ?? null);
157
167
  return thenable.value;
158
168
  }
159
169
  case "rejected":
160
170
  throw thenable.reason;
161
171
  }
162
- pushHookLogEntry("Unresolved", thenable, "Use");
172
+ pushHookLogEntry("Unresolved", thenable, "Use", null, thenable._debugInfo ?? null);
163
173
  throw SuspenseException;
164
174
  }
175
+ if ("$$typeof" in usable && usable.$$typeof === REACT_RECOVERABLE_TYPE) {
176
+ pushHookLogEntry("Recoverable", undefined, "Use");
177
+ return undefined;
178
+ }
165
179
  if ("$$typeof" in usable && usable.$$typeof === REACT_CONTEXT_TYPE && isReactContext(usable)) {
166
180
  const context: InspectableReactContext = {
167
181
  _currentValue: usable._currentValue,
168
182
  displayName: typeof usable.displayName === "string" ? usable.displayName : undefined,
169
183
  };
170
184
  const value = readContext(context);
171
- pushHookLogEntry("Context (use)", value, "Use", context.displayName ?? "Context");
185
+ pushHookLogEntry("Context (use)", value, "Use", context.displayName || "Context");
172
186
  return value;
173
187
  }
174
188
  }
@@ -177,7 +191,7 @@ const dispatcherUse = (usable: unknown): unknown => {
177
191
 
178
192
  const dispatcherUseContext = (context: InspectableReactContext): unknown => {
179
193
  const value = readContext(context);
180
- pushHookLogEntry("Context", value, "Context", context.displayName ?? null);
194
+ pushHookLogEntry("Context", value, "Context", context.displayName || null);
181
195
  return value;
182
196
  };
183
197
 
@@ -304,7 +318,7 @@ const dispatcherUseId = (): string => {
304
318
 
305
319
  const dispatcherUseMemoCache = (size: number): unknown[] => {
306
320
  const fiber = currentFiber;
307
- if (fiber === null || fiber === undefined) return [];
321
+ if (fiber === null) return [];
308
322
 
309
323
  const memoCache = fiber.updateQueue?.memoCache;
310
324
  if (memoCache === null || memoCache === undefined) return [];
@@ -333,6 +347,7 @@ const inspectActionStateHook = (
333
347
  initialState: unknown,
334
348
  ): InspectedActionState => {
335
349
  let value: unknown;
350
+ let debugInfo: ReactDebugInfo[] | null = null;
336
351
  let error: unknown = null;
337
352
  if (hook !== null) {
338
353
  const actionResult = hook.memoizedState;
@@ -340,6 +355,7 @@ const inspectActionStateHook = (
340
355
  switch (actionResult.status) {
341
356
  case "fulfilled":
342
357
  value = actionResult.value;
358
+ debugInfo = actionResult._debugInfo ?? null;
343
359
  break;
344
360
  case "rejected":
345
361
  error = actionResult.reason;
@@ -347,6 +363,7 @@ const inspectActionStateHook = (
347
363
  default:
348
364
  error = SuspenseException;
349
365
  value = actionResult;
366
+ debugInfo = actionResult._debugInfo ?? null;
350
367
  }
351
368
  } else {
352
369
  value = actionResult;
@@ -354,7 +371,7 @@ const inspectActionStateHook = (
354
371
  } else {
355
372
  value = initialState;
356
373
  }
357
- return { value, error };
374
+ return { value, debugInfo, error };
358
375
  };
359
376
 
360
377
  const createActionStateDispatcher =
@@ -363,8 +380,8 @@ const createActionStateDispatcher =
363
380
  const hook = nextHook();
364
381
  nextHook();
365
382
  nextHook();
366
- const { value, error } = inspectActionStateHook(hook, initialState);
367
- pushHookLogEntry(primitive, value, primitive);
383
+ const { value, debugInfo, error } = inspectActionStateHook(hook, initialState);
384
+ pushHookLogEntry(primitive, value, primitive, null, debugInfo);
368
385
  if (error !== null) throw error;
369
386
  return [value, () => {}, false];
370
387
  };
@@ -449,6 +466,7 @@ const getPrimitiveStackCache = (): Map<string, StackFrame[]> => {
449
466
  dispatcher.useActionState((state: unknown) => state, null);
450
467
  dispatcher.useHostTransitionStatus();
451
468
  dispatcher.use({ $$typeof: REACT_CONTEXT_TYPE, _currentValue: null });
469
+ dispatcher.use({ $$typeof: REACT_RECOVERABLE_TYPE });
452
470
  const fulfilledPromise = Promise.resolve(null);
453
471
  Reflect.set(fulfilledPromise, "status", "fulfilled");
454
472
  Reflect.set(fulfilledPromise, "value", null);
@@ -532,6 +550,19 @@ const parseHookName = (functionName: string | undefined): string => {
532
550
  return functionName.slice(startIndex);
533
551
  };
534
552
 
553
+ // Unlike upstream, bippy routes every dispatcher method through a shared log helper, so those
554
+ // frames appear in captured stacks and must not become tree levels. Names are read off the
555
+ // live functions rather than matched by prefix, so user hooks cannot collide and minified
556
+ // builds stay accurate.
557
+ const INTERNAL_HOOK_FRAME_NAMES = new Set(
558
+ [pushHookLogEntry, ...Object.values(dispatcher)].map((internalFunction) =>
559
+ parseHookName(internalFunction.name),
560
+ ),
561
+ );
562
+
563
+ const isInternalHookFrame = (hookName: string): boolean =>
564
+ hookName !== "" && INTERNAL_HOOK_FRAME_NAMES.has(hookName);
565
+
535
566
  const isReactWrapper = (functionName: string | undefined, wrapperName: string): boolean => {
536
567
  const hookName = parseHookName(functionName);
537
568
  if (wrapperName === "HostTransitionStatus") {
@@ -602,8 +633,10 @@ const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): Ho
602
633
  const [primitiveFrame, stack] = parseTrimmedStack(rootStack, hook);
603
634
  let displayName = hook.displayName;
604
635
  if (displayName === null && primitiveFrame !== null) {
605
- displayName =
606
- parseHookName(primitiveFrame.functionName) || parseHookName(hook.dispatcherHookName);
636
+ const primitiveName = parseHookName(primitiveFrame.functionName);
637
+ displayName = isInternalHookFrame(primitiveName)
638
+ ? null
639
+ : primitiveName || parseHookName(hook.dispatcherHookName);
607
640
  }
608
641
 
609
642
  if (stack !== null) {
@@ -628,6 +661,7 @@ const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): Ho
628
661
  name: parseHookName(stack[stackIndex - 1].functionName),
629
662
  value: undefined,
630
663
  subHooks: children,
664
+ debugInfo: null,
631
665
  hookSource: {
632
666
  lineNumber: stackFrame.lineNumber ?? null,
633
667
  columnNumber: stackFrame.columnNumber ?? null,
@@ -655,21 +689,52 @@ const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): Ho
655
689
  fileName: firstStackFrame?.fileName ?? null,
656
690
  };
657
691
 
658
- levelChildren.push({ id, isStateEditable, name, value: hook.value, subHooks: [], hookSource });
692
+ levelChildren.push({
693
+ id,
694
+ isStateEditable,
695
+ name,
696
+ value: hook.value,
697
+ subHooks: [],
698
+ debugInfo: hook.debugInfo,
699
+ hookSource,
700
+ });
659
701
  }
660
702
 
661
703
  processDebugValues(rootChildren, null);
704
+ removeInternalHookFrames(rootChildren, null);
662
705
  return rootChildren;
663
706
  };
664
707
 
708
+ const removeInternalHookFrames = (
709
+ hooksTree: HooksTree,
710
+ parentHooksNode: HooksNode | null,
711
+ ): void => {
712
+ for (let nodeIndex = 0; nodeIndex < hooksTree.length; nodeIndex++) {
713
+ const hooksNode = hooksTree[nodeIndex];
714
+ if (isInternalHookFrame(hooksNode.name)) {
715
+ if (parentHooksNode !== null && hooksNode.value !== undefined) {
716
+ if (parentHooksNode.value === undefined) parentHooksNode.value = hooksNode.value;
717
+ else if (Array.isArray(parentHooksNode.value)) {
718
+ parentHooksNode.value = [...parentHooksNode.value, hooksNode.value];
719
+ } else parentHooksNode.value = [parentHooksNode.value, hooksNode.value];
720
+ }
721
+ hooksTree.splice(nodeIndex, 1, ...hooksNode.subHooks);
722
+ nodeIndex--;
723
+ } else {
724
+ removeInternalHookFrames(hooksNode.subHooks, hooksNode);
725
+ }
726
+ }
727
+ };
728
+
665
729
  const processDebugValues = (hooksTree: HooksTree, parentHooksNode: HooksNode | null): void => {
666
730
  const debugValueNodes: HooksNode[] = [];
667
731
  for (let nodeIndex = 0; nodeIndex < hooksTree.length; nodeIndex++) {
668
732
  const hooksNode = hooksTree[nodeIndex];
669
- if (hooksNode.name === "DebugValue" && hooksNode.subHooks.length === 0) {
670
- hooksTree.splice(nodeIndex, 1);
733
+ if (hooksNode.name === "DebugValue") {
734
+ hooksTree.splice(nodeIndex, 1, ...hooksNode.subHooks);
671
735
  nodeIndex--;
672
- debugValueNodes.push(hooksNode);
736
+ // Internal frames surface as valueless DebugValue levels; only real labels bubble up.
737
+ if (hooksNode.value !== undefined) debugValueNodes.push(hooksNode);
673
738
  } else {
674
739
  processDebugValues(hooksNode.subHooks, hooksNode);
675
740
  }
@@ -723,8 +788,8 @@ const resolveDefaultProps = (
723
788
  baseProps: Record<string, unknown>,
724
789
  ): Record<string, unknown> => {
725
790
  if (
726
- component &&
727
- typeof component === "object" &&
791
+ component !== null &&
792
+ (typeof component === "object" || typeof component === "function") &&
728
793
  "defaultProps" in component &&
729
794
  typeof component.defaultProps === "object" &&
730
795
  component.defaultProps !== null
@@ -760,9 +825,12 @@ const restoreConsole = (originalMethods: Record<string, unknown>): void => {
760
825
  }
761
826
  };
762
827
 
763
- const performDispatcherInspection = (
828
+ // The render call must happen in the same frame that captures the ancestor stack,
829
+ // otherwise an extra frame lands in every hook's trimmed stack and becomes a bogus tree level.
830
+ const performDispatcherInspection = <TArgs extends unknown[]>(
764
831
  dispatcherRef: RendererDispatcherRef,
765
- renderFn: () => void,
832
+ renderFunction: (...args: TArgs) => unknown,
833
+ ...renderArgs: TArgs
766
834
  ): HooksTree => {
767
835
  const previousDispatcher = readDispatcher(dispatcherRef);
768
836
  writeDispatcher(dispatcherRef, dispatcherProxy);
@@ -772,7 +840,7 @@ const performDispatcherInspection = (
772
840
 
773
841
  try {
774
842
  ancestorStackError = new Error();
775
- renderFn();
843
+ renderFunction(...renderArgs);
776
844
  } catch (renderError) {
777
845
  handleRenderFunctionError(renderError);
778
846
  } finally {
@@ -785,8 +853,12 @@ const performDispatcherInspection = (
785
853
  return buildTree(rootStack, capturedHookLog);
786
854
  };
787
855
 
788
- const requireDispatcherRef = (): RendererDispatcherRef => {
789
- const dispatcherRef = getRendererDispatcherRefs()[0];
856
+ const requireDispatcherRef = (
857
+ fiber?: Fiber,
858
+ target: ReactDevToolsTarget = globalThis,
859
+ ): RendererDispatcherRef => {
860
+ const rendererDispatcherRef = fiber ? getRenderer(fiber, target)?.currentDispatcherRef : null;
861
+ const dispatcherRef = rendererDispatcherRef ?? getRendererDispatcherRefs(target)[0];
790
862
  if (!dispatcherRef) {
791
863
  throw new BippyHookInspectionError(
792
864
  "Bippy couldn’t find a React renderer. Load React and install Bippy’s hook.",
@@ -823,8 +895,73 @@ const resolveContextDependency = (fiber: Fiber): void => {
823
895
  }
824
896
  };
825
897
 
898
+ const normalizeStandaloneHooks = (hooksTree: HooksTree): HooksTree => {
899
+ const normalizeNode = (hooksNode: HooksNode): HooksNode => {
900
+ const subHooks = hooksNode.subHooks.map(normalizeNode);
901
+ if (subHooks.length === 1) {
902
+ const childHook = subHooks[0];
903
+ if (hooksNode.name === "Context" && childHook.id === null) {
904
+ return { ...childHook, hookSource: hooksNode.hookSource };
905
+ }
906
+ if (hooksNode.name === "FormStatus" && childHook.name === "HostTransitionStatus") {
907
+ return { ...hooksNode, subHooks: [], value: childHook.value };
908
+ }
909
+ if (hooksNode.name === "Use") {
910
+ if (childHook.name === "Context") {
911
+ return { ...childHook, hookSource: hooksNode.hookSource };
912
+ }
913
+ if (childHook.name === "Promise" || childHook.name === "Unresolved") {
914
+ return {
915
+ ...hooksNode,
916
+ debugInfo: childHook.debugInfo,
917
+ subHooks: [],
918
+ value: childHook.value,
919
+ };
920
+ }
921
+ }
922
+ if (
923
+ hooksNode.id === null &&
924
+ hooksNode.value === undefined &&
925
+ (hooksNode.name === childHook.name || hooksNode.name === "<anonymous>")
926
+ ) {
927
+ return { ...childHook, hookSource: hooksNode.hookSource };
928
+ }
929
+ }
930
+ return { ...hooksNode, subHooks };
931
+ };
932
+
933
+ return hooksTree.map(normalizeNode);
934
+ };
935
+
936
+ export const inspectHooks = (
937
+ renderFunction: (props: Record<string, unknown>) => unknown,
938
+ props: Record<string, unknown>,
939
+ target: ReactDevToolsTarget = globalThis,
940
+ ): HooksTree => {
941
+ const dispatcherRef = requireDispatcherRef(undefined, target);
942
+ getPrimitiveStackCache();
943
+ currentHook = null;
944
+ currentFiber = null;
945
+ currentContextDependency = null;
946
+ currentThenableState = null;
947
+ currentThenableIndex = 0;
948
+ const originalConsoleMethods = suppressConsole();
949
+ try {
950
+ return normalizeStandaloneHooks(
951
+ performDispatcherInspection(dispatcherRef, renderFunction, props),
952
+ );
953
+ } finally {
954
+ currentHook = null;
955
+ currentFiber = null;
956
+ currentContextDependency = null;
957
+ currentThenableState = null;
958
+ currentThenableIndex = 0;
959
+ restoreConsole(originalConsoleMethods);
960
+ }
961
+ };
962
+
826
963
  export const getFiberHooks = (fiber: Fiber): HooksTree => {
827
- const dispatcherRef = requireDispatcherRef();
964
+ const dispatcherRef = requireDispatcherRef(fiber);
828
965
  const workTags = getReactWorkTagsForFiber(fiber);
829
966
 
830
967
  if (
@@ -870,9 +1007,15 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
870
1007
  if (!isForwardRefRenderType(type)) {
871
1008
  throw new BippyHookInspectionError("ForwardRef fiber is missing its render function.");
872
1009
  }
873
- return performDispatcherInspection(dispatcherRef, () => {
874
- type.render(props, fiber.ref);
875
- });
1010
+ const hooksTree = normalizeStandaloneHooks(
1011
+ performDispatcherInspection(dispatcherRef, type.render, props, fiber.ref),
1012
+ );
1013
+ for (const hooksNode of hooksTree) {
1014
+ if (hooksNode.hookSource?.functionName === "Object.render") {
1015
+ hooksNode.hookSource.functionName = null;
1016
+ }
1017
+ }
1018
+ return hooksTree;
876
1019
  }
877
1020
 
878
1021
  if (typeof type !== "function") {
@@ -880,9 +1023,7 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
880
1023
  "Function component fiber is missing its component function.",
881
1024
  );
882
1025
  }
883
- return performDispatcherInspection(dispatcherRef, () => {
884
- type(props);
885
- });
1026
+ return normalizeStandaloneHooks(performDispatcherInspection(dispatcherRef, type, props));
886
1027
  } finally {
887
1028
  currentFiber = null;
888
1029
  currentHook = null;
@@ -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;
@@ -4,6 +4,7 @@ import {
4
4
  getSourceFromSourceMap,
5
5
  getSourceMap,
6
6
  type SourceFetch,
7
+ type SourceMapRequestOptions,
7
8
  } from "./symbolication.js";
8
9
 
9
10
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
@@ -19,8 +20,10 @@ const UNNAMED_HOOKS = new Set([
19
20
 
20
21
  // HACK: matches `const/let/var [name, ...] = use...(...` or `const/let/var name = use...(...`
21
22
  // across up to 10 lines; handles TypeScript generics like `useState<T>(`
23
+ // The member-access prefix consumes one dotless segment per repetition. Letting a segment
24
+ // span dots too would make the parse ambiguous and backtrack exponentially on long chains.
22
25
  const HOOK_DECLARATION_REGEX =
23
- /(?:const|let|var)\s+((?:\[[\s\S]*?\]|\w+))\s*=\s*(?:[\w$.]+\.)*use[A-Z]\w*\s*(?:<[\s\S]*?>)?\s*\(/g;
26
+ /(?:const|let|var)\s+((?:\[[\s\S]*?\]|\w+))\s*=\s*(?:(?:[\w$]+|require\s*\([^)]*\))\.)*use[A-Z]\w*\s*(?:<[\s\S]*?>)?\s*\(/g;
24
27
 
25
28
  export const getHookSourceLocationKey = (hookSource: HookSource): string =>
26
29
  `${hookSource.fileName ?? ""}:${hookSource.lineNumber ?? 0}:${hookSource.columnNumber ?? 0}`;
@@ -63,12 +66,13 @@ export const extractHookVariableName = (
63
66
 
64
67
  const allMatches = [...sourceChunk.matchAll(HOOK_DECLARATION_REGEX)];
65
68
 
66
- const hookPositionInChunk = sourceChunk.lastIndexOf("\n") + 1 + columnNumber;
67
- const closestMatch = allMatches.filter((match) => match.index! <= hookPositionInChunk).at(-1);
69
+ const hookLineStart = sourceChunk.lastIndexOf("\n") + 1;
70
+ const hookPositionInChunk = hookLineStart + columnNumber;
71
+ const closestMatch =
72
+ allMatches.filter((match) => match.index! <= hookPositionInChunk).at(-1) ??
73
+ (columnNumber === 0 ? allMatches.find((match) => match.index! >= hookLineStart) : undefined);
68
74
 
69
- if (closestMatch) {
70
- return extractVariableNameFromBinding(closestMatch[1]);
71
- }
75
+ if (closestMatch) return extractVariableNameFromBinding(closestMatch[1]);
72
76
 
73
77
  return null;
74
78
  };
@@ -82,6 +86,7 @@ interface ResolvedSource {
82
86
  interface SourceResolutionContext {
83
87
  sourceContentCache: Map<string, string | null>;
84
88
  fetchFn?: SourceFetch;
89
+ requestOptions?: SourceMapRequestOptions;
85
90
  }
86
91
 
87
92
  const resolveOriginalSource = async (
@@ -90,9 +95,9 @@ const resolveOriginalSource = async (
90
95
  runtimeColumn: number,
91
96
  context: SourceResolutionContext,
92
97
  ): Promise<ResolvedSource | null> => {
93
- const { sourceContentCache, fetchFn } = context;
98
+ const { sourceContentCache, fetchFn, requestOptions } = context;
94
99
 
95
- const sourceMap = await getSourceMap(runtimeFileName, true, fetchFn);
100
+ const sourceMap = await getSourceMap(runtimeFileName, true, fetchFn, requestOptions);
96
101
 
97
102
  if (sourceMap) {
98
103
  const originalLocation = getSourceFromSourceMap(sourceMap, runtimeLine, runtimeColumn);
@@ -136,6 +141,7 @@ const resolveOriginalSource = async (
136
141
  export const parseHookNames = async (
137
142
  hooksTree: HooksTree,
138
143
  fetchFn?: SourceFetch,
144
+ requestOptions?: SourceMapRequestOptions,
139
145
  ): Promise<HookNames> => {
140
146
  const hookNames: HookNames = new Map();
141
147
  const hooksList = flattenHooksTree(hooksTree);
@@ -145,6 +151,7 @@ export const parseHookNames = async (
145
151
  const resolutionContext: SourceResolutionContext = {
146
152
  sourceContentCache: new Map(),
147
153
  fetchFn,
154
+ requestOptions,
148
155
  };
149
156
 
150
157
  await Promise.all(
@@ -31,7 +31,9 @@ export const parseStack = (stackString: string, options?: ParseOptions): StackFr
31
31
  const parsed = parseV8OrIeString(rawLine)[0];
32
32
  if (parsed) frames.push(parsed);
33
33
  } else if (/^\s*in\s+/.test(rawLine)) {
34
- const elementName = rawLine.replace(/^\s*in\s+/, "").replace(/\s*\(at .*\)$/, "");
34
+ const elementName = rawLine
35
+ .replace(/^\s*in\s+/, "")
36
+ .replace(/\s*(?:\(at .*\)|\[[^\]]+\])$/, "");
35
37
  frames.push({ functionName: elementName, source: rawLine });
36
38
  } else if (rawLine.match(FIREFOX_SAFARI_STACK_REGEXP)) {
37
39
  const parsed = parseFFOrSafariString(rawLine)[0];