bippy 0.6.1-dev.3a24abf → 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 (48) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +7 -1
  3. package/dist/core.cjs +1 -1
  4. package/dist/core.d.cts +39 -41
  5. package/dist/core.d.ts +39 -41
  6. package/dist/core.js +1 -1
  7. package/dist/core2.cjs +1 -1
  8. package/dist/core2.d.cts +11 -3
  9. package/dist/core2.d.ts +11 -3
  10. package/dist/core2.js +1 -1
  11. package/dist/{types.d.cts → errors.d.cts} +205 -31
  12. package/dist/{types.d.ts → errors.d.ts} +205 -31
  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 +8 -0
  20. package/dist/install-hook-only.d.ts +8 -0
  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 +13 -13
  26. package/dist/source.d.cts +27 -9
  27. package/dist/source.d.ts +27 -9
  28. package/dist/source.js +13 -13
  29. package/package.json +10 -6
  30. package/src/core.ts +269 -224
  31. package/src/errors.ts +99 -0
  32. package/src/rdt-hook.ts +154 -99
  33. package/src/react-internals/generated/react-work-tags.ts +314 -0
  34. package/src/react-internals/index.ts +67 -0
  35. package/src/react-internals/semver.ts +78 -0
  36. package/src/{types.ts → react-internals/types.ts} +13 -3
  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 +2 -2
  40. package/src/source/index.ts +9 -1
  41. package/src/source/inspect-hooks.ts +199 -165
  42. package/src/source/owner-stack.ts +26 -39
  43. package/src/source/parse-debug-stack.ts +4 -4
  44. package/src/source/parse-hook-names.ts +1 -1
  45. package/src/source/symbolication.ts +468 -101
  46. package/src/generated/react-work-tags.ts +0 -169
  47. package/src/react-internals.ts +0 -67
  48. package/src/unsubscribe.ts +0 -17
package/src/core.ts CHANGED
@@ -10,78 +10,44 @@ import type {
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
23
  isRealReactDevtools,
24
+ onRDTHookReplace,
21
25
  onRendererInject,
22
26
  } from "./rdt-hook.js";
27
+ import type { Unsubscribe } from "./rdt-hook.js";
23
28
  import {
24
- ClassComponentTag,
25
- CONCURRENT_MODE_NUMBER,
26
- CONCURRENT_MODE_SYMBOL_DESCRIPTION,
27
- CONCURRENT_MODE_SYMBOL_STRING,
28
- ContextConsumerTag,
29
- DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION,
30
- DEPRECATED_ASYNC_MODE_SYMBOL_STRING,
31
- DehydratedSuspenseComponentTag,
32
- ELEMENT_TYPE_SYMBOL_STRING,
33
- ForwardRefTag,
34
- FragmentTag,
35
- FunctionComponentTag,
36
- HostComponentTag,
37
- HostHoistableTag,
38
- HostRootTag,
39
- HostSingletonTag,
40
- HostTextTag,
41
- LegacyHiddenComponentTag,
42
- MemoComponentTag,
29
+ getReactWorkTagsForFiber,
43
30
  MutationMask,
44
- OffscreenComponentTag,
45
31
  ReactBuildType,
46
32
  ReactFiberFlags,
47
- SimpleMemoComponentTag,
48
- SuspenseComponentTag,
49
- TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING,
50
- } from "./react-internals.js";
51
- import { toUnsubscribe, type Unsubscribe } from "./unsubscribe.js";
33
+ ReactSymbols,
34
+ setReactWorkTagsForFiber,
35
+ } from "./react-internals/index.js";
52
36
 
