@samitouri / QOS-React-2 / commits / 77df6e1db1

[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.

Ruslan Lesiutin committed Jun 17, 2026 at 10:16 UTC 77df6e1db10c55bab61c064f4e74fa3524e0200d
11 files changed +377
.eslintrc.js
+1
@@ -336,6 +336,7 @@ module.exports = {
336 'packages/react-test-renderer/**/*.js',
337 'packages/react-debug-tools/**/*.js',
338 'packages/react-devtools-extensions/**/*.js',
339 + 'packages/react-devtools-facade/**/*.js',
340 'packages/react-devtools-timeline/**/*.js',
341 'packages/react-native-renderer/**/*.js',
342 'packages/eslint-plugin-react-hooks/**/*.js',
packages/react-devtools-facade/README.md new
+37
@@ -0,0 +1,37 @@
1 +# react-devtools-facade
2 +
3 +Experimental, private package that defines building blocks for querying React runtime state.
4 +
5 +The facade installs the `__REACT_DEVTOOLS_GLOBAL_HOOK__` that React looks for at
6 +initialization time — enabling fiber-root tracking without the overhead of the
7 +full DevTools backend — and exposes a small, framework-agnostic library API that
8 +integrators compose into tools.
9 +
10 +This package is intentionally low-level. It does **not** install any tool
11 +globals and it does **not** decide how tools are surfaced. It installs the hook,
12 +tracks fiber roots, and hands back building blocks; the integrator (for example,
13 +a `chrome-devtools-mcp` integration) decides everything else — including whether
14 +to expose anything else on globals.
15 +
16 +## API
17 +
18 +### `installFacade(target = globalThis): Facade`
19 +
20 +Installs `__REACT_DEVTOOLS_GLOBAL_HOOK__` on `target` and returns a `Facade`
21 +handle holding the hook plus the runtime state it tracks (`fiberRoots`,
22 +`rendererInternals`, `profilingState`). Building blocks read from the returned
23 +`Facade`; they never reach for globals.
24 +
25 +Call this **before** React initializes so the hook captures the first commit:
26 +
27 +```js
28 +import {installFacade} from 'react-devtools-facade';
29 +
30 +const facade = installFacade();
31 +// ...load React, render your app...
32 +```
33 +
34 +`installFacade` installs **only** the DevTools hook. It does not install
35 +`__REACT_TOOLS__`, `__REACT_LLM_TOOLS__`, or any other global. Once the tool
36 +building blocks land, an integrator composes them from the returned `Facade` and
37 +decides whether to expose anything on globals.
packages/react-devtools-facade/index.js new
+10
@@ -0,0 +1,10 @@
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 +export * from './src/DevToolsFacade';
packages/react-devtools-facade/package.json new
+13
@@ -0,0 +1,13 @@
1 +{
2 + "name": "react-devtools-facade",
3 + "version": "0.0.0",
4 + "private": true,
5 + "description": "Building blocks for querying React runtime state",
6 + "license": "MIT",
7 + "main": "index.js",
8 + "repository": {
9 + "type": "git",
10 + "url": "https://github.com/facebook/react.git",
11 + "directory": "packages/react-devtools-facade"
12 + }
13 +}
packages/react-devtools-facade/src/DevToolsFacade.js new
+186
@@ -0,0 +1,186 @@
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<string, any>,
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<number, Set<FiberRoot>>,
54 + rendererInternals: Map<number, RendererInternals>,
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<number, Set<FiberRoot>> = new Map();
82 +
83 + const rendererInternals: Map<number, RendererInternals> = 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<[string, string]> {
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 +}
packages/react-devtools-facade/src/__tests__/DevToolsFacade-test.js new
+125
@@ -0,0 +1,125 @@
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 +
8 +'use strict';
9 +
10 +let installFacade;
11 +let facade;
12 +let React;
13 +let ReactDOMClient;
14 +let act;
15 +let container;
16 +
17 +describe('react-devtools-facade', () => {
18 + beforeEach(() => {
19 + jest.resetModules();
20 + global.IS_REACT_ACT_ENVIRONMENT = true;
21 +
22 + // The hook lives on globalThis, which jsdom shares across tests in this
23 + // file, so a leftover hook would make installFacade() below throw. Remove
24 + // it for a clean slate. (The facade never installs any other global, which
25 + // the "does not install any tool globals" test verifies.)
26 + delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
27 +
28 + // Install the facade BEFORE React so the hook captures the first commit.
29 + // Import through the package entry point to exercise the public surface.
30 + installFacade = require('../../index').installFacade;
31 + facade = installFacade();
32 +
33 + React = require('react');
34 + ReactDOMClient = require('react-dom/client');
35 + act = React.act;
36 +
37 + container = document.createElement('div');
38 + document.body.appendChild(container);
39 + });
40 +
41 + afterEach(() => {
42 + document.body.removeChild(container);
43 + container = null;
44 + });
45 +
46 + it('installs __REACT_DEVTOOLS_GLOBAL_HOOK__ on globalThis', () => {
47 + expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(facade.hook);
48 + });
49 +
50 + it('returns a Facade handle exposing the hook and tracked state', () => {
51 + expect(facade.hook).toBe(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__);
52 + expect(facade.fiberRoots).toBeInstanceOf(Map);
53 + expect(facade.rendererInternals).toBeInstanceOf(Map);
54 + expect(facade.profilingState).toEqual({
55 + isActive: false,
56 + currentTraceName: null,
57 + traces: expect.any(Map),
58 + onCommit: null,
59 + onPostCommit: null,
60 + });
61 + });
62 +
63 + it('does not install any tool globals (the integrator decides those)', () => {
64 + expect(globalThis.__REACT_TOOLS__).toBeUndefined();
65 + expect(globalThis.__REACT_LLM_TOOLS__).toBeUndefined();
66 + });
67 +
68 + it('throws if a DevTools hook is already installed', () => {
69 + // A hook was already installed on globalThis in beforeEach.
70 + expect(() => installFacade()).toThrow(
71 + /React DevTools global hook is already installed/,
72 + );
73 + });
74 +
75 + it('installs onto an explicit target without touching globalThis', () => {
76 + const target = {};
77 + const localFacade = installFacade(target);
78 +
79 + expect(target.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(localFacade.hook);
80 + // The explicit-target facade is fully independent of the global one.
81 + expect(localFacade.hook).not.toBe(facade.hook);
82 + expect(localFacade.fiberRoots).not.toBe(facade.fiberRoots);
83 + // ...and installing onto a target does not disturb the global hook.
84 + expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(facade.hook);
85 + });
86 +
87 + it('records the renderer and its fiber root on mount', () => {
88 + function Greeting() {
89 + return <div>Hello</div>;
90 + }
91 +
92 + act(() => {
93 + ReactDOMClient.createRoot(container).render(<Greeting />);
94 + });
95 +
96 + // React injected a renderer: its internal constants were captured...
97 + expect(facade.rendererInternals.size).toBeGreaterThan(0);
98 + // ...and the hook recorded the committed root in facade.fiberRoots.
99 + let totalRoots = 0;
100 + facade.fiberRoots.forEach(roots => {
101 + totalRoots += roots.size;
102 + });
103 + expect(totalRoots).toBeGreaterThan(0);
104 + });
105 +
106 + it('removes unmounted roots from tracking', () => {
107 + function App() {
108 + return <div>hello</div>;
109 + }
110 +
111 + const root = ReactDOMClient.createRoot(container);
112 + act(() => {
113 + root.render(<App />);
114 + });
115 +
116 + const rendererID = Array.from(facade.hook.renderers.keys())[0];
117 + expect(facade.hook.getFiberRoots(rendererID).size).toBeGreaterThan(0);
118 +
119 + act(() => {
120 + root.unmount();
121 + });
122 +
123 + expect(facade.hook.getFiberRoots(rendererID).size).toBe(0);
124 + });
125 +});
scripts/jest/config.build.js
+1
@@ -60,6 +60,7 @@ module.exports = Object.assign({}, baseConfig, {
60 modulePathIgnorePatterns: [
61 ...baseConfig.modulePathIgnorePatterns,
62 'packages/react-devtools-extensions',
63 + 'packages/react-devtools-facade',
64 'packages/react-devtools-shared',
65 ],
66 // Don't run bundle tests on -test.internal.* files
scripts/jest/config.source-persistent.js
+1
@@ -6,6 +6,7 @@ module.exports = Object.assign({}, baseConfig, {
6 modulePathIgnorePatterns: [
7 ...baseConfig.modulePathIgnorePatterns,
8 'packages/react-devtools-extensions',
9 + 'packages/react-devtools-facade',
10 'packages/react-devtools-shared',
11 'ReactIncrementalPerf',
12 'ReactIncrementalUpdatesMinimalism',
scripts/jest/config.source-www.js
+1
@@ -6,6 +6,7 @@ module.exports = Object.assign({}, baseConfig, {
6 modulePathIgnorePatterns: [
7 ...baseConfig.modulePathIgnorePatterns,
8 'packages/react-devtools-extensions',
9 + 'packages/react-devtools-facade',
10 'packages/react-devtools-shared',
11 ],
12 setupFiles: [
scripts/jest/config.source-xplat.js
+1
@@ -6,6 +6,7 @@ module.exports = Object.assign({}, baseConfig, {
6 modulePathIgnorePatterns: [
7 ...baseConfig.modulePathIgnorePatterns,
8 'packages/react-devtools-extensions',
9 + 'packages/react-devtools-facade',
10 'packages/react-devtools-shared',
11 'ReactIncrementalPerf',
12 'ReactIncrementalUpdatesMinimalism',
scripts/jest/config.source.js
+1
@@ -6,6 +6,7 @@ module.exports = Object.assign({}, baseConfig, {
6 modulePathIgnorePatterns: [
7 ...baseConfig.modulePathIgnorePatterns,
8 'packages/react-devtools-extensions',
9 + 'packages/react-devtools-facade',
10 'packages/react-devtools-shared',
11 ],
12 setupFiles: [