bippy 0.7.3-dev.8ba0705 → 0.7.3-dev.c0e872e

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,
@@ -21,6 +22,7 @@ import {
21
22
  removeActiveListener,
22
23
  } from "./rdt-hook.js";
23
24
  import type { ReactDevToolsTarget, Unsubscribe } from "./rdt-hook.js";
25
+ import { getCurrentFiberFromRoot } from "./react-internals/current-fiber.js";
24
26
  import {
25
27
  getReactWorkTagsForFiber,
26
28
  ReactBuildType,
@@ -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
 
@@ -166,16 +168,20 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
166
168
  // https://github.com/bvaughn/react-devtools-experimental/issues/197
167
169
  return true;
168
170
 
169
- case workTags.Fragment:
171
+ case workTags.HostPortal:
170
172
  case workTags.HostText:
171
173
  case workTags.LegacyHiddenComponent:
172
174
  case workTags.OffscreenComponent:
175
+ case workTags.Throw:
173
176
  return true;
174
177
 
175
178
  case workTags.HostRoot:
176
179
  // It is never valid to filter the root element.
177
180
  return false;
178
181
 
182
+ case workTags.Fragment:
183
+ return fiber.key === null;
184
+
179
185
  default: {
180
186
  const symbolOrNumber =
181
187
  typeof fiber.type === "object" && fiber.type !== null ? fiber.type.$$typeof : fiber.type;
@@ -183,7 +189,8 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
183
189
  if (typeof symbolOrNumber === "symbol") {
184
190
  return (
185
191
  symbolOrNumber.description === ReactSymbols.CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
186
- symbolOrNumber.description === ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION
192
+ symbolOrNumber.description === ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_DESCRIPTION ||
193
+ symbolOrNumber.description === ReactSymbols.STRICT_MODE_SYMBOL_DESCRIPTION
187
194
  );
188
195
  }
189
196
 
@@ -191,6 +198,8 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
191
198
  case ReactSymbols.CONCURRENT_MODE_NUMBER:
192
199
  case ReactSymbols.CONCURRENT_MODE_SYMBOL_STRING:
193
200
  case ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
201
+ case ReactSymbols.STRICT_MODE_NUMBER:
202
+ case ReactSymbols.STRICT_MODE_SYMBOL_STRING:
194
203
  return true;
195
204
 
196
205
  default:
@@ -200,73 +209,64 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
200
209
  }
201
210
  };
202
211
 
212
+ interface TraverseFiber {
213
+ (
214
+ fiber: Fiber | null,
215
+ selector: (node: Fiber) => boolean | void,
216
+ ascending?: boolean,
217
+ ): Fiber | null;
218
+ (
219
+ fiber: Fiber | null,
220
+ selector: (node: Fiber) => Promise<boolean | void>,
221
+ ascending?: boolean,
222
+ ): Promise<Fiber | null>;
223
+ (
224
+ fiber: Fiber | null,
225
+ selector: FiberSelector,
226
+ ascending?: boolean,
227
+ ): Fiber | null | Promise<Fiber | null>;
228
+ }
229
+
203
230
  /**
204
231
  * Traverses up or down a {@link Fiber}, return `true` to stop and select a node.
205
232
  */
