bippy 0.6.1-dev.fe75557 → 0.7.0-dev.697d535

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.
Files changed (67) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +223 -461
  3. package/dist/core.cjs +1 -1
  4. package/dist/core.js +1 -1
  5. package/dist/errors.d.cts +410 -0
  6. package/dist/errors.d.ts +410 -0
  7. package/dist/index.cjs +1 -1
  8. package/dist/index.d.cts +153 -3
  9. package/dist/index.d.ts +153 -3
  10. package/dist/index.js +1 -1
  11. package/dist/install-hook-only.cjs +1 -1
  12. package/dist/install-hook-only.d.cts +9 -1
  13. package/dist/install-hook-only.d.ts +9 -1
  14. package/dist/install-hook-only.js +1 -1
  15. package/dist/rdt-hook.cjs +1 -1
  16. package/dist/rdt-hook.js +1 -1
  17. package/dist/source.cjs +14 -5
  18. package/dist/source.d.cts +86 -69
  19. package/dist/source.d.ts +86 -69
  20. package/dist/source.js +14 -5
  21. package/package.json +26 -27
  22. package/src/core.ts +353 -685
  23. package/src/errors.ts +34 -0
  24. package/src/index.ts +1 -0
  25. package/src/install-hook-only.ts +2 -2
  26. package/src/rdt-hook.ts +160 -161
  27. package/src/react-internals/generated/react-work-tags.ts +318 -0
  28. package/src/react-internals/index.ts +67 -0
  29. package/src/react-internals/semver.ts +80 -0
  30. package/src/react-internals/types.ts +186 -0
  31. package/src/react.ts +76 -0
  32. package/src/source/constants.ts +1 -1
  33. package/src/source/error-stack.ts +11 -0
  34. package/src/source/get-display-name-from-source.ts +44 -40
  35. package/src/source/get-source.ts +43 -26
  36. package/src/source/index.ts +11 -1
  37. package/src/source/inspect-hooks.ts +220 -207
  38. package/src/source/owner-stack.ts +119 -174
  39. package/src/source/parse-debug-stack.ts +4 -4
  40. package/src/source/parse-hook-names.ts +14 -53
  41. package/src/source/parse-stack.ts +14 -31
  42. package/src/source/renderer-dispatchers.ts +30 -0
  43. package/src/source/symbolication.ts +709 -120
  44. package/dist/core.d.cts +0 -214
  45. package/dist/core.d.ts +0 -214
  46. package/dist/core2.d.cts +0 -3
  47. package/dist/core2.d.ts +0 -3
  48. package/dist/get-source.cjs +0 -19
  49. package/dist/get-source.js +0 -19
  50. package/dist/index.iife.js +0 -9
  51. package/dist/install-hook-only.iife.js +0 -9
  52. package/dist/react-refresh.cjs +0 -9
  53. package/dist/react-refresh.d.cts +0 -66
  54. package/dist/react-refresh.d.ts +0 -66
  55. package/dist/react-refresh.js +0 -9
  56. package/dist/unsubscribe.d.cts +0 -298
  57. package/dist/unsubscribe.d.ts +0 -298
  58. package/src/react-refresh/constants.ts +0 -9
  59. package/src/react-refresh/detect-hmr-transport.ts +0 -33
  60. package/src/react-refresh/index.ts +0 -173
  61. package/src/react-refresh/metro-hmr-transport.ts +0 -188
  62. package/src/react-refresh/next-webpack-hmr-transport.ts +0 -72
  63. package/src/react-refresh/normalize-hmr-file-path.ts +0 -24
  64. package/src/react-refresh/types.ts +0 -7
  65. package/src/react-refresh/vite-hmr-transport.ts +0 -116
  66. package/src/types.ts +0 -438
  67. package/src/unsubscribe.ts +0 -17
package/src/core.ts CHANGED
@@ -1,71 +1,58 @@
1
- // [!!!] IMPORTANT: do not import React in this file
2
- // since it will be executed before the react devtools hook is created
3
-
1
+ // React must remain a type-only import because this module loads immediately after the DevTools hook.
4
2
  import type * as React from "react";
5
3
 
6
4
  import type {
7
- ContextDependency,
8
5
  Fiber,
9
6
  FiberRoot,
10
- MemoizedState,
7
+ HostFiber,
11
8
  ReactDevToolsGlobalHook,
12
9
  ReactRenderer,
13
- } from "./types.js";
10
+ } from "./react-internals/index.js";
14
11
 
15
12
  import {
16
13
  _onActiveListeners,
17
14
  BIPPY_INSTRUMENTATION_STRING,
15
+ createUnsubscribe,
18
16
  getRDTHook,
19
17
  hasRDTHook,
20
- isReactRefresh,
21
18
  isRealReactDevtools,
22
- onRendererInject,
19
+ onRDTHookReplace,
23
20
  } from "./rdt-hook.js";
24
- import { toUnsubscribe, type Unsubscribe } from "./unsubscribe.js";
25
-
26
- // https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactWorkTags.js
27
- export const FunctionComponentTag = 0;
28
- export const ClassComponentTag = 1;
29
- export const HostRootTag = 3;
30
- export const HostPortalTag = 4;
31
- export const HostComponentTag = 5;
32
- export const HostTextTag = 6;
33
- export const FragmentTag = 7;
34
- export const ContextConsumerTag = 9;
35
- export const ForwardRefTag = 11;
36
- export const SuspenseComponentTag = 13;
37
- export const MemoComponentTag = 14;
38
- export const SimpleMemoComponentTag = 15;
39
- export const LazyComponentTag = 16;
40
- export const DehydratedSuspenseComponentTag = 18;
41
- export const SuspenseListComponentTag = 19;
42
- export const OffscreenComponentTag = 22;
43
- export const LegacyHiddenComponentTag = 23;
44
- export const HostHoistableTag = 26;
45
- export const HostSingletonTag = 27;
46
- export const ActivityComponentTag = 28;
47
- export const ViewTransitionComponentTag = 30;
48
-
49
- export const CONCURRENT_MODE_NUMBER = 0xeacf;
50
- export const ELEMENT_TYPE_SYMBOL_STRING = "Symbol(react.element)";
51
- export const TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING = "Symbol(react.transitional.element)";
52
- export const CONCURRENT_MODE_SYMBOL_STRING = "Symbol(react.concurrent_mode)";
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";
56
-
57
- // https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberFlags.js
58
- const PerformedWork = 0b1;
59
- const Placement = 0b10;
60
- const Hydrating = 0b1000000000000;
61
- const Update = 0b100;
62
- const Cloned = 0b1000;
63
- const ChildDeletion = 0b10000;
64
- const ContentReset = 0b100000;
65
- const Snapshot = 0b10000000000;
66
- const Visibility = 0b10000000000000;
67
- const MutationMask =
68
- Placement | Update | ChildDeletion | ContentReset | Hydrating | Visibility | Snapshot;
21
+ import type { Unsubscribe } from "./rdt-hook.js";
22
+ import {
23
+ getReactWorkTagsForFiber,
24
+ MutationMask,
25
+ ReactBuildType,
26
+ ReactFiberFlags,
27
+ ReactSymbols,
28
+ setReactWorkTagsForFiber,
29
+ } from "./react-internals/index.js";
30
+
31
+ export {
32
+ BippyError,
33
+ BippyHookInspectionError,
34
+ BippyHookRenderError,
35
+ BippySourceMapError,
36
+ BippyUnsupportedHookError,
37
+ } from "./errors.js";
38
+
39
+ const isComponentType = (value: unknown): value is React.ComponentType<unknown> =>
40
+ typeof value === "function";
41
+
42
+ const getTypeName = (value: object): string | null => {
43
+ const displayName = "displayName" in value ? value.displayName : null;
44
+ if (typeof displayName === "string" && displayName) return displayName;
45
+ const name = "name" in value ? value.name : null;
46
+ return typeof name === "string" && name ? name : null;
47
+ };
48
+
49
+ interface FiberSelector {
50
+ (node: Fiber): boolean | Promise<boolean | void> | void;
51
+ }
52
+
53
+ export interface RenderHandler {
54
+ (fiber: Fiber, phase: RenderPhase): unknown;
55
+ }
69
56
 
