bippy 0.6.1-dev.3a24abf → 0.6.1-dev.4ed5147

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 (53) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +177 -478
  3. package/dist/core.cjs +1 -1
  4. package/dist/core.d.cts +39 -87
  5. package/dist/core.d.ts +39 -87
  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 -68
  12. package/dist/{types.d.ts → errors.d.ts} +187 -68
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +14 -3
  15. package/dist/index.d.ts +14 -3
  16. package/dist/index.js +1 -1
  17. package/dist/install-hook-only.cjs +1 -1
  18. package/dist/install-hook-only.d.cts +8 -0
  19. package/dist/install-hook-only.d.ts +8 -0
  20. package/dist/install-hook-only.js +1 -1
  21. package/dist/rdt-hook.cjs +1 -1
  22. package/dist/rdt-hook.js +1 -1
  23. package/dist/source.cjs +13 -14
  24. package/dist/source.d.cts +86 -69
  25. package/dist/source.d.ts +86 -69
  26. package/dist/source.js +13 -14
  27. package/package.json +14 -7
  28. package/src/core.ts +245 -629
  29. package/src/errors.ts +34 -0
  30. package/src/index.ts +1 -0
  31. package/src/install-hook-only.ts +2 -2
  32. package/src/rdt-hook.ts +152 -110
  33. package/src/react-internals/generated/react-work-tags.ts +318 -0
  34. package/src/react-internals/index.ts +67 -0
  35. package/src/react-internals/semver.ts +80 -0
  36. package/src/{types.ts → react-internals/types.ts} +13 -86
  37. package/src/react.ts +76 -0
  38. package/src/source/error-stack.ts +11 -0
  39. package/src/source/get-display-name-from-source.ts +44 -40
  40. package/src/source/get-source.ts +42 -26
  41. package/src/source/index.ts +11 -1
  42. package/src/source/inspect-hooks.ts +220 -207
  43. package/src/source/owner-stack.ts +86 -128
  44. package/src/source/parse-debug-stack.ts +4 -4
  45. package/src/source/parse-hook-names.ts +14 -53
  46. package/src/source/parse-stack.ts +14 -31
  47. package/src/source/renderer-dispatchers.ts +30 -0
  48. package/src/source/symbolication.ts +703 -135
  49. package/dist/index.iife.js +0 -9
  50. package/dist/install-hook-only.iife.js +0 -9
  51. package/src/generated/react-work-tags.ts +0 -169
  52. package/src/react-internals.ts +0 -67
  53. package/src/unsubscribe.ts +0 -17
package/src/core.ts CHANGED
@@ -3,109 +3,47 @@
3
3
  import type * as React from "react";
4
4
 
5
5
  import type {
6
- ContextDependency,
7
6
  Fiber,
8
7
  FiberRoot,
9
8
  HostFiber,
10
- MemoizedState,
11
9
  ReactDevToolsGlobalHook,
12
10
  ReactRenderer,
13
- } from "./types.js";
11
+ } from "./react-internals/index.js";
14
12
 
15
13
  import {
16
14
  _onActiveListeners,
17
15
  BIPPY_INSTRUMENTATION_STRING,
16
+ createUnsubscribe,
18
17
  getRDTHook,
19
18
  hasRDTHook,
20
19
  isRealReactDevtools,
21
- onRendererInject,
20
+ onRDTHookReplace,
22
21
  } from "./rdt-hook.js";
22
+ import type { Unsubscribe } from "./rdt-hook.js";
23
23
  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,
24
+ getReactWorkTagsForFiber,
43
25
  MutationMask,
44
- OffscreenComponentTag,
45
26
  ReactBuildType,
46
27
  ReactFiberFlags,
47
- SimpleMemoComponentTag,
48
- SuspenseComponentTag,
49
- TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING,
50
- } from "./react-internals.js";
51
- import { toUnsubscribe, type Unsubscribe } from "./unsubscribe.js";
28
+ ReactSymbols,
29
+ setReactWorkTagsForFiber,
30
+ } from "./react-internals/index.js";
52
31
 
53
32
  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;
33
+ BippyError,
34
+ BippyHookInspectionError,
35
+ BippyHookRenderError,
36
+ BippySourceMapError,
37
+ BippyUnsupportedHookError,
38
+ } from "./errors.js";
88
39
 
89
40
  const isComponentType = (value: unknown): value is React.ComponentType<unknown> =>
90
41
  typeof value === "function";
91
42
 
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
43
  const getTypeName = (value: object): string | null => {
106
- const displayName = getPropertyValue(value, "displayName");
44
+ const displayName = "displayName" in value ? value.displayName : null;
107
45
  if (typeof displayName === "string" && displayName) return displayName;
108
- const name = getPropertyValue(value, "name");
46
+ const name = "name" in value ? value.name : null;
109
47
  return typeof name === "string" && name ? name : null;
110
48
  };
111
49
 
@@ -113,6 +51,10 @@ interface FiberSelector {
113
51
  (node: Fiber): boolean | Promise<boolean | void> | void;
114
52
  }
115
53
 
54
+ export interface RenderHandler {
55
+ (fiber: Fiber, phase: RenderPhase): unknown;
56
+ }
57
+
116
58
  /**
117
59
  * Returns `true` if object is a React Element.
118
60
  *
@@ -122,9 +64,8 @@ export const isValidElement = (element: unknown): element is React.ReactElement
122
64
  typeof element === "object" &&
123
65
  element !== null &&
124
66
  "$$typeof" in element &&
125
- [ELEMENT_TYPE_SYMBOL_STRING, TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING].includes(
126
- String(element.$$typeof),
127
- );
67
+ (String(element.$$typeof) === ReactSymbols.LEGACY_ELEMENT_SYMBOL_STRING ||
68
+ String(element.$$typeof) === ReactSymbols.ELEMENT_SYMBOL_STRING);
128
69
 
129
70
  /**
130
71
  * Returns `true` if object is a React Fiber.
@@ -139,17 +80,24 @@ export const isValidFiber = (fiber: unknown): fiber is Fiber =>
139
80
  "sibling" in fiber &&
140
81
  "flags" in fiber;
141
82
 
83
+ const isFiberRoot = (fiberRoot: unknown): fiberRoot is FiberRoot =>
84
+ typeof fiberRoot === "object" &&
85
+ fiberRoot !== null &&
86
+ "current" in fiberRoot &&
87
+ isValidFiber(fiberRoot.current);
88
+
142
89
  /**
143
90
  * Returns `true` if fiber is a host fiber. Host fibers are DOM nodes in react-dom, `View` in react-native, etc.
144
91
  *
145
92
  * @see https://reactnative.dev/architecture/glossary#host-view-tree-and-host-view
146
93
  */
