bippy 0.7.2 → 0.7.3-dev.0ac832d

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.
package/src/core.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // React must remain a type-only import because this module loads immediately after the DevTools hook.
2
2
  import type * as React from "react";
3
+ import { callListener } from "./call-listener.js";
3
4
 
4
5
  import type {
5
6
  Fiber,
@@ -10,19 +11,20 @@ import type {
10
11
  } from "./react-internals/index.js";
11
12
 
12
13
  import {
13
- _onActiveListeners,
14
14
  BIPPY_INSTRUMENTATION_STRING,
15
15
  createUnsubscribe,
16
16
  getRDTHook,
17
17
  hasRDTHook,
18
+ isFiberRootUnmounted,
18
19
  isReactRefresh,
19
20
  isRealReactDevtools,
20
21
  onRDTHookReplace,
22
+ removeActiveListener,
21
23
  } from "./rdt-hook.js";
22
- import type { Unsubscribe } from "./rdt-hook.js";
24
+ import type { ReactDevToolsTarget, Unsubscribe } from "./rdt-hook.js";
25
+ import { getCurrentFiberFromRoot } from "./react-internals/current-fiber.js";
23
26
  import {
24
27
  getReactWorkTagsForFiber,
25
- MutationMask,
26
28
  ReactBuildType,
27
29
  ReactFiberFlags,
28
30
  ReactSymbols,
@@ -64,8 +66,8 @@ export const isValidElement = (element: unknown): element is React.ReactElement
64
66
  typeof element === "object" &&
65
67
  element !== null &&
66
68
  "$$typeof" in element &&
67
- (String(element.$$typeof) === ReactSymbols.LEGACY_ELEMENT_SYMBOL_STRING ||
68
- String(element.$$typeof) === ReactSymbols.ELEMENT_SYMBOL_STRING);
69
+ (element.$$typeof === Symbol.for("react.element") ||
70
+ element.$$typeof === Symbol.for("react.transitional.element"));
69
71
 
70
72
  /**
71
73
  * Returns `true` if object is a React Fiber.
@@ -78,7 +80,7 @@ export const isFiber = (fiber: unknown): fiber is Fiber =>
78
80
  "return" in fiber &&
79
81
  "child" in fiber &&
80
82
  "sibling" in fiber &&
81
- "flags" in fiber;
83
+ ("flags" in fiber || "effectTag" in fiber);
82
84
 
83
85
  const isFiberRoot = (fiberRoot: unknown): fiberRoot is FiberRoot =>
84
86
  typeof fiberRoot === "object" &&
@@ -128,7 +130,7 @@ export const isCompositeFiber = (fiber: Fiber): boolean => {
128
130
  */
129
131
  export const didFiberRender = (fiber: Fiber): boolean => {
130
132
  const nextProps = fiber.memoizedProps;
131
- const prevProps = fiber.alternate?.memoizedProps || {};
133
+ const prevProps = fiber.alternate?.memoizedProps;
132
134
  const flags = fiber.flags ?? fiber.effectTag ?? 0;
133
135
  const workTags = getReactWorkTagsForFiber(fiber);
134
136
 
@@ -152,16 +154,6 @@ export const didFiberRender = (fiber: Fiber): boolean => {
152
154
  }
153
155
  };
154
156
 
155
- /**
156
- * Returns `true` if the {@link Fiber} has committed. Note that this does not mean the fiber has committed in the current commit, just that it has committed in the past.
157
- */
158
- export const didFiberCommit = (fiber: Fiber): boolean => {
159
- return Boolean(
160
- (fiber.flags & (MutationMask | ReactFiberFlags.Cloned)) !== 0 ||
161
- (fiber.subtreeFlags & (MutationMask | ReactFiberFlags.Cloned)) !== 0,
162
- );
163
- };
164
-
165
157
  /**
166
158
  * Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
167
159
  */
@@ -210,41 +202,57 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
210
202
  }
211
203
  };
212
204
 
205
+ interface TraverseFiber {
206
+ (
207
+ fiber: Fiber | null,
208
+ selector: (node: Fiber) => boolean | void,
209
+ ascending?: boolean,
210
+ ): Fiber | null;
211
+ (
212
+ fiber: Fiber | null,
213
+ selector: (node: Fiber) => Promise<boolean | void>,
214
+ ascending?: boolean,
215
+ ): Promise<Fiber | null>;
216
+ (
217
+ fiber: Fiber | null,
218
+ selector: FiberSelector,
219
+ ascending?: boolean,
220
+ ): Fiber | null | Promise<Fiber | null>;
221
+ }
222
+
213
223
  /**
214
224
  * Traverses up or down a {@link Fiber}, return `true` to stop and select a node.
215
225
  */
