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
76 changes: 59 additions & 17 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -1196,17 +1196,13 @@ function handleRenderFunctionError(error: any): void {
throw wrapperError;
}

export function inspectHooks<Props>(
// Shared implementation. Requires an explicit dispatcher and never references
// ReactSharedInternals, so importing it does not pull React into the bundle.
function inspectHooksImpl<Props>(
renderFunction: Props => React$Node,
props: Props,
currentDispatcher: ?CurrentDispatcherRef,
currentDispatcher: CurrentDispatcherRef,
): HooksTree {
// DevTools will pass the current renderer's injected dispatcher.
// Other apps might compile debug hooks as part of their app though.
if (currentDispatcher == null) {
currentDispatcher = ReactSharedInternals;
}

const previousDispatcher = currentDispatcher.H;
currentDispatcher.H = DispatcherProxy;

Expand All @@ -1231,6 +1227,31 @@ export function inspectHooks<Props>(
return buildTree(rootStack, readHookLog);
}

// DevTools will pass the current renderer's injected dispatcher. Other apps
// might compile debug hooks as part of their app though, so default to the
// running React's shared internals when no dispatcher is provided.
export function inspectHooks<Props>(
renderFunction: Props => React$Node,
props: Props,
currentDispatcher: ?CurrentDispatcherRef,
): HooksTree {
return inspectHooksImpl(
renderFunction,
props,
currentDispatcher ?? ReactSharedInternals,
);
}

// Like inspectHooks but requires an explicit dispatcher and never references
// ReactSharedInternals, so importing it does not pull React into the bundle.
export function inspectHooksWithoutDefaultDispatcher<Props>(
renderFunction: Props => React$Node,
props: Props,
currentDispatcher: CurrentDispatcherRef,
): HooksTree {
return inspectHooksImpl(renderFunction, props, currentDispatcher);
}

function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
let current: null | Fiber = fiber;
while (current) {
Expand Down Expand Up @@ -1297,16 +1318,13 @@ function resolveDefaultProps(Component: any, baseProps: any) {
return baseProps;
}

export function inspectHooksOfFiber(
// Shared implementation. Requires an explicit dispatcher and never references
// ReactSharedInternals (it delegates to inspectHooksImpl), so importing it does
// not pull React into the bundle.
function inspectHooksOfFiberImpl(
fiber: Fiber,
currentDispatcher: ?CurrentDispatcherRef,
currentDispatcher: CurrentDispatcherRef,
): HooksTree {
// DevTools will pass the current renderer's injected dispatcher.
// Other apps might compile debug hooks as part of their app though.
if (currentDispatcher == null) {
currentDispatcher = ReactSharedInternals;
}

if (
fiber.tag !== FunctionComponent &&
fiber.tag !== SimpleMemoComponent &&
Expand Down Expand Up @@ -1381,7 +1399,7 @@ export function inspectHooksOfFiber(
);
}

return inspectHooks(type, props, currentDispatcher);
return inspectHooksImpl(type, props, currentDispatcher);
} finally {
currentFiber = null;
currentHook = null;
Expand All @@ -1392,3 +1410,27 @@ export function inspectHooksOfFiber(
restoreContexts(contextMap);
}
}

// DevTools will pass the current renderer's injected dispatcher. Other apps
// might compile debug hooks as part of their app though, so default to the
// running React's shared internals when no dispatcher is provided.
export function inspectHooksOfFiber(
fiber: Fiber,
currentDispatcher: ?CurrentDispatcherRef,
): HooksTree {
return inspectHooksOfFiberImpl(
fiber,
currentDispatcher ?? ReactSharedInternals,
);
}

// Like inspectHooksOfFiber but requires an explicit dispatcher and never
// references ReactSharedInternals. Callers that always have the renderer's
// injected dispatcher (e.g. react-devtools-facade) can use this to avoid
// pulling React into their bundle.
export function inspectHooksOfFiberWithoutDefaultDispatcher(
fiber: Fiber,
currentDispatcher: CurrentDispatcherRef,
): HooksTree {
return inspectHooksOfFiberImpl(fiber, currentDispatcher);
}
14 changes: 12 additions & 2 deletions packages/react-debug-tools/src/ReactDebugTools.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
* @flow
*/

import {inspectHooks, inspectHooksOfFiber} from './ReactDebugHooks';
import {
inspectHooks,
inspectHooksWithoutDefaultDispatcher,
inspectHooksOfFiber,
inspectHooksOfFiberWithoutDefaultDispatcher,
} from './ReactDebugHooks';

export {inspectHooks, inspectHooksOfFiber};
export {
inspectHooks,
inspectHooksWithoutDefaultDispatcher,
inspectHooksOfFiber,
inspectHooksOfFiberWithoutDefaultDispatcher,
};
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"
}
}
191 changes: 191 additions & 0 deletions packages/react-devtools-facade/src/DevToolsFacade.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/**
* 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<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};
}
Loading
Loading