bippy 0.6.1-dev.61475ed → 0.6.1-dev.879f777

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 (69) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +177 -500
  3. package/dist/core.cjs +1 -1
  4. package/dist/core.d.cts +45 -105
  5. package/dist/core.d.ts +45 -105
  6. package/dist/core.js +1 -1
  7. package/dist/core2.cjs +9 -0
  8. package/dist/core2.d.cts +11 -3
  9. package/dist/core2.d.ts +11 -3
  10. package/dist/core2.js +9 -0
  11. package/dist/errors.d.cts +410 -0
  12. package/dist/errors.d.ts +410 -0
  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 +9 -1
  19. package/dist/install-hook-only.d.ts +9 -1
  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 +14 -5
  24. package/dist/source.d.cts +86 -69
  25. package/dist/source.d.ts +86 -69
  26. package/dist/source.js +14 -5
  27. package/package.json +26 -17
  28. package/src/core.ts +340 -673
  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 +160 -161
  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/react-internals/types.ts +186 -0
  37. package/src/react.ts +76 -0
  38. package/src/source/constants.ts +1 -1
  39. package/src/source/error-stack.ts +11 -0
  40. package/src/source/get-display-name-from-source.ts +44 -40
  41. package/src/source/get-source.ts +43 -26
  42. package/src/source/index.ts +11 -1
  43. package/src/source/inspect-hooks.ts +220 -207
  44. package/src/source/owner-stack.ts +119 -174
  45. package/src/source/parse-debug-stack.ts +4 -4
  46. package/src/source/parse-hook-names.ts +14 -53
  47. package/src/source/parse-stack.ts +14 -31
  48. package/src/source/renderer-dispatchers.ts +30 -0
  49. package/src/source/symbolication.ts +709 -120
  50. package/dist/get-source.cjs +0 -19
  51. package/dist/get-source.js +0 -19
  52. package/dist/index.iife.js +0 -9
  53. package/dist/install-hook-only.iife.js +0 -9
  54. package/dist/react-refresh.cjs +0 -9
  55. package/dist/react-refresh.d.cts +0 -66
  56. package/dist/react-refresh.d.ts +0 -66
  57. package/dist/react-refresh.js +0 -9
  58. package/dist/unsubscribe.d.cts +0 -298
  59. package/dist/unsubscribe.d.ts +0 -298
  60. package/src/react-refresh/constants.ts +0 -9
  61. package/src/react-refresh/detect-hmr-transport.ts +0 -33
  62. package/src/react-refresh/index.ts +0 -173
  63. package/src/react-refresh/metro-hmr-transport.ts +0 -188
  64. package/src/react-refresh/next-webpack-hmr-transport.ts +0 -72
  65. package/src/react-refresh/normalize-hmr-file-path.ts +0 -24
  66. package/src/react-refresh/types.ts +0 -7
  67. package/src/react-refresh/vite-hmr-transport.ts +0 -116
  68. package/src/types.ts +0 -438
  69. package/src/unsubscribe.ts +0 -17
package/src/core.ts CHANGED
@@ -1,71 +1,59 @@
1
- // [!!!] IMPORTANT: do not import React in this file
2
- // since it will be executed before the react devtools hook is created
1
+ // React must remain a type-only import because this module loads immediately after the DevTools hook.
3
2
 
4
3
  import type * as React from "react";
5
4
 
6
5
  import type {
7
- ContextDependency,
8
6
  Fiber,
9
7
  FiberRoot,
10
- MemoizedState,
8
+ HostFiber,
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
- isReactRefresh,
21
19
  isRealReactDevtools,
22
- onRendererInject,
20
+ onRDTHookReplace,
23
21
  } from "./rdt-hook.js";
24
- import { toUnsubscribe, type Unsubscribe } from "./unsubscribe.js";
25
-
26
- // https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactWorkTags.js
27
- export const FunctionComponentTag = 0;
28
- export const ClassComponentTag = 1;
29
- export const HostRootTag = 3;
30
- export const HostPortalTag = 4;
31
- export const HostComponentTag = 5;
32
- export const HostTextTag = 6;
33
- export const FragmentTag = 7;
34
- export const ContextConsumerTag = 9;
35
- export const ForwardRefTag = 11;
36
- export const SuspenseComponentTag = 13;
37
- export const MemoComponentTag = 14;
38
- export const SimpleMemoComponentTag = 15;
39
- export const LazyComponentTag = 16;
40
- export const DehydratedSuspenseComponentTag = 18;
41
- export const SuspenseListComponentTag = 19;
42
- export const OffscreenComponentTag = 22;
43
- export const LegacyHiddenComponentTag = 23;
44
- export const HostHoistableTag = 26;
45
- export const HostSingletonTag = 27;
46
- export const ActivityComponentTag = 28;
47
- export const ViewTransitionComponentTag = 30;
48
-
49
- export const CONCURRENT_MODE_NUMBER = 0xeacf;
50
- export const ELEMENT_TYPE_SYMBOL_STRING = "Symbol(react.element)";
51
- export const TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING = "Symbol(react.transitional.element)";
52
- export const CONCURRENT_MODE_SYMBOL_STRING = "Symbol(react.concurrent_mode)";
53
- export const DEPRECATED_ASYNC_MODE_SYMBOL_STRING = "Symbol(react.async_mode)";
54
- export const CONCURRENT_MODE_SYMBOL_DESCRIPTION = "react.concurrent_mode";
55
- export const DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION = "react.async_mode";
56
-
57
- // https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberFlags.js
58
- const PerformedWork = 0b1;
59
- const Placement = 0b10;
60
- const Hydrating = 0b1000000000000;
61
- const Update = 0b100;
62
- const Cloned = 0b1000;
63
- const ChildDeletion = 0b10000;
64
- const ContentReset = 0b100000;
65
- const Snapshot = 0b10000000000;
66
- const Visibility = 0b10000000000000;
67
- const MutationMask =
68
- Placement | Update | ChildDeletion | ContentReset | Hydrating | Visibility | Snapshot;
22
+ import type { Unsubscribe } from "./rdt-hook.js";
23
+ import {
24
+ getReactWorkTagsForFiber,
25
+ MutationMask,
26
+ ReactBuildType,
27
+ ReactFiberFlags,
28
+ ReactSymbols,
29
+ setReactWorkTagsForFiber,
30
+ } from "./react-internals/index.js";
31
+
32
+ export {
33
+ BippyError,
34
+ BippyHookInspectionError,
35
+ BippyHookRenderError,
36
+ BippySourceMapError,
37
+ BippyUnsupportedHookError,
38
+ } from "./errors.js";
39
+
40
+ const isComponentType = (value: unknown): value is React.ComponentType<unknown> =>
41
+ typeof value === "function";
42
+
43
+ const getTypeName = (value: object): string | null => {
44
+ const displayName = "displayName" in value ? value.displayName : null;
45
+ if (typeof displayName === "string" && displayName) return displayName;
46
+ const name = "name" in value ? value.name : null;
47
+ return typeof name === "string" && name ? name : null;
48
+ };
49
+
50
+ interface FiberSelector {
51
+ (node: Fiber): boolean | Promise<boolean | void> | void;
52
+ }
53
+
54
+ export interface RenderHandler {
55
+ (fiber: Fiber, phase: RenderPhase): unknown;
56
+ }
69
57
 
