| 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 | } |