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

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