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.
- package/README.md +8 -68
- package/dist/core.cjs +1 -1
- package/dist/core.js +1 -1
- package/dist/errors.d.cts +56 -9
- package/dist/errors.d.ts +56 -9
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +8 -35
- package/dist/index.d.ts +8 -35
- package/dist/index.js +1 -1
- package/dist/install-hook-only.cjs +1 -1
- package/dist/install-hook-only.js +1 -1
- package/dist/rdt-hook.cjs +1 -1
- package/dist/rdt-hook.js +1 -1
- package/dist/source.cjs +10 -9
- package/dist/source.d.cts +36 -9
- package/dist/source.d.ts +36 -9
- package/dist/source.js +13 -12
- package/package.json +7 -7
- package/src/core.ts +134 -56
- package/src/rdt-hook.ts +126 -70
- package/src/react-internals/types.ts +33 -6
- package/src/react.ts +1 -0
- package/src/source/constants.ts +1 -3
- package/src/source/get-display-name-from-source.ts +10 -3
- package/src/source/get-source.ts +39 -19
- package/src/source/index.ts +17 -3
- package/src/source/inspect-hooks.ts +169 -28
- package/src/source/owner-stack.ts +43 -35
- package/src/source/parse-debug-stack.ts +1 -1
- package/src/source/parse-hook-names.ts +15 -8
- package/src/source/parse-stack.ts +3 -1
- package/src/source/renderer-dispatchers.ts +9 -4
- package/src/source/symbolication.ts +53 -32
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
606
|
-
|
|
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({
|
|
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"
|
|
670
|
-
hooksTree.splice(nodeIndex, 1);
|
|
733
|
+
if (hooksNode.name === "DebugValue") {
|
|
734
|
+
hooksTree.splice(nodeIndex, 1, ...hooksNode.subHooks);
|
|
671
735
|
nodeIndex--;
|
|
672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
789
|
-
|
|
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
|
-
|
|
874
|
-
type.render
|
|
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 {
|
|
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 = (
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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/
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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/
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
650
|
-
|
|
655
|
+
shouldUseCache = true,
|
|
656
|
+
sourceFetch?: SourceFetch,
|
|
657
|
+
requestOptions: SourceMapRequestOptions = {},
|
|
651
658
|
): Promise<StackFrame[]> => {
|
|
652
|
-
const debugStackFrames =
|
|
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
|
-
|
|
661
|
-
|
|
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,
|
|
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
|
|
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
|
|
67
|
-
const
|
|
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
|
|
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];
|