70
57
  /**
71
58
  * Returns `true` if object is a React Element.
@@ -74,19 +61,17 @@ const MutationMask =
74
61
  */
75
62
  export const isValidElement = (element: unknown): element is React.ReactElement =>
76
63
  typeof element === "object" &&
77
- element != null &&
64
+ element !== null &&
78
65
  "$$typeof" in element &&
79
- // react 18 uses Symbol.for('react.element'), react 19 uses Symbol.for('react.transitional.element')
80
- [ELEMENT_TYPE_SYMBOL_STRING, TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING].includes(
81
- String(element.$$typeof),
82
- );
66
+ (String(element.$$typeof) === ReactSymbols.LEGACY_ELEMENT_SYMBOL_STRING ||
67
+ String(element.$$typeof) === ReactSymbols.ELEMENT_SYMBOL_STRING);
83
68
 
84
69
  /**
85
70
  * Returns `true` if object is a React Fiber.
86
71
  */
87
- export const isValidFiber = (fiber: unknown): fiber is Fiber =>
72
+ export const isFiber = (fiber: unknown): fiber is Fiber =>
88
73
  typeof fiber === "object" &&
89
- fiber != null &&
74
+ fiber !== null &&
90
75
  "tag" in fiber &&
91
76
  "stateNode" in fiber &&
92
77
  "return" in fiber &&
@@ -94,21 +79,27 @@ export const isValidFiber = (fiber: unknown): fiber is Fiber =>
94
79
  "sibling" in fiber &&
95
80
  "flags" in fiber;
96
81
 
82
+ const isFiberRoot = (fiberRoot: unknown): fiberRoot is FiberRoot =>
83
+ typeof fiberRoot === "object" &&
84
+ fiberRoot !== null &&
85
+ "current" in fiberRoot &&
86
+ isFiber(fiberRoot.current);
87
+
97
88
  /**
98
89
  * Returns `true` if fiber is a host fiber. Host fibers are DOM nodes in react-dom, `View` in react-native, etc.
99
90
  *
100
91
  * @see https://reactnative.dev/architecture/glossary#host-view-tree-and-host-view
101
92
  */
