bippy 0.7.2 → 0.7.3-dev.09bae5a

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
  */
@@ -176,16 +168,20 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
176
168
  // https://github.com/bvaughn/react-devtools-experimental/issues/197
177
169
  return true;
178
170
 
179
- case workTags.Fragment:
171
+ case workTags.HostPortal:
180
172
  case workTags.HostText:
181
173
  case workTags.LegacyHiddenComponent:
182
174
  case workTags.OffscreenComponent:
175
+ case workTags.Throw:
183
176
  return true;
184
177
 
185
178
  case workTags.HostRoot:
186
179
  // It is never valid to filter the root element.
187
180
  return false;
188
181
 
182
+ case workTags.Fragment:
183
+ return fiber.key === null;
184
+
189
185
  default: {
190
186
  const symbolOrNumber =
191
187
  typeof fiber.type === "object" && fiber.type !== null ? fiber.type.$$typeof : fiber.type;
@@ -193,7 +189,8 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
193
189
  if (typeof symbolOrNumber === "symbol") {
194
190
  return (
195
191
  symbolOrNumber.description === ReactSymbols.CONCURRENT_MODE_SYMBOL_DESCRIPTION ||
196
- 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
197
194
  );
198
195
  }
199
196
 
@@ -201,6 +198,8 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
201
198
  case ReactSymbols.CONCURRENT_MODE_NUMBER:
202
199
  case ReactSymbols.CONCURRENT_MODE_SYMBOL_STRING:
203
200
  case ReactSymbols.DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
201
+ case ReactSymbols.STRICT_MODE_NUMBER:
202
+ case ReactSymbols.STRICT_MODE_SYMBOL_STRING:
204
203
  return true;
205
204
 
206
205
  default:
@@ -210,73 +209,64 @@ const shouldFilterFiber = (fiber: Fiber): boolean => {
210
209
  }
211
210
  };
212
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
+
213
230
  /**
214
231
  * Traverses up or down a {@link Fiber}, return `true` to stop and select a node.
215
232
  */
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(
233
+ export const traverseFiber = ((
232
234
  fiber: Fiber | null,
233
235
  selector: FiberSelector,
234
236
  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
- }
248
-
249
- const isPromiseLike = <Result>(value: unknown): value is PromiseLike<Result> =>
250
- (typeof value === "object" || typeof value === "function") &&
251
- value !== null &&
252
- "then" in value &&
253
- typeof value.then === "function";
254
-
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
237
  ): 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
- };
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;
280
270
 
281
271
  /**
282
272
  * Returns `true` if the {@link Fiber} uses React Compiler's memo cache.
@@ -291,7 +281,14 @@ export const hasMemoCache = (fiber: Fiber): boolean => {
291
281
  export const getType = (type: unknown): null | React.ComponentType<unknown> => {
292
282
  if (isComponentType(type)) return type;
293
283
  if (typeof type !== "object" || type === null) return null;
294
- 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;
295
292
  };
296
293
 
297
294
  /**
@@ -312,10 +309,7 @@ export const getDisplayName = (type: unknown): null | string => {
312
309
  */
