/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import type { DevToolsHook, WorkTagMap, CurrentDispatcherRef, } from 'react-devtools-shared/src/backend/types'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; import type { getDisplayNameForFiberType, ReactPriorityLevelsType, } from 'react-devtools-shared/src/backend/fiber/shared/DevToolsFiberInternalReactConstants'; import {getInternalReactConstants} from 'react-devtools-shared/src/backend/fiber/shared/DevToolsFiberInternalReactConstants'; // Re-export the tools assembler so the full building-block API is available // from the package entry point (index.js re-exports this module). export {createTools} from './DevToolsFacadeTools'; export type {Tools} from './DevToolsFacadeTools'; // Per-renderer internal constants, initialized at inject() time. Building // blocks read these to translate fibers into human-readable output. export type RendererInternals = { getDisplayNameForFiber: getDisplayNameForFiberType, ReactTypeOfWork: WorkTagMap, ReactPriorityLevels: ReactPriorityLevelsType, currentDispatcherRef: CurrentDispatcherRef, }; // Profiling session state, shared between the hook (which records commits) and // the profiler building blocks (which start/stop sessions and read results). export type ProfilingState = { isActive: boolean, currentTraceName: string | null, traces: Map, onCommit: | (( rendererID: number, root: FiberRoot, schedulerPriority: number | void, ) => void) | null, onPostCommit: ((root: FiberRoot) => void) | null, }; // A self-contained handle over the installed DevTools hook and the runtime // state it tracks. Building blocks (createTools, the tree/profiler factories) // read from a Facade and never touch globals, so the integrator fully owns it. export type Facade = { hook: DevToolsHook, fiberRoots: Map>, rendererInternals: Map, profilingState: ProfilingState, }; // Initialize per-renderer internal constants for a renderer registered with the // hook. Shared by the installed hook's inject() and the attach path. function initializeRendererInternals( rendererInternals: Map, id: number, renderer: any, ): void { const version = renderer.reconcilerVersion || renderer.version; if (version == null) { console.error( 'react-devtools-facade: Renderer %s has no version, internals not initialized.', id, ); return; } const {getDisplayNameForFiber, ReactTypeOfWork, ReactPriorityLevels} = getInternalReactConstants(version); rendererInternals.set(id, { getDisplayNameForFiber, ReactTypeOfWork, ReactPriorityLevels, currentDispatcherRef: renderer.currentDispatcherRef, }); } // Record a commit: keep fiberRoots in sync (add new roots, drop unmounted ones) // and drive a profiling session when one is active. Shared by the installed // hook's onCommitFiberRoot and the attach path's wrapper. function recordCommitFiberRoot( fiberRoots: Map>, profilingState: ProfilingState, rendererID: number, root: any, schedulerPriority?: number, ): void { let mountedRoots = fiberRoots.get(rendererID); if (mountedRoots == null) { mountedRoots = new Set(); fiberRoots.set(rendererID, mountedRoots); } const current = root.current; const isKnownRoot = mountedRoots.has(root); const isUnmounting = current.memoizedState == null || current.memoizedState.element == null; if (!isKnownRoot && !isUnmounting) { mountedRoots.add(root); } else if (isKnownRoot && isUnmounting) { mountedRoots.delete(root); } if (profilingState.isActive && profilingState.onCommit != null) { profilingState.onCommit(rendererID, root, schedulerPriority); } } // Attach to a DevTools hook that is already installed on the page — for example // the React DevTools browser extension. Rather than replacing it (React would // ignore a second hook), read the renderers and fiber roots it is already // tracking, then wrap inject / onCommitFiberRoot / onPostCommitFiberRoot so // future renderers, commits, and passive passes also feed the facade's state. // The existing hook's own bookkeeping is preserved — we always call through to // it first. function attachToExistingHook( hook: any, fiberRoots: Map>, rendererInternals: Map, profilingState: ProfilingState, ): void { // Back-fill renderers and roots registered before we attached (React may have // initialized first). if (hook.renderers instanceof Map) { hook.renderers.forEach((renderer: any, id: number) => { if (!rendererInternals.has(id)) { initializeRendererInternals(rendererInternals, id, renderer); } if (typeof hook.getFiberRoots === 'function') { let roots = fiberRoots.get(id); if (roots == null) { roots = new Set(); fiberRoots.set(id, roots); } // Alias to a const so the non-null refinement survives into the closure. const mountedRoots = roots; hook.getFiberRoots(id).forEach((root: FiberRoot) => { mountedRoots.add(root); }); } }); } const originalInject = hook.inject; hook.inject = function inject(renderer: any, ...rest: Array): number { const id = originalInject.call(hook, renderer, ...rest); if (typeof id === 'number') { initializeRendererInternals(rendererInternals, id, renderer); } return id; }; const originalOnCommitFiberRoot = hook.onCommitFiberRoot; hook.onCommitFiberRoot = function onCommitFiberRoot( rendererID: number, root: any, schedulerPriority?: number, ...rest: Array ) { if (typeof originalOnCommitFiberRoot === 'function') { originalOnCommitFiberRoot.call( hook, rendererID, root, schedulerPriority, ...rest, ); } recordCommitFiberRoot( fiberRoots, profilingState, rendererID, root, schedulerPriority, ); }; const originalOnPostCommitFiberRoot = hook.onPostCommitFiberRoot; hook.onPostCommitFiberRoot = function onPostCommitFiberRoot( rendererID: number, root: any, ...rest: Array ) { if (typeof originalOnPostCommitFiberRoot === 'function') { originalOnPostCommitFiberRoot.call(hook, rendererID, root, ...rest); } if (profilingState.isActive && profilingState.onPostCommit != null) { profilingState.onPostCommit(root); } }; } /** * Install the React DevTools facade and return a Facade handle. * * If `__REACT_DEVTOOLS_GLOBAL_HOOK__` is not yet present, this installs the * facade's own minimal hook (the global React looks for at init). If a hook is * already installed — e.g. the user has the React DevTools browser extension — * the facade attaches to that hook instead of installing a second one. * * Either way the returned Facade exposes the same `{hook, fiberRoots, * rendererInternals, profilingState}` that building blocks such as * `createTools(facade)` read from. Install before React initializes so the first * commit is captured; when attaching, roots committed before attach are * back-filled from the existing hook. */ export function installFacade(target?: any = globalThis): Facade { const fiberRoots: Map> = new Map(); const rendererInternals: Map = new Map(); const profilingState: ProfilingState = { isActive: false, currentTraceName: null, traces: new Map(), onCommit: null, onPostCommit: null, }; // A hook is already installed (e.g. the React DevTools extension). Attach to // it rather than replacing it. const existingHook = target.__REACT_DEVTOOLS_GLOBAL_HOOK__; if (existingHook != null) { attachToExistingHook( existingHook, fiberRoots, rendererInternals, profilingState, ); return {hook: existingHook, fiberRoots, rendererInternals, profilingState}; } let registeredRenderersCount = 0; // $FlowFixMe[incompatible-type] the facade provides a minimal subset of DevToolsHook const hook: DevToolsHook = { listeners: {}, rendererInterfaces: new Map(), renderers: new Map(), hasUnsupportedRendererAttached: false, backends: new Map(), emit() {}, getFiberRoots(rendererID: number) { let roots = fiberRoots.get(rendererID); if (roots == null) { roots = new Set(); fiberRoots.set(rendererID, roots); } return roots; }, inject(renderer: any): number { const id = registeredRenderersCount++; hook.renderers.set(id, renderer); initializeRendererInternals(rendererInternals, id, renderer); return id; }, on() {}, off() {}, sub() { return () => {}; }, supportsFiber: true, supportsFlight: true, checkDCE() {}, onCommitFiberRoot( rendererID: number, root: any, schedulerPriority?: number, ) { recordCommitFiberRoot( fiberRoots, profilingState, rendererID, root, schedulerPriority, ); }, onCommitFiberUnmount() {}, onPostCommitFiberRoot(rendererID: number, root: any) { if (profilingState.isActive && profilingState.onPostCommit != null) { profilingState.onPostCommit(root); } }, }; Object.defineProperty(target, '__REACT_DEVTOOLS_GLOBAL_HOOK__', { configurable: __DEV__, enumerable: false, get() { return hook; }, }); return {hook, fiberRoots, rendererInternals, profilingState}; }