102
- export const isHostFiber = (fiber: Fiber): boolean => {
93
+ export const isHostFiber = (fiber: Fiber): fiber is HostFiber => {
94
+ const workTags = getReactWorkTagsForFiber(fiber);
103
95
  switch (fiber.tag) {
104
- case HostComponentTag:
105
- // @ts-expect-error: it exists
106
- case HostHoistableTag:
107
- // @ts-expect-error: it exists
108
- case HostSingletonTag:
96
+ case workTags.HostComponent:
97
+ case workTags.HostText:
98
+ case workTags.HostHoistable:
99
+ case workTags.HostSingleton:
109
100
  return true;
110
101
  default:
111
- return typeof fiber.type === "string";
102
+ return false;
112
103
  }
113
104
  };
114
105
 
@@ -118,128 +109,36 @@ export const isHostFiber = (fiber: Fiber): boolean => {
118
109
  * @see https://reactnative.dev/architecture/glossary#react-composite-components
119
110
  */
120
111
  export const isCompositeFiber = (fiber: Fiber): boolean => {
112
+ const workTags = getReactWorkTagsForFiber(fiber);
121
113
  switch (fiber.tag) {
122
- case ClassComponentTag:
123
- case ForwardRefTag:
124
- case FunctionComponentTag:
125
- case MemoComponentTag:
126
- case SimpleMemoComponentTag:
114
+ case workTags.ClassComponent:
115
+ case workTags.ForwardRef:
116
+ case workTags.FunctionComponent:
117
+ case workTags.MemoComponent:
118
+ case workTags.SimpleMemoComponent:
127
119
  return true;
128
120
  default:
129
121
  return false;
130
122
  }
131
123
  };
132
124
 
133
- /**
134
- * Returns `true` if the object is a {@link Fiber}
135
- */
136
- export const isFiber = (maybeFiber: unknown): maybeFiber is Fiber => {
137
- if (!maybeFiber || typeof maybeFiber !== "object") return false;
138
- // this is a fast check. pendingProps will ALWAYS exist in fiber
139
- // `containerInfo` is in FiberRootNode, not FiberNode
140
- return "pendingProps" in maybeFiber && !("containerInfo" in maybeFiber);
141
- };
142
-
143
- /**
144
- * Traverses up or down a {@link Fiber}'s contexts, return `true` to stop and select the current and previous context value.
145
- */
146
- export const traverseContexts = (
147
- fiber: Fiber,
148
- selector: (
149
- nextValue: ContextDependency<unknown> | null | undefined,
150
- prevValue: ContextDependency<unknown> | null | undefined,
151
- ) => boolean | void,
152
- ): boolean => {
153
- try {
154
- const nextDependencies = fiber.dependencies;
155
- const prevDependencies = fiber.alternate?.dependencies;
156
-
157
- if (!nextDependencies || !prevDependencies) return false;
158
- if (
159
- typeof nextDependencies !== "object" ||
160
- !("firstContext" in nextDependencies) ||
161
- typeof prevDependencies !== "object" ||
162
- !("firstContext" in prevDependencies)
163
- ) {
164
- return false;
165
- }
166
- let nextContext: ContextDependency<unknown> | null | undefined = nextDependencies.firstContext;
167
- let prevContext: ContextDependency<unknown> | null | undefined = prevDependencies.firstContext;
168
- while (
169
- (nextContext && typeof nextContext === "object" && "memoizedValue" in nextContext) ||
170
- (prevContext && typeof prevContext === "object" && "memoizedValue" in prevContext)
171
- ) {
172
- if (selector(nextContext, prevContext) === true) return true;
173
-
174
- nextContext = nextContext?.next;
175
- prevContext = prevContext?.next;
176
- }
177
- } catch {}
178
- return false;
179
- };
180
-
181
- /**
182
- * Traverses up or down a {@link Fiber}'s states, return `true` to stop and select the current and previous state value. This stores both state values and effects.
183
- */
184
- export const traverseState = (
185
- fiber: Fiber,
186
- selector: (
187
- nextValue: MemoizedState | null | undefined,
188
- prevValue: MemoizedState | null | undefined,
189
- ) => boolean | void,
190
- ): boolean => {
191
- try {
192
- let nextState: MemoizedState | null | undefined = fiber.memoizedState;
193
- let prevState: MemoizedState | null | undefined = fiber.alternate?.memoizedState;
194
-
195
- while (nextState || prevState) {
196
- if (selector(nextState, prevState) === true) return true;
197
-
198
- nextState = nextState?.next;
199
- prevState = prevState?.next;
200
- }
201
- } catch {}
202
- return false;
203
- };
204
-
205
- /**
206
- * Traverses up or down a {@link Fiber}'s props, return `true` to stop and select the current and previous props value.
207
- */
208
- export const traverseProps = (
209
- fiber: Fiber,
210
- selector: (propName: string, nextValue: unknown, prevValue: unknown) => boolean | void,
211
- ): boolean => {
212
- try {
213
- const nextProps = fiber.memoizedProps;
214
- const prevProps = fiber.alternate?.memoizedProps || {};
215
-
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;
222
- }
223
- } catch {}
224
- return false;
225
- };
226
-
227
125
  /**
228
126
  * Returns `true` if the {@link Fiber} has rendered. Note that this does not mean the fiber has rendered in the current commit, just that it has rendered in the past.
229
127
  */
230
128
  export const didFiberRender = (fiber: Fiber): boolean => {
231
129
  const nextProps = fiber.memoizedProps;
232
130
  const prevProps = fiber.alternate?.memoizedProps || {};
233
- const flags = fiber.flags ?? (fiber as unknown as { effectTag: number }).effectTag ?? 0;
131
+ const flags = fiber.flags ?? fiber.effectTag ?? 0;
132
+ const workTags = getReactWorkTagsForFiber(fiber);
234
133
 
235
134
  switch (fiber.tag) {
236
- case ClassComponentTag:
237
- case ContextConsumerTag:
238
- case ForwardRefTag:
239
- case FunctionComponentTag:
240
- case MemoComponentTag:
241
- case SimpleMemoComponentTag: {
242
- return (flags & PerformedWork) === PerformedWork;
135
+ case workTags.ClassComponent:
136
+ case workTags.ContextConsumer:
137
+ case workTags.ForwardRef:
138
+ case workTags.FunctionComponent:
139
+ case workTags.MemoComponent:
140
+ case workTags.SimpleMemoComponent: {
141
+ return (flags & ReactFiberFlags.PerformedWork) === ReactFiberFlags.PerformedWork;
243
142
  }
244
143
  default:
245
144
  // Host nodes (DOM, root, etc.)
@@ -257,57 +156,18 @@ export const didFiberRender = (fiber: Fiber): boolean => {
257
156
  */
258
157
  export const didFiberCommit = (fiber: Fiber): boolean => {
259
158
  return Boolean(
260
- (fiber.flags & (MutationMask | Cloned)) !== 0 ||
261
- (fiber.subtreeFlags & (MutationMask | Cloned)) !== 0,
159
+ (fiber.flags & (MutationMask | ReactFiberFlags.Cloned)) !== 0 ||
160
+ (fiber.subtreeFlags & (MutationMask | ReactFiberFlags.Cloned)) !== 0,
262
161
  );
263
162
  };
264
163
 
265
- /**
266
- * Returns all host {@link Fiber}s that have committed and rendered.
267
- */
268
- export const getMutatedHostFibers = (fiber: Fiber): Fiber[] => {
269
- const mutations: Fiber[] = [];
270
- const stack: Fiber[] = [fiber];
271
-
272
- while (stack.length) {
273
- const node = stack.pop();
274
- if (!node) continue;
275
-
276
- if (isHostFiber(node) && didFiberCommit(node) && didFiberRender(node)) {
277
- mutations.push(node);
278
- }
279
-
280
- if (node.child) stack.push(node.child);
281
- if (node.sibling) stack.push(node.sibling);
282
- }
283
-
284
- return mutations;
285
- };
286
-
287
- /**
288
- * Returns the stack of {@link Fiber}s from the current fiber to the root fiber.
289
- *
290
- * @example
291
- * ```ts
292
- * [fiber, fiber.return, fiber.return.return, ...]
293
- * ```
294
- */
295
- export const getFiberStack = (fiber: Fiber): Fiber[] => {
296
- const stack: Fiber[] = [];
297
- let currentFiber = fiber;
298
- while (currentFiber.return) {
299
- stack.push(currentFiber);
300
- currentFiber = currentFiber.return as Fiber;
301
- }
302
- return stack;
303
- };
304
-
305
164
  /**
306
165
  * Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
307
166
  */
308
167
  const shouldFilterFiber = (fiber: Fiber): boolean => {
168
+ const workTags = getReactWorkTagsForFiber(fiber);
309
169
  switch (fiber.tag) {
310
- case DehydratedSuspenseComponentTag:
170
+ case workTags.DehydratedSuspenseComponent:
311
171
  // TODO: ideally we would show dehydrated Suspense immediately.
312
172
  // However, it has some special behavior (like disconnecting
313
173
  // an alternate and turning into real Suspense) which breaks DevTools.
@@ -315,13 +175,13 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
315
175
  // https://github.com/bvaughn/react-devtools-experimental/issues/197
316
176
  return true;
317
177
 
318
- case FragmentTag:
319
- case HostTextTag:
320
- case LegacyHiddenComponentTag:
321
- case OffscreenComponentTag:
178
+ case workTags.Fragment:
179
+ case workTags.HostText:
180
+ case workTags.LegacyHiddenComponent:
181
+ case workTags.OffscreenComponent:
322
182
  return true;
323
183
 
324
- case HostRootTag:
184
+ case workTags.HostRoot:
325
185
  // It is never valid to filter the root element.
326
186
  return false;
327
187
 
@@ -331,15 +191,15 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
331
191
 
332
192
  if (typeof symbolOrNumber === "symbol") {
333
193
  return (
334
- symbolOrNumber.description === CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
335
- symbolOrNumber.description === DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
194
+ symbolOrNumber.description === ReactSymbols.CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
195
+ symbolOrNumber.description === ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
336
196
  );
337
197
  }
338
198
 
339
199
  switch (symbolOrNumber) {
340
- case CONCURRENT_MODE_NUMBER:
341
- case CONCURRENT_MODE_SYMBOL_STRING:
342
- case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
200
+ case ReactSymbols.CONCURRENT_MODE_NUMBER:
201
+ case ReactSymbols.CONCURRENT_MODE_SYMBOL_STRING:
202
+ case ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
343
203
  return true;
344
204
 
345
205
  default:
@@ -349,47 +209,6 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
349
209
  }
350
210
  };
351
211
 
352
- /**
353
- * Returns the nearest host {@link Fiber} to the current {@link Fiber}.
354
- */
355
- export const getNearestHostFiber = (fiber: Fiber, ascending = false): Fiber | null => {
356
- let hostFiber = traverseFiber(fiber, isHostFiber, ascending);
357
- if (!hostFiber) {
358
- hostFiber = traverseFiber(fiber, isHostFiber, !ascending);
359
- }
360
- return hostFiber;
361
- };
362
-
363
- /**
364
- * Returns all host {@link Fiber}s in the tree that are associated with the current {@link Fiber}.
365
- */
366
- export const getNearestHostFibers = (fiber: Fiber): Fiber[] => {
367
- const hostFibers: Fiber[] = [];
368
- const stack: Fiber[] = [];
369
-
370
- if (isHostFiber(fiber)) {
371
- hostFibers.push(fiber);
372
- } else if (fiber.child) {
373
- stack.push(fiber.child);
374
- }
375
-
376
- while (stack.length) {
377
- const currentNode = stack.pop();
378
- if (!currentNode) break;
379
- if (isHostFiber(currentNode)) {
380
- hostFibers.push(currentNode);
381
- } else if (currentNode.child) {
382
- stack.push(currentNode.child);
383
- }
384
-
385
- if (currentNode.sibling) {
386
- stack.push(currentNode.sibling);
387
- }
388
- }
389
-
390
- return hostFibers;
391
- };
392
-
393
212
  /**
394
213
  * Traverses up or down a {@link Fiber}, return `true` to stop and select a node.
395
214
  */
@@ -405,144 +224,86 @@ export function traverseFiber(
405
224
  ): Promise<Fiber | null>;
406
225
  export function traverseFiber(
407
226
  fiber: Fiber | null,
408
- selector: (node: Fiber) => boolean | Promise<boolean | void> | void,
227
+ selector: FiberSelector,
228
+ ascending?: boolean,
229
+ ): Fiber | null | Promise<Fiber | null>;
230
+ export function traverseFiber(
231
+ fiber: Fiber | null,
232
+ selector: FiberSelector,
409
233
  ascending = false,
410
234
  ): Fiber | null | Promise<Fiber | null> {
411
235
  if (!fiber) return null;
412
236
 
413
- const firstResult = selector(fiber);
414
- if (firstResult instanceof Promise) {
415
- return (async () => {
416
- if ((await firstResult) === true) return fiber;
417
-
418
- let child = ascending ? fiber.return : fiber.child;
419
- while (child) {
420
- const match = await traverseFiberAsync(
421
- child,
422
- selector as (node: Fiber) => Promise<boolean | void>,
423
- ascending,
424
- );
425
- if (match) return match;
426
- child = ascending ? null : child.sibling;
427
- }
428
- return null;
429
- })();
237
+ const selection = selector(fiber);
238
+ if (isPromiseLike<boolean | void>(selection)) {
239
+ return Promise.resolve(selection).then((didSelectFiber) =>
240
+ didSelectFiber === true ? fiber : traverseFiberChildren(fiber, selector, ascending),
241
+ );
430
242
  }
243
+ if (selection === true) return fiber;
431
244
 
432
- if (firstResult === true) return fiber;
433
-
434
- let child = ascending ? fiber.return : fiber.child;
435
- while (child) {
436
- const match = traverseFiberSync(child, selector as (node: Fiber) => boolean | void, ascending);
437
- if (match) return match;
438
- child = ascending ? null : child.sibling;
439
- }
440
- return null;
245
+ return traverseFiberChildren(fiber, selector, ascending);
441
246
  }
442
247
 
443
- const traverseFiberSync = (
444
- fiber: Fiber | null,
445
- selector: (node: Fiber) => boolean | void,
446
- ascending = false,
447
- ): Fiber | null => {
448
- if (!fiber) return null;
449
- if (selector(fiber) === true) return fiber;
248
+ const isPromiseLike = <Result>(value: unknown): value is PromiseLike<Result> =>
249
+ (typeof value === "object" || typeof value === "function") &&
250
+ value !== null &&
251
+ "then" in value &&
252
+ typeof value.then === "function";
450
253
 
451
- let child = ascending ? fiber.return : fiber.child;
452
- while (child) {
453
- const match = traverseFiberSync(child, selector, ascending);
454
- if (match) return match;
455
-
456
- child = ascending ? null : child.sibling;
457
- }
458
- return null;
254
+ const traverseFiberChildren = (
255
+ fiber: Fiber,
256
+ selector: FiberSelector,
257
+ ascending: boolean,
258
+ ): Fiber | null | Promise<Fiber | null> => {
259
+ const firstChild = ascending ? fiber.return : fiber.child;
260
+ return traverseFiberSiblings(firstChild, selector, ascending);
459
261
  };
460
262
 
461
- const traverseFiberAsync = async (
263
+ const traverseFiberSiblings = (
462
264
  fiber: Fiber | null,
463
- selector: (node: Fiber) => Promise<boolean | void>,
464
- ascending = false,
465
- ): Promise<Fiber | null> => {
265
+ selector: FiberSelector,
266
+ ascending: boolean,
267
+ ): Fiber | null | Promise<Fiber | null> => {
466
268
  if (!fiber) return null;
467
- if ((await selector(fiber)) === true) return fiber;
468
269
 
469
- let child = ascending ? fiber.return : fiber.child;
470
- while (child) {
471
- const match = await traverseFiberAsync(child, selector, ascending);
472
- if (match) return match;
473
-
474
- child = ascending ? null : child.sibling;
475
- }
476
- return null;
477
- };
478
-
479
- /**
480
- * Returns the timings of the {@link Fiber}.
481
- *
482
- * @example
483
- * ```ts
484
- * const { selfTime, totalTime } = getTimings(fiber);
485
- * console.log(selfTime, totalTime);
486
- * ```
487
- */
488
- export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime: number } => {
489
- const totalTime = fiber?.actualDuration ?? 0;
490
- let selfTime = totalTime;
491
- // TODO: calculate a DOM time, which is just host component summed up
492
- let child = fiber?.child ?? null;
493
- while (totalTime > 0 && child != null) {
494
- selfTime -= child.actualDuration ?? 0;
495
- child = child.sibling;
270
+ const nextSibling = ascending ? null : fiber.sibling;
271
+ const match = traverseFiber(fiber, selector, ascending);
272
+ if (isPromiseLike<Fiber | null>(match)) {
273
+ return Promise.resolve(match).then(
274
+ (resolvedMatch) => resolvedMatch ?? traverseFiberSiblings(nextSibling, selector, ascending),
275
+ );
496
276
  }
497
- return { selfTime, totalTime };
277
+ return match ?? traverseFiberSiblings(nextSibling, selector, ascending);
498
278
  };
499
279
 
500
280
  /**
501
281
  * Returns `true` if the {@link Fiber} uses React Compiler's memo cache.
502
282
  */
503
283
  export const hasMemoCache = (fiber: Fiber): boolean => {
504
- return Boolean((fiber.updateQueue as unknown as { memoCache: unknown })?.memoCache);
284
+ return Boolean(fiber.updateQueue?.memoCache);
505
285
  };
506
286
 
507
- type FiberType =
508
- | React.ComponentType<unknown>
509
- | React.ForwardRefExoticComponent<unknown>
510
- | React.MemoExoticComponent<React.ComponentType<unknown>>;
511
-
512
287
  /**
513
288
  * Returns the type (e.g. component definition) of the {@link Fiber}
514
289
  */
515
290
  export const getType = (type: unknown): null | React.ComponentType<unknown> => {
516
- const currentType = type as FiberType;
517
- if (typeof currentType === "function") {
518
- return currentType;
519
- }
520
- if (typeof currentType === "object" && currentType) {
521
- // memo / forwardRef case
522
- return getType(
523
- (currentType as React.MemoExoticComponent<React.ComponentType<unknown>>).type ||
524
- (currentType as { render: React.ComponentType<unknown> }).render,
525
- );
526
- }
527
- return null;
291
+ if (isComponentType(type)) return type;
292
+ if (typeof type !== "object" || type === null) return null;
293
+ return getType(Reflect.get(type, "type") ?? Reflect.get(type, "render"));
528
294
  };
529
295
 
530
296
  /**
531
297
  * Returns the display name of the {@link Fiber} type.
532
298
  */
533
299
  export const getDisplayName = (type: unknown): null | string => {
534
- const currentType = type as FiberType;
535
- if (typeof currentType === "string") {
536
- return currentType;
537
- }
538
- if (typeof currentType !== "function" && !(typeof currentType === "object" && currentType)) {
539
- return null;
540
- }
541
- const name = currentType.displayName || currentType.name || null;
300
+ if (typeof type === "string") return type;
301
+ if ((typeof type !== "function" && typeof type !== "object") || type === null) return null;
302
+ const name = getTypeName(type);
542
303
  if (name) return name;
543
- const unwrappedType = getType(currentType);
304
+ const unwrappedType = getType(type);
544
305
  if (!unwrappedType) return null;
545
- return unwrappedType.displayName || unwrappedType.name || null;
306
+ return getTypeName(unwrappedType);
546
307
  };
547
308
 
548
309
  /**
@@ -550,7 +311,10 @@ export const getDisplayName = (type: unknown): null | string => {
550
311
  */
551
312
  export const detectReactBuildType = (renderer: ReactRenderer): "development" | "production" => {
552
313
  try {
553
- if (typeof renderer.version === "string" && renderer.bundleType > 0) {
314
+ if (
315
+ typeof renderer.version === "string" &&
316
+ renderer.bundleType === ReactBuildType.Development
317
+ ) {
554
318
  return "development";
555
319
  }
556
320
  } catch {}
@@ -562,14 +326,11 @@ export const detectReactBuildType = (renderer: ReactRenderer): "development" | "
562
326
  */
563
327
  export const isInstrumentationActive = (): boolean => {
564
328
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
565
- return (
566
- Boolean(rdtHook?._instrumentationIsActive) ||
567
- isRealReactDevtools(rdtHook) ||
568
- isReactRefresh(rdtHook)
569
- );
329
+ return Boolean(rdtHook?._instrumentationIsActive) || isRealReactDevtools(rdtHook);
570
330
  };
571
331
 
572
332
  export const _fiberRoots = new Set<FiberRoot>();
333
+ const rootRendererIds = new WeakMap<FiberRoot, number>();
573
334
 
574
335
  /**
575
336
  * Returns the latest fiber (since it may be double-buffered).
@@ -577,42 +338,71 @@ export const _fiberRoots = new Set<FiberRoot>();
577
338
  export const getLatestFiber = (fiber: Fiber): Fiber => {
578
339
  const alternate = fiber.alternate;
579
340
  if (!alternate) return fiber;
341
+
342
+ let rootFiber = fiber;
343
+ while (rootFiber.return) {
344
+ rootFiber = rootFiber.return;
345
+ }
346
+ if (isFiberRoot(rootFiber.stateNode)) {
347
+ const latestFiber = traverseFiber(rootFiber.stateNode.current, (innerFiber) => {
348
+ if (innerFiber === fiber || innerFiber === alternate) return true;
349
+ });
350
+ if (latestFiber) return latestFiber;
351
+ }
352
+
580
353
  if (alternate.actualStartTime && fiber.actualStartTime) {
581
354
  return alternate.actualStartTime > fiber.actualStartTime ? alternate : fiber;
582
355
  }
583
356
  for (const root of _fiberRoots) {
584
357
  const latestFiber = traverseFiber(root.current, (innerFiber) => {
585
- if (innerFiber === fiber) return true;
358
+ if (innerFiber === fiber || innerFiber === alternate) return true;
586
359
  });
587
360
  if (latestFiber) return latestFiber;
588
361
  }
589
362
  return fiber;
590
363
  };
591
364
 
592
- export type RenderHandler = <S>(fiber: Fiber, phase: RenderPhase, state?: S) => unknown;
365
+ /**
366
+ * Returns the renderer that owns the {@link Fiber}.
367
+ */
368
+ export const getRenderer = (fiber: Fiber): ReactRenderer | null => {
369
+ if (!hasRDTHook()) return null;
370
+
371
+ let rootFiber = fiber;
372
+ while (rootFiber.return) {
373
+ rootFiber = rootFiber.return;
374
+ }
375
+
376
+ const fiberRoot = rootFiber.stateNode;
377
+ if (!isFiberRoot(fiberRoot)) return null;
378
+
379
+ const rendererID = rootRendererIds.get(fiberRoot);
380
+ if (rendererID === undefined) return null;
381
+ return getRDTHook().renderers.get(rendererID) ?? null;
382
+ };
593
383
 
594
384
  export type RenderPhase = "mount" | "unmount" | "update";
595
385
 
596
- let fiberId = 0;
386
+ let nextFiberId = 0;
597
387
  const fiberIdMap = new WeakMap<Fiber, number>();
598
388
 
599
- export const setFiberId = (fiber: Fiber, id: number = fiberId++): void => {
600
- fiberIdMap.set(fiber, id);
389
+ export const setFiberId = (fiber: Fiber, fiberId: number = nextFiberId++): void => {
390
+ fiberIdMap.set(fiber, fiberId);
391
+ if (Number.isSafeInteger(fiberId) && fiberId >= nextFiberId) {
392
+ nextFiberId = fiberId + 1;
393
+ }
601
394
  };
602
395
 
603
- // react fibers are double buffered, so the alternate fiber may
604
- // be switched to the current fiber and vice versa.
605
- // fiber === fiber.alternate.alternate
606
396
  export const getFiberId = (fiber: Fiber): number => {
607
- let id = fiberIdMap.get(fiber);
608
- if (id === undefined && fiber.alternate) {
609
- id = fiberIdMap.get(fiber.alternate);
397
+ let currentFiberId = fiberIdMap.get(fiber);
398
+ if (currentFiberId === undefined && fiber.alternate) {
399
+ currentFiberId = fiberIdMap.get(fiber.alternate);
610
400
  }
611
- if (id === undefined) {
612
- id = fiberId++;
613
- setFiberId(fiber, id);
401
+ if (currentFiberId === undefined) {
402
+ currentFiberId = nextFiberId++;
403
+ setFiberId(fiber, currentFiberId);
614
404
  }
615
- return id;
405
+ return currentFiberId;
616
406
  };
617
407
 
618
408
  const mountFiberRecursively = (
@@ -622,16 +412,14 @@ const mountFiberRecursively = (
622
412
  ): void => {
623
413
  let fiber: Fiber | null = firstChild;
624
414
 
625
- while (fiber != null) {
626
- if (!fiberIdMap.has(fiber)) {
627
- getFiberId(fiber);
628
- }
415
+ while (fiber !== null) {
416
+ getFiberId(fiber);
629
417
  const shouldIncludeInTree = !shouldFilterFiber(fiber);
630
418
  if (shouldIncludeInTree && didFiberRender(fiber)) {
631
419
  onRender(fiber, "mount");
632
420
  }
633
421
 
634
- if (fiber.tag === SuspenseComponentTag) {
422
+ if (fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent) {
635
423
  const isTimedOut = fiber.memoizedState !== null;
636
424
  if (isTimedOut) {
637
425
  // Special case: if Suspense mounts in a timed-out state,
@@ -646,18 +434,12 @@ const mountFiberRecursively = (
646
434
  }
647
435
  }
648
436
  } else {
649
- let primaryChild: Fiber | null = null;
650
- const areSuspenseChildrenConditionallyWrapped = (OffscreenComponentTag as number) === -1;
651
- if (areSuspenseChildrenConditionallyWrapped) {
652
- primaryChild = fiber.child;
653
- } else if (fiber.child !== null) {
654
- primaryChild = fiber.child.child;
655
- }
437
+ const primaryChild = fiber.child?.child ?? null;
656
438
  if (primaryChild !== null) {
657
439
  mountFiberRecursively(onRender, primaryChild, false);
658
440
  }
659
441
  }
660
- } else if (fiber.child != null) {
442
+ } else if (fiber.child !== null) {
661
443
  mountFiberRecursively(onRender, fiber.child, true);
662
444
  }
663
445
  fiber = traverseSiblings ? fiber.sibling : null;
@@ -667,18 +449,13 @@ const mountFiberRecursively = (
667
449
  const updateFiberRecursively = (
668
450
  onRender: RenderHandler,
669
451
  nextFiber: Fiber,
670
- prevFiber: Fiber,
671
- parentFiber: Fiber | null,
452
+ prevFiber: Fiber | null,
672
453
  ): void => {
673
- if (!fiberIdMap.has(nextFiber)) {
674
- getFiberId(nextFiber);
675
- }
454
+ getFiberId(nextFiber);
676
455
  if (!prevFiber) return;
677
- if (!fiberIdMap.has(prevFiber)) {
678
- getFiberId(prevFiber);
679
- }
456
+ getFiberId(prevFiber);
680
457
 
681
- const isSuspense = nextFiber.tag === SuspenseComponentTag;
458
+ const isSuspense = nextFiber.tag === getReactWorkTagsForFiber(nextFiber).SuspenseComponent;
682
459
 
683
460
  const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
684
461
  if (shouldIncludeInTree && didFiberRender(nextFiber)) {
@@ -707,7 +484,7 @@ const updateFiberRecursively = (
707
484
  const prevFallbackChildSet = prevFiber.child?.sibling ?? null;
708
485
 
709
486
  if (nextFallbackChildSet !== null && prevFallbackChildSet !== null) {
710
- updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet, nextFiber);
487
+ updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet);
711
488
  }
712
489
  } else if (prevDidTimeout && !nextDidTimeOut) {
713
490
  // Fallback -> Primary:
@@ -746,14 +523,7 @@ const updateFiberRecursively = (
746
523
  // Schedule updates and mounts depending on whether alternates exist.
747
524
  // We don't track deletions here because they are reported separately.
748
525
  if (nextChild.alternate) {
749
- const prevChild = nextChild.alternate;
750
-
751
- updateFiberRecursively(
752
- onRender,
753
- nextChild,
754
- prevChild,
755
- shouldIncludeInTree ? nextFiber : parentFiber,
756
- );
526
+ updateFiberRecursively(onRender, nextChild, nextChild.alternate);
757
527
  } else {
758
528
  mountFiberRecursively(onRender, nextChild, false);
759
529
  }
@@ -765,7 +535,7 @@ const updateFiberRecursively = (
765
535
  };
766
536
 
767
537
  const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
768
- const isRoot = fiber.tag === HostRootTag;
538
+ const isRoot = fiber.tag === getReactWorkTagsForFiber(fiber).HostRoot;
769
539
 
770
540
  if (isRoot || !shouldFilterFiber(fiber)) {
771
541
  onRender(fiber, "unmount");
@@ -774,7 +544,8 @@ const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
774
544
 
775
545
  const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => {
776
546
  // We might meet a nested Suspense on our way.
777
- const isTimedOutSuspense = fiber.tag === SuspenseComponentTag && fiber.memoizedState !== null;
547
+ const isTimedOutSuspense =
548
+ fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent && fiber.memoizedState !== null;
778
549
  let child = fiber.child;
779
550
 
780
551
  if (isTimedOutSuspense) {
@@ -798,11 +569,9 @@ const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber):
798
569
  }
799
570
  };
800
571
 
801
- let commitId = 0;
802
572
  const rootInstanceMap = new WeakMap<
803
- FiberRoot,
573
+ Fiber | FiberRoot,
804
574
  {
805
- id: number;
806
575
  prevFiber: Fiber | null;
807
576
  }
808
577
  >();
@@ -814,37 +583,39 @@ const rootInstanceMap = new WeakMap<
814
583
  * console.log(phase)
815
584
  * })
816
585
  */
817
- export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler): void => {
586
+ export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: RenderHandler): void => {
818
587
  const fiber = "current" in root ? root.current : root;
819
588
 
820
589
  let rootInstance = rootInstanceMap.get(root);
821
590
 
822
591
  if (!rootInstance) {
823
- rootInstance = { id: commitId++, prevFiber: null };
592
+ rootInstance = { prevFiber: null };
824
593
  rootInstanceMap.set(root, rootInstance);
825
594
  }
826
595
 
827
596
  const { prevFiber } = rootInstance;
828
- // if fiberRoot don't have current instance, means it's been unmounted
829
597
  if (!fiber) {
830
- unmountFiber(onRender, fiber);
598
+ if (prevFiber) {
599
+ unmountFiber(onRender, prevFiber);
600
+ }
831
601
  } else if (prevFiber !== null) {
832
602
  const wasMounted =
833
- prevFiber &&
834
- prevFiber.memoizedState != null &&
835
- prevFiber.memoizedState.element != null &&
603
+ prevFiber.memoizedState !== null &&
604
+ prevFiber.memoizedState.element !== null &&
605
+ prevFiber.memoizedState.element !== undefined &&
836
606
  // A dehydrated root is not considered mounted
837
607
  prevFiber.memoizedState.isDehydrated !== true;
838
608
  const isMounted =
839
- fiber.memoizedState != null &&
840
- fiber.memoizedState.element != null &&
609
+ fiber.memoizedState !== null &&
610
+ fiber.memoizedState.element !== null &&
611
+ fiber.memoizedState.element !== undefined &&
841
612
  // A dehydrated root is not considered mounted
842
613
  fiber.memoizedState.isDehydrated !== true;
843
614
 
844
615
  if (!wasMounted && isMounted) {
845
616
  mountFiberRecursively(onRender, fiber, false);
846
617
  } else if (wasMounted && isMounted) {
847
- updateFiberRecursively(onRender, fiber, fiber.alternate, null);
618
+ updateFiberRecursively(onRender, fiber, fiber.alternate);
848
619
  } else if (wasMounted && !isMounted) {
849
620
  unmountFiber(onRender, fiber);
850
621
  }
@@ -855,191 +626,25 @@ export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler)
855
626
  rootInstance.prevFiber = fiber;
856
627
  };
857
628
 
858
- const overrideRenderers = new Set<ReactRenderer>();
859
- let areOverrideRenderersWired = false;
860
-
861
- const wireOverrideRenderers = (): void => {
862
- if (areOverrideRenderersWired) return;
863
- if (!hasRDTHook()) return;
864
- const rdtHook = getRDTHook();
865
- if (!rdtHook?.renderers) return;
866
- areOverrideRenderersWired = true;
867
-
868
- ensureHookDispatchesToListeners(rdtHook);
869
- for (const renderer of rdtHook.renderers.values()) {
870
- overrideRenderers.add(renderer);
871
- }
872
- onRendererInject((renderer) => {
873
- overrideRenderers.add(renderer);
874
- });
875
- };
876
-
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
- };
887
-
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
- };
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
- };
910
-
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
- };
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;
933
- }
934
- currentFiber = currentFiber.return;
935
- }
936
- return null;
937
- };
938
-
939
- const isPOJO = (maybePOJO: unknown): maybePOJO is Record<string, unknown> => {
940
- return (
941
- Object.prototype.toString.call(maybePOJO) === "[object Object]" &&
942
- (Object.getPrototypeOf(maybePOJO) === Object.prototype ||
943
- Object.getPrototypeOf(maybePOJO) === null)
944
- );
945
- };
946
-
947
- const buildPathsFromValue = (
948
- maybePOJO: Record<string, unknown>,
949
- basePath: string[] = [],
950
- ): Array<{ path: string[]; value: unknown }> => {
951
- if (!isPOJO(maybePOJO)) {
952
- return [{ path: basePath, value: maybePOJO }];
953
- }
954
-
955
- const paths: Array<{ path: string[]; value: unknown }> = [];
956
-
957
- for (const key in maybePOJO) {
958
- const value = maybePOJO[key];
959
- const path = basePath.concat(key);
960
-
961
- if (isPOJO(value)) {
962
- paths.push(...buildPathsFromValue(value, path));
963
- } else {
964
- paths.push({ path, value });
965
- }
966
- }
967
-
968
- return paths;
969
- };
970
-
971
- const buildValueWrites = (partialValue: unknown): Array<{ path: string[]; value: unknown }> =>
972
- isPOJO(partialValue) ? buildPathsFromValue(partialValue) : [{ path: [], value: partialValue }];
973
-
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);
978
- }
979
- };
980
-
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
- }
995
- }
996
- return;
997
- }
998
-
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
- };
1009
-
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);
1018
- }
1019
- }
1020
- };
1021
-
1022
629
  export interface InstrumentationOptions {
1023
630
  name?: string;
1024
631
  onActive?: () => unknown;
1025
- onCommitFiberRoot?: (rendererID: number, root: FiberRoot, priority: number | void) => unknown;
632
+ onCommitFiberRoot?: (
633
+ rendererID: number,
634
+ root: FiberRoot,
635
+ priority: number | void,
636
+ didError?: boolean,
637
+ ) => unknown;
1026
638
  onCommitFiberUnmount?: (rendererID: number, fiber: Fiber) => unknown;
1027
639
  onPostCommitFiberRoot?: (rendererID: number, root: FiberRoot) => unknown;
1028
640
  onScheduleFiberRoot?: (rendererID: number, root: FiberRoot, children: React.ReactNode) => unknown;
1029
641
  }