37
+ export { getReactWorkTags, ReactSymbols } from "./react-internals/index.js";
38
+ export type { ReactWorkTagMap, ReactWorkTagVersion } from "./react-internals/index.js";
53
39
  export {
54
- ActivityComponentTag,
55
- ClassComponentTag,
56
- CONCURRENT_MODE_NUMBER,
57
- CONCURRENT_MODE_SYMBOL_DESCRIPTION,
58
- CONCURRENT_MODE_SYMBOL_STRING,
59
- ContextConsumerTag,
60
- DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION,
61
- DEPRECATED_ASYNC_MODE_SYMBOL_STRING,
62
- DehydratedSuspenseComponentTag,
63
- ELEMENT_TYPE_SYMBOL_STRING,
64
- ForwardRefTag,
65
- FragmentTag,
66
- FunctionComponentTag,
67
- HostComponentTag,
68
- HostHoistableTag,
69
- HostPortalTag,
70
- HostRootTag,
71
- HostSingletonTag,
72
- HostTextTag,
73
- LazyComponentTag,
74
- LegacyHiddenComponentTag,
75
- MemoComponentTag,
76
- OffscreenComponentTag,
77
- SimpleMemoComponentTag,
78
- SuspenseComponentTag,
79
- SuspenseListComponentTag,
80
- TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING,
81
- ViewTransitionComponentTag,
82
- } from "./react-internals.js";
83
-
84
- const { Cloned, PerformedWork } = ReactFiberFlags;
40
+ BippyError,
41
+ BippyHookInstallationError,
42
+ BippyHookInspectionError,
43
+ BippyHookListenerError,
44
+ BippyHookRenderError,
45
+ BippyInstrumentationError,
46
+ BippyReactBuildError,
47
+ BippyReactDevToolsError,
48
+ BippySourceMapError,
49
+ BippyUnsupportedHookError,
50
+ } from "./errors.js";
85
51
 
86
52
  const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
87
53
  typeof value === "object" && value !== null;
@@ -113,6 +79,20 @@ interface FiberSelector {
113
79
  (node: Fiber): boolean | Promise<boolean | void> | void;
114
80
  }
115
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
+ }
95
+
116
96
  /**
117
97
  * Returns `true` if object is a React Element.
118
98
  *
@@ -122,9 +102,8 @@ export const isValidElement = (element: unknown): element is React.ReactElement
122
102
  typeof element === "object" &&
123
103
  element !== null &&
124
104
  "$$typeof" in element &&
125
- [ELEMENT_TYPE_SYMBOL_STRING, TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING].includes(
126
- String(element.$$typeof),
127
- );
105
+ (String(element.$$typeof) === ReactSymbols.LEGACY_ELEMENT_SYMBOL_STRING ||
106
+ String(element.$$typeof) === ReactSymbols.ELEMENT_SYMBOL_STRING);
128
107
 
129
108
  /**
130
109
  * Returns `true` if object is a React Fiber.
@@ -139,17 +118,21 @@ export const isValidFiber = (fiber: unknown): fiber is Fiber =>
139
118
  "sibling" in fiber &&
140
119
  "flags" in fiber;
141
120
 
121
+ const isFiberRoot = (fiberRoot: unknown): fiberRoot is FiberRoot =>
122
+ isObjectRecord(fiberRoot) && isValidFiber(fiberRoot.current);
123
+
142
124
  /**
143
125
  * Returns `true` if fiber is a host fiber. Host fibers are DOM nodes in react-dom, `View` in react-native, etc.
144
126
  *
145
127
  * @see https://reactnative.dev/architecture/glossary#host-view-tree-and-host-view
146
128
  */