313
310
  export const detectReactBuildType = (renderer: ReactRenderer): "development" | "production" => {
314
311
  try {
315
- if (
316
- typeof renderer.version === "string" &&
317
- renderer.bundleType === ReactBuildType.Development
318
- ) {
312
+ if (typeof renderer.version === "string" && renderer.bundleType > ReactBuildType.Production) {
319
313
  return "development";
320
314
  }
321
315
  } catch {}
@@ -325,8 +319,8 @@ export const detectReactBuildType = (renderer: ReactRenderer): "development" | "
325
319
  /**
326
320
  * Returns `true` if bippy's instrumentation is active.
327
321
  */
328
- export const isInstrumentationActive = (): boolean => {
329
- const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
322
+ export const isInstrumentationActive = (target: ReactDevToolsTarget = globalThis): boolean => {
323
+ const rdtHook = target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
330
324
  return (
331
325
  Boolean(rdtHook?._instrumentationIsActive) ||
332
326
  isRealReactDevtools(rdtHook) ||
@@ -336,6 +330,8 @@ export const isInstrumentationActive = (): boolean => {
336
330
 
337
331
  export const _fiberRoots = new Set<FiberRoot>();
338
332
  const rootRendererIds = new WeakMap<FiberRoot, number>();
333
+ const rootHooks = new WeakMap<FiberRoot, ReactDevToolsGlobalHook>();
334
+ let currentUnmountingFiber: Fiber | null = null;
339
335
 
340
336
  /**
341
337
  * Returns the latest fiber (since it may be double-buffered).
@@ -343,6 +339,10 @@ const rootRendererIds = new WeakMap<FiberRoot, number>();
343
339
  export const getLatestFiber = (fiber: Fiber): Fiber => {
344
340
  const alternate = fiber.alternate;
345
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;
346
346
 
347
347
  let rootFiber = fiber;
348
348
  while (rootFiber.return) {
@@ -354,6 +354,7 @@ export const getLatestFiber = (fiber: Fiber): Fiber => {
354
354
  });
355
355
  if (latestFiber) return latestFiber;
356
356
  }
357
+ if (currentFiber) return currentFiber;
357
358
 
358
359
  if (alternate.actualStartTime && fiber.actualStartTime) {
359
360
  return alternate.actualStartTime > fiber.actualStartTime ? alternate : fiber;
@@ -370,29 +371,71 @@ export const getLatestFiber = (fiber: Fiber): Fiber => {
370
371
  /**
371
372
  * Returns the renderer that owns the {@link Fiber}.
372
373
  */
373
- export const getRenderer = (fiber: Fiber): ReactRenderer | null => {
374
- if (!hasRDTHook()) return null;
375
-
374
+ export const getRenderer = (
375
+ fiber: Fiber,
376
+ target: ReactDevToolsTarget = globalThis,
377
+ ): ReactRenderer | null => {
376
378
  let rootFiber = fiber;
377
- while (rootFiber.return) {
378
- rootFiber = rootFiber.return;
379
- }
380
-
379
+ while (rootFiber.return) rootFiber = rootFiber.return;
381
380
  const fiberRoot = rootFiber.stateNode;
382
381
  if (!isFiberRoot(fiberRoot)) return null;
383
382
 
384
- const rendererID = rootRendererIds.get(fiberRoot);
385
- if (rendererID === undefined) return null;
386
- return getRDTHook().renderers.get(rendererID) ?? null;
383
+ const trackedRendererId = rootRendererIds.get(fiberRoot);
384
+ const trackedHook = rootHooks.get(fiberRoot);
385
+ if (trackedRendererId !== undefined && trackedHook) {
386
+ return trackedHook.renderers.get(trackedRendererId) ?? null;
387
+ }
388
+
389
+ const rdtHook = target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
390
+ if (!rdtHook?.getFiberRoots) return null;
391
+ for (const [rendererId, renderer] of rdtHook.renderers) {
392
+ if (!rdtHook.getFiberRoots(rendererId).has(fiberRoot)) continue;
393
+ _fiberRoots.add(fiberRoot);
394
+ rootRendererIds.set(fiberRoot, rendererId);
395
+ rootHooks.set(fiberRoot, rdtHook);
396
+ setReactWorkTagsForFiber(fiberRoot.current, renderer);
397
+ return renderer;
398
+ }
399
+ return null;
387
400
  };
388
401
 
389
402
  export type RenderPhase = "mount" | "unmount" | "update";
390
403
 
404
+ interface FiberReference {
405
+ deref: () => Fiber | undefined;
406
+ }
407
+
391
408
  let nextFiberId = 0;
392
409
  const fiberIdMap = new WeakMap<Fiber, number>();
410
+ const fiberByIdMap = new Map<number, FiberReference>();
411
+ const fiberIdFinalizationRegistry =
412
+ typeof FinalizationRegistry === "function"
413
+ ? new FinalizationRegistry<number>((fiberId) => {
414
+ if (!fiberByIdMap.get(fiberId)?.deref()) fiberByIdMap.delete(fiberId);
415
+ })
416
+ : null;
417
+
418
+ const createFiberReference = (fiber: Fiber): FiberReference =>
419
+ typeof WeakRef === "function" ? new WeakRef(fiber) : { deref: () => fiber };
420
+
421
+ const getNextFiberId = (): number => {
422
+ if (!Number.isSafeInteger(nextFiberId)) throw new RangeError("Fiber ID space exhausted");
423
+ return nextFiberId++;
424
+ };
393
425
 
394
- export const setFiberId = (fiber: Fiber, fiberId: number = nextFiberId++): void => {
426
+ export const setFiberId = (fiber: Fiber, fiberId: number = getNextFiberId()): void => {
427
+ const previousFiberId = fiberIdMap.get(fiber);
428
+ if (
429
+ previousFiberId !== undefined &&
430
+ previousFiberId !== fiberId &&
431
+ fiberByIdMap.get(previousFiberId)?.deref() === fiber
432
+ ) {
433
+ fiberByIdMap.delete(previousFiberId);
434
+ }
395
435
  fiberIdMap.set(fiber, fiberId);
436
+ fiberByIdMap.set(fiberId, createFiberReference(fiber));
437
+ if (previousFiberId !== undefined) fiberIdFinalizationRegistry?.unregister(fiber);
438
+ fiberIdFinalizationRegistry?.register(fiber, fiberId, fiber);
396
439
  if (Number.isSafeInteger(fiberId) && fiberId >= nextFiberId) {
397
440
  nextFiberId = fiberId + 1;
398
441
  }
@@ -402,139 +445,135 @@ export const getFiberId = (fiber: Fiber): number => {
402
445
  let currentFiberId = fiberIdMap.get(fiber);
403
446
  if (currentFiberId === undefined && fiber.alternate) {
404
447
  currentFiberId = fiberIdMap.get(fiber.alternate);
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
+ }
405
456
  }
406
457
  if (currentFiberId === undefined) {
407
- currentFiberId = nextFiberId++;
458
+ currentFiberId = getNextFiberId();
408
459
  setFiberId(fiber, currentFiberId);
409
460
  }
410
461
  return currentFiberId;
411
462
  };
412
463
 
413
- const mountFiberRecursively = (
464
+ export const getFiberById = (fiberId: number): Fiber | null => {
465
+ const fiber = fiberByIdMap.get(fiberId)?.deref();
466
+ if (!fiber) {
467
+ fiberByIdMap.delete(fiberId);
468
+ return null;
469
+ }
470
+ return getLatestFiber(fiber);
471
+ };
472
+
473
+ const releaseFiberId = (fiber: Fiber): void => {
474
+ const relatedFibers = fiber.alternate ? [fiber, fiber.alternate] : [fiber];
475
+ const fiberIds = new Set<number>();
476
+
477
+ for (const relatedFiber of relatedFibers) {
478
+ const fiberId = fiberIdMap.get(relatedFiber);
479
+ if (fiberId !== undefined) fiberIds.add(fiberId);
480
+ fiberIdMap.delete(relatedFiber);
481
+ fiberIdFinalizationRegistry?.unregister(relatedFiber);
482
+ }
483
+
484
+ for (const fiberId of fiberIds) {
485
+ const assignedFiber = fiberByIdMap.get(fiberId)?.deref();
486
+ if (!assignedFiber || relatedFibers.includes(assignedFiber)) {
487
+ fiberByIdMap.delete(fiberId);
488
+ }
489
+ }
490
+ };
491
+
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 = (
414
502
  onRender: RenderHandler,
415
503
  firstChild: Fiber,
416
504
  traverseSiblings: boolean,
417
505
  ): void => {
506
+ const pendingSiblings: Fiber[] = [];
418
507
  let fiber: Fiber | null = firstChild;
419
-
420
- while (fiber !== null) {
508
+ while (fiber) {
421
509
  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;
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;
451
514
  }
452
515
  };
453
516
 
454
- const updateFiberRecursively = (
517
+ interface FiberUpdate {
518
+ fiber: Fiber;
519
+ previousFiber: Fiber | null;
520
+ traverseSiblings: boolean;
521
+ }
522
+
523
+ const updateFiberTree = (
455
524
  onRender: RenderHandler,
456
525
  nextFiber: Fiber,
457
526
  prevFiber: Fiber | null,
458
527
  ): 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);
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
+ });
503
544
  }
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);
545
+ if (!previousFiber) {
546
+ mountFiberTree(onRender, fiber, false);
547
+ continue;
516
548
  }
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);
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
+ });
534
564
  }
535
-
536
- // Try the next child.
537
- 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
+ });
538
577
  }