1030
642
 
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
- >();
643
+ interface InstrumentationSubscription {
644
+ options: InstrumentationOptions;
645
+ }
646
+
647
+ const instrumentationSubscriptions = new Set<InstrumentationSubscription>();
1043
648
 
1044
649
  interface HookDispatchers {
1045
650
  onCommitFiberRoot: ReactDevToolsGlobalHook["onCommitFiberRoot"];
@@ -1049,15 +654,14 @@ interface HookDispatchers {
1049
654
  }
1050
655
 
1051
656
  const hookDispatchers = new WeakMap<ReactDevToolsGlobalHook, Partial<HookDispatchers>>();
1052
-
1053
- const rootRendererIds = new WeakMap<FiberRoot, number>();
657
+ let didSubscribeToHookReplacements = false;
1054
658
 
1055
659
  // each hook event is dispatched from a single re-installable wrapper. If
1056
660
  // something overwrites the hook method (devtools, direct assignment), the
1057
661
  // next instrument() call installs a fresh wrapper over it; a superseded
1058
662
  // wrapper still forwards the previous chain but skips the listeners so
1059
663
  // they never fire twice.
1060
- const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
664
+ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
1061
665
  const dispatchers = hookDispatchers.get(rdtHook) ?? {};
1062
666
  hookDispatchers.set(rdtHook, dispatchers);
1063
667
 
@@ -1070,13 +674,29 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1070
674
  rendererID,
1071
675
  root,
1072
676
  priority,
677
+ didError,
1073
678
  ) => {
1074
- prevOnCommitFiberRoot?.(rendererID, root, priority);
679
+ if (prevOnCommitFiberRoot) {
680
+ prevOnCommitFiberRoot.call(rdtHook, rendererID, root, priority, didError);
681
+ }
1075
682
  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);