70
58
  /**
71
59
  * Returns `true` if object is a React Element.
@@ -74,19 +62,17 @@ const MutationMask =
74
62
  */
75
63
  export const isValidElement = (element: unknown): element is React.ReactElement =>
76
64
  typeof element === "object" &&
77
- element != null &&
65
+ element !== null &&
78
66
  "$$typeof" in element &&
79
- // react 18 uses Symbol.for('react.element'), react 19 uses Symbol.for('react.transitional.element')
80
- [ELEMENT_TYPE_SYMBOL_STRING, TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING].includes(
81
- String(element.$$typeof),
82
- );
67
+ (String(element.$$typeof) === ReactSymbols.LEGACY_ELEMENT_SYMBOL_STRING ||
68
+ String(element.$$typeof) === ReactSymbols.ELEMENT_SYMBOL_STRING);
83
69
 
84
70
  /**
85
71
  * Returns `true` if object is a React Fiber.
86
72
  */
87
73
  export const isValidFiber = (fiber: unknown): fiber is Fiber =>
88
74
  typeof fiber === "object" &&
89
- fiber != null &&
75
+ fiber !== null &&
90
76
  "tag" in fiber &&
91
77
  "stateNode" in fiber &&
92
78
  "return" in fiber &&
@@ -94,21 +80,27 @@ export const isValidFiber = (fiber: unknown): fiber is Fiber =>
94
80
  "sibling" in fiber &&
95
81
  "flags" in fiber;
96
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
+
97
89
  /**
98
90
  * Returns `true` if fiber is a host fiber. Host fibers are DOM nodes in react-dom, `View` in react-native, etc.
99
91
  *
100
92
  * @see https://reactnative.dev/architecture/glossary#host-view-tree-and-host-view
101
93
  */