539
578
  }
540
579
  };
@@ -547,30 +586,18 @@ const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
547
586
  }
548
587
  };
549
588
 
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.
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);
568
594
  if (child.return !== null) {
569
595
  unmountFiber(onRender, child);
570
- unmountFiberChildrenRecursively(onRender, child);
596
+ child = getRenderedChild(child, false);
597
+ } else {
598
+ child = null;
571
599
  }
572
-
573
- child = child.sibling;
600
+ child ??= pendingSiblings.pop() ?? null;
574
601
  }
575
602
  };
576
603
 
@@ -602,39 +629,40 @@ const isRootFiberMounted = (fiber: Fiber): boolean => {
602
629
  */
603
630
  export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: RenderHandler): void => {
604
631
  const fiber = "current" in root ? root.current : root;
632
+ const rootKey = "current" in root || !isFiberRoot(root.stateNode) ? root : root.stateNode;
605
633
 
606
- let rootInstance = rootInstanceMap.get(root);
634
+ let rootInstance = rootInstanceMap.get(rootKey);
607
635
 
608
636
  if (!rootInstance) {
609
637
  rootInstance = { prevFiber: null };
610
- rootInstanceMap.set(root, rootInstance);
638
+ rootInstanceMap.set(rootKey, rootInstance);
611
639
  }
612
640
 
613
641
  const { prevFiber } = rootInstance;
642
+ rootInstance.prevFiber = fiber;
614
643
  if (!fiber) {
615
644
  if (prevFiber) {
616
645
  unmountFiber(onRender, prevFiber);
617
646
  }
618
647
  } else if (prevFiber !== null) {
619
- const wasMounted = isRootFiberMounted(prevFiber);
648
+ const wasMounted = isRootFiberMounted(fiber.alternate ?? prevFiber);
620
649
  const isMounted = isRootFiberMounted(fiber);
621
650
 
622
651
  if (!wasMounted && isMounted) {
623
- mountFiberRecursively(onRender, fiber, false);
652
+ mountFiberTree(onRender, fiber, false);
624
653
  } else if (wasMounted && isMounted) {
625
- updateFiberRecursively(onRender, fiber, fiber.alternate);
654
+ updateFiberTree(onRender, fiber, fiber.alternate);
626
655
  } else if (wasMounted && !isMounted) {
627
656
  unmountFiber(onRender, fiber);
628
657
  }
629
658
  } else {
630
- mountFiberRecursively(onRender, fiber, true);
659
+ mountFiberTree(onRender, fiber, true);
631
660
  }
632
-
633
- rootInstance.prevFiber = fiber;
634
661
  };
