Skip to content
This repository was archived by the owner on Nov 22, 2024. It is now read-only.

Commit f9547e0

Browse files
Juan Tejadafacebook-github-bot
Juan Tejada
authored andcommitted
Allow switching between internal and OSS DevTools (#3139)
Summary: Pull Request resolved: #3139 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. Reviewed By: lunaruan, mweststrate Differential Revision: D32773200 fbshipit-source-id: aa15ffecba7b2b2ea74e109e9f16334d47bf5868
1 parent 618670d commit f9547e0

File tree

2 files changed

+204
-67
lines changed

2 files changed

+204
-67
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
*/
9+
10+
export function getInternalDevToolsModule<TModule>(): TModule {
11+
throw new Error(
12+
"Can't require internal version of React DevTools from public version of Flipper.",
13+
);
14+
}

desktop/plugins/public/reactdevtools/index.tsx

+190-67
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ 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';
27+
import {getInternalDevToolsModule} from './fb-stubs/getInternalDevToolsModule';
2728

2829
const DEV_TOOLS_NODE_ID = 'reactdevtools-out-of-react-node';
2930
const CONNECTED = 'DevTools connected';
@@ -55,10 +56,17 @@ function findGlobalDevTools(): Promise<string | undefined> {
5556
enum 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+
6270
export 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

271359
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() {
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

Comments
 (0)