683
+ setReactWorkTagsForFiber(root.current, rdtHook.renderers.get(rendererID));
684
+ const rootMemoizedState = root.current.memoizedState;
685
+ const isUnmounting =
686
+ rootMemoizedState === null ||
687
+ rootMemoizedState.element === null ||
688
+ rootMemoizedState.element === undefined;
689
+ if (isUnmounting) {
690
+ _fiberRoots.delete(root);
691
+ rootRendererIds.delete(root);
692
+ } else {
693
+ _fiberRoots.add(root);
694
+ rootRendererIds.set(root, rendererID);
695
+ }
696
+ for (const { options } of instrumentationSubscriptions) {
697
+ if (options.onCommitFiberRoot) {
698
+ options.onCommitFiberRoot(rendererID, root, priority, didError);
699
+ }
1080
700
  }
1081
701
  };
1082
702
  dispatchers.onCommitFiberRoot = dispatchCommitFiberRoot;
@@ -1092,12 +712,17 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1092
712
  rendererID,
1093
713
  fiber,
1094
714
  ) => {
1095
- prevOnCommitFiberUnmount?.(rendererID, fiber);
715
+ setReactWorkTagsForFiber(fiber, rdtHook.renderers.get(rendererID));
716
+ if (prevOnCommitFiberUnmount) {
717
+ prevOnCommitFiberUnmount.call(rdtHook, rendererID, fiber);
718
+ }
1096
719
  if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
1097
720
  return;
1098
721
  }
