main
js 302 lines 9.66 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {
11 DevToolsHook,
12 WorkTagMap,
13 CurrentDispatcherRef,
14 } from 'react-devtools-shared/src/backend/types';
15 import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
16 import type {
17 getDisplayNameForFiberType,
18 ReactPriorityLevelsType,
19 } from 'react-devtools-shared/src/backend/fiber/shared/DevToolsFiberInternalReactConstants';
20
21 import {getInternalReactConstants} from 'react-devtools-shared/src/backend/fiber/shared/DevToolsFiberInternalReactConstants';
22
23 // Re-export the tools assembler so the full building-block API is available
24 // from the package entry point (index.js re-exports this module).
25 export {createTools} from './DevToolsFacadeTools';
26 export type {Tools} from './DevToolsFacadeTools';
27
28 // Per-renderer internal constants, initialized at inject() time. Building
29 // blocks read these to translate fibers into human-readable output.
30 export type RendererInternals = {
31 getDisplayNameForFiber: getDisplayNameForFiberType,
32 ReactTypeOfWork: WorkTagMap,
33 ReactPriorityLevels: ReactPriorityLevelsType,
34 currentDispatcherRef: CurrentDispatcherRef,
35 };
36
37 // Profiling session state, shared between the hook (which records commits) and
38 // the profiler building blocks (which start/stop sessions and read results).
39 export type ProfilingState = {
40 isActive: boolean,
41 currentTraceName: string | null,
42 traces: Map<string, any>,
43 onCommit:
44 | ((
45 rendererID: number,
46 root: FiberRoot,
47 schedulerPriority: number | void,
48 ) => void)
49 | null,
50 onPostCommit: ((root: FiberRoot) => void) | null,
51 };
52
53 // A self-contained handle over the installed DevTools hook and the runtime
54 // state it tracks. Building blocks (createTools, the tree/profiler factories)
55 // read from a Facade and never touch globals, so the integrator fully owns it.
56 export type Facade = {
57 hook: DevToolsHook,
58 fiberRoots: Map<number, Set<FiberRoot>>,
59 rendererInternals: Map<number, RendererInternals>,
60 profilingState: ProfilingState,
61 };
62
63 // Initialize per-renderer internal constants for a renderer registered with the
64 // hook. Shared by the installed hook's inject() and the attach path.
65 function initializeRendererInternals(
66 rendererInternals: Map<number, RendererInternals>,
67 id: number,
68 renderer: any,
69 ): void {
70 const version = renderer.reconcilerVersion || renderer.version;
71 if (version == null) {
72 console.error(
73 'react-devtools-facade: Renderer %s has no version, internals not initialized.',
74 id,
75 );
76 return;
77 }
78 const {getDisplayNameForFiber, ReactTypeOfWork, ReactPriorityLevels} =
79 getInternalReactConstants(version);
80 rendererInternals.set(id, {
81 getDisplayNameForFiber,
82 ReactTypeOfWork,
83 ReactPriorityLevels,
84 currentDispatcherRef: renderer.currentDispatcherRef,
85 });
86 }
87
88 // Record a commit: keep fiberRoots in sync (add new roots, drop unmounted ones)
89 // and drive a profiling session when one is active. Shared by the installed
90 // hook's onCommitFiberRoot and the attach path's wrapper.
91 function recordCommitFiberRoot(
92 fiberRoots: Map<number, Set<FiberRoot>>,
93 profilingState: ProfilingState,
94 rendererID: number,
95 root: any,
96 schedulerPriority?: number,
97 ): void {
98 let mountedRoots = fiberRoots.get(rendererID);
99 if (mountedRoots == null) {
100 mountedRoots = new Set();
101 fiberRoots.set(rendererID, mountedRoots);
102 }
103 const current = root.current;
104 const isKnownRoot = mountedRoots.has(root);
105 const isUnmounting =
106 current.memoizedState == null || current.memoizedState.element == null;
107 if (!isKnownRoot && !isUnmounting) {
108 mountedRoots.add(root);
109 } else if (isKnownRoot && isUnmounting) {
110 mountedRoots.delete(root);
111 }
112
113 if (profilingState.isActive && profilingState.onCommit != null) {
114 profilingState.onCommit(rendererID, root, schedulerPriority);
115 }
116 }
117
118 // Attach to a DevTools hook that is already installed on the page — for example
119 // the React DevTools browser extension. Rather than replacing it (React would
120 // ignore a second hook), read the renderers and fiber roots it is already
121 // tracking, then wrap inject / onCommitFiberRoot / onPostCommitFiberRoot so
122 // future renderers, commits, and passive passes also feed the facade's state.
123 // The existing hook's own bookkeeping is preserved — we always call through to
124 // it first.
125 function attachToExistingHook(
126 hook: any,
127 fiberRoots: Map<number, Set<FiberRoot>>,
128 rendererInternals: Map<number, RendererInternals>,
129 profilingState: ProfilingState,
130 ): void {
131 // Back-fill renderers and roots registered before we attached (React may have
132 // initialized first).
133 if (hook.renderers instanceof Map) {
134 hook.renderers.forEach((renderer: any, id: number) => {
135 if (!rendererInternals.has(id)) {
136 initializeRendererInternals(rendererInternals, id, renderer);
137 }
138 if (typeof hook.getFiberRoots === 'function') {
139 let roots = fiberRoots.get(id);
140 if (roots == null) {
141 roots = new Set();
142 fiberRoots.set(id, roots);
143 }
144 // Alias to a const so the non-null refinement survives into the closure.
145 const mountedRoots = roots;
146 hook.getFiberRoots(id).forEach((root: FiberRoot) => {
147 mountedRoots.add(root);
148 });
149 }
150 });
151 }
152
153 const originalInject = hook.inject;
154 hook.inject = function inject(renderer: any, ...rest: Array<mixed>): number {
155 const id = originalInject.call(hook, renderer, ...rest);
156 if (typeof id === 'number') {
157 initializeRendererInternals(rendererInternals, id, renderer);
158 }
159 return id;
160 };
161
162 const originalOnCommitFiberRoot = hook.onCommitFiberRoot;
163 hook.onCommitFiberRoot = function onCommitFiberRoot(
164 rendererID: number,
165 root: any,
166 schedulerPriority?: number,
167 ...rest: Array<mixed>
168 ) {
169 if (typeof originalOnCommitFiberRoot === 'function') {
170 originalOnCommitFiberRoot.call(
171 hook,
172 rendererID,
173 root,
174 schedulerPriority,
175 ...rest,
176 );
177 }
178 recordCommitFiberRoot(
179 fiberRoots,
180 profilingState,
181 rendererID,
182 root,
183 schedulerPriority,
184 );
185 };
186
187 const originalOnPostCommitFiberRoot = hook.onPostCommitFiberRoot;
188 hook.onPostCommitFiberRoot = function onPostCommitFiberRoot(
189 rendererID: number,
190 root: any,
191 ...rest: Array<mixed>
192 ) {
193 if (typeof originalOnPostCommitFiberRoot === 'function') {
194 originalOnPostCommitFiberRoot.call(hook, rendererID, root, ...rest);
195 }
196 if (profilingState.isActive && profilingState.onPostCommit != null) {
197 profilingState.onPostCommit(root);
198 }
199 };
200 }
201
202 /**
203 * Install the React DevTools facade and return a Facade handle.
204 *
205 * If `__REACT_DEVTOOLS_GLOBAL_HOOK__` is not yet present, this installs the
206 * facade's own minimal hook (the global React looks for at init). If a hook is
207 * already installed — e.g. the user has the React DevTools browser extension —
208 * the facade attaches to that hook instead of installing a second one.
209 *
210 * Either way the returned Facade exposes the same `{hook, fiberRoots,
211 * rendererInternals, profilingState}` that building blocks such as
212 * `createTools(facade)` read from. Install before React initializes so the first
213 * commit is captured; when attaching, roots committed before attach are
214 * back-filled from the existing hook.
215 */
216 export function installFacade(target?: any = globalThis): Facade {
217 const fiberRoots: Map<number, Set<FiberRoot>> = new Map();
218 const rendererInternals: Map<number, RendererInternals> = new Map();
219 const profilingState: ProfilingState = {
220 isActive: false,
221 currentTraceName: null,
222 traces: new Map(),
223 onCommit: null,
224 onPostCommit: null,
225 };
226
227 // A hook is already installed (e.g. the React DevTools extension). Attach to
228 // it rather than replacing it.
229 const existingHook = target.__REACT_DEVTOOLS_GLOBAL_HOOK__;
230 if (existingHook != null) {
231 attachToExistingHook(
232 existingHook,
233 fiberRoots,
234 rendererInternals,
235 profilingState,
236 );
237 return {hook: existingHook, fiberRoots, rendererInternals, profilingState};
238 }
239
240 let registeredRenderersCount = 0;
241
242 // $FlowFixMe[incompatible-type] the facade provides a minimal subset of DevToolsHook
243 const hook: DevToolsHook = {
244 listeners: {},
245 rendererInterfaces: new Map(),
246 renderers: new Map(),
247 hasUnsupportedRendererAttached: false,
248 backends: new Map(),
249 emit() {},
250 getFiberRoots(rendererID: number) {
251 let roots = fiberRoots.get(rendererID);
252 if (roots == null) {
253 roots = new Set();
254 fiberRoots.set(rendererID, roots);
255 }
256 return roots;
257 },
258 inject(renderer: any): number {
259 const id = registeredRenderersCount++;
260 hook.renderers.set(id, renderer);
261 initializeRendererInternals(rendererInternals, id, renderer);
262 return id;
263 },
264 on() {},
265 off() {},
266 sub() {
267 return () => {};
268 },
269 supportsFiber: true,
270 supportsFlight: true,
271 checkDCE() {},
272 onCommitFiberRoot(
273 rendererID: number,
274 root: any,
275 schedulerPriority?: number,
276 ) {
277 recordCommitFiberRoot(
278 fiberRoots,
279 profilingState,
280 rendererID,
281 root,
282 schedulerPriority,
283 );
284 },
285 onCommitFiberUnmount() {},
286 onPostCommitFiberRoot(rendererID: number, root: any) {
287 if (profilingState.isActive && profilingState.onPostCommit != null) {
288 profilingState.onPostCommit(root);
289 }
290 },
291 };
292
293 Object.defineProperty(target, '__REACT_DEVTOOLS_GLOBAL_HOOK__', {
294 configurable: __DEV__,
295 enumerable: false,
296 get() {
297 return hook;
298 },
299 });
300
301 return {hook, fiberRoots, rendererInternals, profilingState};
302 }