216
- export function traverseFiber(
217
- fiber: Fiber | null,
218
- selector: (node: Fiber) => boolean | void,
219
- ascending?: boolean,
220
- ): Fiber | null;
221
- export function traverseFiber(
222
- fiber: Fiber | null,
223
- selector: (node: Fiber) => Promise<boolean | void>,
224
- ascending?: boolean,
225
- ): Promise<Fiber | null>;
226
- export function traverseFiber(
227
- fiber: Fiber | null,
228
- selector: FiberSelector,
229
- ascending?: boolean,
230
- ): Fiber | null | Promise<Fiber | null>;
231
- export function traverseFiber(
226
+ export const traverseFiber = ((
232
227
  fiber: Fiber | null,
233
228
  selector: FiberSelector,
234
229
  ascending = false,
235
- ): Fiber | null | Promise<Fiber | null> {
236
- if (!fiber) return null;
237
-
238
- const selection = selector(fiber);
239
- if (isPromiseLike<boolean | void>(selection)) {
240
- return Promise.resolve(selection).then((didSelectFiber) =>
241
- didSelectFiber === true ? fiber : traverseFiberChildren(fiber, selector, ascending),
242
- );
243
- }
244
- if (selection === true) return fiber;
245
-
246
- return traverseFiberChildren(fiber, selector, ascending);
247
- }
230
+ ): Fiber | null | Promise<Fiber | null> => {
231
+ const pendingSiblings: Fiber[] = [];
232
+ const getNextFiber = (currentFiber: Fiber): Fiber | null => {
233
+ if (ascending) return currentFiber.return;
234
+ if (currentFiber !== fiber && currentFiber.sibling) {
235
+ pendingSiblings.push(currentFiber.sibling);
236
+ }
237
+ return currentFiber.child ?? pendingSiblings.pop() ?? null;
238
+ };
239
+ const visit = (firstFiber: Fiber | null): Fiber | null | Promise<Fiber | null> => {
240
+ let currentFiber = firstFiber;
241
+ while (currentFiber) {
242
+ const selectedFiber = currentFiber;
243
+ const selection = selector(selectedFiber);
244
+ if (isPromiseLike<boolean | void>(selection)) {
245
+ return Promise.resolve(selection).then((didSelectFiber) =>
246
+ didSelectFiber === true ? selectedFiber : visit(getNextFiber(selectedFiber)),
247
+ );
248
+ }
249
+ if (selection === true) return selectedFiber;
250
+ currentFiber = getNextFiber(selectedFiber);
251
+ }
252
+ return null;
253
+ };
254
+ return visit(fiber);
255
+ }) as TraverseFiber;
248
256
 
249
257
  const isPromiseLike = <Result>(value: unknown): value is PromiseLike<Result> =>
250
258
  (typeof value === "object" || typeof value === "function") &&
@@ -252,32 +260,6 @@ const isPromiseLike = <Result>(value: unknown): value is PromiseLike<Result> =>
252
260
  "then" in value &&
253
261
  typeof value.then === "function";
254
262
 
255
- const traverseFiberChildren = (
256
- fiber: Fiber,
257
- selector: FiberSelector,
258
- ascending: boolean,
259
- ): Fiber | null | Promise<Fiber | null> => {
260
- const firstChild = ascending ? fiber.return : fiber.child;
261
- return traverseFiberSiblings(firstChild, selector, ascending);
262
- };
263
-
264
- const traverseFiberSiblings = (
265
- fiber: Fiber | null,
266
- selector: FiberSelector,
267
- ascending: boolean,
268
- ): Fiber | null | Promise<Fiber | null> => {
269
- if (!fiber) return null;
270
-
271
- const nextSibling = ascending ? null : fiber.sibling;
272
- const match = traverseFiber(fiber, selector, ascending);
273
- if (isPromiseLike<Fiber | null>(match)) {
274
- return Promise.resolve(match).then(
275
- (resolvedMatch) => resolvedMatch ?? traverseFiberSiblings(nextSibling, selector, ascending),
276
- );
277
- }
278
- return match ?? traverseFiberSiblings(nextSibling, selector, ascending);
279
- };
280
-
281
263
  /**
282
264
  * Returns `true` if the {@link Fiber} uses React Compiler's memo cache.
283
265
  */