206
- export function traverseFiber(
207
- fiber: Fiber | null,
208
- selector: (node: Fiber) => boolean | void,
209
- ascending?: boolean,
210
- ): Fiber | null;
211
- export function traverseFiber(
212
- fiber: Fiber | null,
213
- selector: (node: Fiber) => Promise<boolean | void>,
214
- ascending?: boolean,
215
- ): Promise<Fiber | null>;
216
- export function traverseFiber(
217
- fiber: Fiber | null,
218
- selector: FiberSelector,
219
- ascending?: boolean,
220
- ): Fiber | null | Promise<Fiber | null>;
221
- export function traverseFiber(
233
+ export const traverseFiber = ((
222
234
  fiber: Fiber | null,
223
235
  selector: FiberSelector,
224
236
  ascending = false,
225
- ): Fiber | null | Promise<Fiber | null> {
226
- if (!fiber) return null;
227
-
228
- const selection = selector(fiber);
229
- if (isPromiseLike<boolean | void>(selection)) {
230
- return Promise.resolve(selection).then((didSelectFiber) =>
231
- didSelectFiber === true ? fiber : traverseFiberChildren(fiber, selector, ascending),
232
- );
233
- }
234
- if (selection === true) return fiber;
235
-
236
- return traverseFiberChildren(fiber, selector, ascending);
237
- }
238
-
239
- const isPromiseLike = <Result>(value: unknown): value is PromiseLike<Result> =>
240
- (typeof value === "object" || typeof value === "function") &&
241
- value !== null &&
242
- "then" in value &&
243
- typeof value.then === "function";
244
-
245
- const traverseFiberChildren = (
246
- fiber: Fiber,
247
- selector: FiberSelector,
248
- ascending: boolean,
249
237
  ): Fiber | null | Promise<Fiber | null> => {
250
- const firstChild = ascending ? fiber.return : fiber.child;
251
- return traverseFiberSiblings(firstChild, selector, ascending);
252
- };
253
-
254
- const traverseFiberSiblings = (
255
- fiber: Fiber | null,
256
- selector: FiberSelector,
257
- ascending: boolean,
258
- ): Fiber | null | Promise<Fiber | null> => {
259
- if (!fiber) return null;
260
-
261
- const nextSibling = ascending ? null : fiber.sibling;
262
- const match = traverseFiber(fiber, selector, ascending);
263
- if (isPromiseLike<Fiber | null>(match)) {
264
- return Promise.resolve(match).then(
265
- (resolvedMatch) => resolvedMatch ?? traverseFiberSiblings(nextSibling, selector, ascending),
266
- );
267
- }
268
- return match ?? traverseFiberSiblings(nextSibling, selector, ascending);
269
- };
238
+ const pendingSiblings: Fiber[] = [];
239
+ const getNextFiber = (currentFiber: Fiber): Fiber | null => {
240
+ if (ascending) return currentFiber.return;
241
+ if (currentFiber !== fiber && currentFiber.sibling) {
242
+ pendingSiblings.push(currentFiber.sibling);
243
+ }
244
+ return currentFiber.child ?? pendingSiblings.pop() ?? null;
245
+ };
246
+ const visit = (firstFiber: Fiber | null): Fiber | null | Promise<Fiber | null> => {
247
+ let currentFiber = firstFiber;
248
+ while (currentFiber) {
249
+ const selectedFiber = currentFiber;
250
+ const selection = selector(selectedFiber);
251
+ const then =
252
+ selection !== null && (typeof selection === "object" || typeof selection === "function")
253
+ ? selection.then
254
+ : null;
255
+ if (typeof then === "function") {
256
+ return Promise.resolve<boolean | void>({
257
+ // oxlint-disable-next-line unicorn/no-thenable -- Assimilate the captured method without reading the accessor twice.
258
+ then: (resolve, reject) => Reflect.apply(then, selection, [resolve, reject]),
259
+ }).then((didSelectFiber) =>
260
+ didSelectFiber === true ? selectedFiber : visit(getNextFiber(selectedFiber)),
261
+ );
262
+ }
263
+ if (selection === true) return selectedFiber;
264
+ currentFiber = getNextFiber(selectedFiber);
265
+ }
266
+ return null;
267
+ };
268
+ return visit(fiber);
269
+ }) as TraverseFiber;
270
270
 
