bippy 0.6.1-dev.61475ed → 0.6.1-dev.d7876ea

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 (62) hide show
  1. package/README.md +32 -48
  2. package/dist/core.cjs +1 -1
  3. package/dist/core.d.cts +42 -56
  4. package/dist/core.d.ts +42 -56
  5. package/dist/core.js +1 -1
  6. package/dist/core2.cjs +9 -0
  7. package/dist/core2.d.cts +3 -3
  8. package/dist/core2.d.ts +3 -3
  9. package/dist/core2.js +9 -0
  10. package/dist/errors.d.cts +436 -0
  11. package/dist/errors.d.ts +436 -0
  12. package/dist/index.cjs +1 -1
  13. package/dist/index.d.cts +3 -3
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.iife.js +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/install-hook-only.cjs +1 -1
  18. package/dist/install-hook-only.d.cts +1 -1
  19. package/dist/install-hook-only.d.ts +1 -1
  20. package/dist/install-hook-only.iife.js +1 -1
  21. package/dist/install-hook-only.js +1 -1
  22. package/dist/rdt-hook.cjs +1 -1
  23. package/dist/rdt-hook.js +1 -1
  24. package/dist/source.cjs +14 -4
  25. package/dist/source.d.cts +18 -9
  26. package/dist/source.d.ts +18 -9
  27. package/dist/source.js +14 -4
  28. package/package.json +21 -15
  29. package/src/core.ts +343 -253
  30. package/src/errors.ts +99 -0
  31. package/src/generated/react-work-tags.d.ts +261 -0
  32. package/src/generated/react-work-tags.js +240 -0
  33. package/src/rdt-hook.ts +154 -149
  34. package/src/react-internals.ts +65 -0
  35. package/src/semver.ts +78 -0
  36. package/src/source/constants.ts +1 -1
  37. package/src/source/error-stack.ts +11 -0
  38. package/src/source/get-source.ts +2 -1
  39. package/src/source/index.ts +9 -1
  40. package/src/source/inspect-hooks.ts +199 -164
  41. package/src/source/owner-stack.ts +84 -114
  42. package/src/source/parse-debug-stack.ts +4 -4
  43. package/src/source/parse-stack.ts +1 -1
  44. package/src/source/symbolication.ts +365 -89
  45. package/src/types.ts +165 -334
  46. package/src/unsubscribe.ts +2 -2
  47. package/dist/get-source.cjs +0 -19
  48. package/dist/get-source.js +0 -19
  49. package/dist/react-refresh.cjs +0 -9
  50. package/dist/react-refresh.d.cts +0 -66
  51. package/dist/react-refresh.d.ts +0 -66
  52. package/dist/react-refresh.js +0 -9
  53. package/dist/unsubscribe.d.cts +0 -298
  54. package/dist/unsubscribe.d.ts +0 -298
  55. package/src/react-refresh/constants.ts +0 -9
  56. package/src/react-refresh/detect-hmr-transport.ts +0 -33
  57. package/src/react-refresh/index.ts +0 -173
  58. package/src/react-refresh/metro-hmr-transport.ts +0 -188
  59. package/src/react-refresh/next-webpack-hmr-transport.ts +0 -72
  60. package/src/react-refresh/normalize-hmr-file-path.ts +0 -24
  61. package/src/react-refresh/types.ts +0 -7
  62. package/src/react-refresh/vite-hmr-transport.ts +0 -116
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,91 @@ import type {
7
6
  ContextDependency,
8
7
  Fiber,
9
8
  FiberRoot,
9
+ HostFiber,
10
10
  MemoizedState,
11
11
  ReactDevToolsGlobalHook,
12
12
  ReactRenderer,
13
13
  } from "./types.js";
14
14
 
15
+ import { BippyInstrumentationError, BippyReactDevToolsError, runWithBippyError } from "./errors.js";
16
+
15
17
  import {
16
18
  _onActiveListeners,
17
19
  BIPPY_INSTRUMENTATION_STRING,
18
20
  getRDTHook,
19
21
  hasRDTHook,
20
- isReactRefresh,
21
22
  isRealReactDevtools,
23
+ onRDTHookReplace,
22
24
  onRendererInject,
23
25
  } from "./rdt-hook.js";
26
+ import {
27
+ getReactWorkTagsForFiber,
28
+ MutationMask,
29
+ ReactBuildType,
30
+ ReactFiberFlags,
31
+ ReactSymbols,
32
+ setReactWorkTagsForFiber,
33
+ } from "./react-internals.js";
24
34
  import { toUnsubscribe, type Unsubscribe } from "./unsubscribe.js";
