@samitouri / QOS-React / commits / 4f5c812a3c

DevTools: Rely on sourcemaps to compute hook name of built-in hooks in newer versions (#28593)

Sebastian Silbermann committed Apr 11, 2024 at 22:00 UTC 4f5c812a3c4e52d9ea5d908a27a317ac0f26590a
2 files changed +73 -37
packages/react-debug-tools/src/ReactDebugHooks.js
+71 -35
@@ -47,6 +47,7 @@ type HookLogEntry = {
47 stackError: Error,
48 value: mixed,
49 debugInfo: ReactDebugInfo | null,
50 + dispatcherHookName: string,
51 };
52
53 let hookLog: Array<HookLogEntry> = [];
@@ -131,6 +132,8 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
132 );
133 } catch (x) {}
134 }
135 +
136 + Dispatcher.useId();
137 } finally {
138 readHookLog = hookLog;
139 hookLog = [];
@@ -207,6 +210,7 @@ function use<T>(usable: Usable<T>): T {
210 value: fulfilledValue,
211 debugInfo:
212 thenable._debugInfo === undefined ? null : thenable._debugInfo,
213 + dispatcherHookName: 'Use',
214 });
215 return fulfilledValue;
216 }
@@ -224,6 +228,7 @@ function use<T>(usable: Usable<T>): T {
228 value: thenable,
229 debugInfo:
230 thenable._debugInfo === undefined ? null : thenable._debugInfo,
231 + dispatcherHookName: 'Use',
232 });
233 throw SuspenseException;
234 } else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
@@ -236,6 +241,7 @@ function use<T>(usable: Usable<T>): T {
241 stackError: new Error(),
242 value,
243 debugInfo: null,
244 + dispatcherHookName: 'Use',
245 });
246
247 return value;
@@ -254,6 +260,7 @@ function useContext<T>(context: ReactContext<T>): T {
260 stackError: new Error(),
261 value: value,
262 debugInfo: null,
263 + dispatcherHookName: 'Context',
264 });
265 return value;
266 }
@@ -275,6 +282,7 @@ function useState<S>(
282 stackError: new Error(),
283 value: state,
284 debugInfo: null,
285 + dispatcherHookName: 'State',
286 });
287 return [state, (action: BasicStateAction<S>) => {}];
288 }
@@ -297,6 +305,7 @@ function useReducer<S, I, A>(
305 stackError: new Error(),
306 value: state,
307 debugInfo: null,
308 + dispatcherHookName: 'Reducer',
309 });
310 return [state, (action: A) => {}];
311 }
@@ -310,6 +319,7 @@ function useRef<T>(initialValue: T): {current: T} {
319 stackError: new Error(),
320 value: ref.current,
321 debugInfo: null,
322 + dispatcherHookName: 'Ref',
323 });
324 return ref;
325 }
@@ -322,6 +332,7 @@ function useCacheRefresh(): () => void {
332 stackError: new Error(),
333 value: hook !== null ? hook.memoizedState : function refresh() {},
334 debugInfo: null,
335 + dispatcherHookName: 'CacheRefresh',
336 });
337 return () => {};
338 }
@@ -337,6 +348,7 @@ function useLayoutEffect(
348 stackError: new Error(),
349 value: create,
350 debugInfo: null,
351 + dispatcherHookName: 'LayoutEffect',
352 });
353 }
354
@@ -351,6 +363,7 @@ function useInsertionEffect(
363 stackError: new Error(),
364 value: create,
365 debugInfo: null,
366 + dispatcherHookName: 'InsertionEffect',
367 });
368 }
369
@@ -365,6 +378,7 @@ function useEffect(
378 stackError: new Error(),
379 value: create,
380 debugInfo: null,
381 + dispatcherHookName: 'Effect',
382 });
383 }
384
@@ -388,6 +402,7 @@ function useImperativeHandle<T>(
402 stackError: new Error(),
403 value: instance,
404 debugInfo: null,
405 + dispatcherHookName: 'ImperativeHandle',
406 });
407 }
408
@@ -398,6 +413,7 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
413 stackError: new Error(),
414 value: typeof formatterFn === 'function' ? formatterFn(value) : value,
415 debugInfo: null,
416 + dispatcherHookName: 'DebugValue',
417 });
418 }
419
@@ -409,6 +425,7 @@ function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
425 stackError: new Error(),
426 value: hook !== null ? hook.memoizedState[0] : callback,
427 debugInfo: null,
428 + dispatcherHookName: 'Callback',
429 });
430 return callback;
431 }
@@ -425,6 +442,7 @@ function useMemo<T>(
442 stackError: new Error(),
443 value,
444 debugInfo: null,
445 + dispatcherHookName: 'Memo',
446 });
447 return value;
448 }
@@ -446,6 +464,7 @@ function useSyncExternalStore<T>(
464 stackError: new Error(),
465 value,
466 debugInfo: null,
467 + dispatcherHookName: 'SyncExternalStore',
468 });
469 return value;
470 }
@@ -468,6 +487,7 @@ function useTransition(): [
487 stackError: new Error(),
488 value: isPending,
489 debugInfo: null,
490 + dispatcherHookName: 'Transition',
491 });
492 return [isPending, () => {}];
493 }
@@ -481,6 +501,7 @@ function useDeferredValue<T>(value: T, initialValue?: T): T {
501 stackError: new Error(),
502 value: prevValue,
503 debugInfo: null,
504 + dispatcherHookName: 'DeferredValue',
505 });
506 return prevValue;
507 }
@@ -494,6 +515,7 @@ function useId(): string {
515 stackError: new Error(),
516 value: id,
517 debugInfo: null,
518 + dispatcherHookName: 'Id',
519 });
520 return id;
521 }
@@ -544,6 +566,7 @@ function useOptimistic<S, A>(
566 stackError: new Error(),
567 value: state,
568 debugInfo: null,
569 + dispatcherHookName: 'Optimistic',
570 });
571 return [state, (action: A) => {}];
572 }
@@ -603,6 +626,7 @@ function useFormState<S, P>(
626 stackError: stackError,
627 value: value,
628 debugInfo: debugInfo,
629 + dispatcherHookName: 'FormState',
630 });
631
632 if (error !== null) {
@@ -672,6 +696,7 @@ function useActionState<S, P>(
696 stackError: stackError,
697 value: value,
698 debugInfo: debugInfo,
699 + dispatcherHookName: 'ActionState',
700 });
701
702 if (error !== null) {
@@ -759,8 +784,7 @@ export type HooksTree = Array<HooksNode>;
784 // of a hook call. A simple way to demonstrate this is wrapping `new Error()`
785 // in a wrapper constructor like a polyfill. That'll add an extra frame.
786 // Similar things can happen with the call to the dispatcher. The top frame
762 -// may not be the primitive. Likewise the primitive can have fewer stack frames
763 -// such as when a call to useState got inlined to use dispatcher.useState.
787 +// may not be the primitive.
788 //
789 // We also can't assume that the last frame of the root call is the same
790 // frame as the last frame of the hook call because long stack traces can be
@@ -810,27 +834,8 @@ function findCommonAncestorIndex(rootStack: any, hookStack: any) {
834 return -1;
835 }
836
813 -function isReactWrapper(functionName: any, primitiveName: string) {
814 - if (!functionName) {
815 - return false;
816 - }
817 - switch (primitiveName) {
818 - case 'Context':
819 - case 'Context (use)':
820 - case 'Promise':
821 - case 'Unresolved':
822 - if (functionName.endsWith('use')) {
823 - return true;
824 - }
825 - }
826 - const expectedPrimitiveName = 'use' + primitiveName;
827 - if (functionName.length < expectedPrimitiveName.length) {
828 - return false;
829 - }
830 - return (
831 - functionName.lastIndexOf(expectedPrimitiveName) ===
832 - functionName.length - expectedPrimitiveName.length
833 - );
837 +function isReactWrapper(functionName: any, wrapperName: string) {
838 + return parseHookName(functionName) === wrapperName;
839 }
840
841 function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
@@ -841,17 +846,18 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
846 }
847 for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) {
848 if (primitiveStack[i].source !== hookStack[i].source) {
844 - // If the next two frames are functions called `useX` then we assume that they're part of the
845 - // wrappers that the React packager or other packages adds around the dispatcher.
849 + // If the next frame is a method from the dispatcher, we
850 + // assume that the next frame after that is the actual public API call.
851 + // This prohibits nesting dispatcher calls in hooks.
852 if (
853 i < hookStack.length - 1 &&
848 - isReactWrapper(hookStack[i].functionName, hook.primitive)
854 + isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName)
855 ) {
856 i++;
857 }
858 if (
859 i < hookStack.length - 1 &&
854 - isReactWrapper(hookStack[i].functionName, hook.primitive)
860 + isReactWrapper(hookStack[i].functionName, hook.dispatcherHookName)
861 ) {
862 i++;
863 }
@@ -872,21 +878,41 @@ function parseTrimmedStack(rootStack: any, hook: HookLogEntry) {
878 primitiveIndex === -1 ||
879 rootIndex - primitiveIndex < 2
880 ) {
875 - // Something went wrong. Give up.
876 - return null;
881 + if (primitiveIndex === -1) {
882 + // Something went wrong. Give up.
883 + return [null, null];
884 + } else {
885 + return [hookStack[primitiveIndex - 1], null];
886 + }
887 }
878 - return hookStack.slice(primitiveIndex, rootIndex - 1);
888 + return [
889 + hookStack[primitiveIndex - 1],
890 + hookStack.slice(primitiveIndex, rootIndex - 1),
891 + ];
892 }
893
881 -function parseCustomHookName(functionName: void | string): string {
894 +function parseHookName(functionName: void | string): string {
895 if (!functionName) {
896 return '';
897 }
885 - let startIndex = functionName.lastIndexOf('.');
898 + let startIndex = functionName.lastIndexOf('[as ');
899 +
900 + if (startIndex !== -1) {
901 + // Workaround for sourcemaps in Jest and Chrome.
902 + // In `node --enable-source-maps`, we don't see "Object.useHostTransitionStatus [as useFormStatus]" but "Object.useFormStatus"
903 + // "Object.useHostTransitionStatus [as useFormStatus]" -> "useFormStatus"
904 + return parseHookName(functionName.slice(startIndex + '[as '.length, -1));
905 + }
906 + startIndex = functionName.lastIndexOf('.');
907 if (startIndex === -1) {
908 startIndex = 0;
909 + } else {
910 + startIndex += 1;
911 }
912 if (functionName.slice(startIndex, startIndex + 3) === 'use') {
913 + if (functionName.length - startIndex === 3) {
914 + return 'Use';
915 + }
916 startIndex += 3;
917 }
918 return functionName.slice(startIndex);
@@ -903,7 +929,17 @@ function buildTree(
929 const stackOfChildren = [];
930 for (let i = 0; i < readHookLog.length; i++) {
931 const hook = readHookLog[i];
906 - const stack = parseTrimmedStack(rootStack, hook);
932 + const parseResult = parseTrimmedStack(rootStack, hook);
933 + const primitiveFrame = parseResult[0];
934 + const stack = parseResult[1];
935 + let displayName = hook.displayName;
936 + if (displayName === null && primitiveFrame !== null) {
937 + displayName =
938 + parseHookName(primitiveFrame.functionName) ||
939 + // Older versions of React do not have sourcemaps.
940 + // In those versions there was always a 1:1 mapping between wrapper and dispatcher method.
941 + parseHookName(hook.dispatcherHookName);
942 + }
943 if (stack !== null) {
944 // Note: The indices 0 <= n < length-1 will contain the names.
945 // The indices 1 <= n < length will contain the source locations.
@@ -934,7 +970,7 @@ function buildTree(
970 const levelChild: HooksNode = {
971 id: null,
972 isStateEditable: false,
937 - name: parseCustomHookName(stack[j - 1].functionName),
973 + name: parseHookName(stack[j - 1].functionName),
974 value: undefined,
975 subHooks: children,
976 debugInfo: null,
@@ -952,7 +988,7 @@ function buildTree(
988 }
989 prevStack = stack;
990 }
955 - const {displayName, primitive, debugInfo} = hook;
991 + const {primitive, debugInfo} = hook;
992
993 // For now, the "id" of stateful hooks is just the stateful hook index.
994 // Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).
packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js
+2 -2
@@ -518,7 +518,7 @@ describe('ReactHooksInspection', () => {
518 },
519 "id": null,
520 "isStateEditable": false,
521 - "name": "Promise",
521 + "name": "Use",
522 "subHooks": [],
523 "value": "world",
524 },
@@ -568,7 +568,7 @@ describe('ReactHooksInspection', () => {
568 },
569 "id": null,
570 "isStateEditable": false,
571 - "name": "Unresolved",
571 + "name": "Use",
572 "subHooks": [],
573 "value": Any<Promise>,
574 }