271
271
  /**
272
272
  * Returns `true` if the {@link Fiber} uses React Compiler's memo cache.
@@ -281,7 +281,14 @@ export const hasMemoCache = (fiber: Fiber): boolean => {
281
281
  export const getType = (type: unknown): null | React.ComponentType<unknown> => {
282
282
  if (isComponentType(type)) return type;
283
283
  if (typeof type !== "object" || type === null) return null;
284
- return getType(Reflect.get(type, "type") ?? Reflect.get(type, "render"));
284
+ const visitedTypes = new Set<object>();
285
+ let currentType: unknown = type;
286
+ while (typeof currentType === "object" && currentType !== null) {
287
+ if (visitedTypes.has(currentType)) return null;
288
+ visitedTypes.add(currentType);
289
+ currentType = Reflect.get(currentType, "type") ?? Reflect.get(currentType, "render");
290
+ }
291
+ return isComponentType(currentType) ? currentType : null;
285
292
  };
286
293
 
287
294
  /**
@@ -324,6 +331,7 @@ export const isInstrumentationActive = (target: ReactDevToolsTarget = globalThis
324
331
  export const _fiberRoots = new Set<FiberRoot>();
325
332
  const rootRendererIds = new WeakMap<FiberRoot, number>();
326
333
  const rootHooks = new WeakMap<FiberRoot, ReactDevToolsGlobalHook>();
334
+ let currentUnmountingFiber: Fiber | null = null;
327
335
 
328
336
  /**
329
337
  * Returns the latest fiber (since it may be double-buffered).
@@ -331,6 +339,10 @@ const rootHooks = new WeakMap<FiberRoot, ReactDevToolsGlobalHook>();
331
339
  export const getLatestFiber = (fiber: Fiber): Fiber => {
332
340
  const alternate = fiber.alternate;
333
341
  if (!alternate) return fiber;
342
+ const currentFiber = getCurrentFiberFromRoot(fiber);
343
+ const isUnmountingPair = fiber === currentUnmountingFiber || alternate === currentUnmountingFiber;
344
+ if (currentFiber && (!isUnmountingPair || currentFiber === currentUnmountingFiber))
345
+ return currentFiber;
334
346
 
335
347
  let rootFiber = fiber;
336
348
  while (rootFiber.return) {
@@ -342,6 +354,7 @@ export const getLatestFiber = (fiber: Fiber): Fiber => {
342
354
  });
343
355
  if (latestFiber) return latestFiber;
344
356
  }
357
+ if (currentFiber) return currentFiber;
345
358
 
346
359
  if (alternate.actualStartTime && fiber.actualStartTime) {
347
360
  return alternate.actualStartTime > fiber.actualStartTime ? alternate : fiber;
@@ -405,14 +418,24 @@ const fiberIdFinalizationRegistry =
405
418
  const createFiberReference = (fiber: Fiber): FiberReference =>
406
419
  typeof WeakRef === "function" ? new WeakRef(fiber) : { deref: () => fiber };
407
420
 
408
- export const setFiberId = (fiber: Fiber, fiberId: number = nextFiberId++): void => {
421
+ const getNextFiberId = (): number => {
422
+ if (!Number.isSafeInteger(nextFiberId)) throw new RangeError("Fiber ID space exhausted");
423
+ return nextFiberId++;
424
+ };
425
+
426
+ export const setFiberId = (fiber: Fiber, fiberId: number = getNextFiberId()): void => {
409
427
  const previousFiberId = fiberIdMap.get(fiber);
410
- if (previousFiberId !== undefined && previousFiberId !== fiberId) {
428
+ if (
429
+ previousFiberId !== undefined &&
430
+ previousFiberId !== fiberId &&
431
+ fiberByIdMap.get(previousFiberId)?.deref() === fiber
432
+ ) {
411
433
  fiberByIdMap.delete(previousFiberId);
412
434
  }
413
435
  fiberIdMap.set(fiber, fiberId);
414
436
  fiberByIdMap.set(fiberId, createFiberReference(fiber));
415
- fiberIdFinalizationRegistry?.register(fiber, fiberId);
437
+ if (previousFiberId !== undefined) fiberIdFinalizationRegistry?.unregister(fiber);
438
+ fiberIdFinalizationRegistry?.register(fiber, fiberId, fiber);
416
439
  if (Number.isSafeInteger(fiberId) && fiberId >= nextFiberId) {
417
440
  nextFiberId = fiberId + 1;
418
441
  }
@@ -422,10 +445,17 @@ export const getFiberId = (fiber: Fiber): number => {
422
445
  let currentFiberId = fiberIdMap.get(fiber);
423
446
  if (currentFiberId === undefined && fiber.alternate) {
424
447
  currentFiberId = fiberIdMap.get(fiber.alternate);
425
- if (currentFiberId !== undefined) setFiberId(fiber, currentFiberId);
448
+ if (currentFiberId !== undefined) {
449
+ const assignedFiber = fiberByIdMap.get(currentFiberId)?.deref();
450
+ if (assignedFiber && assignedFiber !== fiber.alternate) {
451
+ fiberIdMap.set(fiber, currentFiberId);
452
+ } else {
453
+ setFiberId(fiber, currentFiberId);
454
+ }
455
+ }
426
456
  }
427
457
  if (currentFiberId === undefined) {
428
- currentFiberId = nextFiberId++;
458
+ currentFiberId = getNextFiberId();
429
459
  setFiberId(fiber, currentFiberId);
430
460
  }
431
461
  return currentFiberId;
@@ -448,6 +478,7 @@ const releaseFiberId = (fiber: Fiber): void => {
448
478
  const fiberId = fiberIdMap.get(relatedFiber);
449
479
  if (fiberId !== undefined) fiberIds.add(fiberId);
450
480
  fiberIdMap.delete(relatedFiber);
481
+ fiberIdFinalizationRegistry?.unregister(relatedFiber);
451
482
  }
452
483
 
453
484
  for (const fiberId of fiberIds) {
@@ -458,131 +489,91 @@ const releaseFiberId = (fiber: Fiber): void => {
458
489
  }
459
490
  };
460
491
 
461
- const mountFiberRecursively = (
492
+ const getRenderedChild = (fiber: Fiber, skipPrimaryWrapper: boolean): Fiber | null => {
493
+ const workTags = getReactWorkTagsForFiber(fiber);
494
+ if (fiber.tag !== workTags.SuspenseComponent) return fiber.child;
495
+ if (fiber.memoizedState !== null) return fiber.child?.sibling?.child ?? null;
496
+ return skipPrimaryWrapper && workTags.OffscreenComponent !== -1
497
+ ? (fiber.child?.child ?? null)
498
+ : fiber.child;
499
+ };
500
+
501
+ const mountFiberTree = (
462
502
  onRender: RenderHandler,
463
503
  firstChild: Fiber,
464
504
  traverseSiblings: boolean,
465
505
  ): void => {
506
+ const pendingSiblings: Fiber[] = [];
466
507
  let fiber: Fiber | null = firstChild;
467
-
468
- while (fiber !== null) {
508
+ while (fiber) {
469
509
  getFiberId(fiber);
470
- const shouldIncludeInTree = !shouldFilterFiber(fiber);
471
- if (shouldIncludeInTree && didFiberRender(fiber)) {
472
- onRender(fiber, "mount");
473
- }
474
-
475
- if (fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent) {
476
- const isTimedOut = fiber.memoizedState !== null;
477
- if (isTimedOut) {
478
- // Special case: if Suspense mounts in a timed-out state,
479
- // get the fallback child from the inner fragment and mount
480
- // it as if it was our own child. Updates handle this too.
481
- const primaryChildFragment = fiber.child;
482
- const fallbackChildFragment = primaryChildFragment ? primaryChildFragment.sibling : null;
483
- if (fallbackChildFragment) {
484
- const fallbackChild = fallbackChildFragment.child;
485
- if (fallbackChild !== null) {
486
- mountFiberRecursively(onRender, fallbackChild, false);
487
- }
488
- }
489
- } else {
490
- const primaryChild = fiber.child?.child ?? null;
491
- if (primaryChild !== null) {
492
- mountFiberRecursively(onRender, primaryChild, false);
493
- }
494
- }
495
- } else if (fiber.child !== null) {
496
- mountFiberRecursively(onRender, fiber.child, true);
497
- }
498
- fiber = traverseSiblings ? fiber.sibling : null;
510
+ if (!shouldFilterFiber(fiber) && didFiberRender(fiber)) onRender(fiber, "mount");
511
+ if (traverseSiblings && fiber.sibling) pendingSiblings.push(fiber.sibling);
512
+ fiber = getRenderedChild(fiber, true) ?? pendingSiblings.pop() ?? null;
513
+ traverseSiblings = true;
499
514
  }
500
515
  };
501
516
 
502
- const updateFiberRecursively = (
517
+ interface FiberUpdate {
518
+ fiber: Fiber;
519
+ previousFiber: Fiber | null;
520
+ traverseSiblings: boolean;
521
+ }
522
+
523
+ const updateFiberTree = (
503
524
  onRender: RenderHandler,
504
525
  nextFiber: Fiber,
505
526
  prevFiber: Fiber | null,
506
527
  ): void => {
507
- getFiberId(nextFiber);
508
- if (!prevFiber) return;
509
- getFiberId(prevFiber);
510
-
511
- const isSuspense = nextFiber.tag === getReactWorkTagsForFiber(nextFiber).SuspenseComponent;
512
-
513
- const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
514
- if (shouldIncludeInTree && didFiberRender(nextFiber)) {
515
- onRender(nextFiber, "update");
516
- }
517
-
518
- // The behavior of timed-out Suspense trees is unique.
519
- // Rather than unmount the timed out content (and possibly lose important state),
520
- // React re-parents this content within a hidden Fragment while the fallback is showing.
521
- // This behavior doesn't need to be observable in the DevTools though.
522
- // It might even result in a bad user experience for e.g. node selection in the Elements panel.
523
- // The easiest fix is to strip out the intermediate Fragment fibers,
524
- // so the Elements panel and Profiler don't need to special case them.
525
- // Suspense components only have a non-null memoizedState if they're timed-out.
526
- const prevDidTimeout = isSuspense && prevFiber.memoizedState !== null;
527
- const nextDidTimeOut = isSuspense && nextFiber.memoizedState !== null;
528
-
529
- // The logic below is inspired by the code paths in updateSuspenseComponent()
530
- // inside ReactFiberBeginWork in the React source code.
531
- if (prevDidTimeout && nextDidTimeOut) {
532
- // Fallback -> Fallback:
533
- // 1. Reconcile fallback set.
534
- const nextFallbackChildSet = nextFiber.child?.sibling ?? null;
535
- // Note: We can't use nextFiber.child.sibling.alternate
536
- // because the set is special and alternate may not exist.
537
- const prevFallbackChildSet = prevFiber.child?.sibling ?? null;
538
-
539
- if (nextFallbackChildSet !== null && prevFallbackChildSet !== null) {
540
- updateFiberRecursively(onRender, nextFallbackChildSet, prevFallbackChildSet);
541
- }
542
- } else if (prevDidTimeout && !nextDidTimeOut) {
543
- // Fallback -> Primary:
544
- // 1. Unmount fallback set
545
- // Note: don't emulate fallback unmount because React actually did it.
546
- // 2. Mount primary set
547
- const nextPrimaryChildSet = nextFiber.child;
548
-
549
- if (nextPrimaryChildSet !== null) {
550
- mountFiberRecursively(onRender, nextPrimaryChildSet, true);
528
+ if (!prevFiber) {
529
+ getFiberId(nextFiber);
530
+ return;
531
+ }
532
+ const pendingUpdates: FiberUpdate[] = [
533
+ { fiber: nextFiber, previousFiber: prevFiber, traverseSiblings: false },
534
+ ];
535
+ let update: FiberUpdate | undefined;
536
+ while ((update = pendingUpdates.pop())) {
537
+ const { fiber, previousFiber, traverseSiblings } = update;
538
+ if (traverseSiblings && fiber.sibling) {
539
+ pendingUpdates.push({
540
+ fiber: fiber.sibling,
541
+ previousFiber: fiber.sibling.alternate,
542
+ traverseSiblings: true,
543
+ });
551
544
  }
552
- } else if (!prevDidTimeout && nextDidTimeOut) {
553
- // Primary -> Fallback:
554
- // 1. Hide primary set
555
- // This is not a real unmount, so it won't get reported by React.
556
- // We need to manually walk the previous tree and record unmounts.
557
- unmountFiberChildrenRecursively(onRender, prevFiber);
558
-
559
- // 2. Mount fallback set
560
- const nextFallbackChildSet = nextFiber.child?.sibling ?? null;
561
-
562
- if (nextFallbackChildSet !== null) {
563
- mountFiberRecursively(onRender, nextFallbackChildSet, true);
545
+ if (!previousFiber) {
546
+ mountFiberTree(onRender, fiber, false);
547
+ continue;
564
548
  }
565
- } else if (nextFiber.child !== prevFiber.child) {
566
- // Common case: Primary -> Primary.
567
- // This is the same code path as for non-Suspense fibers.
568
-
569
- // If the first child is different, we need to traverse them.
570
- // Each next child will be either a new child (mount) or an alternate (update).
571
- let nextChild = nextFiber.child;
572
-
573
- while (nextChild) {
574
- // We already know children will be referentially different because
575
- // they are either new mounts or alternates of previous children.
576
- // Schedule updates and mounts depending on whether alternates exist.
577
- // We don't track deletions here because they are reported separately.
578
- if (nextChild.alternate) {
579
- updateFiberRecursively(onRender, nextChild, nextChild.alternate);
580
- } else {
581
- mountFiberRecursively(onRender, nextChild, false);
549
+ getFiberId(fiber);
550
+ getFiberId(previousFiber);
551
+ if (!shouldFilterFiber(fiber) && didFiberRender(fiber)) onRender(fiber, "update");
552
+ const isSuspense = fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent;
553
+ const wasTimedOut = isSuspense && previousFiber.memoizedState !== null;
554
+ const isTimedOut = isSuspense && fiber.memoizedState !== null;
555
+ if (wasTimedOut && isTimedOut) {
556
+ const nextFallback = fiber.child?.sibling;
557
+ const previousFallback = previousFiber.child?.sibling;
558
+ if (nextFallback) {
559
+ pendingUpdates.push({
560
+ fiber: nextFallback,
561
+ previousFiber: previousFallback ?? null,
562
+ traverseSiblings: false,
563
+ });
582
564
  }
583
-
584
- // Try the next child.
585
- nextChild = nextChild.sibling;
565
+ } else if (wasTimedOut && !isTimedOut) {
566
+ if (fiber.child) mountFiberTree(onRender, fiber.child, true);
567
+ } else if (!wasTimedOut && isTimedOut) {
568
+ unmountFiberChildren(onRender, previousFiber);
569
+ const fallback = fiber.child?.sibling;
570
+ if (fallback) mountFiberTree(onRender, fallback, true);
571
+ } else if (fiber.child && fiber.child !== previousFiber.child) {
572
+ pendingUpdates.push({
573
+ fiber: fiber.child,
574
+ previousFiber: fiber.child.alternate,
575
+ traverseSiblings: true,
576
+ });
586
577
  }
587
578
  }
588
579
  };
@@ -595,30 +586,18 @@ const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
595
586
  }
596
587
  };
597
588
 
598
- const unmountFiberChildrenRecursively = (onRender: RenderHandler, fiber: Fiber): void => {
599
- // We might meet a nested Suspense on our way.
600
- const isTimedOutSuspense =
601
- fiber.tag === getReactWorkTagsForFiber(fiber).SuspenseComponent && fiber.memoizedState !== null;
602
- let child = fiber.child;
603
-
604
- if (isTimedOutSuspense) {
605
- // If it's showing fallback tree, let's traverse it instead.
606
- const primaryChildFragment = fiber.child;
607
- const fallbackChildFragment = primaryChildFragment?.sibling ?? null;
608
-
609
- // Skip over to the real Fiber child.
610
- child = fallbackChildFragment?.child ?? null;
611
- }
612
-
613
- while (child !== null) {
614
- // Record simulated unmounts children-first.
615
- // We skip nodes without return because those are real unmounts.
589
+ const unmountFiberChildren = (onRender: RenderHandler, fiber: Fiber): void => {
590
+ const pendingSiblings: Fiber[] = [];
591
+ let child = getRenderedChild(fiber, false);
592
+ while (child) {
593
+ if (child.sibling) pendingSiblings.push(child.sibling);
616
594
  if (child.return !== null) {
617
595
  unmountFiber(onRender, child);
618
- unmountFiberChildrenRecursively(onRender, child);
596
+ child = getRenderedChild(child, false);
597
+ } else {
598
+ child = null;
619
599
  }
620
-
621
- child = child.sibling;
600
+ child ??= pendingSiblings.pop() ?? null;
622
601
  }
623
602
  };
624
603
 
@@ -650,35 +629,35 @@ const isRootFiberMounted = (fiber: Fiber): boolean => {
650
629
  */