147
129
  export const isHostFiber = (fiber: Fiber): fiber is HostFiber => {
130
+ const workTags = getReactWorkTagsForFiber(fiber);
148
131
  switch (fiber.tag) {
149
- case HostComponentTag:
150
- case HostTextTag:
151
- case HostHoistableTag:
152
- case HostSingletonTag:
132
+ case workTags.HostComponent:
133
+ case workTags.HostText:
134
+ case workTags.HostHoistable:
135
+ case workTags.HostSingleton:
153
136
  return true;
154
137
  default:
155
138
  return false;
@@ -162,12 +145,13 @@ export const isHostFiber = (fiber: Fiber): fiber is HostFiber => {
162
145
  * @see https://reactnative.dev/architecture/glossary#react-composite-components
163
146
  */
164
147
  export const isCompositeFiber = (fiber: Fiber): boolean => {
148
+ const workTags = getReactWorkTagsForFiber(fiber);
165
149
  switch (fiber.tag) {
166
- case ClassComponentTag:
167
- case ForwardRefTag:
168
- case FunctionComponentTag:
169
- case MemoComponentTag:
170
- case SimpleMemoComponentTag:
150
+ case workTags.ClassComponent:
151
+ case workTags.ForwardRef:
152
+ case workTags.FunctionComponent:
153
+ case workTags.MemoComponent:
154
+ case workTags.SimpleMemoComponent:
171
155
  return true;
172
156
  default:
173
157
  return false;
@@ -275,15 +259,16 @@ export const didFiberRender = (fiber: Fiber): boolean => {
275
259
  const nextProps = fiber.memoizedProps;
276
260
  const prevProps = fiber.alternate?.memoizedProps || {};
277
261
  const flags = fiber.flags ?? fiber.effectTag ?? 0;
262
+ const workTags = getReactWorkTagsForFiber(fiber);
278
263
 
279
264
  switch (fiber.tag) {
280
- case ClassComponentTag:
281
- case ContextConsumerTag:
282
- case ForwardRefTag:
283
- case FunctionComponentTag:
284
- case MemoComponentTag:
285
- case SimpleMemoComponentTag: {
286
- 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;
287
272
  }
288
273
  default:
289
274
  // Host nodes (DOM, root, etc.)
@@ -301,8 +286,8 @@ export const didFiberRender = (fiber: Fiber): boolean => {
301
286
  */
302
287
  export const didFiberCommit = (fiber: Fiber): boolean => {
303
288
  return Boolean(
304
- (fiber.flags & (MutationMask | Cloned)) !== 0 ||
305
- (fiber.subtreeFlags & (MutationMask | Cloned)) !== 0,
289
+ (fiber.flags & (MutationMask | ReactFiberFlags.Cloned)) !== 0 ||
290
+ (fiber.subtreeFlags & (MutationMask | ReactFiberFlags.Cloned)) !== 0,
306
291
  );
307
292
  };
308
293
 
@@ -351,8 +336,9 @@ export const getFiberStack = (fiber: Fiber): Fiber[] => {
351
336
  * Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
352
337
  */
353
338
  const shouldFilterFiber = (fiber: Fiber): boolean => {
339
+ const workTags = getReactWorkTagsForFiber(fiber);
354
340
  switch (fiber.tag) {
355
- case DehydratedSuspenseComponentTag:
341
+ case workTags.DehydratedSuspenseComponent:
356
342
  // TODO: ideally we would show dehydrated Suspense immediately.
357
343
  // However, it has some special behavior (like disconnecting
358
344
  // an alternate and turning into real Suspense) which breaks DevTools.
@@ -360,13 +346,13 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
360
346
  // https://github.com/bvaughn/react-devtools-experimental/issues/197
361
347
  return true;
362
348
 
363
- case FragmentTag:
364
- case HostTextTag:
365
- case LegacyHiddenComponentTag:
366
- case OffscreenComponentTag:
349
+ case workTags.Fragment:
350
+ case workTags.HostText:
351
+ case workTags.LegacyHiddenComponent:
352
+ case workTags.OffscreenComponent:
367
353
  return true;
368
354
 
369
- case HostRootTag:
355
+ case workTags.HostRoot:
370
356
  // It is never valid to filter the root element.
371
357
  return false;
372
358
 
@@ -376,15 +362,15 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
376
362
 
377
363
  if (typeof symbolOrNumber === "symbol") {
378
364
  return (
379
- symbolOrNumber.description === CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
380
- symbolOrNumber.description === DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
365
+ symbolOrNumber.description === ReactSymbols.CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
366
+ symbolOrNumber.description === ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
381
367
  );
382
368
  }
383
369
 
384
370
  switch (symbolOrNumber) {
385
- case CONCURRENT_MODE_NUMBER:
386
- case CONCURRENT_MODE_SYMBOL_STRING:
387
- 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:
388
374
  return true;
389
375
 
390
376
  default:
@@ -448,75 +434,67 @@ export function traverseFiber(
448
434
  selector: (node: Fiber) => Promise<boolean | void>,
449
435
  ascending?: boolean,
450
436
  ): Promise<Fiber | null>;
437
+ export function traverseFiber(
438
+ fiber: Fiber | null,
439
+ selector: FiberSelector,
440
+ ascending?: boolean,
441
+ ): Fiber | null | Promise<Fiber | null>;
451
442
  export function traverseFiber(
452
443
  fiber: Fiber | null,
453
444
  selector: FiberSelector,
454
445
  ascending = false,
455
446
  ): Fiber | null | Promise<Fiber | null> {
456
- if (!fiber) return null;
457
-
458
- const firstResult = selector(fiber);
459
- if (firstResult instanceof Promise) {
460
- return (async () => {
461
- if ((await firstResult) === true) return fiber;
462
-
463
- let child = ascending ? fiber.return : fiber.child;
464
- while (child) {
465
- const match = await traverseFiberAsync(child, selector, ascending);
466
- if (match) return match;
467
- child = ascending ? null : child.sibling;
468
- }
469
- return null;
470
- })();
471
- }
447
+ return traverseFiberInternal(fiber, selector, ascending);
448
+ }
472
449
 
473
- 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";
474
455
 
475
- let child = ascending ? fiber.return : fiber.child;
476
- while (child) {
477
- const match = traverseFiberSync(child, selector, ascending);
478
- if (match) return match;
479
- child = ascending ? null : child.sibling;
480
- }
481
- return null;
482
- }
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
+ };
483
464
 
484
- const traverseFiberSync = (
465
+ const traverseFiberSiblings = (
485
466
  fiber: Fiber | null,
486
467
  selector: FiberSelector,
487
- ascending = false,
488
- ): Fiber | null => {
468
+ ascending: boolean,
469
+ ): Fiber | null | Promise<Fiber | null> => {
489
470
  if (!fiber) return null;
490
- const selection = selector(fiber);
491
- if (selection instanceof Promise) return null;
492
- if (selection === true) return fiber;
493
-
494
- let child = ascending ? fiber.return : fiber.child;
495
- while (child) {
496
- const match = traverseFiberSync(child, selector, ascending);
497
- if (match) return match;
498
471
 
499
- 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
+ );
500
478
  }
501
- return null;
479
+ return match ?? traverseFiberSiblings(nextSibling, selector, ascending);
502
480
  };
503
481
 
504
- const traverseFiberAsync = async (
482
+ const traverseFiberInternal = (
505
483
  fiber: Fiber | null,
506
484
  selector: FiberSelector,
507
- ascending = false,
508
- ): Promise<Fiber | null> => {
485
+ ascending: boolean,
486
+ ): Fiber | null | Promise<Fiber | null> => {
509
487
  if (!fiber) return null;
510
- if ((await selector(fiber)) === true) return fiber;
511
-
512
- let child = ascending ? fiber.return : fiber.child;
513
- while (child) {
514
- const match = await traverseFiberAsync(child, selector, ascending);
515
- if (match) return match;
516
488
 
517
- 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
+ );
518
494
  }
519
- return null;
495
+ if (selection === true) return fiber;
496
+
497
+ return traverseFiberChildren(fiber, selector, ascending);
520
498
  };
521
499
 
522
500
  /**
@@ -528,7 +506,7 @@ const traverseFiberAsync = async (
528
506
  * console.log(selfTime, totalTime);
529
507
  * ```
530
508
  */
531
- export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime: number } => {
509
+ export const getTimings = (fiber?: Fiber | null): FiberTimings => {
532
510
  const totalTime = fiber?.actualDuration ?? 0;
533
511
  let selfTime = totalTime;
534
512
  // TODO: calculate a DOM time, which is just host component summed up
@@ -605,35 +583,35 @@ export const getLatestFiber = (fiber: Fiber): Fiber => {
605
583
  }
606
584
  for (const root of _fiberRoots) {
607
585
  const latestFiber = traverseFiber(root.current, (innerFiber) => {
608
- if (innerFiber === fiber) return true;
586
+ if (innerFiber === fiber || innerFiber === alternate) return true;
609
587
  });
610
588
  if (latestFiber) return latestFiber;
611
589
  }
612
590
  return fiber;
613
591
  };
614
592
 
615
- export type RenderHandler = <S>(fiber: Fiber, phase: RenderPhase, state?: S) => unknown;
616
-
617
593
  export type RenderPhase = "mount" | "unmount" | "update";
618
594
 
619
- let fiberId = 0;
595
+ let nextFiberId = 0;
620
596
  const fiberIdMap = new WeakMap<Fiber, number>();
621
597
 
622
- export const setFiberId = (fiber: Fiber, id: number = fiberId++): void => {
623
- 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
+ }
624
603
  };
625
604
 
626
- // Fiber alternates share an identity across double-buffered commits.
627
605
  export const getFiberId = (fiber: Fiber): number => {
628
- let id = fiberIdMap.get(fiber);
629
- if (id === undefined && fiber.alternate) {
630
- id = fiberIdMap.get(fiber.alternate);
606
+ let currentFiberId = fiberIdMap.get(fiber);
607
+ if (currentFiberId === undefined && fiber.alternate) {
608
+ currentFiberId = fiberIdMap.get(fiber.alternate);
631
609
  }
632
- if (id === undefined) {
633
- id = fiberId++;
634
- setFiberId(fiber, id);
610
+ if (currentFiberId === undefined) {
611
+ currentFiberId = nextFiberId++;
612
+ setFiberId(fiber, currentFiberId);
635
613
  }
636
- return id;
614
+ return currentFiberId;
637
615
  };
638
616
 
639
617
  const mountFiberRecursively = (
@@ -652,7 +630,7 @@ const mountFiberRecursively = (
652
630
  onRender(fiber, "mount");
653
631
  }
654
632
 
655
- if (fiber.tag === SuspenseComponentTag) {
633
+ if (fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent) {
656
634
  const isTimedOut = fiber.memoizedState !== null;
657
635
  if (isTimedOut) {
658
636
  // Special case: if Suspense mounts in a timed-out state,
@@ -682,7 +660,7 @@ const mountFiberRecursively = (
682
660
  const updateFiberRecursively = (
683
661
  onRender: RenderHandler,
684
662
  nextFiber: Fiber,
685
- prevFiber: Fiber,
663
+ prevFiber: Fiber | null,
686
664
  parentFiber: Fiber | null,
687
665
  ): void => {
688
666
  if (!fiberIdMap.has(nextFiber)) {
@@ -693,7 +671,7 @@ const updateFiberRecursively = (
693
671
  getFiberId(prevFiber);
694
672
  }
695
673
 
696
- const isSuspense = nextFiber.tag === SuspenseComponentTag;
674
+ const isSuspense = nextFiber.tag === getReactWorkTagsForFiber(nextFiber).SuspenseComponent;
697
675
 
698
676
  const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
699
677
  if (shouldIncludeInTree && didFiberRender(nextFiber)) {
@@ -780,7 +758,7 @@ const updateFiberRecursively = (
780
758
  };
781
759
 
782
760
  const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
783
- const isRoot = fiber.tag === HostRootTag;
761
+ const isRoot = fiber.tag === getReactWorkTagsForFiber(fiber).HostRoot;
784
762
 
785
763
  if (isRoot || !shouldFilterFiber(fiber)) {
786
764
  onRender(fiber, "unmount");
@@ -789,7 +767,8 @@ const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
789
767
 
790
768
  const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => {
791
769
  // We might meet a nested Suspense on our way.
792
- const isTimedOutSuspense = fiber.tag === SuspenseComponentTag && fiber.memoizedState !== null;
770
+ const isTimedOutSuspense =
771
+ fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent && fiber.memoizedState !== null;
793
772
  let child = fiber.child;
794
773
 
795
774
  if (isTimedOutSuspense) {
@@ -815,7 +794,7 @@ const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber):
815
794
 
816
795
  let commitId = 0;
817
796
  const rootInstanceMap = new WeakMap<
818
- FiberRoot,
797
+ Fiber | FiberRoot,
819
798
  {
820
799
  id: number;
821
800
  prevFiber: Fiber | null;
@@ -829,7 +808,7 @@ const rootInstanceMap = new WeakMap<
829
808
  * console.log(phase)
830
809
  * })
831
810
  */
832
- export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler): void => {
811
+ export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: RenderHandler): void => {
833
812
  const fiber = "current" in root ? root.current : root;
834
813
 
835
814
  let rootInstance = rootInstanceMap.get(root);
@@ -878,9 +857,8 @@ let areOverrideRenderersWired = false;
878
857
  const wireOverrideRenderers = (): void => {
879
858
  if (!hasRDTHook()) return;
880
859
  const rdtHook = getRDTHook();
881
- if (!rdtHook?.renderers) return;
882
860
 
883
- ensureHookDispatchesToListeners(rdtHook);
861
+ setHookEventDispatchers(rdtHook);
884
862
  for (const renderer of rdtHook.renderers.values()) {
885
863
  overrideRenderers.add(renderer);
886
864
  }
@@ -897,14 +875,13 @@ const getRootRenderer = (fiber: Fiber): ReactRenderer | null => {
897
875
  while (hostRootFiber.return) {
898
876
  hostRootFiber = hostRootFiber.return;
899
877
  }
900
- const rendererId = rootRendererIds.get(hostRootFiber.stateNode);
878
+ const fiberRoot = hostRootFiber.stateNode;
879
+ if (!isFiberRoot(fiberRoot)) return null;
880
+ const rendererId = rootRendererIds.get(fiberRoot);
901
881
  if (rendererId === undefined) return null;
902
- return getRDTHook().renderers?.get(rendererId) ?? null;
882
+ return getRDTHook().renderers.get(rendererId) ?? null;
903
883
  };
904
884
 
905
- // dispatch to the renderer that owns the fiber's root when known; renderers
906
- // that never committed through bippy's hook patch fall back to "try all",
907
- // which matches the pre-ownership behavior for single-renderer apps
908
885
  const resolveOverrideRenderers = (fiber: Fiber): ReactRenderer[] => {
909
886
  wireOverrideRenderers();
910
887
  const rootRenderer = getRootRenderer(fiber);
@@ -930,7 +907,7 @@ const getHookStateDispatch = (
930
907
  hookIndex: number,
931
908
  ): ((value: unknown) => void) | null => {
932
909
  let hookState = fiber.memoizedState;
933
- for (let i = 0; i < hookIndex; i++) {
910
+ for (let currentHookIndex = 0; currentHookIndex < hookIndex; currentHookIndex++) {
934
911
  if (!hookState?.next) return null;
935
912
  hookState = hookState.next;
936
913
  }
@@ -963,28 +940,33 @@ const isPOJO = (maybePOJO: unknown): maybePOJO is Record<string, unknown> => {
963
940
  const buildPathsFromValue = (
964
941
  maybePOJO: Record<string, unknown>,
965
942
  basePath: string[] = [],
966
- ): Array<{ path: string[]; value: unknown }> => {
943
+ ancestors = new WeakSet<object>(),
944
+ ): ValueWrite[] => {
967
945
  if (!isPOJO(maybePOJO)) {
968
946
  return [{ path: basePath, value: maybePOJO }];
969
947
  }
948
+ if (ancestors.has(maybePOJO)) {
949
+ return [{ path: basePath, value: maybePOJO }];
950
+ }
970
951
 
971
- const paths: Array<{ path: string[]; value: unknown }> = [];
952
+ ancestors.add(maybePOJO);
953
+ const paths: ValueWrite[] = [];
972
954
 
973
- for (const key in maybePOJO) {
974
- const value = maybePOJO[key];
955
+ for (const [key, value] of Object.entries(maybePOJO)) {
975
956
  const path = basePath.concat(key);
976
957
 
977
958
  if (isPOJO(value)) {
978
- paths.push(...buildPathsFromValue(value, path));
959
+ paths.push(...buildPathsFromValue(value, path, ancestors));
979
960
  } else {
980
961
  paths.push({ path, value });
981
962
  }
982
963
  }
983
964
 
965
+ ancestors.delete(maybePOJO);
984
966
  return paths;
985
967
  };
986
968
 
987
- const buildValueWrites = (partialValue: unknown): Array<{ path: string[]; value: unknown }> =>
969
+ const buildValueWrites = (partialValue: unknown): ValueWrite[] =>
988
970
  isPOJO(partialValue) ? buildPathsFromValue(partialValue) : [{ path: [], value: partialValue }];
989
971
 
990
972
  export const overrideProps = (fiber: Fiber, partialValue: Record<string, unknown>) => {
@@ -1048,18 +1030,26 @@ export interface InstrumentationOptions {
1048
1030
  onScheduleFiberRoot?: (rendererID: number, root: FiberRoot, children: React.ReactNode) => unknown;
1049
1031
  }
1050
1032
 
1051
- const commitFiberRootListeners = new Set<
1052
- NonNullable<InstrumentationOptions["onCommitFiberRoot"]>
1053
- >();
1054
- const commitFiberUnmountListeners = new Set<
1055
- NonNullable<InstrumentationOptions["onCommitFiberUnmount"]>
1056
- >();
1057
- const postCommitFiberRootListeners = new Set<
1058
- NonNullable<InstrumentationOptions["onPostCommitFiberRoot"]>
1059
- >();
1060
- const scheduleFiberRootListeners = new Set<
1061
- NonNullable<InstrumentationOptions["onScheduleFiberRoot"]>
1062
- >();
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
+ };
1063
1053
 
1064
1054
  interface HookDispatchers {
1065
1055
  onCommitFiberRoot: ReactDevToolsGlobalHook["onCommitFiberRoot"];
@@ -1069,6 +1059,7 @@ interface HookDispatchers {
1069
1059
  }
1070
1060
 
1071
1061
  const hookDispatchers = new WeakMap<ReactDevToolsGlobalHook, Partial<HookDispatchers>>();
1062
+ let didSubscribeToHookReplacements = false;
1072
1063
 
1073
1064
  const rootRendererIds = new WeakMap<FiberRoot, number>();
1074
1065
 
@@ -1077,7 +1068,7 @@ const rootRendererIds = new WeakMap<FiberRoot, number>();
1077
1068
  // next instrument() call installs a fresh wrapper over it; a superseded
1078
1069
  // wrapper still forwards the previous chain but skips the listeners so
1079
1070
  // they never fire twice.
1080
- const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
1071
+ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
1081
1072
  const dispatchers = hookDispatchers.get(rdtHook) ?? {};
1082
1073
  hookDispatchers.set(rdtHook, dispatchers);
1083
1074
 
@@ -1092,8 +1083,13 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1092
1083
  priority,
1093
1084
  didError,
1094
1085
  ) => {
1095
- prevOnCommitFiberRoot?.(rendererID, root, priority, didError);
1086
+ if (prevOnCommitFiberRoot) {
1087
+ runReactDevToolsCallback("onCommitFiberRoot", () =>
1088
+ prevOnCommitFiberRoot.call(rdtHook, rendererID, root, priority, didError),
1089
+ );
1090
+ }
1096
1091
  if (hookDispatchers.get(rdtHook)?.onCommitFiberRoot !== dispatchCommitFiberRoot) return;
1092
+ setReactWorkTagsForFiber(root.current, rdtHook.renderers.get(rendererID));
1097
1093
  const rootMemoizedState = root.current.memoizedState;
1098
1094
  const isUnmounting =
1099
1095
  rootMemoizedState === null ||
@@ -1106,8 +1102,14 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1106
1102
  _fiberRoots.add(root);
1107
1103
  rootRendererIds.set(root, rendererID);
1108
1104
  }
1109
- for (const listener of commitFiberRootListeners) {
1110
- listener(rendererID, root, priority, didError);
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
+ }
1111
1113
  }
1112
1114
  };
1113
1115
  dispatchers.onCommitFiberRoot = dispatchCommitFiberRoot;
@@ -1123,12 +1125,23 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1123
1125
  rendererID,
1124
1126
  fiber,
1125
1127
  ) => {
1126
- prevOnCommitFiberUnmount?.(rendererID, fiber);
1128
+ setReactWorkTagsForFiber(fiber, rdtHook.renderers.get(rendererID));
1129
+ if (prevOnCommitFiberUnmount) {
1130
+ runReactDevToolsCallback("onCommitFiberUnmount", () =>
1131
+ prevOnCommitFiberUnmount.call(rdtHook, rendererID, fiber),
1132
+ );
1133
+ }
1127
1134
  if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
1128
1135
  return;
1129
1136
  }
1130
- for (const listener of commitFiberUnmountListeners) {
1131
- 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
+ }
1132
1145
  }
1133
1146
  };
1134
1147
  dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
@@ -1144,12 +1157,22 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1144
1157
  rendererID,
1145
1158
  root,
1146
1159
  ) => {
1147
- prevOnPostCommitFiberRoot?.(rendererID, root);
1160
+ if (prevOnPostCommitFiberRoot) {
1161
+ runReactDevToolsCallback("onPostCommitFiberRoot", () =>
1162
+ prevOnPostCommitFiberRoot.call(rdtHook, rendererID, root),
1163
+ );
1164
+ }
1148
1165
  if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
1149
1166
  return;
1150
1167
  }
1151
- for (const listener of postCommitFiberRootListeners) {
1152
- 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
+ }
1153
1176
  }
1154
1177
  };
1155
1178
  dispatchers.onPostCommitFiberRoot = dispatchPostCommitFiberRoot;
@@ -1166,10 +1189,20 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1166
1189
  root,
1167
1190
  children,
1168
1191
  ) => {
1169
- prevOnScheduleFiberRoot?.(rendererID, root, children);
1192
+ if (prevOnScheduleFiberRoot) {
1193
+ runReactDevToolsCallback("onScheduleFiberRoot", () =>
1194
+ prevOnScheduleFiberRoot.call(rdtHook, rendererID, root, children),
1195
+ );
1196
+ }
1170
1197
  if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
1171
- for (const listener of scheduleFiberRootListeners) {
1172
- 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
+ }
1173
1206
  }
1174
1207
  };
1175
1208
  dispatchers.onScheduleFiberRoot = dispatchScheduleFiberRoot;
@@ -1182,8 +1215,7 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1182
1215
  * dispatches to a set of listeners, so multiple `instrument` calls compose
1183
1216
  * without stacking patches. Returns an unsubscribe function that removes
1184
1217
  * exactly the handlers this call registered.
1185
- * The returned function is also a `Disposable`, so it composes with other
1186
- * bippy subscriptions through `using`.
1218
+ * The unsubscribe function can also be disposed through `using`.
1187
1219
  * @example
1188
1220
  * const unsubscribe = instrument({
1189
1221
  * onActive() {
@@ -1196,30 +1228,31 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1196
1228
  * unsubscribe();
1197
1229
  */
1198
1230
  export const instrument = (options: InstrumentationOptions): Unsubscribe => {
1199
- 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
+ }
1200
1246
 
1201
1247
  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
1202
1248
 
1203
- ensureHookDispatchesToListeners(rdtHook);
1204
-
1205
- const {
1206
- onActive,
1207
- onCommitFiberRoot,
1208
- onCommitFiberUnmount,
1209
- onPostCommitFiberRoot,
1210
- onScheduleFiberRoot,
1211
- } = options;
1212
- if (onCommitFiberRoot) commitFiberRootListeners.add(onCommitFiberRoot);
1213
- if (onCommitFiberUnmount) commitFiberUnmountListeners.add(onCommitFiberUnmount);
1214
- if (onPostCommitFiberRoot) postCommitFiberRootListeners.add(onPostCommitFiberRoot);
1215
- if (onScheduleFiberRoot) scheduleFiberRootListeners.add(onScheduleFiberRoot);
1216
-
1217
- return toUnsubscribe(() => {
1249
+ setHookEventDispatchers(rdtHook);
1250
+ const subscription: InstrumentationSubscription = { options };
1251
+ instrumentationSubscriptions.add(subscription);
1252
+
1253
+ return createUnsubscribe(() => {
1218
1254
  if (onActive) _onActiveListeners.delete(onActive);
1219
- if (onCommitFiberRoot) commitFiberRootListeners.delete(onCommitFiberRoot);
1220
- if (onCommitFiberUnmount) commitFiberUnmountListeners.delete(onCommitFiberUnmount);
1221
- if (onPostCommitFiberRoot) postCommitFiberRootListeners.delete(onPostCommitFiberRoot);
1222
- if (onScheduleFiberRoot) scheduleFiberRootListeners.delete(onScheduleFiberRoot);
1255
+ instrumentationSubscriptions.delete(subscription);
1223
1256
  });
1224
1257
  };
1225
1258
 
@@ -1307,6 +1340,18 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1307
1340
  return null;
1308
1341
  };
1309
1342
 
1310
- export * from "./rdt-hook.js";
1311
- export * from "./unsubscribe.js";
1312
- 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";