Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ module.exports = {
'packages/react-test-renderer/**/*.js',
'packages/react-debug-tools/**/*.js',
'packages/react-devtools-extensions/**/*.js',
'packages/react-devtools-facade/**/*.js',
'packages/react-devtools-timeline/**/*.js',
'packages/react-native-renderer/**/*.js',
'packages/eslint-plugin-react-hooks/**/*.js',
Expand Down
37 changes: 37 additions & 0 deletions packages/react-devtools-facade/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# react-devtools-facade

Experimental, private package that defines building blocks for querying React runtime state.

The facade installs the `__REACT_DEVTOOLS_GLOBAL_HOOK__` that React looks for at
initialization time — enabling fiber-root tracking without the overhead of the
full DevTools backend — and exposes a small, framework-agnostic library API that
integrators compose into tools.

This package is intentionally low-level. It does **not** install any tool
globals and it does **not** decide how tools are surfaced. It installs the hook,
tracks fiber roots, and hands back building blocks; the integrator (for example,
a `chrome-devtools-mcp` integration) decides everything else — including whether
to expose anything else on globals.

## API

### `installFacade(target = globalThis): Facade`

Installs `__REACT_DEVTOOLS_GLOBAL_HOOK__` on `target` and returns a `Facade`
handle holding the hook plus the runtime state it tracks (`fiberRoots`,
`rendererInternals`, `profilingState`). Building blocks read from the returned
`Facade`; they never reach for globals.

Call this **before** React initializes so the hook captures the first commit:

```js
import {installFacade} from 'react-devtools-facade';

const facade = installFacade();
// ...load React, render your app...
```

`installFacade` installs **only** the DevTools hook. It does not install
`__REACT_TOOLS__`, `__REACT_LLM_TOOLS__`, or any other global. Once the tool
building blocks land, an integrator composes them from the returned `Facade` and
decides whether to expose anything on globals.
10 changes: 10 additions & 0 deletions packages/react-devtools-facade/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* 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
*/

export * from './src/DevToolsFacade';
13 changes: 13 additions & 0 deletions packages/react-devtools-facade/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "react-devtools-facade",
"version": "0.0.0",
"private": true,
"description": "Building blocks for querying React runtime state",
"license": "MIT",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",
"directory": "packages/react-devtools-facade"
}
}
186 changes: 186 additions & 0 deletions packages/react-devtools-facade/src/DevToolsFacade.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/**
* 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';

// 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<string, any>,
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<number, Set<FiberRoot>>,
rendererInternals: Map<number, RendererInternals>,
profilingState: ProfilingState,
};

/**
* Install the React DevTools facade: install `__REACT_DEVTOOLS_GLOBAL_HOOK__`
* on `target` (defaults to globalThis) and return a Facade handle.
*
* This installs ONLY `__REACT_DEVTOOLS_GLOBAL_HOOK__` — the global React looks
* for at initialization time. It does not install any tool globals: the
* returned Facade is passed to building blocks such as `createTools(facade)`,
* and the integrator decides whether to expose the resulting tools on globals.
*
* Must run BEFORE React initializes so the hook captures the first commit.
*/
export function installFacade(target?: any = globalThis): Facade {
// Guard against double-install (e.g. bundled twice or mixed with full DevTools).
if (target.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
throw new Error(
'React DevTools global hook is already installed. ' +
'react-devtools-facade should not be used with any other React DevTools package.',
);
}

// Fiber root tracking — the only runtime state the hook maintains.
// onCommitFiberRoot adds/removes entries so that unmounted roots are
// garbage-collected. Building blocks walk from these roots on demand.
const fiberRoots: Map<number, Set<FiberRoot>> = new Map();

const rendererInternals: Map<number, RendererInternals> = new Map();

const profilingState: ProfilingState = {
isActive: false,
currentTraceName: null,
traces: new Map(),
onCommit: null,
onPostCommit: null,
};

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);
// Initialize internal constants for this renderer's React version.
const version = renderer.reconcilerVersion || renderer.version;
if (version == null) {
console.error(
'react-devtools-facade: Renderer %s has no version, internals not initialized.',
id,
);
} else {
const {getDisplayNameForFiber, ReactTypeOfWork, ReactPriorityLevels} =
getInternalReactConstants(version);
rendererInternals.set(id, {
getDisplayNameForFiber,
ReactTypeOfWork,
ReactPriorityLevels,
currentDispatcherRef: renderer.currentDispatcherRef,
});
}
return id;
},
on() {},
off() {},
sub() {
return () => {};
},
supportsFiber: true,
supportsFlight: true,
checkDCE() {},
onCommitFiberRoot(
rendererID: number,
root: any,
schedulerPriority?: number,
) {
// Hot path — called on every React commit. Keep minimal: just
// add or remove the root so building blocks can find it later.
const mountedRoots = hook.getFiberRoots(rendererID);
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);
}

