bippy 0.5.27 → 0.5.29
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/README.md +0 -2
- package/dist/{core-BDWE7M7e.d.ts → core-Cjoce0EW.d.ts} +2 -2
- package/dist/core-DBBh-FTl.js +9 -0
- package/dist/{core-CEUgwvkw.d.cts → core-Y1ecSyti.d.cts} +2 -2
- package/dist/core-xjGqMMEY.cjs +9 -0
- package/dist/core.cjs +1 -1
- package/dist/core.d.cts +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.iife.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{install-hook-only-BOBPiBkc.js → install-hook-only-CRye53Kz.cjs} +1 -1
- package/dist/{install-hook-only-aVNdwrnj.cjs → install-hook-only-DfpahZZO.js} +1 -1
- package/dist/install-hook-only.cjs +1 -1
- package/dist/install-hook-only.iife.js +1 -1
- package/dist/install-hook-only.js +1 -1
- package/dist/rdt-hook-Czn6qzdx.js +9 -0
- package/dist/rdt-hook-DnMMBqZs.cjs +9 -0
- package/dist/source.cjs +7 -7
- package/dist/source.d.cts +1 -1
- package/dist/source.d.ts +1 -1
- package/dist/source.js +12 -12
- package/package.json +3 -1
- package/src/core.ts +1325 -0
- package/src/index.ts +3 -0
- package/src/install-hook-only.ts +3 -0
- package/src/rdt-hook.ts +237 -0
- package/src/source/constants.ts +26 -0
- package/src/source/get-display-name-from-source.ts +103 -0
- package/src/source/get-source.ts +218 -0
- package/src/source/index.ts +6 -0
- package/src/source/owner-stack.ts +598 -0
- package/src/source/parse-stack.ts +295 -0
- package/src/source/symbolication.ts +410 -0
- package/src/source/types.ts +6 -0
- package/src/types.ts +244 -0
- package/dist/core-D8j-0_U5.cjs +0 -9
- package/dist/core-coQbWNwP.js +0 -9
- package/dist/rdt-hook-3SlCAu5p.cjs +0 -9
- package/dist/rdt-hook-BZMdLD7S.js +0 -9
package/src/core.ts
ADDED
|
@@ -0,0 +1,1325 @@
|
|
|
1
|
+
// [!!!] IMPORTANT: do not import React in this file
|
|
2
|
+
// since it will be executed before the react devtools hook is created
|
|
3
|
+
|
|
4
|
+
import type * as React from 'react';
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ContextDependency,
|
|
8
|
+
Fiber,
|
|
9
|
+
FiberRoot,
|
|
10
|
+
MemoizedState,
|
|
11
|
+
ReactDevToolsGlobalHook,
|
|
12
|
+
ReactRenderer,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
BIPPY_INSTRUMENTATION_STRING,
|
|
17
|
+
getRDTHook,
|
|
18
|
+
hasRDTHook,
|
|
19
|
+
isReactRefresh,
|
|
20
|
+
isRealReactDevtools,
|
|
21
|
+
} from './rdt-hook.js';
|
|
22
|
+
|
|
23
|
+
// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactWorkTags.js
|
|
24
|
+
export const FunctionComponentTag = 0;
|
|
25
|
+
export const ClassComponentTag = 1;
|
|
26
|
+
export const HostRootTag = 3;
|
|
27
|
+
export const HostComponentTag = 5;
|
|
28
|
+
export const HostTextTag = 6;
|
|
29
|
+
export const FragmentTag = 7;
|
|
30
|
+
export const ContextConsumerTag = 9;
|
|
31
|
+
export const ForwardRefTag = 11;
|
|
32
|
+
export const SuspenseComponentTag = 13;
|
|
33
|
+
export const MemoComponentTag = 14;
|
|
34
|
+
export const SimpleMemoComponentTag = 15;
|
|
35
|
+
export const LazyComponentTag = 16;
|
|
36
|
+
export const DehydratedSuspenseComponentTag = 18;
|
|
37
|
+
export const SuspenseListComponentTag = 19;
|
|
38
|
+
export const OffscreenComponentTag = 22;
|
|
39
|
+
export const LegacyHiddenComponentTag = 23;
|
|
40
|
+
export const HostHoistableTag = 26;
|
|
41
|
+
export const HostSingletonTag = 27;
|
|
42
|
+
export const ActivityComponentTag = 28;
|
|
43
|
+
export const ViewTransitionComponentTag = 30;
|
|
44
|
+
|
|
45
|
+
export const CONCURRENT_MODE_NUMBER = 0xeacf;
|
|
46
|
+
export const ELEMENT_TYPE_SYMBOL_STRING = 'Symbol(react.element)';
|
|
47
|
+
export const TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING =
|
|
48
|
+
'Symbol(react.transitional.element)';
|
|
49
|
+
export const CONCURRENT_MODE_SYMBOL_STRING = 'Symbol(react.concurrent_mode)';
|
|
50
|
+
export const DEPRECATED_ASYNC_MODE_SYMBOL_STRING = 'Symbol(react.async_mode)';
|
|
51
|
+
|
|
52
|
+
// https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberFlags.js
|
|
53
|
+
const PerformedWork = 0b1;
|
|
54
|
+
const Placement = 0b10;
|
|
55
|
+
const Hydrating = 0b1000000000000;
|
|
56
|
+
const Update = 0b100;
|
|
57
|
+
const Cloned = 0b1000;
|
|
58
|
+
const ChildDeletion = 0b10000;
|
|
59
|
+
const ContentReset = 0b100000;
|
|
60
|
+
const Snapshot = 0b10000000000;
|
|
61
|
+
const Visibility = 0b10000000000000;
|
|
62
|
+
const MutationMask =
|
|
63
|
+
Placement |
|
|
64
|
+
Update |
|
|
65
|
+
ChildDeletion |
|
|
66
|
+
ContentReset |
|
|
67
|
+
Hydrating |
|
|
68
|
+
Visibility |
|
|
69
|
+
Snapshot;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns `true` if object is a React Element.
|
|
73
|
+
*
|
|
74
|
+
* @see https://react.dev/reference/react/isValidElement
|
|
75
|
+
*/
|
|
76
|
+
export const isValidElement = (
|
|
77
|
+
element: unknown,
|
|
78
|
+
): element is React.ReactElement =>
|
|
79
|
+
typeof element === 'object' &&
|
|
80
|
+
element != null &&
|
|
81
|
+
'$$typeof' in element &&
|
|
82
|
+
// react 18 uses Symbol.for('react.element'), react 19 uses Symbol.for('react.transitional.element')
|
|
83
|
+
[
|
|
84
|
+
ELEMENT_TYPE_SYMBOL_STRING,
|
|
85
|
+
TRANSITIONAL_ELEMENT_TYPE_SYMBOL_STRING,
|
|
86
|
+
].includes(String(element.$$typeof));
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns `true` if object is a React Fiber.
|
|
90
|
+
*/
|
|
91
|
+
export const isValidFiber = (fiber: unknown): fiber is Fiber =>
|
|
92
|
+
typeof fiber === 'object' &&
|
|
93
|
+
fiber != null &&
|
|
94
|
+
'tag' in fiber &&
|
|
95
|
+
'stateNode' in fiber &&
|
|
96
|
+
'return' in fiber &&
|
|
97
|
+
'child' in fiber &&
|
|
98
|
+
'sibling' in fiber &&
|
|
99
|
+
'flags' in fiber;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Returns `true` if fiber is a host fiber. Host fibers are DOM nodes in react-dom, `View` in react-native, etc.
|
|
103
|
+
*
|
|
104
|
+
* @see https://reactnative.dev/architecture/glossary#host-view-tree-and-host-view
|
|
105
|
+
*/
|
|
106
|
+
export const isHostFiber = (fiber: Fiber): boolean => {
|
|
107
|
+
switch (fiber.tag) {
|
|
108
|
+
case HostComponentTag:
|
|
109
|
+
// @ts-expect-error: it exists
|
|
110
|
+
case HostHoistableTag:
|
|
111
|
+
// @ts-expect-error: it exists
|
|
112
|
+
case HostSingletonTag:
|
|
113
|
+
return true;
|
|
114
|
+
default:
|
|
115
|
+
return typeof fiber.type === 'string';
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Returns `true` if fiber is a composite fiber. Composite fibers are fibers that can render (like functional components, class components, etc.)
|
|
121
|
+
*
|
|
122
|
+
* @see https://reactnative.dev/architecture/glossary#react-composite-components
|
|
123
|
+
*/
|
|
124
|
+
export const isCompositeFiber = (fiber: Fiber): boolean => {
|
|
125
|
+
switch (fiber.tag) {
|
|
126
|
+
case ClassComponentTag:
|
|
127
|
+
case ForwardRefTag:
|
|
128
|
+
case FunctionComponentTag:
|
|
129
|
+
case MemoComponentTag:
|
|
130
|
+
case SimpleMemoComponentTag:
|
|
131
|
+
return true;
|
|
132
|
+
default:
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Returns `true` if the object is a {@link Fiber}
|
|
139
|
+
*/
|
|
140
|
+
export const isFiber = (maybeFiber: unknown): maybeFiber is Fiber => {
|
|
141
|
+
if (!maybeFiber || typeof maybeFiber !== 'object') return false;
|
|
142
|
+
// this is a fast check. pendingProps will ALWAYS exist in fiber
|
|
143
|
+
// `containerInfo` is in FiberRootNode, not FiberNode
|
|
144
|
+
return 'pendingProps' in maybeFiber && !('containerInfo' in maybeFiber);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Returns `true` if the two {@link Fiber}s are the same reference
|
|
149
|
+
*/
|
|
150
|
+
export const areFiberEqual = (fiberA: Fiber, fiberB: Fiber): boolean => {
|
|
151
|
+
return (
|
|
152
|
+
fiberA === fiberB ||
|
|
153
|
+
fiberA.alternate === fiberB ||
|
|
154
|
+
fiberB.alternate === fiberA
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Traverses up or down a {@link Fiber}'s contexts, return `true` to stop and select the current and previous context value.
|
|
160
|
+
*/
|
|
161
|
+
export const traverseContexts = (
|
|
162
|
+
fiber: Fiber,
|
|
163
|
+
selector: (
|
|
164
|
+
nextValue: ContextDependency<unknown> | null | undefined,
|
|
165
|
+
prevValue: ContextDependency<unknown> | null | undefined,
|
|
166
|
+
) => boolean | void,
|
|
167
|
+
): boolean => {
|
|
168
|
+
try {
|
|
169
|
+
const nextDependencies = fiber.dependencies;
|
|
170
|
+
const prevDependencies = fiber.alternate?.dependencies;
|
|
171
|
+
|
|
172
|
+
if (!nextDependencies || !prevDependencies) return false;
|
|
173
|
+
if (
|
|
174
|
+
typeof nextDependencies !== 'object' ||
|
|
175
|
+
!('firstContext' in nextDependencies) ||
|
|
176
|
+
typeof prevDependencies !== 'object' ||
|
|
177
|
+
!('firstContext' in prevDependencies)
|
|
178
|
+
) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
let nextContext: ContextDependency<unknown> | null | undefined =
|
|
182
|
+
nextDependencies.firstContext;
|
|
183
|
+
let prevContext: ContextDependency<unknown> | null | undefined =
|
|
184
|
+
prevDependencies.firstContext;
|
|
185
|
+
while (
|
|
186
|
+
(nextContext &&
|
|
187
|
+
typeof nextContext === 'object' &&
|
|
188
|
+
'memoizedValue' in nextContext) ||
|
|
189
|
+
(prevContext &&
|
|
190
|
+
typeof prevContext === 'object' &&
|
|
191
|
+
'memoizedValue' in prevContext)
|
|
192
|
+
) {
|
|
193
|
+
if (selector(nextContext, prevContext) === true) return true;
|
|
194
|
+
|
|
195
|
+
nextContext = nextContext?.next;
|
|
196
|
+
prevContext = prevContext?.next;
|
|
197
|
+
}
|
|
198
|
+
} catch {}
|
|
199
|
+
return false;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Traverses up or down a {@link Fiber}'s states, return `true` to stop and select the current and previous state value. This stores both state values and effects.
|
|
204
|
+
*/
|
|
205
|
+
export const traverseState = (
|
|
206
|
+
fiber: Fiber,
|
|
207
|
+
selector: (
|
|
208
|
+
nextValue: MemoizedState | null | undefined,
|
|
209
|
+
prevValue: MemoizedState | null | undefined,
|
|
210
|
+
) => boolean | void,
|
|
211
|
+
): boolean => {
|
|
212
|
+
try {
|
|
213
|
+
let nextState: MemoizedState | null | undefined = fiber.memoizedState;
|
|
214
|
+
let prevState: MemoizedState | null | undefined =
|
|
215
|
+
fiber.alternate?.memoizedState;
|
|
216
|
+
|
|
217
|
+
while (nextState || prevState) {
|
|
218
|
+
if (selector(nextState, prevState) === true) return true;
|
|
219
|
+
|
|
220
|
+
nextState = nextState?.next;
|
|
221
|
+
prevState = prevState?.next;
|
|
222
|
+
}
|
|
223
|
+
} catch {}
|
|
224
|
+
return false;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Traverses up or down a {@link Fiber}'s props, return `true` to stop and select the current and previous props value.
|
|
229
|
+
*/
|
|
230
|
+
export const traverseProps = (
|
|
231
|
+
fiber: Fiber,
|
|
232
|
+
selector: (
|
|
233
|
+
propName: string,
|
|
234
|
+
nextValue: unknown,
|
|
235
|
+
prevValue: unknown,
|
|
236
|
+
) => boolean | void,
|
|
237
|
+
): boolean => {
|
|
238
|
+
try {
|
|
239
|
+
const nextProps = fiber.memoizedProps;
|
|
240
|
+
const prevProps = fiber.alternate?.memoizedProps || {};
|
|
241
|
+
|
|
242
|
+
const allKeys = new Set([
|
|
243
|
+
...Object.keys(nextProps),
|
|
244
|
+
...Object.keys(prevProps),
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
for (const propName of allKeys) {
|
|
248
|
+
const prevValue = prevProps?.[propName];
|
|
249
|
+
const nextValue = nextProps?.[propName];
|
|
250
|
+
|
|
251
|
+
if (selector(propName, nextValue, prevValue) === true) return true;
|
|
252
|
+
}
|
|
253
|
+
} catch {}
|
|
254
|
+
return false;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Returns `true` if the {@link Fiber} has rendered. Note that this does not mean the fiber has rendered in the current commit, just that it has rendered in the past.
|
|
259
|
+
*/
|
|
260
|
+
export const didFiberRender = (fiber: Fiber): boolean => {
|
|
261
|
+
const nextProps = fiber.memoizedProps;
|
|
262
|
+
const prevProps = fiber.alternate?.memoizedProps || {};
|
|
263
|
+
const flags =
|
|
264
|
+
fiber.flags ?? (fiber as unknown as { effectTag: number }).effectTag ?? 0;
|
|
265
|
+
|
|
266
|
+
switch (fiber.tag) {
|
|
267
|
+
case ClassComponentTag:
|
|
268
|
+
case ContextConsumerTag:
|
|
269
|
+
case ForwardRefTag:
|
|
270
|
+
case FunctionComponentTag:
|
|
271
|
+
case MemoComponentTag:
|
|
272
|
+
case SimpleMemoComponentTag: {
|
|
273
|
+
return (flags & PerformedWork) === PerformedWork;
|
|
274
|
+
}
|
|
275
|
+
default:
|
|
276
|
+
// Host nodes (DOM, root, etc.)
|
|
277
|
+
if (!fiber.alternate) return true;
|
|
278
|
+
return (
|
|
279
|
+
prevProps !== nextProps ||
|
|
280
|
+
fiber.alternate.memoizedState !== fiber.memoizedState ||
|
|
281
|
+
fiber.alternate.ref !== fiber.ref
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 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.
|
|
288
|
+
*/
|
|
289
|
+
export const didFiberCommit = (fiber: Fiber): boolean => {
|
|
290
|
+
return Boolean(
|
|
291
|
+
(fiber.flags & (MutationMask | Cloned)) !== 0 ||
|
|
292
|
+
(fiber.subtreeFlags & (MutationMask | Cloned)) !== 0,
|
|
293
|
+
);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Returns all host {@link Fiber}s that have committed and rendered.
|
|
298
|
+
*/
|
|
299
|
+
export const getMutatedHostFibers = (fiber: Fiber): Fiber[] => {
|
|
300
|
+
const mutations: Fiber[] = [];
|
|
301
|
+
const stack: Fiber[] = [fiber];
|
|
302
|
+
|
|
303
|
+
while (stack.length) {
|
|
304
|
+
const node = stack.pop();
|
|
305
|
+
if (!node) continue;
|
|
306
|
+
|
|
307
|
+
if (isHostFiber(node) && didFiberCommit(node) && didFiberRender(node)) {
|
|
308
|
+
mutations.push(node);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (node.child) stack.push(node.child);
|
|
312
|
+
if (node.sibling) stack.push(node.sibling);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return mutations;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Returns the stack of {@link Fiber}s from the current fiber to the root fiber.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```ts
|
|
323
|
+
* [fiber, fiber.return, fiber.return.return, ...]
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
export const getFiberStack = (fiber: Fiber): Fiber[] => {
|
|
327
|
+
const stack: Fiber[] = [];
|
|
328
|
+
let currentFiber = fiber;
|
|
329
|
+
while (currentFiber.return) {
|
|
330
|
+
stack.push(currentFiber);
|
|
331
|
+
currentFiber = currentFiber.return as Fiber;
|
|
332
|
+
}
|
|
333
|
+
return stack;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Returns `true` if the {@link Fiber} should be filtered out during reconciliation.
|
|
338
|
+
*/
|
|
339
|
+
export const shouldFilterFiber = (fiber: Fiber): boolean => {
|
|
340
|
+
switch (fiber.tag) {
|
|
341
|
+
case DehydratedSuspenseComponentTag:
|
|
342
|
+
// TODO: ideally we would show dehydrated Suspense immediately.
|
|
343
|
+
// However, it has some special behavior (like disconnecting
|
|
344
|
+
// an alternate and turning into real Suspense) which breaks DevTools.
|
|
345
|
+
// For now, ignore it, and only show it once it gets hydrated.
|
|
346
|
+
// https://github.com/bvaughn/react-devtools-experimental/issues/197
|
|
347
|
+
return true;
|
|
348
|
+
|
|
349
|
+
case FragmentTag:
|
|
350
|
+
case HostTextTag:
|
|
351
|
+
case LegacyHiddenComponentTag:
|
|
352
|
+
case OffscreenComponentTag:
|
|
353
|
+
return true;
|
|
354
|
+
|
|
355
|
+
case HostRootTag:
|
|
356
|
+
// It is never valid to filter the root element.
|
|
357
|
+
return false;
|
|
358
|
+
|
|
359
|
+
default: {
|
|
360
|
+
const symbolOrNumber =
|
|
361
|
+
typeof fiber.type === 'object' && fiber.type !== null
|
|
362
|
+
? fiber.type.$$typeof
|
|
363
|
+
: fiber.type;
|
|
364
|
+
|
|
365
|
+
const typeSymbol =
|
|
366
|
+
typeof symbolOrNumber === 'symbol'
|
|
367
|
+
? symbolOrNumber.toString()
|
|
368
|
+
: symbolOrNumber;
|
|
369
|
+
|
|
370
|
+
switch (typeSymbol) {
|
|
371
|
+
case CONCURRENT_MODE_NUMBER:
|
|
372
|
+
case CONCURRENT_MODE_SYMBOL_STRING:
|
|
373
|
+
case DEPRECATED_ASYNC_MODE_SYMBOL_STRING:
|
|
374
|
+
return true;
|
|
375
|
+
|
|
376
|
+
default:
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Returns the nearest host {@link Fiber} to the current {@link Fiber}.
|
|
385
|
+
*/
|
|
386
|
+
export const getNearestHostFiber = (
|
|
387
|
+
fiber: Fiber,
|
|
388
|
+
ascending = false,
|
|
389
|
+
): Fiber | null => {
|
|
390
|
+
let hostFiber = traverseFiber(fiber, isHostFiber, ascending);
|
|
391
|
+
if (!hostFiber) {
|
|
392
|
+
hostFiber = traverseFiber(fiber, isHostFiber, !ascending);
|
|
393
|
+
}
|
|
394
|
+
return hostFiber;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Returns all host {@link Fiber}s in the tree that are associated with the current {@link Fiber}.
|
|
399
|
+
*/
|
|
400
|
+
export const getNearestHostFibers = (fiber: Fiber): Fiber[] => {
|
|
401
|
+
const hostFibers: Fiber[] = [];
|
|
402
|
+
const stack: Fiber[] = [];
|
|
403
|
+
|
|
404
|
+
if (isHostFiber(fiber)) {
|
|
405
|
+
hostFibers.push(fiber);
|
|
406
|
+
} else if (fiber.child) {
|
|
407
|
+
stack.push(fiber.child);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
while (stack.length) {
|
|
411
|
+
const currentNode = stack.pop();
|
|
412
|
+
if (!currentNode) break;
|
|
413
|
+
if (isHostFiber(currentNode)) {
|
|
414
|
+
hostFibers.push(currentNode);
|
|
415
|
+
} else if (currentNode.child) {
|
|
416
|
+
stack.push(currentNode.child);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (currentNode.sibling) {
|
|
420
|
+
stack.push(currentNode.sibling);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return hostFibers;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Traverses up or down a {@link Fiber}, return `true` to stop and select a node.
|
|
429
|
+
*/
|
|
430
|
+
export function traverseFiber(
|
|
431
|
+
fiber: Fiber | null,
|
|
432
|
+
selector: (node: Fiber) => boolean | void,
|
|
433
|
+
ascending?: boolean,
|
|
434
|
+
): Fiber | null;
|
|
435
|
+
export function traverseFiber(
|
|
436
|
+
fiber: Fiber | null,
|
|
437
|
+
selector: (node: Fiber) => Promise<boolean | void>,
|
|
438
|
+
ascending?: boolean,
|
|
439
|
+
): Promise<Fiber | null>;
|
|
440
|
+
export function traverseFiber(
|
|
441
|
+
fiber: Fiber | null,
|
|
442
|
+
selector: (node: Fiber) => boolean | Promise<boolean | void> | void,
|
|
443
|
+
ascending = false,
|
|
444
|
+
): Fiber | null | Promise<Fiber | null> {
|
|
445
|
+
if (!fiber) return null;
|
|
446
|
+
|
|
447
|
+
const firstResult = selector(fiber);
|
|
448
|
+
if (firstResult instanceof Promise) {
|
|
449
|
+
return (async () => {
|
|
450
|
+
if ((await firstResult) === true) return fiber;
|
|
451
|
+
|
|
452
|
+
let child = ascending ? fiber.return : fiber.child;
|
|
453
|
+
while (child) {
|
|
454
|
+
const match = await traverseFiberAsync(
|
|
455
|
+
child,
|
|
456
|
+
selector as (node: Fiber) => Promise<boolean | void>,
|
|
457
|
+
ascending,
|
|
458
|
+
);
|
|
459
|
+
if (match) return match;
|
|
460
|
+
child = ascending ? null : child.sibling;
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
})();
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (firstResult === true) return fiber;
|
|
467
|
+
|
|
468
|
+
let child = ascending ? fiber.return : fiber.child;
|
|
469
|
+
while (child) {
|
|
470
|
+
const match = traverseFiberSync(
|
|
471
|
+
child,
|
|
472
|
+
selector as (node: Fiber) => boolean | void,
|
|
473
|
+
ascending,
|
|
474
|
+
);
|
|
475
|
+
if (match) return match;
|
|
476
|
+
child = ascending ? null : child.sibling;
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export const traverseFiberSync = (
|
|
482
|
+
fiber: Fiber | null,
|
|
483
|
+
selector: (node: Fiber) => boolean | void,
|
|
484
|
+
ascending = false,
|
|
485
|
+
): Fiber | null => {
|
|
486
|
+
if (!fiber) return null;
|
|
487
|
+
if (selector(fiber) === true) return fiber;
|
|
488
|
+
|
|
489
|
+
let child = ascending ? fiber.return : fiber.child;
|
|
490
|
+
while (child) {
|
|
491
|
+
const match = traverseFiberSync(child, selector, ascending);
|
|
492
|
+
if (match) return match;
|
|
493
|
+
|
|
494
|
+
child = ascending ? null : child.sibling;
|
|
495
|
+
}
|
|
496
|
+
return null;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
export const traverseFiberAsync = async (
|
|
500
|
+
fiber: Fiber | null,
|
|
501
|
+
selector: (node: Fiber) => Promise<boolean | void>,
|
|
502
|
+
ascending = false,
|
|
503
|
+
): Promise<Fiber | null> => {
|
|
504
|
+
if (!fiber) return null;
|
|
505
|
+
if ((await selector(fiber)) === true) return fiber;
|
|
506
|
+
|
|
507
|
+
let child = ascending ? fiber.return : fiber.child;
|
|
508
|
+
while (child) {
|
|
509
|
+
const match = await traverseFiberAsync(child, selector, ascending);
|
|
510
|
+
if (match) return match;
|
|
511
|
+
|
|
512
|
+
child = ascending ? null : child.sibling;
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Returns the timings of the {@link Fiber}.
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* ```ts
|
|
522
|
+
* const { selfTime, totalTime } = getTimings(fiber);
|
|
523
|
+
* console.log(selfTime, totalTime);
|
|
524
|
+
* ```
|
|
525
|
+
*/
|
|
526
|
+
export const getTimings = (
|
|
527
|
+
fiber?: Fiber | null,
|
|
528
|
+
): { selfTime: number; totalTime: number } => {
|
|
529
|
+
const totalTime = fiber?.actualDuration ?? 0;
|
|
530
|
+
let selfTime = totalTime;
|
|
531
|
+
// TODO: calculate a DOM time, which is just host component summed up
|
|
532
|
+
let child = fiber?.child ?? null;
|
|
533
|
+
while (totalTime > 0 && child != null) {
|
|
534
|
+
selfTime -= child.actualDuration ?? 0;
|
|
535
|
+
child = child.sibling;
|
|
536
|
+
}
|
|
537
|
+
return { selfTime, totalTime };
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Returns `true` if the {@link Fiber} uses React Compiler's memo cache.
|
|
542
|
+
*/
|
|
543
|
+
export const hasMemoCache = (fiber: Fiber): boolean => {
|
|
544
|
+
return Boolean(
|
|
545
|
+
(fiber.updateQueue as unknown as { memoCache: unknown })?.memoCache,
|
|
546
|
+
);
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
type FiberType =
|
|
550
|
+
| React.ComponentType<unknown>
|
|
551
|
+
| React.ForwardRefExoticComponent<unknown>
|
|
552
|
+
| React.MemoExoticComponent<React.ComponentType<unknown>>;
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Returns the type (e.g. component definition) of the {@link Fiber}
|
|
556
|
+
*/
|
|
557
|
+
export const getType = (type: unknown): null | React.ComponentType<unknown> => {
|
|
558
|
+
const currentType = type as FiberType;
|
|
559
|
+
if (typeof currentType === 'function') {
|
|
560
|
+
return currentType;
|
|
561
|
+
}
|
|
562
|
+
if (typeof currentType === 'object' && currentType) {
|
|
563
|
+
// memo / forwardRef case
|
|
564
|
+
return getType(
|
|
565
|
+
(currentType as React.MemoExoticComponent<React.ComponentType<unknown>>)
|
|
566
|
+
.type ||
|
|
567
|
+
(currentType as { render: React.ComponentType<unknown> }).render,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Returns the display name of the {@link Fiber} type.
|
|
575
|
+
*/
|
|
576
|
+
export const getDisplayName = (type: unknown): null | string => {
|
|
577
|
+
const currentType = type as FiberType;
|
|
578
|
+
if (typeof currentType === 'string') {
|
|
579
|
+
return currentType;
|
|
580
|
+
}
|
|
581
|
+
if (
|
|
582
|
+
typeof currentType !== 'function' &&
|
|
583
|
+
!(typeof currentType === 'object' && currentType)
|
|
584
|
+
) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const name = currentType.displayName || currentType.name || null;
|
|
588
|
+
if (name) return name;
|
|
589
|
+
const unwrappedType = getType(currentType);
|
|
590
|
+
if (!unwrappedType) return null;
|
|
591
|
+
return unwrappedType.displayName || unwrappedType.name || null;
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Returns the build type of the React renderer.
|
|
596
|
+
*/
|
|
597
|
+
export const detectReactBuildType = (
|
|
598
|
+
renderer: ReactRenderer,
|
|
599
|
+
): 'development' | 'production' => {
|
|
600
|
+
try {
|
|
601
|
+
if (typeof renderer.version === 'string' && renderer.bundleType > 0) {
|
|
602
|
+
return 'development';
|
|
603
|
+
}
|
|
604
|
+
} catch {}
|
|
605
|
+
return 'production';
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Returns `true` if bippy's instrumentation is active.
|
|
610
|
+
*/
|
|
611
|
+
export const isInstrumentationActive = (): boolean => {
|
|
612
|
+
const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
613
|
+
return (
|
|
614
|
+
Boolean(rdtHook?._instrumentationIsActive) ||
|
|
615
|
+
isRealReactDevtools(rdtHook) ||
|
|
616
|
+
isReactRefresh(rdtHook)
|
|
617
|
+
);
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Returns the latest fiber (since it may be double-buffered).
|
|
622
|
+
*/
|
|
623
|
+
export const getLatestFiber = (fiber: Fiber): Fiber => {
|
|
624
|
+
const alternate = fiber.alternate;
|
|
625
|
+
if (!alternate) return fiber;
|
|
626
|
+
if (alternate.actualStartTime && fiber.actualStartTime) {
|
|
627
|
+
return alternate.actualStartTime > fiber.actualStartTime
|
|
628
|
+
? alternate
|
|
629
|
+
: fiber;
|
|
630
|
+
}
|
|
631
|
+
for (const root of _fiberRoots) {
|
|
632
|
+
const latestFiber = traverseFiber(root.current, (innerFiber) => {
|
|
633
|
+
if (innerFiber === fiber) return true;
|
|
634
|
+
});
|
|
635
|
+
if (latestFiber) return latestFiber;
|
|
636
|
+
}
|
|
637
|
+
return fiber;
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
export type RenderHandler = <S>(
|
|
641
|
+
fiber: Fiber,
|
|
642
|
+
phase: RenderPhase,
|
|
643
|
+
state?: S,
|
|
644
|
+
) => unknown;
|
|
645
|
+
|
|
646
|
+
export type RenderPhase = 'mount' | 'unmount' | 'update';
|
|
647
|
+
|
|
648
|
+
let fiberId = 0;
|
|
649
|
+
export const fiberIdMap = new WeakMap<Fiber, number>();
|
|
650
|
+
|
|
651
|
+
export const setFiberId = (fiber: Fiber, id: number = fiberId++): void => {
|
|
652
|
+
fiberIdMap.set(fiber, id);
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// react fibers are double buffered, so the alternate fiber may
|
|
656
|
+
// be switched to the current fiber and vice versa.
|
|
657
|
+
// fiber === fiber.alternate.alternate
|
|
658
|
+
export const getFiberId = (fiber: Fiber): number => {
|
|
659
|
+
let id = fiberIdMap.get(fiber);
|
|
660
|
+
if (!id && fiber.alternate) {
|
|
661
|
+
id = fiberIdMap.get(fiber.alternate);
|
|
662
|
+
}
|
|
663
|
+
if (!id) {
|
|
664
|
+
id = fiberId++;
|
|
665
|
+
setFiberId(fiber, id);
|
|
666
|
+
}
|
|
667
|
+
return id;
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
export const mountFiberRecursively = (
|
|
671
|
+
onRender: RenderHandler,
|
|
672
|
+
firstChild: Fiber,
|
|
673
|
+
traverseSiblings: boolean,
|
|
674
|
+
): void => {
|
|
675
|
+
let fiber: Fiber | null = firstChild;
|
|
676
|
+
|
|
677
|
+
while (fiber != null) {
|
|
678
|
+
if (!fiberIdMap.has(fiber)) {
|
|
679
|
+
getFiberId(fiber);
|
|
680
|
+
}
|
|
681
|
+
const shouldIncludeInTree = !shouldFilterFiber(fiber);
|
|
682
|
+
if (shouldIncludeInTree && didFiberRender(fiber)) {
|
|
683
|
+
onRender(fiber, 'mount');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (fiber.tag === SuspenseComponentTag) {
|
|
687
|
+
const isTimedOut = fiber.memoizedState !== null;
|
|
688
|
+
if (isTimedOut) {
|
|
689
|
+
// Special case: if Suspense mounts in a timed-out state,
|
|
690
|
+
// get the fallback child from the inner fragment and mount
|
|
691
|
+
// it as if it was our own child. Updates handle this too.
|
|
692
|
+
const primaryChildFragment = fiber.child;
|
|
693
|
+
const fallbackChildFragment = primaryChildFragment
|
|
694
|
+
? primaryChildFragment.sibling
|
|
695
|
+
: null;
|
|
696
|
+
if (fallbackChildFragment) {
|
|
697
|
+
const fallbackChild = fallbackChildFragment.child;
|
|
698
|
+
if (fallbackChild !== null) {
|
|
699
|
+
mountFiberRecursively(onRender, fallbackChild, false);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
} else {
|
|
703
|
+
let primaryChild: Fiber | null = null;
|
|
704
|
+
const areSuspenseChildrenConditionallyWrapped =
|
|
705
|
+
(OffscreenComponentTag as number) === -1;
|
|
706
|
+
if (areSuspenseChildrenConditionallyWrapped) {
|
|
707
|
+
primaryChild = fiber.child;
|
|
708
|
+
} else if (fiber.child !== null) {
|
|
709
|
+
primaryChild = fiber.child.child;
|
|
710
|
+
}
|
|
711
|
+
if (primaryChild !== null) {
|
|
712
|
+
mountFiberRecursively(onRender, primaryChild, false);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
} else if (fiber.child != null) {
|
|
716
|
+
mountFiberRecursively(onRender, fiber.child, true);
|
|
717
|
+
}
|
|
718
|
+
fiber = traverseSiblings ? fiber.sibling : null;
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
export const updateFiberRecursively = (
|
|
723
|
+
onRender: RenderHandler,
|
|
724
|
+
nextFiber: Fiber,
|
|
725
|
+
prevFiber: Fiber,
|
|
726
|
+
parentFiber: Fiber | null,
|
|
727
|
+
): void => {
|
|
728
|
+
if (!fiberIdMap.has(nextFiber)) {
|
|
729
|
+
getFiberId(nextFiber);
|
|
730
|
+
}
|
|
731
|
+
if (!prevFiber) return;
|
|
732
|
+
if (!fiberIdMap.has(prevFiber)) {
|
|
733
|
+
getFiberId(prevFiber);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const isSuspense = nextFiber.tag === SuspenseComponentTag;
|
|
737
|
+
|
|
738
|
+
const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
|
|
739
|
+
if (shouldIncludeInTree && didFiberRender(nextFiber)) {
|
|
740
|
+
onRender(nextFiber, 'update');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// The behavior of timed-out Suspense trees is unique.
|
|
744
|
+
// Rather than unmount the timed out content (and possibly lose important state),
|
|
745
|
+
// React re-parents this content within a hidden Fragment while the fallback is showing.
|
|
746
|
+
// This behavior doesn't need to be observable in the DevTools though.
|
|
747
|
+
// It might even result in a bad user experience for e.g. node selection in the Elements panel.
|
|
748
|
+
// The easiest fix is to strip out the intermediate Fragment fibers,
|
|
749
|
+
// so the Elements panel and Profiler don't need to special case them.
|
|
750
|
+
// Suspense components only have a non-null memoizedState if they're timed-out.
|
|
751
|
+
const prevDidTimeout = isSuspense && prevFiber.memoizedState !== null;
|
|
752
|
+
const nextDidTimeOut = isSuspense && nextFiber.memoizedState !== null;
|
|
753
|
+
|
|
754
|
+
// The logic below is inspired by the code paths in updateSuspenseComponent()
|
|
755
|
+
// inside ReactFiberBeginWork in the React source code.
|
|
756
|
+
if (prevDidTimeout && nextDidTimeOut) {
|
|
757
|
+
// Fallback -> Fallback:
|
|
758
|
+
// 1. Reconcile fallback set.
|
|
759
|
+
const nextFallbackChildSet = nextFiber.child?.sibling ?? null;
|
|
760
|
+
// Note: We can't use nextFiber.child.sibling.alternate
|
|
761
|
+
// because the set is special and alternate may not exist.
|
|
762
|
+
const prevFallbackChildSet = prevFiber.child?.sibling ?? null;
|
|
763
|
+
|
|
764
|
+
if (nextFallbackChildSet !== null && prevFallbackChildSet !== null) {
|
|
765
|
+
updateFiberRecursively(
|
|
766
|
+
onRender,
|
|
767
|
+
nextFallbackChildSet,
|
|
768
|
+
prevFallbackChildSet,
|
|
769
|
+
nextFiber,
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
} else if (prevDidTimeout && !nextDidTimeOut) {
|
|
773
|
+
// Fallback -> Primary:
|
|
774
|
+
// 1. Unmount fallback set
|
|
775
|
+
// Note: don't emulate fallback unmount because React actually did it.
|
|
776
|
+
// 2. Mount primary set
|
|
777
|
+
const nextPrimaryChildSet = nextFiber.child;
|
|
778
|
+
|
|
779
|
+
if (nextPrimaryChildSet !== null) {
|
|
780
|
+
mountFiberRecursively(onRender, nextPrimaryChildSet, true);
|
|
781
|
+
}
|
|
782
|
+
} else if (!prevDidTimeout && nextDidTimeOut) {
|
|
783
|
+
// Primary -> Fallback:
|
|
784
|
+
// 1. Hide primary set
|
|
785
|
+
// This is not a real unmount, so it won't get reported by React.
|
|
786
|
+
// We need to manually walk the previous tree and record unmounts.
|
|
787
|
+
unmountFiberChildrenRecursively(onRender, prevFiber);
|
|
788
|
+
|
|
789
|
+
// 2. Mount fallback set
|
|
790
|
+
const nextFallbackChildSet = nextFiber.child?.sibling ?? null;
|
|
791
|
+
|
|
792
|
+
if (nextFallbackChildSet !== null) {
|
|
793
|
+
mountFiberRecursively(onRender, nextFallbackChildSet, true);
|
|
794
|
+
}
|
|
795
|
+
} else if (nextFiber.child !== prevFiber.child) {
|
|
796
|
+
// Common case: Primary -> Primary.
|
|
797
|
+
// This is the same code path as for non-Suspense fibers.
|
|
798
|
+
|
|
799
|
+
// If the first child is different, we need to traverse them.
|
|
800
|
+
// Each next child will be either a new child (mount) or an alternate (update).
|
|
801
|
+
let nextChild = nextFiber.child;
|
|
802
|
+
|
|
803
|
+
while (nextChild) {
|
|
804
|
+
// We already know children will be referentially different because
|
|
805
|
+
// they are either new mounts or alternates of previous children.
|
|
806
|
+
// Schedule updates and mounts depending on whether alternates exist.
|
|
807
|
+
// We don't track deletions here because they are reported separately.
|
|
808
|
+
if (nextChild.alternate) {
|
|
809
|
+
const prevChild = nextChild.alternate;
|
|
810
|
+
|
|
811
|
+
updateFiberRecursively(
|
|
812
|
+
onRender,
|
|
813
|
+
nextChild,
|
|
814
|
+
prevChild,
|
|
815
|
+
shouldIncludeInTree ? nextFiber : parentFiber,
|
|
816
|
+
);
|
|
817
|
+
} else {
|
|
818
|
+
mountFiberRecursively(onRender, nextChild, false);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Try the next child.
|
|
822
|
+
nextChild = nextChild.sibling;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
export const unmountFiber = (onRender: RenderHandler, fiber: Fiber): void => {
|
|
828
|
+
const isRoot = fiber.tag === HostRootTag;
|
|
829
|
+
|
|
830
|
+
if (isRoot || !shouldFilterFiber(fiber)) {
|
|
831
|
+
onRender(fiber, 'unmount');
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
export const unmountFiberChildrenRecursively = (
|
|
836
|
+
onRender: RenderHandler,
|
|
837
|
+
fiber: Fiber,
|
|
838
|
+
): void => {
|
|
839
|
+
// We might meet a nested Suspense on our way.
|
|
840
|
+
const isTimedOutSuspense =
|
|
841
|
+
fiber.tag === SuspenseComponentTag && fiber.memoizedState !== null;
|
|
842
|
+
let child = fiber.child;
|
|
843
|
+
|
|
844
|
+
if (isTimedOutSuspense) {
|
|
845
|
+
// If it's showing fallback tree, let's traverse it instead.
|
|
846
|
+
const primaryChildFragment = fiber.child;
|
|
847
|
+
const fallbackChildFragment = primaryChildFragment?.sibling ?? null;
|
|
848
|
+
|
|
849
|
+
// Skip over to the real Fiber child.
|
|
850
|
+
child = fallbackChildFragment?.child ?? null;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
while (child !== null) {
|
|
854
|
+
// Record simulated unmounts children-first.
|
|
855
|
+
// We skip nodes without return because those are real unmounts.
|
|
856
|
+
if (child.return !== null) {
|
|
857
|
+
unmountFiber(onRender, child);
|
|
858
|
+
unmountFiberChildrenRecursively(onRender, child);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
child = child.sibling;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
let commitId = 0;
|
|
866
|
+
const rootInstanceMap = new WeakMap<
|
|
867
|
+
FiberRoot,
|
|
868
|
+
{
|
|
869
|
+
id: number;
|
|
870
|
+
prevFiber: Fiber | null;
|
|
871
|
+
}
|
|
872
|
+
>();
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Creates a fiber visitor function. Must pass a fiber root and a render handler.
|
|
876
|
+
* @example
|
|
877
|
+
* traverseRenderedFibers(root, (fiber, phase) => {
|
|
878
|
+
* console.log(phase)
|
|
879
|
+
* })
|
|
880
|
+
*/
|
|
881
|
+
export const traverseRenderedFibers = (
|
|
882
|
+
root: FiberRoot,
|
|
883
|
+
onRender: RenderHandler,
|
|
884
|
+
): void => {
|
|
885
|
+
const fiber = 'current' in root ? root.current : root;
|
|
886
|
+
|
|
887
|
+
let rootInstance = rootInstanceMap.get(root);
|
|
888
|
+
|
|
889
|
+
if (!rootInstance) {
|
|
890
|
+
rootInstance = { id: commitId++, prevFiber: null };
|
|
891
|
+
rootInstanceMap.set(root, rootInstance);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const { prevFiber } = rootInstance;
|
|
895
|
+
// if fiberRoot don't have current instance, means it's been unmounted
|
|
896
|
+
if (!fiber) {
|
|
897
|
+
unmountFiber(onRender, fiber);
|
|
898
|
+
} else if (prevFiber !== null) {
|
|
899
|
+
const wasMounted =
|
|
900
|
+
prevFiber &&
|
|
901
|
+
prevFiber.memoizedState != null &&
|
|
902
|
+
prevFiber.memoizedState.element != null &&
|
|
903
|
+
// A dehydrated root is not considered mounted
|
|
904
|
+
prevFiber.memoizedState.isDehydrated !== true;
|
|
905
|
+
const isMounted =
|
|
906
|
+
fiber.memoizedState != null &&
|
|
907
|
+
fiber.memoizedState.element != null &&
|
|
908
|
+
// A dehydrated root is not considered mounted
|
|
909
|
+
fiber.memoizedState.isDehydrated !== true;
|
|
910
|
+
|
|
911
|
+
if (!wasMounted && isMounted) {
|
|
912
|
+
mountFiberRecursively(onRender, fiber, false);
|
|
913
|
+
} else if (wasMounted && isMounted) {
|
|
914
|
+
updateFiberRecursively(onRender, fiber, fiber.alternate, null);
|
|
915
|
+
} else if (wasMounted && !isMounted) {
|
|
916
|
+
unmountFiber(onRender, fiber);
|
|
917
|
+
}
|
|
918
|
+
} else {
|
|
919
|
+
mountFiberRecursively(onRender, fiber, true);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
rootInstance.prevFiber = fiber;
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
let _overrideProps: null | ReactRenderer['overrideProps'] = null;
|
|
926
|
+
let _overrideHookState: null | ReactRenderer['overrideHookState'] = null;
|
|
927
|
+
let _overrideContext: null | ReactRenderer['overrideContext'] = null;
|
|
928
|
+
|
|
929
|
+
export const injectOverrideMethods = () => {
|
|
930
|
+
if (!hasRDTHook()) return null;
|
|
931
|
+
const rdtHook = getRDTHook();
|
|
932
|
+
if (!rdtHook?.renderers) return null;
|
|
933
|
+
|
|
934
|
+
if (_overrideProps || _overrideHookState || _overrideContext) {
|
|
935
|
+
return {
|
|
936
|
+
overrideContext: _overrideContext,
|
|
937
|
+
overrideHookState: _overrideHookState,
|
|
938
|
+
overrideProps: _overrideProps,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
for (const [, renderer] of Array.from(rdtHook.renderers)) {
|
|
943
|
+
try {
|
|
944
|
+
if (_overrideHookState) {
|
|
945
|
+
const prevOverrideHookState = _overrideHookState;
|
|
946
|
+
_overrideHookState = (
|
|
947
|
+
fiber: Fiber,
|
|
948
|
+
id: string,
|
|
949
|
+
path: string[],
|
|
950
|
+
value: unknown,
|
|
951
|
+
) => {
|
|
952
|
+
let current = fiber.memoizedState;
|
|
953
|
+
for (let i = 0; i < Number(id); i++) {
|
|
954
|
+
if (!current?.next) break;
|
|
955
|
+
current = current.next;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (current?.queue) {
|
|
959
|
+
const queue = current.queue;
|
|
960
|
+
if (isPOJO(queue) && 'dispatch' in queue) {
|
|
961
|
+
const dispatch = queue.dispatch as (value: unknown) => void;
|
|
962
|
+
dispatch(value);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
prevOverrideHookState(fiber, id, path, value);
|
|
968
|
+
renderer.overrideHookState?.(fiber, id, path, value);
|
|
969
|
+
};
|
|
970
|
+
} else if (renderer.overrideHookState) {
|
|
971
|
+
_overrideHookState = renderer.overrideHookState;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (_overrideProps) {
|
|
975
|
+
const prevOverrideProps = _overrideProps;
|
|
976
|
+
_overrideProps = (
|
|
977
|
+
fiber: Fiber,
|
|
978
|
+
path: Array<string>,
|
|
979
|
+
value: unknown,
|
|
980
|
+
) => {
|
|
981
|
+
prevOverrideProps(fiber, path, value);
|
|
982
|
+
renderer.overrideProps?.(fiber, path, value);
|
|
983
|
+
};
|
|
984
|
+
} else if (renderer.overrideProps) {
|
|
985
|
+
_overrideProps = renderer.overrideProps;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
_overrideContext = (
|
|
989
|
+
fiber: Fiber,
|
|
990
|
+
contextType: unknown,
|
|
991
|
+
path: string[],
|
|
992
|
+
value: unknown,
|
|
993
|
+
) => {
|
|
994
|
+
let current: Fiber | null = fiber;
|
|
995
|
+
while (current) {
|
|
996
|
+
const type = current.type as { Provider?: unknown };
|
|
997
|
+
if (type === contextType || type?.Provider === contextType) {
|
|
998
|
+
if (_overrideProps) {
|
|
999
|
+
_overrideProps(current, ['value', ...path], value);
|
|
1000
|
+
if (current.alternate) {
|
|
1001
|
+
_overrideProps(current.alternate, ['value', ...path], value);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
current = current.return;
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
} catch {
|
|
1010
|
+
/**/
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
const isPOJO = (maybePOJO: unknown): maybePOJO is Record<string, unknown> => {
|
|
1016
|
+
return (
|
|
1017
|
+
Object.prototype.toString.call(maybePOJO) === '[object Object]' &&
|
|
1018
|
+
(Object.getPrototypeOf(maybePOJO) === Object.prototype ||
|
|
1019
|
+
Object.getPrototypeOf(maybePOJO) === null)
|
|
1020
|
+
);
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
const buildPathsFromValue = (
|
|
1024
|
+
maybePOJO: Record<string, unknown>,
|
|
1025
|
+
basePath: string[] = [],
|
|
1026
|
+
): Array<{ path: string[]; value: unknown }> => {
|
|
1027
|
+
if (!isPOJO(maybePOJO)) {
|
|
1028
|
+
return [{ path: basePath, value: maybePOJO }];
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const paths: Array<{ path: string[]; value: unknown }> = [];
|
|
1032
|
+
|
|
1033
|
+
for (const key in maybePOJO) {
|
|
1034
|
+
const value = maybePOJO[key];
|
|
1035
|
+
const path = basePath.concat(key);
|
|
1036
|
+
|
|
1037
|
+
if (isPOJO(value)) {
|
|
1038
|
+
paths.push(...buildPathsFromValue(value, path));
|
|
1039
|
+
} else {
|
|
1040
|
+
paths.push({ path, value });
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
return paths;
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
export const overrideProps = (
|
|
1048
|
+
fiber: Fiber,
|
|
1049
|
+
partialValue: Record<string, unknown>,
|
|
1050
|
+
) => {
|
|
1051
|
+
injectOverrideMethods();
|
|
1052
|
+
|
|
1053
|
+
const paths = buildPathsFromValue(partialValue);
|
|
1054
|
+
|
|
1055
|
+
for (const { path, value } of paths) {
|
|
1056
|
+
try {
|
|
1057
|
+
_overrideProps?.(fiber, path, value);
|
|
1058
|
+
} catch {}
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
export const overrideHookState = (
|
|
1063
|
+
fiber: Fiber,
|
|
1064
|
+
id: number,
|
|
1065
|
+
partialValue: Record<string, unknown>,
|
|
1066
|
+
) => {
|
|
1067
|
+
injectOverrideMethods();
|
|
1068
|
+
|
|
1069
|
+
const hookId = String(id);
|
|
1070
|
+
|
|
1071
|
+
if (isPOJO(partialValue)) {
|
|
1072
|
+
const paths = buildPathsFromValue(partialValue);
|
|
1073
|
+
|
|
1074
|
+
for (const { path, value } of paths) {
|
|
1075
|
+
try {
|
|
1076
|
+
_overrideHookState?.(fiber, hookId, path, value);
|
|
1077
|
+
} catch {}
|
|
1078
|
+
}
|
|
1079
|
+
} else {
|
|
1080
|
+
try {
|
|
1081
|
+
_overrideHookState?.(fiber, hookId, [], partialValue);
|
|
1082
|
+
} catch {}
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
export const overrideContext = (
|
|
1087
|
+
fiber: Fiber,
|
|
1088
|
+
contextType: unknown,
|
|
1089
|
+
partialValue: Record<string, unknown>,
|
|
1090
|
+
) => {
|
|
1091
|
+
injectOverrideMethods();
|
|
1092
|
+
|
|
1093
|
+
if (isPOJO(partialValue)) {
|
|
1094
|
+
const paths = buildPathsFromValue(partialValue);
|
|
1095
|
+
|
|
1096
|
+
for (const { path, value } of paths) {
|
|
1097
|
+
try {
|
|
1098
|
+
_overrideContext?.(fiber, contextType, path, value);
|
|
1099
|
+
} catch {}
|
|
1100
|
+
}
|
|
1101
|
+
} else {
|
|
1102
|
+
try {
|
|
1103
|
+
_overrideContext?.(fiber, contextType, [], partialValue);
|
|
1104
|
+
} catch {}
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
export interface InstrumentationOptions {
|
|
1109
|
+
name?: string;
|
|
1110
|
+
onActive?: () => unknown;
|
|
1111
|
+
onCommitFiberRoot?: (
|
|
1112
|
+
rendererID: number,
|
|
1113
|
+
root: FiberRoot,
|
|
1114
|
+
priority: number | void,
|
|
1115
|
+
) => unknown;
|
|
1116
|
+
onCommitFiberUnmount?: (rendererID: number, fiber: Fiber) => unknown;
|
|
1117
|
+
onPostCommitFiberRoot?: (rendererID: number, root: FiberRoot) => unknown;
|
|
1118
|
+
onScheduleFiberRoot?: (
|
|
1119
|
+
rendererID: number,
|
|
1120
|
+
root: FiberRoot,
|
|
1121
|
+
children: React.ReactNode,
|
|
1122
|
+
) => unknown;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Instruments the DevTools hook.
|
|
1127
|
+
* @example
|
|
1128
|
+
* const hook = instrument({
|
|
1129
|
+
* onActive() {
|
|
1130
|
+
* console.log('initialized');
|
|
1131
|
+
* },
|
|
1132
|
+
* onCommitFiberRoot(rendererID, root) {
|
|
1133
|
+
* console.log('fiberRoot', root.current)
|
|
1134
|
+
* },
|
|
1135
|
+
* });
|
|
1136
|
+
*/
|
|
1137
|
+
export const instrument = (
|
|
1138
|
+
options: InstrumentationOptions,
|
|
1139
|
+
): ReactDevToolsGlobalHook => {
|
|
1140
|
+
const rdtHook = getRDTHook(options.onActive);
|
|
1141
|
+
|
|
1142
|
+
rdtHook._instrumentationSource = options.name ?? BIPPY_INSTRUMENTATION_STRING;
|
|
1143
|
+
|
|
1144
|
+
const prevOnCommitFiberRoot = rdtHook.onCommitFiberRoot;
|
|
1145
|
+
if (options.onCommitFiberRoot) {
|
|
1146
|
+
const nextOnCommitFiberRoot = (
|
|
1147
|
+
rendererID: number,
|
|
1148
|
+
root: FiberRoot,
|
|
1149
|
+
priority: number | void,
|
|
1150
|
+
) => {
|
|
1151
|
+
if (prevOnCommitFiberRoot === nextOnCommitFiberRoot) return;
|
|
1152
|
+
// TODO: validate whether the bottom version is more correct here
|
|
1153
|
+
// for preventing infinite loops
|
|
1154
|
+
// if (rdtHook.onCommitFiberRoot !== handler) return;
|
|
1155
|
+
prevOnCommitFiberRoot?.(rendererID, root, priority);
|
|
1156
|
+
options.onCommitFiberRoot?.(rendererID, root, priority);
|
|
1157
|
+
};
|
|
1158
|
+
rdtHook.onCommitFiberRoot = nextOnCommitFiberRoot;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const prevOnCommitFiberUnmount = rdtHook.onCommitFiberUnmount;
|
|
1162
|
+
if (options.onCommitFiberUnmount) {
|
|
1163
|
+
const handler = (rendererID: number, root: FiberRoot) => {
|
|
1164
|
+
if (rdtHook.onCommitFiberUnmount !== handler) return;
|
|
1165
|
+
prevOnCommitFiberUnmount?.(rendererID, root);
|
|
1166
|
+
options.onCommitFiberUnmount?.(rendererID, root);
|
|
1167
|
+
};
|
|
1168
|
+
rdtHook.onCommitFiberUnmount = handler;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const prevOnPostCommitFiberRoot = rdtHook.onPostCommitFiberRoot;
|
|
1172
|
+
if (options.onPostCommitFiberRoot) {
|
|
1173
|
+
const handler = (rendererID: number, root: FiberRoot) => {
|
|
1174
|
+
if (rdtHook.onPostCommitFiberRoot !== handler) return;
|
|
1175
|
+
prevOnPostCommitFiberRoot?.(rendererID, root);
|
|
1176
|
+
options.onPostCommitFiberRoot?.(rendererID, root);
|
|
1177
|
+
};
|
|
1178
|
+
rdtHook.onPostCommitFiberRoot = handler;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
return rdtHook;
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
export const getFiberFromHostInstance = <T>(hostInstance: T): Fiber | null => {
|
|
1185
|
+
const rdtHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1186
|
+
if (rdtHook?.renderers) {
|
|
1187
|
+
for (const renderer of rdtHook.renderers.values()) {
|
|
1188
|
+
try {
|
|
1189
|
+
const fiber = renderer.findFiberByHostInstance?.(hostInstance);
|
|
1190
|
+
if (fiber) return fiber;
|
|
1191
|
+
} catch {}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
if (typeof hostInstance === 'object' && hostInstance != null) {
|
|
1196
|
+
if ('_reactRootContainer' in hostInstance) {
|
|
1197
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1198
|
+
return (hostInstance._reactRootContainer as any)?._internalRoot?.current
|
|
1199
|
+
?.child;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
for (const key in hostInstance) {
|
|
1203
|
+
if (
|
|
1204
|
+
key.startsWith('__reactContainer$') ||
|
|
1205
|
+
key.startsWith('__reactInternalInstance$') ||
|
|
1206
|
+
key.startsWith('__reactFiber')
|
|
1207
|
+
) {
|
|
1208
|
+
return (hostInstance[key] || null) as Fiber | null;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return null;
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
export const INSTALL_ERROR = new Error();
|
|
1216
|
+
|
|
1217
|
+
export const _fiberRoots = new Set<FiberRoot>();
|
|
1218
|
+
|
|
1219
|
+
export const secure = (
|
|
1220
|
+
options: InstrumentationOptions,
|
|
1221
|
+
secureOptions: {
|
|
1222
|
+
dangerouslyRunInProduction?: boolean;
|
|
1223
|
+
installCheckTimeout?: number;
|
|
1224
|
+
isProduction?: boolean;
|
|
1225
|
+
minReactMajorVersion?: number;
|
|
1226
|
+
onError?: (error?: unknown) => unknown;
|
|
1227
|
+
} = {},
|
|
1228
|
+
): InstrumentationOptions => {
|
|
1229
|
+
const onActive = options.onActive;
|
|
1230
|
+
const isRDTHookInstalled = hasRDTHook();
|
|
1231
|
+
const isUsingRealReactDevtools = isRealReactDevtools();
|
|
1232
|
+
const isUsingReactRefresh = isReactRefresh();
|
|
1233
|
+
let timeout: number | undefined;
|
|
1234
|
+
let isDevelopment = !secureOptions.isProduction;
|
|
1235
|
+
|
|
1236
|
+
options.onActive = () => {
|
|
1237
|
+
clearTimeout(timeout);
|
|
1238
|
+
let isSecure = true;
|
|
1239
|
+
try {
|
|
1240
|
+
const rdtHook = getRDTHook();
|
|
1241
|
+
|
|
1242
|
+
for (const renderer of rdtHook.renderers.values()) {
|
|
1243
|
+
const [majorVersion] = renderer.version.split('.');
|
|
1244
|
+
if (Number(majorVersion) < (secureOptions.minReactMajorVersion ?? 17)) {
|
|
1245
|
+
isSecure = false;
|
|
1246
|
+
}
|
|
1247
|
+
const buildType = detectReactBuildType(renderer);
|
|
1248
|
+
if (buildType === 'development') {
|
|
1249
|
+
isDevelopment = true;
|
|
1250
|
+
} else if (!secureOptions.dangerouslyRunInProduction) {
|
|
1251
|
+
isSecure = false;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
} catch (err) {
|
|
1255
|
+
secureOptions.onError?.(err);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (!isSecure) {
|
|
1259
|
+
options.onCommitFiberRoot = undefined;
|
|
1260
|
+
options.onCommitFiberUnmount = undefined;
|
|
1261
|
+
options.onPostCommitFiberRoot = undefined;
|
|
1262
|
+
options.onActive = undefined;
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
onActive?.();
|
|
1266
|
+
|
|
1267
|
+
try {
|
|
1268
|
+
const onCommitFiberRoot = options.onCommitFiberRoot;
|
|
1269
|
+
if (onCommitFiberRoot) {
|
|
1270
|
+
options.onCommitFiberRoot = (rendererID, root, priority) => {
|
|
1271
|
+
if (!_fiberRoots.has(root)) {
|
|
1272
|
+
_fiberRoots.add(root);
|
|
1273
|
+
}
|
|
1274
|
+
try {
|
|
1275
|
+
onCommitFiberRoot(rendererID, root, priority);
|
|
1276
|
+
} catch (err) {
|
|
1277
|
+
secureOptions.onError?.(err);
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const onCommitFiberUnmount = options.onCommitFiberUnmount;
|
|
1283
|
+
if (onCommitFiberUnmount) {
|
|
1284
|
+
options.onCommitFiberUnmount = (rendererID, root) => {
|
|
1285
|
+
try {
|
|
1286
|
+
onCommitFiberUnmount(rendererID, root);
|
|
1287
|
+
} catch (err) {
|
|
1288
|
+
secureOptions.onError?.(err);
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const onPostCommitFiberRoot = options.onPostCommitFiberRoot;
|
|
1294
|
+
if (onPostCommitFiberRoot) {
|
|
1295
|
+
options.onPostCommitFiberRoot = (rendererID, root) => {
|
|
1296
|
+
try {
|
|
1297
|
+
onPostCommitFiberRoot(rendererID, root);
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
secureOptions.onError?.(err);
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
secureOptions.onError?.(err);
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
if (
|
|
1309
|
+
!isRDTHookInstalled &&
|
|
1310
|
+
!isUsingRealReactDevtools &&
|
|
1311
|
+
!isUsingReactRefresh
|
|
1312
|
+
) {
|
|
1313
|
+
timeout = setTimeout(() => {
|
|
1314
|
+
if (isDevelopment) {
|
|
1315
|
+
secureOptions.onError?.(INSTALL_ERROR);
|
|
1316
|
+
}
|
|
1317
|
+
stop();
|
|
1318
|
+
}, secureOptions.installCheckTimeout ?? 100) as unknown as number;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
return options;
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
export * from './rdt-hook.js';
|
|
1325
|
+
export type * from './types.js';
|