25
35
 
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;
36
+ export { getReactWorkTags, ReactSymbols } from "./react-internals.js";
37
+ export type { ReactWorkTagMap, ReactWorkTagVersion } from "./react-internals.js";
38
+ export {
39
+ BippyError,
40
+ BippyHookInstallationError,
41
+ BippyHookInspectionError,
42
+ BippyHookListenerError,
43
+ BippyHookRenderError,
44
+ BippyInstrumentationError,
45
+ BippyReactBuildError,
46
+ BippyReactDevToolsError,
47
+ BippySourceMapError,
48
+ BippyUnsupportedHookError,
49
+ } from "./errors.js";
50
+
51
+ const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
52
+ typeof value === "object" && value !== null;
53
+
54
+ const isComponentType = (value: unknown): value is React.ComponentType<unknown> =>
55
+ typeof value === "function";
56
+
57
+ const getPropertyValue = (value: object, key: PropertyKey): unknown => {
58
+ let currentValue: object | null = value;
59
+ while (currentValue) {
60
+ const descriptor = Object.getOwnPropertyDescriptor(currentValue, key);
61
+ if (descriptor) {
62
+ if ("value" in descriptor) return descriptor.value;
63
+ return descriptor.get?.call(value);
64
+ }
65
+ currentValue = Object.getPrototypeOf(currentValue);
66
+ }
67
+ return undefined;
68
+ };
69
+
70
+ const getTypeName = (value: object): string | null => {
71
+ const displayName = getPropertyValue(value, "displayName");
72
+ if (typeof displayName === "string" && displayName) return displayName;
73
+ const name = getPropertyValue(value, "name");
74
+ return typeof name === "string" && name ? name : null;
75
+ };
76
+
77
+ interface FiberSelector {
78
+ (node: Fiber): boolean | Promise<boolean | void> | void;
79
+ }
80
+
81
+ export interface FiberTimings {
82
+ selfTime: number;
83
+ totalTime: number;
84
+ }
85
+
86
+ export interface RenderHandler {
87
+ <State>(fiber: Fiber, phase: RenderPhase, state?: State): unknown;
88
+ }
89
+
90
+ interface ValueWrite {
91
+ path: string[];
92
+ value: unknown;
93
+ }
69
94
 
70
95
  /**
71
96
  * Returns `true` if object is a React Element.
@@ -74,10 +99,9 @@ const MutationMask =
74
99
  */
75
100
  export const isValidElement = (element: unknown): element is React.ReactElement =>
76
101
  typeof element === "object" &&
77
- element != null &&
102
+ element !== null &&
78
103
  "$$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(
104
+ [ReactSymbols.LEGACY_ELEMENT_SYMBOL_STRING, ReactSymbols.ELEMENT_SYMBOL_STRING].includes(
81
105
  String(element.$$typeof),
82
106
  );
83
107
 
@@ -86,7 +110,7 @@ export const isValidElement = (element: unknown): element is React.ReactElement
86
110
  */
87
111
  export const isValidFiber = (fiber: unknown): fiber is Fiber =>
88
112
  typeof fiber === "object" &&
89
- fiber != null &&
113
+ fiber !== null &&
90
114
  "tag" in fiber &&
91
115
  "stateNode" in fiber &&
92
116
  "return" in fiber &&
@@ -99,16 +123,16 @@ export const isValidFiber = (fiber: unknown): fiber is Fiber =>
99
123
  *
100
124
  * @see https://reactnative.dev/architecture/glossary#host-view-tree-and-host-view
101
125
  */
