[react-devtools-facade] 1/ scaffold package + installFacade building β¦
[react-devtools-facade] 1/ scaffold package + installFacade building block (#36593) Introduces **`react-devtools-facade`**, a private, source-only package of building blocks for React runtime state introspection. Integrates with Reconciler through React DevTools global hook. ### Plan The facade is the shared, low-level layer that public, MCP-serverβspecific packages build on: - **`react-devtools-facade` (private)** β installs the React DevTools global hook and exposes a small, framework-agnostic API: install the hook, then assemble a set of tools from the returned handle. It installs *no* tool globals and makes no decision about how tools are surfaced. - **Integration packages (public)**, e.g. `react-devtools-cdt-mcp` β compose the facade's tools and target a specific MCP server (chrome-devtools-mcp first). This keeps all runtime-introspection logic in one reusable place while each integration owns its own protocol, packaging, serialisation and globals. ### This PR - `installFacade(target = globalThis): Facade` β installs **only** `__REACT_DEVTOOLS_GLOBAL_HOOK__` (the global React looks for at init) and returns a `Facade` handle `{hook, fiberRoots, rendererInternals, profilingState}`. The hook tracks fiber roots on commit and forwards commits to the profiling state when a session is active; building blocks read from the returned handle and never touch globals. - Guards against double-install (mixing with the full DevTools backend). *Temporary*, will later add compatibility for the RDT extension scenario. - Package scaffold: `package.json` (private, source-only), `index.js`, `README.md`. - Excluded from the general jest runners; runs under the build-devtools project, matching `react-devtools-shared` / `react-devtools-extensions`. > Tool building blocks and `createTools(facade)` land in the follow-ups.
| | 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 | +// Per-renderer internal constants, initialized at inject() time. Building | | 24 | +// blocks read these to translate fibers into human-readable output. | | 25 | +export type RendererInternals = { | | 26 | + getDisplayNameForFiber: getDisplayNameForFiberType, | | 27 | + ReactTypeOfWork: WorkTagMap, | | 28 | + ReactPriorityLevels: ReactPriorityLevelsType, | | 29 | + currentDispatcherRef: CurrentDispatcherRef, | | 30 | +}; | | 31 | + | | 32 | +// Profiling session state, shared between the hook (which records commits) and | | 33 | +// the profiler building blocks (which start/stop sessions and read results). | | 34 | +export type ProfilingState = { | | 35 | + isActive: boolean, | | 36 | + currentTraceName: string | null, | | 37 | + traces: Map , | | 38 | + onCommit: | | 39 | + | (( | | 40 | + rendererID: number, | | 41 | + root: FiberRoot, | | 42 | + schedulerPriority: number | void, | | 43 | + ) => void) | | 44 | + | null, | | 45 | + onPostCommit: ((root: FiberRoot) => void) | null, | | 46 | +}; | | 47 | + | | 48 | +// A self-contained handle over the installed DevTools hook and the runtime | | 49 | +// state it tracks. Building blocks (createTools, the tree/profiler factories) | | 50 | +// read from a Facade and never touch globals, so the integrator fully owns it. | | 51 | +export type Facade = { | | 52 | + hook: DevToolsHook, | | 53 | + fiberRoots: Map >, | | 54 | + rendererInternals: Map , | | 55 | + profilingState: ProfilingState, | | 56 | +}; | | 57 | + | | 58 | +/** | | 59 | + * Install the React DevTools facade: install `__REACT_DEVTOOLS_GLOBAL_HOOK__` | | 60 | + * on `target` (defaults to globalThis) and return a Facade handle. | | 61 | + * | | 62 | + * This installs ONLY `__REACT_DEVTOOLS_GLOBAL_HOOK__` β the global React looks | | 63 | + * for at initialization time. It does not install any tool globals: the | | 64 | + * returned Facade is passed to building blocks such as `createTools(facade)`, | | 65 | + * and the integrator decides whether to expose the resulting tools on globals. | | 66 | + * | | 67 | + * Must run BEFORE React initializes so the hook captures the first commit. | | 68 | + */ | | 69 | +export function installFacade(target?: any = globalThis): Facade { | | 70 | + // Guard against double-install (e.g. bundled twice or mixed with full DevTools). | | 71 | + if (target.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { | | 72 | + throw new Error( | | 73 | + 'React DevTools global hook is already installed. ' + | | 74 | + 'react-devtools-facade should not be used with any other React DevTools package.', | | 75 | + ); | | 76 | + } | | 77 | + | | 78 | + // Fiber root tracking β the only runtime state the hook maintains. | | 79 | + // onCommitFiberRoot adds/removes entries so that unmounted roots are | | 80 | + // garbage-collected. Building blocks walk from these roots on demand. | | 81 | + const fiberRoots: Map > = new Map(); | | 82 | + | | 83 | + const rendererInternals: Map = new Map(); | | 84 | + | | 85 | + const profilingState: ProfilingState = { | | 86 | + isActive: false, | | 87 | + currentTraceName: null, | | 88 | + traces: new Map(), | | 89 | + onCommit: null, | | 90 | + onPostCommit: null, | | 91 | + }; | | 92 | + | | 93 | + let registeredRenderersCount = 0; | | 94 | + | | 95 | + // $FlowFixMe[incompatible-type] the facade provides a minimal subset of DevToolsHook | | 96 | + const hook: DevToolsHook = { | | 97 | + listeners: {}, | | 98 | + rendererInterfaces: new Map(), | | 99 | + renderers: new Map(), | | 100 | + hasUnsupportedRendererAttached: false, | | 101 | + backends: new Map(), | | 102 | + emit() {}, | | 103 | + getFiberRoots(rendererID: number) { | | 104 | + let roots = fiberRoots.get(rendererID); | | 105 | + if (roots == null) { | | 106 | + roots = new Set(); | | 107 | + fiberRoots.set(rendererID, roots); | | 108 | + } | | 109 | + return roots; | | 110 | + }, | | 111 | + inject(renderer: any): number { | | 112 | + const id = registeredRenderersCount++; | | 113 | + hook.renderers.set(id, renderer); | | 114 | + // Initialize internal constants for this renderer's React version. | | 115 | + const version = renderer.reconcilerVersion || renderer.version; | | 116 | + if (version == null) { | | 117 | + console.error( | | 118 | + 'react-devtools-facade: Renderer %s has no version, internals not initialized.', | | 119 | + id, | | 120 | + ); | | 121 | + } else { | | 122 | + const {getDisplayNameForFiber, ReactTypeOfWork, ReactPriorityLevels} = | | 123 | + getInternalReactConstants(version); | | 124 | + rendererInternals.set(id, { | | 125 | + getDisplayNameForFiber, | | 126 | + ReactTypeOfWork, | | 127 | + ReactPriorityLevels, | | 128 | + currentDispatcherRef: renderer.currentDispatcherRef, | | 129 | + }); | | 130 | + } | | 131 | + return id; | | 132 | + }, | | 133 | + on() {}, | | 134 | + off() {}, | | 135 | + sub() { | | 136 | + return () => {}; | | 137 | + }, | | 138 | + supportsFiber: true, | | 139 | + supportsFlight: true, | | 140 | + checkDCE() {}, | | 141 | + onCommitFiberRoot( | | 142 | + rendererID: number, | | 143 | + root: any, | | 144 | + schedulerPriority?: number, | | 145 | + ) { | | 146 | + // Hot path β called on every React commit. Keep minimal: just | | 147 | + // add or remove the root so building blocks can find it later. | | 148 | + const mountedRoots = hook.getFiberRoots(rendererID); | | 149 | + const current = root.current; | | 150 | + const isKnownRoot = mountedRoots.has(root); | | 151 | + const isUnmounting = | | 152 | + current.memoizedState == null || current.memoizedState.element == null; | | 153 | + if (!isKnownRoot && !isUnmounting) { | | 154 | + mountedRoots.add(root); | | 155 | + } else if (isKnownRoot && isUnmounting) { | | 156 | + mountedRoots.delete(root); | | 157 | + } | | 158 | + | | 159 | + // Profiling: record commit durations when a session is active. | | 160 | + if (profilingState.isActive && profilingState.onCommit != null) { | | 161 | + profilingState.onCommit(rendererID, root, schedulerPriority); | | 162 | + } | | 163 | + }, | | 164 | + onCommitFiberUnmount() {}, | | 165 | + onPostCommitFiberRoot(rendererID: number, root: any) { | | 166 | + if (profilingState.isActive && profilingState.onPostCommit != null) { | | 167 | + profilingState.onPostCommit(root); | | 168 | + } | | 169 | + }, | | 170 | + getInternalModuleRanges(): Array { | | 171 | + return []; | | 172 | + }, | | 173 | + registerInternalModuleStart() {}, | | 174 | + registerInternalModuleStop() {}, | | 175 | + }; | | 176 | + | | 177 | + Object.defineProperty(target, '__REACT_DEVTOOLS_GLOBAL_HOOK__', { | | 178 | + configurable: __DEV__, | | 179 | + enumerable: false, | | 180 | + get() { | | 181 | + return hook; | | 182 | + }, | | 183 | + }); | | 184 | + | | 185 | + return {hook, fiberRoots, rendererInternals, profilingState}; | | 186 | +} | 0 commit comments
Comments
No comments yet. Start the discussion.