From 296f80f3420fca92d2c996d88ea0c69d7e0e5cd9 Mon Sep 17 00:00:00 2001 From: Jakub Gonet Date: Mon, 6 May 2024 11:15:43 +0200 Subject: [PATCH 1/3] Add option to override default metro config --- packages/docs/docs/launch-configuration.md | 8 +++-- packages/vscode-extension/lib/metro_config.js | 7 +++- packages/vscode-extension/package.json | 4 +++ packages/vscode-extension/src/extension.ts | 25 +++++---------- .../vscode-extension/src/project/metro.ts | 32 +++++++++++++++---- .../vscode-extension/src/utilities/common.ts | 15 +++++++++ .../src/utilities/launchConfiguration.ts | 31 ++++++++---------- 7 files changed, 78 insertions(+), 44 deletions(-) diff --git a/packages/docs/docs/launch-configuration.md b/packages/docs/docs/launch-configuration.md index ef42518c2..d0b738e7e 100644 --- a/packages/docs/docs/launch-configuration.md +++ b/packages/docs/docs/launch-configuration.md @@ -127,9 +127,12 @@ Below is an example of how the `launch.json` file could look like with android v Here, we list other attributes that can be configured using launch configuration which doesn't fit in any of the above categories: - `appRoot` – Location of the React Native application root folder relative to the workspace. This is used for monorepo type setups when the workspace root is not the root of the React Native project. The IDE extension tries to locate the React Native application root automatically, but in case it failes to do so (i.e. there are multiple applications defined in the workspace), you can use this setting to override the location. -- `env` – Environment variables to be passed to all build/run commands that the IDE is launching. +- `env` – Environment variables to be passed to all build/run commands that the + IDE is launching. +- `metroConfigPath` — Path to metro config relative to the workspace. By default it tries to find + `metro.config.js` or `metro.config.ts`. -Below is a sample `launch.json` config file with `appRoot` and `env` setting specified: +Below is a sample `launch.json` config file with `appRoot`, `metroConfigPath`, and `env` setting specified: ```json { @@ -140,6 +143,7 @@ Below is a sample `launch.json` config file with `appRoot` and `env` setting spe "request": "launch", "name": "React Native IDE panel", "appRoot": "packages/mobile", + "metroConfigPath": "metro.config.dev.js", "env": { "MY_SECRET_KEY": "bananas" } diff --git a/packages/vscode-extension/lib/metro_config.js b/packages/vscode-extension/lib/metro_config.js index f8caa88ce..e34cbfdf8 100644 --- a/packages/vscode-extension/lib/metro_config.js +++ b/packages/vscode-extension/lib/metro_config.js @@ -4,6 +4,11 @@ const { adaptMetroConfig, requireFromAppDir, metroServerReadyHandler } = require const { loadConfig } = requireFromAppDir("metro-config"); module.exports = async function () { - const config = await loadConfig({}, {}); + const customMetroConfigPath = process.env.RN_IDE_METRO_CONFIG_PATH; + let options = {}; + if (customMetroConfigPath) { + options = { config: customMetroConfigPath }; + } + const config = await loadConfig(options, {}); return adaptMetroConfig(config); }; diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index 1fa28e73d..f70c1b08e 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -152,6 +152,10 @@ "type": "object", "description": "Environment variables to be passed to all build/run commands that the IDE is launching." }, + "metroConfigPath": { + "type": "string", + "description": "Location of Metro config relative to the workspace. This is used for using custom configs for e.g. development." + }, "ios": { "description": "Provides a way to customize Xcode builds for iOS", "type": "object", diff --git a/packages/vscode-extension/src/extension.ts b/packages/vscode-extension/src/extension.ts index dfd1908b7..2e113c802 100644 --- a/packages/vscode-extension/src/extension.ts +++ b/packages/vscode-extension/src/extension.ts @@ -27,7 +27,7 @@ import fs from "fs"; import { SidePanelViewProvider } from "./panels/SidepanelViewProvider"; import { PanelLocation } from "./common/WorkspaceConfig"; import { getLaunchConfiguration } from "./utilities/launchConfiguration"; -import { getTelemetryReporter } from "./utilities/telemetry"; +import { findSingleFileInWorkspace } from "./utilities/common"; const BIN_MODIFICATION_DATE_KEY = "bin_modification_date"; const OPEN_PANEL_ON_ACTIVATION = "open_panel_on_activation"; @@ -145,16 +145,6 @@ export async function activate(context: ExtensionContext) { await configureAppRootFolder(); } -async function findSingleFileInWorkspace(fileGlobPattern: string, excludePattern: string | null) { - const files = await workspace.findFiles(fileGlobPattern, excludePattern, 2); - if (files.length === 1) { - return files[0]; - } else if (files.length > 1) { - Logger.error(`Found multiple ${fileGlobPattern} files in the workspace`); - } - return undefined; -} - function extensionActivated() { if (extensionContext.workspaceState.get(OPEN_PANEL_ON_ACTIVATION)) { commands.executeCommand("RNIDE.openPanel"); @@ -173,7 +163,8 @@ async function configureAppRootFolder() { } async function findAppRootFolder() { - const appRootFromLaunchConfig = getLaunchConfiguration().appRoot; + const launchConfiguration = getLaunchConfiguration(); + const appRootFromLaunchConfig = launchConfiguration.appRoot; if (appRootFromLaunchConfig) { let appRoot: string | undefined; workspace.workspaceFolders?.forEach((folder) => { @@ -199,11 +190,11 @@ async function findAppRootFolder() { } return appRoot; } - - const metroConfigUri = await findSingleFileInWorkspace( - "**/metro.config.{js,ts}", - "**/node_modules" - ); + let metroConfigGlob = "**/metro.config.{js,ts}"; + if (launchConfiguration.metroConfigPath) { + metroConfigGlob = `**/${launchConfiguration.metroConfigPath}`; + } + const metroConfigUri = await findSingleFileInWorkspace(metroConfigGlob, "**/node_modules"); if (metroConfigUri) { return Uri.joinPath(metroConfigUri, "..").fsPath; } diff --git a/packages/vscode-extension/src/project/metro.ts b/packages/vscode-extension/src/project/metro.ts index d7819d7cc..b885c3d61 100644 --- a/packages/vscode-extension/src/project/metro.ts +++ b/packages/vscode-extension/src/project/metro.ts @@ -1,11 +1,12 @@ import path from "path"; -import { Disposable } from "vscode"; +import { Disposable, Uri } from "vscode"; import { exec, ChildProcess, lineReader } from "../utilities/subprocess"; import { Logger } from "../Logger"; import { extensionContext, getAppRootFolder } from "../utilities/extensionContext"; import { Devtools } from "./devtools"; import stripAnsi from "strip-ansi"; import { getLaunchConfiguration } from "../utilities/launchConfiguration"; +import { findSingleFileInWorkspace } from "../utilities/common"; export interface MetroDelegate { onBundleError(): void; @@ -96,7 +97,8 @@ export class Metro implements Disposable { appRootFolder: string, libPath: string, resetCache: boolean, - metroEnv: typeof process.env + metroEnv: typeof process.env, + metroConfigUri?: Uri ) { const reactNativeRoot = path.dirname( require.resolve("react-native", { paths: [appRootFolder] }) @@ -117,7 +119,10 @@ export class Metro implements Disposable { ], { cwd: appRootFolder, - env: metroEnv, + env: { + ...metroEnv, + ...(metroConfigUri ? { RN_IDE_METRO_CONFIG_PATH: metroConfigUri.path } : {}), + }, buffer: false, } ); @@ -127,14 +132,15 @@ export class Metro implements Disposable { resetCache: boolean, progressListener: (newStageProgress: number) => void ) { - let appRootFolder = getAppRootFolder(); + const appRootFolder = getAppRootFolder(); + const launchConfiguration = getLaunchConfiguration(); await this.devtools.ready(); const libPath = path.join(extensionContext.extensionPath, "lib"); const metroEnv = { ...process.env, - ...getLaunchConfiguration().env, + ...launchConfiguration.env, NODE_PATH: path.join(appRootFolder, "node_modules"), RCT_METRO_PORT: "0", RCT_DEVTOOLS_PORT: this.devtools.port.toString(), @@ -142,10 +148,20 @@ export class Metro implements Disposable { }; let bundlerProcess: ChildProcess; + let metroConfigUri: Uri | undefined; + if (launchConfiguration.metroConfigPath) { + metroConfigUri = await findMetroConfig(launchConfiguration.metroConfigPath); + } if (shouldUseExpoCLI()) { bundlerProcess = this.launchExpoMetro(appRootFolder, libPath, resetCache, metroEnv); } else { - bundlerProcess = this.launchPackager(appRootFolder, libPath, resetCache, metroEnv); + bundlerProcess = this.launchPackager( + appRootFolder, + libPath, + resetCache, + metroEnv, + metroConfigUri + ); } this.subprocess = bundlerProcess; @@ -251,6 +267,10 @@ export class Metro implements Disposable { } } +function findMetroConfig(configPath: string) { + return findSingleFileInWorkspace(configPath, "**/node_modules"); +} + function shouldUseExpoCLI() { // for expo launcher we use expo_start.js script that override some metro settings since it is not possible to // do that by passing command line option like in the case of community CLI's packager script. diff --git a/packages/vscode-extension/src/utilities/common.ts b/packages/vscode-extension/src/utilities/common.ts index 7e112ca6a..96e7b0fc8 100644 --- a/packages/vscode-extension/src/utilities/common.ts +++ b/packages/vscode-extension/src/utilities/common.ts @@ -5,6 +5,8 @@ import { Readable } from "stream"; import { finished } from "stream/promises"; import fs from "fs"; import { ReadableStream } from "stream/web"; +import { workspace } from "vscode"; +import { Logger } from "../Logger"; export enum CPU_ARCHITECTURE { ARM64 = "arm64-v8a", @@ -18,6 +20,19 @@ export function getDevServerScriptUrl() { return process.env.DEV_SCRIPT_URL; } +export async function findSingleFileInWorkspace( + fileGlobPattern: string, + excludePattern: string | null +) { + const files = await workspace.findFiles(fileGlobPattern, excludePattern, 2); + if (files.length === 1) { + return files[0]; + } else if (files.length > 1) { + Logger.error(`Found multiple ${fileGlobPattern} files in the workspace`); + } + return undefined; +} + export function getCpuArchitecture() { const arch = os.arch(); switch (arch) { diff --git a/packages/vscode-extension/src/utilities/launchConfiguration.ts b/packages/vscode-extension/src/utilities/launchConfiguration.ts index 5de5dc196..0254cb8b5 100644 --- a/packages/vscode-extension/src/utilities/launchConfiguration.ts +++ b/packages/vscode-extension/src/utilities/launchConfiguration.ts @@ -1,24 +1,19 @@ import { workspace } from "vscode"; export type LaunchConfigurationOptions = { - appRoot: string | undefined; - env: Record | undefined; - ios: - | { - scheme: string | undefined; - configuration: string | undefined; - } - | undefined; - android: - | { - variant: string | undefined; - } - | undefined; - preview: - | { - waitForAppLaunch: boolean | undefined; - } - | undefined; + appRoot?: string; + metroConfigPath?: string; + env?: Record; + ios?: { + scheme?: string; + configuration?: string; + }; + android?: { + variant?: string; + }; + preview?: { + waitForAppLaunch?: boolean; + }; }; export function getLaunchConfiguration(): LaunchConfigurationOptions { From 64fdfe56f0fc430f8ef4e1cde36708cf2805501b Mon Sep 17 00:00:00 2001 From: Jakub Gonet Date: Wed, 8 May 2024 15:35:38 +0200 Subject: [PATCH 2/3] Don't recursively check for metro config when specified --- packages/vscode-extension/src/extension.ts | 10 +++++----- packages/vscode-extension/src/project/metro.ts | 14 ++++++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/vscode-extension/src/extension.ts b/packages/vscode-extension/src/extension.ts index 2e113c802..183812f7e 100644 --- a/packages/vscode-extension/src/extension.ts +++ b/packages/vscode-extension/src/extension.ts @@ -190,11 +190,11 @@ async function findAppRootFolder() { } return appRoot; } - let metroConfigGlob = "**/metro.config.{js,ts}"; - if (launchConfiguration.metroConfigPath) { - metroConfigGlob = `**/${launchConfiguration.metroConfigPath}`; - } - const metroConfigUri = await findSingleFileInWorkspace(metroConfigGlob, "**/node_modules"); + + const metroConfigUri = await findSingleFileInWorkspace( + "**/metro.config.{js,ts}", + "**/node_modules" + ); if (metroConfigUri) { return Uri.joinPath(metroConfigUri, "..").fsPath; } diff --git a/packages/vscode-extension/src/project/metro.ts b/packages/vscode-extension/src/project/metro.ts index b885c3d61..337b45d6e 100644 --- a/packages/vscode-extension/src/project/metro.ts +++ b/packages/vscode-extension/src/project/metro.ts @@ -1,12 +1,12 @@ import path from "path"; -import { Disposable, Uri } from "vscode"; +import { Disposable, Uri, workspace } from "vscode"; import { exec, ChildProcess, lineReader } from "../utilities/subprocess"; import { Logger } from "../Logger"; import { extensionContext, getAppRootFolder } from "../utilities/extensionContext"; import { Devtools } from "./devtools"; import stripAnsi from "strip-ansi"; import { getLaunchConfiguration } from "../utilities/launchConfiguration"; -import { findSingleFileInWorkspace } from "../utilities/common"; +import fs from "fs"; export interface MetroDelegate { onBundleError(): void; @@ -150,7 +150,7 @@ export class Metro implements Disposable { let metroConfigUri: Uri | undefined; if (launchConfiguration.metroConfigPath) { - metroConfigUri = await findMetroConfig(launchConfiguration.metroConfigPath); + metroConfigUri = findMetroConfig(launchConfiguration.metroConfigPath); } if (shouldUseExpoCLI()) { bundlerProcess = this.launchExpoMetro(appRootFolder, libPath, resetCache, metroEnv); @@ -268,7 +268,13 @@ export class Metro implements Disposable { } function findMetroConfig(configPath: string) { - return findSingleFileInWorkspace(configPath, "**/node_modules"); + for (const folder of workspace.workspaceFolders ?? []) { + const possibleMetroConfigLocation = Uri.joinPath(folder.uri, configPath); + if (fs.existsSync(possibleMetroConfigLocation.fsPath)) { + return possibleMetroConfigLocation; + } + } + return undefined; } function shouldUseExpoCLI() { From d7e516609d1849b6974b84a98e4b9664fb3074aa Mon Sep 17 00:00:00 2001 From: Jakub Gonet Date: Thu, 9 May 2024 12:16:10 +0200 Subject: [PATCH 3/3] Move metro config to the env creation, add error when path isn't valid --- .../vscode-extension/src/project/metro.ts | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/packages/vscode-extension/src/project/metro.ts b/packages/vscode-extension/src/project/metro.ts index 337b45d6e..c77067df2 100644 --- a/packages/vscode-extension/src/project/metro.ts +++ b/packages/vscode-extension/src/project/metro.ts @@ -97,8 +97,7 @@ export class Metro implements Disposable { appRootFolder: string, libPath: string, resetCache: boolean, - metroEnv: typeof process.env, - metroConfigUri?: Uri + metroEnv: typeof process.env ) { const reactNativeRoot = path.dirname( require.resolve("react-native", { paths: [appRootFolder] }) @@ -119,10 +118,7 @@ export class Metro implements Disposable { ], { cwd: appRootFolder, - env: { - ...metroEnv, - ...(metroConfigUri ? { RN_IDE_METRO_CONFIG_PATH: metroConfigUri.path } : {}), - }, + env: metroEnv, buffer: false, } ); @@ -137,10 +133,14 @@ export class Metro implements Disposable { await this.devtools.ready(); const libPath = path.join(extensionContext.extensionPath, "lib"); - + let metroConfigPath: string | undefined; + if (launchConfiguration.metroConfigPath) { + metroConfigPath = findCustomMetroConfig(launchConfiguration.metroConfigPath); + } const metroEnv = { ...process.env, ...launchConfiguration.env, + ...(metroConfigPath ? { RN_IDE_METRO_CONFIG_PATH: metroConfigPath } : {}), NODE_PATH: path.join(appRootFolder, "node_modules"), RCT_METRO_PORT: "0", RCT_DEVTOOLS_PORT: this.devtools.port.toString(), @@ -148,20 +148,10 @@ export class Metro implements Disposable { }; let bundlerProcess: ChildProcess; - let metroConfigUri: Uri | undefined; - if (launchConfiguration.metroConfigPath) { - metroConfigUri = findMetroConfig(launchConfiguration.metroConfigPath); - } if (shouldUseExpoCLI()) { bundlerProcess = this.launchExpoMetro(appRootFolder, libPath, resetCache, metroEnv); } else { - bundlerProcess = this.launchPackager( - appRootFolder, - libPath, - resetCache, - metroEnv, - metroConfigUri - ); + bundlerProcess = this.launchPackager(appRootFolder, libPath, resetCache, metroEnv); } this.subprocess = bundlerProcess; @@ -267,14 +257,14 @@ export class Metro implements Disposable { } } -function findMetroConfig(configPath: string) { +function findCustomMetroConfig(configPath: string) { for (const folder of workspace.workspaceFolders ?? []) { const possibleMetroConfigLocation = Uri.joinPath(folder.uri, configPath); if (fs.existsSync(possibleMetroConfigLocation.fsPath)) { - return possibleMetroConfigLocation; + return possibleMetroConfigLocation.fsPath; } } - return undefined; + throw new Error("Metro config cannot be found, please check if `metroConfigPath` path is valid"); } function shouldUseExpoCLI() {