bippy 0.7.2 → 0.7.3-dev.04de4f7

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/rdt-hook.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // This module must load before React so renderers can inject into the hook.
2
2
 
3
- import type { ReactDevToolsGlobalHook, ReactRenderer } from "./react-internals/index.js";
3
+ import { callListener } from "./call-listener.js";
4
+ import type { FiberRoot, ReactDevToolsGlobalHook, ReactRenderer } from "./react-internals/index.js";
4
5
 
5
6
  export interface Unsubscribe extends Disposable {
6
7
  (): void;
@@ -14,8 +15,21 @@ interface RendererInjectListener {
14
15
  (renderer: ReactRenderer): void;
15
16
  }
16
17
 
18
+ interface RendererInjectSubscription {
19
+ listener: RendererInjectListener;
20
+ target: ReactDevToolsTarget;
21
+ }
22
+
17
23
  interface RDTHookReplaceListener {
18
- (rdtHook: ReactDevToolsGlobalHook): void;
24
+ (rdtHook: ReactDevToolsGlobalHook, target: ReactDevToolsTarget): void;
25
+ }
26
+
27
+ interface RDTHookReplaceSubscription {
28
+ listener: RDTHookReplaceListener;
29
+ }
30
+
31
+ export interface ReactDevToolsTarget {
32
+ __REACT_DEVTOOLS_GLOBAL_HOOK__?: ReactDevToolsGlobalHook;
19
33
  }
20
34
 
21
35
  export const version = process.env.VERSION;
@@ -25,10 +39,23 @@ const objectDefineProperty = Object.defineProperty;
25
39
 
26
40
  const noOp = (): void => {};
27
41
 
42
+ const getTargetHook = (target: ReactDevToolsTarget): ReactDevToolsGlobalHook | undefined =>
43
+ target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
44
+
28
45
  export const createUnsubscribe = (unsubscribe: () => void): Unsubscribe =>
29
46
  Object.assign(unsubscribe, { [Symbol.dispose]: unsubscribe });
30
47
 
48
+ export const isFiberRootUnmounted = (fiberRoot: FiberRoot): boolean => {
49
+ const rootState = fiberRoot.current.memoizedState;
50
+ return (
51
+ rootState === null ||
52
+ (rootState !== undefined && (rootState.element === null || rootState.element === undefined))
53
+ );
54
+ };
55
+
31
56
  const checkDCE = (functionToCheck: unknown): void => {
57
+ // HACK: Node loads React's unbundled entrypoints, where development guards are intentionally not eliminated.
58
+ if (typeof process !== "undefined" && process.versions?.node) return;
32
59
  try {
33
60
  const code = Function.prototype.toString.call(functionToCheck);
34
61
  if (code.indexOf("^_^") > -1) {
@@ -47,7 +74,7 @@ const checkDCE = (functionToCheck: unknown): void => {
47
74
  export const isRealReactDevtools = (
48
75
  rdtHook: ReactDevToolsGlobalHook | undefined | null = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__,
49
76
  ): boolean => {
50
- return Boolean(rdtHook && "getFiberRoots" in rdtHook);
77
+ return Boolean(rdtHook && "getFiberRoots" in rdtHook && rdtHook._isBippyHook !== true);
51
78
  };
52
79
 
53
80
  // HACK: react-refresh's inject wrapper is the only stock hook whose parameter is named
@@ -69,65 +96,90 @@ export const _onActiveListeners = new Set<ActiveListener>();
69
96
 
70
97
  export const _renderers = new Set<ReactRenderer>();
71
98
 
72
- const rendererInjectListeners = new Set<RendererInjectListener>();
73
- const rdtHookReplaceListeners = new Set<RDTHookReplaceListener>();
74
- // HACK: Hook replacement can leave an old inject wrapper in the call chain, so notifications must be deduplicated.
75
- const notifiedRenderers = new WeakSet<ReactRenderer>();
76
- const notifyingInjectByHook = new WeakMap<
77
- ReactDevToolsGlobalHook,
78
- ReactDevToolsGlobalHook["inject"]
79
- >();
80
-
81
- const notifyActiveListeners = (): void => {
82
- for (const listener of _onActiveListeners) {
83
- listener();
99
+ const activeListenerTargets = new WeakMap<ActiveListener, Set<ReactDevToolsTarget>>();
100
+ const rendererInjectSubscriptions = new Set<RendererInjectSubscription>();
101
+ const rdtHookReplaceSubscriptions = new Set<RDTHookReplaceSubscription>();
102
+ const notifiedRenderersByTarget = new WeakMap<ReactDevToolsTarget, WeakSet<ReactRenderer>>();
103
+
104
+ const addActiveListener = (listener: ActiveListener, target: ReactDevToolsTarget): void => {
105
+ _onActiveListeners.add(listener);
106
+ const targets = activeListenerTargets.get(listener) ?? new Set<ReactDevToolsTarget>();
107
+ targets.add(target);
108
+ activeListenerTargets.set(listener, targets);
109
+ };
110
+
111
+ export const removeActiveListener = (
112
+ listener: ActiveListener,
113
+ target: ReactDevToolsTarget,
114
+ ): void => {
115
+ const targets = activeListenerTargets.get(listener);
116
+ targets?.delete(target);
117
+ if (!targets || targets.size === 0) {
118
+ activeListenerTargets.delete(listener);
119
+ _onActiveListeners.delete(listener);
84
120
  }
85
121
  };
86
122
 
87
- const notifyRendererInjectListeners = (renderer: ReactRenderer): void => {
88
- for (const listener of rendererInjectListeners) {
89
- listener(renderer);
123
+ const notifyActiveListeners = (
124
+ target: ReactDevToolsTarget,
125
+ pendingListeners = [..._onActiveListeners],
126
+ ): void => {
127
+ for (const listener of pendingListeners) {
128
+ if (activeListenerTargets.get(listener)?.has(target)) callListener(listener, undefined);
90
129
  }
91
130
  };
92
131
 
93
- const notifyRDTHookReplaceListeners = (rdtHook: ReactDevToolsGlobalHook): void => {
94
- for (const listener of rdtHookReplaceListeners) {
95
- listener(rdtHook);
132
+ const notifyRendererInjectListeners = (
133
+ target: ReactDevToolsTarget,
134
+ renderer: ReactRenderer,
135
+ ): void => {
136
+ const subscriptionSnapshot = [...rendererInjectSubscriptions];
137
+ for (const subscription of subscriptionSnapshot) {
138
+ if (rendererInjectSubscriptions.has(subscription) && subscription.target === target) {
139
+ callListener(subscription.listener, subscription, renderer);
140
+ }
96
141
  }
97
142
  };
98
143
 
99
- const setRendererInjectDispatcher = (rdtHook: ReactDevToolsGlobalHook): void => {
100
- if (rdtHook.inject === notifyingInjectByHook.get(rdtHook)) return;
101
- const previousInject = rdtHook.inject;
102
- const nextInject = (renderer: ReactRenderer) => {
103
- const rendererId = previousInject.call(rdtHook, renderer);
104
- if (!notifiedRenderers.has(renderer)) {
105
- notifiedRenderers.add(renderer);
106
- notifyRendererInjectListeners(renderer);
144
+ const notifyRDTHookReplaceListeners = (
145
+ rdtHook: ReactDevToolsGlobalHook,
146
+ target: ReactDevToolsTarget,
147
+ ): void => {
148
+ const subscriptionSnapshot = [...rdtHookReplaceSubscriptions];
149
+ for (const subscription of subscriptionSnapshot) {
150
+ if (rdtHookReplaceSubscriptions.has(subscription)) {
151
+ callListener(subscription.listener, undefined, rdtHook, target);
107
152
  }
108
- return rendererId;
109
- };
110
- rdtHook.inject = nextInject;
111
- notifyingInjectByHook.set(rdtHook, nextInject);
153
+ }
112
154
  };
113
155
 
114
- export const onRendererInject = (listener: RendererInjectListener): Unsubscribe => {
115
- setRendererInjectDispatcher(getRDTHook());
116
- rendererInjectListeners.add(listener);
156
+ export const onRendererInject = (
157
+ listener: RendererInjectListener,
158
+ target: ReactDevToolsTarget = globalThis,
159
+ ): Unsubscribe => {
160
+ getRDTHook(undefined, target);
161
+ const subscription = { listener, target };
162
+ rendererInjectSubscriptions.add(subscription);
117
163
  return createUnsubscribe(() => {
118
- rendererInjectListeners.delete(listener);
164
+ rendererInjectSubscriptions.delete(subscription);
119
165
  });
120
166
  };
121
167
 
122
168
  export const onRDTHookReplace = (listener: RDTHookReplaceListener): Unsubscribe => {
123
- rdtHookReplaceListeners.add(listener);
169
+ const subscription = { listener };
170
+ rdtHookReplaceSubscriptions.add(subscription);
124
171
  return createUnsubscribe(() => {
125
- rdtHookReplaceListeners.delete(listener);
172
+ rdtHookReplaceSubscriptions.delete(subscription);
126
173
  });
127
174
  };
128
175
 
176
+ // A Map built in another realm (an iframe target) fails instanceof, so replacing it
177
+ // would discard that realm's renderer registry. Object.prototype.toString is realm-agnostic.
178
+ export const isRendererMap = (value: unknown): value is Map<number, ReactRenderer> =>
179
+ Object.prototype.toString.call(value) === "[object Map]";
180
+
129
181
  const getRendererMap = (rdtHook: ReactDevToolsGlobalHook): Map<number, ReactRenderer> => {
130
- if (!(rdtHook.renderers instanceof Map)) {
182
+ if (!isRendererMap(rdtHook.renderers)) {
131
183
  rdtHook.renderers = new Map();
132
184
  }
133
185
  return rdtHook.renderers;
@@ -135,6 +187,7 @@ const getRendererMap = (rdtHook: ReactDevToolsGlobalHook): Map<number, ReactRend
135
187
 
136
188
  const trackInjectedRenderer = (
137
189
  rdtHook: ReactDevToolsGlobalHook,
190
+ target: ReactDevToolsTarget,
138
191
  renderers: Map<number, ReactRenderer>,
139
192
  rendererId: number,
140
193
  renderer: ReactRenderer,
@@ -143,28 +196,47 @@ const trackInjectedRenderer = (
143
196
  _renderers.add(renderer);
144
197
  if (!rdtHook._instrumentationIsActive) {
145
198
  rdtHook._instrumentationIsActive = true;
146
- notifyActiveListeners();
199
+ notifyActiveListeners(target);
147
200
  }
201
+ const notifiedRenderers = notifiedRenderersByTarget.get(target) ?? new WeakSet<ReactRenderer>();
202
+ notifiedRenderersByTarget.set(target, notifiedRenderers);
203
+ if (notifiedRenderers.has(renderer)) return;
204
+ notifiedRenderers.add(renderer);
205
+ notifyRendererInjectListeners(target, renderer);
148
206
  };
149
207
 
150
- export const installRDTHook = (onActive?: ActiveListener): ReactDevToolsGlobalHook => {
151
- if (onActive) {
152
- _onActiveListeners.add(onActive);
153
- }
208
+ export const installRDTHook = (
209
+ onActive?: ActiveListener,
210
+ target: ReactDevToolsTarget = globalThis,
211
+ ): ReactDevToolsGlobalHook => {
212
+ if (onActive) addActiveListener(onActive, target);
154
213
  const renderers = new Map<number, ReactRenderer>();
214
+ const fiberRoots = new Map<number, Set<FiberRoot>>();
215
+ // Callers register roots by mutating the returned set, so it must be memoized.
216
+ const getRendererFiberRoots = (rendererId: number): Set<FiberRoot> => {
217
+ const rendererRoots = fiberRoots.get(rendererId) ?? new Set<FiberRoot>();
218
+ fiberRoots.set(rendererId, rendererRoots);
219
+ return rendererRoots;
220
+ };
155
221
  let rendererIdCounter = 0;
156
222
  let rdtHook: ReactDevToolsGlobalHook = {
157
223
  _instrumentationIsActive: false,
158
224
  _instrumentationSource: BIPPY_INSTRUMENTATION_STRING,
225
+ _isBippyHook: true,
159
226
  checkDCE,
227
+ getFiberRoots: getRendererFiberRoots,
160
228
  hasUnsupportedRendererAttached: false,
161
229
  inject: (renderer) => {
162
230
  const nextRendererId = ++rendererIdCounter;
163
- trackInjectedRenderer(rdtHook, renderers, nextRendererId, renderer);
231
+ trackInjectedRenderer(rdtHook, target, renderers, nextRendererId, renderer);
164
232
  return nextRendererId;
165
233
  },
166
234
  on: noOp,
167
- onCommitFiberRoot: noOp,
235
+ onCommitFiberRoot: (rendererId, fiberRoot) => {
236
+ const rendererRoots = getRendererFiberRoots(rendererId);
237
+ if (isFiberRootUnmounted(fiberRoot)) rendererRoots.delete(fiberRoot);
238
+ else rendererRoots.add(fiberRoot);
239
+ },
168
240
  onCommitFiberUnmount: noOp,
169
241
  onPostCommitFiberRoot: noOp,
170
242
  renderers,
@@ -172,13 +244,11 @@ export const installRDTHook = (onActive?: ActiveListener): ReactDevToolsGlobalHo
172
244
  supportsFlight: true,
173
245
  };
174
246
  try {
175
- objectDefineProperty(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__", {
247
+ objectDefineProperty(target, "__REACT_DEVTOOLS_GLOBAL_HOOK__", {
176
248
  configurable: true,
177
249
  enumerable: true,
178
- get() {
179
- return rdtHook;
180
- },
181
- set(newHook) {
250
+ get: () => rdtHook,
251
+ set: (newHook: ReactDevToolsGlobalHook | undefined) => {
182
252
  if (newHook && typeof newHook === "object") {
183
253
  const ourRenderers = rdtHook.renderers;
184
254
  rdtHook = newHook;
@@ -187,14 +257,15 @@ export const installRDTHook = (onActive?: ActiveListener): ReactDevToolsGlobalHo
187
257
  _renderers.add(renderer);
188
258
  nextRenderers.set(rendererId, renderer);
189
259
  });
190
- if (ourRenderers.size > 0 || rdtHookReplaceListeners.size > 0) {
191
- patchRDTHook(onActive);
260
+ if (ourRenderers.size > 0 || rdtHookReplaceSubscriptions.size > 0) {
261
+ patchRDTHook(undefined, target, () => notifyRDTHookReplaceListeners(newHook, target));
262
+ } else {
263
+ notifyRDTHookReplaceListeners(newHook, target);
192
264
  }
193
- notifyRDTHookReplaceListeners(rdtHook);
194
265
  }
195
266
  },
196
267
  });
197
- if (typeof window !== "undefined") {
268
+ if (target === globalThis && typeof window !== "undefined") {
198
269
  // HACK: The DevTools extension uses hasOwnProperty to decide whether it may replace an earlier hook.
199
270
  const originalWindowHasOwnProperty = window.hasOwnProperty.bind(window);
200
271
  const originalHasOwnPropertyDescriptor = Object.getOwnPropertyDescriptor(
@@ -224,17 +295,19 @@ export const installRDTHook = (onActive?: ActiveListener): ReactDevToolsGlobalHo
224
295
  });
225
296
  }
226
297
  } catch {
227
- patchRDTHook(onActive);
298
+ patchRDTHook(onActive, target);
228
299
  }
229
300
  return rdtHook;
230
301
  };
231
302
 
232
- export const patchRDTHook = (onActive?: ActiveListener): void => {
233
- if (onActive) {
234
- _onActiveListeners.add(onActive);
235
- }
236
- let didNotifyActiveListeners = false;
237
- const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
303
+ export const patchRDTHook = (
304
+ onActive?: ActiveListener,
305
+ target: ReactDevToolsTarget = globalThis,
306
+ onReady?: () => void,
307
+ ): void => {
308
+ if (onActive) addActiveListener(onActive, target);
309
+ let shouldNotifyActiveListeners = false;
310
+ const rdtHook = getTargetHook(target);
238
311
  if (!rdtHook) return;
239
312
  const renderers = getRendererMap(rdtHook);
240
313
  if (!rdtHook._instrumentationSource) {
@@ -249,43 +322,42 @@ export const patchRDTHook = (onActive?: ActiveListener): void => {
249
322
  if (!isReactDevtools) {
250
323
  rdtHook.on = noOp;
251
324
  }
252
- if (renderers.size) {
253
- renderers.forEach((renderer) => _renderers.add(renderer));
254
- rdtHook._instrumentationIsActive = true;
255
- notifyActiveListeners();
256
- didNotifyActiveListeners = true;
257
- } else if (!isReactDevtools && isReactRefresh(rdtHook)) {
258
- // HACK: react-refresh's stub inject never records renderers, so a React app
259
- // that injected before bippy loaded is undetectable through the renderers map.
260
- // A react-refresh hook implies a dev renderer, so activate immediately.
261
- rdtHook._instrumentationIsActive = true;
262
- notifyActiveListeners();
263
- didNotifyActiveListeners = true;
264
- }
325
+ const shouldActivate = renderers.size > 0 || (!isReactDevtools && isReactRefresh(rdtHook));
265
326
  const previousInject = rdtHook.inject;
266
327
  rdtHook.inject = (renderer) => {
267
328
  const rendererId = previousInject.call(rdtHook, renderer);
268
- trackInjectedRenderer(rdtHook, renderers, rendererId, renderer);
329
+ trackInjectedRenderer(rdtHook, target, renderers, rendererId, renderer);
269
330
  return rendererId;
270
331
  };
332
+ if (shouldActivate) {
333
+ renderers.forEach((renderer) => _renderers.add(renderer));
334
+ rdtHook._instrumentationIsActive = true;
335
+ shouldNotifyActiveListeners = true;
336
+ }
271
337
  }
272
- if (!didNotifyActiveListeners && (renderers.size || rdtHook._instrumentationIsActive)) {
273
- onActive?.();
338
+ const pendingListeners = shouldNotifyActiveListeners ? [..._onActiveListeners] : [];
339
+ onReady?.();
340
+ if (shouldNotifyActiveListeners) {
341
+ notifyActiveListeners(target, pendingListeners);
342
+ } else if (onActive && (renderers.size || rdtHook._instrumentationIsActive)) {
343
+ callListener(onActive, undefined);
274
344
  }
275
345
  };
276
346
 
277
- export const hasRDTHook = (): boolean => {
278
- return Object.hasOwn(globalThis, "__REACT_DEVTOOLS_GLOBAL_HOOK__");
279
- };
347
+ export const hasRDTHook = (target: ReactDevToolsTarget = globalThis): boolean =>
348
+ Object.hasOwn(target, "__REACT_DEVTOOLS_GLOBAL_HOOK__");
280
349
 
281
350
  /**
282
351
  * Returns the current React DevTools global hook.
283
352
  */
284
- export const getRDTHook = (onActive?: ActiveListener): ReactDevToolsGlobalHook => {
285
- if (!hasRDTHook()) {
286
- return installRDTHook(onActive);
353
+ export const getRDTHook = (
354
+ onActive?: ActiveListener,
355
+ target: ReactDevToolsTarget = globalThis,
356
+ ): ReactDevToolsGlobalHook => {
357
+ if (!hasRDTHook(target)) {
358
+ return installRDTHook(onActive, target);
287
359
  }
288
360
 
289
- patchRDTHook(onActive);
290
- return globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ ?? installRDTHook(onActive);
361
+ patchRDTHook(onActive, target);
362
+ return getTargetHook(target) ?? installRDTHook(onActive, target);
291
363
  };
@@ -0,0 +1,84 @@
1
+ // Adapted from ReactFiberTreeReflection.js. Copyright (c) Meta Platforms, Inc. and affiliates. MIT licensed.
2
+ import type { Fiber } from "./types.js";
3
+ import { getReactWorkTagsForFiber } from "./index.js";
4
+
5
+ export const getCurrentFiberFromRoot = (fiber: Fiber): Fiber | null => {
6
+ const alternate = fiber.alternate;
7
+ if (!alternate) return null;
8
+ let firstBranch = fiber;
9
+ let secondBranch = alternate;
10
+
11
+ while (firstBranch.return) {
12
+ const firstParent = firstBranch.return;
13
+ const secondParent = firstParent.alternate;
14
+ if (!secondParent) {
15
+ const nextParent = firstParent.return;
16
+ if (!nextParent) return null;
17
+ firstBranch = secondBranch = nextParent;
18
+ continue;
19
+ }
20
+
21
+ if (firstParent.child === secondParent.child) {
22
+ let child = firstParent.child;
23
+ while (child) {
24
+ if (child === firstBranch || child === secondBranch) {
25
+ let root = firstParent;
26
+ while (root.return) root = root.return;
27
+ if (root.tag !== getReactWorkTagsForFiber(root).HostRoot) return null;
28
+ return child === firstBranch ? fiber : alternate;
29
+ }
30
+ child = child.sibling;
31
+ }
32
+ return null;
33
+ }
34
+
35
+ if (firstBranch.return !== secondBranch.return) {
36
+ firstBranch = firstParent;
37
+ secondBranch = secondParent;
38
+ } else {
39
+ let didFindChild = false;
40
+ let child = firstParent.child;
41
+ while (child) {
42
+ if (child === firstBranch) {
43
+ firstBranch = firstParent;
44
+ secondBranch = secondParent;
45
+ didFindChild = true;
46
+ break;
47
+ }
48
+ if (child === secondBranch) {
49
+ firstBranch = secondParent;
50
+ secondBranch = firstParent;
51
+ didFindChild = true;
52
+ break;
53
+ }
54
+ child = child.sibling;
55
+ }
56
+ if (!didFindChild) {
57
+ child = secondParent.child;
58
+ while (child) {
59
+ if (child === firstBranch) {
60
+ firstBranch = secondParent;
61
+ secondBranch = firstParent;
62
+ didFindChild = true;
63
+ break;
64
+ }
65
+ if (child === secondBranch) {
66
+ firstBranch = firstParent;
67
+ secondBranch = secondParent;
68
+ didFindChild = true;
69
+ break;
70
+ }
71
+ child = child.sibling;
72
+ }
73
+ if (!didFindChild) return null;
74
+ }
75
+ }
76
+ if (firstBranch.alternate !== secondBranch) return null;
77
+ }
78
+
79
+ if (firstBranch.tag !== getReactWorkTagsForFiber(firstBranch).HostRoot) return null;
80
+ const root = firstBranch.stateNode;
81
+ if (typeof root !== "object" || root === null || !("current" in root)) return null;
82
+ if (root.current === firstBranch) return fiber;
83
+ return root.current === secondBranch ? alternate : null;
84
+ };
@@ -25,6 +25,9 @@ export const ReactSymbols = {
25
25
  DEPRECATED_ASYNC_MODE_SYMBOL_STRING: "Symbol(react.async_mode)",
26
26
  ELEMENT_SYMBOL_STRING: "Symbol(react.transitional.element)",
27
27
  LEGACY_ELEMENT_SYMBOL_STRING: "Symbol(react.element)",
28
+ STRICT_MODE_NUMBER: 60108,
29
+ STRICT_MODE_SYMBOL_DESCRIPTION: "react.strict_mode",
30
+ STRICT_MODE_SYMBOL_STRING: "Symbol(react.strict_mode)",
28
31
  } as const;
29
32
 
30
33
  export interface ReactWorkTagMap {
@@ -18,8 +18,24 @@ export type {
18
18
  } from "./generated/react-work-tags.js";
19
19
  export * from "./types.js";
20
20
 
21
+ interface InheritedWorkTags {
22
+ workTags: Readonly<ReactWorkTagMap>;
23
+ generation: number;
24
+ }
25
+
21
26
  const defaultReactWorkTags = getReactWorkTags();
22
27
  const fiberReactWorkTags = new WeakMap<Fiber, Readonly<ReactWorkTagMap>>();
28
+ const inheritedFiberWorkTags = new WeakMap<Fiber, InheritedWorkTags>();
29
+ let workTagGeneration = 0;
30
+
31
+ const getCachedWorkTags = (fiber: Fiber): Readonly<ReactWorkTagMap> | undefined => {
32
+ const assignedWorkTags = fiberReactWorkTags.get(fiber);
33
+ if (assignedWorkTags) return assignedWorkTags;
34
+ const inheritedWorkTags = inheritedFiberWorkTags.get(fiber);
35
+ return inheritedWorkTags?.generation === workTagGeneration
36
+ ? inheritedWorkTags.workTags
37
+ : undefined;
38
+ };
23
39
 
24
40
  // React's experimental channel historically reported "0.0.0-experimental-<sha>"
25
41
  // as the runtime version; those builds use modern work tags, not the 16.x rows
@@ -39,25 +55,36 @@ export const getReactWorkTagsForRenderer = (
39
55
 
40
56
  export const setReactWorkTagsForFiber = (fiber: Fiber, renderer?: ReactRenderer): void => {
41
57
  const workTags = getReactWorkTagsForRenderer(renderer);
58
+ if (
59
+ getCachedWorkTags(fiber) !== workTags ||
60
+ (fiber.alternate && getCachedWorkTags(fiber.alternate) !== workTags)
61
+ ) {
62
+ workTagGeneration++;
63
+ }
42
64
  fiberReactWorkTags.set(fiber, workTags);
43
65
  if (fiber.alternate) fiberReactWorkTags.set(fiber.alternate, workTags);
44
66
  };
45
67
 
46
68
  export const getReactWorkTagsForFiber = (fiber: Fiber): Readonly<ReactWorkTagMap> => {
47
- const cachedWorkTags = fiberReactWorkTags.get(fiber);
48
- if (cachedWorkTags) return cachedWorkTags;
49
-
50
- const traversedFibers: Fiber[] = [fiber];
51
- let rootFiber = fiber;
52
- while (rootFiber.return) {
53
- rootFiber = rootFiber.return;
54
- traversedFibers.push(rootFiber);
69
+ let workTags = getCachedWorkTags(fiber);
70
+ if (workTags) return workTags;
71
+ const traversedFibers: Fiber[] = [];
72
+ let ancestor = fiber;
73
+ while (!workTags) {
74
+ traversedFibers.push(ancestor);
75
+ const parent = ancestor.return;
76
+ if (!parent) {
77
+ workTags = inheritedFiberWorkTags.get(ancestor)?.workTags ?? defaultReactWorkTags;
78
+ break;
79
+ }
80
+ ancestor = parent;
81
+ workTags = getCachedWorkTags(ancestor);
55
82
  }
56
- const workTags = fiberReactWorkTags.get(rootFiber) ?? defaultReactWorkTags;
83
+ const inheritedWorkTags = { workTags, generation: workTagGeneration };
57
84
  for (const traversedFiber of traversedFibers) {
58
- fiberReactWorkTags.set(traversedFiber, workTags);
85
+ inheritedFiberWorkTags.set(traversedFiber, inheritedWorkTags);
59
86
  if (traversedFiber.alternate) {
60
- fiberReactWorkTags.set(traversedFiber.alternate, workTags);
87
+ inheritedFiberWorkTags.set(traversedFiber.alternate, inheritedWorkTags);
61
88
  }
62
89
  }
63
90
  return workTags;
@@ -31,12 +31,30 @@ export interface ServerComponentInfo {
31
31
  env?: string;
32
32
  owner?: Fiber | ServerComponentInfo | null;
33
33
  debugStack?: Error | null;
34
+ debugLocation?: Error | null;
35
+ }
36
+
37
+ export interface ReactIOInfo {
38
+ name: string;
39
+ start: number;
40
+ end: number;
41
+ byteSize?: number;
42
+ value?: Promise<unknown> | null;
43
+ env?: string;
44
+ owner?: ServerComponentInfo | null;
45
+ stack?: unknown[] | null;
46
+ debugStack?: Error | null;
34
47
  }
35
48
 
36
49
  export interface ReactDebugInfo {
37
- debugLocation?: unknown;
50
+ awaited?: ReactIOInfo;
51
+ debugLocation?: Error | null;
38
52
  env?: string;
39
53
  name?: string;
54
+ owner?: ServerComponentInfo | null;
55
+ stack?: unknown[] | null;
56
+ debugStack?: Error | null;
57
+ time?: number;
40
58
  }
41
59
 
42
60
  export interface ReactMemoCache {
@@ -71,10 +89,10 @@ export interface Fiber<T = unknown> extends Omit<
71
89
  | "tag"
72
90
  | "updateQueue"
73
91
  > {
74
- _debugInfo?: ReactDebugInfo[];
92
+ _debugInfo?: ReactDebugInfo[] | null;
75
93
  _debugOwner?: Fiber | ServerComponentInfo | null;
76
94
  _debugSource?: FiberDebugSource | null;
77
- _debugStack?: Error & { stack: string };
95
+ _debugStack?: Error | null;
78
96
  alternate: Fiber | null;
79
97
  child: Fiber | null;
80
98
  deletions: Fiber[] | null;
@@ -96,6 +114,8 @@ export interface HostFiber<T = unknown> extends Fiber<T> {
96
114
 
97
115
  export interface FiberRoot {
98
116
  current: Fiber;
117
+ effectDuration?: number;
118
+ passiveEffectDuration?: number;
99
119
  }
100
120
 
101
121
  export interface MemoizedState {
@@ -115,6 +135,7 @@ export interface ReactDevToolsEventHandler {
115
135
  export interface ReactDevToolsGlobalHook {
116
136
  _instrumentationIsActive?: boolean;
117
137
  _instrumentationSource?: string;
138
+ _isBippyHook?: boolean;
118
139
  checkDCE: (fn: unknown) => void;
119
140
  emit?: (event: string, data?: unknown) => void;
120
141
  getFiberRoots?: (rendererID: number) => Set<FiberRoot>;
@@ -149,14 +170,20 @@ export type RendererDispatcherRef = CurrentDispatcherRef | LegacyDispatcherRef;
149
170
 
150
171
  export interface ReactRenderer extends Omit<
151
172
  ReactReconciler.DevToolsConfig<unknown, unknown, unknown>,
152
- "findFiberByHostInstance"
173
+ "bundleType" | "findFiberByHostInstance"
153
174
  > {
175
+ bundleType: number;
154
176
  currentDispatcherRef?: RendererDispatcherRef | null;
155
177
  findFiberByHostInstance?: (hostInstance: unknown) => Fiber | null;
156
178
  getCurrentFiber?: () => Fiber | null;
157
179
  overrideContext?: (fiber: Fiber, contextType: unknown, path: string[], value: unknown) => void;
158
180
 
159
- overrideHookState?: (fiber: Fiber, id: number, path: string[], value: unknown) => void;
181
+ overrideHookState?: (
182
+ fiber: Fiber,
183
+ id: number,
184
+ path: Array<number | string>,
185
+ value: unknown,
186
+ ) => void;
160
187
  overrideHookStateDeletePath?: (fiber: Fiber, id: number, path: Array<number | string>) => void;
161
188
  overrideHookStateRenamePath?: (
162
189
  fiber: Fiber,
@@ -164,7 +191,7 @@ export interface ReactRenderer extends Omit<
164
191
  oldPath: Array<number | string>,
165
192
  newPath: Array<number | string>,
166
193
  ) => void;
167
- overrideProps?: (fiber: Fiber, path: string[], value: unknown) => void;
194
+ overrideProps?: (fiber: Fiber, path: Array<number | string>, value: unknown) => void;
168
195
  overridePropsDeletePath?: (fiber: Fiber, path: Array<number | string>) => void;
169
196
  overridePropsRenamePath?: (
170
197
  fiber: Fiber,