1099
- for (const listener of commitFiberUnmountListeners) {
1100
- listener(rendererID, fiber);
722
+ for (const { options } of instrumentationSubscriptions) {
723
+ if (options.onCommitFiberUnmount) {
724
+ options.onCommitFiberUnmount(rendererID, fiber);
725
+ }
1101
726
  }
1102
727
  };
1103
728
  dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
@@ -1113,12 +738,16 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1113
738
  rendererID,
1114
739
  root,
1115
740
  ) => {
1116
- prevOnPostCommitFiberRoot?.(rendererID, root);
741
+ if (prevOnPostCommitFiberRoot) {
742
+ prevOnPostCommitFiberRoot.call(rdtHook, rendererID, root);
743
+ }
1117
744
  if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
1118
745
  return;
1119
746
  }
1120
- for (const listener of postCommitFiberRootListeners) {
1121
- listener(rendererID, root);
747
+ for (const { options } of instrumentationSubscriptions) {
748
+ if (options.onPostCommitFiberRoot) {
749
+ options.onPostCommitFiberRoot(rendererID, root);
750
+ }
1122
751
  }
1123
752
  };
1124
753
  dispatchers.onPostCommitFiberRoot = dispatchPostCommitFiberRoot;
@@ -1135,10 +764,14 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1135
764
  root,
