diff --git a/desktop/plugins/public/reactdevtools/index.tsx b/desktop/plugins/public/reactdevtools/index.tsx index a86690a16..b0dd2a8c6 100644 --- a/desktop/plugins/public/reactdevtools/index.tsx +++ b/desktop/plugins/public/reactdevtools/index.tsx @@ -16,10 +16,14 @@ import { useValue, theme, sleep, + Toolbar, } from 'flipper-plugin'; import React, {createRef, useEffect} from 'react'; 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'; @@ -28,6 +32,29 @@ interface MetroDevice { sendCommand(command: string, params?: any): void; } +function findGlobalDevTools(): Promise { + 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 { const div = document.createElement('div'); div.id = DEV_TOOLS_NODE_ID; @@ -77,17 +104,42 @@ export function devicePlugin(client: DevicePluginClient) { const connectionStatus = createState( ConnectionStatus.Initializing, ); + const globalDevToolsPath = createState(); + const useGlobalDevTools = createState(false); // TODO: store in local storage T69989583 + let devToolsInstance: typeof ReactDevToolsStandalone = + ReactDevToolsStandalone; + let startResult: {close(): void} | undefined = undefined; const containerRef = createRef(); let pollHandle: NodeJS.Timeout | undefined = undefined; 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() { isMounted = true; let devToolsNode = findDevToolsNode(); if (!devToolsNode) { devToolsNode = createDevToolsNode(); } + attachDevTools(containerRef.current!, devToolsNode); initializeDevTools(devToolsNode); setStatus( @@ -169,11 +221,12 @@ export function devicePlugin(client: DevicePluginClient) { ); // 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) + startResult = devToolsInstance + .setContentDOMNode(devToolsNode) .setStatusListener((status) => { setStatus(ConnectionStatus.Initializing, status); }) - .startServer(port); + .startServer(port) as any; setStatus(ConnectionStatus.Initializing, 'Waiting for device'); // 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 { devtoolsHaveStarted, connectionStatus, @@ -219,6 +282,9 @@ export function devicePlugin(client: DevicePluginClient) { metroDevice, containerRef, stopDevtools, + globalDevToolsPath, + useGlobalDevTools, + toggleUseGlobalDevTools, }; } @@ -226,6 +292,8 @@ export function Component() { const instance = usePlugin(devicePlugin); const connectionStatus = useValue(instance.connectionStatus); const statusMessage = useValue(instance.statusMessage); + const globalDevToolsPath = useValue(instance.globalDevToolsPath); + const useGlobalDevTools = useValue(instance.useGlobalDevTools); useEffect(() => { instance.bootDevTools(); @@ -234,6 +302,19 @@ export function Component() { return ( + {globalDevToolsPath ? ( + + + Use globally installed DevTools + + } + /> + ) : null} {!instance.devtoolsHaveStarted() ? ( diff --git a/docs/troubleshooting.mdx b/docs/troubleshooting.mdx index c8495fe6e..afa5f7c14 100644 --- a/docs/troubleshooting.mdx +++ b/docs/troubleshooting.mdx @@ -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 ..." 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 "resolutions": {