@@ -291,7 +273,14 @@ export const hasMemoCache = (fiber: Fiber): boolean => {
291
273
  export const getType = (type: unknown): null | React.ComponentType<unknown> => {
292
274
  if (isComponentType(type)) return type;
293
275
  if (typeof type !== "object" || type === null) return null;
294
- return getType(Reflect.get(type, "type") ?? Reflect.get(type, "render"));
276
+ const visitedTypes = new Set<object>();
277
+ let currentType: unknown = type;
278
+ while (typeof currentType === "object" && currentType !== null) {
279
+ if (visitedTypes.has(currentType)) return null;
280
+ visitedTypes.add(currentType);
281
+ currentType = Reflect.get(currentType, "type") ?? Reflect.get(currentType, "render");
282
+ }
283
+ return isComponentType(currentType) ? currentType : null;
295
284
  };
296
285
 
297
286
  /**
@@ -312,10 +301,7 @@ export const getDisplayName = (type: unknown): null | string => {
312
301
  */
313
302
  export const detectReactBuildType = (renderer: ReactRenderer): "development" | "production" => {
314
303
  try {
315
- if (
316
- typeof renderer.version === "string" &&
317
- renderer.bundleType === ReactBuildType.Development
318
- ) {
304
+ if (typeof renderer.version === "string" && renderer.bundleType > ReactBuildType.Production) {
319
305
  return "development";
320
306
  }
321
307
  } catch {}
@@ -325,8 +311,8 @@ export const detectReactBuildType = (renderer: ReactRenderer): "development" | "
325
311
  /**
326
312
  * Returns `true` if bippy's instrumentation is active.
327
313
  */
328
- export const isInstrumentationActive = (): boolean => {
329
- const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
314
+ export const isInstrumentationActive = (target: ReactDevToolsTarget = globalThis): boolean => {
315
+ const rdtHook = target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
330
316
  return (
331
317
  Boolean(rdtHook?._instrumentationIsActive) ||
332
318
  isRealReactDevtools(rdtHook) ||
@@ -336,6 +322,7 @@ export const isInstrumentationActive = (): boolean => {
336
322
 
337
323
  export const _fiberRoots = new Set<FiberRoot>();
338
324
  const rootRendererIds = new WeakMap<FiberRoot, number>();
325
+ const rootHooks = new WeakMap<FiberRoot, ReactDevToolsGlobalHook>();
339
326
 
340
327
  /**
341
328
  * Returns the latest fiber (since it may be double-buffered).
@@ -343,6 +330,8 @@ const rootRendererIds = new WeakMap<FiberRoot, number>();
343
330
  export const getLatestFiber = (fiber: Fiber): Fiber => {
344
331
  const alternate = fiber.alternate;
345
332
  if (!alternate) return fiber;
333
+ const currentFiber = getCurrentFiberFromRoot(fiber);
334
+ if (currentFiber) return currentFiber;
346
335
 
347
336
  let rootFiber = fiber;
348
337
  while (rootFiber.return) {
@@ -370,29 +359,61 @@ export const getLatestFiber = (fiber: Fiber): Fiber => {
370
359
  /**
371
360
  * Returns the renderer that owns the {@link Fiber}.
372
361
  */
373
- export const getRenderer = (fiber: Fiber): ReactRenderer | null => {
374
- if (!hasRDTHook()) return null;
375
-
362
+ export const getRenderer = (
363
+ fiber: Fiber,
364
+ target: ReactDevToolsTarget = globalThis,
365
+ ): ReactRenderer | null => {
376
366
  let rootFiber = fiber;
377
- while (rootFiber.return) {
378
- rootFiber = rootFiber.return;
379
- }
380
-
367
+ while (rootFiber.return) rootFiber = rootFiber.return;
381
368
  const fiberRoot = rootFiber.stateNode;
382
369
  if (!isFiberRoot(fiberRoot)) return null;
383
370
 
384
- const rendererID = rootRendererIds.get(fiberRoot);
385
- if (rendererID === undefined) return null;
386
- return getRDTHook().renderers.get(rendererID) ?? null;
371
+ const trackedRendererId = rootRendererIds.get(fiberRoot);
372
+ const trackedHook = rootHooks.get(fiberRoot);
373
+ if (trackedRendererId !== undefined && trackedHook) {
374
+ return trackedHook.renderers.get(trackedRendererId) ?? null;
375
+ }
376
+
377
+ const rdtHook = target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
378
+ if (!rdtHook?.getFiberRoots) return null;
379
+ for (const [rendererId, renderer] of rdtHook.renderers) {
380
+ if (!rdtHook.getFiberRoots(rendererId).has(fiberRoot)) continue;
381
+ _fiberRoots.add(fiberRoot);
382
+ rootRendererIds.set(fiberRoot, rendererId);
383
+ rootHooks.set(fiberRoot, rdtHook);
384
+ setReactWorkTagsForFiber(fiberRoot.current, renderer);
385
+ return renderer;
386
+ }
387
+ return null;
387
388
  };
388
389
 
389
390
  export type RenderPhase = "mount" | "unmount" | "update";
390
391
 
392
+ interface FiberReference {
393
+ deref: () => Fiber | undefined;
394
+ }
395
+
391
396
  let nextFiberId = 0;
392
397
  const fiberIdMap = new WeakMap<Fiber, number>();
398
+ const fiberByIdMap = new Map<number, FiberReference>();
399
+ const fiberIdFinalizationRegistry =
400
+ typeof FinalizationRegistry === "function"
401
+ ? new FinalizationRegistry<number>((fiberId) => {
402
+ if (!fiberByIdMap.get(fiberId)?.deref()) fiberByIdMap.delete(fiberId);
403
+ })
404
+ : null;
405
+
406
+ const createFiberReference = (fiber: Fiber): FiberReference =>
407
+ typeof WeakRef === "function" ? new WeakRef(fiber) : { deref: () => fiber };
393
408
 
394
409
  export const setFiberId = (fiber: Fiber, fiberId: number = nextFiberId++): void => {
410
+ const previousFiberId = fiberIdMap.get(fiber);
411
+ if (previousFiberId !== undefined && previousFiberId !== fiberId) {
412
+ fiberByIdMap.delete(previousFiberId);
413
+ }
395
414
  fiberIdMap.set(fiber, fiberId);
415
+ fiberByIdMap.set(fiberId, createFiberReference(fiber));
416
+ fiberIdFinalizationRegistry?.register(fiber, fiberId);
396
417
  if (Number.isSafeInteger(fiberId) && fiberId >= nextFiberId) {
397
418
  nextFiberId = fiberId + 1;
398
419
  }
@@ -402,6 +423,7 @@ export const getFiberId = (fiber: Fiber): number => {
402
423
  let currentFiberId = fiberIdMap.get(fiber);
403
424
  if (currentFiberId === undefined && fiber.alternate) {
404
425
  currentFiberId = fiberIdMap.get(fiber.alternate);
426
+ if (currentFiberId !== undefined) setFiberId(fiber, currentFiberId);
405
427
  }
406
428
  if (currentFiberId === undefined) {
407
429
  currentFiberId = nextFiberId++;
@@ -410,131 +432,118 @@ export const getFiberId = (fiber: Fiber): number => {
410
432
  return currentFiberId;
411
433
  };
412
434
 
413
- const mountFiberRecursively = (
435
+ export const getFiberById = (fiberId: number): Fiber | null => {
436
+ const fiber = fiberByIdMap.get(fiberId)?.deref();
437
+ if (!fiber) {
438
+ fiberByIdMap.delete(fiberId);
439
+ return null;
440
+ }
441
+ return getLatestFiber(fiber);
442
+ };
443
+
444
+ const releaseFiberId = (fiber: Fiber): void => {
445
+ const relatedFibers = fiber.alternate ? [fiber, fiber.alternate] : [fiber];
446
+ const fiberIds = new Set<number>();
447
+
448
+ for (const relatedFiber of relatedFibers) {
449
+ const fiberId = fiberIdMap.get(relatedFiber);
450
+ if (fiberId !== undefined) fiberIds.add(fiberId);
451
+ fiberIdMap.delete(relatedFiber);
452
+ }
453
+
454
+ for (const fiberId of fiberIds) {
455
+ const assignedFiber = fiberByIdMap.get(fiberId)?.deref();
456
+ if (!assignedFiber || relatedFibers.includes(assignedFiber)) {
457
+ fiberByIdMap.delete(fiberId);
458
+ }
459
+ }
460
+ };
461
+
462
+ const getRenderedChild = (fiber: Fiber, skipPrimaryWrapper: boolean): Fiber | null => {
463
+ const workTags = getReactWorkTagsForFiber(fiber);
464
+ if (fiber.tag !== workTags.SuspenseComponent) return fiber.child;
465
+ if (fiber.memoizedState !== null) return fiber.child?.sibling?.child ?? null;
466
+ return skipPrimaryWrapper && workTags.OffscreenComponent !== -1
467
+ ? (fiber.child?.child ?? null)
468
+ : fiber.child;
469
+ };
470
+
471
+ const mountFiberTree = (
414
472
  onRender: RenderHandler,
415
473
  firstChild: Fiber,
416
474
  traverseSiblings: boolean,
417
475
  ): void => {
476
+ const pendingSiblings: Fiber[] = [];
418
477
  let fiber: Fiber | null = firstChild;
419
-
420
- while (fiber !== null) {
478
+ while (fiber) {
421
479
  getFiberId(fiber);
422
- const shouldIncludeInTree = !shouldFilterFiber(fiber);
423
- if (shouldIncludeInTree && didFiberRender(fiber)) {
424
- onRender(fiber, "mount");
425
- }
426
-
427
- if (fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent) {
428
- const isTimedOut = fiber.memoizedState !== null;
429
- if (isTimedOut) {
430
- // Special case: if Suspense mounts in a timed-out state,
431
- // get the fallback child from the inner fragment and mount
432
- // it as if it was our own child. Updates handle this too.
433
- const primaryChildFragment = fiber.child;
434
- const fallbackChildFragment = primaryChildFragment ? primaryChildFragment.sibling : null;
435
- if (fallbackChildFragment) {
436
- const fallbackChild = fallbackChildFragment.child;
437
- if (fallbackChild !== null) {
438
- mountFiberRecursively(onRender, fallbackChild, false);
439
- }
440
- }
441
- } else {
442
- const primaryChild = fiber.child?.child ?? null;
443
- if (primaryChild !== null) {
444
- mountFiberRecursively(onRender, primaryChild, false);
445
- }
446
- }
447
- } else if (fiber.child !== null) {
448
- mountFiberRecursively(onRender, fiber.child, true);
449
- }
450
- fiber = traverseSiblings ? fiber.sibling : null;
480
+ if (!shouldFilterFiber(fiber) && didFiberRender(fiber)) onRender(fiber, "mount");
481
+ if (traverseSiblings && fiber.sibling) pendingSiblings.push(fiber.sibling);
482
+ fiber = getRenderedChild(fiber, true) ?? pendingSiblings.pop() ?? null;
483
+ traverseSiblings = true;
451
484
  }
452
485
  };
453
486
 
454
- const updateFiberRecursively = (
487
+ interface FiberUpdate {
488
+ fiber: Fiber;
489
+ previousFiber: Fiber | null;
490
+ traverseSiblings: boolean;
491
+ }
492
+
493
+ const updateFiberTree = (
455
494
  onRender: RenderHandler,
456
495
  nextFiber: Fiber,
457
496
  prevFiber: Fiber | null,
458
497
  ): void => {
459
- getFiberId(nextFiber);
460
- if (!prevFiber) return;
461
- getFiberId(prevFiber);
462
-
463
- const isSuspense = nextFiber.tag === getReactWorkTagsForFiber(nextFiber).SuspenseComponent;
464
-
465
- const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
466
- if (shouldIncludeInTree && didFiberRender(nextFiber)) {
467
- onRender(nextFiber, "update");
468
- }
469
-
470
- // The behavior of timed-out Suspense trees is unique.
471
- // Rather than unmount the timed out content (and possibly lose important state),
472
- // React re-parents this content within a hidden Fragment while the fallback is showing.
473
- // This behavior doesn't need to be observable in the DevTools though.
474
- // It might even result in a bad user experience for e.g. node selection in the Elements panel.
475
- // The easiest fix is to strip out the intermediate Fragment fibers,
476
- // so the Elements panel and Profiler don't need to special case them.
477
- // Suspense components only have a non-null memoizedState if they're timed-out.
478
- const prevDidTimeout = isSuspense && prevFiber.memoizedState !== null;
479
- const nextDidTimeOut = isSuspense && nextFiber.memoizedState !== null;
480
-
481
- // The logic below is inspired by the code paths in updateSuspenseComponent()
482
- // inside ReactFiberBeginWork in the React source code.
483
- if (prevDidTimeout && nextDidTimeOut) {
484
- // Fallback -> Fallback:
485
- // 1. Reconcile fallback set.
486
- const nextFallbackChildSet = nextFiber.child?.sibling ?? null;
487
- // Note: We can't use nextFiber.child.sibling.alternate
488
- // because the set is special and alternate may not exist.
489
- const prevFallbackChildSet = prevFiber.child?.sibling ?? null;
490
-
491
- if (nextFallbackChildSet !== null && prevFallbackChildSet !== null) {
492
- updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet);
493
- }
494
- } else if (prevDidTimeout && !nextDidTimeOut) {
495
- // Fallback -> Primary:
496
- // 1. Unmount fallback set
497
- // Note: don't emulate fallback unmount because React actually did it.
498
- // 2. Mount primary set
499
- const nextPrimaryChildSet = nextFiber.child;
500
-
501
- if (nextPrimaryChildSet !== null) {
502
- mountFiberRecursively(onRender, nextPrimaryChildSet, true);
498
+ if (!prevFiber) {
499
+ getFiberId(nextFiber);
500
+ return;
501
+ }
502
+ const pendingUpdates: FiberUpdate[] = [
503
+ { fiber: nextFiber, previousFiber: prevFiber, traverseSiblings: false },
504
+ ];
505
+ let update: FiberUpdate | undefined;
506
+ while ((update = pendingUpdates.pop())) {
507
+ const { fiber, previousFiber, traverseSiblings } = update;
508
+ if (traverseSiblings && fiber.sibling) {
509
+ pendingUpdates.push({
510
+ fiber: fiber.sibling,
511
+ previousFiber: fiber.sibling.alternate,
512
+ traverseSiblings: true,
513
+ });
503
514
  }
504
- } else if (!prevDidTimeout && nextDidTimeOut) {
505
- // Primary -> Fallback:
506
- // 1. Hide primary set
507
- // This is not a real unmount, so it won't get reported by React.
508
- // We need to manually walk the previous tree and record unmounts.
509
- unmountFiberChildrenRecursively(onRender, prevFiber);
510
-
511
- // 2. Mount fallback set
512
- const nextFallbackChildSet = nextFiber.child?.sibling ?? null;
513
-
514
- if (nextFallbackChildSet !== null) {
515
- mountFiberRecursively(onRender, nextFallbackChildSet, true);
515
+ if (!previousFiber) {
516
+ mountFiberTree(onRender, fiber, false);
517
+ continue;
516
518
  }
517
- } else if (nextFiber.child !== prevFiber.child) {
518
- // Common case: Primary -> Primary.
519
- // This is the same code path as for non-Suspense fibers.
520
-
521
- // If the first child is different, we need to traverse them.
522
- // Each next child will be either a new child (mount) or an alternate (update).
523
- let nextChild = nextFiber.child;
524
-
525
- while (nextChild) {
526
- // We already know children will be referentially different because
527
- // they are either new mounts or alternates of previous children.
528
- // Schedule updates and mounts depending on whether alternates exist.
529
- // We don't track deletions here because they are reported separately.
530
- if (nextChild.alternate) {
531
- updateFiberRecursively(onRender, nextChild, nextChild.alternate);
532
- } else {
533
- mountFiberRecursively(onRender, nextChild, false);
519
+ getFiberId(fiber);
520
+ getFiberId(previousFiber);
521
+ if (!shouldFilterFiber(fiber) && didFiberRender(fiber)) onRender(fiber, "update");
522
+ const isSuspense = fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent;
523
+ const wasTimedOut = isSuspense && previousFiber.memoizedState !== null;
524
+ const isTimedOut = isSuspense && fiber.memoizedState !== null;
525
+ if (wasTimedOut && isTimedOut) {
526
+ const nextFallback = fiber.child?.sibling;
527
+ const previousFallback = previousFiber.child?.sibling;
528
+ if (nextFallback && previousFallback) {
529
+ pendingUpdates.push({
530
+ fiber: nextFallback,
531
+ previousFiber: previousFallback,
532
+ traverseSiblings: false,
533
+ });
534
534
  }
535
-
536
- // Try the next child.
537
- nextChild = nextChild.sibling;
535
+ } else if (wasTimedOut && !isTimedOut) {
536
+ if (fiber.child) mountFiberTree(onRender, fiber.child, true);
537
+ } else if (!wasTimedOut && isTimedOut) {
538
+ unmountFiberChildren(onRender, previousFiber);
539
+ const fallback = fiber.child?.sibling;
540
+ if (fallback) mountFiberTree(onRender, fallback, true);
541
+ } else if (fiber.child && fiber.child !== previousFiber.child) {
542
+ pendingUpdates.push({
543
+ fiber: fiber.child,
544
+ previousFiber: fiber.child.alternate,
545
+ traverseSiblings: true,
546
+ });
538
547
  }
539
548
  }
540
549
  };
@@ -547,30 +556,18 @@ const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
547
556
  }
548
557
  };
549
558
 
550
- const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => {
551
- // We might meet a nested Suspense on our way.
552
- const isTimedOutSuspense =
553
- fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent && fiber.memoizedState !== null;
554
- let child = fiber.child;
555
-
556
- if (isTimedOutSuspense) {
557
- // If it's showing fallback tree, let's traverse it instead.
558
- const primaryChildFragment = fiber.child;
559
- const fallbackChildFragment = primaryChildFragment?.sibling ?? null;
560
-
561
- // Skip over to the real Fiber child.
562
- child = fallbackChildFragment?.child ?? null;
563
- }
564
-
565
- while (child !== null) {
566
- // Record simulated unmounts children-first.
567
- // We skip nodes without return because those are real unmounts.
559
+ const unmountFiberChildren = (onRender: RenderHandler, fiber: Fiber): void => {
560
+ const pendingSiblings: Fiber[] = [];
561
+ let child = getRenderedChild(fiber, false);
562
+ while (child) {
563
+ if (child.sibling) pendingSiblings.push(child.sibling);
568
564
  if (child.return !== null) {
569
565
  unmountFiber(onRender, child);
570
- unmountFiberChildrenRecursively(onRender, child);
566
+ child = getRenderedChild(child, false);
567
+ } else {
568
+ child = null;
571
569
  }
572
-
573
- child = child.sibling;
570
+ child ??= pendingSiblings.pop() ?? null;
574
571
  }
575
572
  };
576
573
 
@@ -620,14 +617,14 @@ export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: Render
620
617
  const isMounted = isRootFiberMounted(fiber);
621
618
 
622
619
  if (!wasMounted && isMounted) {
623
- mountFiberRecursively(onRender, fiber, false);
620
+ mountFiberTree(onRender, fiber, false);
624
621
  } else if (wasMounted && isMounted) {
625
- updateFiberRecursively(onRender, fiber, fiber.alternate);
622
+ updateFiberTree(onRender, fiber, fiber.alternate);
626
623
  } else if (wasMounted && !isMounted) {
627
624
  unmountFiber(onRender, fiber);
628
625
  }
629
626
  } else {
630
- mountFiberRecursively(onRender, fiber, true);
627
+ mountFiberTree(onRender, fiber, true);
631
628
  }
632
629
 
633
630
  rootInstance.prevFiber = fiber;
@@ -635,6 +632,7 @@ export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: Render
635
632
 
636
633
  export interface InstrumentationOptions {
637
634
  name?: string;
635
+ target?: ReactDevToolsTarget;
638
636
  onActive?: () => unknown;
639
637
  onCommitFiberRoot?: (
640
638
  rendererID: number,
@@ -649,6 +647,7 @@ export interface InstrumentationOptions {
649
647
 
650
648
  interface InstrumentationSubscription {
651
649
  options: InstrumentationOptions;
650
+ target: ReactDevToolsTarget;
652
651
  }
653
652
 
654
653
  const instrumentationSubscriptions = new Set<InstrumentationSubscription>();
@@ -661,6 +660,7 @@ interface HookDispatchers {
661
660
  }
662
661
 
663
662
  const hookDispatchers = new WeakMap<ReactDevToolsGlobalHook, Partial<HookDispatchers>>();
663
+ const hookTargets = new WeakMap<ReactDevToolsGlobalHook, ReactDevToolsTarget>();
664
664
  let didSubscribeToHookReplacements = false;
665
665
 
666
666
  // each hook event is dispatched from a single re-installable wrapper. If
@@ -684,27 +684,24 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
684
684
  didError,
685
685
  ) => {
686
686
  if (prevOnCommitFiberRoot) {
687
- prevOnCommitFiberRoot.call(rdtHook, rendererID, root, priority, didError);
687
+ callListener(prevOnCommitFiberRoot, rdtHook, rendererID, root, priority, didError);
688
688
  }
689
689
  if (hookDispatchers.get(rdtHook)?.onCommitFiberRoot !== dispatchCommitFiberRoot) return;
690
690
  setReactWorkTagsForFiber(root.current, rdtHook.renderers.get(rendererID));
691
691
  // Custom renderers and test harnesses commit roots without a memoizedState;
692
692
  // those must stay tracked, so only explicit unmount evidence removes a root.
693
- const rootMemoizedState = root.current.memoizedState;
694
- const isUnmounting =
695
- rootMemoizedState === null ||
696
- (rootMemoizedState !== undefined &&
697
- (rootMemoizedState.element === null || rootMemoizedState.element === undefined));
698
- if (isUnmounting) {
693
+ if (isFiberRootUnmounted(root)) {
699
694
  _fiberRoots.delete(root);
700
695
  rootRendererIds.delete(root);
696
+ rootHooks.delete(root);
701
697
  } else {
702
698
  _fiberRoots.add(root);
703
699
  rootRendererIds.set(root, rendererID);
700
+ rootHooks.set(root, rdtHook);
704
701
  }
705
- for (const { options } of instrumentationSubscriptions) {
706
- if (options.onCommitFiberRoot) {
707
- options.onCommitFiberRoot(rendererID, root, priority, didError);
702
+ for (const { options, target } of instrumentationSubscriptions) {
703
+ if (target === hookTargets.get(rdtHook) && options.onCommitFiberRoot) {
704
+ callListener(options.onCommitFiberRoot, options, rendererID, root, priority, didError);
708
705
  }
709
706
  }
710
707
  };
@@ -723,15 +720,19 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
723
720
  ) => {
724
721
  setReactWorkTagsForFiber(fiber, rdtHook.renderers.get(rendererID));
725
722
  if (prevOnCommitFiberUnmount) {
726
- prevOnCommitFiberUnmount.call(rdtHook, rendererID, fiber);
723
+ callListener(prevOnCommitFiberUnmount, rdtHook, rendererID, fiber);
727
724
  }
728
725
  if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
729
726
  return;
730
727
  }
731
- for (const { options } of instrumentationSubscriptions) {
732
- if (options.onCommitFiberUnmount) {
733
- options.onCommitFiberUnmount(rendererID, fiber);
728
+ try {
729
+ for (const { options, target } of instrumentationSubscriptions) {
730
+ if (target === hookTargets.get(rdtHook) && options.onCommitFiberUnmount) {
731
+ callListener(options.onCommitFiberUnmount, options, rendererID, fiber);
732
+ }
734
733
  }
734
+ } finally {
735
+ releaseFiberId(fiber);
735
736
  }
736
737
  };
737
738
  dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
@@ -748,14 +749,14 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
748
749
  root,
749
750
  ) => {
750
751
  if (prevOnPostCommitFiberRoot) {
751
- prevOnPostCommitFiberRoot.call(rdtHook, rendererID, root);
752
+ callListener(prevOnPostCommitFiberRoot, rdtHook, rendererID, root);
752
753
  }
753
754
  if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
754
755
  return;
755
756
  }
756
- for (const { options } of instrumentationSubscriptions) {
757
- if (options.onPostCommitFiberRoot) {
758
- options.onPostCommitFiberRoot(rendererID, root);
757
+ for (const { options, target } of instrumentationSubscriptions) {
758
+ if (target === hookTargets.get(rdtHook) && options.onPostCommitFiberRoot) {
759
+ callListener(options.onPostCommitFiberRoot, options, rendererID, root);
759
760
  }
760
761
  }
761
762
  };
@@ -774,12 +775,12 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
774
775
  children,
775
776
  ) => {
776
777
  if (prevOnScheduleFiberRoot) {
777
- prevOnScheduleFiberRoot.call(rdtHook, rendererID, root, children);
778
+ callListener(prevOnScheduleFiberRoot, rdtHook, rendererID, root, children);
778
779
  }
779
780
  if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
780
- for (const { options } of instrumentationSubscriptions) {
781
- if (options.onScheduleFiberRoot) {
782
- options.onScheduleFiberRoot(rendererID, root, children);
781
+ for (const { options, target } of instrumentationSubscriptions) {
782
+ if (target === hookTargets.get(rdtHook) && options.onScheduleFiberRoot) {
783
+ callListener(options.onScheduleFiberRoot, options, rendererID, root, children);
783
784
  }
784
785
  }
785
786
  };
@@ -788,11 +789,23 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
788
789
  }
789
790
  };
790
791
 
791
- const wireHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
792
+ const handleHookReplacement = (
793
+ rdtHook: ReactDevToolsGlobalHook,
794
+ target: ReactDevToolsTarget,
795
+ ): void => {
796
+ hookTargets.set(rdtHook, target);
797
+ setHookEventDispatchers(rdtHook);
798
+ };
799
+
800
+ const wireHookEventDispatchers = (
801
+ rdtHook: ReactDevToolsGlobalHook,
802
+ target: ReactDevToolsTarget = globalThis,
803
+ ): void => {
792
804
  if (!didSubscribeToHookReplacements) {
793
- onRDTHookReplace(setHookEventDispatchers);
805
+ onRDTHookReplace(handleHookReplacement);
794
806
  didSubscribeToHookReplacements = true;
795
807
  }
808
+ hookTargets.set(rdtHook, target);
796
809
  setHookEventDispatchers(rdtHook);
797
810
  };
798
811
 
@@ -822,15 +835,16 @@ try {
822
835
  * unsubscribe();
823
836
  */
824
837
  export const instrument = (options: InstrumentationOptions): Unsubscribe => {
825
- const rdtHook = getRDTHook(options.onActive);
838
+ const target = options.target ?? globalThis;
839
+ const rdtHook = getRDTHook(options.onActive, target);
826
840
  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
827
841
 
828
- wireHookEventDispatchers(rdtHook);
829
- const subscription: InstrumentationSubscription = { options };
842
+ wireHookEventDispatchers(rdtHook, target);
843
+ const subscription: InstrumentationSubscription = { options, target };
830
844
  instrumentationSubscriptions.add(subscription);
831
845
 
832
846
  return createUnsubscribe(() => {
833
- if (options.onActive) _onActiveListeners.delete(options.onActive);
847
+ if (options.onActive) removeActiveListener(options.onActive, target);
834
848
  instrumentationSubscriptions.delete(subscription);
835
849
  });
836
850
  };
@@ -874,9 +888,12 @@ const getPublicHostInstance = (stateNode: unknown): unknown => {
874
888
  return typeof nativeTag === "number" ? nativeTag : stateNode;
875
889
  };
876
890
 
877
- export const getFiber = <T>(hostInstance: T): Fiber | null => {
878
- const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
879
- if (rdtHook?.renderers) {
891
+ export const getFiber = <HostInstance>(
892
+ hostInstance: HostInstance,
893
+ target: ReactDevToolsTarget = globalThis,
894
+ ): Fiber | null => {
895
+ const rdtHook = target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
896
+ if (rdtHook) {
880
897
  for (const renderer of rdtHook.renderers.values()) {
881
898
  try {
882
899
  const fiber = renderer.findFiberByHostInstance?.(hostInstance);
@@ -899,19 +916,31 @@ export const getFiber = <T>(hostInstance: T): Fiber | null => {
899
916
 
900
917
  for (const key of Object.keys(hostInstance)) {
901
918
  if (isFiberPropertyKey(key)) {
902
- knownFiberPropertyKeys.add(key);
903
919
  const fiber = Reflect.get(hostInstance, key);
904
- if (isFiber(fiber)) return fiber;
920
+ if (isFiber(fiber)) {
921
+ knownFiberPropertyKeys.add(key);
922
+ return fiber;
923
+ }
905
924
  }
906
925
  }
907
926
  }
908
927
 
909
928
  if (
910
929
  hostInstance !== null &&
911
- hostInstance !== undefined &&
912
930
  (typeof hostInstance === "object" || typeof hostInstance === "number")
913
931
  ) {
932
+ const targetFiberRoots = new Set<FiberRoot>();
933
+ if (rdtHook?.getFiberRoots) {
934
+ for (const rendererId of rdtHook.renderers.keys()) {
935
+ for (const fiberRoot of rdtHook.getFiberRoots(rendererId)) {
936
+ targetFiberRoots.add(fiberRoot);
937
+ }
938
+ }
939
+ }
914
940
  for (const fiberRoot of _fiberRoots) {
941
+ if (rootHooks.get(fiberRoot) === rdtHook) targetFiberRoots.add(fiberRoot);
942
+ }
943
+ for (const fiberRoot of targetFiberRoots) {
915
944
  const fiber = traverseFiber(
916
945
  fiberRoot.current,
917
946
  (candidateFiber) =>
@@ -928,16 +957,16 @@ export const getFiberFromHostInstance = getFiber;
928
957
 
929
958
  export {
930
959
  BIPPY_INSTRUMENTATION_STRING,
931
- _onActiveListeners,
932
960
  _renderers,
933
961
  getRDTHook,
934
962
  hasRDTHook,
935
963
  installRDTHook,
964
+ isFiberRootUnmounted,
936
965
  isReactRefresh,
937
966
  isRealReactDevtools,
938
967
  onRendererInject,
939
968
  patchRDTHook,
940
969
  version,
941
970
  } from "./rdt-hook.js";
942
- export type { Unsubscribe } from "./rdt-hook.js";
971
+ export type { ReactDevToolsTarget, Unsubscribe } from "./rdt-hook.js";
943
972
  export * from "./react-internals/index.js";