1136
765
  children,
1137
766
  ) => {
1138
- prevOnScheduleFiberRoot?.(rendererID, root, children);
767
+ if (prevOnScheduleFiberRoot) {
768
+ prevOnScheduleFiberRoot.call(rdtHook, rendererID, root, children);
769
+ }
1139
770
  if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
1140
- for (const listener of scheduleFiberRootListeners) {
1141
- listener(rendererID, root, children);
771
+ for (const { options } of instrumentationSubscriptions) {
772
+ if (options.onScheduleFiberRoot) {
773
+ options.onScheduleFiberRoot(rendererID, root, children);
774
+ }
1142
775
  }
1143
776
  };
1144
777
  dispatchers.onScheduleFiberRoot = dispatchScheduleFiberRoot;
@@ -1146,13 +779,24 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1146
779
  }
1147
780
  };
1148
781
 
782
+ const wireHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
783
+ if (!didSubscribeToHookReplacements) {
784
+ onRDTHookReplace(setHookEventDispatchers);
785
+ didSubscribeToHookReplacements = true;
786
+ }
787
+ setHookEventDispatchers(rdtHook);
788
+ };
789
+
790
+ if (hasRDTHook()) {
791
+ wireHookEventDispatchers(getRDTHook());
792
+ }
793
+
1149
794
  /**
1150
795
  * Instruments the DevTools hook. Each hook event is patched once and
1151
796
  * dispatches to a set of listeners, so multiple `instrument` calls compose
1152
797
  * without stacking patches. Returns an unsubscribe function that removes
1153
798
  * exactly the handlers this call registered.
1154
- * The returned function is also a `Disposable`, so it composes with other
1155
- * bippy subscriptions through `using`.
799
+ * The unsubscribe function can also be disposed through `using`.
1156
800
  * @example
1157
801
  * const unsubscribe = instrument({
1158
802
  * onActive() {
@@ -1166,35 +810,19 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1166
810
  */
