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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -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 +420 -0
  12. package/dist/errors.d.ts +420 -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 +27 -11
  27. package/dist/source.d.ts +27 -11
  28. package/dist/source.js +14 -5
  29. package/package.json +22 -16
  30. package/src/core.ts +349 -328
  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 +197 -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 +32 -25
  41. package/src/source/get-source.ts +5 -5
  42. package/src/source/index.ts +9 -1
  43. package/src/source/inspect-hooks.ts +215 -182
  44. package/src/source/owner-stack.ts +127 -166
  45. package/src/source/parse-debug-stack.ts +4 -4
  46. package/src/source/parse-hook-names.ts +1 -1
  47. package/src/source/parse-stack.ts +1 -3
  48. package/src/source/symbolication.ts +490 -90
  49. package/dist/get-source.cjs +0 -19
  50. package/dist/get-source.js +0 -19
  51. package/dist/react-refresh.cjs +0 -9
  52. package/dist/react-refresh.d.cts +0 -66
  53. package/dist/react-refresh.d.ts +0 -66
  54. package/dist/react-refresh.js +0 -9
  55. package/dist/unsubscribe.d.cts +0 -298
  56. package/dist/unsubscribe.d.ts +0 -298
  57. package/src/react-refresh/constants.ts +0 -9
  58. package/src/react-refresh/detect-hmr-transport.ts +0 -33
  59. package/src/react-refresh/index.ts +0 -173
  60. package/src/react-refresh/metro-hmr-transport.ts +0 -188
  61. package/src/react-refresh/next-webpack-hmr-transport.ts +0 -72
  62. package/src/react-refresh/normalize-hmr-file-path.ts +0 -24
  63. package/src/react-refresh/types.ts +0 -7
  64. package/src/react-refresh/vite-hmr-transport.ts +0 -116
  65. package/src/types.ts +0 -438
  66. package/src/unsubscribe.ts +0 -17
package/src/core.ts CHANGED
@@ -1,5 +1,4 @@
1
- // [!!!] IMPORTANT: do not import React in this file
2
- // since it will be executed before the react devtools hook is created
1
+ // React must remain a type-only import because this module loads immediately after the DevTools hook.
3
2
 
4
3
  import type * as React from "react";
5
4
 
@@ -7,65 +6,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,8 @@ 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
+ const parentFiber = currentFiber.return;
310
+ currentFiber = parentFiber;
301
311
  }
302
312
  return stack;
303
313
  };