651
630
  export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: RenderHandler): void => {
652
631
  const fiber = "current" in root ? root.current : root;
632
+ const rootKey = "current" in root || !isFiberRoot(root.stateNode) ? root : root.stateNode;
653
633
 
654
- let rootInstance = rootInstanceMap.get(root);
634
+ let rootInstance = rootInstanceMap.get(rootKey);
655
635
 
656
636
  if (!rootInstance) {
657
637
  rootInstance = { prevFiber: null };
658
- rootInstanceMap.set(root, rootInstance);
638
+ rootInstanceMap.set(rootKey, rootInstance);
659
639
  }
660
640
 
661
641
  const { prevFiber } = rootInstance;
642
+ rootInstance.prevFiber = fiber;
662
643
  if (!fiber) {
663
644
  if (prevFiber) {
664
645
  unmountFiber(onRender, prevFiber);
665
646
  }
666
647
  } else if (prevFiber !== null) {
667
- const wasMounted = isRootFiberMounted(prevFiber);
648
+ const wasMounted = isRootFiberMounted(fiber.alternate ?? prevFiber);
668
649
  const isMounted = isRootFiberMounted(fiber);
669
650
 
670
651
  if (!wasMounted && isMounted) {
671
- mountFiberRecursively(onRender, fiber, false);
652
+ mountFiberTree(onRender, fiber, false);
672
653
  } else if (wasMounted && isMounted) {
673
- updateFiberRecursively(onRender, fiber, fiber.alternate);
654
+ updateFiberTree(onRender, fiber, fiber.alternate);
674
655
  } else if (wasMounted && !isMounted) {
675
656
  unmountFiber(onRender, fiber);
676
657
  }
677
658
  } else {
678
- mountFiberRecursively(onRender, fiber, true);
659
+ mountFiberTree(onRender, fiber, true);
679
660
  }
680
-
681
- rootInstance.prevFiber = fiber;
682
661
  };