1167
811
  export const instrument = (options: InstrumentationOptions): Unsubscribe => {
1168
812
  const rdtHook = getRDTHook(options.onActive);
1169
-
1170
813
  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
1171
814
 
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);
815
+ wireHookEventDispatchers(rdtHook);
816
+ const subscription: InstrumentationSubscription = { options };
817
+ instrumentationSubscriptions.add(subscription);
818
+
819
+ return createUnsubscribe(() => {
820
+ if (options.onActive) _onActiveListeners.delete(options.onActive);
821
+ instrumentationSubscriptions.delete(subscription);
1192
822
  });
1193
823
  };
1194
824
 
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.
825
+ // React uses per-renderer suffixes for host-instance Fiber keys, so discovered keys are cached.
1198
826
  const knownFiberPropertyKeys = new Set<string>();
1199
827
 
1200
828
  const isFiberPropertyKey = (key: string): boolean =>
@@ -1202,7 +830,38 @@ const isFiberPropertyKey = (key: string): boolean =>
1202
830
  key.startsWith("__reactInternalInstance$") ||
1203
831
  key.startsWith("__reactFiber");
1204
832
 
1205
- export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
833
+ const getLegacyRootFiber = (hostInstance: object): Fiber | null => {
834
+ const reactRootContainer = Reflect.get(hostInstance, "_reactRootContainer");
835
+ if (typeof reactRootContainer !== "object" || reactRootContainer === null) return null;
836
+ const internalRoot = Reflect.get(reactRootContainer, "_internalRoot");
837
+ if (typeof internalRoot !== "object" || internalRoot === null) return null;
838
+ const current = Reflect.get(internalRoot, "current");
839
+ if (typeof current !== "object" || current === null) return null;
840
+ const child = Reflect.get(current, "child");
841
+ return isFiber(child) ? child : null;
842
+ };
843
+
844
+ const getInternalInstanceHandle = (hostInstance: object): Fiber | null => {
845
+ const internalInstanceHandle =
846
+ Reflect.get(hostInstance, "__internalInstanceHandle") ??
847
+ Reflect.get(hostInstance, "_internalInstanceHandle");
848
+ return isFiber(internalInstanceHandle) ? internalInstanceHandle : null;
849
+ };
850
+
851
+ const getPublicHostInstance = (stateNode: unknown): unknown => {
852
+ if (typeof stateNode !== "object" || stateNode === null) return stateNode;
853
+ const canonical = Reflect.get(stateNode, "canonical");
854
+ if (typeof canonical === "object" && canonical !== null) {
855
+ const publicInstance = Reflect.get(canonical, "publicInstance");
856
+ if (typeof publicInstance === "object" && publicInstance !== null) {
857
+ return publicInstance;
858
+ }
859
+ }
860
+ const nativeTag = Reflect.get(stateNode, "_nativeTag");
861
+ return typeof nativeTag === "number" ? nativeTag : stateNode;
862
+ };
863
+
864
+ export const getFiber = <T>(hostInstance: T): Fiber | null => {
1206
865
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1207
866
  if (rdtHook?.renderers) {
1208
867
  for (const renderer of rdtHook.renderers.values()) {
@@ -1213,42 +872,38 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1213
872
  }
1214
873
  }
1215
874
 
1216
- if (typeof hostInstance === "object" && hostInstance != null) {
1217
- if ("_reactRootContainer" in hostInstance) {
1218
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1219
- return (hostInstance._reactRootContainer as any)?._internalRoot?.current?.child;
1220
- }
875
+ if (typeof hostInstance === "object" && hostInstance !== null) {
876
+ const legacyRootFiber = getLegacyRootFiber(hostInstance);
877
+ if (legacyRootFiber) return legacyRootFiber;
1221
878
 
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;
1232
- }
879
+ const internalInstanceHandle = getInternalInstanceHandle(hostInstance);
880
+ if (internalInstanceHandle) return internalInstanceHandle;
1233
881
 
1234
- const hostInstanceRecord = hostInstance as Record<string, unknown>;
1235
882
  for (const knownKey of knownFiberPropertyKeys) {
1236
- const fiber = hostInstanceRecord[knownKey];
1237
- if (fiber) return fiber as Fiber;
883
+ const fiber = Reflect.get(hostInstance, knownKey);
884
+ if (isFiber(fiber)) return fiber;
1238
885
  }
1239
886
 
1240
- for (const key of Object.keys(hostInstanceRecord)) {
887
+ for (const key of Object.keys(hostInstance)) {
1241
888
  if (isFiberPropertyKey(key)) {
1242
889
  knownFiberPropertyKeys.add(key);
1243
- return (hostInstanceRecord[key] || null) as Fiber | null;
890
+ const fiber = Reflect.get(hostInstance, key);
891
+ if (isFiber(fiber)) return fiber;
1244
892
  }
1245
893
  }
894
+ }
1246
895
 
896
+ if (
897
+ hostInstance !== null &&
898
+ hostInstance !== undefined &&
899
+ (typeof hostInstance === "object" || typeof hostInstance === "number")
900
+ ) {
1247
901
  for (const fiberRoot of _fiberRoots) {
1248
- if (getRootRenderer(fiberRoot.current)?.findFiberByHostInstance) continue;
1249
902
  const fiber = traverseFiber(
1250
903
  fiberRoot.current,
1251
- (candidateFiber) => candidateFiber.stateNode === hostInstance,
904
+ (candidateFiber) =>
905
+ isHostFiber(candidateFiber) &&
906
+ getPublicHostInstance(candidateFiber.stateNode) === hostInstance,
1252
907
  );
1253
908
  if (fiber) return fiber;
1254
909
  }
@@ -1256,6 +911,19 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1256
911
  return null;
1257
912
  };
1258
913
 
1259
- export * from "./rdt-hook.js";
1260
- export * from "./unsubscribe.js";
1261
- export type * from "./types.js";
914
+ export const getFiberFromHostInstance = getFiber;
915
+
916
+ export {
917
+ BIPPY_INSTRUMENTATION_STRING,
918
+ _onActiveListeners,
919
+ _renderers,
920
+ getRDTHook,
921
+ hasRDTHook,
922
+ installRDTHook,
923
+ isRealReactDevtools,
924
+ onRendererInject,
925
+ patchRDTHook,
926
+ version,
927
+ } from "./rdt-hook.js";
928
+ export type { Unsubscribe } from "./rdt-hook.js";
929
+ export * from "./react-internals/index.js";