bippy 0.6.1-dev.3a24abf → 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 (50) 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 -42
  5. package/dist/core.d.ts +39 -42
  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} +187 -58
  12. package/dist/{types.d.ts → errors.d.ts} +187 -58
  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 -14
  26. package/dist/source.d.cts +27 -11
  27. package/dist/source.d.ts +27 -11
  28. package/dist/source.js +13 -14
  29. package/package.json +10 -6
  30. package/src/core.ts +244 -274
  31. package/src/errors.ts +34 -0
  32. package/src/install-hook-only.ts +2 -2
  33. package/src/rdt-hook.ts +152 -110
  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/{types.ts → react-internals/types.ts} +13 -75
  38. package/src/source/error-stack.ts +11 -0
  39. package/src/source/get-display-name-from-source.ts +32 -25
  40. package/src/source/get-source.ts +3 -4
  41. package/src/source/index.ts +9 -1
  42. package/src/source/inspect-hooks.ts +215 -182
  43. package/src/source/owner-stack.ts +70 -96
  44. package/src/source/parse-debug-stack.ts +4 -4
  45. package/src/source/parse-hook-names.ts +1 -1
  46. package/src/source/parse-stack.ts +0 -2
  47. package/src/source/symbolication.ts +483 -104
  48. package/src/generated/react-work-tags.ts +0 -169
  49. package/src/react-internals.ts +0 -67
  50. package/src/unsubscribe.ts +0 -17
package/src/core.ts CHANGED
@@ -10,102 +10,45 @@ import type {
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
21
  isRealReactDevtools,
22
+ onRDTHookReplace,
21
23
  onRendererInject,
22
24
  } from "./rdt-hook.js";
25
+ import type { Unsubscribe } from "./rdt-hook.js";
23
26
  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,
27
+ getReactWorkTagsForFiber,
43
28
  MutationMask,
44
- OffscreenComponentTag,
45
29
  ReactBuildType,
46
30
  ReactFiberFlags,
47
- SimpleMemoComponentTag,
48
- SuspenseComponentTag,
49
- TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING,
50
- } from "./react-internals.js";
51
- import { toUnsubscribe, type Unsubscribe } from "./unsubscribe.js";
31
+ ReactSymbols,
32
+ setReactWorkTagsForFiber,
33
+ } from "./react-internals/index.js";
52
34
 
35
+ export { getReactWorkTags, ReactSymbols } from "./react-internals/index.js";
36
+ export type { ReactWorkTagMap, ReactWorkTagVersion } from "./react-internals/index.js";
53
37
  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;
85
-
86
- const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
87
- typeof value === "object" && value !== null;
38
+ BippyError,
39
+ BippyHookInspectionError,
40
+ BippyHookRenderError,
41
+ BippySourceMapError,
42
+ BippyUnsupportedHookError,
43
+ } from "./errors.js";
88
44
 
89
45
  const isComponentType = (value: unknown): value is React.ComponentType<unknown> =>
90
46
  typeof value === "function";
91
47
 
92
- const getPropertyValue = (value: object, key: PropertyKey): unknown => {
93
- let currentValue: object | null = value;
94
- while (currentValue) {
95
- const descriptor = Object.getOwnPropertyDescriptor(currentValue, key);
96
- if (descriptor) {
97
- if ("value" in descriptor) return descriptor.value;
98
- return descriptor.get?.call(value);
99
- }
100
- currentValue = Object.getPrototypeOf(currentValue);
101
- }
102
- return undefined;
103
- };
104
-
105
48
  const getTypeName = (value: object): string | null => {
106
- const displayName = getPropertyValue(value, "displayName");
49
+ const displayName = "displayName" in value ? value.displayName : null;
107
50
  if (typeof displayName === "string" && displayName) return displayName;
108
- const name = getPropertyValue(value, "name");
51
+ const name = "name" in value ? value.name : null;
109
52
  return typeof name === "string" && name ? name : null;
110
53
  };
111
54
 
@@ -113,6 +56,20 @@ interface FiberSelector {
113
56
  (node: Fiber): boolean | Promise<boolean | void> | void;
114
57
  }