635
662
 
636
663
  export interface InstrumentationOptions {
637
664
  name?: string;
665
+ target?: ReactDevToolsTarget;
638
666
  onActive?: () => unknown;
639
667
  onCommitFiberRoot?: (
640
668
  rendererID: number,
@@ -649,6 +677,7 @@ export interface InstrumentationOptions {
649
677
 
650
678
  interface InstrumentationSubscription {
651
679
  options: InstrumentationOptions;
680
+ target: ReactDevToolsTarget;
652
681
  }
653
682
 
654
683
  const instrumentationSubscriptions = new Set<InstrumentationSubscription>();
@@ -661,6 +690,7 @@ interface HookDispatchers {
661
690
  }
662
691
 
663
692
  const hookDispatchers = new WeakMap<ReactDevToolsGlobalHook, Partial<HookDispatchers>>();
693
+ const hookTargets = new WeakMap<ReactDevToolsGlobalHook, ReactDevToolsTarget>();
664
694
  let didSubscribeToHookReplacements = false;
665
695
 
666
696
  // each hook event is dispatched from a single re-installable wrapper. If
@@ -683,28 +713,32 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
683
713
  priority,
684
714
  didError,
685
715
  ) => {
716
+ const isCurrentDispatcher =
717
+ hookDispatchers.get(rdtHook)?.onCommitFiberRoot === dispatchCommitFiberRoot;
686
718
  if (prevOnCommitFiberRoot) {
687
- prevOnCommitFiberRoot.call(rdtHook, rendererID, root, priority, didError);
719
+ callListener(prevOnCommitFiberRoot, rdtHook, rendererID, root, priority, didError);
688
720
  }
689
- if (hookDispatchers.get(rdtHook)?.onCommitFiberRoot !== dispatchCommitFiberRoot) return;
721
+ if (!isCurrentDispatcher) return;
690
722
  setReactWorkTagsForFiber(root.current, rdtHook.renderers.get(rendererID));
691
723
  // Custom renderers and test harnesses commit roots without a memoizedState;
692
724
  // 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) {
725
+ if (isFiberRootUnmounted(root)) {
699
726
  _fiberRoots.delete(root);
700
727
  rootRendererIds.delete(root);
728
+ rootHooks.delete(root);
701
729
  } else {
702
730
  _fiberRoots.add(root);
703
731
  rootRendererIds.set(root, rendererID);
732
+ rootHooks.set(root, rdtHook);
704
733
  }
705
- for (const { options } of instrumentationSubscriptions) {
706
- if (options.onCommitFiberRoot) {
707
- 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
+ );
708
742
  }
709
743
  }
710
744
  };
@@ -721,17 +755,29 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
721
755
  rendererID,
722
756
  fiber,
723
757
  ) => {
758
+ const isCurrentDispatcher =
759
+ hookDispatchers.get(rdtHook)?.onCommitFiberUnmount === dispatchCommitFiberUnmount;
724
760
  setReactWorkTagsForFiber(fiber, rdtHook.renderers.get(rendererID));
725
- if (prevOnCommitFiberUnmount) {
726
- prevOnCommitFiberUnmount.call(rdtHook, rendererID, fiber);
727
- }
728
- if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
729
- return;
730
- }
731
- for (const { options } of instrumentationSubscriptions) {
732
- if (options.onCommitFiberUnmount) {
733
- options.onCommitFiberUnmount(rendererID, fiber);
761
+ const previousUnmountingFiber = currentUnmountingFiber;
762
+ currentUnmountingFiber = fiber;
763
+ try {
764
+ if (prevOnCommitFiberUnmount) {
765
+ callListener(prevOnCommitFiberUnmount, rdtHook, rendererID, fiber);
734
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);
776
+ }
777
+ }
778
+ } finally {
779
+ currentUnmountingFiber = previousUnmountingFiber;
780
+ if (isCurrentDispatcher) releaseFiberId(fiber);
735
781
  }