102
- export const isHostFiber = (fiber: Fiber): boolean => {
126
+ export const isHostFiber = (fiber: Fiber): fiber is HostFiber => {
127
+ const workTags = getReactWorkTagsForFiber(fiber);
103
128
  switch (fiber.tag) {
104
- case HostComponentTag:
105
- // @ts-expect-error: it exists
106
- case HostHoistableTag:
107
- // @ts-expect-error: it exists
108
- case HostSingletonTag:
129
+ case workTags.HostComponent:
130
+ case workTags.HostText:
131
+ case workTags.HostHoistable:
132
+ case workTags.HostSingleton:
109
133
  return true;
110
134
  default:
111
- return typeof fiber.type === "string";
135
+ return false;
112
136
  }
113
137
  };
114
138
 
@@ -118,12 +142,13 @@ export const isHostFiber = (fiber: Fiber): boolean => {
118
142
  * @see https://reactnative.dev/architecture/glossary#react-composite-components
119
143
  */
120
144
  export const isCompositeFiber = (fiber: Fiber): boolean => {
145
+ const workTags = getReactWorkTagsForFiber(fiber);
121
146
  switch (fiber.tag) {
122
- case ClassComponentTag:
123
- case ForwardRefTag:
124
- case FunctionComponentTag:
125
- case MemoComponentTag:
126
- case SimpleMemoComponentTag:
147
+ case workTags.ClassComponent:
148
+ case workTags.ForwardRef:
149
+ case workTags.FunctionComponent:
150
+ case workTags.MemoComponent:
151
+ case workTags.SimpleMemoComponent:
127
152
  return true;
128
153
  default:
129
154
  return false;
@@ -230,16 +255,17 @@ export const traverseProps = (
230
255
  export const didFiberRender = (fiber: Fiber): boolean => {
231
256
  const nextProps = fiber.memoizedProps;
232
257
  const prevProps = fiber.alternate?.memoizedProps || {};
233
- const flags = fiber.flags ?? (fiber as unknown as { effectTag: number }).effectTag ?? 0;
258
+ const flags = fiber.flags ?? fiber.effectTag ?? 0;
259
+ const workTags = getReactWorkTagsForFiber(fiber);
234
260
 
235
261
  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;
262
+ case workTags.ClassComponent:
263
+ case workTags.ContextConsumer:
264
+ case workTags.ForwardRef:
265
+ case workTags.FunctionComponent:
266
+ case workTags.MemoComponent:
267
+ case workTags.SimpleMemoComponent: {
268
+ return (flags & ReactFiberFlags.PerformedWork) === ReactFiberFlags.PerformedWork;
243
269
  }
244
270
  default:
245
271
  // Host nodes (DOM, root, etc.)
@@ -257,8 +283,8 @@ export const didFiberRender = (fiber: Fiber): boolean => {
257
283
  */
258
284
  export const didFiberCommit = (fiber: Fiber): boolean => {
259
285
  return Boolean(
260
- (fiber.flags & (MutationMask | Cloned)) !== 0 ||
261
- (fiber.subtreeFlags & (MutationMask | Cloned)) !== 0,
286
+ (fiber.flags & (MutationMask | ReactFiberFlags.Cloned)) !== 0 ||
287
+ (fiber.subtreeFlags & (MutationMask | ReactFiberFlags.Cloned)) !== 0,
262
288
  );
263
289
  };
264
290
 
@@ -297,7 +323,8 @@ export const getFiberStack = (fiber: Fiber): Fiber[] => {
297
323
  let currentFiber = fiber;
298
324
  while (currentFiber.return) {
299
325
  stack.push(currentFiber);
300
- currentFiber = currentFiber.return as Fiber;
326
+ const parentFiber = currentFiber.return;
327
+ currentFiber = parentFiber;
301
328
  }
302
329
  return stack;
303
330
  };
@@ -306,8 +333,9 @@ export const getFiberStack = (fiber: Fiber): Fiber[] => {
306
333
  * Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
307
334
  */
308
335
  const shouldFilterFiber = (fiber: Fiber): boolean => {
336
+ const workTags = getReactWorkTagsForFiber(fiber);
309
337
  switch (fiber.tag) {
310
- case DehydratedSuspenseComponentTag:
338
+ case workTags.DehydratedSuspenseComponent:
311
339
  // TODO: ideally we would show dehydrated Suspense immediately.
312
340
  // However, it has some special behavior (like disconnecting
313
341
  // an alternate and turning into real Suspense) which breaks DevTools.
@@ -315,13 +343,13 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
315
343
  // https://github.com/bvaughn/react-devtools-experimental/issues/197
316
344
  return true;
317
345
 
318
- case FragmentTag:
319
- case HostTextTag:
320
- case LegacyHiddenComponentTag:
321
- case OffscreenComponentTag:
346
+ case workTags.Fragment:
347
+ case workTags.HostText:
348
+ case workTags.LegacyHiddenComponent:
349
+ case workTags.OffscreenComponent:
322
350
  return true;
323
351
 
324
- case HostRootTag:
352
+ case workTags.HostRoot:
325
353
  // It is never valid to filter the root element.
326
354
  return false;
327
355
 
@@ -331,15 +359,15 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
331
359
 
332
360
  if (typeof symbolOrNumber === "symbol") {
333
361
  return (
334
- symbolOrNumber.description === CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
335
- symbolOrNumber.description === DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
362
+ symbolOrNumber.description === ReactSymbols.CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
363
+ symbolOrNumber.description === ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
336
364
  );
337
365
  }
338
366
 
339
367
  switch (symbolOrNumber) {
340
- case CONCURRENT_MODE_NUMBER:
341
- case CONCURRENT_MODE_SYMBOL_STRING:
342
- case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
368
+ case ReactSymbols.CONCURRENT_MODE_NUMBER:
369
+ case ReactSymbols.CONCURRENT_MODE_SYMBOL_STRING:
370
+ case ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
343
371
  return true;
344
372
 
345
373
  default:
@@ -405,7 +433,7 @@ export function traverseFiber(
405
433
  ): Promise<Fiber | null>;
406
434
  export function traverseFiber(
407
435
  fiber: Fiber | null,
408
- selector: (node: Fiber) => boolean | Promise<boolean | void> | void,
436
+ selector: FiberSelector,
409
437
  ascending = false,
410
438
  ): Fiber | null | Promise<Fiber | null> {
411
439
  if (!fiber) return null;
@@ -417,11 +445,7 @@ export function traverseFiber(
417
445
 
418
446
  let child = ascending ? fiber.return : fiber.child;
419
447
  while (child) {
420
- const match = await traverseFiberAsync(
421
- child,
422
- selector as (node: Fiber) => Promise<boolean | void>,
423
- ascending,
424
- );
448
+ const match = await traverseFiberAsync(child, selector, ascending);
425
449
  if (match) return match;
426
450
  child = ascending ? null : child.sibling;
427
451
  }
@@ -433,7 +457,7 @@ export function traverseFiber(
433
457
 
434
458
  let child = ascending ? fiber.return : fiber.child;
435
459
  while (child) {
436
- const match = traverseFiberSync(child, selector as (node: Fiber) => boolean | void, ascending);
460
+ const match = traverseFiberSync(child, selector, ascending);
437
461
  if (match) return match;
438
462
  child = ascending ? null : child.sibling;
439
463
  }
@@ -442,11 +466,13 @@ export function traverseFiber(
442
466
 
443
467
  const traverseFiberSync = (
444
468
  fiber: Fiber | null,
445
- selector: (node: Fiber) => boolean | void,
469
+ selector: FiberSelector,
446
470
  ascending = false,
447
471
  ): Fiber | null => {
448
472
  if (!fiber) return null;
449
- if (selector(fiber) === true) return fiber;
473
+ const selection = selector(fiber);
474
+ if (selection instanceof Promise) return null;
475
+ if (selection === true) return fiber;
450
476
 
451
477
  let child = ascending ? fiber.return : fiber.child;
452
478
  while (child) {
@@ -460,7 +486,7 @@ const traverseFiberSync = (
460
486
 
461
487
  const traverseFiberAsync = async (
462
488
  fiber: Fiber | null,
463
- selector: (node: Fiber) => Promise<boolean | void>,
489
+ selector: FiberSelector,
464
490
  ascending = false,
465
491
  ): Promise<Fiber | null> => {
466
492
  if (!fiber) return null;
@@ -485,12 +511,12 @@ const traverseFiberAsync = async (
485
511
  * console.log(selfTime, totalTime);
486
512
  * ```
487
513
  */
488
- export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime: number } => {
514
+ export const getTimings = (fiber?: Fiber | null): FiberTimings => {
489
515
  const totalTime = fiber?.actualDuration ?? 0;
490
516
  let selfTime = totalTime;
491
517
  // TODO: calculate a DOM time, which is just host component summed up
492
518
  let child = fiber?.child ?? null;
493
- while (totalTime > 0 && child != null) {
519
+ while (totalTime > 0 && child !== null) {
494
520
  selfTime -= child.actualDuration ?? 0;
495
521
  child = child.sibling;
496
522
  }
@@ -501,48 +527,29 @@ export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime:
501
527
  * Returns `true` if the {@link Fiber} uses React Compiler's memo cache.
502
528
  */
503
529
  export const hasMemoCache = (fiber: Fiber): boolean => {
504
- return Boolean((fiber.updateQueue as unknown as { memoCache: unknown })?.memoCache);
530
+ return Boolean(fiber.updateQueue?.memoCache);
505
531
  };
506
532
 
507
- type FiberType =
508
- | React.ComponentType<unknown>
509
- | React.ForwardRefExoticComponent<unknown>
510
- | React.MemoExoticComponent<React.ComponentType<unknown>>;
511
-
512
533
  /**
513
534
  * Returns the type (e.g. component definition) of the {@link Fiber}
514
535
  */
515
536
  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;
537
+ if (isComponentType(type)) return type;
538
+ if (!isObjectRecord(type)) return null;
539
+ return getType(type.type ?? type.render);
528
540
  };
529
541
 
530
542
  /**
531
543
  * Returns the display name of the {@link Fiber} type.
532
544
  */
533
545
  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;
546
+ if (typeof type === "string") return type;
547
+ if ((typeof type !== "function" && typeof type !== "object") || type === null) return null;
548
+ const name = getTypeName(type);
542
549
  if (name) return name;
543
- const unwrappedType = getType(currentType);
550
+ const unwrappedType = getType(type);
544
551
  if (!unwrappedType) return null;
545
- return unwrappedType.displayName || unwrappedType.name || null;
552
+ return getTypeName(unwrappedType);
546
553
  };
547
554
 
548
555
  /**
@@ -550,7 +557,10 @@ export const getDisplayName = (type: unknown): null | string => {
550
557
  */
551
558
  export const detectReactBuildType = (renderer: ReactRenderer): "development" | "production" => {
552
559
  try {
553
- if (typeof renderer.version === "string" && renderer.bundleType > 0) {
560
+ if (
561
+ typeof renderer.version === "string" &&
562
+ renderer.bundleType === ReactBuildType.Development
563
+ ) {
554
564
  return "development";
555
565
  }
556
566
  } catch {}
@@ -562,11 +572,7 @@ export const detectReactBuildType = (renderer: ReactRenderer): "development" | "
562
572
  */
563
573
  export const isInstrumentationActive = (): boolean => {
564
574
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
565
- return (
566
- Boolean(rdtHook?._instrumentationIsActive) ||
567
- isRealReactDevtools(rdtHook) ||
568
- isReactRefresh(rdtHook)
569
- );
575
+ return Boolean(rdtHook?._instrumentationIsActive) || isRealReactDevtools(rdtHook);
570
576
  };
571
577
 
572
578
  export const _fiberRoots = new Set<FiberRoot>();
@@ -589,30 +595,25 @@ export const getLatestFiber = (fiber: Fiber): Fiber => {
589
595
  return fiber;
590
596
  };
591
597
 
592
- export type RenderHandler = <S>(fiber: Fiber, phase: RenderPhase, state?: S) => unknown;
593
-
594
598
  export type RenderPhase = "mount" | "unmount" | "update";
595
599
 
596
- let fiberId = 0;
600
+ let nextFiberId = 0;
597
601
  const fiberIdMap = new WeakMap<Fiber, number>();
598
602
 
599
- export const setFiberId = (fiber: Fiber, id: number = fiberId++): void => {
600
- fiberIdMap.set(fiber, id);
603
+ export const setFiberId = (fiber: Fiber, fiberId: number = nextFiberId++): void => {
604
+ fiberIdMap.set(fiber, fiberId);
601
605
  };
602
606
 
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
607
  export const getFiberId = (fiber: Fiber): number => {
607
- let id = fiberIdMap.get(fiber);
608
- if (id === undefined && fiber.alternate) {
609
- id = fiberIdMap.get(fiber.alternate);
608
+ let currentFiberId = fiberIdMap.get(fiber);
609
+ if (currentFiberId === undefined && fiber.alternate) {
610
+ currentFiberId = fiberIdMap.get(fiber.alternate);
610
611
  }
611
- if (id === undefined) {
612
- id = fiberId++;
613
- setFiberId(fiber, id);
612
+ if (currentFiberId === undefined) {
613
+ currentFiberId = nextFiberId++;
614
+ setFiberId(fiber, currentFiberId);
614
615
  }
615
- return id;
616
+ return currentFiberId;
616
617
  };
617
618
 
618
619
  const mountFiberRecursively = (
@@ -622,7 +623,7 @@ const mountFiberRecursively = (
622
623
  ): void => {
623
624
  let fiber: Fiber | null = firstChild;
624
625
 
625
- while (fiber != null) {
626
+ while (fiber !== null) {
626
627
  if (!fiberIdMap.has(fiber)) {
627
628
  getFiberId(fiber);
628
629
  }
@@ -631,7 +632,7 @@ const mountFiberRecursively = (
631
632
  onRender(fiber, "mount");
632
633
  }
633
634
 
634
- if (fiber.tag === SuspenseComponentTag) {
635
+ if (fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent) {
635
636
  const isTimedOut = fiber.memoizedState !== null;
636
637
  if (isTimedOut) {
637
638
  // Special case: if Suspense mounts in a timed-out state,
@@ -646,18 +647,12 @@ const mountFiberRecursively = (
646
647
  }
647
648
  }
648
649
  } 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
- }
650
+ const primaryChild = fiber.child?.child ?? null;
656
651
  if (primaryChild !== null) {
657
652
  mountFiberRecursively(onRender, primaryChild, false);
658
653
  }
659
654
  }
660
- } else if (fiber.child != null) {
655
+ } else if (fiber.child !== null) {
661
656
  mountFiberRecursively(onRender, fiber.child, true);
662
657
  }
663
658
  fiber = traverseSiblings ? fiber.sibling : null;
@@ -678,7 +673,7 @@ const updateFiberRecursively = (
678
673
  getFiberId(prevFiber);
679
674
  }
680
675
 
681
- const isSuspense = nextFiber.tag === SuspenseComponentTag;
676
+ const isSuspense = nextFiber.tag === getReactWorkTagsForFiber(nextFiber).SuspenseComponent;
682
677
 
683
678
  const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
684
679
  if (shouldIncludeInTree && didFiberRender(nextFiber)) {
@@ -765,7 +760,7 @@ const updateFiberRecursively = (
765
760
  };
766
761
 
767
762
  const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
768
- const isRoot = fiber.tag === HostRootTag;
763
+ const isRoot = fiber.tag === getReactWorkTagsForFiber(fiber).HostRoot;
769
764
 
770
765
  if (isRoot || !shouldFilterFiber(fiber)) {
771
766
  onRender(fiber, "unmount");
@@ -774,7 +769,8 @@ const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
774
769
 
775
770
  const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => {
776
771
  // We might meet a nested Suspense on our way.
777
- const isTimedOutSuspense = fiber.tag === SuspenseComponentTag && fiber.memoizedState !== null;
772
+ const isTimedOutSuspense =
773
+ fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent && fiber.memoizedState !== null;
778
774
  let child = fiber.child;
779
775
 
780
776
  if (isTimedOutSuspense) {
@@ -831,13 +827,15 @@ export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler)
831
827
  } else if (prevFiber !== null) {
832
828
  const wasMounted =
833
829
  prevFiber &&
834
- prevFiber.memoizedState != null &&
835
- prevFiber.memoizedState.element != null &&
830
+ prevFiber.memoizedState !== null &&
831
+ prevFiber.memoizedState.element !== null &&
832
+ prevFiber.memoizedState.element !== undefined &&
836
833
  // A dehydrated root is not considered mounted
837
834
  prevFiber.memoizedState.isDehydrated !== true;
838
835
  const isMounted =
839
- fiber.memoizedState != null &&
840
- fiber.memoizedState.element != null &&
836
+ fiber.memoizedState !== null &&
837
+ fiber.memoizedState.element !== null &&
838
+ fiber.memoizedState.element !== undefined &&
841
839
  // A dehydrated root is not considered mounted
842
840
  fiber.memoizedState.isDehydrated !== true;
843
841
 
@@ -859,16 +857,15 @@ const overrideRenderers = new Set<ReactRenderer>();
859
857
  let areOverrideRenderersWired = false;
860
858
 
861
859
  const wireOverrideRenderers = (): void => {
862
- if (areOverrideRenderersWired) return;
863
860
  if (!hasRDTHook()) return;
864
861
  const rdtHook = getRDTHook();
865
- if (!rdtHook?.renderers) return;
866
- areOverrideRenderersWired = true;
867
862
 
868
- ensureHookDispatchesToListeners(rdtHook);
863
+ setHookEventDispatchers(rdtHook);
869
864
  for (const renderer of rdtHook.renderers.values()) {
870
865
  overrideRenderers.add(renderer);
871
866
  }
867
+ if (areOverrideRenderersWired) return;
868
+ areOverrideRenderersWired = true;
872
869
  onRendererInject((renderer) => {
873
870
  overrideRenderers.add(renderer);
874
871
  });
@@ -882,12 +879,9 @@ const getRootRenderer = (fiber: Fiber): ReactRenderer | null => {
882
879
  }
883
880
  const rendererId = rootRendererIds.get(hostRootFiber.stateNode);
884
881
  if (rendererId === undefined) return null;
885
- return getRDTHook().renderers?.get(rendererId) ?? null;
882
+ return getRDTHook().renderers.get(rendererId) ?? null;
886
883
  };
887
884
 
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
885
  const resolveOverrideRenderers = (fiber: Fiber): ReactRenderer[] => {
892
886
  wireOverrideRenderers();
893
887
  const rootRenderer = getRootRenderer(fiber);
@@ -913,15 +907,14 @@ const getHookStateDispatch = (
913
907
  hookIndex: number,
914
908
  ): ((value: unknown) => void) | null => {
915
909
  let hookState = fiber.memoizedState;
916
- for (let i = 0; i < hookIndex; i++) {
910
+ for (let currentHookIndex = 0; currentHookIndex < hookIndex; currentHookIndex++) {
917
911
  if (!hookState?.next) return null;
918
912
  hookState = hookState.next;
919
913
  }
920
914
  const queue = hookState?.queue;
921
- if (isPOJO(queue) && typeof queue.dispatch === "function") {
922
- return queue.dispatch as (value: unknown) => void;
923
- }
924
- return null;
915
+ if (!isPOJO(queue)) return null;
916
+ const dispatch = queue.dispatch;
917
+ return typeof dispatch === "function" ? (value) => dispatch(value) : null;
925
918
  };
926
919
 
927
920
  const findContextProviderFiber = (fiber: Fiber, contextType: unknown): Fiber | null => {
@@ -947,12 +940,12 @@ const isPOJO = (maybePOJO: unknown): maybePOJO is Record<string, unknown> => {
947
940
  const buildPathsFromValue = (
948
941
  maybePOJO: Record<string, unknown>,
949
942
  basePath: string[] = [],
950
- ): Array<{ path: string[]; value: unknown }> => {
943
+ ): ValueWrite[] => {
951
944
  if (!isPOJO(maybePOJO)) {
952
945
  return [{ path: basePath, value: maybePOJO }];
953
946
  }
954
947
 
955
- const paths: Array<{ path: string[]; value: unknown }> = [];
948
+ const paths: ValueWrite[] = [];
956
949
 
957
950
  for (const key in maybePOJO) {
958
951
  const value = maybePOJO[key];
@@ -968,7 +961,7 @@ const buildPathsFromValue = (
968
961
  return paths;
969
962
  };
970
963
 
971
- const buildValueWrites = (partialValue: unknown): Array<{ path: string[]; value: unknown }> =>
964
+ const buildValueWrites = (partialValue: unknown): ValueWrite[] =>
972
965
  isPOJO(partialValue) ? buildPathsFromValue(partialValue) : [{ path: [], value: partialValue }];
973
966
 
974
967
  export const overrideProps = (fiber: Fiber, partialValue: Record<string, unknown>) => {
@@ -985,11 +978,10 @@ export const overrideHookState = (fiber: Fiber, id: number, partialValue: unknow
985
978
  const writes = buildValueWrites(partialValue);
986
979
 
987
980
  if (renderers.length > 0) {
988
- const hookId = String(id);
989
981
  for (const renderer of renderers) {
990
982
  for (const { path, value } of writes) {
991
983
  try {
992
- renderer.overrideHookState?.(fiber, hookId, path, value);
984
+ renderer.overrideHookState?.(fiber, id, path, value);
993
985
  } catch {}
994
986
  }
995
987
  }
@@ -1022,24 +1014,35 @@ export const overrideContext = (fiber: Fiber, contextType: unknown, partialValue
1022
1014
  export interface InstrumentationOptions {
1023
1015
  name?: string;
1024
1016
  onActive?: () => unknown;
1025
- onCommitFiberRoot?: (rendererID: number, root: FiberRoot, priority: number | void) => unknown;
1017
+ onCommitFiberRoot?: (
1018
+ rendererID: number,
1019
+ root: FiberRoot,
1020
+ priority: number | void,
1021
+ didError?: boolean,
1022
+ ) => unknown;
1026
1023
  onCommitFiberUnmount?: (rendererID: number, fiber: Fiber) => unknown;
1027
1024
  onPostCommitFiberRoot?: (rendererID: number, root: FiberRoot) => unknown;
1028
1025
  onScheduleFiberRoot?: (rendererID: number, root: FiberRoot, children: React.ReactNode) => unknown;
1029
1026
  }
1030
1027
 
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
- >();
1028
+ interface InstrumentationSubscription {
1029
+ options: InstrumentationOptions;
1030
+ }
1031
+
1032
+ const instrumentationSubscriptions = new Set<InstrumentationSubscription>();
1033
+
1034
+ const runInstrumentationCallback = (
1035
+ instrumentationName: string,
1036
+ callbackName: string,
1037
+ callback: () => unknown,
1038
+ ): void =>
1039
+ runWithBippyError(
1040
+ callback,
1041
+ (cause) => new BippyInstrumentationError(instrumentationName, callbackName, cause),
1042
+ );
1043
+
1044
+ const runReactDevToolsCallback = (callbackName: string, callback: () => unknown): void =>
1045
+ runWithBippyError(callback, (cause) => new BippyReactDevToolsError(callbackName, cause));
1043
1046
 
1044
1047
  interface HookDispatchers {
1045
1048
  onCommitFiberRoot: ReactDevToolsGlobalHook["onCommitFiberRoot"];
@@ -1049,6 +1052,7 @@ interface HookDispatchers {
1049
1052
  }
1050
1053
 
1051
1054
  const hookDispatchers = new WeakMap<ReactDevToolsGlobalHook, Partial<HookDispatchers>>();
1055
+ let didSubscribeToHookReplacements = false;
1052
1056
 
1053
1057
  const rootRendererIds = new WeakMap<FiberRoot, number>();
1054
1058
 
@@ -1057,7 +1061,7 @@ const rootRendererIds = new WeakMap<FiberRoot, number>();
1057
1061
  // next instrument() call installs a fresh wrapper over it; a superseded
1058
1062
  // wrapper still forwards the previous chain but skips the listeners so
1059
1063
  // they never fire twice.
1060
- const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
1064
+ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
1061
1065
  const dispatchers = hookDispatchers.get(rdtHook) ?? {};
1062
1066
  hookDispatchers.set(rdtHook, dispatchers);
1063
1067
 
@@ -1070,13 +1074,35 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1070
1074
  rendererID,
1071
1075
  root,
1072
1076
  priority,
1077
+ didError,
1073
1078
  ) => {
1074
- prevOnCommitFiberRoot?.(rendererID, root, priority);
1079
+ if (prevOnCommitFiberRoot) {
1080
+ runReactDevToolsCallback("onCommitFiberRoot", () =>
1081
+ prevOnCommitFiberRoot(rendererID, root, priority, didError),
1082
+ );
1083
+ }
1075
1084
  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);
1085
+ setReactWorkTagsForFiber(root.current, rdtHook.renderers.get(rendererID));
1086
+ const rootMemoizedState = root.current.memoizedState;
1087
+ const isUnmounting =
1088
+ rootMemoizedState === null ||
1089
+ rootMemoizedState.element === null ||
1090
+ rootMemoizedState.element === undefined;
1091
+ if (isUnmounting) {
1092
+ _fiberRoots.delete(root);
1093
+ rootRendererIds.delete(root);
1094
+ } else {
1095
+ _fiberRoots.add(root);
1096
+ rootRendererIds.set(root, rendererID);
1097
+ }
1098
+ for (const { options } of instrumentationSubscriptions) {
1099
+ if (options.onCommitFiberRoot) {
1100
+ runInstrumentationCallback(
1101
+ options.name ?? BIPPY_INSTRUMENTATION_STRING,
1102
+ "onCommitFiberRoot",
1103
+ () => options.onCommitFiberRoot?.(rendererID, root, priority, didError),
1104
+ );
1105
+ }
1080
1106
  }
1081
1107
  };
1082
1108
  dispatchers.onCommitFiberRoot = dispatchCommitFiberRoot;
@@ -1092,12 +1118,23 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1092
1118
  rendererID,
1093
1119
  fiber,
1094
1120
  ) => {
1095
- prevOnCommitFiberUnmount?.(rendererID, fiber);
1121
+ setReactWorkTagsForFiber(fiber, rdtHook.renderers.get(rendererID));
1122
+ if (prevOnCommitFiberUnmount) {
1123
+ runReactDevToolsCallback("onCommitFiberUnmount", () =>
1124
+ prevOnCommitFiberUnmount(rendererID, fiber),
1125
+ );
1126
+ }
1096
1127
  if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
1097
1128
  return;
1098
1129
  }
1099
- for (const listener of commitFiberUnmountListeners) {
1100
- listener(rendererID, fiber);
1130
+ for (const { options } of instrumentationSubscriptions) {
1131
+ if (options.onCommitFiberUnmount) {
1132
+ runInstrumentationCallback(
1133
+ options.name ?? BIPPY_INSTRUMENTATION_STRING,
1134
+ "onCommitFiberUnmount",
1135
+ () => options.onCommitFiberUnmount?.(rendererID, fiber),
1136
+ );
1137
+ }
1101
1138
  }
1102
1139
  };
1103
1140
  dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
@@ -1113,12 +1150,22 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1113
1150
  rendererID,
1114
1151
  root,
1115
1152
  ) => {
1116
- prevOnPostCommitFiberRoot?.(rendererID, root);
1153
+ if (prevOnPostCommitFiberRoot) {
1154
+ runReactDevToolsCallback("onPostCommitFiberRoot", () =>
1155
+ prevOnPostCommitFiberRoot(rendererID, root),
1156
+ );
1157
+ }
1117
1158
  if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
1118
1159
  return;
1119
1160
  }
1120
- for (const listener of postCommitFiberRootListeners) {
1121
- listener(rendererID, root);
1161
+ for (const { options } of instrumentationSubscriptions) {
1162
+ if (options.onPostCommitFiberRoot) {
1163
+ runInstrumentationCallback(
1164
+ options.name ?? BIPPY_INSTRUMENTATION_STRING,
1165
+ "onPostCommitFiberRoot",
1166
+ () => options.onPostCommitFiberRoot?.(rendererID, root),
1167
+ );
1168
+ }
1122
1169
  }
1123
1170
  };
1124
1171
  dispatchers.onPostCommitFiberRoot = dispatchPostCommitFiberRoot;
@@ -1135,10 +1182,20 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1135
1182
  root,
1136
1183
  children,
1137
1184
  ) => {
1138
- prevOnScheduleFiberRoot?.(rendererID, root, children);
1185
+ if (prevOnScheduleFiberRoot) {
1186
+ runReactDevToolsCallback("onScheduleFiberRoot", () =>
1187
+ prevOnScheduleFiberRoot(rendererID, root, children),
1188
+ );
1189
+ }
1139
1190
  if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
1140
- for (const listener of scheduleFiberRootListeners) {
1141
- listener(rendererID, root, children);
1191
+ for (const { options } of instrumentationSubscriptions) {
1192
+ if (options.onScheduleFiberRoot) {
1193
+ runInstrumentationCallback(
1194
+ options.name ?? BIPPY_INSTRUMENTATION_STRING,
1195
+ "onScheduleFiberRoot",
1196
+ () => options.onScheduleFiberRoot?.(rendererID, root, children),
1197
+ );
1198
+ }
1142
1199
  }
1143
1200
  };
1144
1201
  dispatchers.onScheduleFiberRoot = dispatchScheduleFiberRoot;
@@ -1165,36 +1222,35 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1165
1222
  * unsubscribe();
1166
1223
  */
1167
1224
  export const instrument = (options: InstrumentationOptions): Unsubscribe => {
1168
- const rdtHook = getRDTHook(options.onActive);
1225
+ const onActiveListener = options.onActive;
1226
+ const onActive = onActiveListener
1227
+ ? () =>
1228
+ runInstrumentationCallback(
1229
+ options.name ?? BIPPY_INSTRUMENTATION_STRING,
1230
+ "onActive",
1231
+ onActiveListener,
1232
+ )
1233
+ : undefined;
1234
+ const rdtHook = getRDTHook(onActive);
1235
+
1236
+ if (!didSubscribeToHookReplacements) {
1237
+ onRDTHookReplace(setHookEventDispatchers);
1238
+ didSubscribeToHookReplacements = true;
1239
+ }
1169
1240
 
1170
1241
  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
1171
1242
 
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);
1243
+ setHookEventDispatchers(rdtHook);
1244
+ const subscription: InstrumentationSubscription = { options };
1245
+ instrumentationSubscriptions.add(subscription);
1185
1246
 
1186
1247
  return toUnsubscribe(() => {
1187
1248
  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);
1249
+ instrumentationSubscriptions.delete(subscription);
1192
1250
  });
1193
1251
  };
1194
1252
 
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.
1253
+ // React uses per-renderer suffixes for host-instance Fiber keys, so discovered keys are cached.
1198
1254
  const knownFiberPropertyKeys = new Set<string>();
1199
1255
 
1200
1256
  const isFiberPropertyKey = (key: string): boolean =>
@@ -1202,6 +1258,32 @@ const isFiberPropertyKey = (key: string): boolean =>
1202
1258
  key.startsWith("__reactInternalInstance$") ||
1203
1259
  key.startsWith("__reactFiber");
1204
1260
 
1261
+ const getLegacyRootFiber = (hostInstance: Record<string, unknown>): Fiber | null => {
1262
+ const reactRootContainer = hostInstance._reactRootContainer;
1263
+ if (!isObjectRecord(reactRootContainer)) return null;
1264
+ const internalRoot = reactRootContainer._internalRoot;
1265
+ if (!isObjectRecord(internalRoot)) return null;
1266
+ const current = internalRoot.current;
1267
+ if (!isObjectRecord(current)) return null;
1268
+ const child = current.child;
1269
+ return isValidFiber(child) ? child : null;
1270
+ };
1271
+
1272
+ const getInternalInstanceHandle = (hostInstance: Record<string, unknown>): Fiber | null => {
1273
+ const internalInstanceHandle =
1274
+ hostInstance.__internalInstanceHandle ?? hostInstance._internalInstanceHandle;
1275
+ return isValidFiber(internalInstanceHandle) ? internalInstanceHandle : null;
1276
+ };
1277
+
1278
+ const getPublicHostInstance = (stateNode: unknown): unknown => {
1279
+ if (!isObjectRecord(stateNode)) return stateNode;
1280
+ const canonical = stateNode.canonical;
1281
+ if (isObjectRecord(canonical) && isObjectRecord(canonical.publicInstance)) {
1282
+ return canonical.publicInstance;
1283
+ }
1284
+ return typeof stateNode._nativeTag === "number" ? stateNode._nativeTag : stateNode;
1285
+ };
1286
+
1205
1287
  export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1206
1288
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1207
1289
  if (rdtHook?.renderers) {
@@ -1213,42 +1295,38 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1213
1295
  }
1214
1296
  }
1215
1297
 
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
- }
1298
+ if (isObjectRecord(hostInstance)) {
1299
+ const legacyRootFiber = getLegacyRootFiber(hostInstance);
1300
+ if (legacyRootFiber) return legacyRootFiber;
1221
1301
 
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
- }
1302
+ const internalInstanceHandle = getInternalInstanceHandle(hostInstance);
1303
+ if (internalInstanceHandle) return internalInstanceHandle;
1233
1304
 
1234
- const hostInstanceRecord = hostInstance as Record<string, unknown>;
1235
1305
  for (const knownKey of knownFiberPropertyKeys) {
1236
- const fiber = hostInstanceRecord[knownKey];
1237
- if (fiber) return fiber as Fiber;
1306
+ const fiber = hostInstance[knownKey];
1307
+ if (isValidFiber(fiber)) return fiber;
1238
1308
  }
1239
1309
 
1240
- for (const key of Object.keys(hostInstanceRecord)) {
1310
+ for (const key of Object.keys(hostInstance)) {
1241
1311
  if (isFiberPropertyKey(key)) {
1242
1312
  knownFiberPropertyKeys.add(key);
1243
- return (hostInstanceRecord[key] || null) as Fiber | null;
1313
+ const fiber = hostInstance[key];
1314
+ if (isValidFiber(fiber)) return fiber;
1244
1315
  }
1245
1316
  }
1317
+ }
1246
1318
 
1319
+ if (
1320
+ hostInstance !== null &&
1321
+ hostInstance !== undefined &&
1322
+ (typeof hostInstance === "object" || typeof hostInstance === "number")
1323
+ ) {
1247
1324
  for (const fiberRoot of _fiberRoots) {
1248
- if (getRootRenderer(fiberRoot.current)?.findFiberByHostInstance) continue;
1249
1325
  const fiber = traverseFiber(
1250
1326
  fiberRoot.current,
1251
- (candidateFiber) => candidateFiber.stateNode === hostInstance,
1327
+ (candidateFiber) =>
1328
+ isHostFiber(candidateFiber) &&
1329
+ getPublicHostInstance(candidateFiber.stateNode) === hostInstance,
1252
1330
  );
1253
1331
  if (fiber) return fiber;
1254
1332
  }
@@ -1256,6 +1334,18 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1256
1334
  return null;
1257
1335
  };
1258
1336
 
1259
- export * from "./rdt-hook.js";
1337
+ export {
1338
+ BIPPY_INSTRUMENTATION_STRING,
1339
+ _onActiveListeners,
1340
+ _renderers,
1341
+ getRDTHook,
1342
+ hasRDTHook,
1343
+ installRDTHook,
1344
+ isRealReactDevtools,
1345
+ onRendererInject,
1346
+ patchRDTHook,
1347
+ safelyInstallRDTHook,
1348
+ version,
1349
+ } from "./rdt-hook.js";
1260
1350
  export * from "./unsubscribe.js";
1261
1351
  export type * from "./types.js";