Convert React DevTools to Sandy
Summary: Converted ReactDevTools to Sandy, no real semantic changes. Will add those in next diffs. Made some minor flow optimizations. The port forwarding setup interacted directly with the Flipper store, so made an escape hatch for that. Will clean that up again in next diffs. Reviewed By: passy Differential Revision: D28380055 fbshipit-source-id: 053979fd10bf3b62089a4f1e27b0e02b4b05e2e1
This commit is contained in:
committed by
Facebook GitHub Bot
parent
e7670e4e42
commit
4062950fbe
@@ -13,6 +13,7 @@ import reducers, {Actions, State as StoreState, Store} from './reducers/index';
|
|||||||
import {stateSanitizer} from './utils/reduxDevToolsConfig';
|
import {stateSanitizer} from './utils/reduxDevToolsConfig';
|
||||||
import isProduction from './utils/isProduction';
|
import isProduction from './utils/isProduction';
|
||||||
import {_SandyPluginDefinition} from 'flipper-plugin';
|
import {_SandyPluginDefinition} from 'flipper-plugin';
|
||||||
|
|
||||||
export const store: Store = createStore<StoreState, Actions, any, any>(
|
export const store: Store = createStore<StoreState, Actions, any, any>(
|
||||||
rootReducer,
|
rootReducer,
|
||||||
// @ts-ignore Type definition mismatch
|
// @ts-ignore Type definition mismatch
|
||||||
@@ -36,3 +37,7 @@ if (!isProduction()) {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.flipperStore = store;
|
window.flipperStore = store;
|
||||||
}
|
}
|
||||||
|
// Escape hatch during Sandy conversion;
|
||||||
|
// Some plugins directly interact with the Store and need further abstractions
|
||||||
|
// @ts-ignore
|
||||||
|
window.__SECRET_FLIPPER_STORE_DONT_USE_OR_YOU_WILL_BE_FIRED__ = store;
|
||||||
|
|||||||
@@ -9,32 +9,25 @@
|
|||||||
|
|
||||||
import ReactDevToolsStandalone from 'react-devtools-core/standalone';
|
import ReactDevToolsStandalone from 'react-devtools-core/standalone';
|
||||||
import {
|
import {
|
||||||
FlipperDevicePlugin,
|
Layout,
|
||||||
AndroidDevice,
|
usePlugin,
|
||||||
styled,
|
DevicePluginClient,
|
||||||
View,
|
createState,
|
||||||
MetroDevice,
|
useValue,
|
||||||
ReduxState,
|
theme,
|
||||||
connect,
|
sleep,
|
||||||
Device,
|
} from 'flipper-plugin';
|
||||||
CenteredView,
|
import React, {createRef, useEffect} from 'react';
|
||||||
RoundedSection,
|
|
||||||
Text,
|
|
||||||
Button,
|
|
||||||
} from 'flipper';
|
|
||||||
import React, {useEffect} from 'react';
|
|
||||||
import getPort from 'get-port';
|
import getPort from 'get-port';
|
||||||
|
import {Alert, Button} from 'antd';
|
||||||
const Container = styled.div({
|
|
||||||
display: 'flex',
|
|
||||||
flex: '1 1 0%',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'stretch',
|
|
||||||
height: '100%',
|
|
||||||
});
|
|
||||||
|
|
||||||
const DEV_TOOLS_NODE_ID = 'reactdevtools-out-of-react-node';
|
const DEV_TOOLS_NODE_ID = 'reactdevtools-out-of-react-node';
|
||||||
|
|
||||||
|
interface MetroDevice {
|
||||||
|
ws?: WebSocket;
|
||||||
|
sendCommand(command: string, params?: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
function createDevToolsNode(): HTMLElement {
|
function createDevToolsNode(): HTMLElement {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.id = DEV_TOOLS_NODE_ID;
|
div.id = DEV_TOOLS_NODE_ID;
|
||||||
@@ -65,30 +58,6 @@ function detachDevTools(devToolsNode: HTMLElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CONNECTED = 'DevTools connected';
|
const CONNECTED = 'DevTools connected';
|
||||||
|
|
||||||
type GrabMetroDeviceStoreProps = {metroDevice: MetroDevice};
|
|
||||||
type GrabMetroDeviceOwnProps = {onHasDevice(device: MetroDevice): void};
|
|
||||||
|
|
||||||
// Utility component to grab the metroDevice from the store if there is one
|
|
||||||
const GrabMetroDevice = connect<
|
|
||||||
GrabMetroDeviceStoreProps,
|
|
||||||
{},
|
|
||||||
GrabMetroDeviceOwnProps,
|
|
||||||
ReduxState
|
|
||||||
>(({connections: {devices}}) => ({
|
|
||||||
metroDevice: devices.find(
|
|
||||||
(device: Device) => device.os === 'Metro' && !device.isArchived,
|
|
||||||
) as MetroDevice,
|
|
||||||
}))(function ({
|
|
||||||
metroDevice,
|
|
||||||
onHasDevice,
|
|
||||||
}: GrabMetroDeviceStoreProps & GrabMetroDeviceOwnProps) {
|
|
||||||
useEffect(() => {
|
|
||||||
onHasDevice(metroDevice);
|
|
||||||
}, [metroDevice, onHasDevice]);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const SUPPORTED_OCULUS_DEVICE_TYPES = ['quest', 'go', 'pacific'];
|
const SUPPORTED_OCULUS_DEVICE_TYPES = ['quest', 'go', 'pacific'];
|
||||||
|
|
||||||
enum ConnectionStatus {
|
enum ConnectionStatus {
|
||||||
@@ -98,184 +67,193 @@ enum ConnectionStatus {
|
|||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ReactDevTools extends FlipperDevicePlugin<
|
export function devicePlugin(client: DevicePluginClient) {
|
||||||
{status: string},
|
const metroDevice: MetroDevice = client.device.realDevice;
|
||||||
any,
|
if (!metroDevice.sendCommand || !('ws' in metroDevice)) {
|
||||||
{}
|
throw new Error('Invalid metroDevice');
|
||||||
> {
|
|
||||||
static supportsDevice(device: Device) {
|
|
||||||
return !device.isArchived && device.os === 'Metro';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pollHandle?: NodeJS.Timeout;
|
const statusMessage = createState('initializing');
|
||||||
containerRef: React.RefObject<HTMLDivElement> = React.createRef();
|
const connectionStatus = createState<ConnectionStatus>(
|
||||||
connectionStatus: ConnectionStatus = ConnectionStatus.Initializing;
|
ConnectionStatus.Initializing,
|
||||||
metroDevice?: MetroDevice;
|
);
|
||||||
|
|
||||||
|
const containerRef = createRef<HTMLDivElement>();
|
||||||
|
let pollHandle: NodeJS.Timeout | undefined = undefined;
|
||||||
|
let isMounted = false;
|
||||||
|
|
||||||
|
async function bootDevTools() {
|
||||||
isMounted = true;
|
isMounted = true;
|
||||||
|
|
||||||
state = {
|
|
||||||
status: 'initializing',
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.bootDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.isMounted = false;
|
|
||||||
if (this.pollHandle) {
|
|
||||||
clearTimeout(this.pollHandle);
|
|
||||||
}
|
|
||||||
const devToolsNode = findDevToolsNode();
|
|
||||||
devToolsNode && detachDevTools(devToolsNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus(connectionStatus: ConnectionStatus, status: string) {
|
|
||||||
this.connectionStatus = connectionStatus;
|
|
||||||
if (!this.isMounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (status.startsWith('The server is listening on')) {
|
|
||||||
this.setState({status: status + ' Waiting for connection...'});
|
|
||||||
} else {
|
|
||||||
this.setState({status});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
devtoolsHaveStarted() {
|
|
||||||
return !!findDevToolsNode()?.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
bootDevTools() {
|
|
||||||
let devToolsNode = findDevToolsNode();
|
let devToolsNode = findDevToolsNode();
|
||||||
if (!devToolsNode) {
|
if (!devToolsNode) {
|
||||||
devToolsNode = createDevToolsNode();
|
devToolsNode = createDevToolsNode();
|
||||||
this.initializeDevTools(devToolsNode);
|
|
||||||
}
|
}
|
||||||
this.setStatus(
|
attachDevTools(containerRef.current!, devToolsNode);
|
||||||
|
initializeDevTools(devToolsNode);
|
||||||
|
setStatus(
|
||||||
ConnectionStatus.Initializing,
|
ConnectionStatus.Initializing,
|
||||||
'DevTools have been initialized, waiting for connection...',
|
'DevTools have been initialized, waiting for connection...',
|
||||||
);
|
);
|
||||||
if (this.devtoolsHaveStarted()) {
|
|
||||||
this.setStatus(ConnectionStatus.Connected, CONNECTED);
|
await sleep(5); // give node time to move
|
||||||
|
if (devtoolsHaveStarted()) {
|
||||||
|
setStatus(ConnectionStatus.Connected, CONNECTED);
|
||||||
} else {
|
} else {
|
||||||
this.startPollForConnection();
|
startPollForConnection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attachDevTools(this.containerRef?.current!, devToolsNode);
|
function setStatus(cs: ConnectionStatus, status: string) {
|
||||||
this.startPollForConnection();
|
connectionStatus.set(cs);
|
||||||
|
if (!isMounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status.startsWith('The server is listening on')) {
|
||||||
|
statusMessage.set(status + ' Waiting for connection...');
|
||||||
|
} else {
|
||||||
|
statusMessage.set(status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startPollForConnection(delay = 3000) {
|
function startPollForConnection(delay = 3000) {
|
||||||
this.pollHandle = setTimeout(() => {
|
pollHandle = setTimeout(async () => {
|
||||||
switch (true) {
|
switch (true) {
|
||||||
// Closed already, ignore
|
// Closed already, ignore
|
||||||
case !this.isMounted:
|
case !isMounted:
|
||||||
return;
|
return;
|
||||||
// Found DevTools!
|
// Found DevTools!
|
||||||
case this.devtoolsHaveStarted():
|
case devtoolsHaveStarted():
|
||||||
this.setStatus(ConnectionStatus.Connected, CONNECTED);
|
setStatus(ConnectionStatus.Connected, CONNECTED);
|
||||||
return;
|
return;
|
||||||
// Waiting for connection, but we do have an active Metro connection, lets force a reload to enter Dev Mode on app
|
// Waiting for connection, but we do have an active Metro connection, lets force a reload to enter Dev Mode on app
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
case this.connectionStatus === ConnectionStatus.Initializing && !!this.metroDevice?.ws:
|
case connectionStatus.get() === ConnectionStatus.Initializing && !!metroDevice?.ws:
|
||||||
this.setStatus(
|
setStatus(
|
||||||
ConnectionStatus.WaitingForReload,
|
ConnectionStatus.WaitingForReload,
|
||||||
"Sending 'reload' to Metro to force the DevTools to connect...",
|
"Sending 'reload' to Metro to force the DevTools to connect...",
|
||||||
);
|
);
|
||||||
this.metroDevice!.sendCommand('reload');
|
metroDevice!.sendCommand('reload');
|
||||||
this.startPollForConnection(10000);
|
startPollForConnection(2000);
|
||||||
return;
|
return;
|
||||||
// Waiting for initial connection, but no WS bridge available
|
// Waiting for initial connection, but no WS bridge available
|
||||||
case this.connectionStatus === ConnectionStatus.Initializing:
|
case connectionStatus.get() === ConnectionStatus.Initializing:
|
||||||
this.setStatus(
|
setStatus(
|
||||||
ConnectionStatus.WaitingForReload,
|
ConnectionStatus.WaitingForReload,
|
||||||
"The DevTools didn't connect yet. Please trigger the DevMenu in the React Native app, or Reload it to connect",
|
"The DevTools didn't connect yet. Please trigger the DevMenu in the React Native app, or Reload it to connect",
|
||||||
);
|
);
|
||||||
this.startPollForConnection(10000);
|
startPollForConnection(10000);
|
||||||
return;
|
return;
|
||||||
// Still nothing? Users might not have done manual action, or some other tools have picked it up?
|
// Still nothing? Users might not have done manual action, or some other tools have picked it up?
|
||||||
case this.connectionStatus === ConnectionStatus.WaitingForReload:
|
case connectionStatus.get() === ConnectionStatus.WaitingForReload:
|
||||||
this.setStatus(
|
setStatus(
|
||||||
ConnectionStatus.WaitingForReload,
|
ConnectionStatus.WaitingForReload,
|
||||||
"The DevTools didn't connect yet. Please verify your React Native app is in development mode, and that no other instance of the React DevTools are attached to the app already.",
|
"The DevTools didn't connect yet. Please verify your React Native app is in development mode, and that no other instance of the React DevTools are attached to the app already.",
|
||||||
);
|
);
|
||||||
this.startPollForConnection();
|
startPollForConnection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializeDevTools(devToolsNode: HTMLElement) {
|
function devtoolsHaveStarted() {
|
||||||
|
return (findDevToolsNode()?.childElementCount ?? 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeDevTools(devToolsNode: HTMLElement) {
|
||||||
try {
|
try {
|
||||||
this.setStatus(ConnectionStatus.Initializing, 'Waiting for port 8097');
|
setStatus(ConnectionStatus.Initializing, 'Waiting for port 8097');
|
||||||
const port = await getPort({port: 8097}); // default port for dev tools
|
const port = await getPort({port: 8097}); // default port for dev tools
|
||||||
this.setStatus(
|
setStatus(
|
||||||
ConnectionStatus.Initializing,
|
ConnectionStatus.Initializing,
|
||||||
'Starting DevTools server on ' + port,
|
'Starting DevTools server on ' + port,
|
||||||
);
|
);
|
||||||
|
// Currently a new port is negotatiated every time the plugin is opened.
|
||||||
|
// This can be potentially optimized by keeping the devTools instance around
|
||||||
ReactDevToolsStandalone.setContentDOMNode(devToolsNode)
|
ReactDevToolsStandalone.setContentDOMNode(devToolsNode)
|
||||||
.setStatusListener((status) => {
|
.setStatusListener((status) => {
|
||||||
this.setStatus(ConnectionStatus.Initializing, status);
|
setStatus(ConnectionStatus.Initializing, status);
|
||||||
})
|
})
|
||||||
.startServer(port);
|
.startServer(port);
|
||||||
this.setStatus(ConnectionStatus.Initializing, 'Waiting for device');
|
setStatus(ConnectionStatus.Initializing, 'Waiting for device');
|
||||||
const device = this.device;
|
|
||||||
|
|
||||||
if (device) {
|
// This is a hack that should be cleaned up. Instead of setting up port forwarding
|
||||||
|
// for any physical android device, we should introduce a mechanism to detect all connected
|
||||||
|
// metro apps, and connect to one of them.
|
||||||
|
// Since this is not how we want (or can) reliably detect the device we intend to interact with,
|
||||||
|
// leaving this here until we can get a list of connected applications & ports from Metro or Flipper
|
||||||
|
(window as any).__SECRET_FLIPPER_STORE_DONT_USE_OR_YOU_WILL_BE_FIRED__
|
||||||
|
.getState()
|
||||||
|
.connections.devices.forEach((d: any) => {
|
||||||
if (
|
if (
|
||||||
device.deviceType === 'physical' ||
|
(d.deviceType === 'physical' && d.os === 'Android') ||
|
||||||
SUPPORTED_OCULUS_DEVICE_TYPES.includes(device.title.toLowerCase())
|
SUPPORTED_OCULUS_DEVICE_TYPES.includes(d.title.toLowerCase())
|
||||||
) {
|
) {
|
||||||
this.setStatus(
|
console.log(
|
||||||
ConnectionStatus.Initializing,
|
`[React DevTools] Forwarding port ${port} for device ${d.title}`,
|
||||||
`Setting up reverse port mapping: ${port}:${port}`,
|
|
||||||
);
|
);
|
||||||
(device as AndroidDevice).reverse([port, port]);
|
d.reverse([port]);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error('Failed to initalize React DevTools' + e);
|
||||||
this.setStatus(
|
setStatus(ConnectionStatus.Error, 'Failed to initialize DevTools: ' + e);
|
||||||
ConnectionStatus.Error,
|
|
||||||
'Failed to initialize DevTools: ' + e,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
function stopDevtools() {
|
||||||
return (
|
isMounted = false;
|
||||||
<View grow>
|
if (pollHandle) {
|
||||||
{!this.devtoolsHaveStarted() ? this.renderStatus() : null}
|
clearTimeout(pollHandle);
|
||||||
<Container ref={this.containerRef} />
|
}
|
||||||
<GrabMetroDevice
|
const devToolsNode = findDevToolsNode();
|
||||||
onHasDevice={(device) => {
|
if (devToolsNode) {
|
||||||
this.metroDevice = device;
|
detachDevTools(devToolsNode);
|
||||||
}}
|
}
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderStatus() {
|
return {
|
||||||
|
devtoolsHaveStarted,
|
||||||
|
connectionStatus,
|
||||||
|
statusMessage,
|
||||||
|
bootDevTools,
|
||||||
|
metroDevice,
|
||||||
|
containerRef,
|
||||||
|
stopDevtools,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Component() {
|
||||||
|
const instance = usePlugin(devicePlugin);
|
||||||
|
const connectionStatus = useValue(instance.connectionStatus);
|
||||||
|
const statusMessage = useValue(instance.statusMessage);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
instance.bootDevTools();
|
||||||
|
return instance.stopDevtools;
|
||||||
|
}, [instance]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredView>
|
<Layout.Container grow>
|
||||||
<RoundedSection title={this.connectionStatus}>
|
{!instance.devtoolsHaveStarted() ? (
|
||||||
<Text>{this.state.status}</Text>
|
<Layout.Container
|
||||||
{(this.connectionStatus === ConnectionStatus.WaitingForReload &&
|
style={{width: 400, margin: `${theme.space.large}px auto`}}>
|
||||||
this.metroDevice?.ws) ||
|
<Alert message={statusMessage} type="warning" showIcon>
|
||||||
this.connectionStatus === ConnectionStatus.Error ? (
|
{(connectionStatus === ConnectionStatus.WaitingForReload &&
|
||||||
|
instance.metroDevice?.ws) ||
|
||||||
|
connectionStatus === ConnectionStatus.Error ? (
|
||||||
<Button
|
<Button
|
||||||
style={{width: 200, margin: '10px auto 0 auto'}}
|
style={{width: 200, margin: '10px auto 0 auto'}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.metroDevice?.sendCommand('reload');
|
instance.metroDevice?.sendCommand('reload');
|
||||||
this.bootDevTools();
|
instance.bootDevTools();
|
||||||
}}>
|
}}>
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</RoundedSection>
|
</Alert>
|
||||||
</CenteredView>
|
</Layout.Container>
|
||||||
|
) : null}
|
||||||
|
<Layout.Container grow ref={instance.containerRef} />
|
||||||
|
</Layout.Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -26,5 +26,8 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"email": "danielbuechele@fb.com"
|
"email": "danielbuechele@fb.com"
|
||||||
},
|
},
|
||||||
"devDependencies": {}
|
"devDependencies": {},
|
||||||
|
"peerDependencies": {
|
||||||
|
"flipper-plugin": "*"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user