@@ -60,33 +60,162 @@ export type Facade = {
6060 profilingState : ProfilingState ,
6161} ;
6262
63+ // Initialize per-renderer internal constants for a renderer registered with the
64+ // hook. Shared by the installed hook's inject() and the attach path.
65+ function initializeRendererInternals (
66+ rendererInternals : Map < number , RendererInternals > ,
67+ id : number ,
68+ renderer : any ,
69+ ) : void {
70+ const version = renderer . reconcilerVersion || renderer . version ;
71+ if ( version == null ) {
72+ console . error (
73+ 'react-devtools-facade: Renderer %s has no version, internals not initialized.' ,
74+ id ,
75+ ) ;
76+ return ;
77+ }
78+ const { getDisplayNameForFiber, ReactTypeOfWork, ReactPriorityLevels} =
79+ getInternalReactConstants ( version ) ;
80+ rendererInternals . set ( id , {
81+ getDisplayNameForFiber,
82+ ReactTypeOfWork,
83+ ReactPriorityLevels,
84+ currentDispatcherRef : renderer . currentDispatcherRef ,
85+ } ) ;
86+ }
87+
88+ // Record a commit: keep fiberRoots in sync (add new roots, drop unmounted ones)
89+ // and drive a profiling session when one is active. Shared by the installed
90+ // hook's onCommitFiberRoot and the attach path's wrapper.
91+ function recordCommitFiberRoot (
92+ fiberRoots : Map < number , Set < FiberRoot >> ,
93+ profilingState : ProfilingState ,
94+ rendererID : number ,
95+ root : any ,
96+ schedulerPriority ?: number ,
97+ ) : void {
98+ let mountedRoots = fiberRoots . get ( rendererID ) ;
99+ if ( mountedRoots == null ) {
100+ mountedRoots = new Set ( ) ;
101+ fiberRoots . set ( rendererID , mountedRoots ) ;
102+ }
103+ const current = root . current ;
104+ const isKnownRoot = mountedRoots . has ( root ) ;
105+ const isUnmounting =
106+ current . memoizedState == null || current . memoizedState . element == null ;
107+ if ( ! isKnownRoot && ! isUnmounting ) {
108+ mountedRoots . add ( root ) ;
109+ } else if ( isKnownRoot && isUnmounting ) {
110+ mountedRoots . delete ( root ) ;
111+ }
112+
113+ if ( profilingState . isActive && profilingState . onCommit != null ) {
114+ profilingState . onCommit ( rendererID , root , schedulerPriority ) ;
115+ }
116+ }
117+
118+ // Attach to a DevTools hook that is already installed on the page — for example
119+ // the React DevTools browser extension. Rather than replacing it (React would
120+ // ignore a second hook), read the renderers and fiber roots it is already
121+ // tracking, then wrap inject / onCommitFiberRoot / onPostCommitFiberRoot so
122+ // future renderers, commits, and passive passes also feed the facade's state.
123+ // The existing hook's own bookkeeping is preserved — we always call through to
124+ // it first.
125+ function attachToExistingHook (
126+ hook : any ,
127+ fiberRoots : Map < number , Set < FiberRoot >> ,
128+ rendererInternals : Map < number , RendererInternals > ,
129+ profilingState : ProfilingState ,
130+ ) : void {
131+ // Back-fill renderers and roots registered before we attached (React may have
132+ // initialized first).
133+ if ( hook . renderers instanceof Map ) {
134+ hook . renderers . forEach ( ( renderer : any , id : number ) => {
135+ if ( ! rendererInternals . has ( id ) ) {
136+ initializeRendererInternals ( rendererInternals , id , renderer ) ;
137+ }
138+ if ( typeof hook . getFiberRoots === 'function' ) {
139+ let roots = fiberRoots . get ( id ) ;
140+ if ( roots == null ) {
141+ roots = new Set ( ) ;
142+ fiberRoots . set ( id , roots ) ;
143+ }
144+ // Alias to a const so the non-null refinement survives into the closure.
145+ const mountedRoots = roots ;
146+ hook . getFiberRoots ( id ) . forEach ( ( root : FiberRoot ) => {
147+ mountedRoots . add ( root ) ;
148+ } ) ;
149+ }
150+ } ) ;
151+ }
152+
153+ const originalInject = hook . inject ;
154+ hook . inject = function inject ( renderer : any , ...rest : Array < mixed > ) : number {
155+ const id = originalInject . call ( hook , renderer , ...rest ) ;
156+ if ( typeof id === 'number' ) {
157+ initializeRendererInternals ( rendererInternals , id , renderer ) ;
158+ }
159+ return id ;
160+ } ;
161+
162+ const originalOnCommitFiberRoot = hook . onCommitFiberRoot ;
163+ hook . onCommitFiberRoot = function onCommitFiberRoot (
164+ rendererID : number ,
165+ root : any ,
166+ schedulerPriority ?: number ,
167+ ...rest : Array < mixed >
168+ ) {
169+ if ( typeof originalOnCommitFiberRoot === 'function' ) {
170+ originalOnCommitFiberRoot . call (
171+ hook ,
172+ rendererID ,
173+ root ,
174+ schedulerPriority ,
175+ ...rest ,
176+ ) ;
177+ }
178+ recordCommitFiberRoot (
179+ fiberRoots ,
180+ profilingState ,
181+ rendererID ,
182+ root ,
183+ schedulerPriority ,
184+ ) ;
185+ } ;
186+
187+ const originalOnPostCommitFiberRoot = hook . onPostCommitFiberRoot ;
188+ hook . onPostCommitFiberRoot = function onPostCommitFiberRoot (
189+ rendererID : number ,
190+ root : any ,
191+ ...rest : Array < mixed >
192+ ) {
193+ if ( typeof originalOnPostCommitFiberRoot === 'function' ) {
194+ originalOnPostCommitFiberRoot . call ( hook , rendererID , root , ...rest ) ;
195+ }
196+ if ( profilingState . isActive && profilingState . onPostCommit != null ) {
197+ profilingState . onPostCommit ( root ) ;
198+ }
199+ } ;
200+ }
201+
63202/**
64- * Install the React DevTools facade: install `__REACT_DEVTOOLS_GLOBAL_HOOK__`
65- * on `target` (defaults to globalThis) and return a Facade handle.
203+ * Install the React DevTools facade and return a Facade handle.
66204 *
67- * This installs ONLY `__REACT_DEVTOOLS_GLOBAL_HOOK__` — the global React looks
68- * for at initialization time. It does not install any tool globals: the
69- * returned Facade is passed to building blocks such as `createTools(facade)`,
70- * and the integrator decides whether to expose the resulting tools on globals .
205+ * If `__REACT_DEVTOOLS_GLOBAL_HOOK__` is not yet present, this installs the
206+ * facade's own minimal hook (the global React looks for at init). If a hook is
207+ * already installed — e.g. the user has the React DevTools browser extension —
208+ * the facade attaches to that hook instead of installing a second one .
71209 *
72- * Must run BEFORE React initializes so the hook captures the first commit.
210+ * Either way the returned Facade exposes the same `{hook, fiberRoots,
211+ * rendererInternals, profilingState}` that building blocks such as
212+ * `createTools(facade)` read from. Install before React initializes so the first
213+ * commit is captured; when attaching, roots committed before attach are
214+ * back-filled from the existing hook.
73215 */
74216export function installFacade ( target ?: any = globalThis ) : Facade {
75- // Guard against double-install (e.g. bundled twice or mixed with full DevTools).
76- if ( target . hasOwnProperty ( '__REACT_DEVTOOLS_GLOBAL_HOOK__' ) ) {
77- throw new Error (
78- 'React DevTools global hook is already installed. ' +
79- 'react-devtools-facade should not be used with any other React DevTools package.' ,
80- ) ;
81- }
82-
83- // Fiber root tracking — the only runtime state the hook maintains.
84- // onCommitFiberRoot adds/removes entries so that unmounted roots are
85- // garbage-collected. Building blocks walk from these roots on demand.
86217 const fiberRoots : Map < number , Set< FiberRoot > > = new Map ( ) ;
87-
88218 const rendererInternals : Map < number , RendererInternals > = new Map ( ) ;
89-
90219 const profilingState : ProfilingState = {
91220 isActive : false ,
92221 currentTraceName : null ,
@@ -95,6 +224,19 @@ export function installFacade(target?: any = globalThis): Facade {
95224 onPostCommit : null ,
96225 } ;
97226
227+ // A hook is already installed (e.g. the React DevTools extension). Attach to
228+ // it rather than replacing it.
229+ const existingHook = target . __REACT_DEVTOOLS_GLOBAL_HOOK__ ;
230+ if ( existingHook != null ) {
231+ attachToExistingHook (
232+ existingHook ,
233+ fiberRoots ,
234+ rendererInternals ,
235+ profilingState ,
236+ ) ;
237+ return { hook : existingHook , fiberRoots, rendererInternals, profilingState} ;
238+ }
239+
98240 let registeredRenderersCount = 0 ;
99241
100242 // $FlowFixMe[incompatible-type] the facade provides a minimal subset of DevToolsHook
@@ -116,23 +258,7 @@ export function installFacade(target?: any = globalThis): Facade {
116258 inject ( renderer : any ) : number {
117259 const id = registeredRenderersCount ++ ;
118260 hook . renderers . set ( id , renderer ) ;
119- // Initialize internal constants for this renderer's React version.
120- const version = renderer . reconcilerVersion || renderer . version ;
121- if ( version == null ) {
122- console . error (
123- 'react-devtools-facade: Renderer %s has no version, internals not initialized.' ,
124- id ,
125- ) ;
126- } else {
127- const { getDisplayNameForFiber, ReactTypeOfWork, ReactPriorityLevels} =
128- getInternalReactConstants ( version ) ;
129- rendererInternals . set ( id , {
130- getDisplayNameForFiber,
131- ReactTypeOfWork,
132- ReactPriorityLevels,
133- currentDispatcherRef : renderer . currentDispatcherRef ,
134- } ) ;
135- }
261+ initializeRendererInternals ( rendererInternals , id , renderer ) ;
136262 return id ;
137263 } ,
138264 on ( ) { } ,
@@ -148,23 +274,13 @@ export function installFacade(target?: any = globalThis): Facade {
148274 root : any ,
149275 schedulerPriority ?: number ,
150276 ) {
151- // Hot path — called on every React commit. Keep minimal: just
152- // add or remove the root so building blocks can find it later.
153- const mountedRoots = hook . getFiberRoots ( rendererID ) ;
154- const current = root . current ;
155- const isKnownRoot = mountedRoots . has ( root ) ;
156- const isUnmounting =
157- current . memoizedState == null || current . memoizedState . element == null ;
158- if ( ! isKnownRoot && ! isUnmounting ) {
159- mountedRoots . add ( root ) ;
160- } else if ( isKnownRoot && isUnmounting ) {
161- mountedRoots . delete ( root ) ;
162- }
163-
164- // Profiling: record commit durations when a session is active.
165- if ( profilingState . isActive && profilingState . onCommit != null ) {
166- profilingState . onCommit ( rendererID , root , schedulerPriority ) ;
167- }
277+ recordCommitFiberRoot (
278+ fiberRoots ,
279+ profilingState ,
280+ rendererID ,
281+ root ,
282+ schedulerPriority ,
283+ ) ;
168284 } ,
169285 onCommitFiberUnmount ( ) { } ,
170286 onPostCommitFiberRoot ( rendererID : number , root : any ) {
0 commit comments