736
782
  };
737
783
  dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
@@ -747,15 +793,17 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
747
793
  rendererID,
748
794
  root,
749
795
  ) => {
796
+ const isCurrentDispatcher =
797
+ hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot === dispatchPostCommitFiberRoot;
750
798
  if (prevOnPostCommitFiberRoot) {
751
- prevOnPostCommitFiberRoot.call(rdtHook, rendererID, root);
799
+ callListener(prevOnPostCommitFiberRoot, rdtHook, rendererID, root);
752
800
  }
753
- if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
754
- return;
755
- }
756
- for (const { options } of instrumentationSubscriptions) {
757
- if (options.onPostCommitFiberRoot) {
758
- 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);
759
807
  }
760
808
  }
761
809
  };
@@ -773,13 +821,17 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
773
821
  root,
774
822
  children,
775
823
  ) => {
824
+ const isCurrentDispatcher =
825
+ hookDispatchers.get(rdtHook)?.onScheduleFiberRoot === dispatchScheduleFiberRoot;
776
826
  if (prevOnScheduleFiberRoot) {
777
- prevOnScheduleFiberRoot.call(rdtHook, rendererID, root, children);
827
+ callListener(prevOnScheduleFiberRoot, rdtHook, rendererID, root, children);
778
828
  }
779
- if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
780
- for (const { options } of instrumentationSubscriptions) {
781
- if (options.onScheduleFiberRoot) {
782
- 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);
783
835
  }
784
836
  }
785
837
  };
@@ -788,11 +840,23 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
788
840
  }
789
841
  };
790
842
 
791
- const wireHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
843
+ const handleHookReplacement = (
844
+ rdtHook: ReactDevToolsGlobalHook,
845
+ target: ReactDevToolsTarget,
846
+ ): void => {
847
+ hookTargets.set(rdtHook, target);
848
+ setHookEventDispatchers(rdtHook);
849
+ };
850
+
851
+ const wireHookEventDispatchers = (
852
+ rdtHook: ReactDevToolsGlobalHook,
853
+ target: ReactDevToolsTarget = globalThis,
854
+ ): void => {
792
855
  if (!didSubscribeToHookReplacements) {
793
- onRDTHookReplace(setHookEventDispatchers);
856
+ onRDTHookReplace(handleHookReplacement);
794
857
  didSubscribeToHookReplacements = true;
795
858
  }
859
+ hookTargets.set(rdtHook, target);
796
860
  setHookEventDispatchers(rdtHook);
797
861
  };
