bippy 0.6.0 → 0.6.1-dev.03b829a

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 (66) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +126 -136
  3. package/dist/core.cjs +1 -1
  4. package/dist/core.d.cts +45 -60
  5. package/dist/core.d.ts +45 -60
  6. package/dist/core.js +1 -1
  7. package/dist/core2.cjs +9 -0
  8. package/dist/core2.d.cts +11 -3
  9. package/dist/core2.d.ts +11 -3
  10. package/dist/core2.js +9 -0
  11. package/dist/errors.d.cts +447 -0
  12. package/dist/errors.d.ts +447 -0
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +11 -3
  15. package/dist/index.d.ts +11 -3
  16. package/dist/index.iife.js +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/install-hook-only.cjs +1 -1
  19. package/dist/install-hook-only.d.cts +9 -1
  20. package/dist/install-hook-only.d.ts +9 -1
  21. package/dist/install-hook-only.iife.js +1 -1
  22. package/dist/install-hook-only.js +1 -1
  23. package/dist/rdt-hook.cjs +1 -1
  24. package/dist/rdt-hook.js +1 -1
  25. package/dist/source.cjs +14 -4
  26. package/dist/source.d.cts +27 -9
  27. package/dist/source.d.ts +27 -9
  28. package/dist/source.js +14 -4
  29. package/package.json +32 -22
  30. package/src/core.ts +368 -314
  31. package/src/errors.ts +34 -0
  32. package/src/install-hook-only.ts +2 -2
  33. package/src/rdt-hook.ts +150 -153
  34. package/src/react-internals/generated/react-work-tags.ts +318 -0
  35. package/src/react-internals/index.ts +67 -0
  36. package/src/react-internals/semver.ts +78 -0
  37. package/src/react-internals/types.ts +269 -0
  38. package/src/source/constants.ts +1 -1
  39. package/src/source/error-stack.ts +11 -0
  40. package/src/source/get-display-name-from-source.ts +1 -1
  41. package/src/source/get-source.ts +4 -3
  42. package/src/source/index.ts +9 -1
  43. package/src/source/inspect-hooks.ts +221 -169
  44. package/src/source/owner-stack.ts +88 -114
  45. package/src/source/parse-debug-stack.ts +4 -4
  46. package/src/source/parse-hook-names.ts +1 -1
  47. package/src/source/parse-stack.ts +1 -1
  48. package/src/source/symbolication.ts +488 -88
  49. package/dist/get-source.cjs +0 -19
  50. package/dist/get-source.js +0 -19
  51. package/dist/react-refresh.cjs +0 -9
  52. package/dist/react-refresh.d.cts +0 -66
  53. package/dist/react-refresh.d.ts +0 -66
  54. package/dist/react-refresh.js +0 -9
  55. package/dist/unsubscribe.d.cts +0 -298
  56. package/dist/unsubscribe.d.ts +0 -298
  57. package/src/react-refresh/constants.ts +0 -9
  58. package/src/react-refresh/detect-hmr-transport.ts +0 -33
  59. package/src/react-refresh/index.ts +0 -173
  60. package/src/react-refresh/metro-hmr-transport.ts +0 -188
  61. package/src/react-refresh/next-webpack-hmr-transport.ts +0 -72
  62. package/src/react-refresh/normalize-hmr-file-path.ts +0 -24
  63. package/src/react-refresh/types.ts +0 -7
  64. package/src/react-refresh/vite-hmr-transport.ts +0 -116
  65. package/src/types.ts +0 -438
  66. package/src/unsubscribe.ts +0 -17
package/src/core.ts CHANGED
@@ -1,5 +1,4 @@
1
- // [!!!] IMPORTANT: do not import React in this file
2
- // since it will be executed before the react devtools hook is created
1
+ // React must remain a type-only import because this module loads immediately after the DevTools hook.
3
2
 
4
3
  import type * as React from "react";
5
4
 
@@ -7,65 +6,82 @@ import type {
7
6
  ContextDependency,
8
7
  Fiber,
9
8
  FiberRoot,
9
+ HostFiber,
10
10
  MemoizedState,
11
11
  ReactDevToolsGlobalHook,
12
12
  ReactRenderer,
13
- } from "./types.js";
13
+ } from "./react-internals/index.js";
14
14
 
15
15
  import {
16
16
  _onActiveListeners,
17
17
  BIPPY_INSTRUMENTATION_STRING,
18
+ createUnsubscribe,
18
19
  getRDTHook,
19
20
  hasRDTHook,
20
- isReactRefresh,
21
21
  isRealReactDevtools,
22
+ onRDTHookReplace,
22
23
  onRendererInject,
23
24
  } 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;
25
+ import type { Unsubscribe } from "./rdt-hook.js";
26
+ import {
27
+ getReactWorkTagsForFiber,
28
+ MutationMask,
29
+ ReactBuildType,
30
+ ReactFiberFlags,
31
+ ReactSymbols,
32
+ setReactWorkTagsForFiber,
33
+ } from "./react-internals/index.js";
34
+
35
+ export { getReactWorkTags, ReactSymbols } from "./react-internals/index.js";
36
+ export type { ReactWorkTagMap, ReactWorkTagVersion } from "./react-internals/index.js";
37
+ export {
38
+ BippyError,
39
+ BippyHookInspectionError,
40
+ BippyHookRenderError,
41
+ BippySourceMapError,
42
+ BippyUnsupportedHookError,
43
+ } from "./errors.js";
44
+
45
+ const isComponentType = (value: unknown): value is React.ComponentType<unknown> =>
46
+ typeof value === "function";
47
+
48
+ const getPropertyValue = (value: object, key: PropertyKey): unknown => {
49
+ let currentValue: object | null = value;
50
+ while (currentValue) {
51
+ const descriptor = Object.getOwnPropertyDescriptor(currentValue, key);
52
+ if (descriptor) {
53
+ if ("value" in descriptor) return descriptor.value;
54
+ return descriptor.get?.call(value);
55
+ }
56
+ currentValue = Object.getPrototypeOf(currentValue);
57
+ }
58
+ return undefined;
59
+ };
60
+
61
+ const getTypeName = (value: object): string | null => {
62
+ const displayName = getPropertyValue(value, "displayName");
63
+ if (typeof displayName === "string" && displayName) return displayName;
64
+ const name = getPropertyValue(value, "name");
65
+ return typeof name === "string" && name ? name : null;
66
+ };
67
+
68
+ interface FiberSelector {
69
+ (node: Fiber): boolean | Promise<boolean | void> | void;
70
+ }
71
+
72
+ export interface FiberTimings {
73
+ selfTime: number;
74
+ totalTime: number;
75
+ }
76
+
77
+ export interface RenderHandler {
78
+ <State>(fiber: Fiber, phase: RenderPhase, state?: State): unknown;
79
+ }
80
+
81
+ interface ValueWrite {
82
+ path: string[];
83
+ value: unknown;
84
+ }
69
85
 