115
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
+ }
72
+
116
73
  /**
117
74
  * Returns `true` if object is a React Element.
118
75
  *
@@ -122,9 +79,8 @@ export const isValidElement = (element: unknown): element is React.ReactElement
122
79
  typeof element === "object" &&
123
80
  element !== null &&
124
81
  "$$typeof" in element &&
125
- [ELEMENT_TYPE_SYMBOL_STRING, TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING].includes(
126
- String(element.$$typeof),
127
- );
82
+ (String(element.$$typeof) === ReactSymbols.LEGACY_ELEMENT_SYMBOL_STRING ||
83
+ String(element.$$typeof) === ReactSymbols.ELEMENT_SYMBOL_STRING);
128
84
 
129
85
  /**
130
86
  * Returns `true` if object is a React Fiber.
@@ -139,17 +95,24 @@ export const isValidFiber = (fiber: unknown): fiber is Fiber =>
139
95
  "sibling" in fiber &&
140
96
  "flags" in fiber;
141
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
+
142
104
  /**
143
105
  * Returns `true` if fiber is a host fiber. Host fibers are DOM nodes in react-dom, `View` in react-native, etc.
144
106
  *
145
107
  * @see https://reactnative.dev/architecture/glossary#host-view-tree-and-host-view
146
108
  */