147
94
  export const isHostFiber = (fiber: Fiber): fiber is HostFiber => {
95
+ const workTags = getReactWorkTagsForFiber(fiber);
148
96
  switch (fiber.tag) {
149
- case HostComponentTag:
150
- case HostTextTag:
151
- case HostHoistableTag:
152
- case HostSingletonTag:
97
+ case workTags.HostComponent:
98
+ case workTags.HostText:
99
+ case workTags.HostHoistable:
100
+ case workTags.HostSingleton:
153
101
  return true;
154
102
  default:
155
103
  return false;
@@ -162,12 +110,13 @@ export const isHostFiber = (fiber: Fiber): fiber is HostFiber => {
162
110
  * @see https://reactnative.dev/architecture/glossary#react-composite-components
163
111
  */
164
112
  export const isCompositeFiber = (fiber: Fiber): boolean => {
113
+ const workTags = getReactWorkTagsForFiber(fiber);
165
114
  switch (fiber.tag) {
166
- case ClassComponentTag:
167
- case ForwardRefTag:
168
- case FunctionComponentTag:
169
- case MemoComponentTag:
170
- case SimpleMemoComponentTag:
115
+ case workTags.ClassComponent:
116
+ case workTags.ForwardRef:
117
+ case workTags.FunctionComponent:
118
+ case workTags.MemoComponent:
119
+ case workTags.SimpleMemoComponent:
171
120
  return true;
172
121
  default:
173
122
  return false;
@@ -184,90 +133,6 @@ export const isFiber = (maybeFiber: unknown): maybeFiber is Fiber => {
184
133
  return "pendingProps" in maybeFiber && !("containerInfo" in maybeFiber);
185
134
  };
186
135
 
187
- /**
188
- * Traverses up or down a {@link Fiber}'s contexts, return `true` to stop and select the current and previous context value.
189
- */
190
- export const traverseContexts = (
191
- fiber: Fiber,
192
- selector: (
193
- nextValue: ContextDependency<unknown> | null | undefined,
194
- prevValue: ContextDependency<unknown> | null | undefined,
195
- ) => boolean | void,
196
- ): boolean => {
197
- try {
198
- const nextDependencies = fiber.dependencies;
199
- const prevDependencies = fiber.alternate?.dependencies;
200
-
201
- if (!nextDependencies || !prevDependencies) return false;
202
- if (
203
- typeof nextDependencies !== "object" ||
204
- !("firstContext" in nextDependencies) ||
205
- typeof prevDependencies !== "object" ||
206
- !("firstContext" in prevDependencies)
207
- ) {
208
- return false;
209
- }
210
- let nextContext: ContextDependency<unknown> | null | undefined = nextDependencies.firstContext;
211
- let prevContext: ContextDependency<unknown> | null | undefined = prevDependencies.firstContext;
212
- while (
213
- (nextContext && typeof nextContext === "object" && "memoizedValue" in nextContext) ||
214
- (prevContext && typeof prevContext === "object" && "memoizedValue" in prevContext)
215
- ) {
216
- if (selector(nextContext, prevContext) === true) return true;
217
-
218
- nextContext = nextContext?.next;
219
- prevContext = prevContext?.next;
220
- }
221
- } catch {}
222
- return false;
223
- };
224
-
225
- /**
226
- * Traverses up or down a {@link Fiber}'s states, return `true` to stop and select the current and previous state value. This stores both state values and effects.
227
- */
228
- export const traverseState = (
229
- fiber: Fiber,
230
- selector: (
231
- nextValue: MemoizedState | null | undefined,
232
- prevValue: MemoizedState | null | undefined,
233
- ) => boolean | void,
234
- ): boolean => {
235
- try {
236
- let nextState: MemoizedState | null | undefined = fiber.memoizedState;
237
- let prevState: MemoizedState | null | undefined = fiber.alternate?.memoizedState;
238
-
239
- while (nextState || prevState) {
240
- if (selector(nextState, prevState) === true) return true;
241
-
242
- nextState = nextState?.next;
243
- prevState = prevState?.next;
244
- }
245
- } catch {}
246
- return false;
247
- };
248
-
249
- /**
250
- * Traverses up or down a {@link Fiber}'s props, return `true` to stop and select the current and previous props value.
251
- */
252
- export const traverseProps = (
253
- fiber: Fiber,
254
- selector: (propName: string, nextValue: unknown, prevValue: unknown) => boolean | void,
255
- ): boolean => {
256
- try {
257
- const nextProps = fiber.memoizedProps;
258
- const prevProps = fiber.alternate?.memoizedProps || {};
259
-
260
- for (const propName of Object.keys(nextProps)) {
261
- if (selector(propName, nextProps[propName], prevProps[propName]) === true) return true;
262
- }
263
- for (const propName of Object.keys(prevProps)) {
264
- if (propName in nextProps) continue;
265
- if (selector(propName, nextProps[propName], prevProps[propName]) === true) return true;
266
- }
267
- } catch {}
268
- return false;
269
- };
270
-
271
136
  /**
272
137
  * Returns `true` if the {@link Fiber} has rendered. Note that this does not mean the fiber has rendered in the current commit, just that it has rendered in the past.
273
138
  */
@@ -275,15 +140,16 @@ export const didFiberRender = (fiber: Fiber): boolean => {
275
140
  const nextProps = fiber.memoizedProps;
276
141
  const prevProps = fiber.alternate?.memoizedProps || {};
277
142
  const flags = fiber.flags ?? fiber.effectTag ?? 0;
143
+ const workTags = getReactWorkTagsForFiber(fiber);
278
144
 
279
145
  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;
146
+ case workTags.ClassComponent:
147
+ case workTags.ContextConsumer:
148
+ case workTags.ForwardRef:
149
+ case workTags.FunctionComponent:
150
+ case workTags.MemoComponent:
151
+ case workTags.SimpleMemoComponent: {
152
+ return (flags & ReactFiberFlags.PerformedWork) === ReactFiberFlags.PerformedWork;
287
153
  }
288
154
  default:
289
155
  // Host nodes (DOM, root, etc.)
@@ -301,58 +167,18 @@ export const didFiberRender = (fiber: Fiber): boolean => {
301
167
  */
302
168
  export const didFiberCommit = (fiber: Fiber): boolean => {
303
169
  return Boolean(
304
- (fiber.flags & (MutationMask | Cloned)) !== 0 ||
305
- (fiber.subtreeFlags & (MutationMask | Cloned)) !== 0,
170
+ (fiber.flags & (MutationMask | ReactFiberFlags.Cloned)) !== 0 ||
171
+ (fiber.subtreeFlags & (MutationMask | ReactFiberFlags.Cloned)) !== 0,
306
172
  );
307
173
  };
308
174
 
309
- /**
310
- * Returns all host {@link Fiber}s that have committed and rendered.
311
- */
312
- export const getMutatedHostFibers = (fiber: Fiber): Fiber[] => {
313
- const mutations: Fiber[] = [];
314
- const stack: Fiber[] = [fiber];
315
-
316
- while (stack.length) {
317
- const node = stack.pop();
318
- if (!node) continue;
319
-
320
- if (isHostFiber(node) && didFiberCommit(node) && didFiberRender(node)) {
321
- mutations.push(node);
322
- }
323
-
324
- if (node.child) stack.push(node.child);
325
- if (node.sibling) stack.push(node.sibling);
326
- }
327
-
328
- return mutations;
329
- };
330
-
331
- /**
332
- * Returns the stack of {@link Fiber}s from the current fiber to the root fiber.
333
- *
334
- * @example
335
- * ```ts
336
- * [fiber, fiber.return, fiber.return.return, ...]
337
- * ```
338
- */
339
- export const getFiberStack = (fiber: Fiber): Fiber[] => {
340
- const stack: Fiber[] = [];
341
- let currentFiber = fiber;
342
- while (currentFiber.return) {
343
- stack.push(currentFiber);
344
- const parentFiber = currentFiber.return;
345
- currentFiber = parentFiber;
346
- }
347
- return stack;
348
- };
349
-
350
175
  /**
351
176
  * Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
352
177
  */
353
178
  const shouldFilterFiber = (fiber: Fiber): boolean => {
179
+ const workTags = getReactWorkTagsForFiber(fiber);
354
180
  switch (fiber.tag) {
355
- case DehydratedSuspenseComponentTag:
181
+ case workTags.DehydratedSuspenseComponent:
356
182
  // TODO: ideally we would show dehydrated Suspense immediately.
357
183
  // However, it has some special behavior (like disconnecting
358
184
  // an alternate and turning into real Suspense) which breaks DevTools.
@@ -360,13 +186,13 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
360
186
  // https://github.com/bvaughn/react-devtools-experimental/issues/197
361
187
  return true;
362
188
 
363
- case FragmentTag:
364
- case HostTextTag:
365
- case LegacyHiddenComponentTag:
366
- case OffscreenComponentTag:
189
+ case workTags.Fragment:
190
+ case workTags.HostText:
191
+ case workTags.LegacyHiddenComponent:
192
+ case workTags.OffscreenComponent:
367
193
  return true;
368
194
 
369
- case HostRootTag:
195
+ case workTags.HostRoot:
370
196
  // It is never valid to filter the root element.
371
197
  return false;
372
198
 
@@ -376,15 +202,15 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
376
202
 
377
203
  if (typeof symbolOrNumber === "symbol") {
378
204
  return (
379
- symbolOrNumber.description === CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
380
- symbolOrNumber.description === DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
205
+ symbolOrNumber.description === ReactSymbols.CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
206
+ symbolOrNumber.description === ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
381
207
  );
382
208
  }
383
209
 
384
210
  switch (symbolOrNumber) {
385
- case CONCURRENT_MODE_NUMBER:
386
- case CONCURRENT_MODE_SYMBOL_STRING:
387
- case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
211
+ case ReactSymbols.CONCURRENT_MODE_NUMBER:
212
+ case ReactSymbols.CONCURRENT_MODE_SYMBOL_STRING:
213
+ case ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
388
214
  return true;
389
215
 
390
216
  default:
@@ -394,47 +220,6 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
394
220
  }
395
221
  };
396
222
 
397
- /**
398
- * Returns the nearest host {@link Fiber} to the current {@link Fiber}.
399
- */
400
- export const getNearestHostFiber = (fiber: Fiber, ascending = false): Fiber | null => {
401
- let hostFiber = traverseFiber(fiber, isHostFiber, ascending);
402
- if (!hostFiber) {
403
- hostFiber = traverseFiber(fiber, isHostFiber, !ascending);
404
- }
405
- return hostFiber;
406
- };
407
-
408
- /**
409
- * Returns all host {@link Fiber}s in the tree that are associated with the current {@link Fiber}.
410
- */
411
- export const getNearestHostFibers = (fiber: Fiber): Fiber[] => {
412
- const hostFibers: Fiber[] = [];
413
- const stack: Fiber[] = [];
414
-
415
- if (isHostFiber(fiber)) {
416
- hostFibers.push(fiber);
417
- } else if (fiber.child) {
418
- stack.push(fiber.child);
419
- }
420
-
421
- while (stack.length) {
422
- const currentNode = stack.pop();
423
- if (!currentNode) break;
424
- if (isHostFiber(currentNode)) {
425
- hostFibers.push(currentNode);
426
- } else if (currentNode.child) {
427
- stack.push(currentNode.child);
428
- }
429
-
430
- if (currentNode.sibling) {
431
- stack.push(currentNode.sibling);
432
- }
433
- }
434
-
435
- return hostFibers;
436
- };
437
-
438
223
  /**
439
224
  * Traverses up or down a {@link Fiber}, return `true` to stop and select a node.
440
225
  */
@@ -448,6 +233,11 @@ export function traverseFiber(
448
233
  selector: (node: Fiber) => Promise<boolean | void>,
449
234
  ascending?: boolean,
450
235
  ): Promise<Fiber | null>;
236
+ export function traverseFiber(
237
+ fiber: Fiber | null,
238
+ selector: FiberSelector,
239
+ ascending?: boolean,
240
+ ): Fiber | null | Promise<Fiber | null>;
451
241
  export function traverseFiber(
452
242
  fiber: Fiber | null,
453
243
  selector: FiberSelector,
@@ -455,89 +245,47 @@ export function traverseFiber(
455
245
  ): Fiber | null | Promise<Fiber | null> {
456
246
  if (!fiber) return null;
457
247
 
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
- })();
248
+ const selection = selector(fiber);
249
+ if (isPromiseLike<boolean | void>(selection)) {
250
+ return Promise.resolve(selection).then((didSelectFiber) =>
251
+ didSelectFiber === true ? fiber : traverseFiberChildren(fiber, selector, ascending),
252
+ );
471
253
  }
254
+ if (selection === true) return fiber;
472
255
 
473
- if (firstResult === true) return fiber;
474
-
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;
256
+ return traverseFiberChildren(fiber, selector, ascending);
482
257
  }
483
258
 
484
- const traverseFiberSync = (
485
- fiber: Fiber | null,
486
- selector: FiberSelector,
487
- ascending = false,
488
- ): Fiber | null => {
489
- 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;
259
+ const isPromiseLike = <Result>(value: unknown): value is PromiseLike<Result> =>
260
+ (typeof value === "object" || typeof value === "function") &&
261
+ value !== null &&
262
+ "then" in value &&
263
+ typeof value.then === "function";
498
264
 
499
- child = ascending ? null : child.sibling;
500
- }
501
- return null;
265
+ const traverseFiberChildren = (
266
+ fiber: Fiber,
267
+ selector: FiberSelector,
268
+ ascending: boolean,
269
+ ): Fiber | null | Promise<Fiber | null> => {
270
+ const firstChild = ascending ? fiber.return : fiber.child;
271
+ return traverseFiberSiblings(firstChild, selector, ascending);
502
272
  };
503
273
 
504
- const traverseFiberAsync = async (
274
+ const traverseFiberSiblings = (
505
275
  fiber: Fiber | null,
506
276
  selector: FiberSelector,
507
- ascending = false,
508
- ): Promise<Fiber | null> => {
277
+ ascending: boolean,
278
+ ): Fiber | null | Promise<Fiber | null> => {
509
279
  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
280
 
517
- child = ascending ? null : child.sibling;
281
+ const nextSibling = ascending ? null : fiber.sibling;
282
+ const match = traverseFiber(fiber, selector, ascending);
283
+ if (isPromiseLike<Fiber | null>(match)) {
284
+ return Promise.resolve(match).then(
285
+ (resolvedMatch) => resolvedMatch ?? traverseFiberSiblings(nextSibling, selector, ascending),
286
+ );
518
287
  }
519
- return null;
520
- };
521
-
522
- /**
523
- * Returns the timings of the {@link Fiber}.
524
- *
525
- * @example
526
- * ```ts
527
- * const { selfTime, totalTime } = getTimings(fiber);
528
- * console.log(selfTime, totalTime);
529
- * ```
530
- */
531
- export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime: number } => {
532
- const totalTime = fiber?.actualDuration ?? 0;
533
- let selfTime = totalTime;
534
- // TODO: calculate a DOM time, which is just host component summed up
535
- let child = fiber?.child ?? null;
536
- while (totalTime > 0 && child !== null) {
537
- selfTime -= child.actualDuration ?? 0;
538
- child = child.sibling;
539
- }
540
- return { selfTime, totalTime };
288
+ return match ?? traverseFiberSiblings(nextSibling, selector, ascending);
541
289
  };
542
290
 
543
291
  /**
@@ -552,8 +300,8 @@ export const hasMemoCache = (fiber: Fiber): boolean => {
552
300
  */
553
301
  export const getType = (type: unknown): null | React.ComponentType<unknown> => {
554
302
  if (isComponentType(type)) return type;
555
- if (!isObjectRecord(type)) return null;
556
- return getType(type.type ?? type.render);
303
+ if (typeof type !== "object" || type === null) return null;
304
+ return getType(Reflect.get(type, "type") ?? Reflect.get(type, "render"));
557
305
  };
558
306
 
559
307
  /**
@@ -593,6 +341,7 @@ export const isInstrumentationActive = (): boolean => {
593
341
  };
594
342
 
595
343
  export const _fiberRoots = new Set<FiberRoot>();
344
+ const rootRendererIds = new WeakMap<FiberRoot, number>();
596
345
 
597
346
  /**
598
347
  * Returns the latest fiber (since it may be double-buffered).
@@ -605,35 +354,54 @@ export const getLatestFiber = (fiber: Fiber): Fiber => {
605
354
  }
606
355
  for (const root of _fiberRoots) {
607
356
  const latestFiber = traverseFiber(root.current, (innerFiber) => {
608
- if (innerFiber === fiber) return true;
357
+ if (innerFiber === fiber || innerFiber === alternate) return true;
609
358
  });
610
359
  if (latestFiber) return latestFiber;
611
360
  }
612
361
  return fiber;
613
362
  };
614
363
 
615
- export type RenderHandler = <S>(fiber: Fiber, phase: RenderPhase, state?: S) => unknown;
364
+ /**
365
+ * Returns the renderer that owns the {@link Fiber}.
366
+ */
367
+ export const getRenderer = (fiber: Fiber): ReactRenderer | null => {
368
+ if (!hasRDTHook()) return null;
369
+
370
+ let rootFiber = fiber;
371
+ while (rootFiber.return) {
372
+ rootFiber = rootFiber.return;
373
+ }
374
+
375
+ const fiberRoot = rootFiber.stateNode;
376
+ if (!isFiberRoot(fiberRoot)) return null;
377
+
378
+ const rendererID = rootRendererIds.get(fiberRoot);
379
+ if (rendererID === undefined) return null;
380
+ return getRDTHook().renderers.get(rendererID) ?? null;
381
+ };
616
382
 
617
383
  export type RenderPhase = "mount" | "unmount" | "update";
618
384
 
619
- let fiberId = 0;
385
+ let nextFiberId = 0;
620
386
  const fiberIdMap = new WeakMap<Fiber, number>();
621
387
 
622
- export const setFiberId = (fiber: Fiber, id: number = fiberId++): void => {
623
- fiberIdMap.set(fiber, id);
388
+ export const setFiberId = (fiber: Fiber, fiberId: number = nextFiberId++): void => {
389
+ fiberIdMap.set(fiber, fiberId);
390
+ if (Number.isSafeInteger(fiberId) && fiberId >= nextFiberId) {
391
+ nextFiberId = fiberId + 1;
392
+ }
624
393
  };
625
394
 
626
- // Fiber alternates share an identity across double-buffered commits.
627
395
  export const getFiberId = (fiber: Fiber): number => {
628
- let id = fiberIdMap.get(fiber);
629
- if (id === undefined && fiber.alternate) {
630
- id = fiberIdMap.get(fiber.alternate);
396
+ let currentFiberId = fiberIdMap.get(fiber);
397
+ if (currentFiberId === undefined && fiber.alternate) {
398
+ currentFiberId = fiberIdMap.get(fiber.alternate);
631
399
  }
632
- if (id === undefined) {
633
- id = fiberId++;
634
- setFiberId(fiber, id);
400
+ if (currentFiberId === undefined) {
401
+ currentFiberId = nextFiberId++;
402
+ setFiberId(fiber, currentFiberId);
635
403
  }
636
- return id;
404
+ return currentFiberId;
637
405
  };
638
406
 
639
407
  const mountFiberRecursively = (
@@ -644,15 +412,13 @@ const mountFiberRecursively = (
644
412
  let fiber: Fiber | null = firstChild;
645
413
 
646
414
  while (fiber !== null) {
647
- if (!fiberIdMap.has(fiber)) {
648
- getFiberId(fiber);
649
- }
415
+ getFiberId(fiber);
650
416
  const shouldIncludeInTree = !shouldFilterFiber(fiber);
651
417
  if (shouldIncludeInTree && didFiberRender(fiber)) {
652
418
  onRender(fiber, "mount");
653
419
  }
654
420
 
655
- if (fiber.tag === SuspenseComponentTag) {
421
+ if (fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent) {
656
422
  const isTimedOut = fiber.memoizedState !== null;
657
423
  if (isTimedOut) {
658
424
  // Special case: if Suspense mounts in a timed-out state,
@@ -682,18 +448,13 @@ const mountFiberRecursively = (
682
448
  const updateFiberRecursively = (
683
449
  onRender: RenderHandler,
684
450
  nextFiber: Fiber,
685
- prevFiber: Fiber,
686
- parentFiber: Fiber | null,
451
+ prevFiber: Fiber | null,
687
452
  ): void => {
688
- if (!fiberIdMap.has(nextFiber)) {
689
- getFiberId(nextFiber);
690
- }
453
+ getFiberId(nextFiber);
691
454
  if (!prevFiber) return;
692
- if (!fiberIdMap.has(prevFiber)) {
693
- getFiberId(prevFiber);
694
- }
455
+ getFiberId(prevFiber);
695
456
 
696
- const isSuspense = nextFiber.tag === SuspenseComponentTag;
457
+ const isSuspense = nextFiber.tag === getReactWorkTagsForFiber(nextFiber).SuspenseComponent;
697
458
 
698
459
  const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
699
460
  if (shouldIncludeInTree && didFiberRender(nextFiber)) {
@@ -722,7 +483,7 @@ const updateFiberRecursively = (
722
483
  const prevFallbackChildSet = prevFiber.child?.sibling ?? null;
723
484
 
724
485
  if (nextFallbackChildSet !== null && prevFallbackChildSet !== null) {
725
- updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet, nextFiber);
486
+ updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet);
726
487
  }
727
488
  } else if (prevDidTimeout && !nextDidTimeOut) {
728
489
  // Fallback -> Primary:
@@ -761,14 +522,7 @@ const updateFiberRecursively = (
761
522
  // Schedule updates and mounts depending on whether alternates exist.
762
523
  // We don't track deletions here because they are reported separately.
763
524
  if (nextChild.alternate) {
764
- const prevChild = nextChild.alternate;
765
-
766
- updateFiberRecursively(
767
- onRender,
768
- nextChild,
769
- prevChild,
770
- shouldIncludeInTree ? nextFiber : parentFiber,
771
- );
525
+ updateFiberRecursively(onRender, nextChild, nextChild.alternate);
772
526
  } else {
773
527
  mountFiberRecursively(onRender, nextChild, false);
774
528
  }
@@ -780,7 +534,7 @@ const updateFiberRecursively = (
780
534
  };
781
535
 
782
536
  const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
783
- const isRoot = fiber.tag === HostRootTag;
537
+ const isRoot = fiber.tag === getReactWorkTagsForFiber(fiber).HostRoot;
784
538
 
785
539
  if (isRoot || !shouldFilterFiber(fiber)) {
786
540
  onRender(fiber, "unmount");
@@ -789,7 +543,8 @@ const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
789
543
 
790
544
  const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => {
791
545
  // We might meet a nested Suspense on our way.
792
- const isTimedOutSuspense = fiber.tag === SuspenseComponentTag && fiber.memoizedState !== null;
546
+ const isTimedOutSuspense =
547
+ fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent && fiber.memoizedState !== null;
793
548
  let child = fiber.child;
794
549
 
795
550
  if (isTimedOutSuspense) {
@@ -813,11 +568,9 @@ const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber):
813
568
  }
814
569
  };
815
570
 
816
- let commitId = 0;
817
571
  const rootInstanceMap = new WeakMap<
818
- FiberRoot,
572
+ Fiber | FiberRoot,
819
573
  {
820
- id: number;
821
574
  prevFiber: Fiber | null;
822
575
  }
823
576
  >();
@@ -829,23 +582,23 @@ const rootInstanceMap = new WeakMap<
829
582
  * console.log(phase)
830
583
  * })
831
584
  */
832
- export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler): void => {
585
+ export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: RenderHandler): void => {
833
586
  const fiber = "current" in root ? root.current : root;
834
587
 
835
588
  let rootInstance = rootInstanceMap.get(root);
836
589
 
837
590
  if (!rootInstance) {
838
- rootInstance = { id: commitId++, prevFiber: null };
591
+ rootInstance = { prevFiber: null };
839
592
  rootInstanceMap.set(root, rootInstance);
840
593
  }
841
594
 
842
595
  const { prevFiber } = rootInstance;
843
- // if fiberRoot don't have current instance, means it's been unmounted
844
596
  if (!fiber) {
845
- unmountFiber(onRender, fiber);
597
+ if (prevFiber) {
598
+ unmountFiber(onRender, prevFiber);
599
+ }
846
600
  } else if (prevFiber !== null) {
847
601
  const wasMounted =
848
- prevFiber &&
849
602
  prevFiber.memoizedState !== null &&
850
603
  prevFiber.memoizedState.element !== null &&
851
604
  prevFiber.memoizedState.element !== undefined &&
@@ -861,7 +614,7 @@ export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler)
861
614
  if (!wasMounted && isMounted) {
862
615
  mountFiberRecursively(onRender, fiber, false);
863
616
  } else if (wasMounted && isMounted) {
864
- updateFiberRecursively(onRender, fiber, fiber.alternate, null);
617
+ updateFiberRecursively(onRender, fiber, fiber.alternate);
865
618
  } else if (wasMounted && !isMounted) {
866
619
  unmountFiber(onRender, fiber);
867
620
  }
@@ -872,168 +625,6 @@ export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler)
872
625
  rootInstance.prevFiber = fiber;
873
626
  };
874
627
 
875
- const overrideRenderers = new Set<ReactRenderer>();
876
- let areOverrideRenderersWired = false;
877
-
878
- const wireOverrideRenderers = (): void => {
879
- if (!hasRDTHook()) return;
880
- const rdtHook = getRDTHook();
881
- if (!rdtHook?.renderers) return;
882
-
883
- ensureHookDispatchesToListeners(rdtHook);
884
- for (const renderer of rdtHook.renderers.values()) {
885
- overrideRenderers.add(renderer);
886
- }
887
- if (areOverrideRenderersWired) return;
888
- areOverrideRenderersWired = true;
889
- onRendererInject((renderer) => {
890
- overrideRenderers.add(renderer);
891
- });
892
- };
893
-
894
- const getRootRenderer = (fiber: Fiber): ReactRenderer | null => {
895
- if (!hasRDTHook()) return null;
896
- let hostRootFiber = fiber;
897
- while (hostRootFiber.return) {
898
- hostRootFiber = hostRootFiber.return;
899
- }
900
- const rendererId = rootRendererIds.get(hostRootFiber.stateNode);
901
- if (rendererId === undefined) return null;
902
- return getRDTHook().renderers?.get(rendererId) ?? null;
903
- };
904
-
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
- const resolveOverrideRenderers = (fiber: Fiber): ReactRenderer[] => {
909
- wireOverrideRenderers();
910
- const rootRenderer = getRootRenderer(fiber);
911
- if (rootRenderer) return [rootRenderer];
912
- return Array.from(overrideRenderers);
913
- };
914
-
915
- const applyPropsOverride = (
916
- renderers: ReactRenderer[],
917
- fiber: Fiber,
918
- path: string[],
919
- value: unknown,
920
- ): void => {
921
- for (const renderer of renderers) {
922
- try {
923
- renderer.overrideProps?.(fiber, path, value);
924
- } catch {}
925
- }
926
- };
927
-
928
- const getHookStateDispatch = (
929
- fiber: Fiber,
930
- hookIndex: number,
931
- ): ((value: unknown) => void) | null => {
932
- let hookState = fiber.memoizedState;
933
- for (let i = 0; i < hookIndex; i++) {
934
- if (!hookState?.next) return null;
935
- hookState = hookState.next;
936
- }
937
- const queue = hookState?.queue;
938
- if (!isPOJO(queue)) return null;
939
- const dispatch = queue.dispatch;
940
- return typeof dispatch === "function" ? (value) => dispatch(value) : null;
941
- };
942
-
943
- const findContextProviderFiber = (fiber: Fiber, contextType: unknown): Fiber | null => {
944
- let currentFiber: Fiber | null = fiber;
945
- while (currentFiber) {
946
- const fiberType = currentFiber.type;
947
- if (fiberType === contextType || fiberType?.Provider === contextType) {
948
- return currentFiber;
949
- }
950
- currentFiber = currentFiber.return;
951
- }
952
- return null;
953
- };
954
-
955
- const isPOJO = (maybePOJO: unknown): maybePOJO is Record<string, unknown> => {
956
- return (
957
- Object.prototype.toString.call(maybePOJO) === "[object Object]" &&
958
- (Object.getPrototypeOf(maybePOJO) === Object.prototype ||
959
- Object.getPrototypeOf(maybePOJO) === null)
960
- );
961
- };
962
-
963
- const buildPathsFromValue = (
964
- maybePOJO: Record<string, unknown>,
965
- basePath: string[] = [],
966
- ): Array<{ path: string[]; value: unknown }> => {
967
- if (!isPOJO(maybePOJO)) {
968
- return [{ path: basePath, value: maybePOJO }];
969
- }
970
-
971
- const paths: Array<{ path: string[]; value: unknown }> = [];
972
-
973
- for (const key in maybePOJO) {
974
- const value = maybePOJO[key];
975
- const path = basePath.concat(key);
976
-
977
- if (isPOJO(value)) {
978
- paths.push(...buildPathsFromValue(value, path));
979
- } else {
980
- paths.push({ path, value });
981
- }
982
- }
983
-
984
- return paths;
985
- };
986
-
987
- const buildValueWrites = (partialValue: unknown): Array<{ path: string[]; value: unknown }> =>
988
- isPOJO(partialValue) ? buildPathsFromValue(partialValue) : [{ path: [], value: partialValue }];
989
-
990
- export const overrideProps = (fiber: Fiber, partialValue: Record<string, unknown>) => {
991
- const renderers = resolveOverrideRenderers(fiber);
992
- for (const { path, value } of buildValueWrites(partialValue)) {
993
- applyPropsOverride(renderers, fiber, path, value);
994
- }
995
- };
996
-
997
- export const overrideHookState = (fiber: Fiber, id: number, partialValue: unknown) => {
998
- const renderers = resolveOverrideRenderers(fiber).filter((renderer) =>
999
- Boolean(renderer.overrideHookState),
1000
- );
1001
- const writes = buildValueWrites(partialValue);
1002
-
1003
- if (renderers.length > 0) {
1004
- for (const renderer of renderers) {
1005
- for (const { path, value } of writes) {
1006
- try {
1007
- renderer.overrideHookState?.(fiber, id, path, value);
1008
- } catch {}
1009
- }
1010
- }
1011
- return;
1012
- }
1013
-
1014
- // production renderers don't expose overrideHookState; dispatching through
1015
- // the hook's own queue still works there, but only for whole-value writes
1016
- // (a path write through dispatch would replace the entire hook state)
1017
- if (isPOJO(partialValue)) return;
1018
- const dispatch = getHookStateDispatch(fiber, id);
1019
- if (!dispatch) return;
1020
- try {
1021
- dispatch(partialValue);
1022
- } catch {}
1023
- };
1024
-
1025
- export const overrideContext = (fiber: Fiber, contextType: unknown, partialValue: unknown) => {
1026
- const providerFiber = findContextProviderFiber(fiber, contextType);
1027
- if (!providerFiber) return;
1028
- const renderers = resolveOverrideRenderers(providerFiber);
1029
- for (const { path, value } of buildValueWrites(partialValue)) {
1030
- applyPropsOverride(renderers, providerFiber, ["value", ...path], value);
1031
- if (providerFiber.alternate) {
1032
- applyPropsOverride(renderers, providerFiber.alternate, ["value", ...path], value);
1033
- }
1034
- }
1035
- };
1036
-
1037
628
  export interface InstrumentationOptions {
1038
629
  name?: string;
1039
630
  onActive?: () => unknown;
@@ -1048,18 +639,11 @@ export interface InstrumentationOptions {
1048
639
  onScheduleFiberRoot?: (rendererID: number, root: FiberRoot, children: React.ReactNode) => unknown;
1049
640
  }
1050
641
 
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
- >();
642
+ interface InstrumentationSubscription {
643
+ options: InstrumentationOptions;
644
+ }
645
+
646
+ const instrumentationSubscriptions = new Set<InstrumentationSubscription>();
1063
647
 
1064
648
  interface HookDispatchers {
1065
649
  onCommitFiberRoot: ReactDevToolsGlobalHook["onCommitFiberRoot"];
@@ -1069,15 +653,14 @@ interface HookDispatchers {
1069
653
  }
1070
654
 
1071
655
  const hookDispatchers = new WeakMap<ReactDevToolsGlobalHook, Partial<HookDispatchers>>();
1072
-
1073
- const rootRendererIds = new WeakMap<FiberRoot, number>();
656
+ let didSubscribeToHookReplacements = false;
1074
657
 
1075
658
  // each hook event is dispatched from a single re-installable wrapper. If
1076
659
  // something overwrites the hook method (devtools, direct assignment), the
1077
660
  // next instrument() call installs a fresh wrapper over it; a superseded
1078
661
  // wrapper still forwards the previous chain but skips the listeners so
1079
662
  // they never fire twice.
1080
- const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
663
+ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
1081
664
  const dispatchers = hookDispatchers.get(rdtHook) ?? {};
1082
665
  hookDispatchers.set(rdtHook, dispatchers);
1083
666
 
@@ -1092,8 +675,11 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1092
675
  priority,
1093
676
  didError,
1094
677
  ) => {
1095
- prevOnCommitFiberRoot?.(rendererID, root, priority, didError);
678
+ if (prevOnCommitFiberRoot) {
679
+ prevOnCommitFiberRoot.call(rdtHook, rendererID, root, priority, didError);
680
+ }
1096
681
  if (hookDispatchers.get(rdtHook)?.onCommitFiberRoot !== dispatchCommitFiberRoot) return;
682
+ setReactWorkTagsForFiber(root.current, rdtHook.renderers.get(rendererID));
1097
683
  const rootMemoizedState = root.current.memoizedState;
1098
684
  const isUnmounting =
1099
685
  rootMemoizedState === null ||
@@ -1106,8 +692,10 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1106
692
  _fiberRoots.add(root);
1107
693
  rootRendererIds.set(root, rendererID);
1108
694
  }
1109
- for (const listener of commitFiberRootListeners) {
1110
- listener(rendererID, root, priority, didError);
695
+ for (const { options } of instrumentationSubscriptions) {
696
+ if (options.onCommitFiberRoot) {
697
+ options.onCommitFiberRoot(rendererID, root, priority, didError);
698
+ }
1111
699
  }
1112
700
  };
1113
701
  dispatchers.onCommitFiberRoot = dispatchCommitFiberRoot;
@@ -1123,12 +711,17 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1123
711
  rendererID,
1124
712
  fiber,
1125
713
  ) => {
1126
- prevOnCommitFiberUnmount?.(rendererID, fiber);
714
+ setReactWorkTagsForFiber(fiber, rdtHook.renderers.get(rendererID));
715
+ if (prevOnCommitFiberUnmount) {
716
+ prevOnCommitFiberUnmount.call(rdtHook, rendererID, fiber);
717
+ }
1127
718
  if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
1128
719
  return;
1129
720
  }
1130
- for (const listener of commitFiberUnmountListeners) {
1131
- listener(rendererID, fiber);
721
+ for (const { options } of instrumentationSubscriptions) {
722
+ if (options.onCommitFiberUnmount) {
723
+ options.onCommitFiberUnmount(rendererID, fiber);
724
+ }
1132
725
  }
1133
726
  };
1134
727
  dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
@@ -1144,12 +737,16 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1144
737
  rendererID,
1145
738
  root,
1146
739
  ) => {
1147
- prevOnPostCommitFiberRoot?.(rendererID, root);
740
+ if (prevOnPostCommitFiberRoot) {
741
+ prevOnPostCommitFiberRoot.call(rdtHook, rendererID, root);
742
+ }
1148
743
  if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
1149
744
  return;
1150
745
  }
1151
- for (const listener of postCommitFiberRootListeners) {
1152
- listener(rendererID, root);
746
+ for (const { options } of instrumentationSubscriptions) {
747
+ if (options.onPostCommitFiberRoot) {
748
+ options.onPostCommitFiberRoot(rendererID, root);
749
+ }
1153
750
  }
1154
751
  };
1155
752
  dispatchers.onPostCommitFiberRoot = dispatchPostCommitFiberRoot;
@@ -1166,10 +763,14 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1166
763
  root,
1167
764
  children,
1168
765
  ) => {
1169
- prevOnScheduleFiberRoot?.(rendererID, root, children);
766
+ if (prevOnScheduleFiberRoot) {
767
+ prevOnScheduleFiberRoot.call(rdtHook, rendererID, root, children);
768
+ }
1170
769
  if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
1171
- for (const listener of scheduleFiberRootListeners) {
1172
- listener(rendererID, root, children);
770
+ for (const { options } of instrumentationSubscriptions) {
771
+ if (options.onScheduleFiberRoot) {
772
+ options.onScheduleFiberRoot(rendererID, root, children);
773
+ }
1173
774
  }
1174
775
  };
1175
776
  dispatchers.onScheduleFiberRoot = dispatchScheduleFiberRoot;
@@ -1177,13 +778,24 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1177
778
  }
1178
779
  };
1179
780
 
781
+ const wireHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
782
+ if (!didSubscribeToHookReplacements) {
783
+ onRDTHookReplace(setHookEventDispatchers);
784
+ didSubscribeToHookReplacements = true;
785
+ }
786
+ setHookEventDispatchers(rdtHook);
787
+ };
788
+
789
+ if (hasRDTHook()) {
790
+ wireHookEventDispatchers(getRDTHook());
791
+ }
792
+
1180
793
  /**
1181
794
  * Instruments the DevTools hook. Each hook event is patched once and
1182
795
  * dispatches to a set of listeners, so multiple `instrument` calls compose
1183
796
  * without stacking patches. Returns an unsubscribe function that removes
1184
797
  * exactly the handlers this call registered.
1185
- * The returned function is also a `Disposable`, so it composes with other
1186
- * bippy subscriptions through `using`.
798
+ * The unsubscribe function can also be disposed through `using`.
1187
799
  * @example
1188
800
  * const unsubscribe = instrument({
1189
801
  * onActive() {
@@ -1197,29 +809,15 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1197
809
  */
1198
810
  export const instrument = (options: InstrumentationOptions): Unsubscribe => {
1199
811
  const rdtHook = getRDTHook(options.onActive);
1200
-
1201
812
  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
1202
813
 
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);
814
+ wireHookEventDispatchers(rdtHook);
815
+ const subscription: InstrumentationSubscription = { options };
816
+ instrumentationSubscriptions.add(subscription);
817
+
818
+ return createUnsubscribe(() => {
819
+ if (options.onActive) _onActiveListeners.delete(options.onActive);
820
+ instrumentationSubscriptions.delete(subscription);
1223
821
  });
1224
822
  };
1225
823
 
@@ -1231,33 +829,38 @@ const isFiberPropertyKey = (key: string): boolean =>
1231
829
  key.startsWith("__reactInternalInstance$") ||
1232
830
  key.startsWith("__reactFiber");
1233
831
 
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;
832
+ const getLegacyRootFiber = (hostInstance: object): Fiber | null => {
833
+ const reactRootContainer = Reflect.get(hostInstance, "_reactRootContainer");
834
+ if (typeof reactRootContainer !== "object" || reactRootContainer === null) return null;
835
+ const internalRoot = Reflect.get(reactRootContainer, "_internalRoot");
836
+ if (typeof internalRoot !== "object" || internalRoot === null) return null;
837
+ const current = Reflect.get(internalRoot, "current");
838
+ if (typeof current !== "object" || current === null) return null;
839
+ const child = Reflect.get(current, "child");
1242
840
  return isValidFiber(child) ? child : null;
1243
841
  };
1244
842
 
1245
- const getInternalInstanceHandle = (hostInstance: Record<string, unknown>): Fiber | null => {
843
+ const getInternalInstanceHandle = (hostInstance: object): Fiber | null => {
1246
844
  const internalInstanceHandle =
1247
- hostInstance.__internalInstanceHandle ?? hostInstance._internalInstanceHandle;
845
+ Reflect.get(hostInstance, "__internalInstanceHandle") ??
846
+ Reflect.get(hostInstance, "_internalInstanceHandle");
1248
847
  return isValidFiber(internalInstanceHandle) ? internalInstanceHandle : null;
1249
848
  };
1250
849
 
1251
850
  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;
851
+ if (typeof stateNode !== "object" || stateNode === null) return stateNode;
852
+ const canonical = Reflect.get(stateNode, "canonical");
853
+ if (typeof canonical === "object" && canonical !== null) {
854
+ const publicInstance = Reflect.get(canonical, "publicInstance");
855
+ if (typeof publicInstance === "object" && publicInstance !== null) {
856
+ return publicInstance;
857
+ }
1256
858
  }
1257
- return typeof stateNode._nativeTag === "number" ? stateNode._nativeTag : stateNode;
859
+ const nativeTag = Reflect.get(stateNode, "_nativeTag");
860
+ return typeof nativeTag === "number" ? nativeTag : stateNode;
1258
861
  };
1259
862
 
1260
- export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
863
+ export const getFiber = <T>(hostInstance: T): Fiber | null => {
1261
864
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1262
865
  if (rdtHook?.renderers) {
1263
866
  for (const renderer of rdtHook.renderers.values()) {
@@ -1268,7 +871,7 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1268
871
  }
1269
872
  }
1270
873
 
1271
- if (isObjectRecord(hostInstance)) {
874
+ if (typeof hostInstance === "object" && hostInstance !== null) {
1272
875
  const legacyRootFiber = getLegacyRootFiber(hostInstance);
1273
876
  if (legacyRootFiber) return legacyRootFiber;
1274
877
 
@@ -1276,14 +879,14 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1276
879
  if (internalInstanceHandle) return internalInstanceHandle;
1277
880
 
1278
881
  for (const knownKey of knownFiberPropertyKeys) {
1279
- const fiber = hostInstance[knownKey];
882
+ const fiber = Reflect.get(hostInstance, knownKey);
1280
883
  if (isValidFiber(fiber)) return fiber;
1281
884
  }
1282
885
 
1283
886
  for (const key of Object.keys(hostInstance)) {
1284
887
  if (isFiberPropertyKey(key)) {
1285
888
  knownFiberPropertyKeys.add(key);
1286
- const fiber = hostInstance[key];
889
+ const fiber = Reflect.get(hostInstance, key);
1287
890
  if (isValidFiber(fiber)) return fiber;
1288
891
  }
1289
892
  }
@@ -1307,6 +910,19 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1307
910
  return null;
1308
911
  };
1309
912
 
1310
- export * from "./rdt-hook.js";
1311
- export * from "./unsubscribe.js";
1312
- export type * from "./types.js";
913
+ export const getFiberFromHostInstance = getFiber;
914
+
915
+ export {
916
+ BIPPY_INSTRUMENTATION_STRING,
917
+ _onActiveListeners,
918
+ _renderers,
919
+ getRDTHook,
920
+ hasRDTHook,
921
+ installRDTHook,
922
+ isRealReactDevtools,
923
+ onRendererInject,
924
+ patchRDTHook,
925
+ version,
926
+ } from "./rdt-hook.js";
927
+ export type { Unsubscribe } from "./rdt-hook.js";
928
+ export * from "./react-internals/index.js";