70
86
  /**
71
87
  * Returns `true` if object is a React Element.
@@ -74,19 +90,17 @@ const MutationMask =
74
90
  */
75
91
  export const isValidElement = (element: unknown): element is React.ReactElement =>
76
92
  typeof element === "object" &&
77
- element != null &&
93
+ element !== null &&
78
94
  "$$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
- );
95
+ (String(element.$$typeof) === ReactSymbols.LEGACY_ELEMENT_SYMBOL_STRING ||
96
+ String(element.$$typeof) === ReactSymbols.ELEMENT_SYMBOL_STRING);
83
97
 
84
98
  /**
85
99
  * Returns `true` if object is a React Fiber.
86
100
  */
87
101
  export const isValidFiber = (fiber: unknown): fiber is Fiber =>
88
102
  typeof fiber === "object" &&
89
- fiber != null &&
103
+ fiber !== null &&
90
104
  "tag" in fiber &&
91
105
  "stateNode" in fiber &&
92
106
  "return" in fiber &&
@@ -94,21 +108,27 @@ export const isValidFiber = (fiber: unknown): fiber is Fiber =>
94
108
  "sibling" in fiber &&
95
109
  "flags" in fiber;
96
110
 
111
+ const isFiberRoot = (fiberRoot: unknown): fiberRoot is FiberRoot =>
112
+ typeof fiberRoot === "object" &&
113
+ fiberRoot !== null &&
114
+ "current" in fiberRoot &&
115
+ isValidFiber(fiberRoot.current);
116
+
97
117
  /**
98
118
  * Returns `true` if fiber is a host fiber. Host fibers are DOM nodes in react-dom, `View` in react-native, etc.
99
119
  *
100
120
  * @see https://reactnative.dev/architecture/glossary#host-view-tree-and-host-view
101
121
  */