// Profiling: record commit durations when a session is active.
if (profilingState.isActive && profilingState.onCommit != null) {
profilingState.onCommit(rendererID, root, schedulerPriority);
}
},
onCommitFiberUnmount() {},
onPostCommitFiberRoot(rendererID: number, root: any) {
if (profilingState.isActive && profilingState.onPostCommit != null) {
profilingState.onPostCommit(root);
}
},
getInternalModuleRanges(): Array<[string, string]> {
return [];
},
registerInternalModuleStart() {},
registerInternalModuleStop() {},
};

Object.defineProperty(target, '__REACT_DEVTOOLS_GLOBAL_HOOK__', {
configurable: __DEV__,
enumerable: false,
get() {
return hook;
},
});

return {hook, fiberRoots, rendererInternals, profilingState};
}
125 changes: 125 additions & 0 deletions packages/react-devtools-facade/src/__tests__/DevToolsFacade-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* 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.
*/

'use strict';

let installFacade;
let facade;
let React;
let ReactDOMClient;
let act;
let container;

describe('react-devtools-facade', () => {
beforeEach(() => {
jest.resetModules();
global.IS_REACT_ACT_ENVIRONMENT = true;

// The hook lives on globalThis, which jsdom shares across tests in this
// file, so a leftover hook would make installFacade() below throw. Remove
// it for a clean slate. (The facade never installs any other global, which
// the "does not install any tool globals" test verifies.)
delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;

// Install the facade BEFORE React so the hook captures the first commit.
// Import through the package entry point to exercise the public surface.
installFacade = require('../../index').installFacade;
facade = installFacade();

React = require('react');
ReactDOMClient = require('react-dom/client');
act = React.act;

container = document.createElement('div');
document.body.appendChild(container);
});

afterEach(() => {
document.body.removeChild(container);
container = null;
});

it('installs __REACT_DEVTOOLS_GLOBAL_HOOK__ on globalThis', () => {
expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(facade.hook);
});

it('returns a Facade handle exposing the hook and tracked state', () => {
expect(facade.hook).toBe(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__);
expect(facade.fiberRoots).toBeInstanceOf(Map);
expect(facade.rendererInternals).toBeInstanceOf(Map);
expect(facade.profilingState).toEqual({
isActive: false,
currentTraceName: null,
traces: expect.any(Map),
onCommit: null,
onPostCommit: null,
});
});

it('does not install any tool globals (the integrator decides those)', () => {
expect(globalThis.__REACT_TOOLS__).toBeUndefined();
expect(globalThis.__REACT_LLM_TOOLS__).toBeUndefined();
});

it('throws if a DevTools hook is already installed', () => {
// A hook was already installed on globalThis in beforeEach.
expect(() => installFacade()).toThrow(
/React DevTools global hook is already installed/,
);
});

it('installs onto an explicit target without touching globalThis', () => {
const target = {};
const localFacade = installFacade(target);

expect(target.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(localFacade.hook);
// The explicit-target facade is fully independent of the global one.
expect(localFacade.hook).not.toBe(facade.hook);
expect(localFacade.fiberRoots).not.toBe(facade.fiberRoots);
// ...and installing onto a target does not disturb the global hook.
expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBe(facade.hook);
});

it('records the renderer and its fiber root on mount', () => {
function Greeting() {
return <div>Hello</div>;
}

act(() => {
ReactDOMClient.createRoot(container).render(<Greeting />);
});

// React injected a renderer: its internal constants were captured...
expect(facade.rendererInternals.size).toBeGreaterThan(0);
// ...and the hook recorded the committed root in facade.fiberRoots.
let totalRoots = 0;
facade.fiberRoots.forEach(roots => {
totalRoots += roots.size;
});
expect(totalRoots).toBeGreaterThan(0);
});

it('removes unmounted roots from tracking', () => {
function App() {
return <div>hello</div>;
}

const root = ReactDOMClient.createRoot(container);
act(() => {
root.render(<App />);
});

const rendererID = Array.from(facade.hook.renderers.keys())[0];
expect(facade.hook.getFiberRoots(rendererID).size).toBeGreaterThan(0);

act(() => {
root.unmount();
});

expect(facade.hook.getFiberRoots(rendererID).size).toBe(0);
});
});
1 change: 1 addition & 0 deletions scripts/jest/config.build.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ module.exports = Object.assign({}, baseConfig, {
modulePathIgnorePatterns: [
...baseConfig.modulePathIgnorePatterns,
'packages/react-devtools-extensions',
'packages/react-devtools-facade',
'packages/react-devtools-shared',
],
// Don't run bundle tests on -test.internal.* files
Expand Down
1 change: 1 addition & 0 deletions scripts/jest/config.source-persistent.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = Object.assign({}, baseConfig, {
modulePathIgnorePatterns: [
...baseConfig.modulePathIgnorePatterns,
'packages/react-devtools-extensions',
'packages/react-devtools-facade',
'packages/react-devtools-shared',
'ReactIncrementalPerf',
'ReactIncrementalUpdatesMinimalism',
Expand Down
Loading
Loading