102
- export const isHostFiber = (fiber: Fiber): boolean => {
94
+ export const isHostFiber = (fiber: Fiber): fiber is HostFiber => {
95
+ const workTags = getReactWorkTagsForFiber(fiber);
103
96
  switch (fiber.tag) {
104
- case HostComponentTag:
105
- // @ts-expect-error: it exists
106
- case HostHoistableTag:
107
- // @ts-expect-error: it exists
108
- case HostSingletonTag:
97
+ case workTags.HostComponent:
98
+ case workTags.HostText:
99
+ case workTags.HostHoistable:
100
+ case workTags.HostSingleton:
109
101
  return true;
110
102
  default:
111
- return typeof fiber.type === "string";
103
+ return false;
112
104
  }
113
105
  };
114
106
 
@@ -118,12 +110,13 @@ export const isHostFiber = (fiber: Fiber): boolean => {
118
110
  * @see https://reactnative.dev/architecture/glossary#react-composite-components
119
111
  */
120
112
  export const isCompositeFiber = (fiber: Fiber): boolean => {
113
+ const workTags = getReactWorkTagsForFiber(fiber);
121
114
  switch (fiber.tag) {
122
- case ClassComponentTag:
123
- case ForwardRefTag:
124
- case FunctionComponentTag:
125
- case MemoComponentTag:
126
- case SimpleMemoComponentTag:
115
+ case workTags.ClassComponent:
116
+ case workTags.ForwardRef:
117
+ case workTags.FunctionComponent:
118
+ case workTags.MemoComponent:
119
+ case workTags.SimpleMemoComponent:
127
120
  return true;
128
121
  default:
129
122
  return false;
@@ -140,106 +133,23 @@ export const isFiber = (maybeFiber: unknown): maybeFiber is Fiber => {
140
133
  return "pendingProps" in maybeFiber && !("containerInfo" in maybeFiber);
141
134
  };
142
135
 
143
- /**
144
- * Traverses up or down a {@link Fiber}'s contexts, return `true` to stop and select the current and previous context value.
145
- */
146
- export const traverseContexts = (
147
- fiber: Fiber,
148
- selector: (
149
- nextValue: ContextDependency<unknown> | null | undefined,
150
- prevValue: ContextDependency<unknown> | null | undefined,
151
- ) => boolean | void,
152
- ): boolean => {
153
- try {
154
- const nextDependencies = fiber.dependencies;
155
- const prevDependencies = fiber.alternate?.dependencies;
156
-
157
- if (!nextDependencies || !prevDependencies) return false;
158
- if (
159
- typeof nextDependencies !== "object" ||
160
- !("firstContext" in nextDependencies) ||
161
- typeof prevDependencies !== "object" ||
162
- !("firstContext" in prevDependencies)
163
- ) {
164
- return false;
165
- }
166
- let nextContext: ContextDependency<unknown> | null | undefined = nextDependencies.firstContext;
167
- let prevContext: ContextDependency<unknown> | null | undefined = prevDependencies.firstContext;
168
- while (
169
- (nextContext && typeof nextContext === "object" && "memoizedValue" in nextContext) ||
170
- (prevContext && typeof prevContext === "object" && "memoizedValue" in prevContext)
171
- ) {
172
- if (selector(nextContext, prevContext) === true) return true;
173
-
174
- nextContext = nextContext?.next;
175
- prevContext = prevContext?.next;
176
- }
177
- } catch {}
178
- return false;
179
- };
180
-
181
- /**
182
- * 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.
183
- */
184
- export const traverseState = (
185
- fiber: Fiber,
186
- selector: (
187
- nextValue: MemoizedState | null | undefined,
188
- prevValue: MemoizedState | null | undefined,
189
- ) => boolean | void,
190
- ): boolean => {
191
- try {
192
- let nextState: MemoizedState | null | undefined = fiber.memoizedState;
193
- let prevState: MemoizedState | null | undefined = fiber.alternate?.memoizedState;
194
-
195
- while (nextState || prevState) {
196
- if (selector(nextState, prevState) === true) return true;
197
-
198
- nextState = nextState?.next;
199
- prevState = prevState?.next;
200
- }
201
- } catch {}
202
- return false;
203
- };
204
-
205
- /**
206
- * Traverses up or down a {@link Fiber}'s props, return `true` to stop and select the current and previous props value.
207
- */
208
- export const traverseProps = (
209
- fiber: Fiber,
210
- selector: (propName: string, nextValue: unknown, prevValue: unknown) => boolean | void,
211
- ): boolean => {
212
- try {
213
- const nextProps = fiber.memoizedProps;
214
- const prevProps = fiber.alternate?.memoizedProps || {};
215
-
216
- for (const propName of Object.keys(nextProps)) {
217
- if (selector(propName, nextProps[propName], prevProps[propName]) === true) return true;
218
- }
219
- for (const propName of Object.keys(prevProps)) {
220
- if (propName in nextProps) continue;
221
- if (selector(propName, nextProps[propName], prevProps[propName]) === true) return true;
222
- }
223
- } catch {}
224
- return false;
225
- };
226
-
227
136
  /**
228
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.
229
138
  */
230
139
  export const didFiberRender = (fiber: Fiber): boolean => {
231
140
  const nextProps = fiber.memoizedProps;
232
141
  const prevProps = fiber.alternate?.memoizedProps || {};
233
- const flags = fiber.flags ?? (fiber as unknown as { effectTag: number }).effectTag ?? 0;
142
+ const flags = fiber.flags ?? fiber.effectTag ?? 0;
143
+ const workTags = getReactWorkTagsForFiber(fiber);
234
144
 
235
145
  switch (fiber.tag) {
236
- case ClassComponentTag:
237
- case ContextConsumerTag:
238
- case ForwardRefTag:
239
- case FunctionComponentTag:
240
- case MemoComponentTag:
241
- case SimpleMemoComponentTag: {
242
- return (flags & PerformedWork) === PerformedWork;
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;
243
153
  }
244
154
  default:
245
155
  // Host nodes (DOM, root, etc.)
@@ -257,57 +167,18 @@ export const didFiberRender = (fiber: Fiber): boolean => {
257
167
  */
258
168
  export const didFiberCommit = (fiber: Fiber): boolean => {
259
169
  return Boolean(
260
- (fiber.flags & (MutationMask | Cloned)) !== 0 ||
261
- (fiber.subtreeFlags & (MutationMask | Cloned)) !== 0,
170
+ (fiber.flags & (MutationMask | ReactFiberFlags.Cloned)) !== 0 ||
171
+ (fiber.subtreeFlags & (MutationMask | ReactFiberFlags.Cloned)) !== 0,
262
172
  );
263
173
  };
264
174
 
265
- /**
266
- * Returns all host {@link Fiber}s that have committed and rendered.
267
- */
268
- export const getMutatedHostFibers = (fiber: Fiber): Fiber[] => {
269
- const mutations: Fiber[] = [];
270
- const stack: Fiber[] = [fiber];
271
-
272
- while (stack.length) {
273
- const node = stack.pop();
274
- if (!node) continue;
275
-
276
- if (isHostFiber(node) && didFiberCommit(node) && didFiberRender(node)) {
277
- mutations.push(node);
278
- }
279
-
280
- if (node.child) stack.push(node.child);
281
- if (node.sibling) stack.push(node.sibling);
282
- }
283
-
284
- return mutations;
285
- };
286
-
287
- /**
288
- * Returns the stack of {@link Fiber}s from the current fiber to the root fiber.
289
- *
290
- * @example
291
- * ```ts
292
- * [fiber, fiber.return, fiber.return.return, ...]
293
- * ```
294
- */
295
- export const getFiberStack = (fiber: Fiber): Fiber[] => {
296
- const stack: Fiber[] = [];
297
- let currentFiber = fiber;
298
- while (currentFiber.return) {
299
- stack.push(currentFiber);
300
- currentFiber = currentFiber.return as Fiber;
301
- }
302
- return stack;
303
- };
304
-
305
175
  /**
306
176
  * Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
307
177
  */
308
178
  const shouldFilterFiber = (fiber: Fiber): boolean => {
179
+ const workTags = getReactWorkTagsForFiber(fiber);
309
180
  switch (fiber.tag) {
310
- case DehydratedSuspenseComponentTag:
181
+ case workTags.DehydratedSuspenseComponent:
311
182
  // TODO: ideally we would show dehydrated Suspense immediately.
312
183
  // However, it has some special behavior (like disconnecting
313
184
  // an alternate and turning into real Suspense) which breaks DevTools.
@@ -315,13 +186,13 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
315
186
  // https://github.com/bvaughn/react-devtools-experimental/issues/197
316
187
  return true;
317
188
 
318
- case FragmentTag:
319
- case HostTextTag:
320
- case LegacyHiddenComponentTag:
321
- case OffscreenComponentTag:
189
+ case workTags.Fragment:
190
+ case workTags.HostText:
191
+ case workTags.LegacyHiddenComponent:
192
+ case workTags.OffscreenComponent:
322
193
  return true;
323
194
 
324
- case HostRootTag:
195
+ case workTags.HostRoot:
325
196
  // It is never valid to filter the root element.
326
197
  return false;
327
198
 
@@ -331,15 +202,15 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
331
202
 
332
203
  if (typeof symbolOrNumber === "symbol") {
333
204
  return (
334
- symbolOrNumber.description === CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
335
- symbolOrNumber.description === DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
205
+ symbolOrNumber.description === ReactSymbols.CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
206
+ symbolOrNumber.description === ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
336
207
  );
337
208
  }
338
209
 
339
210
  switch (symbolOrNumber) {
340
- case CONCURRENT_MODE_NUMBER:
341
- case CONCURRENT_MODE_SYMBOL_STRING:
342
- 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:
343
214
  return true;
344
215
 
345
216
  default:
@@ -349,47 +220,6 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
349
220
  }
350
221
  };
351
222
 
352
- /**
353
- * Returns the nearest host {@link Fiber} to the current {@link Fiber}.
354
- */
355
- export const getNearestHostFiber = (fiber: Fiber, ascending = false): Fiber | null => {
356
- let hostFiber = traverseFiber(fiber, isHostFiber, ascending);
357
- if (!hostFiber) {
358
- hostFiber = traverseFiber(fiber, isHostFiber, !ascending);
359
- }
360
- return hostFiber;
361
- };
362
-
363
- /**
364
- * Returns all host {@link Fiber}s in the tree that are associated with the current {@link Fiber}.
365
- */
366
- export const getNearestHostFibers = (fiber: Fiber): Fiber[] => {
367
- const hostFibers: Fiber[] = [];
368
- const stack: Fiber[] = [];
369
-
370
- if (isHostFiber(fiber)) {
371
- hostFibers.push(fiber);
372
- } else if (fiber.child) {
373
- stack.push(fiber.child);
374
- }
375
-
376
- while (stack.length) {
377
- const currentNode = stack.pop();
378
- if (!currentNode) break;
379
- if (isHostFiber(currentNode)) {
380
- hostFibers.push(currentNode);
381
- } else if (currentNode.child) {
382
- stack.push(currentNode.child);
383
- }
384
-
385
- if (currentNode.sibling) {
386
- stack.push(currentNode.sibling);
387
- }
388
- }
389
-
390
- return hostFibers;
391
- };
392
-
393
223
  /**
394
224
  * Traverses up or down a {@link Fiber}, return `true` to stop and select a node.
395
225
  */
@@ -405,144 +235,86 @@ export function traverseFiber(
405
235
  ): Promise<Fiber | null>;
406
236
  export function traverseFiber(
407
237
  fiber: Fiber | null,
408
- selector: (node: Fiber) => boolean | Promise<boolean | void> | void,
238
+ selector: FiberSelector,
239
+ ascending?: boolean,
240
+ ): Fiber | null | Promise<Fiber | null>;
241
+ export function traverseFiber(
242
+ fiber: Fiber | null,
243
+ selector: FiberSelector,
409
244
  ascending = false,
410
245
  ): Fiber | null | Promise<Fiber | null> {
411
246
  if (!fiber) return null;
412
247
 
413
- const firstResult = selector(fiber);
414
- if (firstResult instanceof Promise) {
415
- return (async () => {
416
- if ((await firstResult) === true) return fiber;
417
-
418
- let child = ascending ? fiber.return : fiber.child;
419
- while (child) {
420
- const match = await traverseFiberAsync(
421
- child,
422
- selector as (node: Fiber) => Promise<boolean | void>,
423
- ascending,
424
- );
425
- if (match) return match;
426
- child = ascending ? null : child.sibling;
427
- }
428
- return null;
429
- })();
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
+ );
430
253
  }
254
+ if (selection === true) return fiber;
431
255
 
432
- if (firstResult === true) return fiber;
433
-
434
- let child = ascending ? fiber.return : fiber.child;
435
- while (child) {
436
- const match = traverseFiberSync(child, selector as (node: Fiber) => boolean | void, ascending);
437
- if (match) return match;
438
- child = ascending ? null : child.sibling;
439
- }
440
- return null;
256
+ return traverseFiberChildren(fiber, selector, ascending);
441
257
  }
442
258
 
443
- const traverseFiberSync = (
444
- fiber: Fiber | null,
445
- selector: (node: Fiber) => boolean | void,
446
- ascending = false,
447
- ): Fiber | null => {
448
- if (!fiber) return null;
449
- if (selector(fiber) === true) return fiber;
450
-
451
- let child = ascending ? fiber.return : fiber.child;
452
- while (child) {
453
- const match = traverseFiberSync(child, selector, ascending);
454
- if (match) return match;
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";
455
264
 
456
- child = ascending ? null : child.sibling;
457
- }
458
- 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);
459
272
  };
460
273
 
461
- const traverseFiberAsync = async (
274
+ const traverseFiberSiblings = (
462
275
  fiber: Fiber | null,
463
- selector: (node: Fiber) => Promise<boolean | void>,
464
- ascending = false,
465
- ): Promise<Fiber | null> => {
276
+ selector: FiberSelector,
277
+ ascending: boolean,
278
+ ): Fiber | null | Promise<Fiber | null> => {
466
279
  if (!fiber) return null;
467
- if ((await selector(fiber)) === true) return fiber;
468
-
469
- let child = ascending ? fiber.return : fiber.child;
470
- while (child) {
471
- const match = await traverseFiberAsync(child, selector, ascending);
472
- if (match) return match;
473
280
 
474
- child = ascending ? null : child.sibling;
475
- }
476
- return null;
477
- };
478
-
479
- /**
480
- * Returns the timings of the {@link Fiber}.
481
- *
482
- * @example
483
- * ```ts
484
- * const { selfTime, totalTime } = getTimings(fiber);
485
- * console.log(selfTime, totalTime);
486
- * ```
487
- */
488
- export const getTimings = (fiber?: Fiber | null): { selfTime: number; totalTime: number } => {
489
- const totalTime = fiber?.actualDuration ?? 0;
490
- let selfTime = totalTime;
491
- // TODO: calculate a DOM time, which is just host component summed up
492
- let child = fiber?.child ?? null;
493
- while (totalTime > 0 && child != null) {
494
- selfTime -= child.actualDuration ?? 0;
495
- child = 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
+ );
496
287
  }
497
- return { selfTime, totalTime };
288
+ return match ?? traverseFiberSiblings(nextSibling, selector, ascending);
498
289
  };
499
290
 
500
291
  /**
501
292
  * Returns `true` if the {@link Fiber} uses React Compiler's memo cache.
502
293
  */
503
294
  export const hasMemoCache = (fiber: Fiber): boolean => {
504
- return Boolean((fiber.updateQueue as unknown as { memoCache: unknown })?.memoCache);
295
+ return Boolean(fiber.updateQueue?.memoCache);
505
296
  };
506
297
 
507
- type FiberType =
508
- | React.ComponentType<unknown>
509
- | React.ForwardRefExoticComponent<unknown>
510
- | React.MemoExoticComponent<React.ComponentType<unknown>>;
511
-
512
298
  /**
513
299
  * Returns the type (e.g. component definition) of the {@link Fiber}
514
300
  */
515
301
  export const getType = (type: unknown): null | React.ComponentType<unknown> => {
516
- const currentType = type as FiberType;
517
- if (typeof currentType === "function") {
518
- return currentType;
519
- }
520
- if (typeof currentType === "object" && currentType) {
521
- // memo / forwardRef case
522
- return getType(
523
- (currentType as React.MemoExoticComponent<React.ComponentType<unknown>>).type ||
524
- (currentType as { render: React.ComponentType<unknown> }).render,
525
- );
526
- }
527
- return null;
302
+ if (isComponentType(type)) return type;
303
+ if (typeof type !== "object" || type === null) return null;
304
+ return getType(Reflect.get(type, "type") ?? Reflect.get(type, "render"));
528
305
  };
529
306
 
530
307
  /**
531
308
  * Returns the display name of the {@link Fiber} type.
532
309
  */
533
310
  export const getDisplayName = (type: unknown): null | string => {
534
- const currentType = type as FiberType;
535
- if (typeof currentType === "string") {
536
- return currentType;
537
- }
538
- if (typeof currentType !== "function" && !(typeof currentType === "object" && currentType)) {
539
- return null;
540
- }
541
- const name = currentType.displayName || currentType.name || null;
311
+ if (typeof type === "string") return type;
312
+ if ((typeof type !== "function" && typeof type !== "object") || type === null) return null;
313
+ const name = getTypeName(type);
542
314
  if (name) return name;
543
- const unwrappedType = getType(currentType);
315
+ const unwrappedType = getType(type);
544
316
  if (!unwrappedType) return null;
545
- return unwrappedType.displayName || unwrappedType.name || null;
317
+ return getTypeName(unwrappedType);
546
318
  };
547
319
 
548
320
  /**
@@ -550,7 +322,10 @@ export const getDisplayName = (type: unknown): null | string => {
550
322
  */
551
323
  export const detectReactBuildType = (renderer: ReactRenderer): "development" | "production" => {
552
324
  try {
553
- if (typeof renderer.version === "string" && renderer.bundleType > 0) {
325
+ if (
326
+ typeof renderer.version === "string" &&
327
+ renderer.bundleType === ReactBuildType.Development
328
+ ) {
554
329
  return "development";
555
330
  }
556
331
  } catch {}
@@ -562,14 +337,11 @@ export const detectReactBuildType = (renderer: ReactRenderer): "development" | "
562
337
  */
563
338
  export const isInstrumentationActive = (): boolean => {
564
339
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
565
- return (
566
- Boolean(rdtHook?._instrumentationIsActive) ||
567
- isRealReactDevtools(rdtHook) ||
568
- isReactRefresh(rdtHook)
569
- );
340
+ return Boolean(rdtHook?._instrumentationIsActive) || isRealReactDevtools(rdtHook);
570
341
  };
571
342
 
572
343
  export const _fiberRoots = new Set<FiberRoot>();
344
+ const rootRendererIds = new WeakMap<FiberRoot, number>();
573
345
 
574
346
  /**
575
347
  * Returns the latest fiber (since it may be double-buffered).
@@ -582,37 +354,54 @@ export const getLatestFiber = (fiber: Fiber): Fiber => {
582
354
  }
583
355
  for (const root of _fiberRoots) {
584
356
  const latestFiber = traverseFiber(root.current, (innerFiber) => {
585
- if (innerFiber === fiber) return true;
357
+ if (innerFiber === fiber || innerFiber === alternate) return true;
586
358
  });
587
359
  if (latestFiber) return latestFiber;
588
360
  }
589
361
  return fiber;
590
362
  };
591
363
 
592
- 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
+ };
593
382
 
594
383
  export type RenderPhase = "mount" | "unmount" | "update";
595
384
 
596
- let fiberId = 0;
385
+ let nextFiberId = 0;
597
386
  const fiberIdMap = new WeakMap<Fiber, number>();
598
387
 
599
- export const setFiberId = (fiber: Fiber, id: number = fiberId++): void => {
600
- 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
+ }
601
393
  };
602
394
 
603
- // react fibers are double buffered, so the alternate fiber may
604
- // be switched to the current fiber and vice versa.
605
- // fiber === fiber.alternate.alternate
606
395
  export const getFiberId = (fiber: Fiber): number => {
607
- let id = fiberIdMap.get(fiber);
608
- if (id === undefined && fiber.alternate) {
609
- id = fiberIdMap.get(fiber.alternate);
396
+ let currentFiberId = fiberIdMap.get(fiber);
397
+ if (currentFiberId === undefined && fiber.alternate) {
398
+ currentFiberId = fiberIdMap.get(fiber.alternate);
610
399
  }
611
- if (id === undefined) {
612
- id = fiberId++;
613
- setFiberId(fiber, id);
400
+ if (currentFiberId === undefined) {
401
+ currentFiberId = nextFiberId++;
402
+ setFiberId(fiber, currentFiberId);
614
403
  }
615
- return id;
404
+ return currentFiberId;
616
405
  };
617
406
 
618
407
  const mountFiberRecursively = (
@@ -622,16 +411,14 @@ const mountFiberRecursively = (
622
411
  ): void => {
623
412
  let fiber: Fiber | null = firstChild;
624
413
 
625
- while (fiber != null) {
626
- if (!fiberIdMap.has(fiber)) {
627
- getFiberId(fiber);
628
- }
414
+ while (fiber !== null) {
415
+ getFiberId(fiber);
629
416
  const shouldIncludeInTree = !shouldFilterFiber(fiber);
630
417
  if (shouldIncludeInTree && didFiberRender(fiber)) {
631
418
  onRender(fiber, "mount");
632
419
  }
633
420
 
634
- if (fiber.tag === SuspenseComponentTag) {
421
+ if (fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent) {
635
422
  const isTimedOut = fiber.memoizedState !== null;
636
423
  if (isTimedOut) {
637
424
  // Special case: if Suspense mounts in a timed-out state,
@@ -646,18 +433,12 @@ const mountFiberRecursively = (
646
433
  }
647
434
  }
648
435
  } else {
649
- let primaryChild: Fiber | null = null;
650
- const areSuspenseChildrenConditionallyWrapped = (OffscreenComponentTag as number) === -1;
651
- if (areSuspenseChildrenConditionallyWrapped) {
652
- primaryChild = fiber.child;
653
- } else if (fiber.child !== null) {
654
- primaryChild = fiber.child.child;
655
- }
436
+ const primaryChild = fiber.child?.child ?? null;
656
437
  if (primaryChild !== null) {
657
438
  mountFiberRecursively(onRender, primaryChild, false);
658
439
  }
659
440
  }
660
- } else if (fiber.child != null) {
441
+ } else if (fiber.child !== null) {
661
442
  mountFiberRecursively(onRender, fiber.child, true);
662
443
  }
663
444
  fiber = traverseSiblings ? fiber.sibling : null;
@@ -667,18 +448,13 @@ const mountFiberRecursively = (
667
448
  const updateFiberRecursively = (
668
449
  onRender: RenderHandler,
669
450
  nextFiber: Fiber,
670
- prevFiber: Fiber,
671
- parentFiber: Fiber | null,
451
+ prevFiber: Fiber | null,
672
452
  ): void => {
673
- if (!fiberIdMap.has(nextFiber)) {
674
- getFiberId(nextFiber);
675
- }
453
+ getFiberId(nextFiber);
676
454
  if (!prevFiber) return;
677
- if (!fiberIdMap.has(prevFiber)) {
678
- getFiberId(prevFiber);
679
- }
455
+ getFiberId(prevFiber);
680
456
 
681
- const isSuspense = nextFiber.tag === SuspenseComponentTag;
457
+ const isSuspense = nextFiber.tag === getReactWorkTagsForFiber(nextFiber).SuspenseComponent;
682
458
 
683
459
  const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
684
460
  if (shouldIncludeInTree && didFiberRender(nextFiber)) {
@@ -707,7 +483,7 @@ const updateFiberRecursively = (
707
483
  const prevFallbackChildSet = prevFiber.child?.sibling ?? null;
708
484
 
709
485
  if (nextFallbackChildSet !== null && prevFallbackChildSet !== null) {
710
- updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet, nextFiber);
486
+ updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet);
711
487
  }
712
488
  } else if (prevDidTimeout && !nextDidTimeOut) {
713
489
  // Fallback -> Primary:
@@ -746,14 +522,7 @@ const updateFiberRecursively = (
746
522
  // Schedule updates and mounts depending on whether alternates exist.
747
523
  // We don't track deletions here because they are reported separately.
748
524
  if (nextChild.alternate) {
749
- const prevChild = nextChild.alternate;
750
-
751
- updateFiberRecursively(
752
- onRender,
753
- nextChild,
754
- prevChild,
755
- shouldIncludeInTree ? nextFiber : parentFiber,
756
- );
525
+ updateFiberRecursively(onRender, nextChild, nextChild.alternate);
757
526
  } else {
758
527
  mountFiberRecursively(onRender, nextChild, false);
759
528
  }
@@ -765,7 +534,7 @@ const updateFiberRecursively = (
765
534
  };
766
535
 
767
536
  const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
768
- const isRoot = fiber.tag === HostRootTag;
537
+ const isRoot = fiber.tag === getReactWorkTagsForFiber(fiber).HostRoot;
769
538
 
770
539
  if (isRoot || !shouldFilterFiber(fiber)) {
771
540
  onRender(fiber, "unmount");
@@ -774,7 +543,8 @@ const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
774
543
 
775
544
  const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => {
776
545
  // We might meet a nested Suspense on our way.
777
- const isTimedOutSuspense = fiber.tag === SuspenseComponentTag && fiber.memoizedState !== null;
546
+ const isTimedOutSuspense =
547
+ fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent && fiber.memoizedState !== null;
778
548
  let child = fiber.child;
779
549
 
780
550
  if (isTimedOutSuspense) {
@@ -798,11 +568,9 @@ const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber):
798
568
  }
799
569
  };
800
570
 
801
- let commitId = 0;
802
571
  const rootInstanceMap = new WeakMap<
803
- FiberRoot,
572
+ Fiber | FiberRoot,
804
573
  {
805
- id: number;
806
574
  prevFiber: Fiber | null;
807
575
  }
808
576
  >();
@@ -814,37 +582,39 @@ const rootInstanceMap = new WeakMap<
814
582
  * console.log(phase)
815
583
  * })
816
584
  */
817
- export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler): void => {
585
+ export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: RenderHandler): void => {
818
586
  const fiber = "current" in root ? root.current : root;
819
587
 
820
588
  let rootInstance = rootInstanceMap.get(root);
821
589
 
822
590
  if (!rootInstance) {
823
- rootInstance = { id: commitId++, prevFiber: null };
591
+ rootInstance = { prevFiber: null };
824
592
  rootInstanceMap.set(root, rootInstance);
825
593
  }
826
594
 
827
595
  const { prevFiber } = rootInstance;
828
- // if fiberRoot don't have current instance, means it's been unmounted
829
596
  if (!fiber) {
830
- unmountFiber(onRender, fiber);
597
+ if (prevFiber) {
598
+ unmountFiber(onRender, prevFiber);
599
+ }
831
600
  } else if (prevFiber !== null) {
832
601
  const wasMounted =
833
- prevFiber &&
834
- prevFiber.memoizedState != null &&
835
- prevFiber.memoizedState.element != null &&
602
+ prevFiber.memoizedState !== null &&
603
+ prevFiber.memoizedState.element !== null &&
604
+ prevFiber.memoizedState.element !== undefined &&
836
605
  // A dehydrated root is not considered mounted
837
606
  prevFiber.memoizedState.isDehydrated !== true;
838
607
  const isMounted =
839
- fiber.memoizedState != null &&
840
- fiber.memoizedState.element != null &&
608
+ fiber.memoizedState !== null &&
609
+ fiber.memoizedState.element !== null &&
610
+ fiber.memoizedState.element !== undefined &&
841
611
  // A dehydrated root is not considered mounted
842
612
  fiber.memoizedState.isDehydrated !== true;
843
613
 
844
614
  if (!wasMounted && isMounted) {
845
615
  mountFiberRecursively(onRender, fiber, false);
846
616
  } else if (wasMounted && isMounted) {
847
- updateFiberRecursively(onRender, fiber, fiber.alternate, null);
617
+ updateFiberRecursively(onRender, fiber, fiber.alternate);
848
618
  } else if (wasMounted && !isMounted) {
849
619
  unmountFiber(onRender, fiber);
850
620
  }
@@ -855,191 +625,25 @@ export const traverseRenderedFibers = (root: FiberRoot, onRender: RenderHandler)
855
625
  rootInstance.prevFiber = fiber;
856
626
  };
857
627
 
858
- const overrideRenderers = new Set<ReactRenderer>();
859
- let areOverrideRenderersWired = false;
860
-
861
- const wireOverrideRenderers = (): void => {
862
- if (areOverrideRenderersWired) return;
863
- if (!hasRDTHook()) return;
864
- const rdtHook = getRDTHook();
865
- if (!rdtHook?.renderers) return;
866
- areOverrideRenderersWired = true;
867
-
868
- ensureHookDispatchesToListeners(rdtHook);
869
- for (const renderer of rdtHook.renderers.values()) {
870
- overrideRenderers.add(renderer);
871
- }
872
- onRendererInject((renderer) => {
873
- overrideRenderers.add(renderer);
874
- });
875
- };
876
-
877
- const getRootRenderer = (fiber: Fiber): ReactRenderer | null => {
878
- if (!hasRDTHook()) return null;
879
- let hostRootFiber = fiber;
880
- while (hostRootFiber.return) {
881
- hostRootFiber = hostRootFiber.return;
882
- }
883
- const rendererId = rootRendererIds.get(hostRootFiber.stateNode);
884
- if (rendererId === undefined) return null;
885
- return getRDTHook().renderers?.get(rendererId) ?? null;
886
- };
887
-
888
- // dispatch to the renderer that owns the fiber's root when known; renderers
889
- // that never committed through bippy's hook patch fall back to "try all",
890
- // which matches the pre-ownership behavior for single-renderer apps
891
- const resolveOverrideRenderers = (fiber: Fiber): ReactRenderer[] => {
892
- wireOverrideRenderers();
893
- const rootRenderer = getRootRenderer(fiber);
894
- if (rootRenderer) return [rootRenderer];
895
- return Array.from(overrideRenderers);
896
- };
897
-
898
- const applyPropsOverride = (
899
- renderers: ReactRenderer[],
900
- fiber: Fiber,
901
- path: string[],
902
- value: unknown,
903
- ): void => {
904
- for (const renderer of renderers) {
905
- try {
906
- renderer.overrideProps?.(fiber, path, value);
907
- } catch {}
908
- }
909
- };
910
-
911
- const getHookStateDispatch = (
912
- fiber: Fiber,
913
- hookIndex: number,
914
- ): ((value: unknown) => void) | null => {
915
- let hookState = fiber.memoizedState;
916
- for (let i = 0; i < hookIndex; i++) {
917
- if (!hookState?.next) return null;
918
- hookState = hookState.next;
919
- }
920
- const queue = hookState?.queue;
921
- if (isPOJO(queue) && typeof queue.dispatch === "function") {
922
- return queue.dispatch as (value: unknown) => void;
923
- }
924
- return null;
925
- };
926
-
927
- const findContextProviderFiber = (fiber: Fiber, contextType: unknown): Fiber | null => {
928
- let currentFiber: Fiber | null = fiber;
929
- while (currentFiber) {
930
- const fiberType = currentFiber.type;
931
- if (fiberType === contextType || fiberType?.Provider === contextType) {
932
- return currentFiber;
933
- }
934
- currentFiber = currentFiber.return;
935
- }
936
- return null;
937
- };
938
-
939
- const isPOJO = (maybePOJO: unknown): maybePOJO is Record<string, unknown> => {
940
- return (
941
- Object.prototype.toString.call(maybePOJO) === "[object Object]" &&
942
- (Object.getPrototypeOf(maybePOJO) === Object.prototype ||
943
- Object.getPrototypeOf(maybePOJO) === null)
944
- );
945
- };
946
-
947
- const buildPathsFromValue = (
948
- maybePOJO: Record<string, unknown>,
949
- basePath: string[] = [],
950
- ): Array<{ path: string[]; value: unknown }> => {
951
- if (!isPOJO(maybePOJO)) {
952
- return [{ path: basePath, value: maybePOJO }];
953
- }
954
-
955
- const paths: Array<{ path: string[]; value: unknown }> = [];
956
-
957
- for (const key in maybePOJO) {
958
- const value = maybePOJO[key];
959
- const path = basePath.concat(key);
960
-
961
- if (isPOJO(value)) {
962
- paths.push(...buildPathsFromValue(value, path));
963
- } else {
964
- paths.push({ path, value });
965
- }
966
- }
967
-
968
- return paths;
969
- };
970
-
971
- const buildValueWrites = (partialValue: unknown): Array<{ path: string[]; value: unknown }> =>
972
- isPOJO(partialValue) ? buildPathsFromValue(partialValue) : [{ path: [], value: partialValue }];
973
-
974
- export const overrideProps = (fiber: Fiber, partialValue: Record<string, unknown>) => {
975
- const renderers = resolveOverrideRenderers(fiber);
976
- for (const { path, value } of buildValueWrites(partialValue)) {
977
- applyPropsOverride(renderers, fiber, path, value);
978
- }
979
- };
980
-
981
- export const overrideHookState = (fiber: Fiber, id: number, partialValue: unknown) => {
982
- const renderers = resolveOverrideRenderers(fiber).filter((renderer) =>
983
- Boolean(renderer.overrideHookState),
984
- );
985
- const writes = buildValueWrites(partialValue);
986
-
987
- if (renderers.length > 0) {
988
- const hookId = String(id);
989
- for (const renderer of renderers) {
990
- for (const { path, value } of writes) {
991
- try {
992
- renderer.overrideHookState?.(fiber, hookId, path, value);
993
- } catch {}
994
- }
995
- }
996
- return;
997
- }
998
-
999
- // production renderers don't expose overrideHookState; dispatching through
1000
- // the hook's own queue still works there, but only for whole-value writes
1001
- // (a path write through dispatch would replace the entire hook state)
1002
- if (isPOJO(partialValue)) return;
1003
- const dispatch = getHookStateDispatch(fiber, id);
1004
- if (!dispatch) return;
1005
- try {
1006
- dispatch(partialValue);
1007
- } catch {}
1008
- };
1009
-
1010
- export const overrideContext = (fiber: Fiber, contextType: unknown, partialValue: unknown) => {
1011
- const providerFiber = findContextProviderFiber(fiber, contextType);
1012
- if (!providerFiber) return;
1013
- const renderers = resolveOverrideRenderers(providerFiber);
1014
- for (const { path, value } of buildValueWrites(partialValue)) {
1015
- applyPropsOverride(renderers, providerFiber, ["value", ...path], value);
1016
- if (providerFiber.alternate) {
1017
- applyPropsOverride(renderers, providerFiber.alternate, ["value", ...path], value);
1018
- }
1019
- }
1020
- };
1021
-
1022
628
  export interface InstrumentationOptions {
1023
629
  name?: string;
1024
630
  onActive?: () => unknown;
1025
- onCommitFiberRoot?: (rendererID: number, root: FiberRoot, priority: number | void) => unknown;
631
+ onCommitFiberRoot?: (
632
+ rendererID: number,
633
+ root: FiberRoot,
634
+ priority: number | void,
635
+ didError?: boolean,
636
+ ) => unknown;
1026
637
  onCommitFiberUnmount?: (rendererID: number, fiber: Fiber) => unknown;
1027
638
  onPostCommitFiberRoot?: (rendererID: number, root: FiberRoot) => unknown;
1028
639
  onScheduleFiberRoot?: (rendererID: number, root: FiberRoot, children: React.ReactNode) => unknown;
1029
640
  }
1030
641
 
1031
- const commitFiberRootListeners = new Set<
1032
- NonNullable<InstrumentationOptions["onCommitFiberRoot"]>
1033
- >();
1034
- const commitFiberUnmountListeners = new Set<
1035
- NonNullable<InstrumentationOptions["onCommitFiberUnmount"]>
1036
- >();
1037
- const postCommitFiberRootListeners = new Set<
1038
- NonNullable<InstrumentationOptions["onPostCommitFiberRoot"]>
1039
- >();
1040
- const scheduleFiberRootListeners = new Set<
1041
- NonNullable<InstrumentationOptions["onScheduleFiberRoot"]>
1042
- >();
642
+ interface InstrumentationSubscription {
643
+ options: InstrumentationOptions;
644
+ }
645
+
646
+ const instrumentationSubscriptions = new Set<InstrumentationSubscription>();
1043
647
 
1044
648
  interface HookDispatchers {
1045
649
  onCommitFiberRoot: ReactDevToolsGlobalHook["onCommitFiberRoot"];
@@ -1049,15 +653,14 @@ interface HookDispatchers {
1049
653
  }
1050
654
 
1051
655
  const hookDispatchers = new WeakMap<ReactDevToolsGlobalHook, Partial<HookDispatchers>>();
1052
-
1053
- const rootRendererIds = new WeakMap<FiberRoot, number>();
656
+ let didSubscribeToHookReplacements = false;
1054
657
 
1055
658
  // each hook event is dispatched from a single re-installable wrapper. If
1056
659
  // something overwrites the hook method (devtools, direct assignment), the
1057
660
  // next instrument() call installs a fresh wrapper over it; a superseded
1058
661
  // wrapper still forwards the previous chain but skips the listeners so
1059
662
  // they never fire twice.
1060
- const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
663
+ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
1061
664
  const dispatchers = hookDispatchers.get(rdtHook) ?? {};
1062
665
  hookDispatchers.set(rdtHook, dispatchers);
1063
666
 
@@ -1070,13 +673,29 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1070
673
  rendererID,
1071
674
  root,
1072
675
  priority,
676
+ didError,
1073
677
  ) => {
1074
- prevOnCommitFiberRoot?.(rendererID, root, priority);
678
+ if (prevOnCommitFiberRoot) {
679
+ prevOnCommitFiberRoot.call(rdtHook, rendererID, root, priority, didError);
680
+ }
1075
681
  if (hookDispatchers.get(rdtHook)?.onCommitFiberRoot !== dispatchCommitFiberRoot) return;
1076
- _fiberRoots.add(root);
1077
- rootRendererIds.set(root, rendererID);
1078
- for (const listener of commitFiberRootListeners) {
1079
- listener(rendererID, root, priority);
682
+ setReactWorkTagsForFiber(root.current, rdtHook.renderers.get(rendererID));
683
+ const rootMemoizedState = root.current.memoizedState;
684
+ const isUnmounting =
685
+ rootMemoizedState === null ||
686
+ rootMemoizedState.element === null ||
687
+ rootMemoizedState.element === undefined;
688
+ if (isUnmounting) {
689
+ _fiberRoots.delete(root);
690
+ rootRendererIds.delete(root);
691
+ } else {
692
+ _fiberRoots.add(root);
693
+ rootRendererIds.set(root, rendererID);
694
+ }
695
+ for (const { options } of instrumentationSubscriptions) {
696
+ if (options.onCommitFiberRoot) {
697
+ options.onCommitFiberRoot(rendererID, root, priority, didError);
698
+ }
1080
699
  }
1081
700
  };
1082
701
  dispatchers.onCommitFiberRoot = dispatchCommitFiberRoot;
@@ -1092,12 +711,17 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1092
711
  rendererID,
1093
712
  fiber,
1094
713
  ) => {
1095
- prevOnCommitFiberUnmount?.(rendererID, fiber);
714
+ setReactWorkTagsForFiber(fiber, rdtHook.renderers.get(rendererID));
715
+ if (prevOnCommitFiberUnmount) {
716
+ prevOnCommitFiberUnmount.call(rdtHook, rendererID, fiber);
717
+ }
1096
718
  if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
1097
719
  return;
1098
720
  }
1099
- for (const listener of commitFiberUnmountListeners) {
1100
- listener(rendererID, fiber);
721
+ for (const { options } of instrumentationSubscriptions) {
722
+ if (options.onCommitFiberUnmount) {
723
+ options.onCommitFiberUnmount(rendererID, fiber);
724
+ }
1101
725
  }
1102
726
  };
1103
727
  dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
@@ -1113,12 +737,16 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1113
737
  rendererID,
1114
738
  root,
1115
739
  ) => {
1116
- prevOnPostCommitFiberRoot?.(rendererID, root);
740
+ if (prevOnPostCommitFiberRoot) {
741
+ prevOnPostCommitFiberRoot.call(rdtHook, rendererID, root);
742
+ }
1117
743
  if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
1118
744
  return;
1119
745
  }
1120
- for (const listener of postCommitFiberRootListeners) {
1121
- listener(rendererID, root);
746
+ for (const { options } of instrumentationSubscriptions) {
747
+ if (options.onPostCommitFiberRoot) {
748
+ options.onPostCommitFiberRoot(rendererID, root);
749
+ }
1122
750
  }
1123
751
  };
1124
752
  dispatchers.onPostCommitFiberRoot = dispatchPostCommitFiberRoot;
@@ -1135,10 +763,14 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1135
763
  root,
1136
764
  children,
1137
765
  ) => {
1138
- prevOnScheduleFiberRoot?.(rendererID, root, children);
766
+ if (prevOnScheduleFiberRoot) {
767
+ prevOnScheduleFiberRoot.call(rdtHook, rendererID, root, children);
768
+ }
1139
769
  if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
1140
- for (const listener of scheduleFiberRootListeners) {
1141
- listener(rendererID, root, children);
770
+ for (const { options } of instrumentationSubscriptions) {
771
+ if (options.onScheduleFiberRoot) {
772
+ options.onScheduleFiberRoot(rendererID, root, children);
773
+ }
1142
774
  }
1143
775
  };
1144
776
  dispatchers.onScheduleFiberRoot = dispatchScheduleFiberRoot;
@@ -1146,13 +778,24 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1146
778
  }
1147
779
  };
1148
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
+
1149
793
  /**
1150
794
  * Instruments the DevTools hook. Each hook event is patched once and
1151
795
  * dispatches to a set of listeners, so multiple `instrument` calls compose
1152
796
  * without stacking patches. Returns an unsubscribe function that removes
1153
797
  * exactly the handlers this call registered.
1154
- * The returned function is also a `Disposable`, so it composes with other
1155
- * bippy subscriptions through `using`.
798
+ * The unsubscribe function can also be disposed through `using`.
1156
799
  * @example
1157
800
  * const unsubscribe = instrument({
1158
801
  * onActive() {
@@ -1166,35 +809,19 @@ const ensureHookDispatchesToListeners = (rdtHook: ReactDevToolsGlobalHook): void
1166
809
  */
1167
810
  export const instrument = (options: InstrumentationOptions): Unsubscribe => {
1168
811
  const rdtHook = getRDTHook(options.onActive);
1169
-
1170
812
  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
1171
813
 
1172
- ensureHookDispatchesToListeners(rdtHook);
1173
-
1174
- const {
1175
- onActive,
1176
- onCommitFiberRoot,
1177
- onCommitFiberUnmount,
1178
- onPostCommitFiberRoot,
1179
- onScheduleFiberRoot,
1180
- } = options;
1181
- if (onCommitFiberRoot) commitFiberRootListeners.add(onCommitFiberRoot);
1182
- if (onCommitFiberUnmount) commitFiberUnmountListeners.add(onCommitFiberUnmount);
1183
- if (onPostCommitFiberRoot) postCommitFiberRootListeners.add(onPostCommitFiberRoot);
1184
- if (onScheduleFiberRoot) scheduleFiberRootListeners.add(onScheduleFiberRoot);
1185
-
1186
- return toUnsubscribe(() => {
1187
- if (onActive) _onActiveListeners.delete(onActive);
1188
- if (onCommitFiberRoot) commitFiberRootListeners.delete(onCommitFiberRoot);
1189
- if (onCommitFiberUnmount) commitFiberUnmountListeners.delete(onCommitFiberUnmount);
1190
- if (onPostCommitFiberRoot) postCommitFiberRootListeners.delete(onPostCommitFiberRoot);
1191
- if (onScheduleFiberRoot) scheduleFiberRootListeners.delete(onScheduleFiberRoot);
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);
1192
821
  });
1193
822
  };
1194
823
 
1195
- // React stamps fibers under per-renderer random-suffix keys (`__reactFiber$<suffix>`);
1196
- // caching discovered keys makes repeat lookups a single property read instead of a
1197
- // scan over every own key of the element.
824
+ // React uses per-renderer suffixes for host-instance Fiber keys, so discovered keys are cached.
1198
825
  const knownFiberPropertyKeys = new Set<string>();
1199
826
 
1200
827
  const isFiberPropertyKey = (key: string): boolean =>
@@ -1202,7 +829,38 @@ const isFiberPropertyKey = (key: string): boolean =>
1202
829
  key.startsWith("__reactInternalInstance$") ||
1203
830
  key.startsWith("__reactFiber");
1204
831
 
1205
- export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
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");
840
+ return isValidFiber(child) ? child : null;
841
+ };
842
+
843
+ const getInternalInstanceHandle = (hostInstance: object): Fiber | null => {
844
+ const internalInstanceHandle =
845
+ Reflect.get(hostInstance, "__internalInstanceHandle") ??
846
+ Reflect.get(hostInstance, "_internalInstanceHandle");
847
+ return isValidFiber(internalInstanceHandle) ? internalInstanceHandle : null;
848
+ };
849
+
850
+ const getPublicHostInstance = (stateNode: unknown): unknown => {
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
+ }
858
+ }
859
+ const nativeTag = Reflect.get(stateNode, "_nativeTag");
860
+ return typeof nativeTag === "number" ? nativeTag : stateNode;
861
+ };
862
+
863
+ export const getFiber = <T>(hostInstance: T): Fiber | null => {
1206
864
  const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
1207
865
  if (rdtHook?.renderers) {
1208
866
  for (const renderer of rdtHook.renderers.values()) {
@@ -1213,42 +871,38 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1213
871
  }
1214
872
  }
1215
873
 
1216
- if (typeof hostInstance === "object" && hostInstance != null) {
1217
- if ("_reactRootContainer" in hostInstance) {
1218
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1219
- return (hostInstance._reactRootContainer as any)?._internalRoot?.current?.child;
1220
- }
874
+ if (typeof hostInstance === "object" && hostInstance !== null) {
875
+ const legacyRootFiber = getLegacyRootFiber(hostInstance);
876
+ if (legacyRootFiber) return legacyRootFiber;
1221
877
 
1222
- // React Native Fabric public instances (ReactNativeElement /
1223
- // ReactFabricHostComponent) store their fiber as the "internal instance
1224
- // handle" instead of a __reactFiber$-prefixed key
1225
- const fabricInstanceHandle =
1226
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1227
- (hostInstance as any).__internalInstanceHandle ??
1228
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1229
- (hostInstance as any)._internalInstanceHandle;
1230
- if (isFiber(fabricInstanceHandle)) {
1231
- return fabricInstanceHandle;
1232
- }
878
+ const internalInstanceHandle = getInternalInstanceHandle(hostInstance);
879
+ if (internalInstanceHandle) return internalInstanceHandle;
1233
880
 
1234
- const hostInstanceRecord = hostInstance as Record<string, unknown>;
1235
881
  for (const knownKey of knownFiberPropertyKeys) {
1236
- const fiber = hostInstanceRecord[knownKey];
1237
- if (fiber) return fiber as Fiber;
882
+ const fiber = Reflect.get(hostInstance, knownKey);
883
+ if (isValidFiber(fiber)) return fiber;
1238
884
  }
1239
885
 
1240
- for (const key of Object.keys(hostInstanceRecord)) {
886
+ for (const key of Object.keys(hostInstance)) {
1241
887
  if (isFiberPropertyKey(key)) {
1242
888
  knownFiberPropertyKeys.add(key);
1243
- return (hostInstanceRecord[key] || null) as Fiber | null;
889
+ const fiber = Reflect.get(hostInstance, key);
890
+ if (isValidFiber(fiber)) return fiber;
1244
891
  }
1245
892
  }
893
+ }
1246
894
 
895
+ if (
896
+ hostInstance !== null &&
897
+ hostInstance !== undefined &&
898
+ (typeof hostInstance === "object" || typeof hostInstance === "number")
899
+ ) {
1247
900
  for (const fiberRoot of _fiberRoots) {
1248
- if (getRootRenderer(fiberRoot.current)?.findFiberByHostInstance) continue;
1249
901
  const fiber = traverseFiber(
1250
902
  fiberRoot.current,
1251
- (candidateFiber) => candidateFiber.stateNode === hostInstance,
903
+ (candidateFiber) =>
904
+ isHostFiber(candidateFiber) &&
905
+ getPublicHostInstance(candidateFiber.stateNode) === hostInstance,
1252
906
  );
1253
907
  if (fiber) return fiber;
1254
908
  }
@@ -1256,6 +910,19 @@ export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
1256
910
  return null;
1257
911
  };
1258
912
 
1259
- export * from "./rdt-hook.js";
1260
- export * from "./unsubscribe.js";
1261
- 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";