bippy 0.6.1-dev.61475ed → 0.6.1-dev.93556ef

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +51 -48
  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 +410 -0
  12. package/dist/errors.d.ts +410 -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 -5
  26. package/dist/source.d.cts +86 -69
  27. package/dist/source.d.ts +86 -69
  28. package/dist/source.js +14 -5
  29. package/package.json +22 -16
  30. package/src/core.ts +348 -344
  31. package/src/errors.ts +34 -0
  32. package/src/install-hook-only.ts +2 -2
  33. package/src/rdt-hook.ts +160 -161
  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 +80 -0
  37. package/src/react-internals/types.ts +186 -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 +44 -40
  41. package/src/source/get-source.ts +43 -26
  42. package/src/source/index.ts +11 -1
  43. package/src/source/inspect-hooks.ts +220 -207
  44. package/src/source/owner-stack.ts +119 -174
  45. package/src/source/parse-debug-stack.ts +4 -4
  46. package/src/source/parse-hook-names.ts +14 -53
  47. package/src/source/parse-stack.ts +14 -31
  48. package/src/source/renderer-dispatchers.ts +30 -0
  49. package/src/source/symbolication.ts +709 -120
  50. package/dist/get-source.cjs +0 -19
  51. package/dist/get-source.js +0 -19
  52. package/dist/react-refresh.cjs +0 -9
  53. package/dist/react-refresh.d.cts +0 -66
  54. package/dist/react-refresh.d.ts +0 -66
  55. package/dist/react-refresh.js +0 -9
  56. package/dist/unsubscribe.d.cts +0 -298
  57. package/dist/unsubscribe.d.ts +0 -298
  58. package/src/react-refresh/constants.ts +0 -9
  59. package/src/react-refresh/detect-hmr-transport.ts +0 -33
  60. package/src/react-refresh/index.ts +0 -173
  61. package/src/react-refresh/metro-hmr-transport.ts +0 -188
  62. package/src/react-refresh/next-webpack-hmr-transport.ts +0 -72
  63. package/src/react-refresh/normalize-hmr-file-path.ts +0 -24
  64. package/src/react-refresh/types.ts +0 -7
  65. package/src/react-refresh/vite-hmr-transport.ts +0 -116
  66. package/src/types.ts +0 -438
  67. package/src/unsubscribe.ts +0 -17
package/src/core.ts CHANGED
@@ -1,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,69 @@ 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 getTypeName = (value: object): string | null => {
49
+ const displayName = "displayName" in value ? value.displayName : null;
50
+ if (typeof displayName === "string" && displayName) return displayName;
51
+ const name = "name" in value ? value.name : null;
52
+ return typeof name === "string" && name ? name : null;
53
+ };
54
+
55
+ interface FiberSelector {
56
+ (node: Fiber): boolean | Promise<boolean | void> | void;
57
+ }
58
+
59
+ export interface FiberTimings {
60
+ selfTime: number;
61
+ totalTime: number;
62
+ }
63
+
64
+ export interface RenderHandler {
65
+ (fiber: Fiber, phase: RenderPhase): unknown;
66
+ }
67
+
68
+ interface ValueWrite {
69
+ path: string[];
70
+ value: unknown;
71
+ }
69
72
 
70
73
  /**
71
74
  * Returns `true` if object is a React Element.
@@ -74,19 +77,17 @@ const MutationMask =
74
77
  */
75
78
  export const isValidElement = (element: unknown): element is React.ReactElement =>
76
79
  typeof element === "object" &&
77
- element != null &&
80
+ element !== null &&
78
81
  "$$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
- );
82
+ (String(element.$$typeof) === ReactSymbols.LEGACY_ELEMENT_SYMBOL_STRING ||
83
+ String(element.$$typeof) === ReactSymbols.ELEMENT_SYMBOL_STRING);
83
84
 
84
85
  /**
85
86
  * Returns `true` if object is a React Fiber.
86
87
  */
87
88
  export const isValidFiber = (fiber: unknown): fiber is Fiber =>
88
89
  typeof fiber === "object" &&
89
- fiber != null &&
90
+ fiber !== null &&
90
91
  "tag" in fiber &&
91
92
  "stateNode" in fiber &&
92
93
  "return" in fiber &&
@@ -94,21 +95,27 @@ export const isValidFiber = (fiber: unknown): fiber is Fiber =>
94
95
  "sibling" in fiber &&
95
96
  "flags" in fiber;
96
97
 
98
+ const isFiberRoot = (fiberRoot: unknown): fiberRoot is FiberRoot =>
99
+ typeof fiberRoot === "object" &&
100
+ fiberRoot !== null &&
101
+ "current" in fiberRoot &&
102
+ isValidFiber(fiberRoot.current);
103
+
97
104
  /**
98
105
  * Returns `true` if fiber is a host fiber. Host fibers are DOM nodes in react-dom, `View` in react-native, etc.
99
106
  *
100
107
  * @see https://reactnative.dev/architecture/glossary#host-view-tree-and-host-view
101
108
  */
