Skip to content

Commit 0a3a72d

Browse files
Juan Tejadafacebook-github-bot
authored andcommitted
Allow switching between internal and OSS DevTools
Summary: This commit adds new UI in the top level toolbar to allow internal FB users to switch between the internal build of devtools and the OSS one. ## Scenarios **Internal (when `client.isFB`)** - DevTools version will default to the internal version, and will render a `Select` component with option to switch to the OSS version. - If a global install of DevTools is present, the selection menu will also offer the option to switch to the global DevTools version. **External (when `!client.isFB`)** Will preserve previous behavior: - Uses the OSS version by default, and doesn't provide option to switch to internal version. - If a global installation is present, will render a `Switch` component that allows switching between OSS and global installation. ### Implementation This commit refactors some parts of the DevTools plugin to provide a bit more clarity in the loading sequence by renaming and modifying some of the messaging, and fixing lint warnings. A change introduced here is that when switching or loading devtools, when we attempt to reload the device via Metro, don't immediately show a "Retry" button, since at that point nothing has gone wrong, and the Retry button will only occur if the Metro reload doesn't occur after a few seconds. In a future commit, this [PR in Devtools](facebook/react#22848) will allow us to clear any loading messages once DevTools has successfully connected. Differential Revision: D32773200 fbshipit-source-id: 05da664bfabff92597d7a582d410655c4cec5429
1 parent 5e11cbd commit 0a3a72d

File tree

1 file changed

+193
-67
lines changed
  • desktop/plugins/public/reactdevtools

1 file changed

+193
-67
lines changed

desktop/plugins/public/reactdevtools/index.tsx

Lines changed: 193 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from 'flipper-plugin';
2121
import React from 'react';
2222
import getPort from 'get-port';
23-
import {Button, message, Switch, Typography} from 'antd';
23+
import {Button, Select, message, Switch, Typography} from 'antd';
2424
import child_process from 'child_process';
2525
import fs from 'fs';
2626
import {DevToolsEmbedder} from './DevToolsEmbedder';
@@ -55,10 +55,17 @@ function findGlobalDevTools(): Promise<string | undefined> {
5555
enum ConnectionStatus {
5656
Initializing = 'Initializing...',
5757
WaitingForReload = 'Waiting for connection from device...',
58+
WaitingForMetroReload = 'Waiting for Metro to reload...',
5859
Connected = 'Connected',
5960
Error = 'Error',
6061
}
6162

63+
type DevToolsInstanceType = 'global' | 'internal' | 'oss';
64+
type DevToolsInstance = {
65+
type: DevToolsInstanceType;
66+
module: ReactDevToolsStandaloneType;
67+
};
68+
6269
export function devicePlugin(client: DevicePluginClient) {
6370
const metroDevice = client.device;
6471

@@ -72,28 +79,91 @@ export function devicePlugin(client: DevicePluginClient) {
7279
persistToLocalStorage: true,
7380
});
7481

75-
let devToolsInstance = getDefaultDevToolsModule();
82+
let devToolsInstance = getDefaultDevToolsInstance();
83+
const selectedDevToolsInstanceType = createState<DevToolsInstanceType>(
84+
devToolsInstance.type,
85+
);
7686

7787
let startResult: {close(): void} | undefined = undefined;
7888

7989
let pollHandle: NodeJS.Timeout | undefined = undefined;
8090

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 {
82109
// Load right library
83110
if (useGlobalDevTools.get()) {
84-
const module = global.electronRequire(globalDevToolsPath.get()!);
85-
return module.default ?? module;
111+
return {
112+
type: 'global',
113+
module: getGlobalDevToolsModule(),
114+
};
86115
} 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;
88142
}
143+
return {
144+
type: instanceType,
145+
module,
146+
};
89147
}
90148

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();
97167
}
98168

99169
async function toggleUseGlobalDevTools() {
@@ -103,18 +173,29 @@ export function devicePlugin(client: DevicePluginClient) {
103173
);
104174
return;
105175
}
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+
});
106185
useGlobalDevTools.update((v) => !v);
107186

108-
devToolsInstance = getDevToolsModule();
187+
await rebootDevTools();
188+
}
109189