147
109
  export const isHostFiber = (fiber: Fiber): fiber is HostFiber => {
110
+ const workTags = getReactWorkTagsForFiber(fiber);
148
111
  switch (fiber.tag) {
149
- case HostComponentTag:
150
- case HostTextTag:
151
- case HostHoistableTag:
152
- case HostSingletonTag:
112
+ case workTags.HostComponent:
113
+ case workTags.HostText:
114
+ case workTags.HostHoistable:
115
+ case workTags.HostSingleton:
153
116
  return true;
154
117
  default:
155
118
  return false;
@@ -162,12 +125,13 @@ export const isHostFiber = (fiber: Fiber): fiber is HostFiber => {
162
125
  * @see https://reactnative.dev/architecture/glossary#react-composite-components
163
126
  */
164
127
  export const isCompositeFiber = (fiber: Fiber): boolean => {
128
+ const workTags = getReactWorkTagsForFiber(fiber);
165
129
  switch (fiber.tag) {
166
- case ClassComponentTag:
167
- case ForwardRefTag:
168
- case FunctionComponentTag:
169
- case MemoComponentTag:
170
- case SimpleMemoComponentTag:
130
+ case workTags.ClassComponent:
131
+ case workTags.ForwardRef:
132
+ case workTags.FunctionComponent:
133
+ case workTags.MemoComponent:
134
+ case workTags.SimpleMemoComponent:
171
135
  return true;
172
136
  default:
173
137
  return false;
@@ -275,15 +239,16 @@ export const didFiberRender = (fiber: Fiber): boolean => {
275
239
  const nextProps = fiber.memoizedProps;
276
240
  const prevProps = fiber.alternate?.memoizedProps || {};
277
241
  const flags = fiber.flags ?? fiber.effectTag ?? 0;
242
+ const workTags = getReactWorkTagsForFiber(fiber);
278
243
 
279
244
  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;
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;
287
252
  }
288
253
  default:
289
254
  // Host nodes (DOM, root, etc.)
@@ -301,8 +266,8 @@ export const didFiberRender = (fiber: Fiber): boolean => {
301
266
  */
302
267
  export const didFiberCommit = (fiber: Fiber): boolean => {
303
268
  return Boolean(
304
- (fiber.flags & (MutationMask | Cloned)) !== 0 ||
305
- (fiber.subtreeFlags & (MutationMask | Cloned)) !== 0,
269
+ (fiber.flags & (MutationMask | ReactFiberFlags.Cloned)) !== 0 ||
270
+ (fiber.subtreeFlags & (MutationMask | ReactFiberFlags.Cloned)) !== 0,
306
271
  );
307
272
  };
308
273
 
@@ -351,8 +316,9 @@ export const getFiberStack = (fiber: Fiber): Fiber[] => {
351
316
  * Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
352
317
  */
353
318
  const shouldFilterFiber = (fiber: Fiber): boolean => {
319
+ const workTags = getReactWorkTagsForFiber(fiber);
354
320
  switch (fiber.tag) {
355
- case DehydratedSuspenseComponentTag:
321
+ case workTags.DehydratedSuspenseComponent:
356
322
  // TODO: ideally we would show dehydrated Suspense immediately.
357
323
  // However, it has some special behavior (like disconnecting
358
324
  // an alternate and turning into real Suspense) which breaks DevTools.
@@ -360,13 +326,13 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
360
326
  // https://github.com/bvaughn/react-devtools-experimental/issues/197
361
327
  return true;
362
328
 
363
- case FragmentTag:
364
- case HostTextTag:
365
- case LegacyHiddenComponentTag:
366
- case OffscreenComponentTag:
329
+ case workTags.Fragment:
330
+ case workTags.HostText:
331
+ case workTags.LegacyHiddenComponent:
332
+ case workTags.OffscreenComponent:
367
333
  return true;
368
334
 
369
- case HostRootTag:
335
+ case workTags.HostRoot:
370
336
  // It is never valid to filter the root element.
371
337
  return false;
372
338
 
@@ -376,15 +342,15 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
376
342
 
377
343
  if (typeof symbolOrNumber === "symbol") {
378
344
  return (
379
- symbolOrNumber.description === CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
380
- symbolOrNumber.description === DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
345
+ symbolOrNumber.description === ReactSymbols.CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
346
+ symbolOrNumber.description === ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
381
347
  );
382
348
  }
383
349
 
384
350
  switch (symbolOrNumber) {
385
- case CONCURRENT_MODE_NUMBER:
386
- case CONCURRENT_MODE_SYMBOL_STRING:
387
- 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:
388
354
  return true;
389
355
 
390
356
  default:
@@ -448,75 +414,67 @@ export function traverseFiber(
448
414
  selector: (node: Fiber) => Promise<boolean | void>,
449
415
  ascending?: boolean,
450
416
  ): Promise<Fiber | null>;
417
+ export function traverseFiber(
418
+ fiber: Fiber | null,
419
+ selector: FiberSelector,
420
+ ascending?: boolean,
421
+ ): Fiber | null | Promise<Fiber | null>;
451
422
  export function traverseFiber(
452
423
  fiber: Fiber | null,
453
424
  selector: FiberSelector,
454
425
  ascending = false,
455
426
  ): 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
- }
427
+ return traverseFiberInternal(fiber, selector, ascending);
428
+ }
472
429
 
473
- 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";
474
435
 
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
- }
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
+ };
483
444
 
484
- const traverseFiberSync = (
445
+ const traverseFiberSiblings = (
485
446
  fiber: Fiber | null,
486
447
  selector: FiberSelector,
487
- ascending = false,
488
- ): Fiber | null => {
448
+ ascending: boolean,
449
+ ): Fiber | null | Promise<Fiber | null> => {
489
450
  if (!fiber) return null;
490
- const selection = selector(fiber);
491
- if (selection instanceof Promise) return null;
492
- if (selection === true) return fiber;
493
451
 
494
- let child = ascending ? fiber.return : fiber.child;
495
- while (child) {
496
- const match = traverseFiberSync(child, selector, ascending);
497
- if (match) return match;
498
-
499
- 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
+ );
500
458
  }
501
- return null;
459
+ return match ?? traverseFiberSiblings(nextSibling, selector, ascending);
502
460
  };
503
461
 
504
- const traverseFiberAsync = async (
462
+ const traverseFiberInternal = (
505
463
  fiber: Fiber | null,
506
464
  selector: FiberSelector,
507
- ascending = false,
508
- ): Promise<Fiber | null> => {
465
+ ascending: boolean,
466
+ ): Fiber | null | Promise<Fiber | null> => {
509
467
  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
468
 
517
- 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
+ );
518
474
  }
519
- return null;
475
+ if (selection === true) return fiber;
476
+
477
+ return traverseFiberChildren(fiber, selector, ascending);
520
478
  };
521
479
 
522
480
  /**
@@ -528,7 +486,7 @@ const traverseFiberAsync = async (
528
486
  * console.log(selfTime, totalTime);
529
487
  * ```
530
488
  */
531
- export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime: number } => {
489
+ export const getTimings = (fiber?: Fiber | null): FiberTimings => {
532
490
  const totalTime = fiber?.actualDuration ?? 0;
533
491
  let selfTime = totalTime;
534
492
  // TODO: calculate a DOM time, which is just host component summed up
@@ -552,8 +510,8 @@ export const hasMemoCache = (fiber: Fiber): boolean => {
552
510
  */
553
511
  export const getType = (type: unknown): null | React.ComponentType<unknown> => {
554
512
  if (isComponentType(type)) return type;
555
- if (!isObjectRecord(type)) return null;
556
- return getType(type.type ?? type.render);
513
+ if (typeof type !== "object" || type === null) return null;
514
+ return getType(Reflect.get(type, "type") ?? Reflect.get(type, "render"));
557
515
  };
558
516
 
559
517
  /**
@@ -605,35 +563,35 @@ export const getLatestFiber = (fiber: Fiber): Fiber => {
605
563
  }
606
564
  for (const root of _fiberRoots) {
607
565
  const latestFiber = traverseFiber(root.current, (innerFiber) => {
608
- if (innerFiber === fiber) return true;
566
+ if (innerFiber === fiber || innerFiber === alternate) return true;
609
567
  });
610
568
  if (latestFiber) return latestFiber;
611
569
  }
612
570
  return fiber;
613
571
  };
614
572
 
615
- export type RenderHandler = <S>(fiber: Fiber, phase: RenderPhase, state?: S) => unknown;
616
-
617
573
  export type RenderPhase = "mount" | "unmount" | "update";
618
574
 
619
- let fiberId = 0;
575
+ let nextFiberId = 0;
620
576
  const fiberIdMap = new WeakMap<Fiber, number>();
621
577
 
622
- export const setFiberId = (fiber: Fiber, id: number = fiberId++): void => {
623
- 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
+ }
624
583
  };
625
584
 
626
- // Fiber alternates share an identity across double-buffered commits.
627
585
  export const getFiberId = (fiber: Fiber): number => {
628
- let id = fiberIdMap.get(fiber);
629
- if (id === undefined && fiber.alternate) {
630
- id = fiberIdMap.get(fiber.alternate);
586
+ let currentFiberId = fiberIdMap.get(fiber);
587
+ if (currentFiberId === undefined && fiber.alternate) {
588
+ currentFiberId = fiberIdMap.get(fiber.alternate);
631
589
  }
632
- if (id === undefined) {
633
- id = fiberId++;
634
- setFiberId(fiber, id);
590
+ if (currentFiberId === undefined) {
591
+ currentFiberId = nextFiberId++;
592
+ setFiberId(fiber, currentFiberId);
635
593
  }
636
- return id;
594
+ return currentFiberId;
637
595
  };
638
596
 
639
597
  const mountFiberRecursively = (
@@ -652,7 +610,7 @@ const mountFiberRecursively = (
652
610
  onRender(fiber, "mount");
653
611
  }
654
612
 
655
- if (fiber.tag === SuspenseComponentTag) {
613
+ if (fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent) {
656
614
  const isTimedOut = fiber.memoizedState !== null;
657
615
  if (isTimedOut) {
658
616
  // Special case: if Suspense mounts in a timed-out state,
@@ -682,8 +640,7 @@ const mountFiberRecursively = (
682
640
  const updateFiberRecursively = (
683
641
  onRender: RenderHandler,
684
642
  nextFiber: Fiber,
685
- prevFiber: Fiber,
686
- parentFiber: Fiber | null,
643
+ prevFiber: Fiber | null,
687
644
  ): void => {
688
645
  if (!fiberIdMap.has(nextFiber)) {
689
646
  getFiberId(nextFiber);
@@ -693,7 +650,7 @@ const updateFiberRecursively = (
693
650
  getFiberId(prevFiber);
694
651
  }
695
652
 
696
- const isSuspense = nextFiber.tag === SuspenseComponentTag;
653
+ const isSuspense = nextFiber.tag === getReactWorkTagsForFiber(nextFiber).SuspenseComponent;
697
654
 
698
655
  const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
699
656
  if (shouldIncludeInTree && didFiberRender(nextFiber)) {
@@ -722,7 +679,7 @@ const updateFiberRecursively = (
722
679
  const prevFallbackChildSet = prevFiber.child?.sibling ?? null;
723
680
 
724
681
  if (nextFallbackChildSet !== null && prevFallbackChildSet !== null) {
725
- updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet, nextFiber);
682
+ updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet);
726
683
  }
727
684
  } else if (prevDidTimeout && !nextDidTimeOut) {
728
685
  // Fallback -> Primary:
@@ -761,14 +718,7 @@ const updateFiberRecursively = (
761
718
  // Schedule updates and mounts depending on whether alternates exist.
762
719
  // We don't track deletions here because they are reported separately.
763
720
  if (nextChild.alternate) {
764
- const prevChild = nextChild.alternate;
765
-
766
- updateFiberRecursively(
767
- onRender,
768
- nextChild,
769
- prevChild,
770
- shouldIncludeInTree ? nextFiber : parentFiber,
771
- );
721
+ updateFiberRecursively(onRender, nextChild, nextChild.alternate);
772
722
  } else {
773
723
  mountFiberRecursively(onRender, nextChild, false);
774
724
  }
@@ -780,7 +730,7 @@ const updateFiberRecursively = (
780
730
  };
781
731
 
782
732
  const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
783
- const isRoot = fiber.tag === HostRootTag;
733
+ const isRoot = fiber.tag === getReactWorkTagsForFiber(fiber).HostRoot;
784
734
 
785
735
  if (isRoot || !shouldFilterFiber(fiber)) {
786
736
  onRender(fiber, "unmount");
@@ -789,7 +739,8 @@ const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
789
739
 
790
740
  const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => {
791
741
  // We might meet a nested Suspense on our way.
792
- const isTimedOutSuspense = fiber.tag === SuspenseComponentTag && fiber.memoizedState !== null;
742
+ const isTimedOutSuspense =
743
+ fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent && fiber.memoizedState !== null;
793
744
  let child = fiber.child;
794
745
 
795
746
  if (isTimedOutSuspense) {
@@ -815,7 +766,7 @@ const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber):
815
766
 
816
767
  let commitId = 0;
817
768
  const rootInstanceMap = new WeakMap<
818
- FiberRoot,
769
+ Fiber | FiberRoot,
819
770
  {
820
771
  id: number;
821
772
  prevFiber: Fiber | null;
@@ -829,7 +780,7 @@ const rootInstanceMap = new WeakMap<
829
780
  * console.log(phase)
830
781
  * })
831
782
  */
832
- export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler): void => {
783
+ export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: RenderHandler): void => {
833
784
  const fiber = "current" in root ? root.current : root;
834
785
 
835
786
  let rootInstance = rootInstanceMap.get(root);
@@ -861,7 +812,7 @@ export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler)
861
812
  if (!wasMounted && isMounted) {
862
813
  mountFiberRecursively(onRender, fiber, false);
863
814
  } else if (wasMounted && isMounted) {
864
- updateFiberRecursively(onRender, fiber, fiber.alternate, null);
815
+ updateFiberRecursively(onRender, fiber, fiber.alternate);
865
816
  } else if (wasMounted && !isMounted) {
866
817
  unmountFiber(onRender, fiber);
867
818
  }
@@ -878,9 +829,8 @@ let areOverrideRenderersWired = false;
878
829
  const wireOverrideRenderers = (): void => {
879
830
  if (!hasRDTHook()) return;
880
831
  const rdtHook = getRDTHook();
881
- if (!rdtHook?.renderers) return;
882
832
 
883
- ensureHookDispatchesToListeners(rdtHook);
833
+ setHookEventDispatchers(rdtHook);
884
834
  for (const renderer of rdtHook.renderers.values()) {
885
835
  overrideRenderers.add(renderer);
886
836
  }
@@ -897,14 +847,13 @@ const getRootRenderer = (fiber: Fiber): ReactRenderer | null => {
897
847
  while (hostRootFiber.return) {
898
848
  hostRootFiber = hostRootFiber.return;
899
849
  }
900
- const rendererId = rootRendererIds.get(hostRootFiber.stateNode);
850
+ const fiberRoot = hostRootFiber.stateNode;
851
+ if (!isFiberRoot(fiberRoot)) return null;
852
+ const rendererId = rootRendererIds.get(fiberRoot);
901
853
  if (rendererId === undefined) return null;
902
- return getRDTHook().renderers?.get(rendererId) ?? null;
854
+ return getRDTHook().renderers.get(rendererId) ?? null;
903
855
  };
904
856
 
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
857
  const resolveOverrideRenderers = (fiber: Fiber): ReactRenderer[] => {
909
858
  wireOverrideRenderers();
910
859
  const rootRenderer = getRootRenderer(fiber);
@@ -930,7 +879,7 @@ const getHookStateDispatch = (
930
879
  hookIndex: number,
931
880
  ): ((value: unknown) => void) | null => {
932
881
  let hookState = fiber.memoizedState;
933
- for (let i = 0; i < hookIndex; i++) {
882
+ for (let currentHookIndex = 0; currentHookIndex < hookIndex; currentHookIndex++) {
934
883
  if (!hookState?.next) return null;
935
884
  hookState = hookState.next;
936
885
  }
@@ -963,28 +912,30 @@ const isPOJO = (maybePOJO: unknown): maybePOJO is Record<string, unknown> => {
963
912
  const buildPathsFromValue = (
964
913
  maybePOJO: Record<string, unknown>,
965
914
  basePath: string[] = [],
966
- ): Array<{ path: string[]; value: unknown }> => {
967
- if (!isPOJO(maybePOJO)) {
915
+ ancestors = new WeakSet<object>(),
916
+ ): ValueWrite[] => {
917
+ if (ancestors.has(maybePOJO)) {
968
918
  return [{ path: basePath, value: maybePOJO }];
969
919
  }
970
920
 
971
- const paths: Array<{ path: string[]; value: unknown }> = [];
921
+ ancestors.add(maybePOJO);
922
+ const paths: ValueWrite[] = [];
972
923
 
973
- for (const key in maybePOJO) {
974
- const value = maybePOJO[key];
924
+ for (const [key, value] of Object.entries(maybePOJO)) {
975
925
  const path = basePath.concat(key);
976
926
 
977
927
  if (isPOJO(value)) {
978
- paths.push(...buildPathsFromValue(value, path));
928
+ paths.push(...buildPathsFromValue(value, path, ancestors));
979
929
  } else {
980
930
  paths.push({ path, value });
981
931
  }
982
932
  }
983
933
 
934
+ ancestors.delete(maybePOJO);
984
935
  return paths;
985
936
  };
986
937
 
987
- const buildValueWrites = (partialValue: unknown): Array<{ path: string[]; value: unknown }> =>
938
+ const buildValueWrites = (partialValue: unknown): ValueWrite[] =>
988
939
  isPOJO(partialValue) ? buildPathsFromValue(partialValue) : [{ path: [], value: partialValue }];
989
940
 
990
941
  export const overrideProps = (fiber: Fiber, partialValue: Record<string, unknown>) => {
@@ -1048,18 +999,11 @@ export interface InstrumentationOptions {
1048
999
  onScheduleFiberRoot?: (rendererID: number, root: FiberRoot, children: React.ReactNode) => unknown;
1049
1000
  }
1050
1001
 
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
- >();
1002
+ interface InstrumentationSubscription {
1003
+ options: InstrumentationOptions;
1004
+ }
1005
+
1006
+ const instrumentationSubscriptions = new Set<InstrumentationSubscription>();
1063
1007
 
1064
1008
  interface HookDispatchers {
1065
1009
  onCommitFiberRoot: ReactDevToolsGlobalHook["onCommitFiberRoot"];
@@ -1069,6 +1013,7 @@ interface HookDispatchers {
1069
1013
  }
1070
1014
 
1071
1015
  const hookDispatchers = new WeakMap<ReactDevToolsGlobalHook, Partial<HookDispatchers>>();
1016
+ let didSubscribeToHookReplacements = false;
1072
1017
 
1073
1018
  const rootRendererIds = new WeakMap<FiberRoot, number>();
1074
1019
 
@@ -1077,7 +1022,7 @@ const rootRendererIds = new WeakMap<FiberRoot, number>();
1077
1022
  // next instrument() call installs a fresh wrapper over it; a superseded
1078
1023
  // wrapper still forwards the previous chain but skips the listeners so
1079
1024
  // they never fire twice.
1080
- const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
1025
+ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
1081
1026
  const dispatchers = hookDispatchers.get(rdtHook) ?? {};
1082
1027
  hookDispatchers.set(rdtHook, dispatchers);
1083
1028
 
@@ -1092,8 +1037,11 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1092
1037
  priority,
1093
1038
  didError,
1094
1039
  ) => {
1095
- prevOnCommitFiberRoot?.(rendererID, root, priority, didError);
1040
+ if (prevOnCommitFiberRoot) {
1041
+ prevOnCommitFiberRoot.call(rdtHook, rendererID, root, priority, didError);
1042
+ }
1096
1043
  if (hookDispatchers.get(rdtHook)?.onCommitFiberRoot !== dispatchCommitFiberRoot) return;
1044
+ setReactWorkTagsForFiber(root.current, rdtHook.renderers.get(rendererID));
1097
1045
  const rootMemoizedState = root.current.memoizedState;
1098
1046
  const isUnmounting =
1099
1047
  rootMemoizedState === null ||
@@ -1106,8 +1054,10 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1106
1054
  _fiberRoots.add(root);
1107
1055
  rootRendererIds.set(root, rendererID);
1108
1056
  }
1109
- for (const listener of commitFiberRootListeners) {
1110
- listener(rendererID, root, priority, didError);
1057
+ for (const { options } of instrumentationSubscriptions) {
1058
+ if (options.onCommitFiberRoot) {
1059
+ options.onCommitFiberRoot(rendererID, root, priority, didError);
1060
+ }
1111
1061
  }
1112
1062
  };
1113
1063
  dispatchers.onCommitFiberRoot = dispatchCommitFiberRoot;
@@ -1123,12 +1073,17 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1123
1073
  rendererID,
1124
1074
  fiber,
1125
1075
  ) => {
1126
- prevOnCommitFiberUnmount?.(rendererID, fiber);
1076
+ setReactWorkTagsForFiber(fiber, rdtHook.renderers.get(rendererID));
1077
+ if (prevOnCommitFiberUnmount) {
1078
+ prevOnCommitFiberUnmount.call(rdtHook, rendererID, fiber);
1079
+ }
1127
1080
  if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
1128
1081
  return;
1129
1082
  }
1130
- for (const listener of commitFiberUnmountListeners) {
1131
- listener(rendererID, fiber);
1083
+ for (const { options } of instrumentationSubscriptions) {
1084
+ if (options.onCommitFiberUnmount) {
1085
+ options.onCommitFiberUnmount(rendererID, fiber);
1086
+ }
1132
1087
  }
1133
1088
  };
1134
1089
  dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
@@ -1144,12 +1099,16 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1144
1099
  rendererID,
1145
1100
  root,
1146
1101
  ) => {
1147
- prevOnPostCommitFiberRoot?.(rendererID, root);
1102
+ if (prevOnPostCommitFiberRoot) {
1103
+ prevOnPostCommitFiberRoot.call(rdtHook, rendererID, root);
1104
+ }
1148
1105
  if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
1149
1106
  return;
1150
1107
  }
1151
- for (const listener of postCommitFiberRootListeners) {
1152
- listener(rendererID, root);
1108
+ for (const { options } of instrumentationSubscriptions) {
1109
+ if (options.onPostCommitFiberRoot) {
1110
+ options.onPostCommitFiberRoot(rendererID, root);
1111
+ }
1153
1112
  }
1154
1113
  };
1155
1114
  dispatchers.onPostCommitFiberRoot = dispatchPostCommitFiberRoot;
@@ -1166,10 +1125,14 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1166
1125
  root,
1167
1126
  children,
1168
1127
  ) => {
1169
- prevOnScheduleFiberRoot?.(rendererID, root, children);
1128
+ if (prevOnScheduleFiberRoot) {
1129
+ prevOnScheduleFiberRoot.call(rdtHook, rendererID, root, children);
1130
+ }
1170
1131
  if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
1171
- for (const listener of scheduleFiberRootListeners) {
1172
- listener(rendererID, root, children);
1132
+ for (const { options } of instrumentationSubscriptions) {
1133
+ if (options.onScheduleFiberRoot) {
1134
+ options.onScheduleFiberRoot(rendererID, root, children);
1135
+ }
1173
1136
  }
1174
1137
  };
1175
1138
  dispatchers.onScheduleFiberRoot = dispatchScheduleFiberRoot;
@@ -1182,8 +1145,7 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1182
1145
  * dispatches to a set of listeners, so multiple `instrument` calls compose
1183
1146
  * without stacking patches. Returns an unsubscribe function that removes
1184
1147
  * exactly the handlers this call registered.
1185
- * The returned function is also a `Disposable`, so it composes with other
1186
- * bippy subscriptions through `using`.
1148
+ * The unsubscribe function can also be disposed through `using`.
1187
1149
  * @example
1188
1150
  * const unsubscribe = instrument({
1189
1151
  * onActive() {
@@ -1198,28 +1160,20 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1198
1160
  export const instrument = (options: InstrumentationOptions): Unsubscribe => {
1199
1161
  const rdtHook = getRDTHook(options.onActive);
1200
1162
 
1163
+ if (!didSubscribeToHookReplacements) {
1164
+ onRDTHookReplace(setHookEventDispatchers);
1165
+ didSubscribeToHookReplacements = true;
1166
+ }
1167
+
1201
1168
  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
1202
1169
 
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(() => {
1218
- 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);
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);
1223
1177
  });
1224
1178
  };
1225
1179
 
@@ -1231,30 +1185,35 @@ const isFiberPropertyKey = (key: string): boolean =>
1231
1185
  key.startsWith("__reactInternalInstance$") ||
1232
1186
  key.startsWith("__reactFiber");
1233
1187
 
1234
- const getLegacyRootFiber = (hostInstance: Record<string, unknown>): Fiber | null => {
1235
- const reactRootContainer = hostInstance._reactRootContainer;
1236
- if (!isObjectRecord(reactRootContainer)) return null;
1237
- const internalRoot = reactRootContainer._internalRoot;
1238
- if (!isObjectRecord(internalRoot)) return null;
1239
- const current = internalRoot.current;
1240
- if (!isObjectRecord(current)) return null;
1241
- const child = current.child;
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");
1242
1196
  return isValidFiber(child) ? child : null;
1243
1197
  };
1244
1198
 
1245
- const getInternalInstanceHandle = (hostInstance: Record<string, unknown>): Fiber | null => {
1199
+ const getInternalInstanceHandle = (hostInstance: object): Fiber | null => {
1246
1200
  const internalInstanceHandle =
1247
- hostInstance.__internalInstanceHandle ?? hostInstance._internalInstanceHandle;
1201
+ Reflect.get(hostInstance, "__internalInstanceHandle") ??
1202
+ Reflect.get(hostInstance, "_internalInstanceHandle");
1248
1203
  return isValidFiber(internalInstanceHandle) ? internalInstanceHandle : null;
1249
1204
  };
1250
1205
 
1251
1206
  const getPublicHostInstance = (stateNode: unknown): unknown => {
1252
- if (!isObjectRecord(stateNode)) return stateNode;
1253
- const canonical = stateNode.canonical;
1254
- if (isObjectRecord(canonical) && isObjectRecord(canonical.publicInstance)) {
1255
- return canonical.publicInstance;
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
+ }
1256
1214
  }
1257
- return typeof stateNode._nativeTag === "number" ? stateNode._nativeTag : stateNode;
1215
+ const nativeTag = Reflect.get(stateNode, "_nativeTag");
1216
+ return typeof nativeTag === "number" ? nativeTag : stateNode;
1258
1217
  };
1259
1218
 
1260
1219
  export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
@@ -1268,7 +1227,7 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1268
1227
  }
1269
1228
  }
1270
1229
 
1271
- if (isObjectRecord(hostInstance)) {
1230
+ if (typeof hostInstance === "object" && hostInstance !== null) {
1272
1231
  const legacyRootFiber = getLegacyRootFiber(hostInstance);
1273
1232
  if (legacyRootFiber) return legacyRootFiber;
1274
1233
 
@@ -1276,14 +1235,14 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1276
1235
  if (internalInstanceHandle) return internalInstanceHandle;
1277
1236
 
1278
1237
  for (const knownKey of knownFiberPropertyKeys) {
1279
- const fiber = hostInstance[knownKey];
1238
+ const fiber = Reflect.get(hostInstance, knownKey);
1280
1239
  if (isValidFiber(fiber)) return fiber;
1281
1240
  }
1282
1241
 
1283
1242
  for (const key of Object.keys(hostInstance)) {
1284
1243
  if (isFiberPropertyKey(key)) {
1285
1244
  knownFiberPropertyKeys.add(key);
1286
- const fiber = hostInstance[key];
1245
+ const fiber = Reflect.get(hostInstance, key);
1287
1246
  if (isValidFiber(fiber)) return fiber;
1288
1247
  }
1289
1248
  }
@@ -1307,6 +1266,17 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1307
1266
  return null;
1308
1267
  };
1309
1268
 
1310
- export * from "./rdt-hook.js";
1311
- export * from "./unsubscribe.js";
1312
- 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";