bippy 0.5.42 → 0.6.0
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/dist/core.cjs +1 -1
- package/dist/core.d.cts +28 -313
- package/dist/core.d.ts +28 -313
- package/dist/core.js +1 -1
- package/dist/core2.d.cts +3 -2
- package/dist/core2.d.ts +3 -2
- package/dist/get-source.cjs +19 -0
- package/dist/get-source.js +19 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.iife.js +1 -1
- package/dist/index.js +1 -1
- package/dist/install-hook-only.cjs +1 -1
- package/dist/install-hook-only.iife.js +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/react-refresh.cjs +9 -0
- package/dist/react-refresh.d.cts +66 -0
- package/dist/react-refresh.d.ts +66 -0
- package/dist/react-refresh.js +9 -0
- package/dist/source.cjs +4 -19
- package/dist/source.d.cts +35 -27
- package/dist/source.d.ts +35 -27
- package/dist/source.js +4 -19
- package/dist/unsubscribe.d.cts +298 -0
- package/dist/unsubscribe.d.ts +298 -0
- package/package.json +14 -3
- package/src/core.ts +312 -334
- package/src/rdt-hook.ts +57 -16
- package/src/react-refresh/constants.ts +9 -0
- package/src/react-refresh/detect-hmr-transport.ts +33 -0
- package/src/react-refresh/index.ts +173 -0
- package/src/react-refresh/metro-hmr-transport.ts +188 -0
- package/src/react-refresh/next-webpack-hmr-transport.ts +72 -0
- package/src/react-refresh/normalize-hmr-file-path.ts +24 -0
- package/src/react-refresh/types.ts +7 -0
- package/src/react-refresh/vite-hmr-transport.ts +116 -0
- package/src/source/constants.ts +11 -0
- package/src/source/get-display-name-from-source.ts +3 -3
- package/src/source/get-source.ts +53 -9
- package/src/source/index.ts +17 -8
- package/src/source/inspect-hooks.ts +7 -4
- package/src/source/owner-stack.ts +193 -29
- package/src/source/parse-debug-stack.ts +133 -0
- package/src/source/parse-stack.ts +6 -105
- package/src/source/symbolication.ts +48 -13
- package/src/types.ts +20 -7
- package/src/unsubscribe.ts +17 -0
package/src/core.ts
CHANGED
|
@@ -13,12 +13,15 @@ import type {
|
|
|
13
13
|
} from "./types.js";
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
|
+
_onActiveListeners,
|
|
16
17
|
BIPPY_INSTRUMENTATION_STRING,
|
|
17
18
|
getRDTHook,
|
|
18
19
|
hasRDTHook,
|
|
19
20
|
isReactRefresh,
|
|
20
21
|
isRealReactDevtools,
|
|
22
|
+
onRendererInject,
|
|
21
23
|
} from "./rdt-hook.js";
|
|
24
|
+
import { toUnsubscribe, type Unsubscribe } from "./unsubscribe.js";
|
|
22
25
|
|
|
23
26
|
// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactWorkTags.js
|
|
24
27
|
export const FunctionComponentTag = 0;
|
|
@@ -48,6 +51,8 @@ export const ELEMENT_TYPE_SYMBOL_STRING = "Symbol(react.element)";
|
|
|
48
51
|
export const TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING = "Symbol(react.transitional.element)";
|
|
49
52
|
export const CONCURRENT_MODE_SYMBOL_STRING = "Symbol(react.concurrent_mode)";
|
|
50
53
|
export const DEPRECATED_ASYNC_MODE_SYMBOL_STRING = "Symbol(react.async_mode)";
|
|
54
|
+
export const CONCURRENT_MODE_SYMBOL_DESCRIPTION = "react.concurrent_mode";
|
|
55
|
+
export const DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION = "react.async_mode";
|
|
51
56
|
|
|
52
57
|
// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberFlags.js
|
|
53
58
|
const PerformedWork = 0b1;
|
|
@@ -135,13 +140,6 @@ export const isFiber = (maybeFiber: unknown): maybeFiber is Fiber => {
|
|
|
135
140
|
return "pendingProps" in maybeFiber && !("containerInfo" in maybeFiber);
|
|
136
141
|
};
|
|
137
142
|
|
|
138
|
-
/**
|
|
139
|
-
* Returns `true` if the two {@link Fiber}s are the same reference
|
|
140
|
-
*/
|
|
141
|
-
export const areFiberEqual = (fiberA: Fiber, fiberB: Fiber): boolean => {
|
|
142
|
-
return fiberA === fiberB || fiberA.alternate === fiberB || fiberB.alternate === fiberA;
|
|
143
|
-
};
|
|
144
|
-
|
|
145
143
|
/**
|
|
146
144
|
* Traverses up or down a {@link Fiber}'s contexts, return `true` to stop and select the current and previous context value.
|
|
147
145
|
*/
|
|
@@ -215,13 +213,12 @@ export const traverseProps = (
|
|
|
215
213
|
const nextProps = fiber.memoizedProps;
|
|
216
214
|
const prevProps = fiber.alternate?.memoizedProps || {};
|
|
217
215
|
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (selector(propName, nextValue, prevValue) === true) return true;
|
|
216
|
+
for (const propName of Object.keys(nextProps)) {
|
|
217
|
+
if (selector(propName, nextProps[propName], prevProps[propName]) === true) return true;
|
|
218
|
+
}
|
|
219
|
+
for (const propName of Object.keys(prevProps)) {
|
|
220
|
+
if (propName in nextProps) continue;
|
|
221
|
+
if (selector(propName, nextProps[propName], prevProps[propName]) === true) return true;
|
|
225
222
|
}
|
|
226
223
|
} catch {}
|
|
227
224
|
return false;
|
|
@@ -308,7 +305,7 @@ export const getFiberStack = (fiber: Fiber): Fiber[] => {
|
|
|
308
305
|
/**
|
|
309
306
|
* Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
|
|
310
307
|
*/
|
|
311
|
-
|
|
308
|
+
const shouldFilterFiber = (fiber: Fiber): boolean => {
|
|
312
309
|
switch (fiber.tag) {
|
|
313
310
|
case DehydratedSuspenseComponentTag:
|
|
314
311
|
// TODO: ideally we would show dehydrated Suspense immediately.
|
|
@@ -332,10 +329,14 @@ export const shouldFilterFiber = (fiber: Fiber): boolean => {
|
|
|
332
329
|
const symbolOrNumber =
|
|
333
330
|
typeof fiber.type === "object" && fiber.type !== null ? fiber.type.$$typeof : fiber.type;
|
|
334
331
|
|
|
335
|
-
|
|
336
|
-
|
|
332
|
+
if (typeof symbolOrNumber === "symbol") {
|
|
333
|
+
return (
|
|
334
|
+
symbolOrNumber.description === CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
|
|
335
|
+
symbolOrNumber.description === DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
|
|
336
|
+
);
|
|
337
|
+
}
|
|
337
338
|
|
|
338
|
-
switch (
|
|
339
|
+
switch (symbolOrNumber) {
|
|
339
340
|
case CONCURRENT_MODE_NUMBER:
|
|
340
341
|
case CONCURRENT_MODE_SYMBOL_STRING:
|
|
341
342
|
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
|
|
@@ -439,7 +440,7 @@ export function traverseFiber(
|
|
|
439
440
|
return null;
|
|
440
441
|
}
|
|
441
442
|
|
|
442
|
-
|
|
443
|
+
const traverseFiberSync = (
|
|
443
444
|
fiber: Fiber | null,
|
|
444
445
|
selector: (node: Fiber) => boolean | void,
|
|
445
446
|
ascending = false,
|
|
@@ -457,7 +458,7 @@ export const traverseFiberSync = (
|
|
|
457
458
|
return null;
|
|
458
459
|
};
|
|
459
460
|
|
|
460
|
-
|
|
461
|
+
const traverseFiberAsync = async (
|
|
461
462
|
fiber: Fiber | null,
|
|
462
463
|
selector: (node: Fiber) => Promise<boolean | void>,
|
|
463
464
|
ascending = false,
|
|
@@ -568,6 +569,8 @@ export const isInstrumentationActive = (): boolean => {
|
|
|
568
569
|
);
|
|
569
570
|
};
|
|
570
571
|
|
|
572
|
+
export const _fiberRoots = new Set<FiberRoot>();
|
|
573
|
+
|
|
571
574
|
/**
|
|
572
575
|
* Returns the latest fiber (since it may be double-buffered).
|
|
573
576
|
*/
|
|
@@ -591,7 +594,7 @@ export type RenderHandler = <S>(fiber: Fiber, phase: RenderPhase, state?: S) =>
|
|
|
591
594
|
export type RenderPhase = "mount" | "unmount" | "update";
|
|
592
595
|
|
|
593
596
|
let fiberId = 0;
|
|
594
|
-
|
|
597
|
+
const fiberIdMap = new WeakMap<Fiber, number>();
|
|
595
598
|
|
|
596
599
|
export const setFiberId = (fiber: Fiber, id: number = fiberId++): void => {
|
|
597
600
|
fiberIdMap.set(fiber, id);
|
|
@@ -612,7 +615,7 @@ export const getFiberId = (fiber: Fiber): number => {
|
|
|
612
615
|
return id;
|
|
613
616
|
};
|
|
614
617
|
|
|
615
|
-
|
|
618
|
+
const mountFiberRecursively = (
|
|
616
619
|
onRender: RenderHandler,
|
|
617
620
|
firstChild: Fiber,
|
|
618
621
|
traverseSiblings: boolean,
|
|
@@ -661,7 +664,7 @@ export const mountFiberRecursively = (
|
|
|
661
664
|
}
|
|
662
665
|
};
|
|
663
666
|
|
|
664
|
-
|
|
667
|
+
const updateFiberRecursively = (
|
|
665
668
|
onRender: RenderHandler,
|
|
666
669
|
nextFiber: Fiber,
|
|
667
670
|
prevFiber: Fiber,
|
|
@@ -761,7 +764,7 @@ export const updateFiberRecursively = (
|
|
|
761
764
|
}
|
|
762
765
|
};
|
|
763
766
|
|
|
764
|
-
|
|
767
|
+
const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
|
|
765
768
|
const isRoot = fiber.tag === HostRootTag;
|
|
766
769
|
|
|
767
770
|
if (isRoot || !shouldFilterFiber(fiber)) {
|
|
@@ -769,7 +772,7 @@ export const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
|
|
|
769
772
|
}
|
|
770
773
|
};
|
|
771
774
|
|
|
772
|
-
|
|
775
|
+
const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => {
|
|
773
776
|
// We might meet a nested Suspense on our way.
|
|
774
777
|
const isTimedOutSuspense = fiber.tag === SuspenseComponentTag && fiber.memoizedState !== null;
|
|
775
778
|
let child = fiber.child;
|
|
@@ -852,80 +855,85 @@ export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler)
|
|
|
852
855
|
rootInstance.prevFiber = fiber;
|
|
853
856
|
};
|
|
854
857
|
|
|
855
|
-
|
|
856
|
-
let
|
|
857
|
-
let _overrideContext: null | ReactRenderer["overrideContext"] = null;
|
|
858
|
+
const overrideRenderers = new Set<ReactRenderer>();
|
|
859
|
+
let areOverrideRenderersWired = false;
|
|
858
860
|
|
|
859
|
-
|
|
860
|
-
if (
|
|
861
|
+
const wireOverrideRenderers = (): void => {
|
|
862
|
+
if (areOverrideRenderersWired) return;
|
|
863
|
+
if (!hasRDTHook()) return;
|
|
861
864
|
const rdtHook = getRDTHook();
|
|
862
|
-
if (!rdtHook?.renderers) return
|
|
865
|
+
if (!rdtHook?.renderers) return;
|
|
866
|
+
areOverrideRenderersWired = true;
|
|
863
867
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
overrideHookState: _overrideHookState,
|
|
868
|
-
overrideProps: _overrideProps,
|
|
869
|
-
};
|
|
868
|
+
ensureHookDispatchesToListeners(rdtHook);
|
|
869
|
+
for (const renderer of rdtHook.renderers.values()) {
|
|
870
|
+
overrideRenderers.add(renderer);
|
|
870
871
|
}
|
|
872
|
+
onRendererInject((renderer) => {
|
|
873
|
+
overrideRenderers.add(renderer);
|
|
874
|
+
});
|
|
875
|
+
};
|
|
871
876
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
877
|
+
const getRootRenderer = (fiber: Fiber): ReactRenderer | null => {
|
|
878
|
+
if (!hasRDTHook()) return null;
|
|
879
|
+
let hostRootFiber = fiber;
|
|
880
|
+
while (hostRootFiber.return) {
|
|
881
|
+
hostRootFiber = hostRootFiber.return;
|
|
882
|
+
}
|
|
883
|
+
const rendererId = rootRendererIds.get(hostRootFiber.stateNode);
|
|
884
|
+
if (rendererId === undefined) return null;
|
|
885
|
+
return getRDTHook().renderers?.get(rendererId) ?? null;
|
|
886
|
+
};
|
|
882
887
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
888
|
+
// dispatch to the renderer that owns the fiber's root when known; renderers
|
|
889
|
+
// that never committed through bippy's hook patch fall back to "try all",
|
|
890
|
+
// which matches the pre-ownership behavior for single-renderer apps
|
|
891
|
+
const resolveOverrideRenderers = (fiber: Fiber): ReactRenderer[] => {
|
|
892
|
+
wireOverrideRenderers();
|
|
893
|
+
const rootRenderer = getRootRenderer(fiber);
|
|
894
|
+
if (rootRenderer) return [rootRenderer];
|
|
895
|
+
return Array.from(overrideRenderers);
|
|
896
|
+
};
|
|
891
897
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
+
const applyPropsOverride = (
|
|
899
|
+
renderers: ReactRenderer[],
|
|
900
|
+
fiber: Fiber,
|
|
901
|
+
path: string[],
|
|
902
|
+
value: unknown,
|
|
903
|
+
): void => {
|
|
904
|
+
for (const renderer of renderers) {
|
|
905
|
+
try {
|
|
906
|
+
renderer.overrideProps?.(fiber, path, value);
|
|
907
|
+
} catch {}
|
|
908
|
+
}
|
|
909
|
+
};
|
|
898
910
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
911
|
+
const getHookStateDispatch = (
|
|
912
|
+
fiber: Fiber,
|
|
913
|
+
hookIndex: number,
|
|
914
|
+
): ((value: unknown) => void) | null => {
|
|
915
|
+
let hookState = fiber.memoizedState;
|
|
916
|
+
for (let i = 0; i < hookIndex; i++) {
|
|
917
|
+
if (!hookState?.next) return null;
|
|
918
|
+
hookState = hookState.next;
|
|
919
|
+
}
|
|
920
|
+
const queue = hookState?.queue;
|
|
921
|
+
if (isPOJO(queue) && typeof queue.dispatch === "function") {
|
|
922
|
+
return queue.dispatch as (value: unknown) => void;
|
|
923
|
+
}
|
|
924
|
+
return null;
|
|
925
|
+
};
|
|
908
926
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
_overrideProps(current, ["value", ...path], value);
|
|
916
|
-
if (current.alternate) {
|
|
917
|
-
_overrideProps(current.alternate, ["value", ...path], value);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
break;
|
|
921
|
-
}
|
|
922
|
-
current = current.return;
|
|
923
|
-
}
|
|
924
|
-
};
|
|
925
|
-
} catch {
|
|
926
|
-
/**/
|
|
927
|
+
const findContextProviderFiber = (fiber: Fiber, contextType: unknown): Fiber | null => {
|
|
928
|
+
let currentFiber: Fiber | null = fiber;
|
|
929
|
+
while (currentFiber) {
|
|
930
|
+
const fiberType = currentFiber.type;
|
|
931
|
+
if (fiberType === contextType || fiberType?.Provider === contextType) {
|
|
932
|
+
return currentFiber;
|
|
927
933
|
}
|
|
934
|
+
currentFiber = currentFiber.return;
|
|
928
935
|
}
|
|
936
|
+
return null;
|
|
929
937
|
};
|
|
930
938
|
|
|
931
939
|
const isPOJO = (maybePOJO: unknown): maybePOJO is Record<string, unknown> => {
|
|
@@ -960,61 +968,54 @@ const buildPathsFromValue = (
|
|
|
960
968
|
return paths;
|
|
961
969
|
};
|
|
962
970
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
const paths = buildPathsFromValue(partialValue);
|
|
971
|
+
const buildValueWrites = (partialValue: unknown): Array<{ path: string[]; value: unknown }> =>
|
|
972
|
+
isPOJO(partialValue) ? buildPathsFromValue(partialValue) : [{ path: [], value: partialValue }];
|
|
967
973
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
974
|
+
export const overrideProps = (fiber: Fiber, partialValue: Record<string, unknown>) => {
|
|
975
|
+
const renderers = resolveOverrideRenderers(fiber);
|
|
976
|
+
for (const { path, value } of buildValueWrites(partialValue)) {
|
|
977
|
+
applyPropsOverride(renderers, fiber, path, value);
|
|
972
978
|
}
|
|
973
979
|
};
|
|
974
980
|
|
|
975
|
-
export const overrideHookState = (
|
|
976
|
-
fiber
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
_overrideHookState?.(fiber, hookId, path, value);
|
|
990
|
-
} catch {}
|
|
981
|
+
export const overrideHookState = (fiber: Fiber, id: number, partialValue: unknown) => {
|
|
982
|
+
const renderers = resolveOverrideRenderers(fiber).filter((renderer) =>
|
|
983
|
+
Boolean(renderer.overrideHookState),
|
|
984
|
+
);
|
|
985
|
+
const writes = buildValueWrites(partialValue);
|
|
986
|
+
|
|
987
|
+
if (renderers.length > 0) {
|
|
988
|
+
const hookId = String(id);
|
|
989
|
+
for (const renderer of renderers) {
|
|
990
|
+
for (const { path, value } of writes) {
|
|
991
|
+
try {
|
|
992
|
+
renderer.overrideHookState?.(fiber, hookId, path, value);
|
|
993
|
+
} catch {}
|
|
994
|
+
}
|
|
991
995
|
}
|
|
992
|
-
|
|
993
|
-
try {
|
|
994
|
-
_overrideHookState?.(fiber, hookId, [], partialValue);
|
|
995
|
-
} catch {}
|
|
996
|
+
return;
|
|
996
997
|
}
|
|
997
|
-
};
|
|
998
998
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
partialValue
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
999
|
+
// production renderers don't expose overrideHookState; dispatching through
|
|
1000
|
+
// the hook's own queue still works there, but only for whole-value writes
|
|
1001
|
+
// (a path write through dispatch would replace the entire hook state)
|
|
1002
|
+
if (isPOJO(partialValue)) return;
|
|
1003
|
+
const dispatch = getHookStateDispatch(fiber, id);
|
|
1004
|
+
if (!dispatch) return;
|
|
1005
|
+
try {
|
|
1006
|
+
dispatch(partialValue);
|
|
1007
|
+
} catch {}
|
|
1008
|
+
};
|
|
1008
1009
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1010
|
+
export const overrideContext = (fiber: Fiber, contextType: unknown, partialValue: unknown) => {
|
|
1011
|
+
const providerFiber = findContextProviderFiber(fiber, contextType);
|
|
1012
|
+
if (!providerFiber) return;
|
|
1013
|
+
const renderers = resolveOverrideRenderers(providerFiber);
|
|
1014
|
+
for (const { path, value } of buildValueWrites(partialValue)) {
|
|
1015
|
+
applyPropsOverride(renderers, providerFiber, ["value", ...path], value);
|
|
1016
|
+
if (providerFiber.alternate) {
|
|
1017
|
+
applyPropsOverride(renderers, providerFiber.alternate, ["value", ...path], value);
|
|
1013
1018
|
}
|
|
1014
|
-
} else {
|
|
1015
|
-
try {
|
|
1016
|
-
_overrideContext?.(fiber, contextType, [], partialValue);
|
|
1017
|
-
} catch {}
|
|
1018
1019
|
}
|
|
1019
1020
|
};
|
|
1020
1021
|
|
|
@@ -1027,10 +1028,133 @@ export interface InstrumentationOptions {
|
|
|
1027
1028
|
onScheduleFiberRoot?: (rendererID: number, root: FiberRoot, children: React.ReactNode) => unknown;
|
|
1028
1029
|
}
|
|
1029
1030
|
|
|
1031
|
+
const commitFiberRootListeners = new Set<
|
|
1032
|
+
NonNullable<InstrumentationOptions["onCommitFiberRoot"]>
|
|
1033
|
+
>();
|
|
1034
|
+
const commitFiberUnmountListeners = new Set<
|
|
1035
|
+
NonNullable<InstrumentationOptions["onCommitFiberUnmount"]>
|
|
1036
|
+
>();
|
|
1037
|
+
const postCommitFiberRootListeners = new Set<
|
|
1038
|
+
NonNullable<InstrumentationOptions["onPostCommitFiberRoot"]>
|
|
1039
|
+
>();
|
|
1040
|
+
const scheduleFiberRootListeners = new Set<
|
|
1041
|
+
NonNullable<InstrumentationOptions["onScheduleFiberRoot"]>
|
|
1042
|
+
>();
|
|
1043
|
+
|
|
1044
|
+
interface HookDispatchers {
|
|
1045
|
+
onCommitFiberRoot: ReactDevToolsGlobalHook["onCommitFiberRoot"];
|
|
1046
|
+
onCommitFiberUnmount: ReactDevToolsGlobalHook["onCommitFiberUnmount"];
|
|
1047
|
+
onPostCommitFiberRoot: ReactDevToolsGlobalHook["onPostCommitFiberRoot"];
|
|
1048
|
+
onScheduleFiberRoot: NonNullable<ReactDevToolsGlobalHook["onScheduleFiberRoot"]>;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const hookDispatchers = new WeakMap<ReactDevToolsGlobalHook, Partial<HookDispatchers>>();
|
|
1052
|
+
|
|
1053
|
+
const rootRendererIds = new WeakMap<FiberRoot, number>();
|
|
1054
|
+
|
|
1055
|
+
// each hook event is dispatched from a single re-installable wrapper. If
|
|
1056
|
+
// something overwrites the hook method (devtools, direct assignment), the
|
|
1057
|
+
// next instrument() call installs a fresh wrapper over it; a superseded
|
|
1058
|
+
// wrapper still forwards the previous chain but skips the listeners so
|
|
1059
|
+
// they never fire twice.
|
|
1060
|
+
const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
|
|
1061
|
+
const dispatchers = hookDispatchers.get(rdtHook) ?? {};
|
|
1062
|
+
hookDispatchers.set(rdtHook, dispatchers);
|
|
1063
|
+
|
|
1064
|
+
if (
|
|
1065
|
+
!dispatchers.onCommitFiberRoot ||
|
|
1066
|
+
rdtHook.onCommitFiberRoot !== dispatchers.onCommitFiberRoot
|
|
1067
|
+
) {
|
|
1068
|
+
const prevOnCommitFiberRoot = rdtHook.onCommitFiberRoot;
|
|
1069
|
+
const dispatchCommitFiberRoot: HookDispatchers["onCommitFiberRoot"] = (
|
|
1070
|
+
rendererID,
|
|
1071
|
+
root,
|
|
1072
|
+
priority,
|
|
1073
|
+
) => {
|
|
1074
|
+
prevOnCommitFiberRoot?.(rendererID, root, priority);
|
|
1075
|
+
if (hookDispatchers.get(rdtHook)?.onCommitFiberRoot !== dispatchCommitFiberRoot) return;
|
|
1076
|
+
_fiberRoots.add(root);
|
|
1077
|
+
rootRendererIds.set(root, rendererID);
|
|
1078
|
+
for (const listener of commitFiberRootListeners) {
|
|
1079
|
+
listener(rendererID, root, priority);
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
dispatchers.onCommitFiberRoot = dispatchCommitFiberRoot;
|
|
1083
|
+
rdtHook.onCommitFiberRoot = dispatchCommitFiberRoot;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (
|
|
1087
|
+
!dispatchers.onCommitFiberUnmount ||
|
|
1088
|
+
rdtHook.onCommitFiberUnmount !== dispatchers.onCommitFiberUnmount
|
|
1089
|
+
) {
|
|
1090
|
+
const prevOnCommitFiberUnmount = rdtHook.onCommitFiberUnmount;
|
|
1091
|
+
const dispatchCommitFiberUnmount: HookDispatchers["onCommitFiberUnmount"] = (
|
|
1092
|
+
rendererID,
|
|
1093
|
+
fiber,
|
|
1094
|
+
) => {
|
|
1095
|
+
prevOnCommitFiberUnmount?.(rendererID, fiber);
|
|
1096
|
+
if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
for (const listener of commitFiberUnmountListeners) {
|
|
1100
|
+
listener(rendererID, fiber);
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
|
|
1104
|
+
rdtHook.onCommitFiberUnmount = dispatchCommitFiberUnmount;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (
|
|
1108
|
+
!dispatchers.onPostCommitFiberRoot ||
|
|
1109
|
+
rdtHook.onPostCommitFiberRoot !== dispatchers.onPostCommitFiberRoot
|
|
1110
|
+
) {
|
|
1111
|
+
const prevOnPostCommitFiberRoot = rdtHook.onPostCommitFiberRoot;
|
|
1112
|
+
const dispatchPostCommitFiberRoot: HookDispatchers["onPostCommitFiberRoot"] = (
|
|
1113
|
+
rendererID,
|
|
1114
|
+
root,
|
|
1115
|
+
) => {
|
|
1116
|
+
prevOnPostCommitFiberRoot?.(rendererID, root);
|
|
1117
|
+
if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
for (const listener of postCommitFiberRootListeners) {
|
|
1121
|
+
listener(rendererID, root);
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
dispatchers.onPostCommitFiberRoot = dispatchPostCommitFiberRoot;
|
|
1125
|
+
rdtHook.onPostCommitFiberRoot = dispatchPostCommitFiberRoot;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (
|
|
1129
|
+
!dispatchers.onScheduleFiberRoot ||
|
|
1130
|
+
rdtHook.onScheduleFiberRoot !== dispatchers.onScheduleFiberRoot
|
|
1131
|
+
) {
|
|
1132
|
+
const prevOnScheduleFiberRoot = rdtHook.onScheduleFiberRoot;
|
|
1133
|
+
const dispatchScheduleFiberRoot: HookDispatchers["onScheduleFiberRoot"] = (
|
|
1134
|
+
rendererID,
|
|
1135
|
+
root,
|
|
1136
|
+
children,
|
|
1137
|
+
) => {
|
|
1138
|
+
prevOnScheduleFiberRoot?.(rendererID, root, children);
|
|
1139
|
+
if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
|
|
1140
|
+
for (const listener of scheduleFiberRootListeners) {
|
|
1141
|
+
listener(rendererID, root, children);
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
dispatchers.onScheduleFiberRoot = dispatchScheduleFiberRoot;
|
|
1145
|
+
rdtHook.onScheduleFiberRoot = dispatchScheduleFiberRoot;
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1030
1149
|
/**
|
|
1031
|
-
* Instruments the DevTools hook.
|
|
1150
|
+
* Instruments the DevTools hook. Each hook event is patched once and
|
|
1151
|
+
* dispatches to a set of listeners, so multiple `instrument` calls compose
|
|
1152
|
+
* without stacking patches. Returns an unsubscribe function that removes
|
|
1153
|
+
* exactly the handlers this call registered.
|
|
1154
|
+
* The returned function is also a `Disposable`, so it composes with other
|
|
1155
|
+
* bippy subscriptions through `using`.
|
|
1032
1156
|
* @example
|
|
1033
|
-
* const
|
|
1157
|
+
* const unsubscribe = instrument({
|
|
1034
1158
|
* onActive() {
|
|
1035
1159
|
* console.log('initialized');
|
|
1036
1160
|
* },
|
|
@@ -1038,51 +1162,45 @@ export interface InstrumentationOptions {
|
|
|
1038
1162
|
* console.log('fiberRoot', root.current)
|
|
1039
1163
|
* },
|
|
1040
1164
|
* });
|
|
1165
|
+
* unsubscribe();
|
|
1041
1166
|
*/
|
|
1042
|
-
export const instrument = (options: InstrumentationOptions):
|
|
1167
|
+
export const instrument = (options: InstrumentationOptions): Unsubscribe => {
|
|
1043
1168
|
const rdtHook = getRDTHook(options.onActive);
|
|
1044
1169
|
|
|
1045
1170
|
rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
|
|
1046
1171
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
options.onCommitFiberUnmount?.(rendererID, root);
|
|
1070
|
-
};
|
|
1071
|
-
rdtHook.onCommitFiberUnmount = handler;
|
|
1072
|
-
}
|
|
1172
|
+
ensureHookDispatchesToListeners(rdtHook);
|
|
1173
|
+
|
|
1174
|
+
const {
|
|
1175
|
+
onActive,
|
|
1176
|
+
onCommitFiberRoot,
|
|
1177
|
+
onCommitFiberUnmount,
|
|
1178
|
+
onPostCommitFiberRoot,
|
|
1179
|
+
onScheduleFiberRoot,
|
|
1180
|
+
} = options;
|
|
1181
|
+
if (onCommitFiberRoot) commitFiberRootListeners.add(onCommitFiberRoot);
|
|
1182
|
+
if (onCommitFiberUnmount) commitFiberUnmountListeners.add(onCommitFiberUnmount);
|
|
1183
|
+
if (onPostCommitFiberRoot) postCommitFiberRootListeners.add(onPostCommitFiberRoot);
|
|
1184
|
+
if (onScheduleFiberRoot) scheduleFiberRootListeners.add(onScheduleFiberRoot);
|
|
1185
|
+
|
|
1186
|
+
return toUnsubscribe(() => {
|
|
1187
|
+
if (onActive) _onActiveListeners.delete(onActive);
|
|
1188
|
+
if (onCommitFiberRoot) commitFiberRootListeners.delete(onCommitFiberRoot);
|
|
1189
|
+
if (onCommitFiberUnmount) commitFiberUnmountListeners.delete(onCommitFiberUnmount);
|
|
1190
|
+
if (onPostCommitFiberRoot) postCommitFiberRootListeners.delete(onPostCommitFiberRoot);
|
|
1191
|
+
if (onScheduleFiberRoot) scheduleFiberRootListeners.delete(onScheduleFiberRoot);
|
|
1192
|
+
});
|
|
1193
|
+
};
|
|
1073
1194
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
prevOnPostCommitFiberRoot?.(rendererID, root);
|
|
1079
|
-
options.onPostCommitFiberRoot?.(rendererID, root);
|
|
1080
|
-
};
|
|
1081
|
-
rdtHook.onPostCommitFiberRoot = handler;
|
|
1082
|
-
}
|
|
1195
|
+
// React stamps fibers under per-renderer random-suffix keys (`__reactFiber$<suffix>`);
|
|
1196
|
+
// caching discovered keys makes repeat lookups a single property read instead of a
|
|
1197
|
+
// scan over every own key of the element.
|
|
1198
|
+
const knownFiberPropertyKeys = new Set<string>();
|
|
1083
1199
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1200
|
+
const isFiberPropertyKey = (key: string): boolean =>
|
|
1201
|
+
key.startsWith("__reactContainer$") ||
|
|
1202
|
+
key.startsWith("__reactInternalInstance$") ||
|
|
1203
|
+
key.startsWith("__reactFiber");
|
|
1086
1204
|
|
|
1087
1205
|
export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
|
|
1088
1206
|
const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
@@ -1101,174 +1219,34 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
|
|
|
1101
1219
|
return (hostInstance._reactRootContainer as any)?._internalRoot?.current?.child;
|
|
1102
1220
|
}
|
|
1103
1221
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
)
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
return null;
|
|
1115
|
-
};
|
|
1116
|
-
|
|
1117
|
-
export const INSTALL_ERROR = new Error();
|
|
1118
|
-
|
|
1119
|
-
export const _fiberRoots = new Set<FiberRoot>();
|
|
1120
|
-
|
|
1121
|
-
export const secure = (
|
|
1122
|
-
options: InstrumentationOptions,
|
|
1123
|
-
secureOptions: {
|
|
1124
|
-
dangerouslyRunInProduction?: boolean;
|
|
1125
|
-
installCheckTimeout?: number;
|
|
1126
|
-
isProduction?: boolean;
|
|
1127
|
-
minReactMajorVersion?: number;
|
|
1128
|
-
onError?: (error?: unknown) => unknown;
|
|
1129
|
-
} = {},
|
|
1130
|
-
): InstrumentationOptions => {
|
|
1131
|
-
const onActive = options.onActive;
|
|
1132
|
-
const isRDTHookInstalled = hasRDTHook();
|
|
1133
|
-
const isUsingRealReactDevtools = isRealReactDevtools();
|
|
1134
|
-
const isUsingReactRefresh = isReactRefresh();
|
|
1135
|
-
let timeout: number | undefined;
|
|
1136
|
-
let isDevelopment = !secureOptions.isProduction;
|
|
1137
|
-
|
|
1138
|
-
options.onActive = () => {
|
|
1139
|
-
clearTimeout(timeout);
|
|
1140
|
-
let isSecure = true;
|
|
1141
|
-
try {
|
|
1142
|
-
const rdtHook = getRDTHook();
|
|
1143
|
-
|
|
1144
|
-
for (const renderer of rdtHook.renderers.values()) {
|
|
1145
|
-
const [majorVersion] = renderer.version.split(".");
|
|
1146
|
-
if (Number(majorVersion) < (secureOptions.minReactMajorVersion ?? 17)) {
|
|
1147
|
-
isSecure = false;
|
|
1148
|
-
}
|
|
1149
|
-
const buildType = detectReactBuildType(renderer);
|
|
1150
|
-
if (buildType === "development") {
|
|
1151
|
-
isDevelopment = true;
|
|
1152
|
-
} else if (!secureOptions.dangerouslyRunInProduction) {
|
|
1153
|
-
isSecure = false;
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
} catch (err) {
|
|
1157
|
-
secureOptions.onError?.(err);
|
|
1222
|
+
// React Native Fabric public instances (ReactNativeElement /
|
|
1223
|
+
// ReactFabricHostComponent) store their fiber as the "internal instance
|
|
1224
|
+
// handle" instead of a __reactFiber$-prefixed key
|
|
1225
|
+
const fabricInstanceHandle =
|
|
1226
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1227
|
+
(hostInstance as any).__internalInstanceHandle ??
|
|
1228
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1229
|
+
(hostInstance as any)._internalInstanceHandle;
|
|
1230
|
+
if (isFiber(fabricInstanceHandle)) {
|
|
1231
|
+
return fabricInstanceHandle;
|
|
1158
1232
|
}
|
|
1159
1233
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
options.onActive = undefined;
|
|
1165
|
-
return;
|
|
1234
|
+
const hostInstanceRecord = hostInstance as Record<string, unknown>;
|
|
1235
|
+
for (const knownKey of knownFiberPropertyKeys) {
|
|
1236
|
+
const fiber = hostInstanceRecord[knownKey];
|
|
1237
|
+
if (fiber) return fiber as Fiber;
|
|
1166
1238
|
}
|
|
1167
|
-
onActive?.();
|
|
1168
1239
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
if (!_fiberRoots.has(root)) {
|
|
1174
|
-
_fiberRoots.add(root);
|
|
1175
|
-
}
|
|
1176
|
-
try {
|
|
1177
|
-
onCommitFiberRoot(rendererID, root, priority);
|
|
1178
|
-
} catch (err) {
|
|
1179
|
-
secureOptions.onError?.(err);
|
|
1180
|
-
}
|
|
1181
|
-
};
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
const onCommitFiberUnmount = options.onCommitFiberUnmount;
|
|
1185
|
-
if (onCommitFiberUnmount) {
|
|
1186
|
-
options.onCommitFiberUnmount = (rendererID, root) => {
|
|
1187
|
-
try {
|
|
1188
|
-
onCommitFiberUnmount(rendererID, root);
|
|
1189
|
-
} catch (err) {
|
|
1190
|
-
secureOptions.onError?.(err);
|
|
1191
|
-
}
|
|
1192
|
-
};
|
|
1240
|
+
for (const key of Object.keys(hostInstanceRecord)) {
|
|
1241
|
+
if (isFiberPropertyKey(key)) {
|
|
1242
|
+
knownFiberPropertyKeys.add(key);
|
|
1243
|
+
return (hostInstanceRecord[key] || null) as Fiber | null;
|
|
1193
1244
|
}
|
|
1194
|
-
|
|
1195
|
-
const onPostCommitFiberRoot = options.onPostCommitFiberRoot;
|
|
1196
|
-
if (onPostCommitFiberRoot) {
|
|
1197
|
-
options.onPostCommitFiberRoot = (rendererID, root) => {
|
|
1198
|
-
try {
|
|
1199
|
-
onPostCommitFiberRoot(rendererID, root);
|
|
1200
|
-
} catch (err) {
|
|
1201
|
-
secureOptions.onError?.(err);
|
|
1202
|
-
}
|
|
1203
|
-
};
|
|
1204
|
-
}
|
|
1205
|
-
} catch (err) {
|
|
1206
|
-
secureOptions.onError?.(err);
|
|
1207
1245
|
}
|
|
1208
|
-
};
|
|
1209
|
-
|
|
1210
|
-
if (!isRDTHookInstalled && !isUsingRealReactDevtools && !isUsingReactRefresh) {
|
|
1211
|
-
timeout = setTimeout(() => {
|
|
1212
|
-
if (isDevelopment) {
|
|
1213
|
-
secureOptions.onError?.(INSTALL_ERROR);
|
|
1214
|
-
}
|
|
1215
|
-
stop();
|
|
1216
|
-
}, secureOptions.installCheckTimeout ?? 100) as unknown as number;
|
|
1217
1246
|
}
|
|
1218
|
-
|
|
1219
|
-
return options;
|
|
1220
|
-
};
|
|
1221
|
-
|
|
1222
|
-
const swapFiberAndSchedule = (
|
|
1223
|
-
fiber: Fiber,
|
|
1224
|
-
nextType: React.ComponentType<unknown>,
|
|
1225
|
-
renderer: ReactRenderer,
|
|
1226
|
-
): void => {
|
|
1227
|
-
fiber.type = nextType;
|
|
1228
|
-
if (fiber.alternate) {
|
|
1229
|
-
fiber.alternate.type = nextType;
|
|
1230
|
-
}
|
|
1231
|
-
// HACK: shallow-clone memoizedProps so React sees oldProps !== newProps
|
|
1232
|
-
// and skips the bailout path (same trick as overrideHookState in ReactFiberReconciler)
|
|
1233
|
-
fiber.memoizedProps = { ...fiber.memoizedProps };
|
|
1234
|
-
|
|
1235
|
-
if (renderer.scheduleUpdate) {
|
|
1236
|
-
renderer.scheduleUpdate(fiber);
|
|
1237
|
-
}
|
|
1238
|
-
};
|
|
1239
|
-
|
|
1240
|
-
/**
|
|
1241
|
-
* Replaces every fiber whose type matches `prevType` with `nextType` and
|
|
1242
|
-
* triggers a synchronous re-render for each one.
|
|
1243
|
-
* The new function must follow the same Rules of Hooks as the original.
|
|
1244
|
-
* DEV-only — `renderer.scheduleUpdate` is not available in production builds.
|
|
1245
|
-
*/
|
|
1246
|
-
export const hotSwapFiberType = (fiber: Fiber, nextType: React.ComponentType<unknown>): void => {
|
|
1247
|
-
const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1248
|
-
if (!rdtHook?.renderers) return;
|
|
1249
|
-
|
|
1250
|
-
const renderer = Array.from(rdtHook.renderers.values()).find(
|
|
1251
|
-
(innerRenderer) => !!innerRenderer.scheduleUpdate,
|
|
1252
|
-
);
|
|
1253
|
-
if (!renderer) return;
|
|
1254
|
-
|
|
1255
|
-
const prevType = getType(fiber.type);
|
|
1256
|
-
if (!prevType) {
|
|
1257
|
-
swapFiberAndSchedule(fiber, nextType, renderer);
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
let rootFiber: Fiber = fiber;
|
|
1262
|
-
while (rootFiber.return) {
|
|
1263
|
-
rootFiber = rootFiber.return;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
traverseFiberSync(rootFiber, (innerFiber) => {
|
|
1267
|
-
if (getType(innerFiber.type) === prevType) {
|
|
1268
|
-
swapFiberAndSchedule(innerFiber, nextType, renderer);
|
|
1269
|
-
}
|
|
1270
|
-
});
|
|
1247
|
+
return null;
|
|
1271
1248
|
};
|
|
1272
1249
|
|
|
1273
1250
|
export * from "./rdt-hook.js";
|
|
1251
|
+
export * from "./unsubscribe.js";
|
|
1274
1252
|
export type * from "./types.js";
|