683
662
 
684
663
  export interface InstrumentationOptions {
@@ -734,10 +713,12 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
734
713
  priority,
735
714
  didError,
736
715
  ) => {
716
+ const isCurrentDispatcher =
717
+ hookDispatchers.get(rdtHook)?.onCommitFiberRoot === dispatchCommitFiberRoot;
737
718
  if (prevOnCommitFiberRoot) {
738
- prevOnCommitFiberRoot.call(rdtHook, rendererID, root, priority, didError);
719
+ callListener(prevOnCommitFiberRoot, rdtHook, rendererID, root, priority, didError);
739
720
  }
740
- if (hookDispatchers.get(rdtHook)?.onCommitFiberRoot !== dispatchCommitFiberRoot) return;
721
+ if (!isCurrentDispatcher) return;
741
722
  setReactWorkTagsForFiber(root.current, rdtHook.renderers.get(rendererID));
742
723
  // Custom renderers and test harnesses commit roots without a memoizedState;
743
724
  // those must stay tracked, so only explicit unmount evidence removes a root.
@@ -750,9 +731,14 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
750
731
  rootRendererIds.set(root, rendererID);
751
732
  rootHooks.set(root, rdtHook);
752
733
  }
753
- for (const { options, target } of instrumentationSubscriptions) {
754
- if (target === hookTargets.get(rdtHook) && options.onCommitFiberRoot) {
755
- options.onCommitFiberRoot(rendererID, root, priority, didError);
734
+ const subscriptionSnapshot = [...instrumentationSubscriptions];
735
+ for (const subscription of subscriptionSnapshot) {
736
+ const { options, target } = subscription;
737
+ if (instrumentationSubscriptions.has(subscription) && target === hookTargets.get(rdtHook)) {
738
+ callListener(
739
+ () => options.onCommitFiberRoot?.(rendererID, root, priority, didError),
740
+ undefined,
741
+ );
756
742
  }
757
743
  }
758
744
  };
@@ -769,21 +755,29 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
769
755
  rendererID,
770
756
  fiber,
771
757
  ) => {
758
+ const isCurrentDispatcher =
759
+ hookDispatchers.get(rdtHook)?.onCommitFiberUnmount === dispatchCommitFiberUnmount;
772
760
  setReactWorkTagsForFiber(fiber, rdtHook.renderers.get(rendererID));
773
- if (prevOnCommitFiberUnmount) {
774
- prevOnCommitFiberUnmount.call(rdtHook, rendererID, fiber);
775
- }
776
- if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
777
- return;
778
- }
761
+ const previousUnmountingFiber = currentUnmountingFiber;
762
+ currentUnmountingFiber = fiber;
779
763
  try {
780
- for (const { options, target } of instrumentationSubscriptions) {
781
- if (target === hookTargets.get(rdtHook) && options.onCommitFiberUnmount) {
782
- options.onCommitFiberUnmount(rendererID, fiber);
764
+ if (prevOnCommitFiberUnmount) {
765
+ callListener(prevOnCommitFiberUnmount, rdtHook, rendererID, fiber);
766
+ }
767
+ if (!isCurrentDispatcher) return;
768
+ const subscriptionSnapshot = [...instrumentationSubscriptions];
769
+ for (const subscription of subscriptionSnapshot) {
770
+ const { options, target } = subscription;
771
+ if (
772
+ instrumentationSubscriptions.has(subscription) &&
773
+ target === hookTargets.get(rdtHook)
774
+ ) {
775
+ callListener(() => options.onCommitFiberUnmount?.(rendererID, fiber), undefined);
783
776
  }
784
777
  }
785
778
  } finally {
786
- releaseFiberId(fiber);
779
+ currentUnmountingFiber = previousUnmountingFiber;
780
+ if (isCurrentDispatcher) releaseFiberId(fiber);
787
781
  }
788
782
  };
789
783
  dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
@@ -799,15 +793,17 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
799
793
  rendererID,
800
794
  root,
801
795
  ) => {
796
+ const isCurrentDispatcher =
797
+ hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot === dispatchPostCommitFiberRoot;
802
798
  if (prevOnPostCommitFiberRoot) {
803
- prevOnPostCommitFiberRoot.call(rdtHook, rendererID, root);
799
+ callListener(prevOnPostCommitFiberRoot, rdtHook, rendererID, root);
804
800
  }
805
- if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
806
- return;
807
- }
808
- for (const { options, target } of instrumentationSubscriptions) {
809
- if (target === hookTargets.get(rdtHook) && options.onPostCommitFiberRoot) {
810
- options.onPostCommitFiberRoot(rendererID, root);
801
+ if (!isCurrentDispatcher) return;
802
+ const subscriptionSnapshot = [...instrumentationSubscriptions];
803
+ for (const subscription of subscriptionSnapshot) {
804
+ const { options, target } = subscription;
805
+ if (instrumentationSubscriptions.has(subscription) && target === hookTargets.get(rdtHook)) {
806
+ callListener(() => options.onPostCommitFiberRoot?.(rendererID, root), undefined);
811
807
  }
812
808
  }
813
809
  };
@@ -825,13 +821,17 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
825
821
  root,
826
822
  children,
827
823
  ) => {
824
+ const isCurrentDispatcher =
825
+ hookDispatchers.get(rdtHook)?.onScheduleFiberRoot === dispatchScheduleFiberRoot;
828
826
  if (prevOnScheduleFiberRoot) {
829
- prevOnScheduleFiberRoot.call(rdtHook, rendererID, root, children);
827
+ callListener(prevOnScheduleFiberRoot, rdtHook, rendererID, root, children);
830
828
  }
831
- if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
832
- for (const { options, target } of instrumentationSubscriptions) {
833
- if (target === hookTargets.get(rdtHook) && options.onScheduleFiberRoot) {
834
- options.onScheduleFiberRoot(rendererID, root, children);
829
+ if (!isCurrentDispatcher) return;
830
+ const subscriptionSnapshot = [...instrumentationSubscriptions];
831
+ for (const subscription of subscriptionSnapshot) {
832
+ const { options, target } = subscription;
833
+ if (instrumentationSubscriptions.has(subscription) && target === hookTargets.get(rdtHook)) {
834
+ callListener(() => options.onScheduleFiberRoot?.(rendererID, root, children), undefined);
835
835
  }
836
836
  }
837
837
  };
@@ -887,15 +887,20 @@ try {
887
887
  */
888
888
  export const instrument = (options: InstrumentationOptions): Unsubscribe => {
889
889
  const target = options.target ?? globalThis;
890
- const rdtHook = getRDTHook(options.onActive, target);
890
+ const activeListener = options.onActive;
891
+ const onActive = activeListener ? () => activeListener() : undefined;
892
+ const existingHook = target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
893
+ if (existingHook) wireHookEventDispatchers(existingHook, target);
894
+ const rdtHook = getRDTHook(undefined, target);
891
895
  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
892
896
 
893
897
  wireHookEventDispatchers(rdtHook, target);
894
898
  const subscription: InstrumentationSubscription = { options, target };
895
899
  instrumentationSubscriptions.add(subscription);
900
+ if (onActive) getRDTHook(onActive, target);
896
901
 
897
902
  return createUnsubscribe(() => {
898
- if (options.onActive) removeActiveListener(options.onActive, target);
903
+ if (onActive) removeActiveListener(onActive, target);
899
904
  instrumentationSubscriptions.delete(subscription);
900
905
  });
901
906
  };
@@ -967,16 +972,17 @@ export const getFiber = <HostInstance>(
967
972
 
968
973
  for (const key of Object.keys(hostInstance)) {
969
974
  if (isFiberPropertyKey(key)) {
970
- knownFiberPropertyKeys.add(key);
971
975
  const fiber = Reflect.get(hostInstance, key);
972
- if (isFiber(fiber)) return fiber;
976
+ if (isFiber(fiber)) {
977
+ knownFiberPropertyKeys.add(key);
978
+ return fiber;
979
+ }
973
980
  }
974
981
  }
975
982
  }
976
983
 
977
984
  if (
978
985
  hostInstance !== null &&
979
- hostInstance !== undefined &&
980
986
  (typeof hostInstance === "object" || typeof hostInstance === "number")
981
987
  ) {
982
988
  const targetFiberRoots = new Set<FiberRoot>();