102
- export const isHostFiber = (fiber: Fiber): boolean => {
109
+ export const isHostFiber = (fiber: Fiber): fiber is HostFiber => {
110
+ const workTags = getReactWorkTagsForFiber(fiber);
103
111
  switch (fiber.tag) {
104
- case HostComponentTag:
105
- // @ts-expect-error: it exists
106
- case HostHoistableTag:
107
- // @ts-expect-error: it exists
108
- case HostSingletonTag:
112
+ case workTags.HostComponent:
113
+ case workTags.HostText:
114
+ case workTags.HostHoistable:
115
+ case workTags.HostSingleton:
109
116
  return true;
110
117
  default:
111
- return typeof fiber.type === "string";
118
+ return false;
112
119
  }
113
120
  };
114
121
 
@@ -118,12 +125,13 @@ export const isHostFiber = (fiber: Fiber): boolean => {
118
125
  * @see https://reactnative.dev/architecture/glossary#react-composite-components
119
126
  */
120
127
  export const isCompositeFiber = (fiber: Fiber): boolean => {
128
+ const workTags = getReactWorkTagsForFiber(fiber);
121
129
  switch (fiber.tag) {
122
- case ClassComponentTag:
123
- case ForwardRefTag:
124
- case FunctionComponentTag:
125
- case MemoComponentTag:
126
- case SimpleMemoComponentTag:
130
+ case workTags.ClassComponent:
131
+ case workTags.ForwardRef:
132
+ case workTags.FunctionComponent:
133
+ case workTags.MemoComponent:
134
+ case workTags.SimpleMemoComponent:
127
135
  return true;
128
136
  default:
129
137
  return false;
@@ -230,16 +238,17 @@ export const traverseProps = (
230
238
  export const didFiberRender = (fiber: Fiber): boolean => {
231
239
  const nextProps = fiber.memoizedProps;
232
240
  const prevProps = fiber.alternate?.memoizedProps || {};
233
- const flags = fiber.flags ?? (fiber as unknown as { effectTag: number }).effectTag ?? 0;
241
+ const flags = fiber.flags ?? fiber.effectTag ?? 0;
242
+ const workTags = getReactWorkTagsForFiber(fiber);
234
243
 
235
244
  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;
245
+ case workTags.ClassComponent:
246
+ case workTags.ContextConsumer:
247
+ case workTags.ForwardRef:
248
+ case workTags.FunctionComponent:
249
+ case workTags.MemoComponent:
250
+ case workTags.SimpleMemoComponent: {
251
+ return (flags & ReactFiberFlags.PerformedWork) === ReactFiberFlags.PerformedWork;
243
252
  }
244
253
  default:
245
254
  // Host nodes (DOM, root, etc.)
@@ -257,8 +266,8 @@ export const didFiberRender = (fiber: Fiber): boolean => {
257
266
  */
258
267
  export const didFiberCommit = (fiber: Fiber): boolean => {
259
268
  return Boolean(
260
- (fiber.flags & (MutationMask | Cloned)) !== 0 ||
261
- (fiber.subtreeFlags & (MutationMask | Cloned)) !== 0,
269
+ (fiber.flags & (MutationMask | ReactFiberFlags.Cloned)) !== 0 ||
270
+ (fiber.subtreeFlags & (MutationMask | ReactFiberFlags.Cloned)) !== 0,
262
271
  );
263
272
  };
264
273
 
@@ -297,7 +306,7 @@ export const getFiberStack = (fiber: Fiber): Fiber[] => {
297
306
  let currentFiber = fiber;
298
307
  while (currentFiber.return) {
299
308
  stack.push(currentFiber);
300
- currentFiber = currentFiber.return as Fiber;
309
+ currentFiber = currentFiber.return;
301
310
  }
302
311
  return stack;
303
312
  };
@@ -306,8 +315,9 @@ export const getFiberStack = (fiber: Fiber): Fiber[] => {
306
315
  * Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
307
316
  */
308
317
  const shouldFilterFiber = (fiber: Fiber): boolean => {
318
+ const workTags = getReactWorkTagsForFiber(fiber);
309
319
  switch (fiber.tag) {
310
- case DehydratedSuspenseComponentTag:
320
+ case workTags.DehydratedSuspenseComponent:
311
321
  // TODO: ideally we would show dehydrated Suspense immediately.
312
322
  // However, it has some special behavior (like disconnecting
313
323
  // an alternate and turning into real Suspense) which breaks DevTools.
@@ -315,13 +325,13 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
315
325
  // https://github.com/bvaughn/react-devtools-experimental/issues/197
316
326
  return true;
317
327
 
318
- case FragmentTag:
319
- case HostTextTag:
320
- case LegacyHiddenComponentTag:
321
- case OffscreenComponentTag:
328
+ case workTags.Fragment:
329
+ case workTags.HostText:
330
+ case workTags.LegacyHiddenComponent:
331
+ case workTags.OffscreenComponent:
322
332
  return true;
323
333
 
324
- case HostRootTag:
334
+ case workTags.HostRoot:
325
335
  // It is never valid to filter the root element.
326
336
  return false;
327
337
 
@@ -331,15 +341,15 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
331
341
 
332
342
  if (typeof symbolOrNumber === "symbol") {
333
343
  return (
334
- symbolOrNumber.description === CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
335
- symbolOrNumber.description === DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
344
+ symbolOrNumber.description === ReactSymbols.CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
345
+ symbolOrNumber.description === ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
336
346
  );
337
347
  }
338
348
 
339
349
  switch (symbolOrNumber) {
340
- case CONCURRENT_MODE_NUMBER:
341
- case CONCURRENT_MODE_SYMBOL_STRING:
342
- case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
350
+ case ReactSymbols.CONCURRENT_MODE_NUMBER:
351
+ case ReactSymbols.CONCURRENT_MODE_SYMBOL_STRING:
352
+ case ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
343
353
  return true;
344
354
 
345
355
  default:
@@ -405,75 +415,57 @@ export function traverseFiber(
405
415
  ): Promise<Fiber | null>;
406
416
  export function traverseFiber(
407
417
  fiber: Fiber | null,
408
- selector: (node: Fiber) => boolean | Promise<boolean | void> | void,
418
+ selector: FiberSelector,
419
+ ascending?: boolean,
420
+ ): Fiber | null | Promise<Fiber | null>;
421
+ export function traverseFiber(
422
+ fiber: Fiber | null,
423
+ selector: FiberSelector,
409
424
  ascending = false,
410
425
  ): Fiber | null | Promise<Fiber | null> {
411
426
  if (!fiber) return null;
412
427
 
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
- })();
428
+ const selection = selector(fiber);
429
+ if (isPromiseLike<boolean | void>(selection)) {
430
+ return Promise.resolve(selection).then((didSelectFiber) =>
431
+ didSelectFiber === true ? fiber : traverseFiberChildren(fiber, selector, ascending),
432
+ );
430
433
  }
434
+ if (selection === true) return fiber;
431
435
 
432
- if (firstResult === true) return fiber;
433
-
434
- let child = ascending ? fiber.return : fiber.child;
435
- while (child) {
436
- const match = traverseFiberSync(child, selector as (node: Fiber) => boolean | void, ascending);
437
- if (match) return match;
438
- child = ascending ? null : child.sibling;
439
- }
440
- return null;
436
+ return traverseFiberChildren(fiber, selector, ascending);
441
437
  }
442
438
 
443
- const traverseFiberSync = (
444
- fiber: Fiber | null,
445
- selector: (node: Fiber) => boolean | void,
446
- ascending = false,
447
- ): Fiber | null => {
448
- if (!fiber) return null;
449
- if (selector(fiber) === true) return fiber;
450
-
451
- let child = ascending ? fiber.return : fiber.child;
452
- while (child) {
453
- const match = traverseFiberSync(child, selector, ascending);
454
- if (match) return match;
439
+ const isPromiseLike = <Result>(value: unknown): value is PromiseLike<Result> =>
440
+ (typeof value === "object" || typeof value === "function") &&
441
+ value !== null &&
442
+ "then" in value &&
443
+ typeof value.then === "function";
455
444
 
456
- child = ascending ? null : child.sibling;
457
- }
458
- return null;
445
+ const traverseFiberChildren = (
446
+ fiber: Fiber,
447
+ selector: FiberSelector,
448
+ ascending: boolean,
449
+ ): Fiber | null | Promise<Fiber | null> => {
450
+ const firstChild = ascending ? fiber.return : fiber.child;
451
+ return traverseFiberSiblings(firstChild, selector, ascending);
459
452
  };
460
453
 
461
- const traverseFiberAsync = async (
454
+ const traverseFiberSiblings = (
462
455
  fiber: Fiber | null,
463
- selector: (node: Fiber) => Promise<boolean | void>,
464
- ascending = false,
465
- ): Promise<Fiber | null> => {
456
+ selector: FiberSelector,
457
+ ascending: boolean,
458
+ ): Fiber | null | Promise<Fiber | null> => {
466
459
  if (!fiber) return null;
467
- if ((await selector(fiber)) === true) return fiber;
468
460
 
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;
461
+ const nextSibling = ascending ? null : fiber.sibling;
462
+ const match = traverseFiber(fiber, selector, ascending);
463
+ if (isPromiseLike<Fiber | null>(match)) {
464
+ return Promise.resolve(match).then(
465
+ (resolvedMatch) => resolvedMatch ?? traverseFiberSiblings(nextSibling, selector, ascending),
466
+ );
475
467
  }
476
- return null;
468
+ return match ?? traverseFiberSiblings(nextSibling, selector, ascending);
477
469
  };
478
470
 
479
471
  /**
@@ -485,12 +477,12 @@ const traverseFiberAsync = async (
485
477
  * console.log(selfTime, totalTime);
486
478
  * ```
487
479
  */
488
- export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime: number } => {
480
+ export const getTimings = (fiber?: Fiber | null): FiberTimings => {
489
481
  const totalTime = fiber?.actualDuration ?? 0;
490
482
  let selfTime = totalTime;
491
483
  // TODO: calculate a DOM time, which is just host component summed up
492
484
  let child = fiber?.child ?? null;
493
- while (totalTime > 0 && child != null) {
485
+ while (totalTime > 0 && child !== null) {
494
486
  selfTime -= child.actualDuration ?? 0;
495
487
  child = child.sibling;
496
488
  }
@@ -501,48 +493,29 @@ export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime:
501
493
  * Returns `true` if the {@link Fiber} uses React Compiler's memo cache.
502
494
  */
503
495
  export const hasMemoCache = (fiber: Fiber): boolean => {
504
- return Boolean((fiber.updateQueue as unknown as { memoCache: unknown })?.memoCache);
496
+ return Boolean(fiber.updateQueue?.memoCache);
505
497
  };
506
498
 
507
- type FiberType =
508
- | React.ComponentType<unknown>
509
- | React.ForwardRefExoticComponent<unknown>
510
- | React.MemoExoticComponent<React.ComponentType<unknown>>;
511
-
512
499
  /**
513
500
  * Returns the type (e.g. component definition) of the {@link Fiber}
514
501
  */
515
502
  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;
503
+ if (isComponentType(type)) return type;
504
+ if (typeof type !== "object" || type === null) return null;
505
+ return getType(Reflect.get(type, "type") ?? Reflect.get(type, "render"));
528
506
  };
529
507
 
530
508
  /**
531
509
  * Returns the display name of the {@link Fiber} type.
532
510
  */
533
511
  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;
512
+ if (typeof type === "string") return type;
513
+ if ((typeof type !== "function" && typeof type !== "object") || type === null) return null;
514
+ const name = getTypeName(type);
542
515
  if (name) return name;
543
- const unwrappedType = getType(currentType);
516
+ const unwrappedType = getType(type);
544
517
  if (!unwrappedType) return null;
545
- return unwrappedType.displayName || unwrappedType.name || null;
518
+ return getTypeName(unwrappedType);
546
519
  };
547
520
 
548
521
  /**
@@ -550,7 +523,10 @@ export const getDisplayName = (type: unknown): null | string => {
550
523
  */
551
524
  export const detectReactBuildType = (renderer: ReactRenderer): "development" | "production" => {
552
525
  try {
553
- if (typeof renderer.version === "string" && renderer.bundleType > 0) {
526
+ if (
527
+ typeof renderer.version === "string" &&
528
+ renderer.bundleType === ReactBuildType.Development
529
+ ) {
554
530
  return "development";
555
531
  }
556
532
  } catch {}
@@ -562,11 +538,7 @@ export const detectReactBuildType = (renderer: ReactRenderer): "development" | "
562
538
  */
563
539
  export const isInstrumentationActive = (): boolean => {
564
540
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
565
- return (
566
- Boolean(rdtHook?._instrumentationIsActive) ||
567
- isRealReactDevtools(rdtHook) ||
568
- isReactRefresh(rdtHook)
569
- );
541
+ return Boolean(rdtHook?._instrumentationIsActive) || isRealReactDevtools(rdtHook);
570
542
  };
571
543
 
572
544
  export const _fiberRoots = new Set<FiberRoot>();
@@ -582,37 +554,35 @@ export const getLatestFiber = (fiber: Fiber): Fiber => {
582
554
  }
583
555
  for (const root of _fiberRoots) {
584
556
  const latestFiber = traverseFiber(root.current, (innerFiber) => {
585
- if (innerFiber === fiber) return true;
557
+ if (innerFiber === fiber || innerFiber === alternate) return true;
586
558
  });
587
559
  if (latestFiber) return latestFiber;
588
560
  }
589
561
  return fiber;
590
562
  };
591
563
 
592
- export type RenderHandler = <S>(fiber: Fiber, phase: RenderPhase, state?: S) => unknown;
593
-
594
564
  export type RenderPhase = "mount" | "unmount" | "update";
595
565
 
596
- let fiberId = 0;
566
+ let nextFiberId = 0;
597
567
  const fiberIdMap = new WeakMap<Fiber, number>();
598
568
 
599
- export const setFiberId = (fiber: Fiber, id: number = fiberId++): void => {
600
- fiberIdMap.set(fiber, id);
569
+ export const setFiberId = (fiber: Fiber, fiberId: number = nextFiberId++): void => {
570
+ fiberIdMap.set(fiber, fiberId);
571
+ if (Number.isSafeInteger(fiberId) && fiberId >= nextFiberId) {
572
+ nextFiberId = fiberId + 1;
573
+ }
601
574
  };
602
575
 
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
576
  export const getFiberId = (fiber: Fiber): number => {
607
- let id = fiberIdMap.get(fiber);
608
- if (id === undefined && fiber.alternate) {
609
- id = fiberIdMap.get(fiber.alternate);
577
+ let currentFiberId = fiberIdMap.get(fiber);
578
+ if (currentFiberId === undefined && fiber.alternate) {
579
+ currentFiberId = fiberIdMap.get(fiber.alternate);
610
580
  }
611
- if (id === undefined) {
612
- id = fiberId++;
613
- setFiberId(fiber, id);
581
+ if (currentFiberId === undefined) {
582
+ currentFiberId = nextFiberId++;
583
+ setFiberId(fiber, currentFiberId);
614
584
  }
615
- return id;
585
+ return currentFiberId;
616
586
  };
617
587
 
618
588
  const mountFiberRecursively = (
@@ -622,16 +592,14 @@ const mountFiberRecursively = (
622
592
  ): void => {
623
593
  let fiber: Fiber | null = firstChild;
624
594
 
625
- while (fiber != null) {
626
- if (!fiberIdMap.has(fiber)) {
627
- getFiberId(fiber);
628
- }
595
+ while (fiber !== null) {
596
+ getFiberId(fiber);
629
597
  const shouldIncludeInTree = !shouldFilterFiber(fiber);
630
598
  if (shouldIncludeInTree && didFiberRender(fiber)) {
631
599
  onRender(fiber, "mount");
632
600
  }
633
601
 
634
- if (fiber.tag === SuspenseComponentTag) {
602
+ if (fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent) {
635
603
  const isTimedOut = fiber.memoizedState !== null;
636
604
  if (isTimedOut) {
637
605
  // Special case: if Suspense mounts in a timed-out state,
@@ -646,18 +614,12 @@ const mountFiberRecursively = (
646
614
  }
647
615
  }
648
616
  } 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
- }
617
+ const primaryChild = fiber.child?.child ?? null;
656
618
  if (primaryChild !== null) {
657
619
  mountFiberRecursively(onRender, primaryChild, false);
658
620
  }
659
621
  }
660
- } else if (fiber.child != null) {
622
+ } else if (fiber.child !== null) {
661
623
  mountFiberRecursively(onRender, fiber.child, true);
662
624
  }
663
625
  fiber = traverseSiblings ? fiber.sibling : null;
@@ -667,18 +629,13 @@ const mountFiberRecursively = (
667
629
  const updateFiberRecursively = (
668
630
  onRender: RenderHandler,
669
631
  nextFiber: Fiber,
670
- prevFiber: Fiber,
671
- parentFiber: Fiber | null,
632
+ prevFiber: Fiber | null,
672
633
  ): void => {
673
- if (!fiberIdMap.has(nextFiber)) {
674
- getFiberId(nextFiber);
675
- }
634
+ getFiberId(nextFiber);
676
635
  if (!prevFiber) return;
677
- if (!fiberIdMap.has(prevFiber)) {
678
- getFiberId(prevFiber);
679
- }
636
+ getFiberId(prevFiber);
680
637
 
681
- const isSuspense = nextFiber.tag === SuspenseComponentTag;
638
+ const isSuspense = nextFiber.tag === getReactWorkTagsForFiber(nextFiber).SuspenseComponent;
682
639
 
683
640
  const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
684
641
  if (shouldIncludeInTree && didFiberRender(nextFiber)) {
@@ -707,7 +664,7 @@ const updateFiberRecursively = (
707
664
  const prevFallbackChildSet = prevFiber.child?.sibling ?? null;
708
665
 
709
666
  if (nextFallbackChildSet !== null && prevFallbackChildSet !== null) {
710
- updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet, nextFiber);
667
+ updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet);
711
668
  }
712
669
  } else if (prevDidTimeout && !nextDidTimeOut) {
713
670
  // Fallback -> Primary:
@@ -746,14 +703,7 @@ const updateFiberRecursively = (
746
703
  // Schedule updates and mounts depending on whether alternates exist.
747
704
  // We don't track deletions here because they are reported separately.
748
705
  if (nextChild.alternate) {
749
- const prevChild = nextChild.alternate;
750
-
751
- updateFiberRecursively(
752
- onRender,
753
- nextChild,
754
- prevChild,
755
- shouldIncludeInTree ? nextFiber : parentFiber,
756
- );
706
+ updateFiberRecursively(onRender, nextChild, nextChild.alternate);
757
707
  } else {
758
708
  mountFiberRecursively(onRender, nextChild, false);
759
709
  }
@@ -765,7 +715,7 @@ const updateFiberRecursively = (
765
715
  };
766
716
 
767
717
  const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
768
- const isRoot = fiber.tag === HostRootTag;
718
+ const isRoot = fiber.tag === getReactWorkTagsForFiber(fiber).HostRoot;
769
719
 
770
720
  if (isRoot || !shouldFilterFiber(fiber)) {
771
721
  onRender(fiber, "unmount");
@@ -774,7 +724,8 @@ const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
774
724
 
775
725
  const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => {
776
726
  // We might meet a nested Suspense on our way.
777
- const isTimedOutSuspense = fiber.tag === SuspenseComponentTag && fiber.memoizedState !== null;
727
+ const isTimedOutSuspense =
728
+ fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent && fiber.memoizedState !== null;
778
729
  let child = fiber.child;
779
730
 
780
731
  if (isTimedOutSuspense) {
@@ -798,11 +749,9 @@ const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber):
798
749
  }
799
750
  };
800
751
 
801
- let commitId = 0;
802
752
  const rootInstanceMap = new WeakMap<
803
- FiberRoot,
753
+ Fiber | FiberRoot,
804
754
  {
805
- id: number;
806
755
  prevFiber: Fiber | null;
807
756
  }
808
757
  >();
@@ -814,37 +763,39 @@ const rootInstanceMap = new WeakMap<
814
763
  * console.log(phase)
815
764
  * })
816
765
  */
817
- export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler): void => {
766
+ export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: RenderHandler): void => {
818
767
  const fiber = "current" in root ? root.current : root;
819
768
 
820
769
  let rootInstance = rootInstanceMap.get(root);
821
770
 
822
771
  if (!rootInstance) {
823
- rootInstance = { id: commitId++, prevFiber: null };
772
+ rootInstance = { prevFiber: null };
824
773
  rootInstanceMap.set(root, rootInstance);
825
774
  }
826
775
 
827
776
  const { prevFiber } = rootInstance;
828
- // if fiberRoot don't have current instance, means it's been unmounted
829
777
  if (!fiber) {
830
- unmountFiber(onRender, fiber);
778
+ if (prevFiber) {
779
+ unmountFiber(onRender, prevFiber);
780
+ }
831
781
  } else if (prevFiber !== null) {
832
782
  const wasMounted =
833
- prevFiber &&
834
- prevFiber.memoizedState != null &&
835
- prevFiber.memoizedState.element != null &&
783
+ prevFiber.memoizedState !== null &&
784
+ prevFiber.memoizedState.element !== null &&
785
+ prevFiber.memoizedState.element !== undefined &&
836
786
  // A dehydrated root is not considered mounted
837
787
  prevFiber.memoizedState.isDehydrated !== true;
838
788
  const isMounted =
839
- fiber.memoizedState != null &&
840
- fiber.memoizedState.element != null &&
789
+ fiber.memoizedState !== null &&
790
+ fiber.memoizedState.element !== null &&
791
+ fiber.memoizedState.element !== undefined &&
841
792
  // A dehydrated root is not considered mounted
842
793
  fiber.memoizedState.isDehydrated !== true;
843
794
 
844
795
  if (!wasMounted && isMounted) {
845
796
  mountFiberRecursively(onRender, fiber, false);
846
797
  } else if (wasMounted && isMounted) {
847
- updateFiberRecursively(onRender, fiber, fiber.alternate, null);
798
+ updateFiberRecursively(onRender, fiber, fiber.alternate);
848
799
  } else if (wasMounted && !isMounted) {
849
800
  unmountFiber(onRender, fiber);
850
801
  }
@@ -859,16 +810,15 @@ const overrideRenderers = new Set<ReactRenderer>();
859
810
  let areOverrideRenderersWired = false;
860
811
 
861
812
  const wireOverrideRenderers = (): void => {
862
- if (areOverrideRenderersWired) return;
863
813
  if (!hasRDTHook()) return;
864
814
  const rdtHook = getRDTHook();
865
- if (!rdtHook?.renderers) return;
866
- areOverrideRenderersWired = true;
867
815
 
868
- ensureHookDispatchesToListeners(rdtHook);
816
+ setHookEventDispatchers(rdtHook);
869
817
  for (const renderer of rdtHook.renderers.values()) {
870
818
  overrideRenderers.add(renderer);
871
819
  }
820
+ if (areOverrideRenderersWired) return;
821
+ areOverrideRenderersWired = true;
872
822
  onRendererInject((renderer) => {
873
823
  overrideRenderers.add(renderer);
874
824
  });
@@ -880,14 +830,13 @@ const getRootRenderer = (fiber: Fiber): ReactRenderer | null => {
880
830
  while (hostRootFiber.return) {
881
831
  hostRootFiber = hostRootFiber.return;
882
832
  }
883
- const rendererId = rootRendererIds.get(hostRootFiber.stateNode);
833
+ const fiberRoot = hostRootFiber.stateNode;
834
+ if (!isFiberRoot(fiberRoot)) return null;
835
+ const rendererId = rootRendererIds.get(fiberRoot);
884
836
  if (rendererId === undefined) return null;
885
- return getRDTHook().renderers?.get(rendererId) ?? null;
837
+ return getRDTHook().renderers.get(rendererId) ?? null;
886
838
  };
887
839
 
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
840
  const resolveOverrideRenderers = (fiber: Fiber): ReactRenderer[] => {
892
841
  wireOverrideRenderers();
893
842
  const rootRenderer = getRootRenderer(fiber);
@@ -913,15 +862,14 @@ const getHookStateDispatch = (
913
862
  hookIndex: number,
914
863
  ): ((value: unknown) => void) | null => {
915
864
  let hookState = fiber.memoizedState;
916
- for (let i = 0; i < hookIndex; i++) {
865
+ for (let currentHookIndex = 0; currentHookIndex < hookIndex; currentHookIndex++) {
917
866
  if (!hookState?.next) return null;
918
867
  hookState = hookState.next;
919
868
  }
920
869
  const queue = hookState?.queue;
921
- if (isPOJO(queue) && typeof queue.dispatch === "function") {
922
- return queue.dispatch as (value: unknown) => void;
923
- }
924
- return null;
870
+ if (!isPOJO(queue)) return null;
871
+ const dispatch = queue.dispatch;
872
+ return typeof dispatch === "function" ? (value) => dispatch(value) : null;
925
873
  };
926
874
 
927
875
  const findContextProviderFiber = (fiber: Fiber, contextType: unknown): Fiber | null => {
@@ -947,28 +895,30 @@ const isPOJO = (maybePOJO: unknown): maybePOJO is Record<string, unknown> => {
947
895
  const buildPathsFromValue = (
948
896
  maybePOJO: Record<string, unknown>,
949
897
  basePath: string[] = [],
950
- ): Array<{ path: string[]; value: unknown }> => {
951
- if (!isPOJO(maybePOJO)) {
898
+ ancestors = new WeakSet<object>(),
899
+ ): ValueWrite[] => {
900
+ if (ancestors.has(maybePOJO)) {
952
901
  return [{ path: basePath, value: maybePOJO }];
953
902
  }
954
903
 
955
- const paths: Array<{ path: string[]; value: unknown }> = [];
904
+ ancestors.add(maybePOJO);
905
+ const paths: ValueWrite[] = [];
956
906
 
957
- for (const key in maybePOJO) {
958
- const value = maybePOJO[key];
907
+ for (const [key, value] of Object.entries(maybePOJO)) {
959
908
  const path = basePath.concat(key);
960
909
 
961
910
  if (isPOJO(value)) {
962
- paths.push(...buildPathsFromValue(value, path));
911
+ paths.push(...buildPathsFromValue(value, path, ancestors));
963
912
  } else {
964
913
  paths.push({ path, value });
965
914
  }
966
915
  }
967
916
 
917
+ ancestors.delete(maybePOJO);
968
918
  return paths;
969
919
  };
970
920
 
971
- const buildValueWrites = (partialValue: unknown): Array<{ path: string[]; value: unknown }> =>
921
+ const buildValueWrites = (partialValue: unknown): ValueWrite[] =>
972
922
  isPOJO(partialValue) ? buildPathsFromValue(partialValue) : [{ path: [], value: partialValue }];
973
923
 
974
924
  export const overrideProps = (fiber: Fiber, partialValue: Record<string, unknown>) => {
@@ -982,14 +932,13 @@ export const overrideHookState = (fiber: Fiber, id: number, partialValue: unknow
982
932
  const renderers = resolveOverrideRenderers(fiber).filter((renderer) =>
983
933
  Boolean(renderer.overrideHookState),
984
934
  );
985
- const writes = buildValueWrites(partialValue);
986
935
 
987
936
  if (renderers.length > 0) {
988
- const hookId = String(id);
937
+ const writes = buildValueWrites(partialValue);
989
938
  for (const renderer of renderers) {
990
939
  for (const { path, value } of writes) {
991
940
  try {
992
- renderer.overrideHookState?.(fiber, hookId, path, value);
941
+ renderer.overrideHookState?.(fiber, id, path, value);
993
942
  } catch {}
994
943
  }
995
944
  }
@@ -1022,24 +971,22 @@ export const overrideContext = (fiber: Fiber, contextType: unknown, partialValue
1022
971
  export interface InstrumentationOptions {
1023
972
  name?: string;
1024
973
  onActive?: () => unknown;
1025
- onCommitFiberRoot?: (rendererID: number, root: FiberRoot, priority: number | void) => unknown;
974
+ onCommitFiberRoot?: (
975
+ rendererID: number,
976
+ root: FiberRoot,
977
+ priority: number | void,
978
+ didError?: boolean,
979
+ ) => unknown;
1026
980
  onCommitFiberUnmount?: (rendererID: number, fiber: Fiber) => unknown;
1027
981
  onPostCommitFiberRoot?: (rendererID: number, root: FiberRoot) => unknown;
1028
982
  onScheduleFiberRoot?: (rendererID: number, root: FiberRoot, children: React.ReactNode) => unknown;
1029
983
  }
1030
984
 
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
- >();
985
+ interface InstrumentationSubscription {
986
+ options: InstrumentationOptions;
987
+ }
988
+
989
+ const instrumentationSubscriptions = new Set<InstrumentationSubscription>();
1043
990
 
1044
991
  interface HookDispatchers {
1045
992
  onCommitFiberRoot: ReactDevToolsGlobalHook["onCommitFiberRoot"];
@@ -1049,6 +996,7 @@ interface HookDispatchers {
1049
996
  }
1050
997
 
1051
998
  const hookDispatchers = new WeakMap<ReactDevToolsGlobalHook, Partial<HookDispatchers>>();
999
+ let didSubscribeToHookReplacements = false;
1052
1000
 
1053
1001
  const rootRendererIds = new WeakMap<FiberRoot, number>();
1054
1002
 
@@ -1057,7 +1005,7 @@ const rootRendererIds = new WeakMap<FiberRoot, number>();
1057
1005
  // next instrument() call installs a fresh wrapper over it; a superseded
1058
1006
  // wrapper still forwards the previous chain but skips the listeners so
1059
1007
  // they never fire twice.
1060
- const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
1008
+ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
1061
1009
  const dispatchers = hookDispatchers.get(rdtHook) ?? {};
1062
1010
  hookDispatchers.set(rdtHook, dispatchers);
1063
1011
 
@@ -1070,13 +1018,29 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1070
1018
  rendererID,
1071
1019
  root,
1072
1020
  priority,
1021
+ didError,
1073
1022
  ) => {
1074
- prevOnCommitFiberRoot?.(rendererID, root, priority);
1023
+ if (prevOnCommitFiberRoot) {
1024
+ prevOnCommitFiberRoot.call(rdtHook, rendererID, root, priority, didError);
1025
+ }
1075
1026
  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);
1027
+ setReactWorkTagsForFiber(root.current, rdtHook.renderers.get(rendererID));
1028
+ const rootMemoizedState = root.current.memoizedState;
1029
+ const isUnmounting =
1030
+ rootMemoizedState === null ||
1031
+ rootMemoizedState.element === null ||
1032
+ rootMemoizedState.element === undefined;
1033
+ if (isUnmounting) {
1034
+ _fiberRoots.delete(root);
1035
+ rootRendererIds.delete(root);
1036
+ } else {
1037
+ _fiberRoots.add(root);
1038
+ rootRendererIds.set(root, rendererID);
1039
+ }
1040
+ for (const { options } of instrumentationSubscriptions) {
1041
+ if (options.onCommitFiberRoot) {
1042
+ options.onCommitFiberRoot(rendererID, root, priority, didError);
1043
+ }
1080
1044
  }
1081
1045
  };
1082
1046
  dispatchers.onCommitFiberRoot = dispatchCommitFiberRoot;
@@ -1092,12 +1056,17 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1092
1056
  rendererID,
1093
1057
  fiber,
1094
1058
  ) => {
1095
- prevOnCommitFiberUnmount?.(rendererID, fiber);
1059
+ setReactWorkTagsForFiber(fiber, rdtHook.renderers.get(rendererID));
1060
+ if (prevOnCommitFiberUnmount) {
1061
+ prevOnCommitFiberUnmount.call(rdtHook, rendererID, fiber);
1062
+ }
1096
1063
  if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
1097
1064
  return;
1098
1065
  }
1099
- for (const listener of commitFiberUnmountListeners) {
1100
- listener(rendererID, fiber);
1066
+ for (const { options } of instrumentationSubscriptions) {
1067
+ if (options.onCommitFiberUnmount) {
1068
+ options.onCommitFiberUnmount(rendererID, fiber);
1069
+ }
1101
1070
  }
1102
1071
  };
1103
1072
  dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
@@ -1113,12 +1082,16 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1113
1082
  rendererID,
1114
1083
  root,
1115
1084
  ) => {
1116
- prevOnPostCommitFiberRoot?.(rendererID, root);
1085
+ if (prevOnPostCommitFiberRoot) {
1086
+ prevOnPostCommitFiberRoot.call(rdtHook, rendererID, root);
1087
+ }
1117
1088
  if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
1118
1089
  return;
1119
1090
  }
1120
- for (const listener of postCommitFiberRootListeners) {
1121
- listener(rendererID, root);
1091
+ for (const { options } of instrumentationSubscriptions) {
1092
+ if (options.onPostCommitFiberRoot) {
1093
+ options.onPostCommitFiberRoot(rendererID, root);
1094
+ }
1122
1095
  }
1123
1096
  };
1124
1097
  dispatchers.onPostCommitFiberRoot = dispatchPostCommitFiberRoot;
@@ -1135,10 +1108,14 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1135
1108
  root,
1136
1109
  children,
1137
1110
  ) => {
1138
- prevOnScheduleFiberRoot?.(rendererID, root, children);
1111
+ if (prevOnScheduleFiberRoot) {
1112
+ prevOnScheduleFiberRoot.call(rdtHook, rendererID, root, children);
1113
+ }
1139
1114
  if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
1140
- for (const listener of scheduleFiberRootListeners) {
1141
- listener(rendererID, root, children);
1115
+ for (const { options } of instrumentationSubscriptions) {
1116
+ if (options.onScheduleFiberRoot) {
1117
+ options.onScheduleFiberRoot(rendererID, root, children);
1118
+ }
1142
1119
  }
1143
1120
  };
1144
1121
  dispatchers.onScheduleFiberRoot = dispatchScheduleFiberRoot;
@@ -1151,8 +1128,7 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1151
1128
  * dispatches to a set of listeners, so multiple `instrument` calls compose
1152
1129
  * without stacking patches. Returns an unsubscribe function that removes
1153
1130
  * exactly the handlers this call registered.
1154
- * The returned function is also a `Disposable`, so it composes with other
1155
- * bippy subscriptions through `using`.
1131
+ * The unsubscribe function can also be disposed through `using`.
1156
1132
  * @example
1157
1133
  * const unsubscribe = instrument({
1158
1134
  * onActive() {
@@ -1167,34 +1143,24 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1167
1143
  export const instrument = (options: InstrumentationOptions): Unsubscribe => {
1168
1144
  const rdtHook = getRDTHook(options.onActive);
1169
1145
 
1146
+ if (!didSubscribeToHookReplacements) {
1147
+ onRDTHookReplace(setHookEventDispatchers);
1148
+ didSubscribeToHookReplacements = true;
1149
+ }
1150
+
1170
1151
  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
1171
1152
 
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);
1153
+ setHookEventDispatchers(rdtHook);
1154
+ const subscription: InstrumentationSubscription = { options };
1155
+ instrumentationSubscriptions.add(subscription);
1156
+
1157
+ return createUnsubscribe(() => {
1158
+ if (options.onActive) _onActiveListeners.delete(options.onActive);
1159
+ instrumentationSubscriptions.delete(subscription);
1192
1160
  });
1193
1161
  };
1194
1162
 
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.
1163
+ // React uses per-renderer suffixes for host-instance Fiber keys, so discovered keys are cached.
1198
1164
  const knownFiberPropertyKeys = new Set<string>();
1199
1165
 
1200
1166
  const isFiberPropertyKey = (key: string): boolean =>
@@ -1202,6 +1168,37 @@ const isFiberPropertyKey = (key: string): boolean =>
1202
1168
  key.startsWith("__reactInternalInstance$") ||
1203
1169
  key.startsWith("__reactFiber");
1204
1170
 
1171
+ const getLegacyRootFiber = (hostInstance: object): Fiber | null => {
1172
+ const reactRootContainer = Reflect.get(hostInstance, "_reactRootContainer");
1173
+ if (typeof reactRootContainer !== "object" || reactRootContainer === null) return null;
1174
+ const internalRoot = Reflect.get(reactRootContainer, "_internalRoot");
1175
+ if (typeof internalRoot !== "object" || internalRoot === null) return null;
1176
+ const current = Reflect.get(internalRoot, "current");
1177
+ if (typeof current !== "object" || current === null) return null;
1178
+ const child = Reflect.get(current, "child");
1179
+ return isValidFiber(child) ? child : null;
1180
+ };
1181
+
1182
+ const getInternalInstanceHandle = (hostInstance: object): Fiber | null => {
1183
+ const internalInstanceHandle =
1184
+ Reflect.get(hostInstance, "__internalInstanceHandle") ??
1185
+ Reflect.get(hostInstance, "_internalInstanceHandle");
1186
+ return isValidFiber(internalInstanceHandle) ? internalInstanceHandle : null;
1187
+ };
1188
+
1189
+ const getPublicHostInstance = (stateNode: unknown): unknown => {
1190
+ if (typeof stateNode !== "object" || stateNode === null) return stateNode;
1191
+ const canonical = Reflect.get(stateNode, "canonical");
1192
+ if (typeof canonical === "object" && canonical !== null) {
1193
+ const publicInstance = Reflect.get(canonical, "publicInstance");
1194
+ if (typeof publicInstance === "object" && publicInstance !== null) {
1195
+ return publicInstance;
1196
+ }
1197
+ }
1198
+ const nativeTag = Reflect.get(stateNode, "_nativeTag");
1199
+ return typeof nativeTag === "number" ? nativeTag : stateNode;
1200
+ };
1201
+
1205
1202
  export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1206
1203
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1207
1204
  if (rdtHook?.renderers) {
@@ -1213,42 +1210,38 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1213
1210
  }
1214
1211
  }
1215
1212
 
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
- }
1213
+ if (typeof hostInstance === "object" && hostInstance !== null) {
1214
+ const legacyRootFiber = getLegacyRootFiber(hostInstance);
1215
+ if (legacyRootFiber) return legacyRootFiber;
1221
1216
 
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
- }
1217
+ const internalInstanceHandle = getInternalInstanceHandle(hostInstance);
1218
+ if (internalInstanceHandle) return internalInstanceHandle;
1233
1219
 
1234
- const hostInstanceRecord = hostInstance as Record<string, unknown>;
1235
1220
  for (const knownKey of knownFiberPropertyKeys) {
1236
- const fiber = hostInstanceRecord[knownKey];
1237
- if (fiber) return fiber as Fiber;
1221
+ const fiber = Reflect.get(hostInstance, knownKey);
1222
+ if (isValidFiber(fiber)) return fiber;
1238
1223
  }
1239
1224
 
1240
- for (const key of Object.keys(hostInstanceRecord)) {
1225
+ for (const key of Object.keys(hostInstance)) {
1241
1226
  if (isFiberPropertyKey(key)) {
1242
1227
  knownFiberPropertyKeys.add(key);
1243
- return (hostInstanceRecord[key] || null) as Fiber | null;
1228
+ const fiber = Reflect.get(hostInstance, key);
1229
+ if (isValidFiber(fiber)) return fiber;
1244
1230
  }
1245
1231
  }
1232
+ }
1246
1233
 
1234
+ if (
1235
+ hostInstance !== null &&
1236
+ hostInstance !== undefined &&
1237
+ (typeof hostInstance === "object" || typeof hostInstance === "number")
1238
+ ) {
1247
1239
  for (const fiberRoot of _fiberRoots) {
1248
- if (getRootRenderer(fiberRoot.current)?.findFiberByHostInstance) continue;
1249
1240
  const fiber = traverseFiber(
1250
1241
  fiberRoot.current,
1251
- (candidateFiber) => candidateFiber.stateNode === hostInstance,
1242
+ (candidateFiber) =>
1243
+ isHostFiber(candidateFiber) &&
1244
+ getPublicHostInstance(candidateFiber.stateNode) === hostInstance,
1252
1245
  );
1253
1246
  if (fiber) return fiber;
1254
1247
  }
@@ -1256,6 +1249,17 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1256
1249
  return null;
1257
1250
  };
1258
1251
 
1259
- export * from "./rdt-hook.js";
1260
- export * from "./unsubscribe.js";
1261
- export type * from "./types.js";
1252
+ export {
1253
+ BIPPY_INSTRUMENTATION_STRING,
1254
+ _onActiveListeners,
1255
+ _renderers,
1256
+ getRDTHook,
1257
+ hasRDTHook,
1258
+ installRDTHook,
1259
+ isRealReactDevtools,
1260
+ onRendererInject,
1261
+ patchRDTHook,
1262
+ version,
1263
+ } from "./rdt-hook.js";
1264
+ export type { Unsubscribe } from "./rdt-hook.js";
1265
+ export type * from "./react-internals/index.js";