bippy 0.7.2-dev.b756bd6 → 0.7.2-dev.e113600
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 +0 -68
- package/dist/core.cjs +1 -1
- package/dist/core.js +1 -1
- package/dist/errors.d.cts +34 -3
- package/dist/errors.d.ts +34 -3
- 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 +8 -8
- package/dist/source.d.cts +35 -9
- package/dist/source.d.ts +35 -9
- package/dist/source.js +13 -13
- package/package.json +2 -2
- package/src/core.ts +133 -51
- package/src/rdt-hook.ts +126 -70
- package/src/react-internals/types.ts +10 -2
- 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 +133 -17
- package/src/source/owner-stack.ts +19 -11
- package/src/source/parse-hook-names.ts +15 -8
- package/src/source/renderer-dispatchers.ts +9 -4
- package/src/source/symbolication.ts +53 -32
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getRenderer } from "../core.js";
|
|
2
|
+
import type { ReactDevToolsTarget } from "../rdt-hook.js";
|
|
1
3
|
import type {
|
|
2
4
|
Fiber,
|
|
3
5
|
ContextDependency,
|
|
@@ -548,6 +550,19 @@ const parseHookName = (functionName: string | undefined): string => {
|
|
|
548
550
|
return functionName.slice(startIndex);
|
|
549
551
|
};
|
|
550
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
|
+
|
|
551
566
|
const isReactWrapper = (functionName: string | undefined, wrapperName: string): boolean => {
|
|
552
567
|
const hookName = parseHookName(functionName);
|
|
553
568
|
if (wrapperName === "HostTransitionStatus") {
|
|
@@ -618,8 +633,10 @@ const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): Ho
|
|
|
618
633
|
const [primitiveFrame, stack] = parseTrimmedStack(rootStack, hook);
|
|
619
634
|
let displayName = hook.displayName;
|
|
620
635
|
if (displayName === null && primitiveFrame !== null) {
|
|
621
|
-
|
|
622
|
-
|
|
636
|
+
const primitiveName = parseHookName(primitiveFrame.functionName);
|
|
637
|
+
displayName = isInternalHookFrame(primitiveName)
|
|
638
|
+
? null
|
|
639
|
+
: primitiveName || parseHookName(hook.dispatcherHookName);
|
|
623
640
|
}
|
|
624
641
|
|
|
625
642
|
if (stack !== null) {
|
|
@@ -684,17 +701,40 @@ const buildTree = (rootStack: StackFrame[], capturedHookLog: HookLogEntry[]): Ho
|
|
|
684
701
|
}
|
|
685
702
|
|
|
686
703
|
processDebugValues(rootChildren, null);
|
|
704
|
+
removeInternalHookFrames(rootChildren, null);
|
|
687
705
|
return rootChildren;
|
|
688
706
|
};
|
|
689
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
|
+
|
|
690
729
|
const processDebugValues = (hooksTree: HooksTree, parentHooksNode: HooksNode | null): void => {
|
|
691
730
|
const debugValueNodes: HooksNode[] = [];
|
|
692
731
|
for (let nodeIndex = 0; nodeIndex < hooksTree.length; nodeIndex++) {
|
|
693
732
|
const hooksNode = hooksTree[nodeIndex];
|
|
694
|
-
if (hooksNode.name === "DebugValue"
|
|
695
|
-
hooksTree.splice(nodeIndex, 1);
|
|
733
|
+
if (hooksNode.name === "DebugValue") {
|
|
734
|
+
hooksTree.splice(nodeIndex, 1, ...hooksNode.subHooks);
|
|
696
735
|
nodeIndex--;
|
|
697
|
-
|
|
736
|
+
// Internal frames surface as valueless DebugValue levels; only real labels bubble up.
|
|
737
|
+
if (hooksNode.value !== undefined) debugValueNodes.push(hooksNode);
|
|
698
738
|
} else {
|
|
699
739
|
processDebugValues(hooksNode.subHooks, hooksNode);
|
|
700
740
|
}
|
|
@@ -785,9 +825,12 @@ const restoreConsole = (originalMethods: Record<string, unknown>): void => {
|
|
|
785
825
|
}
|
|
786
826
|
};
|
|
787
827
|
|
|
788
|
-
|
|
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[]>(
|
|
789
831
|
dispatcherRef: RendererDispatcherRef,
|
|
790
|
-
|
|
832
|
+
renderFunction: (...args: TArgs) => unknown,
|
|
833
|
+
...renderArgs: TArgs
|
|
791
834
|
): HooksTree => {
|
|
792
835
|
const previousDispatcher = readDispatcher(dispatcherRef);
|
|
793
836
|
writeDispatcher(dispatcherRef, dispatcherProxy);
|
|
@@ -797,7 +840,7 @@ const performDispatcherInspection = (
|
|
|
797
840
|
|
|
798
841
|
try {
|
|
799
842
|
ancestorStackError = new Error();
|
|
800
|
-
|
|
843
|
+
renderFunction(...renderArgs);
|
|
801
844
|
} catch (renderError) {
|
|
802
845
|
handleRenderFunctionError(renderError);
|
|
803
846
|
} finally {
|
|
@@ -810,8 +853,12 @@ const performDispatcherInspection = (
|
|
|
810
853
|
return buildTree(rootStack, capturedHookLog);
|
|
811
854
|
};
|
|
812
855
|
|
|
813
|
-
const requireDispatcherRef = (
|
|
814
|
-
|
|
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];
|
|
815
862
|
if (!dispatcherRef) {
|
|
816
863
|
throw new BippyHookInspectionError(
|
|
817
864
|
"Bippy couldn’t find a React renderer. Load React and install Bippy’s hook.",
|
|
@@ -848,8 +895,73 @@ const resolveContextDependency = (fiber: Fiber): void => {
|
|
|
848
895
|
}
|
|
849
896
|
};
|
|
850
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
|
+
|
|
851
963
|
export const getFiberHooks = (fiber: Fiber): HooksTree => {
|
|
852
|
-
const dispatcherRef = requireDispatcherRef();
|
|
964
|
+
const dispatcherRef = requireDispatcherRef(fiber);
|
|
853
965
|
const workTags = getReactWorkTagsForFiber(fiber);
|
|
854
966
|
|
|
855
967
|
if (
|
|
@@ -895,9 +1007,15 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
|
|
|
895
1007
|
if (!isForwardRefRenderType(type)) {
|
|
896
1008
|
throw new BippyHookInspectionError("ForwardRef fiber is missing its render function.");
|
|
897
1009
|
}
|
|
898
|
-
|
|
899
|
-
type.render
|
|
900
|
-
|
|
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;
|
|
901
1019
|
}
|
|
902
1020
|
|
|
903
1021
|
if (typeof type !== "function") {
|
|
@@ -905,9 +1023,7 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
|
|
|
905
1023
|
"Function component fiber is missing its component function.",
|
|
906
1024
|
);
|
|
907
1025
|
}
|
|
908
|
-
return performDispatcherInspection(dispatcherRef,
|
|
909
|
-
type(props);
|
|
910
|
-
});
|
|
1026
|
+
return normalizeStandaloneHooks(performDispatcherInspection(dispatcherRef, type, props));
|
|
911
1027
|
} finally {
|
|
912
1028
|
currentFiber = null;
|
|
913
1029
|
currentHook = null;
|
|
@@ -18,7 +18,11 @@ import {
|
|
|
18
18
|
readDispatcher,
|
|
19
19
|
writeDispatcher,
|
|
20
20
|
} from "./renderer-dispatchers.js";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
symbolicateStack,
|
|
23
|
+
type SourceFetch,
|
|
24
|
+
type SourceMapRequestOptions,
|
|
25
|
+
} from "./symbolication.js";
|
|
22
26
|
|
|
23
27
|
interface RendererDispatcherSnapshot {
|
|
24
28
|
currentDispatcherRef: RendererDispatcherRef;
|
|
@@ -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
|
};
|
|
@@ -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(
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { RendererDispatcherRef } from "../react-internals/index.js";
|
|
2
|
-
import { _renderers, getRDTHook } from "../rdt-hook.js";
|
|
2
|
+
import { _renderers, getRDTHook, isRendererMap } from "../rdt-hook.js";
|
|
3
|
+
import type { ReactDevToolsTarget } from "../rdt-hook.js";
|
|
3
4
|
|
|
4
|
-
export const getRendererDispatcherRefs = (
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
export const getRendererDispatcherRefs = (
|
|
6
|
+
target: ReactDevToolsTarget = globalThis,
|
|
7
|
+
): RendererDispatcherRef[] => {
|
|
8
|
+
const rdtHook = getRDTHook(undefined, target);
|
|
9
|
+
const targetRenderers = isRendererMap(rdtHook.renderers) ? rdtHook.renderers.values() : [];
|
|
10
|
+
const renderers =
|
|
11
|
+
target === globalThis ? new Set([..._renderers, ...targetRenderers]) : new Set(targetRenderers);
|
|
7
12
|
const currentDispatcherRefs: RendererDispatcherRef[] = [];
|
|
8
13
|
const seenCurrentDispatcherRefs = new Set<object>();
|
|
9
14
|
for (const renderer of renderers) {
|
|
@@ -39,6 +39,7 @@ export interface SourceFetch {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export interface SourceMapRequestOptions {
|
|
42
|
+
allowCrossOriginSourceMap?: boolean;
|
|
42
43
|
allowUnsafeServerFetch?: boolean;
|
|
43
44
|
maxBundleSizeBytes?: number;
|
|
44
45
|
maxSourceMapSizeBytes?: number;
|
|
@@ -49,6 +50,7 @@ export interface SourceMapRequestOptions {
|
|
|
49
50
|
export interface SourceMap {
|
|
50
51
|
file?: string;
|
|
51
52
|
ignoredSourceIndices?: Set<number>;
|
|
53
|
+
sourceMapUrl?: string;
|
|
52
54
|
mappings: SourceMapSegment[][];
|
|
53
55
|
names?: string[];
|
|
54
56
|
sections?: DecodedSourceMapSection[];
|
|
@@ -298,10 +300,14 @@ const resolveUrl = (reference: string, baseUrl: string): string | null => {
|
|
|
298
300
|
return new URL(reference, baseUrl).toString();
|
|
299
301
|
} catch {
|
|
300
302
|
try {
|
|
301
|
-
|
|
302
|
-
return `${resolvedUrl.pathname}${resolvedUrl.search}${resolvedUrl.hash}`;
|
|
303
|
+
return new URL(reference).toString();
|
|
303
304
|
} catch {
|
|
304
|
-
|
|
305
|
+
try {
|
|
306
|
+
const resolvedUrl = new URL(reference, new URL(baseUrl, "https://bippy.invalid/"));
|
|
307
|
+
return `${resolvedUrl.pathname}${resolvedUrl.search}${resolvedUrl.hash}`;
|
|
308
|
+
} catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
305
311
|
}
|
|
306
312
|
}
|
|
307
313
|
};
|
|
@@ -460,6 +466,11 @@ const resolveSourceMapSources = (rawSourceMap: StandardSourceMap, sourceMapUrl:
|
|
|
460
466
|
resolveSourceRoot(rawSourceMap.sourceRoot, source, sourceMapUrl),
|
|
461
467
|
);
|
|
462
468
|
|
|
469
|
+
// An inline URL carries the whole encoded map, so retaining it in the cache would
|
|
470
|
+
// double the memory held per source map for the lifetime of the process.
|
|
471
|
+
const getRetainableSourceMapUrl = (sourceMapUrl: string): string | undefined =>
|
|
472
|
+
INLINE_SOURCEMAP_REGEX.test(sourceMapUrl) ? undefined : sourceMapUrl;
|
|
473
|
+
|
|
463
474
|
const decodeStandardSourceMap = (
|
|
464
475
|
rawSourceMap: StandardSourceMap,
|
|
465
476
|
sourceMapUrl: string,
|
|
@@ -470,6 +481,7 @@ const decodeStandardSourceMap = (
|
|
|
470
481
|
ignoredSourceIndices: getIgnoredSourceIndices(rawSourceMap),
|
|
471
482
|
mappings: decode(rawSourceMap.mappings),
|
|
472
483
|
names: rawSourceMap.names,
|
|
484
|
+
sourceMapUrl: getRetainableSourceMapUrl(sourceMapUrl),
|
|
473
485
|
sourceRoot: rawSourceMap.sourceRoot,
|
|
474
486
|
sources: resolveSourceMapSources(rawSourceMap, sourceMapUrl),
|
|
475
487
|
sourcesContent: rawSourceMap.sourcesContent,
|
|
@@ -532,6 +544,7 @@ const decodeIndexSourceMap = async (
|
|
|
532
544
|
mappings: [],
|
|
533
545
|
names: [],
|
|
534
546
|
sections: decodedSections,
|
|
547
|
+
sourceMapUrl: getRetainableSourceMapUrl(sourceMapUrl),
|
|
535
548
|
sourceRoot: undefined,
|
|
536
549
|
sources: Array.from(allSources),
|
|
537
550
|
sourcesContent: undefined,
|
|
@@ -782,22 +795,23 @@ const readSourceMapDocument = async (
|
|
|
782
795
|
|
|
783
796
|
const getSourceMapUncachedInternal = async (
|
|
784
797
|
bundleUrl: string,
|
|
785
|
-
|
|
798
|
+
sourceFetchOverride?: SourceFetch,
|
|
786
799
|
options: SourceMapRequestOptions = {},
|
|
787
800
|
): Promise<null | SourceMap> => {
|
|
788
|
-
const shouldAllowCustomProtocol =
|
|
801
|
+
const shouldAllowCustomProtocol = sourceFetchOverride !== undefined;
|
|
789
802
|
if (!isFetchableUrl(bundleUrl, shouldAllowCustomProtocol)) {
|
|
790
803
|
return null;
|
|
791
804
|
}
|
|
792
805
|
|
|
793
806
|
const isServer = isServerRuntime();
|
|
794
|
-
const shouldRejectRedirects =
|
|
795
|
-
|
|
807
|
+
const shouldRejectRedirects =
|
|
808
|
+
(isServer || isExtensionUrl(bundleUrl)) && !options.allowCrossOriginSourceMap;
|
|
809
|
+
if (isServer && !sourceFetchOverride) {
|
|
796
810
|
if (!options.allowUnsafeServerFetch || !isAbsoluteHttpUrl(bundleUrl)) return null;
|
|
797
811
|
}
|
|
798
812
|
|
|
799
813
|
const sourceFetch =
|
|
800
|
-
|
|
814
|
+
sourceFetchOverride ??
|
|
801
815
|
(typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : undefined);
|
|
802
816
|
if (!sourceFetch) return null;
|
|
803
817
|
const maxBundleSizeBytes = options.maxBundleSizeBytes ?? defaultMaxBundleSizeBytes;
|
|
@@ -864,11 +878,11 @@ const getSourceMapUncachedInternal = async (
|
|
|
864
878
|
|
|
865
879
|
export const getSourceMapUncached = async (
|
|
866
880
|
bundleUrl: string,
|
|
867
|
-
|
|
881
|
+
sourceFetch?: SourceFetch,
|
|
868
882
|
options: SourceMapRequestOptions = {},
|
|
869
883
|
): Promise<null | SourceMap> => {
|
|
870
884
|
try {
|
|
871
|
-
return await getSourceMapUncachedInternal(bundleUrl,
|
|
885
|
+
return await getSourceMapUncachedInternal(bundleUrl, sourceFetch, options);
|
|
872
886
|
} catch (error) {
|
|
873
887
|
if (error instanceof TransientSourceMapError) return null;
|
|
874
888
|
throw error;
|
|
@@ -878,66 +892,67 @@ export const getSourceMapUncached = async (
|
|
|
878
892
|
const getPerFetchMap = <Value>(
|
|
879
893
|
mapsByFetch: WeakMap<SourceFetch, Map<string, Value>>,
|
|
880
894
|
globalMap: Map<string, Value>,
|
|
881
|
-
|
|
895
|
+
sourceFetch: SourceFetch | undefined,
|
|
882
896
|
): Map<string, Value> => {
|
|
883
|
-
if (!
|
|
884
|
-
let map = mapsByFetch.get(
|
|
897
|
+
if (!sourceFetch) return globalMap;
|
|
898
|
+
let map = mapsByFetch.get(sourceFetch);
|
|
885
899
|
if (!map) {
|
|
886
900
|
map = new Map();
|
|
887
|
-
mapsByFetch.set(
|
|
901
|
+
mapsByFetch.set(sourceFetch, map);
|
|
888
902
|
}
|
|
889
903
|
return map;
|
|
890
904
|
};
|
|
891
905
|
|
|
892
|
-
const getSourceMapCache = (
|
|
893
|
-
getPerFetchMap(sourceMapCachesByFetch, sourceMapCache,
|
|
906
|
+
const getSourceMapCache = (sourceFetch: SourceFetch | undefined): Map<string, null | SourceMap> =>
|
|
907
|
+
getPerFetchMap(sourceMapCachesByFetch, sourceMapCache, sourceFetch);
|
|
894
908
|
|
|
895
909
|
const getPendingSourceMapRequests = (
|
|
896
|
-
|
|
910
|
+
sourceFetch: SourceFetch | undefined,
|
|
897
911
|
): Map<string, Promise<SourceMapResult>> =>
|
|
898
|
-
getPerFetchMap(pendingSourceMapRequestsByFetch, pendingSourceMapRequests,
|
|
912
|
+
getPerFetchMap(pendingSourceMapRequestsByFetch, pendingSourceMapRequests, sourceFetch);
|
|
899
913
|
|
|
900
914
|
const getSourceMapCacheKey = (file: string, options: SourceMapRequestOptions): string =>
|
|
915
|
+
options.allowCrossOriginSourceMap === undefined &&
|
|
901
916
|
options.allowUnsafeServerFetch === undefined &&
|
|
902
917
|
options.maxBundleSizeBytes === undefined &&
|
|
903
918
|
options.maxSourceMapSizeBytes === undefined &&
|
|
904
919
|
options.timeoutMs === undefined
|
|
905
920
|
? file
|
|
906
|
-
: `${file}\0${options.allowUnsafeServerFetch ?? ""}\0${options.maxBundleSizeBytes ?? ""}\0${options.maxSourceMapSizeBytes ?? ""}\0${options.timeoutMs ?? ""}`;
|
|
921
|
+
: `${file}\0${options.allowCrossOriginSourceMap ?? ""}\0${options.allowUnsafeServerFetch ?? ""}\0${options.maxBundleSizeBytes ?? ""}\0${options.maxSourceMapSizeBytes ?? ""}\0${options.timeoutMs ?? ""}`;
|
|
907
922
|
|
|
908
923
|
export const getSourceMap = async (
|
|
909
924
|
file: string,
|
|
910
|
-
|
|
911
|
-
|
|
925
|
+
shouldUseCache = true,
|
|
926
|
+
sourceFetch?: SourceFetch,
|
|
912
927
|
options: SourceMapRequestOptions = {},
|
|
913
928
|
): Promise<null | SourceMap> => {
|
|
914
|
-
const
|
|
915
|
-
const cache = getSourceMapCache(
|
|
916
|
-
const pendingRequests = getPendingSourceMapRequests(
|
|
929
|
+
const canUseCache = shouldUseCache && options.signal === undefined;
|
|
930
|
+
const cache = getSourceMapCache(sourceFetch);
|
|
931
|
+
const pendingRequests = getPendingSourceMapRequests(sourceFetch);
|
|
917
932
|
const cacheKey = getSourceMapCacheKey(file, options);
|
|
918
|
-
if (
|
|
933
|
+
if (canUseCache && cache.has(cacheKey)) {
|
|
919
934
|
return cache.get(cacheKey) ?? null;
|
|
920
935
|
}
|
|
921
936
|
|
|
922
|
-
const pendingRequest =
|
|
937
|
+
const pendingRequest = canUseCache ? pendingRequests.get(cacheKey) : undefined;
|
|
923
938
|
if (pendingRequest) {
|
|
924
939
|
return (await pendingRequest).sourceMap;
|
|
925
940
|
}
|
|
926
941
|
|
|
927
942
|
const fetchPromise: Promise<SourceMapResult> = getSourceMapUncachedInternal(
|
|
928
943
|
file,
|
|
929
|
-
|
|
944
|
+
sourceFetch,
|
|
930
945
|
options,
|
|
931
946
|
).then(
|
|
932
947
|
(sourceMap) => ({ sourceMap, isTransientFailure: false }),
|
|
933
948
|
() => ({ sourceMap: null, isTransientFailure: true }),
|
|
934
949
|
);
|
|
935
|
-
if (
|
|
950
|
+
if (canUseCache) {
|
|
936
951
|
pendingRequests.set(cacheKey, fetchPromise);
|
|
937
952
|
}
|
|
938
953
|
|
|
939
954
|
const { sourceMap, isTransientFailure } = await fetchPromise;
|
|
940
|
-
if (
|
|
955
|
+
if (canUseCache) {
|
|
941
956
|
pendingRequests.delete(cacheKey);
|
|
942
957
|
if (!isTransientFailure) {
|
|
943
958
|
cache.set(cacheKey, sourceMap);
|
|
@@ -949,13 +964,19 @@ export const getSourceMap = async (
|
|
|
949
964
|
|
|
950
965
|
export const symbolicateStack = async (
|
|
951
966
|
stack: StackFrame[],
|
|
952
|
-
|
|
953
|
-
|
|
967
|
+
shouldUseCache = true,
|
|
968
|
+
sourceFetch?: SourceFetch,
|
|
969
|
+
requestOptions: SourceMapRequestOptions = {},
|
|
954
970
|
): Promise<StackFrame[]> => {
|
|
955
971
|
return Promise.all(
|
|
956
972
|
stack.map(async (stackFrame) => {
|
|
957
973
|
if (!stackFrame.fileName) return stackFrame;
|
|
958
|
-
const sourceMap = await getSourceMap(
|
|
974
|
+
const sourceMap = await getSourceMap(
|
|
975
|
+
stackFrame.fileName,
|
|
976
|
+
shouldUseCache,
|
|
977
|
+
sourceFetch,
|
|
978
|
+
requestOptions,
|
|
979
|
+
);
|
|
959
980
|
if (
|
|
960
981
|
!sourceMap ||
|
|
961
982
|
typeof stackFrame.lineNumber !== "number" ||
|