bippy 0.7.2 → 0.7.3

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
@@ -10,19 +10,19 @@ import type {
10
10
  } from "./react-internals/index.js";
11
11
 
12
12
  import {
13
- _onActiveListeners,
14
13
  BIPPY_INSTRUMENTATION_STRING,
15
14
  createUnsubscribe,
16
15
  getRDTHook,
17
16
  hasRDTHook,
17
+ isFiberRootUnmounted,
18
18
  isReactRefresh,
19
19
  isRealReactDevtools,
20
20
  onRDTHookReplace,
21
+ removeActiveListener,
21
22
  } from "./rdt-hook.js";
22
- import type { Unsubscribe } from "./rdt-hook.js";
23
+ import type { ReactDevToolsTarget, Unsubscribe } from "./rdt-hook.js";
23
24
  import {
24
25
  getReactWorkTagsForFiber,
25
- MutationMask,
26
26
  ReactBuildType,
27
27
  ReactFiberFlags,
28
28
  ReactSymbols,
@@ -152,16 +152,6 @@ export const didFiberRender = (fiber: Fiber): boolean => {
152
152
  }
153
153
  };
154
154
 
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
155
  /**
166
156
  * Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
167
157
  */
@@ -312,10 +302,7 @@ export const getDisplayName = (type: unknown): null | string => {
312
302
  */
313
303
  export const detectReactBuildType = (renderer: ReactRenderer): "development" | "production" => {
314
304
  try {
315
- if (
316
- typeof renderer.version === "string" &&
317
- renderer.bundleType === ReactBuildType.Development
318
- ) {
305
+ if (typeof renderer.version === "string" && renderer.bundleType > ReactBuildType.Production) {
319
306
  return "development";
320
307
  }
321
308
  } catch {}
@@ -325,8 +312,8 @@ export const detectReactBuildType = (renderer: ReactRenderer): "development" | "
325
312
  /**
326
313
  * Returns `true` if bippy's instrumentation is active.
327
314
  */
328
- export const isInstrumentationActive = (): boolean => {
329
- const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
315
+ export const isInstrumentationActive = (target: ReactDevToolsTarget = globalThis): boolean => {
316
+ const rdtHook = target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
330
317
  return (
331
318
  Boolean(rdtHook?._instrumentationIsActive) ||
332
319
  isRealReactDevtools(rdtHook) ||
@@ -336,6 +323,7 @@ export const isInstrumentationActive = (): boolean => {
336
323
 
337
324
  export const _fiberRoots = new Set<FiberRoot>();
338
325
  const rootRendererIds = new WeakMap<FiberRoot, number>();
326
+ const rootHooks = new WeakMap<FiberRoot, ReactDevToolsGlobalHook>();
339
327
 
340
328
  /**
341
329
  * Returns the latest fiber (since it may be double-buffered).
@@ -370,29 +358,61 @@ export const getLatestFiber = (fiber: Fiber): Fiber => {
370
358
  /**
371
359
  * Returns the renderer that owns the {@link Fiber}.
372
360
  */
373
- export const getRenderer = (fiber: Fiber): ReactRenderer | null => {
374
- if (!hasRDTHook()) return null;
375
-
361
+ export const getRenderer = (
362
+ fiber: Fiber,
363
+ target: ReactDevToolsTarget = globalThis,
364
+ ): ReactRenderer | null => {
376
365
  let rootFiber = fiber;
377
- while (rootFiber.return) {
378
- rootFiber = rootFiber.return;
379
- }
380
-
366
+ while (rootFiber.return) rootFiber = rootFiber.return;
381
367
  const fiberRoot = rootFiber.stateNode;
382
368
  if (!isFiberRoot(fiberRoot)) return null;
383
369
 
384
- const rendererID = rootRendererIds.get(fiberRoot);
385
- if (rendererID === undefined) return null;
386
- return getRDTHook().renderers.get(rendererID) ?? null;
370
+ const trackedRendererId = rootRendererIds.get(fiberRoot);
371
+ const trackedHook = rootHooks.get(fiberRoot);
372
+ if (trackedRendererId !== undefined && trackedHook) {
373
+ return trackedHook.renderers.get(trackedRendererId) ?? null;
374
+ }
375
+
376
+ const rdtHook = target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
377
+ if (!rdtHook?.getFiberRoots) return null;
378
+ for (const [rendererId, renderer] of rdtHook.renderers) {
379
+ if (!rdtHook.getFiberRoots(rendererId).has(fiberRoot)) continue;
380
+ _fiberRoots.add(fiberRoot);
381
+ rootRendererIds.set(fiberRoot, rendererId);
382
+ rootHooks.set(fiberRoot, rdtHook);
383
+ setReactWorkTagsForFiber(fiberRoot.current, renderer);
384
+ return renderer;
385
+ }
386
+ return null;
387
387
  };
388
388
 
389
389
  export type RenderPhase = "mount" | "unmount" | "update";
390
390
 
391
+ interface FiberReference {
392
+ deref: () => Fiber | undefined;
393
+ }
394
+
391
395
  let nextFiberId = 0;
392
396
  const fiberIdMap = new WeakMap<Fiber, number>();
397
+ const fiberByIdMap = new Map<number, FiberReference>();
398
+ const fiberIdFinalizationRegistry =
399
+ typeof FinalizationRegistry === "function"
400
+ ? new FinalizationRegistry<number>((fiberId) => {
401
+ if (!fiberByIdMap.get(fiberId)?.deref()) fiberByIdMap.delete(fiberId);
402
+ })
403
+ : null;
404
+
405
+ const createFiberReference = (fiber: Fiber): FiberReference =>
406
+ typeof WeakRef === "function" ? new WeakRef(fiber) : { deref: () => fiber };
393
407
 
394
408
  export const setFiberId = (fiber: Fiber, fiberId: number = nextFiberId++): void => {
409
+ const previousFiberId = fiberIdMap.get(fiber);
410
+ if (previousFiberId !== undefined && previousFiberId !== fiberId) {
411
+ fiberByIdMap.delete(previousFiberId);
412
+ }
395
413
  fiberIdMap.set(fiber, fiberId);
414
+ fiberByIdMap.set(fiberId, createFiberReference(fiber));
415
+ fiberIdFinalizationRegistry?.register(fiber, fiberId);
396
416
  if (Number.isSafeInteger(fiberId) && fiberId >= nextFiberId) {
397
417
  nextFiberId = fiberId + 1;
398
418
  }
@@ -402,6 +422,7 @@ export const getFiberId = (fiber: Fiber): number => {
402
422
  let currentFiberId = fiberIdMap.get(fiber);
403
423
  if (currentFiberId === undefined && fiber.alternate) {
404
424
  currentFiberId = fiberIdMap.get(fiber.alternate);
425
+ if (currentFiberId !== undefined) setFiberId(fiber, currentFiberId);
405
426
  }
406
427
  if (currentFiberId === undefined) {
407
428
  currentFiberId = nextFiberId++;
@@ -410,6 +431,33 @@ export const getFiberId = (fiber: Fiber): number => {
410
431
  return currentFiberId;
411
432
  };
412
433
 
434
+ export const getFiberById = (fiberId: number): Fiber | null => {
435
+ const fiber = fiberByIdMap.get(fiberId)?.deref();
436
+ if (!fiber) {
437
+ fiberByIdMap.delete(fiberId);
438
+ return null;
439
+ }
440
+ return getLatestFiber(fiber);
441
+ };
442
+
443
+ const releaseFiberId = (fiber: Fiber): void => {
444
+ const relatedFibers = fiber.alternate ? [fiber, fiber.alternate] : [fiber];
445
+ const fiberIds = new Set<number>();
446
+
447
+ for (const relatedFiber of relatedFibers) {
448
+ const fiberId = fiberIdMap.get(relatedFiber);
449
+ if (fiberId !== undefined) fiberIds.add(fiberId);
450
+ fiberIdMap.delete(relatedFiber);
451
+ }
452
+
453
+ for (const fiberId of fiberIds) {
454
+ const assignedFiber = fiberByIdMap.get(fiberId)?.deref();
455
+ if (!assignedFiber || relatedFibers.includes(assignedFiber)) {
456
+ fiberByIdMap.delete(fiberId);
457
+ }
458
+ }
459
+ };
460
+
413
461
  const mountFiberRecursively = (
414
462
  onRender: RenderHandler,
415
463
  firstChild: Fiber,
@@ -635,6 +683,7 @@ export const traverseRenderedFibers = (root: Fiber | FiberRoot, onRender: Render
635
683
 
636
684
  export interface InstrumentationOptions {
637
685
  name?: string;
686
+ target?: ReactDevToolsTarget;
638
687
  onActive?: () => unknown;
639
688
  onCommitFiberRoot?: (
640
689
  rendererID: number,
@@ -649,6 +698,7 @@ export interface InstrumentationOptions {
649
698
 
650
699
  interface InstrumentationSubscription {
651
700
  options: InstrumentationOptions;
701
+ target: ReactDevToolsTarget;
652
702
  }
653
703
 
654
704
  const instrumentationSubscriptions = new Set<InstrumentationSubscription>();
@@ -661,6 +711,7 @@ interface HookDispatchers {
661
711
  }
662
712
 
663
713
  const hookDispatchers = new WeakMap<ReactDevToolsGlobalHook, Partial<HookDispatchers>>();
714
+ const hookTargets = new WeakMap<ReactDevToolsGlobalHook, ReactDevToolsTarget>();
664
715
  let didSubscribeToHookReplacements = false;
665
716
 
666
717
  // each hook event is dispatched from a single re-installable wrapper. If
@@ -690,20 +741,17 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
690
741
  setReactWorkTagsForFiber(root.current, rdtHook.renderers.get(rendererID));
691
742
  // Custom renderers and test harnesses commit roots without a memoizedState;
692
743
  // 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) {
744
+ if (isFiberRootUnmounted(root)) {
699
745
  _fiberRoots.delete(root);
700
746
  rootRendererIds.delete(root);
747
+ rootHooks.delete(root);
701
748
  } else {
702
749
  _fiberRoots.add(root);
703
750
  rootRendererIds.set(root, rendererID);
751
+ rootHooks.set(root, rdtHook);
704
752
  }
705
- for (const { options } of instrumentationSubscriptions) {
706
- if (options.onCommitFiberRoot) {
753
+ for (const { options, target } of instrumentationSubscriptions) {
754
+ if (target === hookTargets.get(rdtHook) && options.onCommitFiberRoot) {
707
755
  options.onCommitFiberRoot(rendererID, root, priority, didError);
708
756
  }
709
757
  }
@@ -728,10 +776,14 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
728
776
  if (hookDispatchers.get(rdtHook)?.onCommitFiberUnmount !== dispatchCommitFiberUnmount) {
729
777
  return;
730
778
  }
731
- for (const { options } of instrumentationSubscriptions) {
732
- if (options.onCommitFiberUnmount) {
733
- options.onCommitFiberUnmount(rendererID, fiber);
779
+ try {
780
+ for (const { options, target } of instrumentationSubscriptions) {
781
+ if (target === hookTargets.get(rdtHook) && options.onCommitFiberUnmount) {
782
+ options.onCommitFiberUnmount(rendererID, fiber);
783
+ }
734
784
  }
785
+ } finally {
786
+ releaseFiberId(fiber);
735
787
  }
736
788
  };
737
789
  dispatchers.onCommitFiberUnmount = dispatchCommitFiberUnmount;
@@ -753,8 +805,8 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
753
805
  if (hookDispatchers.get(rdtHook)?.onPostCommitFiberRoot !== dispatchPostCommitFiberRoot) {
754
806
  return;
755
807
  }
756
- for (const { options } of instrumentationSubscriptions) {
757
- if (options.onPostCommitFiberRoot) {
808
+ for (const { options, target } of instrumentationSubscriptions) {
809
+ if (target === hookTargets.get(rdtHook) && options.onPostCommitFiberRoot) {
758
810
  options.onPostCommitFiberRoot(rendererID, root);
759
811
  }
760
812
  }
@@ -777,8 +829,8 @@ const setHookEventDispatchers = (rdtHook: ReactDevToolsGlobalHook): void => {
777
829
  prevOnScheduleFiberRoot.call(rdtHook, rendererID, root, children);
778
830
  }
779
831
  if (hookDispatchers.get(rdtHook)?.onScheduleFiberRoot !== dispatchScheduleFiberRoot) return;
780
- for (const { options } of instrumentationSubscriptions) {
781
- if (options.onScheduleFiberRoot) {
832
+ for (const { options, target } of instrumentationSubscriptions) {
833
+ if (target === hookTargets.get(rdtHook) && options.onScheduleFiberRoot) {
782
834
  options.onScheduleFiberRoot(rendererID, root, children);
783
835
  }
784
836
  }
@@ -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,16 @@ 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 rdtHook = getRDTHook(options.onActive, target);
826
891
  rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
827
892
 
828
- wireHookEventDispatchers(rdtHook);
829
- const subscription: InstrumentationSubscription = { options };
893
+ wireHookEventDispatchers(rdtHook, target);
894
+ const subscription: InstrumentationSubscription = { options, target };
830
895
  instrumentationSubscriptions.add(subscription);
831
896
 
832
897
  return createUnsubscribe(() => {
833
- if (options.onActive) _onActiveListeners.delete(options.onActive);
898
+ if (options.onActive) removeActiveListener(options.onActive, target);
834
899
  instrumentationSubscriptions.delete(subscription);
835
900
  });
836
901
  };
@@ -874,9 +939,12 @@ const getPublicHostInstance = (stateNode: unknown): unknown => {
874
939
  return typeof nativeTag === "number" ? nativeTag : stateNode;
875
940
  };
876
941
 
877
- export const getFiber = <T>(hostInstance: T): Fiber | null => {
878
- const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
879
- if (rdtHook?.renderers) {
942
+ export const getFiber = <HostInstance>(
943
+ hostInstance: HostInstance,
944
+ target: ReactDevToolsTarget = globalThis,
945
+ ): Fiber | null => {
946
+ const rdtHook = target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
947
+ if (rdtHook) {
880
948
  for (const renderer of rdtHook.renderers.values()) {
881
949
  try {
882
950
  const fiber = renderer.findFiberByHostInstance?.(hostInstance);
@@ -911,7 +979,18 @@ export const getFiber = <T>(hostInstance: T): Fiber | null => {
911
979
  hostInstance !== undefined &&
912
980
  (typeof hostInstance === "object" || typeof hostInstance === "number")
913
981
  ) {
982
+ const targetFiberRoots = new Set<FiberRoot>();
983
+ if (rdtHook?.getFiberRoots) {
984
+ for (const rendererId of rdtHook.renderers.keys()) {
985
+ for (const fiberRoot of rdtHook.getFiberRoots(rendererId)) {
986
+ targetFiberRoots.add(fiberRoot);
987
+ }
988
+ }
989
+ }
914
990
  for (const fiberRoot of _fiberRoots) {
991
+ if (rootHooks.get(fiberRoot) === rdtHook) targetFiberRoots.add(fiberRoot);
992
+ }
993
+ for (const fiberRoot of targetFiberRoots) {
915
994
  const fiber = traverseFiber(
916
995
  fiberRoot.current,
917
996
  (candidateFiber) =>
@@ -928,16 +1007,16 @@ export const getFiberFromHostInstance = getFiber;
928
1007
 
929
1008
  export {
930
1009
  BIPPY_INSTRUMENTATION_STRING,
931
- _onActiveListeners,
932
1010
  _renderers,
933
1011
  getRDTHook,
934
1012
  hasRDTHook,
935
1013
  installRDTHook,
1014
+ isFiberRootUnmounted,
936
1015
  isReactRefresh,
937
1016
  isRealReactDevtools,
938
1017
  onRendererInject,
939
1018
  patchRDTHook,
940
1019
  version,
941
1020
  } from "./rdt-hook.js";
942
- export type { Unsubscribe } from "./rdt-hook.js";
1021
+ export type { ReactDevToolsTarget, Unsubscribe } from "./rdt-hook.js";
943
1022
  export * from "./react-internals/index.js";