bippy 0.7.3-dev.1d4f35b → 0.7.3-dev.775b69e

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/src/react.ts CHANGED
@@ -1,77 +1,152 @@
1
1
  import "./install-hook-only.js";
2
- import * as React from "react";
3
- import { isFiber } from "./core.js";
2
+ import React from "react";
3
+ import { isFiber, traverseFiber } from "./core.js";
4
+ import { _renderers } from "./rdt-hook.js";
4
5
  import type { Fiber } from "./react-internals/index.js";
5
6
 
6
7
  export type { Fiber } from "./react-internals/index.js";
7
8
 
8
- const preserveState = (state: undefined): undefined => state;
9
- const readEmptySnapshot = (): undefined => undefined;
10
- const unsubscribeFromEmptyStore = (): void => {};
11
- const subscribeToEmptyStore = (): (() => void) => unsubscribeFromEmptyStore;
12
- const useSyncExternalStore: unknown = Reflect.get(React, "useSyncExternalStore");
9
+ interface RenderMarker {
10
+ (state: undefined): undefined;
11
+ }
13
12
 
14
- const captureFiberFromHook = (useCaptureHook: () => void): Fiber | null => {
15
- const originalBind = Function.prototype.bind;
16
- let capturedFiber: Fiber | null = null;
17
- // HACK: React binds hook callbacks to the rendering Fiber in production but exposes no public API for it.
13
+ interface ReducerQueue {
14
+ lastRenderedReducer: unknown;
15
+ }
16
+
17
+ interface FiberCapture {
18
+ fiber: Fiber;
19
+ queue: ReducerQueue | null;
20
+ }
21
+
22
+ const isReducerQueue = (value: unknown): value is ReducerQueue =>
23
+ typeof value === "object" && value !== null && "lastRenderedReducer" in value;
24
+
25
+ const hasRenderMarker = (fiber: Fiber, renderMarker: RenderMarker): boolean => {
26
+ for (let hook = fiber.memoizedState; hook; hook = hook.next) {
27
+ if (Array.isArray(hook.memoizedState) && hook.memoizedState[0] === renderMarker) return true;
28
+ }
29
+ return false;
30
+ };
31
+
32
+ const getReducerQueue = (fiber: Fiber, renderMarker: RenderMarker): ReducerQueue | null => {
33
+ for (let hook = fiber.memoizedState; hook; hook = hook.next) {
34
+ if (isReducerQueue(hook.queue) && hook.queue.lastRenderedReducer === renderMarker)
35
+ return hook.queue;
36
+ }
37
+ return null;
38
+ };
39
+
40
+ const getDevToolsFiber = (renderMarker: RenderMarker): Fiber | null => {
41
+ for (const renderer of _renderers) {
42
+ try {
43
+ const fiber = renderer.getCurrentFiber?.();
44
+ if (fiber && isFiber(fiber) && hasRenderMarker(fiber, renderMarker)) return fiber;
45
+ } catch {}
46
+ }
47
+ return null;
48
+ };
49
+
50
+ const captureReducerFiber = (
51
+ renderMarker: RenderMarker,
52
+ shouldCapture: boolean,
53
+ ): FiberCapture | null => {
54
+ let originalBind: typeof Function.prototype.bind | undefined;
55
+ if (shouldCapture) {
56
+ try {
57
+ originalBind = Function.prototype.bind;
58
+ } catch {}
59
+ }
60
+ if (!originalBind) {
61
+ React.useReducer(renderMarker, undefined);
62
+ return null;
63
+ }
64
+
65
+ let capture: FiberCapture | null = null;
66
+ let isCapturing = true;
67
+ // HACK: Production React binds a new reducer's Fiber and queue; validate the private reducer marker, not argument positions.
18
68
  const bindProxy = new Proxy(originalBind, {
19
- apply: (bind, functionToBind, boundArguments) => {
20
- const fiber = boundArguments[1];
21
- if (!capturedFiber && isFiber(fiber)) {
22
- capturedFiber = fiber;
69
+ apply: (bind, callback, boundArguments) => {
70
+ if (isCapturing && !capture) {
71
+ try {
72
+ const queue = boundArguments.find(
73
+ (argument): argument is ReducerQueue =>
74
+ isReducerQueue(argument) && argument.lastRenderedReducer === renderMarker,
75
+ );
76
+ if (queue) {
77
+ const fiber = boundArguments.find(
78
+ (argument): argument is Fiber => isFiber(argument) && isFiber(argument.return),
79
+ );
80
+ if (fiber) capture = { fiber, queue };
81
+ }
82
+ } catch {}
23
83
  }
24
- return Reflect.apply(bind, functionToBind, boundArguments);
84
+ return Reflect.apply(bind, callback, boundArguments);
25
85
  },
26
86
  });
27
- Reflect.set(Function.prototype, "bind", bindProxy);
28
87
 
29
88
  try {
30
- useCaptureHook();
89
+ try {
90
+ Reflect.set(Function.prototype, "bind", bindProxy);
91
+ } catch {}
92
+ React.useReducer(renderMarker, undefined);
93
+ return capture;
31
94
  } finally {
32
- if (Function.prototype.bind === bindProxy) {
33
- Reflect.set(Function.prototype, "bind", originalBind);
34
- }
95
+ isCapturing = false;
96
+ capture = null;
97
+ try {
98
+ if (Function.prototype.bind === bindProxy) {
99
+ Reflect.set(Function.prototype, "bind", originalBind);
100
+ }
101
+ } catch {}
35
102
  }
36
-
37
- return capturedFiber;
38
103
  };
39
104
 
40
- const useExternalStoreCapture = (): void => {
41
- if (typeof useSyncExternalStore !== "function") return;
42
- Reflect.apply(useSyncExternalStore, React, [
43
- subscribeToEmptyStore,
44
- readEmptySnapshot,
45
- readEmptySnapshot,
46
- ]);
47
- };
48
-
49
- const useReducerCapture = (): void => {
50
- React.useReducer(preserveState, undefined);
105
+ // HACK: React <16.13 attaches hooks after render; the shared reducer queue validates the root-walk fallback.
106
+ const getRenderingFiberFromRoot = (
107
+ { fiber, queue }: FiberCapture,
108
+ renderMarker: RenderMarker,
109
+ ): Fiber | null => {
110
+ if (queue?.lastRenderedReducer !== renderMarker) return null;
111
+ let rootFiber = fiber;
112
+ while (rootFiber.return) rootFiber = rootFiber.return;
113
+ const root = rootFiber.stateNode;
114
+ if (typeof root !== "object" || root === null || !("current" in root) || !isFiber(root.current)) {
115
+ return null;
116
+ }
117
+ const renderingRoot = root.current.alternate;
118
+ return traverseFiber(renderingRoot, (candidate) => {
119
+ if (candidate !== fiber && candidate !== fiber.alternate) return false;
120
+ let parent = candidate;
121
+ while (parent.return) parent = parent.return;
122
+ return parent === renderingRoot;
123
+ });
51
124
  };
52
125
 
53
- const useFiberWithExternalStore = (): Fiber | undefined =>
54
- captureFiberFromHook(useExternalStoreCapture) ?? undefined;
55
-
56
- const useFiberWithReducer = (): Fiber | undefined => {
57
- const committedFiberRef = React.useRef<Fiber | null>(null);
58
- const renderedFiberRef = React.useRef<Fiber | null>(null);
59
- const hookFiber = captureFiberFromHook(useReducerCapture);
126
+ export const useFiber = (): Fiber | undefined => {
127
+ "use no memo";
128
+ const fiberRef = React.useRef<FiberCapture | null>(null);
129
+ const renderMarker: RenderMarker = (state) => state;
130
+ React.useMemo(() => renderMarker, [renderMarker]);
131
+ const devToolsFiber = fiberRef.current ? null : getDevToolsFiber(renderMarker);
132
+ const capture = captureReducerFiber(renderMarker, !fiberRef.current && !devToolsFiber);
133
+ if (devToolsFiber) {
134
+ fiberRef.current = {
135
+ fiber: devToolsFiber,
136
+ queue: getReducerQueue(devToolsFiber, renderMarker),
137
+ };
138
+ return devToolsFiber;
139
+ }
140
+ const knownCapture = capture ?? fiberRef.current;
141
+ if (!knownCapture) return undefined;
142
+ const knownFiber = knownCapture.fiber;
60
143
  const fiber =
61
- hookFiber ??
62
- (renderedFiberRef.current !== committedFiberRef.current
63
- ? renderedFiberRef.current
64
- : committedFiberRef.current?.alternate) ??
65
- committedFiberRef.current;
66
-
67
- renderedFiberRef.current = fiber;
68
-
69
- React.useEffect(() => {
70
- committedFiberRef.current = fiber;
71
- }, [fiber]);
72
-
144
+ capture?.fiber ??
145
+ (hasRenderMarker(knownFiber, renderMarker)
146
+ ? knownFiber
147
+ : knownFiber.alternate && hasRenderMarker(knownFiber.alternate, renderMarker)
148
+ ? knownFiber.alternate
149
+ : getRenderingFiberFromRoot(knownCapture, renderMarker));
150
+ if (fiber) fiberRef.current = { fiber, queue: knownCapture.queue };
73
151
  return fiber ?? undefined;
74
152
  };
75
-
76
- export const useFiber =
77
- typeof useSyncExternalStore === "function" ? useFiberWithExternalStore : useFiberWithReducer;
@@ -87,6 +87,16 @@ let currentHook: MemoizedState | null = null;
87
87
  let currentContextDependency: ContextDependency<unknown> | null = null;
88
88
  let currentThenableIndex = 0;
89
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
+ };
90
100
 
91
101
  const SuspenseException: unknown = new Error(
92
102
  "Suspense interrupted this render. This error is an internal implementation detail of `use`.",
@@ -323,16 +333,8 @@ const dispatcherUseMemoCache = (size: number): unknown[] => {
323
333
  const memoCache = fiber.updateQueue?.memoCache;
324
334
  if (memoCache === null || memoCache === undefined) return [];
325
335
 
326
- let memoCacheSlots = memoCache.data[memoCache.index];
327
- if (memoCacheSlots === undefined) {
328
- memoCacheSlots = memoCache.data[memoCache.index] = Array.from(
329
- { length: size },
330
- () => REACT_MEMO_CACHE_SENTINEL,
331
- );
332
- }
333
-
334
- memoCache.index++;
335
- return memoCacheSlots;
336
+ const memoCacheSlots = memoCache.data[currentMemoCacheIndex++];
337
+ return memoCacheSlots?.slice() ?? Array.from({ length: size }, () => REACT_MEMO_CACHE_SENTINEL);
336
338
  };
337
339
 
338
340
  const dispatcherUseOptimistic = (passthrough: unknown): [unknown, () => void] => {
@@ -834,6 +836,7 @@ const performDispatcherInspection = <TArgs extends unknown[]>(
834
836
  ): HooksTree => {
835
837
  const previousDispatcher = readDispatcher(dispatcherRef);
836
838
  writeDispatcher(dispatcherRef, dispatcherProxy);
839
+ isInspectingHooks = true;
837
840
 
838
841
  let capturedHookLog: HookLogEntry[] = [];
839
842
  let ancestorStackError: Error | undefined;
@@ -846,6 +849,7 @@ const performDispatcherInspection = <TArgs extends unknown[]>(
846
849
  } finally {
847
850
  capturedHookLog = hookLog;
848
851
  hookLog = [];
852
+ isInspectingHooks = false;
849
853
  writeDispatcher(dispatcherRef, previousDispatcher);
850
854
  }
851
855
 
@@ -938,6 +942,7 @@ export const inspectHooks = (
938
942
  props: Record<string, unknown>,
939
943
  target: ReactDevToolsTarget = globalThis,
940
944
  ): HooksTree => {
945
+ assertNotInspectingHooks();
941
946
  const dispatcherRef = requireDispatcherRef(undefined, target);
942
947
  getPrimitiveStackCache();
943
948
  currentHook = null;
@@ -961,6 +966,7 @@ export const inspectHooks = (
961
966
  };
962
967
 
963
968
  export const getFiberHooks = (fiber: Fiber): HooksTree => {
969
+ assertNotInspectingHooks();
964
970
  const dispatcherRef = requireDispatcherRef(fiber);
965
971
  const workTags = getReactWorkTagsForFiber(fiber);
966
972
 
@@ -976,6 +982,7 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
976
982
 
977
983
  currentHook = fiber.memoizedState;
978
984
  currentFiber = fiber;
985
+ currentMemoCacheIndex = 0;
979
986
 
980
987
  const debugThenableState = fiber.dependencies?._debugThenableState;
981
988
  const usedThenables = Array.isArray(debugThenableState)
@@ -984,18 +991,16 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
984
991
  currentThenableState = Array.isArray(usedThenables) ? usedThenables : null;
985
992
  currentThenableIndex = 0;
986
993
 
987
- resolveContextDependency(fiber);
988
-
989
- const type = fiber.type;
990
- let props = fiber.memoizedProps;
991
- if (type !== fiber.elementType) {
992
- props = resolveDefaultProps(type, props);
993
- }
994
-
995
994
  const originalConsoleMethods = suppressConsole();
996
995
  const contextMap = new Map<ReactContext<unknown>, unknown>();
997
996
 
998
997
  try {
998
+ resolveContextDependency(fiber);
999
+ const type = fiber.type;
1000
+ const props =
1001
+ type !== fiber.elementType
1002
+ ? resolveDefaultProps(type, fiber.memoizedProps)
1003
+ : fiber.memoizedProps;
999
1004
  if (
1000
1005
  currentContextDependency !== null &&
1001
1006
  !Object.hasOwn(currentContextDependency, "memoizedValue")
@@ -1030,6 +1035,7 @@ export const getFiberHooks = (fiber: Fiber): HooksTree => {
1030
1035
  currentContextDependency = null;
1031
1036
  currentThenableState = null;
1032
1037
  currentThenableIndex = 0;
1038
+ currentMemoCacheIndex = 0;
1033
1039
  restoreContexts(contextMap);
1034
1040
  restoreConsole(originalConsoleMethods);
1035
1041
  }