@@ -20,7 +20,7 @@ import {
20
20
} from 'flipper-plugin' ;
21
21
import React from 'react' ;
22
22
import getPort from 'get-port' ;
23
- import { Button , message , Switch , Typography } from 'antd' ;
23
+ import { Button , Select , message , Switch , Typography } from 'antd' ;
24
24
import child_process from 'child_process' ;
25
25
import fs from 'fs' ;
26
26
import { DevToolsEmbedder } from './DevToolsEmbedder' ;
@@ -55,10 +55,17 @@ function findGlobalDevTools(): Promise<string | undefined> {
55
55
enum ConnectionStatus {
56
56
Initializing = 'Initializing...' ,
57
57
WaitingForReload = 'Waiting for connection from device...' ,
58
+ WaitingForMetroReload = 'Waiting for Metro to reload...' ,
58
59
Connected = 'Connected' ,
59
60
Error = 'Error' ,
60
61
}
61
62
63
+ type DevToolsInstanceType = 'global' | 'internal' | 'oss' ;
64
+ type DevToolsInstance = {
65
+ type : DevToolsInstanceType ;
66
+ module : ReactDevToolsStandaloneType ;
67
+ } ;
68
+
62
69
export function devicePlugin ( client : DevicePluginClient ) {
63
70
const metroDevice = client . device ;
64
71
@@ -72,28 +79,91 @@ export function devicePlugin(client: DevicePluginClient) {
72
79
persistToLocalStorage : true ,
73
80
} ) ;
74
81
75
- let devToolsInstance = getDefaultDevToolsModule ( ) ;
82
+ let devToolsInstance = getDefaultDevToolsInstance ( ) ;
83
+ const selectedDevToolsInstanceType = createState < DevToolsInstanceType > (
84
+ devToolsInstance . type ,
85
+ ) ;
76
86
77
87
let startResult : { close ( ) : void } | undefined = undefined ;
78
88
79
89
let pollHandle : NodeJS . Timeout | undefined = undefined ;
80
90
81
- function getDevToolsModule ( ) {
91
+ let metroReloadAttempts = 0 ;
92
+
93
+ function getGlobalDevToolsModule ( ) : ReactDevToolsStandaloneType {
94
+ const required = global . electronRequire ( globalDevToolsPath . get ( ) ! ) . default ;
95
+ return required . default ?? required ;
96
+ }
97
+
98
+ function getInternalDevToolsModule ( ) : ReactDevToolsStandaloneType {
99
+ const required = require ( './fb/react-devtools-core/standalone' ) . default ;
100
+ return required . default ?? required ;
101
+ }
102
+
103
+ function getOSSDevToolsModule ( ) : ReactDevToolsStandaloneType {
104
+ const required = require ( 'react-devtools-core/standalone' ) . default ;
105
+ return required . default ?? required ;
106
+ }
107
+
108
+ function getInitialDevToolsInstance ( ) : DevToolsInstance {
82
109
// Load right library
83
110
if ( useGlobalDevTools . get ( ) ) {
84
- const module = global . electronRequire ( globalDevToolsPath . get ( ) ! ) ;
85
- return module . default ?? module ;
111
+ return {
112
+ type : 'global' ,
113
+ module : getGlobalDevToolsModule ( ) ,
114
+ } ;
86
115
} else {
87
- return getDefaultDevToolsModule ( ) ;
116
+ return getDefaultDevToolsInstance ( ) ;
117
+ }
118
+ }
119
+
120
+ function getDefaultDevToolsInstance ( ) : DevToolsInstance {
121
+ const type = client . isFB ? 'internal' : 'oss' ;
122
+ const module = client . isFB
123
+ ? getInternalDevToolsModule ( )
124
+ : getOSSDevToolsModule ( ) ;
125
+ return { type, module} ;
126
+ }
127
+
128
+ function getDevToolsInstance (
129
+ instanceType : DevToolsInstanceType ,
130
+ ) : DevToolsInstance {
131
+ let module ;
132
+ switch ( instanceType ) {
133
+ case 'global' :
134
+ module = getGlobalDevToolsModule ( ) ;
135
+ break ;
136
+ case 'internal' :
137
+ module = getInternalDevToolsModule ( ) ;
138
+ break ;
139
+ case 'oss' :
140
+ module = getOSSDevToolsModule ( ) ;
141
+ break ;
88
142
}
143
+ return {
144
+ type : instanceType ,
145
+ module,
146
+ } ;
89
147
}
90
148
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' ) ;
149
+ async function setDevToolsInstance ( instanceType : DevToolsInstanceType ) {
150
+ selectedDevToolsInstanceType . set ( instanceType ) ;
151
+
152
+ if ( instanceType === 'global' ) {
153
+ if ( ! globalDevToolsPath . get ( ) ) {
154
+ message . warn (
155
+ "No globally installed react-devtools package found. Run 'npm install -g react-devtools'." ,
156
+ ) ;
157
+ return ;
158
+ }
159
+ useGlobalDevTools . set ( true ) ;
160
+ } else {
161
+ useGlobalDevTools . set ( false ) ;
162
+ }
163
+
164
+ devToolsInstance = getDevToolsInstance ( instanceType ) ;
165
+
166
+ await rebootDevTools ( ) ;
97
167
}
98
168
99
169
async function toggleUseGlobalDevTools ( ) {
@@ -103,18 +173,29 @@ export function devicePlugin(client: DevicePluginClient) {
103
173
) ;
104
174
return ;
105
175
}
176
+ selectedDevToolsInstanceType . update ( ( prev : DevToolsInstanceType ) => {
177
+ if ( prev === 'global' ) {
178
+ devToolsInstance = getDefaultDevToolsInstance ( ) ;
179
+ return devToolsInstance . type ;
180
+ } else {
181
+ devToolsInstance = getDevToolsInstance ( 'global' ) ;
182
+ return devToolsInstance . type ;
183
+ }
184
+ } ) ;
106
185
useGlobalDevTools . update ( ( v ) => ! v ) ;
107
186
108
- devToolsInstance = getDevToolsModule ( ) ;
187
+ await rebootDevTools ( ) ;
188
+ }
109
189
110
- statusMessage . set ( 'Switching devTools' ) ;
111
- connectionStatus . set ( ConnectionStatus . Initializing ) ;
190
+ async function rebootDevTools ( ) {
191
+ metroReloadAttempts = 0 ;
192
+ setStatus ( ConnectionStatus . Initializing , 'Switching DevTools...' ) ;
112
193
// clean old instance
113
194
if ( pollHandle ) {
114
195
clearTimeout ( pollHandle ) ;
115
196
}
116
197
startResult ?. close ( ) ;
117
- await sleep ( 1000 ) ; // wait for port to close
198
+ await sleep ( 5000 ) ; // wait for port to close
118
199
startResult = undefined ;
119
200
await bootDevTools ( ) ;
120
201
}
@@ -152,24 +233,24 @@ export function devicePlugin(client: DevicePluginClient) {
152
233
}
153
234
setStatus (
154
235
ConnectionStatus . Initializing ,
155
- 'Starting DevTools server on ' + port ,
236
+ 'Starting DevTools server on ' + DEV_TOOLS_PORT ,
156
237
) ;
157
- startResult = devToolsInstance
238
+ startResult = devToolsInstance . module
158
239
. setContentDOMNode ( devToolsNode )
159
240
. setStatusListener ( ( status : string ) => {
160
241
// TODO: since devToolsInstance is an instance, we are probably leaking memory here
161
242
setStatus ( ConnectionStatus . Initializing , status ) ;
162
243
} )
163
- . startServer ( port ) as any ;
164
- setStatus ( ConnectionStatus . Initializing , 'Waiting for device' ) ;
244
+ . startServer ( DEV_TOOLS_PORT ) as any ;
245
+ setStatus ( ConnectionStatus . Initializing , 'Waiting for device... ' ) ;
165
246
} catch ( e ) {
166
247
console . error ( 'Failed to initalize React DevTools' + e ) ;
167
248
setStatus ( ConnectionStatus . Error , 'Failed to initialize DevTools: ' + e ) ;
168
249
}
169
250
170
251
setStatus (
171
252
ConnectionStatus . Initializing ,
172
- 'DevTools have been initialized, waiting for connection...' ,
253
+ 'DevTools initialized, waiting for connection...' ,
173
254
) ;
174
255
if ( devtoolsHaveStarted ( ) ) {
175
256
setStatus ( ConnectionStatus . Connected , CONNECTED ) ;
@@ -196,27 +277,33 @@ export function devicePlugin(client: DevicePluginClient) {
196
277
return ;
197
278
// Waiting for connection, but we do have an active Metro connection, lets force a reload to enter Dev Mode on app
198
279
// 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 :
280
+ case connectionStatus . get ( ) === ConnectionStatus . Initializing : {
281
+ if ( metroDevice ) {
282
+ const nextConnectionStatus = metroReloadAttempts === 0 ? ConnectionStatus . Initializing : ConnectionStatus . WaitingForMetroReload ;
283
+ metroReloadAttempts ++ ;
284
+ setStatus (
285
+ nextConnectionStatus ,
286
+ "Sending 'reload' to Metro to force DevTools to connect..." ,
287
+ ) ;
288
+ metroDevice . sendMetroCommand ( 'reload' ) ;
289
+ startPollForConnection ( 3000 ) ;
290
+ return ;
291
+ }
292
+
293
+ // Waiting for initial connection, but no WS bridge available
209
294
setStatus (
210
295
ConnectionStatus . WaitingForReload ,
211
- "The DevTools didn't connect yet. Please trigger the DevMenu in the React Native app, or Reload it to connect." ,
296
+ "DevTools didn't connect yet. Please trigger the DevMenu in the React Native app, or Reload it to connect." ,
212
297
) ;
213
298
startPollForConnection ( 10000 ) ;
214
299
return ;
300
+ }
215
301
// Still nothing? Users might not have done manual action, or some other tools have picked it up?
216
302
case connectionStatus . get ( ) === ConnectionStatus . WaitingForReload :
303
+ case connectionStatus . get ( ) === ConnectionStatus . WaitingForMetroReload :
217
304
setStatus (
218
305
ConnectionStatus . WaitingForReload ,
219
- "The DevTools didn 't connect yet. Check if no other instances are running." ,
306
+ "DevTools hasn 't connected yet. Check if no other instances are running, trigger the DevMenu in the React Native app, or Reload it to connect ." ,
220
307
) ;
221
308
startPollForConnection ( ) ;
222
309
return ;
@@ -234,9 +321,10 @@ export function devicePlugin(client: DevicePluginClient) {
234
321
const path = await findGlobalDevTools ( ) ;
235
322
if ( path ) {
236
323
globalDevToolsPath . set ( path + '/standalone' ) ;
324
+ selectedDevToolsInstanceType . set ( 'global' ) ;
237
325
console . log ( 'Found global React DevTools: ' , path ) ;
238
326
// load it, if the flag is set
239
- devToolsInstance = getDevToolsModule ( ) ;
327
+ devToolsInstance = getInitialDevToolsInstance ( ) ;
240
328
} else {
241
329
useGlobalDevTools . set ( false ) ; // disable in case it was enabled
242
330
}
@@ -257,57 +345,95 @@ export function devicePlugin(client: DevicePluginClient) {
257
345
} ) ;
258
346
259
347
return {
348
+ isFB : client . isFB ,
260
349
devtoolsHaveStarted,
261
350
connectionStatus,
262
351
statusMessage,
263
352
bootDevTools,
353
+ rebootDevTools,
264
354
metroDevice,
265
355
globalDevToolsPath,
266
356
useGlobalDevTools,
357
+ selectedDevToolsInstanceType,
358
+ setDevToolsInstance,
267
359
toggleUseGlobalDevTools,
268
360
} ;
269
361
}
270
362
271
363
export function Component ( ) {
364
+ return (
365
+ < Layout . Container grow >
366
+ < DevToolsInstanceToolbar />
367
+ < DevToolsEmbedder offset = { 40 } nodeId = { DEV_TOOLS_NODE_ID } />
368
+ </ Layout . Container >
369
+ ) ;
370
+ }
371
+
372
+ function DevToolsInstanceToolbar ( ) {
272
373
const instance = usePlugin ( devicePlugin ) ;
374
+ const globalDevToolsPath = useValue ( instance . globalDevToolsPath ) ;
273
375
const connectionStatus = useValue ( instance . connectionStatus ) ;
274
376
const statusMessage = useValue ( instance . statusMessage ) ;
275
- const globalDevToolsPath = useValue ( instance . globalDevToolsPath ) ;
276
377
const useGlobalDevTools = useValue ( instance . useGlobalDevTools ) ;
378
+ const selectedDevToolsInstanceType = useValue (
379
+ instance . selectedDevToolsInstanceType ,
380
+ ) ;
381
+
382
+ if ( ! globalDevToolsPath && ! instance . isFB ) {
383
+ return null ;
384
+ }
385
+
386
+ let selectionControl ;
387
+ if ( instance . isFB ) {
388
+ const devToolsInstanceOptions = [ { value : 'internal' } , { value : 'oss' } ] ;
389
+ if ( globalDevToolsPath ) {
390
+ devToolsInstanceOptions . push ( { value : 'global' } ) ;
391
+ }
392
+ selectionControl = (
393
+ < >
394
+ Select preferred DevTools version:
395
+ < Select
396
+ options = { devToolsInstanceOptions }
397
+ value = { selectedDevToolsInstanceType }
398
+ onSelect = { instance . setDevToolsInstance }
399
+ style = { { width : 90 } }
400
+ size = "small"
401
+ />
402
+ </ >
403
+ ) ;
404
+ } else if ( globalDevToolsPath ) {
405
+ selectionControl = (
406
+ < >
407
+ < Switch
408
+ checked = { useGlobalDevTools }
409
+ onChange = { instance . toggleUseGlobalDevTools }
410
+ size = "small"
411
+ />
412
+ Use globally installed DevTools
413
+ </ >
414
+ ) ;
415
+ } else {
416
+ throw new Error (
417
+ 'Should not render Toolbar if not FB build or a global DevTools install not available.' ,
418
+ ) ;
419
+ }
277
420
278
421
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 >
422
+ < Toolbar right = { selectionControl } wash >
423
+ { connectionStatus !== ConnectionStatus . Connected ? (
424
+ < Typography . Text type = "secondary" > { statusMessage } </ Typography . Text >
309
425
) : null }
310
- < DevToolsEmbedder offset = { 40 } nodeId = { DEV_TOOLS_NODE_ID } />
311
- </ Layout . Container >
426
+ { connectionStatus === ConnectionStatus . WaitingForMetroReload ||
427
+ connectionStatus === ConnectionStatus . Error ? (
428
+ < Button
429
+ size = "small"
430
+ onClick = { ( ) => {
431
+ instance . metroDevice ?. sendMetroCommand ( 'reload' ) ;
432
+ instance . rebootDevTools ( ) ;
433
+ } } >
434
+ Retry
435
+ </ Button >
436
+ ) : null }
437
+ </ Toolbar >
312
438
) ;
313
439
}
0 commit comments