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
+}