@@ -20,10 +20,11 @@ import {
2020} from 'flipper-plugin' ;
2121import React from 'react' ;
2222import getPort from 'get-port' ;
23- import { Button , message , Switch , Typography } from 'antd' ;
23+ import { Button , Select , message , Switch , Typography } from 'antd' ;
2424import child_process from 'child_process' ;
2525import fs from 'fs' ;
2626import { DevToolsEmbedder } from './DevToolsEmbedder' ;
27+ import { getInternalDevToolsModule } from './fb-stubs/getInternalDevToolsModule' ;
2728
2829const DEV_TOOLS_NODE_ID = 'reactdevtools-out-of-react-node' ;
2930const CONNECTED = 'DevTools connected' ;
@@ -55,10 +56,17 @@ function findGlobalDevTools(): Promise<string | undefined> {
5556enum ConnectionStatus {
5657 Initializing = 'Initializing...' ,
5758 WaitingForReload = 'Waiting for connection from device...' ,
59+ WaitingForMetroReload = 'Waiting for Metro to reload...' ,
5860 Connected = 'Connected' ,
5961 Error = 'Error' ,
6062}
6163
64+ type DevToolsInstanceType = 'global' | 'internal' | 'oss' ;
65+ type DevToolsInstance = {
66+ type : DevToolsInstanceType ;
67+ module : ReactDevToolsStandaloneType ;
68+ } ;
69+
6270export function devicePlugin ( client : DevicePluginClient ) {
6371 const metroDevice = client . device ;
6472
@@ -72,28 +80,86 @@ export function devicePlugin(client: DevicePluginClient) {
7280 persistToLocalStorage : true ,
7381 } ) ;
7482
75- let devToolsInstance = getDefaultDevToolsModule ( ) ;
83+ let devToolsInstance = getDefaultDevToolsInstance ( ) ;
84+ const selectedDevToolsInstanceType = createState < DevToolsInstanceType > (
85+ devToolsInstance . type ,
86+ ) ;
7687
7788 let startResult : { close ( ) : void } | undefined = undefined ;
7889
7990 let pollHandle : NodeJS . Timeout | undefined = undefined ;
8091
81- function getDevToolsModule ( ) {
92+ let metroReloadAttempts = 0 ;
93+
94+ function getGlobalDevToolsModule ( ) : ReactDevToolsStandaloneType {
95+ const required = global . electronRequire ( globalDevToolsPath . get ( ) ! ) . default ;
96+ return required . default ?? required ;
97+ }
98+
99+ function getOSSDevToolsModule ( ) : ReactDevToolsStandaloneType {
100+ const required = require ( 'react-devtools-core/standalone' ) . default ;
101+ return required . default ?? required ;
102+ }
103+
104+ function getInitialDevToolsInstance ( ) : DevToolsInstance {
82105 // Load right library
83106 if ( useGlobalDevTools . get ( ) ) {
84- const module = global . electronRequire ( globalDevToolsPath . get ( ) ! ) ;
85- return module . default ?? module ;
107+ return {
108+ type : 'global' ,
109+ module : getGlobalDevToolsModule ( ) ,
110+ } ;
86111 } else {
87- return getDefaultDevToolsModule ( ) ;
112+ return getDefaultDevToolsInstance ( ) ;
88113 }
89114 }
90115
91- function getDefaultDevToolsModule ( ) : ReactDevToolsStandaloneType {
92- return client . isFB
93- ? require ( './fb/react-devtools-core/standalone' ) . default ??
94- require ( './fb/react-devtools-core/standalone' )
95- : require ( 'react-devtools-core/standalone' ) . default ??
96- require ( 'react-devtools-core/standalone' ) ;
116+ function getDefaultDevToolsInstance ( ) : DevToolsInstance {
117+ const type = client . isFB ? 'internal' : 'oss' ;
118+ const module = client . isFB
119+ ? getInternalDevToolsModule < ReactDevToolsStandaloneType > ( )
120+ : getOSSDevToolsModule ( ) ;
121+ return { type, module} ;
122+ }
123+
124+ function getDevToolsInstance (
125+ instanceType : DevToolsInstanceType ,
126+ ) : DevToolsInstance {
127+ let module ;
128+ switch ( instanceType ) {
129+ case 'global' :
130+ module = getGlobalDevToolsModule ( ) ;
131+ break ;
132+ case 'internal' :
133+ module = getInternalDevToolsModule < ReactDevToolsStandaloneType > ( ) ;
134+ break ;
135+ case 'oss' :
136+ module = getOSSDevToolsModule ( ) ;
137+ break ;
138+ }
139+ return {
140+ type : instanceType ,
141+ module,
142+ } ;
143+ }
144+
145+ async function setDevToolsInstance ( instanceType : DevToolsInstanceType ) {
146+ selectedDevToolsInstanceType . set ( instanceType ) ;
147+
148+ if ( instanceType === 'global' ) {
149+ if ( ! globalDevToolsPath . get ( ) ) {
150+ message . warn (
151+ "No globally installed react-devtools package found. Run 'npm install -g react-devtools'." ,
152+ ) ;
153+ return ;
154+ }
155+ useGlobalDevTools . set ( true ) ;
156+ } else {
157+ useGlobalDevTools . set ( false ) ;
158+ }
159+
160+ devToolsInstance = getDevToolsInstance ( instanceType ) ;
161+
162+ await rebootDevTools ( ) ;
97163 }
98164
99165 async function toggleUseGlobalDevTools ( ) {
@@ -103,18 +169,29 @@ export function devicePlugin(client: DevicePluginClient) {
103169 ) ;
104170 return ;
105171 }
172+ selectedDevToolsInstanceType . update ( ( prev : DevToolsInstanceType ) => {
173+ if ( prev === 'global' ) {
174+ devToolsInstance = getDefaultDevToolsInstance ( ) ;
175+ return devToolsInstance . type ;
176+ } else {
177+ devToolsInstance = getDevToolsInstance ( 'global' ) ;
178+ return devToolsInstance . type ;
179+ }
180+ } ) ;
106181 useGlobalDevTools . update ( ( v ) => ! v ) ;
107182
108- devToolsInstance = getDevToolsModule ( ) ;
183+ await rebootDevTools ( ) ;
184+ }
109185
110- statusMessage . set ( 'Switching devTools' ) ;
111- connectionStatus . set ( ConnectionStatus . Initializing ) ;
186+ async function rebootDevTools ( ) {
187+ metroReloadAttempts = 0 ;
188+ setStatus ( ConnectionStatus . Initializing , 'Loading DevTools...' ) ;
112189 // clean old instance
113190 if ( pollHandle ) {
114191 clearTimeout ( pollHandle ) ;
115192 }
116193 startResult ?. close ( ) ;
117- await sleep ( 1000 ) ; // wait for port to close
194+ await sleep ( 5000 ) ; // wait for port to close
118195 startResult = undefined ;
119196 await bootDevTools ( ) ;
120197 }
@@ -152,24 +229,24 @@ export function devicePlugin(client: DevicePluginClient) {
152229 }
153230 setStatus (
154231 ConnectionStatus . Initializing ,
155- 'Starting DevTools server on ' + port ,
232+ 'Starting DevTools server on ' + DEV_TOOLS_PORT ,
156233 ) ;
157- startResult = devToolsInstance
234+ startResult = devToolsInstance . module
158235 . setContentDOMNode ( devToolsNode )
159236 . setStatusListener ( ( status : string ) => {
160237 // TODO: since devToolsInstance is an instance, we are probably leaking memory here
161238 setStatus ( ConnectionStatus . Initializing , status ) ;
162239 } )
163- . startServer ( port ) as any ;
164- setStatus ( ConnectionStatus . Initializing , 'Waiting for device' ) ;
240+ . startServer ( DEV_TOOLS_PORT ) as any ;
241+ setStatus ( ConnectionStatus . Initializing , 'Waiting for device... ' ) ;
165242 } catch ( e ) {
166243 console . error ( 'Failed to initalize React DevTools' + e ) ;
167244 setStatus ( ConnectionStatus . Error , 'Failed to initialize DevTools: ' + e ) ;
168245 }
169246
170247 setStatus (
171248 ConnectionStatus . Initializing ,
172- 'DevTools have been initialized, waiting for connection...' ,
249+ 'DevTools initialized, waiting for connection...' ,
173250 ) ;
174251 if ( devtoolsHaveStarted ( ) ) {
175252 setStatus ( ConnectionStatus . Connected , CONNECTED ) ;
@@ -196,27 +273,33 @@ export function devicePlugin(client: DevicePluginClient) {
196273 return ;
197274 // Waiting for connection, but we do have an active Metro connection, lets force a reload to enter Dev Mode on app
198275 // prettier-ignore
199- case connectionStatus . get ( ) === ConnectionStatus . Initializing :
200- setStatus (
201- ConnectionStatus . WaitingForReload ,
202- "Sending 'reload' to Metro to force the DevTools to connect..." ,
203- ) ;
204- metroDevice ! . sendMetroCommand ( 'reload' ) ;
205- startPollForConnection ( 2000 ) ;
206- return ;
207- // Waiting for initial connection, but no WS bridge available
208- case connectionStatus . get ( ) === ConnectionStatus . Initializing :
276+ case connectionStatus . get ( ) === ConnectionStatus . Initializing : {
277+ if ( metroDevice ) {
278+ const nextConnectionStatus = metroReloadAttempts === 0 ? ConnectionStatus . Initializing : ConnectionStatus . WaitingForMetroReload ;
279+ metroReloadAttempts ++ ;
280+ setStatus (
281+ nextConnectionStatus ,
282+ "Sending 'reload' to Metro to force DevTools to connect..." ,
283+ ) ;
284+ metroDevice . sendMetroCommand ( 'reload' ) ;
285+ startPollForConnection ( 3000 ) ;
286+ return ;
287+ }
288+
289+ // Waiting for initial connection, but no WS bridge available
209290 setStatus (
210291 ConnectionStatus . WaitingForReload ,
211- "The DevTools didn't connect yet. Please trigger the DevMenu in the React Native app, or Reload it to connect." ,
292+ "DevTools is unable to connect yet. Please trigger the DevMenu in the RN app, or reload it to connect." ,
212293 ) ;
213294 startPollForConnection ( 10000 ) ;
214295 return ;
296+ }
215297 // Still nothing? Users might not have done manual action, or some other tools have picked it up?
216298 case connectionStatus . get ( ) === ConnectionStatus . WaitingForReload :
299+ case connectionStatus . get ( ) === ConnectionStatus . WaitingForMetroReload :
217300 setStatus (
218301 ConnectionStatus . WaitingForReload ,
219- "The DevTools didn't connect yet. Check if no other instances are running." ,
302+ ' DevTools is unable to connect yet. Check for other instances, trigger the DevMenu in the RN app, or reload it to connect.' ,
220303 ) ;
221304 startPollForConnection ( ) ;
222305 return ;
@@ -234,9 +317,10 @@ export function devicePlugin(client: DevicePluginClient) {
234317 const path = await findGlobalDevTools ( ) ;
235318 if ( path ) {
236319 globalDevToolsPath . set ( path + '/standalone' ) ;
320+ selectedDevToolsInstanceType . set ( 'global' ) ;
237321 console . log ( 'Found global React DevTools: ' , path ) ;
238322 // load it, if the flag is set
239- devToolsInstance = getDevToolsModule ( ) ;
323+ devToolsInstance = getInitialDevToolsInstance ( ) ;
240324 } else {
241325 useGlobalDevTools . set ( false ) ; // disable in case it was enabled
242326 }
@@ -257,57 +341,96 @@ export function devicePlugin(client: DevicePluginClient) {
257341 } ) ;
258342
259343 return {
344+ isFB : client . isFB ,
260345 devtoolsHaveStarted,
261346 connectionStatus,
262347 statusMessage,
263348 bootDevTools,
349+ rebootDevTools,
264350 metroDevice,
265351 globalDevToolsPath,
266352 useGlobalDevTools,
353+ selectedDevToolsInstanceType,
354+ setDevToolsInstance,
267355 toggleUseGlobalDevTools,
268356 } ;
269357}
270358
271359export function Component ( ) {
360+ return (
361+ < Layout . Container grow >
362+ < DevToolsInstanceToolbar />
363+ < DevToolsEmbedder offset = { 40 } nodeId = { DEV_TOOLS_NODE_ID } />
364+ </ Layout . Container >
365+ ) ;
366+ }
367+
368+ function DevToolsInstanceToolbar ( ) {
272369 const instance = usePlugin ( devicePlugin ) ;
370+ const globalDevToolsPath = useValue ( instance . globalDevToolsPath ) ;
273371 const connectionStatus = useValue ( instance . connectionStatus ) ;
274372 const statusMessage = useValue ( instance . statusMessage ) ;
275- const globalDevToolsPath = useValue ( instance . globalDevToolsPath ) ;
276373 const useGlobalDevTools = useValue ( instance . useGlobalDevTools ) ;
374+ const selectedDevToolsInstanceType = useValue (
375+ instance . selectedDevToolsInstanceType ,
376+ ) ;
377+
378+ if ( ! globalDevToolsPath && ! instance . isFB ) {
379+ return null ;
380+ }
381+
382+ let selectionControl ;
383+ if ( instance . isFB ) {
384+ const devToolsInstanceOptions = [ { value : 'internal' } , { value : 'oss' } ] ;
385+ if ( globalDevToolsPath ) {
386+ devToolsInstanceOptions . push ( { value : 'global' } ) ;
387+ }
388+ selectionControl = (
389+ < >
390+ Select preferred DevTools version:
391+ < Select
392+ options = { devToolsInstanceOptions }
393+ value = { selectedDevToolsInstanceType }
394+ onSelect = { instance . setDevToolsInstance }
395+ style = { { width : 90 } }
396+ size = "small"
397+ />
398+ </ >
399+ ) ;
400+ } else if ( globalDevToolsPath ) {
401+ selectionControl = (
402+ < >
403+ < Switch
404+ checked = { useGlobalDevTools }
405+ onChange = { instance . toggleUseGlobalDevTools }
406+ size = "small"
407+ />
408+ Use globally installed DevTools
409+ </ >
410+ ) ;
411+ } else {
412+ throw new Error (
413+ 'Should not render Toolbar if not FB build or a global DevTools install not available.' ,
414+ ) ;
415+ }
277416
278417 return (
279- < Layout . Container grow >
280- { globalDevToolsPath ? (
281- < Toolbar
282- right = {
283- < >
284- < Switch
285- checked = { useGlobalDevTools }
286- onChange = { instance . toggleUseGlobalDevTools }
287- size = "small"
288- />
289- Use globally installed DevTools
290- </ >
291- }
292- wash >
293- { connectionStatus !== ConnectionStatus . Connected ? (
294- < Typography . Text type = "secondary" > { statusMessage } </ Typography . Text >
295- ) : null }
296- { ( connectionStatus === ConnectionStatus . WaitingForReload &&
297- instance . metroDevice ) ||
298- connectionStatus === ConnectionStatus . Error ? (
299- < Button
300- size = "small"
301- onClick = { ( ) => {
302- instance . metroDevice ?. sendMetroCommand ( 'reload' ) ;
303- instance . bootDevTools ( ) ;
304- } } >
305- Retry
306- </ Button >
307- ) : null }
308- </ Toolbar >
418+ < Toolbar right = { selectionControl } wash >
419+ { connectionStatus !== ConnectionStatus . Connected ? (
420+ < Typography . Text type = "secondary" > { statusMessage } </ Typography . Text >
309421 ) : null }
310- < DevToolsEmbedder offset = { 40 } nodeId = { DEV_TOOLS_NODE_ID } />
311- </ Layout . Container >
422+ { connectionStatus === ConnectionStatus . WaitingForReload ||
423+ connectionStatus === ConnectionStatus . WaitingForMetroReload ||
424+ connectionStatus === ConnectionStatus . Error ? (
425+ < Button
426+ size = "small"
427+ onClick = { ( ) => {
428+ instance . metroDevice ?. sendMetroCommand ( 'reload' ) ;
429+ instance . rebootDevTools ( ) ;
430+ } } >
431+ Retry
432+ </ Button >
433+ ) : null }
434+ </ Toolbar >
312435 ) ;
313436}
0 commit comments