@@ -306,8 +316,9 @@ export const getFiberStack = (fiber: Fiber): Fiber[] => {
306
316
  * Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
307
317
  */
308
318
  const shouldFilterFiber = (fiber: Fiber): boolean => {
319
+ const workTags = getReactWorkTagsForFiber(fiber);
309
320
  switch (fiber.tag) {
310
- case DehydratedSuspenseComponentTag:
321
+ case workTags.DehydratedSuspenseComponent:
311
322
  // TODO: ideally we would show dehydrated Suspense immediately.
312
323
  // However, it has some special behavior (like disconnecting
313
324
  // an alternate and turning into real Suspense) which breaks DevTools.
@@ -315,13 +326,13 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
315
326
  // https://github.com/bvaughn/react-devtools-experimental/issues/197
316
327
  return true;
317
328
 
318
- case FragmentTag:
319
- case HostTextTag:
320
- case LegacyHiddenComponentTag:
321
- case OffscreenComponentTag:
329
+ case workTags.Fragment:
330
+ case workTags.HostText:
331
+ case workTags.LegacyHiddenComponent:
332
+ case workTags.OffscreenComponent:
322
333
  return true;
323
334
 
324
- case HostRootTag:
335
+ case workTags.HostRoot:
325
336
  // It is never valid to filter the root element.
326
337
  return false;
327
338
 
@@ -331,15 +342,15 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
331
342
 
332
343
  if (typeof symbolOrNumber === "symbol") {
333
344
  return (
334
- symbolOrNumber.description === CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
335
- symbolOrNumber.description === DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
345
+ symbolOrNumber.description === ReactSymbols.CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
346
+ symbolOrNumber.description === ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
336
347
  );
337
348
  }
338
349
 
339
350
  switch (symbolOrNumber) {
340
- case CONCURRENT_MODE_NUMBER:
341
- case CONCURRENT_MODE_SYMBOL_STRING:
342
- case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
351
+ case ReactSymbols.CONCURRENT_MODE_NUMBER:
352
+ case ReactSymbols.CONCURRENT_MODE_SYMBOL_STRING:
353
+ case ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
343
354
  return true;
344
355
 
345
356
  default:
@@ -405,75 +416,65 @@ export function traverseFiber(
405
416
  ): Promise<Fiber | null>;
406
417
  export function traverseFiber(
407
418
  fiber: Fiber | null,
408
- selector: (node: Fiber) => boolean | Promise<boolean | void> | void,
419
+ selector: FiberSelector,
420
+ ascending?: boolean,
421
+ ): Fiber | null | Promise<Fiber | null>;
422
+ export function traverseFiber(
423
+ fiber: Fiber | null,
424
+ selector: FiberSelector,
409
425
  ascending = false,
410
426
  ): Fiber | null | Promise<Fiber | null> {
411
- if (!fiber) return null;
412
-
413
- const firstResult = selector(fiber);
414
- if (firstResult instanceof Promise) {
415
- return (async () => {
416
- if ((await firstResult) === true) return fiber;
417
-
418
- let child = ascending ? fiber.return : fiber.child;
419
- while (child) {
420
- const match = await traverseFiberAsync(
421
- child,
422
- selector as (node: Fiber) => Promise<boolean | void>,
423
- ascending,
424
- );
425
- if (match) return match;
426
- child = ascending ? null : child.sibling;
427
- }
428
- return null;
429
- })();
430
- }
427
+ return traverseFiberInternal(fiber, selector, ascending);
428
+ }
431
429
 
432
- if (firstResult === true) return fiber;
430
+ const isPromiseLike = <Result>(value: unknown): value is PromiseLike<Result> =>
431
+ (typeof value === "object" || typeof value === "function") &&
432
+ value !== null &&
433
+ "then" in value &&
434
+ typeof value.then === "function";
433
435
 
434
- let child = ascending ? fiber.return : fiber.child;
435
- while (child) {
436
- const match = traverseFiberSync(child, selector as (node: Fiber) => boolean | void, ascending);
437
- if (match) return match;
438
- child = ascending ? null : child.sibling;
439
- }
440
- return null;
441
- }
436
+ const traverseFiberChildren = (
437
+ fiber: Fiber,
438
+ selector: FiberSelector,
439
+ ascending: boolean,
440
+ ): Fiber | null | Promise<Fiber | null> => {
441
+ const firstChild = ascending ? fiber.return : fiber.child;
442
+ return traverseFiberSiblings(firstChild, selector, ascending);
443
+ };
442
444
 
443
- const traverseFiberSync = (
445
+ const traverseFiberSiblings = (
444
446
  fiber: Fiber | null,
445
- selector: (node: Fiber) => boolean | void,
446
- ascending = false,
447
- ): Fiber | null => {
447
+ selector: FiberSelector,
448
+ ascending: boolean,
449
+ ): Fiber | null | Promise<Fiber | null> => {
448
450
  if (!fiber) return null;
449
- if (selector(fiber) === true) return fiber;
450
451
 
451
- let child = ascending ? fiber.return : fiber.child;
452
- while (child) {
453
- const match = traverseFiberSync(child, selector, ascending);
454
- if (match) return match;
455
-
456
- child = ascending ? null : child.sibling;
452
+ const nextSibling = ascending ? null : fiber.sibling;
453
+ const match = traverseFiberInternal(fiber, selector, ascending);
454
+ if (isPromiseLike<Fiber | null>(match)) {
455
+ return Promise.resolve(match).then(
456
+ (resolvedMatch) => resolvedMatch ?? traverseFiberSiblings(nextSibling, selector, ascending),
457
+ );
457
458
  }
458
- return null;
459
+ return match ?? traverseFiberSiblings(nextSibling, selector, ascending);
459
460
  };
460
461
 
461
- const traverseFiberAsync = async (
462
+ const traverseFiberInternal = (
462
463
  fiber: Fiber | null,
463
- selector: (node: Fiber) => Promise<boolean | void>,
464
- ascending = false,
465
- ): Promise<Fiber | null> => {
464
+ selector: FiberSelector,
465
+ ascending: boolean,
466
+ ): Fiber | null | Promise<Fiber | null> => {
466
467
  if (!fiber) return null;
467
- if ((await selector(fiber)) === true) return fiber;
468
-
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
468
 
474
- child = ascending ? null : child.sibling;
469
+ const selection = selector(fiber);
470
+ if (isPromiseLike<boolean | void>(selection)) {
471
+ return Promise.resolve(selection).then((didSelectFiber) =>
472
+ didSelectFiber === true ? fiber : traverseFiberChildren(fiber, selector, ascending),
473
+ );
475
474
  }
476
- return null;
475
+ if (selection === true) return fiber;
476
+
477
+ return traverseFiberChildren(fiber, selector, ascending);
477
478
  };
478
479
 
479
480
  /**
@@ -485,12 +486,12 @@ const traverseFiberAsync = async (
485
486
  * console.log(selfTime, totalTime);
486
487
  * ```
487
488
  */
488
- export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime: number } => {
489
+ export const getTimings = (fiber?: Fiber | null): FiberTimings => {
489
490
  const totalTime = fiber?.actualDuration ?? 0;
490
491
  let selfTime = totalTime;
491
492
  // TODO: calculate a DOM time, which is just host component summed up
492
493
  let child = fiber?.child ?? null;
493
- while (totalTime > 0 && child != null) {
494
+ while (totalTime > 0 && child !== null) {
494
495
  selfTime -= child.actualDuration ?? 0;
495
496
  child = child.sibling;
496
497
  }
@@ -501,48 +502,29 @@ export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime:
501
502
  * Returns `true` if the {@link Fiber} uses React Compiler's memo cache.
502
503
  */
503
504
  export const hasMemoCache = (fiber: Fiber): boolean => {
504
- return Boolean((fiber.updateQueue as unknown as { memoCache: unknown })?.memoCache);
505
+ return Boolean(fiber.updateQueue?.memoCache);
505
506
  };
506
507
 
507
- type FiberType =
508
- | React.ComponentType<unknown>
509
- | React.ForwardRefExoticComponent<unknown>
510
- | React.MemoExoticComponent<React.ComponentType<unknown>>;
511
-
512
508
  /**
513
509
  * Returns the type (e.g. component definition) of the {@link Fiber}
514
510
  */
515
511
  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;
512
+ if (isComponentType(type)) return type;
513
+ if (typeof type !== "object" || type === null) return null;
514
+ return getType(Reflect.get(type, "type") ?? Reflect.get(type, "render"));
528
515
  };
529
516
 
530
517
  /**
531
518
  * Returns the display name of the {@link Fiber} type.
532
519
  */
533
520
  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;
521
+ if (typeof type === "string") return type;
522
+ if ((typeof type !== "function" && typeof type !== "object") || type === null) return null;
523
+ const name = getTypeName(type);
542
524
  if (name) return name;
543
- const unwrappedType = getType(currentType);
525
+ const unwrappedType = getType(type);
544
526
  if (!unwrappedType) return null;
545
- return unwrappedType.displayName || unwrappedType.name || null;
527
+ return getTypeName(unwrappedType);
546
528
  };
547
529
 
548
530
  /**
@@ -550,7 +532,10 @@ export const getDisplayName = (type: unknown): null | string => {
550
532
  */
551
533
  export const detectReactBuildType = (renderer: ReactRenderer): "development" | "production" => {
552
534
  try {
553
- if (typeof renderer.version === "string" && renderer.bundleType > 0) {
535
+ if (
536
+ typeof renderer.version === "string" &&
537
+ renderer.bundleType === ReactBuildType.Development
538
+ ) {
554
539
  return "development";
555
540
  }
556
541
  } catch {}
@@ -562,11 +547,7 @@ export const detectReactBuildType = (renderer: ReactRenderer): "development" | "
562
547
  */
563
548
  export const isInstrumentationActive = (): boolean => {
564
549
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
565
- return (
566
- Boolean(rdtHook?._instrumentationIsActive) ||
567
- isRealReactDevtools(rdtHook) ||
568
- isReactRefresh(rdtHook)
569
- );
550
+ return Boolean(rdtHook?._instrumentationIsActive) || isRealReactDevtools(rdtHook);
570
551
  };
571
552
 
572
553
  export const _fiberRoots = new Set<FiberRoot>();
@@ -582,37 +563,35 @@ export const getLatestFiber = (fiber: Fiber): Fiber => {
582
563
  }
583
564
  for (const root of _fiberRoots) {
584
565
  const latestFiber = traverseFiber(root.current, (innerFiber) => {
585
- if (innerFiber === fiber) return true;
566
+ if (innerFiber === fiber || innerFiber === alternate) return true;
586
567
  });
587
568
  if (latestFiber) return latestFiber;
588
569
  }
589
570
  return fiber;
590
571
  };
591
572
 
592
- export type RenderHandler = <S>(fiber: Fiber, phase: RenderPhase, state?: S) => unknown;
593
-
594
573
  export type RenderPhase = "mount" | "unmount" | "update";
595
574
 
596
- let fiberId = 0;
575
+ let nextFiberId = 0;
597
576
  const fiberIdMap = new WeakMap<Fiber, number>();
598
577
 
599
- export const setFiberId = (fiber: Fiber, id: number = fiberId++): void => {
600
- fiberIdMap.set(fiber, id);
578
+ export const setFiberId = (fiber: Fiber, fiberId: number = nextFiberId++): void => {
579
+ fiberIdMap.set(fiber, fiberId);
580
+ if (Number.isSafeInteger(fiberId) && fiberId >= nextFiberId) {
581
+ nextFiberId = fiberId + 1;
582
+ }
601
583
  };
602
584
 
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
585
  export const getFiberId = (fiber: Fiber): number => {
607
- let id = fiberIdMap.get(fiber);
608
- if (id === undefined && fiber.alternate) {
609
- id = fiberIdMap.get(fiber.alternate);
586
+ let currentFiberId = fiberIdMap.get(fiber);
587
+ if (currentFiberId === undefined && fiber.alternate) {
588
+ currentFiberId = fiberIdMap.get(fiber.alternate);
610
589
  }
611
- if (id === undefined) {
612
- id = fiberId++;
613
- setFiberId(fiber, id);
590
+ if (currentFiberId === undefined) {
591
+ currentFiberId = nextFiberId++;
592
+ setFiberId(fiber, currentFiberId);
614
593
  }
615
- return id;
594
+ return currentFiberId;
616
595
  };
617
596
 
618
597
  const mountFiberRecursively = (
@@ -622,7 +601,7 @@ const mountFiberRecursively = (
622
601
  ): void => {
623
602
  let fiber: Fiber | null = firstChild;
624
603
 
625
- while (fiber != null) {
604
+ while (fiber !== null) {
626
605
  if (!fiberIdMap.has(fiber)) {
627
606
  getFiberId(fiber);
628
607
  }
@@ -631,7 +610,7 @@ const mountFiberRecursively = (
631
610
  onRender(fiber, "mount");
632
611
  }
633
612
 
634
- if (fiber.tag === SuspenseComponentTag) {
613
+ if (fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent) {
635
614
  const isTimedOut = fiber.memoizedState !== null;
636
615
  if (isTimedOut) {
637
616
  // Special case: if Suspense mounts in a timed-out state,
@@ -646,18 +625,12 @@ const mountFiberRecursively = (
646
625
  }
647
626
  }
648
627
  } 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
- }
628
+ const primaryChild = fiber.child?.child ?? null;
656
629
  if (primaryChild !== null) {
657
630
  mountFiberRecursively(onRender, primaryChild, false);
658
631
  }
659
632
  }
660
- } else if (fiber.child != null) {
633
+ } else if (fiber.child !== null) {
661
634
  mountFiberRecursively(onRender, fiber.child, true);
662
635
  }
663
636
  fiber = traverseSiblings ? fiber.sibling : null;
@@ -667,8 +640,7 @@ const mountFiberRecursively = (
667
640
  const updateFiberRecursively = (
668
641
  onRender: RenderHandler,
669
642
  nextFiber: Fiber,
670
- prevFiber: Fiber,
671
- parentFiber: Fiber | null,
643
+ prevFiber: Fiber | null,
672
644
  ): void => {
673
645
  if (!fiberIdMap.has(nextFiber)) {
674
646
  getFiberId(nextFiber);
@@ -678,7 +650,7 @@ const updateFiberRecursively = (
678
650
  getFiberId(prevFiber);
679
651
  }
680
652
 
681
- const isSuspense = nextFiber.tag === SuspenseComponentTag;
653
+ const isSuspense = nextFiber.tag === getReactWorkTagsForFiber(nextFiber).SuspenseComponent;
682
654
 
683
655
  const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
684
656
  if (shouldIncludeInTree && didFiberRender(nextFiber)) {
@@ -707,7 +679,7 @@ const updateFiberRecursively = (
707
679
  const prevFallbackChildSet = prevFiber.child?.sibling ?? null;
708
680
 
709
681
  if (nextFallbackChildSet !== null && prevFallbackChildSet !== null) {
710
- updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet, nextFiber);
682
+ updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet);
711
683
  }
712
684
  } else if (prevDidTimeout && !nextDidTimeOut) {
713
685
  // Fallback -> Primary:
@@ -746,14 +718,7 @@ const updateFiberRecursively = (
746
718
  // Schedule updates and mounts depending on whether alternates exist.
747
719
  // We don't track deletions here because they are reported separately.
748
720
  if (nextChild.alternate) {
749
- const prevChild = nextChild.alternate;
750
-
751
- updateFiberRecursively(
752
- onRender,
753
- nextChild,
754
- prevChild,
755
- shouldIncludeInTree ? nextFiber : parentFiber,
756
- );
721
+ updateFiberRecursively(onRender, nextChild, nextChild.alternate);
757
722
  } else {
758
723
  mountFiberRecursively(onRender, nextChild, false);
759
724
  }
@@ -765,7 +730,7 @@ const updateFiberRecursively = (
765
730
  };
766
731
 
767
732
  const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
768
- const isRoot = fiber.tag === HostRootTag;
733
+ const isRoot = fiber.tag === getReactWorkTagsForFiber(fiber).HostRoot;
769
734
 
770
735
  if (isRoot || !shouldFilterFiber(fiber)) {
771
736
  onRender(fiber, "unmount");
@@ -774,7 +739,8 @@ const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
774
739
 
775
740
  const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => {
776
741
  // We might meet a nested Suspense on our way.
777
- const isTimedOutSuspense = fiber.tag === SuspenseComponentTag && fiber.memoizedState !== null;
742
+ const isTimedOutSuspense =
743
+ fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent && fiber.memoizedState !== null;
778
744
  let child = fiber.child;
779
745
 
780
746
  if (isTimedOutSuspense) {
@@ -800,7 +766,7 @@ const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber):
800
766
 
801
767
  let commitId = 0;
802
768
  const rootInstanceMap = new WeakMap<
803
- FiberRoot,
769
+ Fiber | FiberRoot,
804
770
  {
805
771
  id: number;
806
772
  prevFiber: Fiber | null;
@@ -814,7 +780,7 @@ const rootInstanceMap = new WeakMap<
814
780
  * console.log(phase)
815
781
  * })
816
782
  */
817
- export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler): void => {
783
+ export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: RenderHandler): void => {
818
784
  const fiber = "current" in root ? root.current : root;
819
785
 
820
786
  let rootInstance = rootInstanceMap.get(root);
@@ -831,20 +797,22 @@ export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler)
831
797
  } else if (prevFiber !== null) {
832
798
  const wasMounted =
833
799
  prevFiber &&
834
- prevFiber.memoizedState != null &&
835
- prevFiber.memoizedState.element != null &&
800
+ prevFiber.memoizedState !== null &&
801
+ prevFiber.memoizedState.element !== null &&
802
+ prevFiber.memoizedState.element !== undefined &&
836
803
  // A dehydrated root is not considered mounted
837
804
  prevFiber.memoizedState.isDehydrated !== true;
838
805
  const isMounted =
839
- fiber.memoizedState != null &&
840
- fiber.memoizedState.element != null &&
806
+ fiber.memoizedState !== null &&
807
+ fiber.memoizedState.element !== null &&
808
+ fiber.memoizedState.element !== undefined &&
841
809
  // A dehydrated root is not considered mounted
842
810
  fiber.memoizedState.isDehydrated !== true;
843
811
 
844
812
  if (!wasMounted && isMounted) {
845
813
  mountFiberRecursively(onRender, fiber, false);
846
814
  } else if (wasMounted && isMounted) {
847
- updateFiberRecursively(onRender, fiber, fiber.alternate, null);
815
+ updateFiberRecursively(onRender, fiber, fiber.alternate);
848
816
  } else if (wasMounted && !isMounted) {
849
817
  unmountFiber(onRender, fiber);
850
818
  }
@@ -859,16 +827,15 @@ const overrideRenderers = new Set<ReactRenderer>();
859
827
  let areOverrideRenderersWired = false;
860
828
 
861
829
  const wireOverrideRenderers = (): void => {
862
- if (areOverrideRenderersWired) return;
863
830
  if (!hasRDTHook()) return;
864
831
  const rdtHook = getRDTHook();
865
- if (!rdtHook?.renderers) return;
866
- areOverrideRenderersWired = true;
867
832
 
868
- ensureHookDispatchesToListeners(rdtHook);
833
+ setHookEventDispatchers(rdtHook);
869
834
  for (const renderer of rdtHook.renderers.values()) {
870
835
  overrideRenderers.add(renderer);
871
836
  }
837
+ if (areOverrideRenderersWired) return;
838
+ areOverrideRenderersWired = true;
872
839
  onRendererInject((renderer) => {
873
840
  overrideRenderers.add(renderer);
874
841
  });
@@ -880,14 +847,13 @@ const getRootRenderer = (fiber: Fiber): ReactRenderer | null => {
880
847
  while (hostRootFiber.return) {
881
848
  hostRootFiber = hostRootFiber.return;
882
849
  }
883
- const rendererId = rootRendererIds.get(hostRootFiber.stateNode);
850
+ const fiberRoot = hostRootFiber.stateNode;
851
+ if (!isFiberRoot(fiberRoot)) return null;
852
+ const rendererId = rootRendererIds.get(fiberRoot);
884
853
  if (rendererId === undefined) return null;
885
- return getRDTHook().renderers?.get(rendererId) ?? null;
854
+ return getRDTHook().renderers.get(rendererId) ?? null;
886
855
  };
887
856
 
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
857
  const resolveOverrideRenderers = (fiber: Fiber): ReactRenderer[] => {
892
858
  wireOverrideRenderers();
893
859
  const rootRenderer = getRootRenderer(fiber);
@@ -913,15 +879,14 @@ const getHookStateDispatch = (
913
879
  hookIndex: number,
914
880
  ): ((value: unknown) => void) | null => {
915
881
  let hookState = fiber.memoizedState;
916
- for (let i = 0; i < hookIndex; i++) {
882
+ for (let currentHookIndex = 0; currentHookIndex < hookIndex; currentHookIndex++) {
917
883
  if (!hookState?.next) return null;
918
884
  hookState = hookState.next;
919
885
  }
920
886
  const queue = hookState?.queue;
921
- if (isPOJO(queue) && typeof queue.dispatch === "function") {
922
- return queue.dispatch as (value: unknown) => void;
923
- }
924
- return null;
887
+ if (!isPOJO(queue)) return null;
888
+ const dispatch = queue.dispatch;
889
+ return typeof dispatch === "function" ? (value) => dispatch(value) : null;
925
890
  };
926
891
 
927
892
  const findContextProviderFiber = (fiber: Fiber, contextType: unknown): Fiber | null => {
@@ -947,28 +912,30 @@ const isPOJO = (maybePOJO: unknown): maybePOJO is Record<string, unknown> => {
947
912
  const buildPathsFromValue = (
948
913
  maybePOJO: Record<string, unknown>,
949
914
  basePath: string[] = [],
950
- ): Array<{ path: string[]; value: unknown }> => {
951
- if (!isPOJO(maybePOJO)) {
915
+ ancestors = new WeakSet<object>(),
916
+ ): ValueWrite[] => {
917
+ if (ancestors.has(maybePOJO)) {
952
918
  return [{ path: basePath, value: maybePOJO }];
953
919
  }
954
920
 
955
- const paths: Array<{ path: string[]; value: unknown }> = [];
921
+ ancestors.add(maybePOJO);
922
+ const paths: ValueWrite[] = [];
956
923
 
957
- for (const key in maybePOJO) {
958
- const value = maybePOJO[key];
924
+ for (const [key, value] of Object.entries(maybePOJO)) {
959
925
  const path = basePath.concat(key);
960
926
 
961
927
  if (isPOJO(value)) {
962
- paths.push(...buildPathsFromValue(value, path));
928
+ paths.push(...buildPathsFromValue(value, path, ancestors));
963
929
  } else {
964
930
  paths.push({ path, value });
965
931
  }
966
932
  }
967
933
 
934
+ ancestors.delete(maybePOJO);
968
935
  return paths;
969
936
  };
970
937
 
971
- const buildValueWrites = (partialValue: unknown): Array<{ path: string[]; value: unknown }> =>
938
+ const buildValueWrites = (partialValue: unknown): ValueWrite[] =>
972
939
  isPOJO(partialValue) ? buildPathsFromValue(partialValue) : [{ path: [], value: partialValue }];
973
940
 
974
941
  export const overrideProps = (fiber: Fiber, partialValue: Record<string, unknown>) => {
@@ -985,11 +952,10 @@ export const overrideHookState = (fiber: Fiber, id: number, partialValue: unknow
985
952
  const writes = buildValueWrites(partialValue);
986
953
 
987
954
  if (renderers.length > 0) {
988
- const hookId = String(id);
989
955
  for (const renderer of renderers) {
990
956
  for (const { path, value } of writes) {
991
957
  try {
992
- renderer.overrideHookState?.(fiber, hookId, path, value);
958
+ renderer.overrideHookState?.(fiber, id, path, value);
993
959
  } catch {}
994
960
  }
995
961
  }
@@ -1022,24 +988,22 @@ export const overrideContext = (fiber: Fiber, contextType: unknown, partialValue
1022
988
  export interface InstrumentationOptions {
1023
989
  name?: string;
1024
990
  onActive?: () => unknown;
1025
- onCommitFiberRoot?: (rendererID: number, root: FiberRoot, priority: number | void) => unknown;
991
+ onCommitFiberRoot?: (
992
+ rendererID: number,
993
+ root: FiberRoot,
994
+ priority: number | void,
995
+ didError?: boolean,
996
+ ) => unknown;
1026
997
  onCommitFiberUnmount?: (rendererID: number, fiber: Fiber) => unknown;
1027
998
  onPostCommitFiberRoot?: (rendererID: number, root: FiberRoot) => unknown;
1028
999
  onScheduleFiberRoot?: (rendererID: number, root: FiberRoot, children: React.ReactNode) => unknown;
1029
1000
  }
1030
1001
 
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
- >();
1002
+ interface InstrumentationSubscription {
1003
+ options: InstrumentationOptions;
1004
+ }
1005
+
1006
+ const instrumentationSubscriptions = new Set<InstrumentationSubscription>();
1043
1007
 
1044
1008
  interface HookDispatchers {
1045
1009
  onCommitFiberRoot: ReactDevToolsGlobalHook["onCommitFiberRoot"];
@@ -1049,6 +1013,7 @@ interface HookDispatchers {
1049
1013
  }
1050
1014
 
1051
1015
  const hookDispatchers = new WeakMap<ReactDevToolsGlobalHook, Partial<HookDispatchers>>();
1016
+ let didSubscribeToHookReplacements = false;
1052
1017
 
1053
1018
  const rootRendererIds = new WeakMap<FiberRoot, number>();
1054
1019
 
@@ -1057,7 +1022,7 @@ const rootRendererIds = new WeakMap<FiberRoot, number>();
1057
1022
  // next instrument() call installs a fresh wrapper over it; a superseded
1058
1023
  // wrapper still forwards the previous chain but skips the listeners so
1059
1024
  // they never fire twice.
1060
- const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
1025
+ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
1061
1026
  const dispatchers = hookDispatchers.get(rdtHook) ?? {};
1062
1027
  hookDispatchers.set(rdtHook, dispatchers);
1063
1028
 
@@ -1070,13 +1035,29 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1070
1035
  rendererID,
1071
1036
  root,
1072
1037
  priority,
1038
+ didError,
1073
1039
  ) => {
1074
- prevOnCommitFiberRoot?.(rendererID, root, priority);
1040
+ if (prevOnCommitFiberRoot) {
1041
+ prevOnCommitFiberRoot.call(rdtHook, rendererID, root, priority, didError);
1042
+ }
1075
1043
  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);
1044
+ setReactWorkTagsForFiber(root.current, rdtHook.renderers.get(rendererID));
1045
+ const rootMemoizedState = root.current.memoizedState;
1046
+ const isUnmounting =
1047
+ rootMemoizedState === null ||
1048
+ rootMemoizedState.element === null ||
1049
+ rootMemoizedState.element === undefined;
1050
+ if (isUnmounting) {
1051
+ _fiberRoots.delete(root);
1052
+ rootRendererIds.delete(root);
1053
+ } else {
1054
+ _fiberRoots.add(root);
1055
+ rootRendererIds.set(root, rendererID);
1056
+ }
1057
+ for (const { options } of instrumentationSubscriptions) {
1058
+ if (options.onCommitFiberRoot) {
1059
+ options.onCommitFiberRoot(rendererID, root, priority, didError);
1060
+ }
1080
1061
  }
1081
1062
  };
1082
1063
  dispatchers.onCommitFiberRoot = dispatchCommitFiberRoot;
@@ -1092,12 +1073,17 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1092
1073
  rendererID,
1093
1074
  fiber,
1094
1075
  ) => {
1095
- prevOnCommitFiberUnmount?.(rendererID, fiber);
1076
+ setReactWorkTagsForFiber(fiber, rdtHook.renderers.get(rendererID));
1077
+ if (prevOnCommitFiberUnmount) {
1078
+ prevOnCommitFiberUnmount.call(rdtHook, rendererID, fiber);
1079
+ }
1096
1080
  if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
1097
1081
  return;
1098
1082
  }
1099
- for (const listener of commitFiberUnmountListeners) {
1100
- listener(rendererID, fiber);
1083
+ for (const { options } of instrumentationSubscriptions) {
1084
+ if (options.onCommitFiberUnmount) {
1085
+ options.onCommitFiberUnmount(rendererID, fiber);
1086
+ }
1101
1087
  }
1102
1088
  };
1103
1089
  dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
@@ -1113,12 +1099,16 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1113
1099
  rendererID,
1114
1100
  root,
1115
1101
  ) => {
1116
- prevOnPostCommitFiberRoot?.(rendererID, root);
1102
+ if (prevOnPostCommitFiberRoot) {
1103
+ prevOnPostCommitFiberRoot.call(rdtHook, rendererID, root);
1104
+ }
1117
1105
  if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
1118
1106
  return;
1119
1107
  }
1120
- for (const listener of postCommitFiberRootListeners) {
1121
- listener(rendererID, root);
1108
+ for (const { options } of instrumentationSubscriptions) {
1109
+ if (options.onPostCommitFiberRoot) {
1110
+ options.onPostCommitFiberRoot(rendererID, root);
1111
+ }
1122
1112
  }
1123
1113
  };
1124
1114
  dispatchers.onPostCommitFiberRoot = dispatchPostCommitFiberRoot;
@@ -1135,10 +1125,14 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1135
1125
  root,
1136
1126
  children,
1137
1127
  ) => {
1138
- prevOnScheduleFiberRoot?.(rendererID, root, children);
1128
+ if (prevOnScheduleFiberRoot) {
1129
+ prevOnScheduleFiberRoot.call(rdtHook, rendererID, root, children);
1130
+ }
1139
1131
  if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
1140
- for (const listener of scheduleFiberRootListeners) {
1141
- listener(rendererID, root, children);
1132
+ for (const { options } of instrumentationSubscriptions) {
1133
+ if (options.onScheduleFiberRoot) {
1134
+ options.onScheduleFiberRoot(rendererID, root, children);
1135
+ }
1142
1136
  }
1143
1137
  };
1144
1138
  dispatchers.onScheduleFiberRoot = dispatchScheduleFiberRoot;
@@ -1151,8 +1145,7 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1151
1145
  * dispatches to a set of listeners, so multiple `instrument` calls compose
1152
1146
  * without stacking patches. Returns an unsubscribe function that removes
1153
1147
  * exactly the handlers this call registered.
1154
- * The returned function is also a `Disposable`, so it composes with other
1155
- * bippy subscriptions through `using`.
1148
+ * The unsubscribe function can also be disposed through `using`.
1156
1149
  * @example
1157
1150
  * const unsubscribe = instrument({
1158
1151
  * onActive() {
@@ -1167,34 +1160,24 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1167
1160
  export const instrument = (options: InstrumentationOptions): Unsubscribe => {
1168
1161
  const rdtHook = getRDTHook(options.onActive);
1169
1162
 
1163
+ if (!didSubscribeToHookReplacements) {
1164
+ onRDTHookReplace(setHookEventDispatchers);
1165
+ didSubscribeToHookReplacements = true;
1166
+ }
1167
+
1170
1168
  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
1171
1169
 
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);
1170
+ setHookEventDispatchers(rdtHook);
1171
+ const subscription: InstrumentationSubscription = { options };
1172
+ instrumentationSubscriptions.add(subscription);
1173
+
1174
+ return createUnsubscribe(() => {
1175
+ if (options.onActive) _onActiveListeners.delete(options.onActive);
1176
+ instrumentationSubscriptions.delete(subscription);
1192
1177
  });
1193
1178
  };
1194
1179
 
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.
1180
+ // React uses per-renderer suffixes for host-instance Fiber keys, so discovered keys are cached.
1198
1181
  const knownFiberPropertyKeys = new Set<string>();
1199
1182
 
1200
1183
  const isFiberPropertyKey = (key: string): boolean =>
@@ -1202,6 +1185,37 @@ const isFiberPropertyKey = (key: string): boolean =>
1202
1185
  key.startsWith("__reactInternalInstance$") ||
1203
1186
  key.startsWith("__reactFiber");
1204
1187
 
1188
+ const getLegacyRootFiber = (hostInstance: object): Fiber | null => {
1189
+ const reactRootContainer = Reflect.get(hostInstance, "_reactRootContainer");
1190
+ if (typeof reactRootContainer !== "object" || reactRootContainer === null) return null;
1191
+ const internalRoot = Reflect.get(reactRootContainer, "_internalRoot");
1192
+ if (typeof internalRoot !== "object" || internalRoot === null) return null;
1193
+ const current = Reflect.get(internalRoot, "current");
1194
+ if (typeof current !== "object" || current === null) return null;
1195
+ const child = Reflect.get(current, "child");
1196
+ return isValidFiber(child) ? child : null;
1197
+ };
1198
+
1199
+ const getInternalInstanceHandle = (hostInstance: object): Fiber | null => {
1200
+ const internalInstanceHandle =
1201
+ Reflect.get(hostInstance, "__internalInstanceHandle") ??
1202
+ Reflect.get(hostInstance, "_internalInstanceHandle");
1203
+ return isValidFiber(internalInstanceHandle) ? internalInstanceHandle : null;
1204
+ };
1205
+
1206
+ const getPublicHostInstance = (stateNode: unknown): unknown => {
1207
+ if (typeof stateNode !== "object" || stateNode === null) return stateNode;
1208
+ const canonical = Reflect.get(stateNode, "canonical");
1209
+ if (typeof canonical === "object" && canonical !== null) {
1210
+ const publicInstance = Reflect.get(canonical, "publicInstance");
1211
+ if (typeof publicInstance === "object" && publicInstance !== null) {
1212
+ return publicInstance;
1213
+ }
1214
+ }
1215
+ const nativeTag = Reflect.get(stateNode, "_nativeTag");
1216
+ return typeof nativeTag === "number" ? nativeTag : stateNode;
1217
+ };
1218
+
1205
1219
  export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1206
1220
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1207
1221
  if (rdtHook?.renderers) {
@@ -1213,42 +1227,38 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1213
1227
  }
1214
1228
  }
1215
1229
 
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
- }
1230
+ if (typeof hostInstance === "object" && hostInstance !== null) {
1231
+ const legacyRootFiber = getLegacyRootFiber(hostInstance);
1232
+ if (legacyRootFiber) return legacyRootFiber;
1221
1233
 
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
- }
1234
+ const internalInstanceHandle = getInternalInstanceHandle(hostInstance);
1235
+ if (internalInstanceHandle) return internalInstanceHandle;
1233
1236
 
1234
- const hostInstanceRecord = hostInstance as Record<string, unknown>;
1235
1237
  for (const knownKey of knownFiberPropertyKeys) {
1236
- const fiber = hostInstanceRecord[knownKey];
1237
- if (fiber) return fiber as Fiber;
1238
+ const fiber = Reflect.get(hostInstance, knownKey);
1239
+ if (isValidFiber(fiber)) return fiber;
1238
1240
  }
1239
1241
 
1240
- for (const key of Object.keys(hostInstanceRecord)) {
1242
+ for (const key of Object.keys(hostInstance)) {
1241
1243
  if (isFiberPropertyKey(key)) {
1242
1244
  knownFiberPropertyKeys.add(key);
1243
- return (hostInstanceRecord[key] || null) as Fiber | null;
1245
+ const fiber = Reflect.get(hostInstance, key);
1246
+ if (isValidFiber(fiber)) return fiber;
1244
1247
  }
1245
1248
  }
1249
+ }
1246
1250
 
1251
+ if (
1252
+ hostInstance !== null &&
1253
+ hostInstance !== undefined &&
1254
+ (typeof hostInstance === "object" || typeof hostInstance === "number")
1255
+ ) {
1247
1256
  for (const fiberRoot of _fiberRoots) {
1248
- if (getRootRenderer(fiberRoot.current)?.findFiberByHostInstance) continue;
1249
1257
  const fiber = traverseFiber(
1250
1258
  fiberRoot.current,
1251
- (candidateFiber) => candidateFiber.stateNode === hostInstance,
1259
+ (candidateFiber) =>
1260
+ isHostFiber(candidateFiber) &&
1261
+ getPublicHostInstance(candidateFiber.stateNode) === hostInstance,
1252
1262
  );
1253
1263
  if (fiber) return fiber;
1254
1264
  }
@@ -1256,6 +1266,17 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1256
1266
  return null;
1257
1267
  };
1258
1268
 
1259
- export * from "./rdt-hook.js";
1260
- export * from "./unsubscribe.js";
1261
- export type * from "./types.js";
1269
+ export {
1270
+ BIPPY_INSTRUMENTATION_STRING,
1271
+ _onActiveListeners,
1272
+ _renderers,
1273
+ getRDTHook,
1274
+ hasRDTHook,
1275
+ installRDTHook,
1276
+ isRealReactDevtools,
1277
+ onRendererInject,
1278
+ patchRDTHook,
1279
+ version,
1280
+ } from "./rdt-hook.js";
1281
+ export type { Unsubscribe } from "./rdt-hook.js";
1282
+ export type * from "./react-internals/index.js";