110-
statusMessage.set('Switching devTools');
111-
connectionStatus.set(ConnectionStatus.Initializing);
190+
async function rebootDevTools() {
191+
metroReloadAttempts = 0;
192+
setStatus(ConnectionStatus.Initializing, 'Switching DevTools...');
112193
// clean old instance
113194
if (pollHandle) {
114195
clearTimeout(pollHandle);
115196
}
116197
startResult?.close();
117-
await sleep(1000); // wait for port to close
198+
await sleep(5000); // wait for port to close
118199
startResult = undefined;
119200
await bootDevTools();
120201
}
@@ -152,24 +233,24 @@ export function devicePlugin(client: DevicePluginClient) {
152233
}
153234
setStatus(
154235
ConnectionStatus.Initializing,
155-
'Starting DevTools server on ' + port,
236+
'Starting DevTools server on ' + DEV_TOOLS_PORT,
156237
);
157-
startResult = devToolsInstance
238+
startResult = devToolsInstance.module
158239
.setContentDOMNode(devToolsNode)
159240
.setStatusListener((status: string) => {
160241
// TODO: since devToolsInstance is an instance, we are probably leaking memory here
161242
setStatus(ConnectionStatus.Initializing, status);
162243
})
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...');
165246
} catch (e) {
166247
console.error('Failed to initalize React DevTools' + e);
167248
setStatus(ConnectionStatus.Error, 'Failed to initialize DevTools: ' + e);
168249
}
169250

170251
setStatus(
171252
ConnectionStatus.Initializing,
172-
'DevTools have been initialized, waiting for connection...',
253+
'DevTools initialized, waiting for connection...',
173254
);
174255
if (devtoolsHaveStarted()) {
175256
setStatus(ConnectionStatus.Connected, CONNECTED);
@@ -196,27 +277,33 @@ export function devicePlugin(client: DevicePluginClient) {
196277
return;
197278
// Waiting for connection, but we do have an active Metro connection, lets force a reload to enter Dev Mode on app
198279
// 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
209294
setStatus(
210295
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.",
212297
);
213298
startPollForConnection(10000);
214299
return;
300+
}
215301
// Still nothing? Users might not have done manual action, or some other tools have picked it up?
216302
case connectionStatus.get() === ConnectionStatus.WaitingForReload:
303+
case connectionStatus.get() === ConnectionStatus.WaitingForMetroReload:
217304
setStatus(
218305
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.",
220307
);
221308
startPollForConnection();
222309
return;
@@ -234,9 +321,10 @@ export function devicePlugin(client: DevicePluginClient) {
234321
const path = await findGlobalDevTools();
235322
if (path) {
236323
globalDevToolsPath.set(path + '/standalone');
324+
selectedDevToolsInstanceType.set('global');
237325
console.log('Found global React DevTools: ', path);
238326
// load it, if the flag is set
239-
devToolsInstance = getDevToolsModule();
327+
devToolsInstance = getInitialDevToolsInstance();
240328
} else {
241329
useGlobalDevTools.set(false); // disable in case it was enabled
242330
}
@@ -257,57 +345,95 @@ export function devicePlugin(client: DevicePluginClient) {
257345
});
258346

259347
return {
348+
isFB: client.isFB,
260349
devtoolsHaveStarted,
261350
connectionStatus,
262351
statusMessage,
263352
bootDevTools,
353+
rebootDevTools,
264354
metroDevice,
265355
globalDevToolsPath,
266356
useGlobalDevTools,
357+
selectedDevToolsInstanceType,
358+
setDevToolsInstance,
267359
toggleUseGlobalDevTools,
268360
};
269361
}
270362

271363
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() {
272373
const instance = usePlugin(devicePlugin);
374+
const globalDevToolsPath = useValue(instance.globalDevToolsPath);
273375
const connectionStatus = useValue(instance.connectionStatus);
274376
const statusMessage = useValue(instance.statusMessage);
275-
const globalDevToolsPath = useValue(instance.globalDevToolsPath);
276377
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+
}
277420

278421
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>
309425
) : 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>
312438
);
313439
}

0 commit comments

Comments
 (0)