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.
- package/LICENSE +1 -0
- package/README.md +9 -76
- 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 +14 -38
- package/dist/index.d.ts +18 -42
- 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 +11 -11
- package/dist/source.d.cts +36 -9
- package/dist/source.d.ts +36 -9
- package/dist/source.js +11 -11
- package/package.json +5 -33
- package/src/call-listener.ts +13 -0
- package/src/core.ts +287 -258
- package/src/rdt-hook.ts +130 -71
- package/src/react-internals/current-fiber.ts +84 -0
- package/src/react-internals/index.ts +38 -11
- package/src/react-internals/types.ts +33 -6
- package/src/react.ts +141 -55
- 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 +198 -49
- 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 +114 -64
- package/src/source/renderer-dispatchers.ts +9 -4
- package/src/source/symbolication.ts +68 -36
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
313
|
-
|
|
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 =
|
|
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
|
-
|
|
606
|
-
|
|
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({
|
|
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"
|
|
670
|
-
hooksTree.splice(nodeIndex, 1);
|
|
737
|
+
if (hooksNode.name === "DebugValue") {
|
|
738
|
+
hooksTree.splice(nodeIndex, 1, ...hooksNode.subHooks);
|
|
671
739
|
nodeIndex--;
|
|
672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
789
|
-
|
|
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
|
-
|
|
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
|
-
|
|
874
|
-
type.render
|
|
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 {
|
|
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
|
};
|