From d776a850bf515fca4b56fe2bfdb0bbc48b2f43d5 Mon Sep 17 00:00:00 2001 From: Krzysztof Magiera Date: Tue, 14 Jan 2025 11:06:31 +0100 Subject: [PATCH] Support for launching expo dev plugin tools in separate panels within VSCode (#878) This PR adds support for redux and react query dev tools expo plugins and makes their UI display as separate panel in VSCode. Apart from the support for the listed plugins, this change builds an infrastructure for supporting more expo dev plugins and other types of plugins in the future. Key changes that the PR introduces: 1. We are expanding panels configuration in package.json. Apparently there's no way to define panels dynamically and we need one panel for every dev tool such that they can be run side-by-side when necessary. To avoid having too many panels, every panel is hidden behind its own context variable that we can dynamically set when we detect the user requested to open this panel 2. We introduce a change to babel transformer that allows us to capture the use of expo-dev-plugins we currently support. This way we can detect which plugins the user has installed and only show relevant tools for the user to open. 3. For the above to work correctly, we are adding a new type of event that is passed via the devtools channel (same that we use for events such as "app ready"). We use this event to notify about the plugins that the app uses. 4. We're adding a UI component to the top menu bar with a "tools" dropdown. The dropdown displays the list of available tools and a checkbox next to every tool allowing the user to enable each tool separately. When tools is enabled, the new panel displaying the tool UI will launch. 5. Finally, we're implementing a new "ToolPlugin" interface and its implementation that coordinates all the expo-dev-plugin specific needs. The interface allows for tools to be listed as available and implements a persistent storage for saving which tools are enabled. The expo-dev-plugin implementation of the tool manages the registration of webview panels, listening to events about the plugins being used by the app, and controls the context variables that shows/hide the panel. As a follow up to this change, we will be adding a docs update that explains how tools can be used. Currently, we only support epxo-dev-plugin tools, so for the whole setup to work corrently the user needs to use Expo and have the specific tools configured according to the documentation here: https://github.com/expo/dev-plugins ### How Has This Been Tested: 1. I used a sample Expo project and added redux and react query dev tools following the docs here: https://github.com/expo/dev-plugins 2. I tested enabling/disabling each plugin separately. When plugin is enabled we expect the panel to open immediately. 3. I tested resetting metro cache which forces metro to reset at which point it stars at a new port. This is important as the plugin UI is hosted via metro. In this scenario is it expected for the panel to close while the app is reloading. --------- Co-authored-by: filip131311 <159789821+filip131311@users.noreply.github.com> --- packages/vscode-extension/assets/mmkv.svg | 3 + .../vscode-extension/assets/react-query.svg | 1 + packages/vscode-extension/assets/redux.svg | 10 ++ .../vscode-extension/lib/babel_transformer.js | 13 ++- .../vscode-extension/lib/expo_dev_plugins.js | 3 + packages/vscode-extension/lib/runtime.js | 4 + packages/vscode-extension/lib/wrapper.js | 18 ++++ packages/vscode-extension/package.json | 43 +++++++- .../vscode-extension/src/common/Project.ts | 9 ++ .../ExpoDevPluginWebviewProvider.ts | 50 ++++++++++ .../expo-dev-plugins/expo-dev-plugins.ts | 97 +++++++++++++++++++ .../vscode-extension/src/project/project.ts | 22 ++++- .../vscode-extension/src/project/tools.ts | 95 ++++++++++++++++++ .../src/webview/components/ToolsDropdown.css | 4 + .../src/webview/components/ToolsDropdown.tsx | 85 ++++++++++++++++ .../src/webview/providers/ProjectProvider.tsx | 10 ++ .../src/webview/utilities/rpc.ts | 2 +- .../src/webview/views/PreviewView.tsx | 42 ++++---- 18 files changed, 488 insertions(+), 23 deletions(-) create mode 100644 packages/vscode-extension/assets/mmkv.svg create mode 100644 packages/vscode-extension/assets/react-query.svg create mode 100644 packages/vscode-extension/assets/redux.svg create mode 100644 packages/vscode-extension/lib/expo_dev_plugins.js create mode 100644 packages/vscode-extension/src/plugins/expo-dev-plugins/ExpoDevPluginWebviewProvider.ts create mode 100644 packages/vscode-extension/src/plugins/expo-dev-plugins/expo-dev-plugins.ts create mode 100644 packages/vscode-extension/src/project/tools.ts create mode 100644 packages/vscode-extension/src/webview/components/ToolsDropdown.css create mode 100644 packages/vscode-extension/src/webview/components/ToolsDropdown.tsx diff --git a/packages/vscode-extension/assets/mmkv.svg b/packages/vscode-extension/assets/mmkv.svg new file mode 100644 index 000000000..9e8d24111 --- /dev/null +++ b/packages/vscode-extension/assets/mmkv.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/vscode-extension/assets/react-query.svg b/packages/vscode-extension/assets/react-query.svg new file mode 100644 index 000000000..b500b4f70 --- /dev/null +++ b/packages/vscode-extension/assets/react-query.svg @@ -0,0 +1 @@ +React Query \ No newline at end of file diff --git a/packages/vscode-extension/assets/redux.svg b/packages/vscode-extension/assets/redux.svg new file mode 100644 index 000000000..0ca461992 --- /dev/null +++ b/packages/vscode-extension/assets/redux.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/vscode-extension/lib/babel_transformer.js b/packages/vscode-extension/lib/babel_transformer.js index 36443f299..7cef9a1ea 100644 --- a/packages/vscode-extension/lib/babel_transformer.js +++ b/packages/vscode-extension/lib/babel_transformer.js @@ -73,6 +73,12 @@ function transformWrapper({ filename, src, ...rest }) { isTransforming("node_modules/radon-ide/index.js") ) { src = `${src};preview = require("__RNIDE_lib__/preview.js").preview;`; + } else if (isTransforming("node_modules/@dev-plugins/react-query/build/index.js")) { + src = `require("__RNIDE_lib__/expo_dev_plugins.js").register("@dev-plugins/react-query");${src}`; + } else if (isTransforming("node_modules/@dev-plugins/react-native-mmkv/build/index.js")) { + src = `require("__RNIDE_lib__/expo_dev_plugins.js").register("@dev-plugins/react-native-mmkv");${src}`; + } else if (isTransforming("node_modules/redux-devtools-expo-dev-plugin/build/index.js")) { + src = `require("__RNIDE_lib__/expo_dev_plugins.js").register("redux-devtools-expo-dev-plugin");${src}`; } else if ( isTransforming( "node_modules/react-native/Libraries/Renderer/implementations/ReactFabric-dev.js" @@ -98,7 +104,12 @@ function transformWrapper({ filename, src, ...rest }) { // is experimental as it has some performance implications and may be removed in future versions. // const { version } = requireFromAppDir("react-native/package.json"); - if (version.startsWith("0.74") || version.startsWith("0.75") || version.startsWith("0.76") || version.startsWith("0.77")) { + if ( + version.startsWith("0.74") || + version.startsWith("0.75") || + version.startsWith("0.76") || + version.startsWith("0.77") + ) { const rendererFileName = filename.split(path.sep).pop(); src = `module.exports = require("__RNIDE_lib__/rn-renderer/${rendererFileName}");`; } diff --git a/packages/vscode-extension/lib/expo_dev_plugins.js b/packages/vscode-extension/lib/expo_dev_plugins.js new file mode 100644 index 000000000..7674c7b8b --- /dev/null +++ b/packages/vscode-extension/lib/expo_dev_plugins.js @@ -0,0 +1,3 @@ +export function register(pluginName) { + global.__RNIDE_register_expo_dev_plugin && global.__RNIDE_register_expo_dev_plugin(pluginName); +} diff --git a/packages/vscode-extension/lib/runtime.js b/packages/vscode-extension/lib/runtime.js index c17730f27..d81c163a0 100644 --- a/packages/vscode-extension/lib/runtime.js +++ b/packages/vscode-extension/lib/runtime.js @@ -89,6 +89,10 @@ global.__RNIDE_register_navigation_plugin = function (name, plugin) { require("__RNIDE_lib__/wrapper.js").registerNavigationPlugin(name, plugin); }; +global.__RNIDE_register_expo_dev_plugin = function (name) { + require("__RNIDE_lib__/wrapper.js").registerExpoDevPlugin(name); +}; + AppRegistry.setWrapperComponentProvider((appParameters) => { return require("__RNIDE_lib__/wrapper.js").AppWrapper; }); diff --git a/packages/vscode-extension/lib/wrapper.js b/packages/vscode-extension/lib/wrapper.js index 94fd0bebb..6fdb353b5 100644 --- a/packages/vscode-extension/lib/wrapper.js +++ b/packages/vscode-extension/lib/wrapper.js @@ -20,6 +20,13 @@ export function registerNavigationPlugin(name, plugin) { navigationPlugins.push({ name, plugin }); } +const expoDevPlugins = new Set(); +let expoDevPluginsChanged = undefined; +export function registerExpoDevPlugin(name) { + expoDevPlugins.add(name); + expoDevPluginsChanged?.(); +} + let navigationHistory = new Map(); const InternalImports = { @@ -367,6 +374,17 @@ export function AppWrapper({ children, initialProps, fabric }) { appKey, navigationPlugins: navigationPlugins.map((plugin) => plugin.name), }); + devtoolsAgent._bridge.send("RNIDE_expoDevPluginsChanged", { + plugins: Array.from(expoDevPlugins.values()), + }); + expoDevPluginsChanged = () => { + devtoolsAgent._bridge.send("RNIDE_expoDevPluginsChanged", { + plugins: Array.from(expoDevPlugins.values()), + }); + }; + return () => { + devtoolsPluginsChanged = undefined; + }; } }, [!!devtoolsAgent && hasLayout]); diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 6fd6eb9bd..b16aa1278 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -167,6 +167,23 @@ "title": "Radon IDE", "icon": "assets/logo.svg" } + ], + "panel": [ + { + "id": "RadonIDEToolExpoDevPluginReactQuery", + "title": "React Query", + "icon": "assets/react-query.svg" + }, + { + "id": "RadonIDEToolExpoDevPluginMMKV", + "title": "MMKV", + "icon": "assets/mmkv.svg" + }, + { + "id": "RadonIDEToolExpoDevPluginReduxDevTools", + "title": "Redux", + "icon": "assets/redux.svg" + } ] }, "views": { @@ -175,7 +192,31 @@ "type": "webview", "id": "RadonIDE.view", "name": "", - "when": "config.RadonIDE.panelLocation != 'tab' && !RNIDE.sidePanelIsClosed" + "when": "RNIDE.extensionIsActive && config.RadonIDE.panelLocation != 'tab' && !RNIDE.sidePanelIsClosed" + } + ], + "RadonIDEToolExpoDevPluginReactQuery": [ + { + "type": "webview", + "id": "RNIDE.Tool.ExpoDevPlugin.ReactQuery.view", + "name": "", + "when": "RNIDE.Tool.ExpoDevPlugin.ReactQuery.available && RNIDE.panelIsOpen" + } + ], + "RadonIDEToolExpoDevPluginMMKV": [ + { + "type": "webview", + "id": "RNIDE.Tool.ExpoDevPlugin.MMKV.view", + "name": "", + "when": "RNIDE.Tool.ExpoDevPlugin.MMKV.available && RNIDE.panelIsOpen" + } + ], + "RadonIDEToolExpoDevPluginReduxDevTools": [ + { + "type": "webview", + "id": "RNIDE.Tool.ExpoDevPlugin.ReduxDevTools.view", + "name": "", + "when": "RNIDE.Tool.ExpoDevPlugin.ReduxDevTools.available && RNIDE.panelIsOpen" } ] }, diff --git a/packages/vscode-extension/src/common/Project.ts b/packages/vscode-extension/src/common/Project.ts index bec0317e2..fe55c44a7 100644 --- a/packages/vscode-extension/src/common/Project.ts +++ b/packages/vscode-extension/src/common/Project.ts @@ -16,6 +16,10 @@ export type DeviceSettings = { showTouches: boolean; }; +export type ToolsState = { + [key: string]: { enabled: boolean; label: string }; +}; + export type ProjectState = { status: | "starting" @@ -113,6 +117,7 @@ export interface ProjectEventMap { log: { type: string }; projectStateChanged: ProjectState; deviceSettingsChanged: DeviceSettings; + toolsStateChanged: ToolsState; licenseActivationChanged: boolean; navigationChanged: { displayName: string; id: string }; needsNativeRebuild: void; @@ -143,6 +148,10 @@ export interface ProjectInterface { updateDeviceSettings(deviceSettings: DeviceSettings): Promise; sendBiometricAuthorization(match: boolean): Promise; + getToolsState(): Promise; + updateToolEnabledState(toolName: keyof ToolsState, enabled: boolean): Promise; + openTool(toolName: keyof ToolsState): Promise; + resumeDebugger(): Promise; stepOverDebugger(): Promise; focusBuildOutput(): Promise; diff --git a/packages/vscode-extension/src/plugins/expo-dev-plugins/ExpoDevPluginWebviewProvider.ts b/packages/vscode-extension/src/plugins/expo-dev-plugins/ExpoDevPluginWebviewProvider.ts new file mode 100644 index 000000000..3ae118b11 --- /dev/null +++ b/packages/vscode-extension/src/plugins/expo-dev-plugins/ExpoDevPluginWebviewProvider.ts @@ -0,0 +1,50 @@ +import { + CancellationToken, + ExtensionContext, + Uri, + WebviewView, + WebviewViewProvider, + WebviewViewResolveContext, +} from "vscode"; +import { IDE } from "../../project/ide"; +import { ExpoDevPluginToolName } from "./expo-dev-plugins"; +import { Logger } from "../../Logger"; + +function generateWebviewContent(pluginName: ExpoDevPluginToolName, metroPort: number): string { + const iframeURL = `http://localhost:${metroPort}/_expo/plugins/${pluginName}`; + return /*html*/ ` + + + + +