From ab17bbd5559e9ad2149124503c395ff2e02fc3ed Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Wed, 12 May 2021 14:20:57 -0700 Subject: [PATCH] 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 --- .../plugins/public/reactdevtools/index.tsx | 87 ++++++++++++++++++- docs/troubleshooting.mdx | 6 +- 2 files changed, 89 insertions(+), 4 deletions(-) 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": {