@@ -20,10 +20,11 @@ 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' ;
27
+ import { getInternalDevToolsModule } from './fb-stubs/getInternalDevToolsModule' ;
27
28
28
29
const DEV_TOOLS_NODE_ID = 'reactdevtools-out-of-react-node' ;
29
30
const CONNECTED = 'DevTools connected' ;
@@ -55,10 +56,17 @@ function findGlobalDevTools(): Promise<string | undefined> {
55
56
enum ConnectionStatus {
56
57
Initializing = 'Initializing...' ,
57
58
WaitingForReload = 'Waiting for connection from device...' ,
59
+ WaitingForMetroReload = 'Waiting for Metro to reload...' ,
58
60
Connected = 'Connected' ,
59
61
Error = 'Error' ,
60
62
}
61
63
64
+ type DevToolsInstanceType = 'global' | 'internal' | 'oss' ;
65
+ type DevToolsInstance = {
66
+ type : DevToolsInstanceType ;
67
+ module : ReactDevToolsStandaloneType ;
68
+ } ;
69
+
62
70
export function devicePlugin ( client : DevicePluginClient ) {
63
71
const metroDevice = client . device ;
64
72
@@ -72,28 +80,86 @@ export function devicePlugin(client: DevicePluginClient) {
72
80
persistToLocalStorage : true ,
73
81
} ) ;
74
82
75
- let devToolsInstance = getDefaultDevToolsModule ( ) ;
83
+ let devToolsInstance = getDefaultDevToolsInstance ( ) ;
84
+ const selectedDevToolsInstanceType = createState < DevToolsInstanceType > (
85
+ devToolsInstance . type ,
86
+ ) ;
76
87
77
88
let startResult : { close ( ) : void } | undefined = undefined ;
78
89
79
90
let pollHandle : NodeJS . Timeout | undefined = undefined ;
80
91
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 {
82
105
// Load right library
83
106
if ( useGlobalDevTools . get ( ) ) {
84
- const module = global . electronRequire ( globalDevToolsPath . get ( ) ! ) ;
85
- return module . default ?? module ;
107
+ return {
108
+ type : 'global' ,
109
+ module : getGlobalDevToolsModule ( ) ,
110
+ } ;
86
111
} else {
87
- return getDefaultDevToolsModule ( ) ;
112
+ return getDefaultDevToolsInstance ( ) ;
88
113
}
89
114
}
90
115
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 ( ) ;
97
163
}
98
164
99
165
async function toggleUseGlobalDevTools ( ) {
@@ -103,18 +169,29 @@ export function devicePlugin(client: DevicePluginClient) {
103
169
) ;
104
170
return ;
105
171
}
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
+ } ) ;
106
181
useGlobalDevTools . update ( ( v ) => ! v ) ;
107
182
108
- devToolsInstance = getDevToolsModule ( ) ;
183
+ await rebootDevTools ( ) ;
184
+ }
109
185
110
- statusMessage . set ( 'Switching devTools' ) ;
111
- connectionStatus . set ( ConnectionStatus . Initializing ) ;
186
+ async function rebootDevTools ( ) {
187
+ metroReloadAttempts = 0 ;
188
+ setStatus ( ConnectionStatus . Initializing , 'Loading DevTools...' ) ;
112
189
// clean old instance
113
190
if ( pollHandle ) {
114
191
clearTimeout ( pollHandle ) ;
115
192
}
116
193
startResult ?. close ( ) ;
117
- await sleep ( 1000 ) ; // wait for port to close
194
+ await sleep ( 5000 ) ; // wait for port to close
118
195
startResult = undefined ;
119
196
await bootDevTools ( ) ;
120
197
}
@@ -152,24 +229,24 @@ export function devicePlugin(client: DevicePluginClient) {
152
229
}
153
230
setStatus (
154
231
ConnectionStatus . Initializing ,
155
- 'Starting DevTools server on ' + port ,
232
+ 'Starting DevTools server on ' + DEV_TOOLS_PORT ,
156
233
) ;
157
- startResult = devToolsInstance
234
+ startResult = devToolsInstance . module
158
235
. setContentDOMNode ( devToolsNode )
159
236
. setStatusListener ( ( status : string ) => {
160
237
// TODO: since devToolsInstance is an instance, we are probably leaking memory here
161
238
setStatus ( ConnectionStatus . Initializing , status ) ;
162
239
} )
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... ' ) ;
165
242
} catch ( e ) {
166
243
console . error ( 'Failed to initalize React DevTools' + e ) ;
167
244
setStatus ( ConnectionStatus . Error , 'Failed to initialize DevTools: ' + e ) ;
168
245
}
169
246
170
247
setStatus (
171
248
ConnectionStatus . Initializing ,
172
- 'DevTools have been initialized, waiting for connection...' ,
249
+ 'DevTools initialized, waiting for connection...' ,
173
250
) ;
174
251
if ( devtoolsHaveStarted ( ) ) {
175
252
setStatus ( ConnectionStatus . Connected , CONNECTED ) ;
@@ -196,27 +273,33 @@ export function devicePlugin(client: DevicePluginClient) {
196
273
return ;
197
274
// Waiting for connection, but we do have an active Metro connection, lets force a reload to enter Dev Mode on app
198
275
// 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
209
290
setStatus (
210
291
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." ,
212
293
) ;
213
294
startPollForConnection ( 10000 ) ;
214
295
return ;
296
+ }
215
297
// Still nothing? Users might not have done manual action, or some other tools have picked it up?
216
298
case connectionStatus . get ( ) === ConnectionStatus . WaitingForReload :
299
+ case connectionStatus . get ( ) === ConnectionStatus . WaitingForMetroReload :
217
300
setStatus (
218
301
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.' ,
220
303
) ;
221
304
startPollForConnection ( ) ;
222
305
return ;
@@ -234,9 +317,10 @@ export function devicePlugin(client: DevicePluginClient) {
234
317
const path = await findGlobalDevTools ( ) ;
235
318
if ( path ) {
236
319
globalDevToolsPath . set ( path + '/standalone' ) ;
320
+ selectedDevToolsInstanceType . set ( 'global' ) ;
237
321
console . log ( 'Found global React DevTools: ' , path ) ;
238
322
// load it, if the flag is set
239
- devToolsInstance = getDevToolsModule ( ) ;
323
+ devToolsInstance = getInitialDevToolsInstance ( ) ;
240
324
} else {
241
325
useGlobalDevTools . set ( false ) ; // disable in case it was enabled
242
326
}
@@ -257,57 +341,96 @@ export function devicePlugin(client: DevicePluginClient) {
257
341
} ) ;
258
342
259
343
return {
344
+ isFB : client . isFB ,
260
345
devtoolsHaveStarted,
261
346
connectionStatus,
262
347
statusMessage,
263
348
bootDevTools,
349
+ rebootDevTools,
264
350
metroDevice,
265
351
globalDevToolsPath,
266
352
useGlobalDevTools,
353
+ selectedDevToolsInstanceType,
354
+ setDevToolsInstance,
267
355
toggleUseGlobalDevTools,
268
356
} ;
269
357
}
270
358
271
359
export 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 ( ) {
272
369
const instance = usePlugin ( devicePlugin ) ;
370
+ const globalDevToolsPath = useValue ( instance . globalDevToolsPath ) ;
273
371
const connectionStatus = useValue ( instance . connectionStatus ) ;
274
372
const statusMessage = useValue ( instance . statusMessage ) ;
275
- const globalDevToolsPath = useValue ( instance . globalDevToolsPath ) ;
276
373
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
+ }
277
416
278
417
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 >
309
421
) : 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 >
312
435
) ;
313
436
}
0 commit comments