798
862
 
@@ -822,15 +886,21 @@ try {
822
886
  * unsubscribe();
823
887
  */
824
888
  export const instrument = (options: InstrumentationOptions): Unsubscribe => {
825
- const rdtHook = getRDTHook(options.onActive);
889
+ const target = options.target ?? globalThis;
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);
826
895
  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
827
896
 
828
- wireHookEventDispatchers(rdtHook);
829
- const subscription: InstrumentationSubscription = { options };
897
+ wireHookEventDispatchers(rdtHook, target);
898
+ const subscription: InstrumentationSubscription = { options, target };
830
899
  instrumentationSubscriptions.add(subscription);
900
+ if (onActive) getRDTHook(onActive, target);
831
901
 
832
902
  return createUnsubscribe(() => {
833
- if (options.onActive) _onActiveListeners.delete(options.onActive);
903
+ if (onActive) removeActiveListener(onActive, target);
834
904
  instrumentationSubscriptions.delete(subscription);
835
905
  });
836
906
  };
@@ -874,9 +944,12 @@ const getPublicHostInstance = (stateNode: unknown): unknown => {
874
944
  return typeof nativeTag === "number" ? nativeTag : stateNode;
875
945
  };
876
946
 
877
- export const getFiber = <T>(hostInstance: T): Fiber | null => {
878
- const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
879
- if (rdtHook?.renderers) {
947
+ export const getFiber = <HostInstance>(
948
+ hostInstance: HostInstance,
949
+ target: ReactDevToolsTarget = globalThis,
950
+ ): Fiber | null => {
951
+ const rdtHook = target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
952
+ if (rdtHook) {
880
953
  for (const renderer of rdtHook.renderers.values()) {
881
954
  try {
882
955
  const fiber = renderer.findFiberByHostInstance?.(hostInstance);
@@ -899,19 +972,31 @@ export const getFiber = <T>(hostInstance: T): Fiber | null => {
899
972
 
900
973
  for (const key of Object.keys(hostInstance)) {
901
974
  if (isFiberPropertyKey(key)) {
902
- knownFiberPropertyKeys.add(key);
903
975
  const fiber = Reflect.get(hostInstance, key);
904
- if (isFiber(fiber)) return fiber;
976
+ if (isFiber(fiber)) {
977
+ knownFiberPropertyKeys.add(key);
978
+ return fiber;
979
+ }
905
980
  }
906
981
  }
907
982
  }
908
983
 
909
984
  if (
910
985
  hostInstance !== null &&
911
- hostInstance !== undefined &&
912
986
  (typeof hostInstance === "object" || typeof hostInstance === "number")
913
987
  ) {
988
+ const targetFiberRoots = new Set<FiberRoot>();
989
+ if (rdtHook?.getFiberRoots) {
990
+ for (const rendererId of rdtHook.renderers.keys()) {
991
+ for (const fiberRoot of rdtHook.getFiberRoots(rendererId)) {
992
+ targetFiberRoots.add(fiberRoot);
993
+ }
994
+ }
995
+ }
914
996
  for (const fiberRoot of _fiberRoots) {
997
+ if (rootHooks.get(fiberRoot) === rdtHook) targetFiberRoots.add(fiberRoot);
998
+ }
999
+ for (const fiberRoot of targetFiberRoots) {
915
1000
  const fiber = traverseFiber(
916
1001
  fiberRoot.current,
917
1002
  (candidateFiber) =>
@@ -928,16 +1013,16 @@ export const getFiberFromHostInstance = getFiber;
928
1013
 
929
1014
  export {
930
1015
  BIPPY_INSTRUMENTATION_STRING,
931
- _onActiveListeners,
932
1016
  _renderers,
933
1017
  getRDTHook,
934
1018
  hasRDTHook,
935
1019
  installRDTHook,
1020
+ isFiberRootUnmounted,
936
1021
  isReactRefresh,
937
1022
  isRealReactDevtools,
938
1023
  onRendererInject,
939
1024
  patchRDTHook,
940
1025
  version,
941
1026
  } from "./rdt-hook.js";
942
- export type { Unsubscribe } from "./rdt-hook.js";
1027
+ export type { ReactDevToolsTarget, Unsubscribe } from "./rdt-hook.js";
943
1028
  export * from "./react-internals/index.js";