Summary: Changelog: Migrate from react-devtools-core to -react-devtools-inline Technical design doc: https://docs.google.com/document/d/1STUSUhXzrW_KkvqSu7Ge-rxjVFF7oU3_NbwzimkO_Z4 At this point, React DevTools doe snot support globally installed DevTools. Only the bundled version. The support for the globally installed DevTools comes in the subsequent diffs along with on-the-fly transpilation. Reviewed By: mweststrate Differential Revision: D34926472 fbshipit-source-id: fde1d4cf386adfbf8a8581ee5a54e950d2cb34ef
384 lines
11 KiB
TypeScript
384 lines
11 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
*/
|
|
|
|
import {createRoot} from 'react-dom/client';
|
|
import {
|
|
Layout,
|
|
usePlugin,
|
|
DevicePluginClient,
|
|
createState,
|
|
useValue,
|
|
Toolbar,
|
|
} from 'flipper-plugin';
|
|
import React from 'react';
|
|
import {Button, message, Switch, Typography} from 'antd';
|
|
// @ts-expect-error
|
|
import * as ReactDevToolsOSS from 'react-devtools-inline/frontend';
|
|
import {DevToolsEmbedder} from './DevToolsEmbedder';
|
|
import {Events, Methods} from './contract';
|
|
|
|
const DEV_TOOLS_NODE_ID = 'reactdevtools-out-of-react-node';
|
|
const CONNECTED = 'DevTools connected';
|
|
|
|
enum ConnectionStatus {
|
|
None = 'None',
|
|
Initializing = 'Initializing...',
|
|
WaitingForReload = 'Waiting for connection from device...',
|
|
WaitingForMetroReload = 'Waiting for Metro to reload...',
|
|
Connected = 'Connected',
|
|
Error = 'Error',
|
|
}
|
|
|
|
type DevToolsInstanceType = 'global' | 'oss';
|
|
type DevToolsInstance = {
|
|
type: DevToolsInstanceType;
|
|
module: any;
|
|
};
|
|
|
|
export function devicePlugin(client: DevicePluginClient<Events, Methods>) {
|
|
const metroDevice = client.device;
|
|
|
|
const statusMessage = createState('Empty');
|
|
const connectionStatus = createState<ConnectionStatus>(ConnectionStatus.None);
|
|
const initialized = createState(false);
|
|
|
|
const globalDevToolsAvailable = createState(false);
|
|
let globalDevToolsInstance: DevToolsInstance | undefined;
|
|
const useGlobalDevTools = createState(false, {
|
|
persist: 'useGlobalDevTools',
|
|
persistToLocalStorage: true,
|
|
});
|
|
|
|
let devToolsInstance: DevToolsInstance | undefined;
|
|
const selectedDevToolsInstanceType = createState<DevToolsInstanceType>('oss');
|
|
|
|
let pollHandle: NodeJS.Timeout | undefined = undefined;
|
|
|
|
let metroReloadAttempts = 0;
|
|
|
|
async function maybeGetInitialGlobalDevTools(): Promise<DevToolsInstance> {
|
|
console.debug(
|
|
'flipper-plugin-react-devtools.maybeGetInitialGlobalDevTools',
|
|
);
|
|
try {
|
|
const newGlobalDevToolsSource = await client.sendToServerAddOn(
|
|
'globalDevTools',
|
|
);
|
|
|
|
if (newGlobalDevToolsSource) {
|
|
globalDevToolsInstance = {
|
|
type: 'global',
|
|
// eslint-disable-next-line no-eval
|
|
module: eval(newGlobalDevToolsSource),
|
|
};
|
|
|
|
globalDevToolsAvailable.set(true);
|
|
}
|
|
} catch (e) {
|
|
console.error(
|
|
'flipper-plugin-react-devtools.maybeGetInitialGlobalDevTools -> failed to load global devtools',
|
|
e,
|
|
);
|
|
}
|
|
|
|
if (useGlobalDevTools.get() && globalDevToolsInstance) {
|
|
console.debug(
|
|
'flipper-plugin-react-devtools.maybeGetInitialGlobalDevTools -> using global devtools',
|
|
);
|
|
return globalDevToolsInstance;
|
|
}
|
|
|
|
useGlobalDevTools.set(false); // disable in case it was enabled
|
|
console.debug(
|
|
'flipper-plugin-react-devtools.maybeGetInitialGlobalDevTools -> using OSS devtools',
|
|
);
|
|
return {type: 'oss', module: ReactDevToolsOSS};
|
|
}
|
|
|
|
function getDevToolsInstance(
|
|
instanceType: DevToolsInstanceType,
|
|
): DevToolsInstance {
|
|
let module;
|
|
switch (instanceType) {
|
|
case 'global':
|
|
module = globalDevToolsInstance!.module;
|
|
break;
|
|
case 'oss':
|
|
module = ReactDevToolsOSS;
|
|
break;
|
|
}
|
|
return {
|
|
type: instanceType,
|
|
module,
|
|
};
|
|
}
|
|
|
|
async function toggleUseGlobalDevTools() {
|
|
if (!globalDevToolsInstance) {
|
|
message.warn(
|
|
"No globally installed react-devtools package found. Run 'npm install -g react-devtools'.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
selectedDevToolsInstanceType.update((prev: DevToolsInstanceType) => {
|
|
devToolsInstance = getDevToolsInstance(
|
|
prev === 'global' ? 'oss' : 'global',
|
|
);
|
|
return devToolsInstance.type;
|
|
});
|
|
useGlobalDevTools.update((v) => !v);
|
|
|
|
await rebootDevTools();
|
|
}
|
|
|
|
async function rebootDevTools() {
|
|
metroReloadAttempts = 0;
|
|
setStatus(ConnectionStatus.None, 'Loading DevTools...');
|
|
// clean old instance
|
|
if (pollHandle) {
|
|
clearTimeout(pollHandle);
|
|
}
|
|
await bootDevTools();
|
|
}
|
|
|
|
async function bootDevTools() {
|
|
if (connectionStatus.get() !== ConnectionStatus.None) {
|
|
return;
|
|
}
|
|
|
|
if (!initialized.get()) {
|
|
console.debug(
|
|
'flipper-plugin-react-devtools -> waiting for initialization',
|
|
);
|
|
await new Promise<void>((resolve) =>
|
|
initialized.subscribe((newInitialized) => {
|
|
if (newInitialized) {
|
|
resolve();
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
const devToolsNode = document.getElementById(DEV_TOOLS_NODE_ID);
|
|
if (!devToolsNode) {
|
|
setStatus(ConnectionStatus.Error, 'Failed to find target DOM Node');
|
|
return;
|
|
}
|
|
|
|
if (devtoolsHaveStarted()) {
|
|
setStatus(ConnectionStatus.Connected, CONNECTED);
|
|
return;
|
|
}
|
|
|
|
// They're new!
|
|
try {
|
|
console.debug('flipper-plugin-react-devtools -> waiting for device');
|
|
setStatus(ConnectionStatus.Initializing, 'Waiting for device...');
|
|
client.onServerAddOnMessage('connected', () => {
|
|
if (pollHandle) {
|
|
clearTimeout(pollHandle);
|
|
}
|
|
|
|
console.debug('flipper-plugin-react-devtools -> device found');
|
|
setStatus(
|
|
ConnectionStatus.Initializing,
|
|
'Device found. Initializing frontend...',
|
|
);
|
|
|
|
const wall = {
|
|
listen(listener: any) {
|
|
client.onServerAddOnMessage('message', (data) => {
|
|
console.debug(
|
|
'flipper-plugin-react-devtools.onServerAddOnMessage',
|
|
data,
|
|
);
|
|
listener(data);
|
|
});
|
|
},
|
|
send(event: any, payload: any) {
|
|
const data = {event, payload};
|
|
client.sendToServerAddOn('message', data);
|
|
},
|
|
};
|
|
|
|
const bridge = devToolsInstance!.module.createBridge(window, wall);
|
|
const store = devToolsInstance!.module.createStore(bridge);
|
|
|
|
const DevTools = devToolsInstance!.module.initialize(window, {
|
|
bridge,
|
|
store,
|
|
});
|
|
|
|
const root = createRoot(devToolsNode);
|
|
root.render(React.createElement(DevTools));
|
|
|
|
console.debug('flipper-plugin-react-devtools -> connected');
|
|
setStatus(ConnectionStatus.Connected, 'Connected');
|
|
});
|
|
|
|
startPollForConnection();
|
|
} catch (e) {
|
|
console.error('Failed to initalize React DevTools' + e);
|
|
setStatus(ConnectionStatus.Error, 'Failed to initialize DevTools: ' + e);
|
|
}
|
|
}
|
|
|
|
function setStatus(cs: ConnectionStatus, status: string) {
|
|
connectionStatus.set(cs);
|
|
if (status.startsWith('The server is listening on')) {
|
|
statusMessage.set(status + ' Waiting for connection...');
|
|
} else {
|
|
statusMessage.set(status);
|
|
}
|
|
}
|
|
|
|
function startPollForConnection(delay = 3000) {
|
|
pollHandle = setTimeout(async () => {
|
|
switch (true) {
|
|
// Found DevTools!
|
|
case devtoolsHaveStarted():
|
|
setStatus(ConnectionStatus.Connected, CONNECTED);
|
|
return;
|
|
// Waiting for connection, but we do have an active Metro connection, lets force a reload to enter Dev Mode on app
|
|
// prettier-ignore
|
|
case connectionStatus.get() === ConnectionStatus.Initializing: {
|
|
if (metroDevice) {
|
|
const nextConnectionStatus = metroReloadAttempts === 0 ? ConnectionStatus.Initializing : ConnectionStatus.WaitingForMetroReload;
|
|
metroReloadAttempts++;
|
|
setStatus(
|
|
nextConnectionStatus,
|
|
"Sending 'reload' to Metro to force DevTools to connect...",
|
|
);
|
|
metroDevice.sendMetroCommand('reload');
|
|
startPollForConnection(3000);
|
|
return;
|
|
}
|
|
|
|
// Waiting for initial connection, but no WS bridge available
|
|
setStatus(
|
|
ConnectionStatus.WaitingForReload,
|
|
"DevTools is unable to connect yet. Please trigger the DevMenu in the RN app, or reload it to connect.",
|
|
);
|
|
startPollForConnection(10000);
|
|
return;
|
|
}
|
|
// Still nothing? Users might not have done manual action, or some other tools have picked it up?
|
|
case connectionStatus.get() === ConnectionStatus.WaitingForReload:
|
|
case connectionStatus.get() === ConnectionStatus.WaitingForMetroReload:
|
|
setStatus(
|
|
ConnectionStatus.WaitingForReload,
|
|
'DevTools is unable to connect yet. Check for other instances, trigger the DevMenu in the RN app, or reload it to connect.',
|
|
);
|
|
startPollForConnection();
|
|
return;
|
|
}
|
|
}, delay);
|
|
}
|
|
|
|
function devtoolsHaveStarted() {
|
|
return (
|
|
(document.getElementById(DEV_TOOLS_NODE_ID)?.childElementCount ?? 0) > 0
|
|
);
|
|
}
|
|
|
|
client.onReady(() => {
|
|
client.onServerAddOnStart(async () => {
|
|
devToolsInstance = await maybeGetInitialGlobalDevTools();
|
|
initialized.set(true);
|
|
});
|
|
});
|
|
|
|
client.onActivate(() => {
|
|
client.onServerAddOnStart(async () => {
|
|
bootDevTools();
|
|
});
|
|
});
|
|
|
|
client.onDeactivate(() => {
|
|
if (pollHandle) {
|
|
clearTimeout(pollHandle);
|
|
}
|
|
});
|
|
|
|
return {
|
|
devtoolsHaveStarted,
|
|
connectionStatus,
|
|
statusMessage,
|
|
bootDevTools,
|
|
rebootDevTools,
|
|
metroDevice,
|
|
globalDevToolsAvailable,
|
|
useGlobalDevTools,
|
|
selectedDevToolsInstanceType,
|
|
toggleUseGlobalDevTools,
|
|
initialized,
|
|
};
|
|
}
|
|
|
|
export function Component() {
|
|
const instance = usePlugin(devicePlugin);
|
|
const globalDevToolsAvailable = useValue(instance.globalDevToolsAvailable);
|
|
const connectionStatus = useValue(instance.connectionStatus);
|
|
|
|
const displayToolbar =
|
|
globalDevToolsAvailable || connectionStatus !== ConnectionStatus.Connected;
|
|
|
|
return (
|
|
<>
|
|
<DevToolsInstanceToolbar />
|
|
<DevToolsEmbedder
|
|
offset={displayToolbar ? 40 : 0}
|
|
nodeId={DEV_TOOLS_NODE_ID}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function DevToolsInstanceToolbar() {
|
|
const instance = usePlugin(devicePlugin);
|
|
const globalDevToolsAvailable = useValue(instance.globalDevToolsAvailable);
|
|
const connectionStatus = useValue(instance.connectionStatus);
|
|
const statusMessage = useValue(instance.statusMessage);
|
|
const useGlobalDevTools = useValue(instance.useGlobalDevTools);
|
|
const initialized = useValue(instance.initialized);
|
|
|
|
const selectionControl = globalDevToolsAvailable ? (
|
|
<>
|
|
<Switch
|
|
checked={useGlobalDevTools}
|
|
onChange={instance.toggleUseGlobalDevTools}
|
|
size="small"
|
|
disabled={!initialized}
|
|
/>
|
|
Use globally installed DevTools
|
|
</>
|
|
) : null;
|
|
|
|
return (
|
|
<Layout.Container grow>
|
|
<Toolbar right={selectionControl} wash>
|
|
<Typography.Text type="secondary">{statusMessage}</Typography.Text>
|
|
{connectionStatus === ConnectionStatus.WaitingForReload ||
|
|
connectionStatus === ConnectionStatus.WaitingForMetroReload ||
|
|
connectionStatus === ConnectionStatus.Error ? (
|
|
<Button
|
|
size="small"
|
|
onClick={() => {
|
|
instance.metroDevice?.sendMetroCommand('reload');
|
|
instance.rebootDevTools();
|
|
}}>
|
|
Retry
|
|
</Button>
|
|
) : null}
|
|
</Toolbar>
|
|
</Layout.Container>
|
|
);
|
|
}
|