102
- export const isHostFiber = (fiber: Fiber): boolean => {
122
+ export const isHostFiber = (fiber: Fiber): fiber is HostFiber => {
123
+ const workTags = getReactWorkTagsForFiber(fiber);
103
124
  switch (fiber.tag) {
104
- case HostComponentTag:
105
- // @ts-expect-error: it exists
106
- case HostHoistableTag:
107
- // @ts-expect-error: it exists
108
- case HostSingletonTag:
125
+ case workTags.HostComponent:
126
+ case workTags.HostText:
127
+ case workTags.HostHoistable:
128
+ case workTags.HostSingleton:
109
129
  return true;
110
130
  default:
111
- return typeof fiber.type === "string";
131
+ return false;
112
132
  }
113
133
  };
114
134
 
@@ -118,12 +138,13 @@ export const isHostFiber = (fiber: Fiber): boolean => {
118
138
  * @see https://reactnative.dev/architecture/glossary#react-composite-components
119
139
  */
120
140
  export const isCompositeFiber = (fiber: Fiber): boolean => {
141
+ const workTags = getReactWorkTagsForFiber(fiber);
121
142
  switch (fiber.tag) {
122
- case ClassComponentTag:
123
- case ForwardRefTag:
124
- case FunctionComponentTag:
125
- case MemoComponentTag:
126
- case SimpleMemoComponentTag:
143
+ case workTags.ClassComponent:
144
+ case workTags.ForwardRef:
145
+ case workTags.FunctionComponent:
146
+ case workTags.MemoComponent:
147
+ case workTags.SimpleMemoComponent:
127
148
  return true;
128
149
  default:
129
150
  return false;
@@ -230,16 +251,17 @@ export const traverseProps = (
230
251
  export const didFiberRender = (fiber: Fiber): boolean => {
231
252
  const nextProps = fiber.memoizedProps;
232
253
  const prevProps = fiber.alternate?.memoizedProps || {};
233
- const flags = fiber.flags ?? (fiber as unknown as { effectTag: number }).effectTag ?? 0;
254
+ const flags = fiber.flags ?? fiber.effectTag ?? 0;
255
+ const workTags = getReactWorkTagsForFiber(fiber);
234
256
 
235
257
  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;
258
+ case workTags.ClassComponent:
259
+ case workTags.ContextConsumer:
260
+ case workTags.ForwardRef:
261
+ case workTags.FunctionComponent:
262
+ case workTags.MemoComponent:
263
+ case workTags.SimpleMemoComponent: {
264
+ return (flags & ReactFiberFlags.PerformedWork) === ReactFiberFlags.PerformedWork;
243
265
  }
244
266
  default:
245
267
  // Host nodes (DOM, root, etc.)
@@ -257,8 +279,8 @@ export const didFiberRender = (fiber: Fiber): boolean => {
257
279
  */
258
280
  export const didFiberCommit = (fiber: Fiber): boolean => {
259
281
  return Boolean(
260
- (fiber.flags & (MutationMask | Cloned)) !== 0 ||
261
- (fiber.subtreeFlags & (MutationMask | Cloned)) !== 0,
282
+ (fiber.flags & (MutationMask | ReactFiberFlags.Cloned)) !== 0 ||
283
+ (fiber.subtreeFlags & (MutationMask | ReactFiberFlags.Cloned)) !== 0,
262
284
  );
263
285
  };
264
286
 
@@ -297,7 +319,8 @@ export const getFiberStack = (fiber: Fiber): Fiber[] => {
297
319
  let currentFiber = fiber;
298
320
  while (currentFiber.return) {
299
321
  stack.push(currentFiber);
300
- currentFiber = currentFiber.return as Fiber;
322
+ const parentFiber = currentFiber.return;
323
+ currentFiber = parentFiber;
301
324
  }
302
325
  return stack;
303
326
  };
@@ -306,8 +329,9 @@ export const getFiberStack = (fiber: Fiber): Fiber[] => {
306
329
  * Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
307
330
  */
308
331
  const shouldFilterFiber = (fiber: Fiber): boolean => {
332
+ const workTags = getReactWorkTagsForFiber(fiber);
309
333
  switch (fiber.tag) {
310
- case DehydratedSuspenseComponentTag:
334
+ case workTags.DehydratedSuspenseComponent:
311
335
  // TODO: ideally we would show dehydrated Suspense immediately.
312
336
  // However, it has some special behavior (like disconnecting
313
337
  // an alternate and turning into real Suspense) which breaks DevTools.
@@ -315,13 +339,13 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
315
339
  // https://github.com/bvaughn/react-devtools-experimental/issues/197
316
340
  return true;
317
341
 
318
- case FragmentTag:
319
- case HostTextTag:
320
- case LegacyHiddenComponentTag:
321
- case OffscreenComponentTag:
342
+ case workTags.Fragment:
343
+ case workTags.HostText:
344
+ case workTags.LegacyHiddenComponent:
345
+ case workTags.OffscreenComponent:
322
346
  return true;
323
347
 
324
- case HostRootTag:
348
+ case workTags.HostRoot:
325
349
  // It is never valid to filter the root element.
326
350
  return false;
327
351
 
@@ -331,15 +355,15 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
331
355
 
332
356
  if (typeof symbolOrNumber === "symbol") {
333
357
  return (
334
- symbolOrNumber.description === CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
335
- symbolOrNumber.description === DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
358
+ symbolOrNumber.description === ReactSymbols.CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
359
+ symbolOrNumber.description === ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
336
360
  );
337
361
  }
338
362
 
339
363
  switch (symbolOrNumber) {
340
- case CONCURRENT_MODE_NUMBER:
341
- case CONCURRENT_MODE_SYMBOL_STRING:
342
- case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
364
+ case ReactSymbols.CONCURRENT_MODE_NUMBER:
365
+ case ReactSymbols.CONCURRENT_MODE_SYMBOL_STRING:
366
+ case ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
343
367
  return true;
344
368
 
345
369
  default:
@@ -405,75 +429,65 @@ export function traverseFiber(
405
429
  ): Promise<Fiber | null>;
406
430
  export function traverseFiber(
407
431
  fiber: Fiber | null,
408
- selector: (node: Fiber) => boolean | Promise<boolean | void> | void,
432
+ selector: FiberSelector,
433
+ ascending?: boolean,
434
+ ): Fiber | null | Promise<Fiber | null>;
435
+ export function traverseFiber(
436
+ fiber: Fiber | null,
437
+ selector: FiberSelector,
409
438
  ascending = false,
410
439
  ): Fiber | null | Promise<Fiber | null> {
411
- if (!fiber) return null;
412
-
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
- })();
430
- }
440
+ return traverseFiberInternal(fiber, selector, ascending);
441
+ }
431
442
 
432
- if (firstResult === true) return fiber;
443
+ const isPromiseLike = <Result>(value: unknown): value is PromiseLike<Result> =>
444
+ (typeof value === "object" || typeof value === "function") &&
445
+ value !== null &&
446
+ "then" in value &&
447
+ typeof value.then === "function";
433
448
 
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;
441
- }
449
+ const traverseFiberChildren = (
450
+ fiber: Fiber,
451
+ selector: FiberSelector,
452
+ ascending: boolean,
453
+ ): Fiber | null | Promise<Fiber | null> => {
454
+ const firstChild = ascending ? fiber.return : fiber.child;
455
+ return traverseFiberSiblings(firstChild, selector, ascending);
456
+ };
442
457
 
443
- const traverseFiberSync = (
458
+ const traverseFiberSiblings = (
444
459
  fiber: Fiber | null,
445
- selector: (node: Fiber) => boolean | void,
446
- ascending = false,
447
- ): Fiber | null => {
460
+ selector: FiberSelector,
461
+ ascending: boolean,
462
+ ): Fiber | null | Promise<Fiber | null> => {
448
463
  if (!fiber) return null;
449
- if (selector(fiber) === true) return fiber;
450
-
451
- let child = ascending ? fiber.return : fiber.child;
452
- while (child) {
453
- const match = traverseFiberSync(child, selector, ascending);
454
- if (match) return match;
455
464
 
456
- child = ascending ? null : child.sibling;
465
+ const nextSibling = ascending ? null : fiber.sibling;
466
+ const match = traverseFiberInternal(fiber, selector, ascending);
467
+ if (isPromiseLike<Fiber | null>(match)) {
468
+ return Promise.resolve(match).then(
469
+ (resolvedMatch) => resolvedMatch ?? traverseFiberSiblings(nextSibling, selector, ascending),
470
+ );
457
471
  }
458
- return null;
472
+ return match ?? traverseFiberSiblings(nextSibling, selector, ascending);
459
473
  };
460
474
 
461
- const traverseFiberAsync = async (
475
+ const traverseFiberInternal = (
462
476
  fiber: Fiber | null,
463
- selector: (node: Fiber) => Promise<boolean | void>,
464
- ascending = false,
465
- ): Promise<Fiber | null> => {
477
+ selector: FiberSelector,
478
+ ascending: boolean,
479
+ ): Fiber | null | Promise<Fiber | null> => {
466
480
  if (!fiber) return null;
467
- if ((await selector(fiber)) === true) return fiber;
468
481
 
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;
482
+ const selection = selector(fiber);
483
+ if (isPromiseLike<boolean | void>(selection)) {
484
+ return Promise.resolve(selection).then((didSelectFiber) =>
485
+ didSelectFiber === true ? fiber : traverseFiberChildren(fiber, selector, ascending),
486
+ );
475
487
  }
476
- return null;
488
+ if (selection === true) return fiber;
489
+
490
+ return traverseFiberChildren(fiber, selector, ascending);
477
491
  };
478
492
 
479
493
  /**
@@ -485,12 +499,12 @@ const traverseFiberAsync = async (
485
499
  * console.log(selfTime, totalTime);
486
500
  * ```
487
501
  */
488
- export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime: number } => {
502
+ export const getTimings = (fiber?: Fiber | null): FiberTimings => {
489
503
  const totalTime = fiber?.actualDuration ?? 0;
490
504
  let selfTime = totalTime;
491
505
  // TODO: calculate a DOM time, which is just host component summed up
492
506
  let child = fiber?.child ?? null;
493
- while (totalTime > 0 && child != null) {
507
+ while (totalTime > 0 && child !== null) {
494
508
  selfTime -= child.actualDuration ?? 0;
495
509
  child = child.sibling;
496
510
  }
@@ -501,48 +515,29 @@ export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime:
501
515
  * Returns `true` if the {@link Fiber} uses React Compiler's memo cache.
502
516
  */
503
517
  export const hasMemoCache = (fiber: Fiber): boolean => {
504
- return Boolean((fiber.updateQueue as unknown as { memoCache: unknown })?.memoCache);
518
+ return Boolean(fiber.updateQueue?.memoCache);
505
519
  };
506
520
 
507
- type FiberType =
508
- | React.ComponentType<unknown>
509
- | React.ForwardRefExoticComponent<unknown>
510
- | React.MemoExoticComponent<React.ComponentType<unknown>>;
511
-
512
521
  /**
513
522
  * Returns the type (e.g. component definition) of the {@link Fiber}
514
523
  */
515
524
  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;
525
+ if (isComponentType(type)) return type;
526
+ if (typeof type !== "object" || type === null) return null;
527
+ return getType(Reflect.get(type, "type") ?? Reflect.get(type, "render"));
528
528
  };
529
529
 
530
530
  /**
531
531
  * Returns the display name of the {@link Fiber} type.
532
532
  */
533
533
  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;
534
+ if (typeof type === "string") return type;
535
+ if ((typeof type !== "function" && typeof type !== "object") || type === null) return null;
536
+ const name = getTypeName(type);
542
537
  if (name) return name;
543
- const unwrappedType = getType(currentType);
538
+ const unwrappedType = getType(type);
544
539
  if (!unwrappedType) return null;
545
- return unwrappedType.displayName || unwrappedType.name || null;
540
+ return getTypeName(unwrappedType);
546
541
  };
547
542
 
548
543
  /**
@@ -550,7 +545,10 @@ export const getDisplayName = (type: unknown): null | string => {
550
545
  */
551
546
  export const detectReactBuildType = (renderer: ReactRenderer): "development" | "production" => {
552
547
  try {
553
- if (typeof renderer.version === "string" && renderer.bundleType > 0) {
548
+ if (
549
+ typeof renderer.version === "string" &&
550
+ renderer.bundleType === ReactBuildType.Development
551
+ ) {
554
552
  return "development";
555
553
  }
556
554
  } catch {}
@@ -562,11 +560,7 @@ export const detectReactBuildType = (renderer: ReactRenderer): "development" | "
562
560
  */
563
561
  export const isInstrumentationActive = (): boolean => {
564
562
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
565
- return (
566
- Boolean(rdtHook?._instrumentationIsActive) ||
567
- isRealReactDevtools(rdtHook) ||
568
- isReactRefresh(rdtHook)
569
- );
563
+ return Boolean(rdtHook?._instrumentationIsActive) || isRealReactDevtools(rdtHook);
570
564
  };
571
565
 
572
566
  export const _fiberRoots = new Set<FiberRoot>();
@@ -582,37 +576,35 @@ export const getLatestFiber = (fiber: Fiber): Fiber => {
582
576
  }
583
577
  for (const root of _fiberRoots) {
584
578
  const latestFiber = traverseFiber(root.current, (innerFiber) => {
585
- if (innerFiber === fiber) return true;
579
+ if (innerFiber === fiber || innerFiber === alternate) return true;
586
580
  });
587
581
  if (latestFiber) return latestFiber;
588
582
  }
589
583
  return fiber;
590
584
  };
591
585
 
592
- export type RenderHandler = <S>(fiber: Fiber, phase: RenderPhase, state?: S) => unknown;
593
-
594
586
  export type RenderPhase = "mount" | "unmount" | "update";
595
587
 
596
- let fiberId = 0;
588
+ let nextFiberId = 0;
597
589
  const fiberIdMap = new WeakMap<Fiber, number>();
598
590
 
599
- export const setFiberId = (fiber: Fiber, id: number = fiberId++): void => {
600
- fiberIdMap.set(fiber, id);
591
+ export const setFiberId = (fiber: Fiber, fiberId: number = nextFiberId++): void => {
592
+ fiberIdMap.set(fiber, fiberId);
593
+ if (Number.isSafeInteger(fiberId) && fiberId >= nextFiberId) {
594
+ nextFiberId = fiberId + 1;
595
+ }
601
596
  };
602
597
 
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
598
  export const getFiberId = (fiber: Fiber): number => {
607
- let id = fiberIdMap.get(fiber);
608
- if (!id && fiber.alternate) {
609
- id = fiberIdMap.get(fiber.alternate);
599
+ let currentFiberId = fiberIdMap.get(fiber);
600
+ if (currentFiberId === undefined && fiber.alternate) {
601
+ currentFiberId = fiberIdMap.get(fiber.alternate);
610
602
  }
611
- if (!id) {
612
- id = fiberId++;
613
- setFiberId(fiber, id);
603
+ if (currentFiberId === undefined) {
604
+ currentFiberId = nextFiberId++;
605
+ setFiberId(fiber, currentFiberId);
614
606
  }
615
- return id;
607
+ return currentFiberId;
616
608
  };
617
609
 
618
610
  const mountFiberRecursively = (
@@ -622,7 +614,7 @@ const mountFiberRecursively = (
622
614
  ): void => {
623
615
  let fiber: Fiber | null = firstChild;
624
616
 
625
- while (fiber != null) {
617
+ while (fiber !== null) {
626
618
  if (!fiberIdMap.has(fiber)) {
627
619
  getFiberId(fiber);
628
620
  }
@@ -631,7 +623,7 @@ const mountFiberRecursively = (
631
623
  onRender(fiber, "mount");
632
624
  }
633
625
 
634
- if (fiber.tag === SuspenseComponentTag) {
626
+ if (fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent) {
635
627
  const isTimedOut = fiber.memoizedState !== null;
636
628
  if (isTimedOut) {
637
629
  // Special case: if Suspense mounts in a timed-out state,
@@ -646,18 +638,12 @@ const mountFiberRecursively = (
646
638
  }
647
639
  }
648
640
  } 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
- }
641
+ const primaryChild = fiber.child?.child ?? null;
656
642
  if (primaryChild !== null) {
657
643
  mountFiberRecursively(onRender, primaryChild, false);
658
644
  }
659
645
  }
660
- } else if (fiber.child != null) {
646
+ } else if (fiber.child !== null) {
661
647
  mountFiberRecursively(onRender, fiber.child, true);
662
648
  }
663
649
  fiber = traverseSiblings ? fiber.sibling : null;
@@ -667,7 +653,7 @@ const mountFiberRecursively = (
667
653
  const updateFiberRecursively = (
668
654
  onRender: RenderHandler,
669
655
  nextFiber: Fiber,
670
- prevFiber: Fiber,
656
+ prevFiber: Fiber | null,
671
657
  parentFiber: Fiber | null,
672
658
  ): void => {
673
659
  if (!fiberIdMap.has(nextFiber)) {
@@ -678,7 +664,7 @@ const updateFiberRecursively = (
678
664
  getFiberId(prevFiber);
679
665
  }
680
666
 
681
- const isSuspense = nextFiber.tag === SuspenseComponentTag;
667
+ const isSuspense = nextFiber.tag === getReactWorkTagsForFiber(nextFiber).SuspenseComponent;
682
668
 
683
669
  const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
684
670
  if (shouldIncludeInTree && didFiberRender(nextFiber)) {
@@ -765,7 +751,7 @@ const updateFiberRecursively = (
765
751
  };
766
752
 
767
753
  const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
768
- const isRoot = fiber.tag === HostRootTag;
754
+ const isRoot = fiber.tag === getReactWorkTagsForFiber(fiber).HostRoot;
769
755
 
770
756
  if (isRoot || !shouldFilterFiber(fiber)) {
771
757
  onRender(fiber, "unmount");
@@ -774,7 +760,8 @@ const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
774
760
 
775
761
  const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => {
776
762
  // We might meet a nested Suspense on our way.
777
- const isTimedOutSuspense = fiber.tag === SuspenseComponentTag && fiber.memoizedState !== null;
763
+ const isTimedOutSuspense =
764
+ fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent && fiber.memoizedState !== null;
778
765
  let child = fiber.child;
779
766
 
780
767
  if (isTimedOutSuspense) {
@@ -800,7 +787,7 @@ const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber):
800
787
 
801
788
  let commitId = 0;
802
789
  const rootInstanceMap = new WeakMap<
803
- FiberRoot,
790
+ Fiber | FiberRoot,
804
791
  {
805
792
  id: number;
806
793
  prevFiber: Fiber | null;
@@ -814,7 +801,7 @@ const rootInstanceMap = new WeakMap<
814
801
  * console.log(phase)
815
802
  * })
816
803
  */
817
- export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler): void => {
804
+ export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: RenderHandler): void => {
818
805
  const fiber = "current" in root ? root.current : root;
819
806
 
820
807
  let rootInstance = rootInstanceMap.get(root);
@@ -831,13 +818,15 @@ export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler)
831
818
  } else if (prevFiber !== null) {
832
819
  const wasMounted =
833
820
  prevFiber &&
834
- prevFiber.memoizedState != null &&
835
- prevFiber.memoizedState.element != null &&
821
+ prevFiber.memoizedState !== null &&
822
+ prevFiber.memoizedState.element !== null &&
823
+ prevFiber.memoizedState.element !== undefined &&
836
824
  // A dehydrated root is not considered mounted
837
825
  prevFiber.memoizedState.isDehydrated !== true;
838
826
  const isMounted =
839
- fiber.memoizedState != null &&
840
- fiber.memoizedState.element != null &&
827
+ fiber.memoizedState !== null &&
828
+ fiber.memoizedState.element !== null &&
829
+ fiber.memoizedState.element !== undefined &&
841
830
  // A dehydrated root is not considered mounted
842
831
  fiber.memoizedState.isDehydrated !== true;
843
832
 
@@ -859,16 +848,15 @@ const overrideRenderers = new Set<ReactRenderer>();
859
848
  let areOverrideRenderersWired = false;
860
849
 
861
850
  const wireOverrideRenderers = (): void => {
862
- if (areOverrideRenderersWired) return;
863
851
  if (!hasRDTHook()) return;
864
852
  const rdtHook = getRDTHook();
865
- if (!rdtHook?.renderers) return;
866
- areOverrideRenderersWired = true;
867
853
 
868
- ensureHookDispatchesToListeners(rdtHook);
854
+ setHookEventDispatchers(rdtHook);
869
855
  for (const renderer of rdtHook.renderers.values()) {
870
856
  overrideRenderers.add(renderer);
871
857
  }
858
+ if (areOverrideRenderersWired) return;
859
+ areOverrideRenderersWired = true;
872
860
  onRendererInject((renderer) => {
873
861
  overrideRenderers.add(renderer);
874
862
  });
@@ -880,14 +868,13 @@ const getRootRenderer = (fiber: Fiber): ReactRenderer | null => {
880
868
  while (hostRootFiber.return) {
881
869
  hostRootFiber = hostRootFiber.return;
882
870
  }
883
- const rendererId = rootRendererIds.get(hostRootFiber.stateNode);
871
+ const fiberRoot = hostRootFiber.stateNode;
872
+ if (!isFiberRoot(fiberRoot)) return null;
873
+ const rendererId = rootRendererIds.get(fiberRoot);
884
874
  if (rendererId === undefined) return null;
885
- return getRDTHook().renderers?.get(rendererId) ?? null;
875
+ return getRDTHook().renderers.get(rendererId) ?? null;
886
876
  };
887
877
 
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
878
  const resolveOverrideRenderers = (fiber: Fiber): ReactRenderer[] => {
892
879
  wireOverrideRenderers();
893
880
  const rootRenderer = getRootRenderer(fiber);
@@ -913,15 +900,14 @@ const getHookStateDispatch = (
913
900
  hookIndex: number,
914
901
  ): ((value: unknown) => void) | null => {
915
902
  let hookState = fiber.memoizedState;
916
- for (let i = 0; i < hookIndex; i++) {
903
+ for (let currentHookIndex = 0; currentHookIndex < hookIndex; currentHookIndex++) {
917
904
  if (!hookState?.next) return null;
918
905
  hookState = hookState.next;
919
906
  }
920
907
  const queue = hookState?.queue;
921
- if (isPOJO(queue) && typeof queue.dispatch === "function") {
922
- return queue.dispatch as (value: unknown) => void;
923
- }
924
- return null;
908
+ if (!isPOJO(queue)) return null;
909
+ const dispatch = queue.dispatch;
910
+ return typeof dispatch === "function" ? (value) => dispatch(value) : null;
925
911
  };
926
912
 
927
913
  const findContextProviderFiber = (fiber: Fiber, contextType: unknown): Fiber | null => {
@@ -947,28 +933,33 @@ const isPOJO = (maybePOJO: unknown): maybePOJO is Record<string, unknown> => {
947
933
  const buildPathsFromValue = (
948
934
  maybePOJO: Record<string, unknown>,
949
935
  basePath: string[] = [],
950
- ): Array<{ path: string[]; value: unknown }> => {
936
+ ancestors = new WeakSet<object>(),
937
+ ): ValueWrite[] => {
951
938
  if (!isPOJO(maybePOJO)) {
952
939
  return [{ path: basePath, value: maybePOJO }];
953
940
  }
941
+ if (ancestors.has(maybePOJO)) {
942
+ return [{ path: basePath, value: maybePOJO }];
943
+ }
954
944
 
955
- const paths: Array<{ path: string[]; value: unknown }> = [];
945
+ ancestors.add(maybePOJO);
946
+ const paths: ValueWrite[] = [];
956
947
 
957
- for (const key in maybePOJO) {
958
- const value = maybePOJO[key];
948
+ for (const [key, value] of Object.entries(maybePOJO)) {
959
949
  const path = basePath.concat(key);
960
950
 
961
951
  if (isPOJO(value)) {
962
- paths.push(...buildPathsFromValue(value, path));
952
+ paths.push(...buildPathsFromValue(value, path, ancestors));
963
953
  } else {
964
954
  paths.push({ path, value });
965
955
  }
966
956
  }
967
957
 
958
+ ancestors.delete(maybePOJO);
968
959
  return paths;
969
960
  };
970
961
 
971
- const buildValueWrites = (partialValue: unknown): Array<{ path: string[]; value: unknown }> =>
962
+ const buildValueWrites = (partialValue: unknown): ValueWrite[] =>
972
963
  isPOJO(partialValue) ? buildPathsFromValue(partialValue) : [{ path: [], value: partialValue }];
973
964
 
974
965
  export const overrideProps = (fiber: Fiber, partialValue: Record<string, unknown>) => {
@@ -985,11 +976,10 @@ export const overrideHookState = (fiber: Fiber, id: number, partialValue: unknow
985
976
  const writes = buildValueWrites(partialValue);
986
977
 
987
978
  if (renderers.length > 0) {
988
- const hookId = String(id);
989
979
  for (const renderer of renderers) {
990
980
  for (const { path, value } of writes) {
991
981
  try {
992
- renderer.overrideHookState?.(fiber, hookId, path, value);
982
+ renderer.overrideHookState?.(fiber, id, path, value);
993
983
  } catch {}
994
984
  }
995
985
  }
@@ -1022,24 +1012,22 @@ export const overrideContext = (fiber: Fiber, contextType: unknown, partialValue
1022
1012
  export interface InstrumentationOptions {
1023
1013
  name?: string;
1024
1014
  onActive?: () => unknown;
1025
- onCommitFiberRoot?: (rendererID: number, root: FiberRoot, priority: number | void) => unknown;
1015
+ onCommitFiberRoot?: (
1016
+ rendererID: number,
1017
+ root: FiberRoot,
1018
+ priority: number | void,
1019
+ didError?: boolean,
1020
+ ) => unknown;
1026
1021
  onCommitFiberUnmount?: (rendererID: number, fiber: Fiber) => unknown;
1027
1022
  onPostCommitFiberRoot?: (rendererID: number, root: FiberRoot) => unknown;
1028
1023
  onScheduleFiberRoot?: (rendererID: number, root: FiberRoot, children: React.ReactNode) => unknown;
1029
1024
  }
1030
1025
 
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
- >();
1026
+ interface InstrumentationSubscription {
1027
+ options: InstrumentationOptions;
1028
+ }
1029
+
1030
+ const instrumentationSubscriptions = new Set<InstrumentationSubscription>();
1043
1031
 
1044
1032
  interface HookDispatchers {
1045
1033
  onCommitFiberRoot: ReactDevToolsGlobalHook["onCommitFiberRoot"];
@@ -1049,6 +1037,7 @@ interface HookDispatchers {
1049
1037
  }
1050
1038
 
1051
1039
  const hookDispatchers = new WeakMap<ReactDevToolsGlobalHook, Partial<HookDispatchers>>();
1040
+ let didSubscribeToHookReplacements = false;
1052
1041
 
1053
1042
  const rootRendererIds = new WeakMap<FiberRoot, number>();
1054
1043
 
@@ -1057,7 +1046,7 @@ const rootRendererIds = new WeakMap<FiberRoot, number>();
1057
1046
  // next instrument() call installs a fresh wrapper over it; a superseded
1058
1047
  // wrapper still forwards the previous chain but skips the listeners so
1059
1048
  // they never fire twice.
1060
- const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
1049
+ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
1061
1050
  const dispatchers = hookDispatchers.get(rdtHook) ?? {};
1062
1051
  hookDispatchers.set(rdtHook, dispatchers);
1063
1052
 
@@ -1070,13 +1059,29 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1070
1059
  rendererID,
1071
1060
  root,
1072
1061
  priority,
1062
+ didError,
1073
1063
  ) => {
1074
- prevOnCommitFiberRoot?.(rendererID, root, priority);
1064
+ if (prevOnCommitFiberRoot) {
1065
+ prevOnCommitFiberRoot.call(rdtHook, rendererID, root, priority, didError);
1066
+ }
1075
1067
  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);
1068
+ setReactWorkTagsForFiber(root.current, rdtHook.renderers.get(rendererID));
1069
+ const rootMemoizedState = root.current.memoizedState;
1070
+ const isUnmounting =
1071
+ rootMemoizedState === null ||
1072
+ rootMemoizedState.element === null ||
1073
+ rootMemoizedState.element === undefined;
1074
+ if (isUnmounting) {
1075
+ _fiberRoots.delete(root);
1076
+ rootRendererIds.delete(root);
1077
+ } else {
1078
+ _fiberRoots.add(root);
1079
+ rootRendererIds.set(root, rendererID);
1080
+ }
1081
+ for (const { options } of instrumentationSubscriptions) {
1082
+ if (options.onCommitFiberRoot) {
1083
+ options.onCommitFiberRoot(rendererID, root, priority, didError);
1084
+ }
1080
1085
  }
1081
1086
  };
1082
1087
  dispatchers.onCommitFiberRoot = dispatchCommitFiberRoot;
@@ -1092,12 +1097,17 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1092
1097
  rendererID,
1093
1098
  fiber,
1094
1099
  ) => {
1095
- prevOnCommitFiberUnmount?.(rendererID, fiber);
1100
+ setReactWorkTagsForFiber(fiber, rdtHook.renderers.get(rendererID));
1101
+ if (prevOnCommitFiberUnmount) {
1102
+ prevOnCommitFiberUnmount.call(rdtHook, rendererID, fiber);
1103
+ }
1096
1104
  if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
1097
1105
  return;
1098
1106
  }
1099
- for (const listener of commitFiberUnmountListeners) {
1100
- listener(rendererID, fiber);
1107
+ for (const { options } of instrumentationSubscriptions) {
1108
+ if (options.onCommitFiberUnmount) {
1109
+ options.onCommitFiberUnmount(rendererID, fiber);
1110
+ }
1101
1111
  }
1102
1112
  };
1103
1113
  dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
@@ -1113,12 +1123,16 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1113
1123
  rendererID,
1114
1124
  root,
1115
1125
  ) => {
1116
- prevOnPostCommitFiberRoot?.(rendererID, root);
1126
+ if (prevOnPostCommitFiberRoot) {
1127
+ prevOnPostCommitFiberRoot.call(rdtHook, rendererID, root);
1128
+ }
1117
1129
  if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
1118
1130
  return;
1119
1131
  }
1120
- for (const listener of postCommitFiberRootListeners) {
1121
- listener(rendererID, root);
1132
+ for (const { options } of instrumentationSubscriptions) {
1133
+ if (options.onPostCommitFiberRoot) {
1134
+ options.onPostCommitFiberRoot(rendererID, root);
1135
+ }
1122
1136
  }
1123
1137
  };
1124
1138
  dispatchers.onPostCommitFiberRoot = dispatchPostCommitFiberRoot;
@@ -1135,10 +1149,14 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1135
1149
  root,
1136
1150
  children,
1137
1151
  ) => {
1138
- prevOnScheduleFiberRoot?.(rendererID, root, children);
1152
+ if (prevOnScheduleFiberRoot) {
1153
+ prevOnScheduleFiberRoot.call(rdtHook, rendererID, root, children);
1154
+ }
1139
1155
  if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
1140
- for (const listener of scheduleFiberRootListeners) {
1141
- listener(rendererID, root, children);
1156
+ for (const { options } of instrumentationSubscriptions) {
1157
+ if (options.onScheduleFiberRoot) {
1158
+ options.onScheduleFiberRoot(rendererID, root, children);
1159
+ }
1142
1160
  }
1143
1161
  };
1144
1162
  dispatchers.onScheduleFiberRoot = dispatchScheduleFiberRoot;
@@ -1151,8 +1169,7 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1151
1169
  * dispatches to a set of listeners, so multiple `instrument` calls compose
1152
1170
  * without stacking patches. Returns an unsubscribe function that removes
1153
1171
  * exactly the handlers this call registered.
1154
- * The returned function is also a `Disposable`, so it composes with other
1155
- * bippy subscriptions through `using`.
1172
+ * The unsubscribe function can also be disposed through `using`.
1156
1173
  * @example
1157
1174
  * const unsubscribe = instrument({
1158
1175
  * onActive() {
@@ -1167,34 +1184,24 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1167
1184
  export const instrument = (options: InstrumentationOptions): Unsubscribe => {
1168
1185
  const rdtHook = getRDTHook(options.onActive);
1169
1186
 
1187
+ if (!didSubscribeToHookReplacements) {
1188
+ onRDTHookReplace(setHookEventDispatchers);
1189
+ didSubscribeToHookReplacements = true;
1190
+ }
1191
+
1170
1192
  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
1171
1193
 
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);
1194
+ setHookEventDispatchers(rdtHook);
1195
+ const subscription: InstrumentationSubscription = { options };
1196
+ instrumentationSubscriptions.add(subscription);
1197
+
1198
+ return createUnsubscribe(() => {
1199
+ if (options.onActive) _onActiveListeners.delete(options.onActive);
1200
+ instrumentationSubscriptions.delete(subscription);
1192
1201
  });
1193
1202
  };
1194
1203
 
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.
1204
+ // React uses per-renderer suffixes for host-instance Fiber keys, so discovered keys are cached.
1198
1205
  const knownFiberPropertyKeys = new Set<string>();
1199
1206
 
1200
1207
  const isFiberPropertyKey = (key: string): boolean =>
@@ -1202,6 +1209,37 @@ const isFiberPropertyKey = (key: string): boolean =>
1202
1209
  key.startsWith("__reactInternalInstance$") ||
1203
1210
  key.startsWith("__reactFiber");
1204
1211
 
1212
+ const getLegacyRootFiber = (hostInstance: object): Fiber | null => {
1213
+ const reactRootContainer = Reflect.get(hostInstance, "_reactRootContainer");
1214
+ if (typeof reactRootContainer !== "object" || reactRootContainer === null) return null;
1215
+ const internalRoot = Reflect.get(reactRootContainer, "_internalRoot");
1216
+ if (typeof internalRoot !== "object" || internalRoot === null) return null;
1217
+ const current = Reflect.get(internalRoot, "current");
1218
+ if (typeof current !== "object" || current === null) return null;
1219
+ const child = Reflect.get(current, "child");
1220
+ return isValidFiber(child) ? child : null;
1221
+ };
1222
+
1223
+ const getInternalInstanceHandle = (hostInstance: object): Fiber | null => {
1224
+ const internalInstanceHandle =
1225
+ Reflect.get(hostInstance, "__internalInstanceHandle") ??
1226
+ Reflect.get(hostInstance, "_internalInstanceHandle");
1227
+ return isValidFiber(internalInstanceHandle) ? internalInstanceHandle : null;
1228
+ };
1229
+
1230
+ const getPublicHostInstance = (stateNode: unknown): unknown => {
1231
+ if (typeof stateNode !== "object" || stateNode === null) return stateNode;
1232
+ const canonical = Reflect.get(stateNode, "canonical");
1233
+ if (typeof canonical === "object" && canonical !== null) {
1234
+ const publicInstance = Reflect.get(canonical, "publicInstance");
1235
+ if (typeof publicInstance === "object" && publicInstance !== null) {
1236
+ return publicInstance;
1237
+ }
1238
+ }
1239
+ const nativeTag = Reflect.get(stateNode, "_nativeTag");
1240
+ return typeof nativeTag === "number" ? nativeTag : stateNode;
1241
+ };
1242
+
1205
1243
  export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1206
1244
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1207
1245
  if (rdtHook?.renderers) {
@@ -1213,40 +1251,56 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1213
1251
  }
1214
1252
  }
1215
1253
 
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
- }
1254
+ if (typeof hostInstance === "object" && hostInstance !== null) {
1255
+ const legacyRootFiber = getLegacyRootFiber(hostInstance);
1256
+ if (legacyRootFiber) return legacyRootFiber;
1221
1257
 
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
- }
1258
+ const internalInstanceHandle = getInternalInstanceHandle(hostInstance);
1259
+ if (internalInstanceHandle) return internalInstanceHandle;
1233
1260
 
1234
- const hostInstanceRecord = hostInstance as Record<string, unknown>;
1235
1261
  for (const knownKey of knownFiberPropertyKeys) {
1236
- const fiber = hostInstanceRecord[knownKey];
1237
- if (fiber) return fiber as Fiber;
1262
+ const fiber = Reflect.get(hostInstance, knownKey);
1263
+ if (isValidFiber(fiber)) return fiber;
1238
1264
  }
1239
1265
 
1240
- for (const key of Object.keys(hostInstanceRecord)) {
1266
+ for (const key of Object.keys(hostInstance)) {
1241
1267
  if (isFiberPropertyKey(key)) {
1242
1268
  knownFiberPropertyKeys.add(key);
1243
- return (hostInstanceRecord[key] || null) as Fiber | null;
1269
+ const fiber = Reflect.get(hostInstance, key);
1270
+ if (isValidFiber(fiber)) return fiber;
1244
1271
  }
1245
1272
  }
1246
1273
  }
1274
+
1275
+ if (
1276
+ hostInstance !== null &&
1277
+ hostInstance !== undefined &&
1278
+ (typeof hostInstance === "object" || typeof hostInstance === "number")
1279
+ ) {
1280
+ for (const fiberRoot of _fiberRoots) {
1281
+ const fiber = traverseFiber(
1282
+ fiberRoot.current,
1283
+ (candidateFiber) =>
1284
+ isHostFiber(candidateFiber) &&
1285
+ getPublicHostInstance(candidateFiber.stateNode) === hostInstance,
1286
+ );
1287
+ if (fiber) return fiber;
1288
+ }
1289
+ }
1247
1290
  return null;
1248
1291
  };
1249
1292
 
1250
- export * from "./rdt-hook.js";
1251
- export * from "./unsubscribe.js";
1252
- export type * from "./types.js";
1293
+ export {
1294
+ BIPPY_INSTRUMENTATION_STRING,
1295
+ _onActiveListeners,
1296
+ _renderers,
1297
+ getRDTHook,
1298
+ hasRDTHook,
1299
+ installRDTHook,
1300
+ isRealReactDevtools,
1301
+ onRendererInject,
1302
+ patchRDTHook,
1303
+ version,
1304
+ } from "./rdt-hook.js";
1305
+ export type { Unsubscribe } from "./rdt-hook.js";
1306
+ export type * from "./react-internals/index.js";