Allow using global devTools

Summary:
Currently Flipper embeds the React devTools, and as a result the version of the React devTools is strictly coupled to the Flipper version. This is troublesome when connecting to (slightly) older React Native versions, that use a different version of the tools. That results in errors like this one:

{F615263497}

This diff introduces a feature to use globally installed devTools instead of the embedded ones, giving users the flexibility to pick their own version.

{F615263580}

This addresses

https://fb.workplace.com/groups/flippersupport/permalink/1125669971246993/
https://github.com/facebook/flipper/issues/2250
https://github.com/facebook/flipper/issues/2224

Changelog: [React DevTools] It is now possible to switch between the embedded and globally installed version of the React DevTools. This will enable the React DevTools to connect to older RN versions. Fixes #2250, #2224

Reviewed By: passy

Differential Revision: D28382586

fbshipit-source-id: a5386a5043933acda5aab2db74078bf7ceb105ca
This commit is contained in:
Michel Weststrate
2021-05-12 14:20:57 -07:00
committed by Facebook GitHub Bot
parent 4062950fbe
commit ab17bbd555
2 changed files with 89 additions and 4 deletions

View File

@@ -16,10 +16,14 @@ import {
useValue, useValue,
theme, theme,
sleep, sleep,
Toolbar,
} from 'flipper-plugin'; } from 'flipper-plugin';
import React, {createRef, useEffect} from 'react'; import React, {createRef, useEffect} from 'react';
import getPort from 'get-port'; import getPort from 'get-port';
import {Alert, Button} from 'antd'; import {Alert, Button, Switch} from 'antd';
import child_process from 'child_process';
import fs from 'fs';
import path from 'path';
const DEV_TOOLS_NODE_ID = 'reactdevtools-out-of-react-node'; const DEV_TOOLS_NODE_ID = 'reactdevtools-out-of-react-node';
@@ -28,6 +32,29 @@ interface MetroDevice {
sendCommand(command: string, params?: any): void; sendCommand(command: string, params?: any): void;
} }
function findGlobalDevTools(): Promise<string | undefined> {
return new Promise((resolve) => {
child_process.exec('npm root -g', (error, basePath) => {
if (error) {
console.warn(
'Failed to find globally installed React DevTools: ' + error,
);
resolve(undefined);
} else {
const devToolsPath = path.join(
basePath.trim(),
'react-devtools',
'node_modules',
'react-devtools-core',
);
fs.stat(devToolsPath, (err, stats) => {
resolve(!err && stats ? devToolsPath : undefined);
});
}
});
});
}
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;
@@ -77,17 +104,42 @@ export function devicePlugin(client: DevicePluginClient) {
const connectionStatus = createState<ConnectionStatus>( const connectionStatus = createState<ConnectionStatus>(
ConnectionStatus.Initializing, ConnectionStatus.Initializing,
); );
const globalDevToolsPath = createState<string>();
const useGlobalDevTools = createState(false); // TODO: store in local storage T69989583
let devToolsInstance: typeof ReactDevToolsStandalone =
ReactDevToolsStandalone;
let startResult: {close(): void} | undefined = undefined;
const containerRef = createRef<HTMLDivElement>(); const containerRef = createRef<HTMLDivElement>();
let pollHandle: NodeJS.Timeout | undefined = undefined; let pollHandle: NodeJS.Timeout | undefined = undefined;
let isMounted = false; let isMounted = false;
async function toggleUseGlobalDevTools() {
if (!globalDevToolsPath.get()) {
return;
}
useGlobalDevTools.update((v) => !v);
if (useGlobalDevTools.get()) {
console.log('Loading ' + globalDevToolsPath.get());
devToolsInstance = global.electronRequire(
globalDevToolsPath.get()!,
).default;
} else {
devToolsInstance = ReactDevToolsStandalone;
}
startResult?.close();
stopDevtools();
findDevToolsNode()!.remove();
await bootDevTools();
}
async function bootDevTools() { async function bootDevTools() {
isMounted = true; isMounted = true;
let devToolsNode = findDevToolsNode(); let devToolsNode = findDevToolsNode();
if (!devToolsNode) { if (!devToolsNode) {
devToolsNode = createDevToolsNode(); devToolsNode = createDevToolsNode();
} }
attachDevTools(containerRef.current!, devToolsNode); attachDevTools(containerRef.current!, devToolsNode);
initializeDevTools(devToolsNode); initializeDevTools(devToolsNode);
setStatus( setStatus(
@@ -169,11 +221,12 @@ export function devicePlugin(client: DevicePluginClient) {
); );
// Currently a new port is negotatiated every time the plugin is opened. // Currently a new port is negotatiated every time the plugin is opened.
// This can be potentially optimized by keeping the devTools instance around // This can be potentially optimized by keeping the devTools instance around
ReactDevToolsStandalone.setContentDOMNode(devToolsNode) startResult = devToolsInstance
.setContentDOMNode(devToolsNode)
.setStatusListener((status) => { .setStatusListener((status) => {
setStatus(ConnectionStatus.Initializing, status); setStatus(ConnectionStatus.Initializing, status);
}) })
.startServer(port); .startServer(port) as any;
setStatus(ConnectionStatus.Initializing, 'Waiting for device'); setStatus(ConnectionStatus.Initializing, 'Waiting for device');
// This is a hack that should be cleaned up. Instead of setting up port forwarding // This is a hack that should be cleaned up. Instead of setting up port forwarding
@@ -211,6 +264,16 @@ export function devicePlugin(client: DevicePluginClient) {
} }
} }
client.onReady(() => {
console.log('searching');
findGlobalDevTools().then((path) => {
globalDevToolsPath.set(path + '/standalone');
if (path) {
console.log('Found global React DevTools: ', path);
}
});
});
return { return {
devtoolsHaveStarted, devtoolsHaveStarted,
connectionStatus, connectionStatus,
@@ -219,6 +282,9 @@ export function devicePlugin(client: DevicePluginClient) {
metroDevice, metroDevice,
containerRef, containerRef,
stopDevtools, stopDevtools,
globalDevToolsPath,
useGlobalDevTools,
toggleUseGlobalDevTools,
}; };
} }
@@ -226,6 +292,8 @@ export function Component() {
const instance = usePlugin(devicePlugin); const instance = usePlugin(devicePlugin);
const connectionStatus = useValue(instance.connectionStatus); const connectionStatus = useValue(instance.connectionStatus);
const statusMessage = useValue(instance.statusMessage); const statusMessage = useValue(instance.statusMessage);
const globalDevToolsPath = useValue(instance.globalDevToolsPath);
const useGlobalDevTools = useValue(instance.useGlobalDevTools);
useEffect(() => { useEffect(() => {
instance.bootDevTools(); instance.bootDevTools();
@@ -234,6 +302,19 @@ export function Component() {
return ( return (
<Layout.Container grow> <Layout.Container grow>
{globalDevToolsPath ? (
<Toolbar
right={
<>
<Switch
checked={useGlobalDevTools}
onChange={instance.toggleUseGlobalDevTools}
/>
Use globally installed DevTools
</>
}
/>
) : null}
{!instance.devtoolsHaveStarted() ? ( {!instance.devtoolsHaveStarted() ? (
<Layout.Container <Layout.Container
style={{width: 400, margin: `${theme.space.large}px auto`}}> style={{width: 400, margin: `${theme.space.large}px auto`}}>

View File

@@ -185,7 +185,11 @@ That is correct, the dependencies won't be actually included in the release (whe
#### Q: Cannot inspect an element in the React DevTools: "Could not inspect element with id ..." #### Q: Cannot inspect an element in the React DevTools: "Could not inspect element with id ..."
The "Could not inspect element with id XXX" error will appear when selecting a specific element in the React DevTools, when the version of the DevTools shipped in Flipper is incompatible with the `react-devtools-core` package used by the React Native application. The "Could not inspect element with id XXX" error will appear when selecting a specific element in the React DevTools, when the version of the DevTools shipped in Flipper is incompatible with the `react-devtools-core` package used by the React Native application.
A way to fix this is to set the `resolutions` field in the `package.json` of the app to force a specific version and then run `yarn install`, for example:
Flipper supports using a globally installed `react-devtools` (after using `npm install -g react-devtools@x.x.x`) instead of the embedded one.
This should help with any compatibility issues.
Another way to fix this is to set the `resolutions` field in the `package.json` of the app to force a specific version and then run `yarn install`, for example:
```json ```json
"resolutions": { "resolutions": {