diff --git a/packages/vscode-extension/lib/expo_start.js b/packages/vscode-extension/lib/expo_start.js index 45130ec7c..4111a69e6 100644 --- a/packages/vscode-extension/lib/expo_start.js +++ b/packages/vscode-extension/lib/expo_start.js @@ -17,10 +17,5 @@ metroConfig.loadConfig = async function (...args) { // base terminal reporter class from metro that Expo CLI extends overrideModuleFromAppDir("metro/src/lib/TerminalReporter", require("./metro_reporter")); -// In addition, expo uses freeport-async to check whether provided port is busy. -// Apparently, this module returns 11000 port when 0 is provided, so we need to -// override this behavior here. -overrideModuleFromAppDir("freeport-async", async (port) => port); - const { expoStart } = requireFromAppDir("@expo/cli/build/src/start/index"); expoStart(process.argv.slice(2)); // pass argv but strip node and script name diff --git a/packages/vscode-extension/lib/metro_helpers.js b/packages/vscode-extension/lib/metro_helpers.js index e7dff5536..b3cf3660a 100644 --- a/packages/vscode-extension/lib/metro_helpers.js +++ b/packages/vscode-extension/lib/metro_helpers.js @@ -5,6 +5,7 @@ const appRoot = path.resolve(); // Instead of using require in this code, we should use require_app, which will // resolve modules relative to the app root, not the extension lib root. function requireFromAppDir(module) { + // eslint-disable-next-line @typescript-eslint/no-shadow const path = require.resolve(module, { paths: [appRoot] }); return require(path); } @@ -59,10 +60,6 @@ function adaptMetroConfig(config) { return origProcessModuleFilter(module); }; - // We actually need to reset port number here again, because CLI overrides it - // thinking that value 0 means "use default port". - config.server.port = 0; - config.watchFolders = [...(config.watchFolders || []), extensionLib]; // Handle the case when resolver is not defined in the config @@ -172,31 +169,6 @@ function adaptMetroConfig(config) { return config; } -// An ugly workaround for packager script to print actual port number. -// Since we want to start packager on ephemeral port, we need to know the actual port number. -// Apparently, metro only reports port provided to the config, which will be 0. -// This workaround overrides http server prototype and prints the port number along -// with setting some env variables specific to expo that are populated with "0" port as well. -function patchHttpListen() { - const http = require("http"); - const originalListen = http.Server.prototype.listen; - - http.Server.prototype.listen = function (...args) { - const server = this; - originalListen.apply(server, args); - server.on("listening", () => { - const port = server.address().port; - process.env.EXPO_PACKAGER_PROXY_URL = - process.env.EXPO_MANIFEST_PROXY_URL = `http://localhost:${port}`; - process.stdout.write(JSON.stringify({ type: "RNIDE_initialize_done", port })); - process.stdout.write("\n"); - }); - return server; - }; -} - -patchHttpListen(); - module.exports = { appRoot, adaptMetroConfig, diff --git a/packages/vscode-extension/src/project/metro.ts b/packages/vscode-extension/src/project/metro.ts index 0c7aca8b4..59671dd6b 100644 --- a/packages/vscode-extension/src/project/metro.ts +++ b/packages/vscode-extension/src/project/metro.ts @@ -11,6 +11,7 @@ import { Devtools } from "./devtools"; import { getLaunchConfiguration } from "../utilities/launchConfiguration"; import { EXPO_GO_BUNDLE_ID, EXPO_GO_PACKAGE_NAME } from "../builders/expoGo"; import { connectCDPAndEval } from "../utilities/connectCDPAndEval"; +import { getOpenPort } from "../utilities/common"; export interface MetroDelegate { onBundleError(): void; @@ -50,7 +51,7 @@ type MetroEvent = } | { type: "RNIDE_expo_env_prelude_lines"; lineCount: number } | { - type: "RNIDE_initialize_done"; + type: "initialize_done"; port: number; } | { @@ -160,6 +161,7 @@ export class Metro implements Disposable { private launchPackager( appRootFolder: string, + port: number, libPath: string, resetCache: boolean, metroEnv: typeof process.env @@ -175,7 +177,7 @@ export class Metro implements Disposable { ...(resetCache ? ["--reset-cache"] : []), "--no-interactive", "--port", - "0", + `${port}`, "--config", path.join(libPath, "metro_config.js"), "--customLogReporterPath", @@ -204,11 +206,14 @@ export class Metro implements Disposable { metroConfigPath = findCustomMetroConfig(launchConfiguration.metroConfigPath); } const isExtensionDev = extensionContext.extensionMode === ExtensionMode.Development; + + const port = await getOpenPort(); + const metroEnv = { ...launchConfiguration.env, ...(metroConfigPath ? { RN_IDE_METRO_CONFIG_PATH: metroConfigPath } : {}), NODE_PATH: path.join(appRoot, "node_modules"), - RCT_METRO_PORT: "0", + RCT_METRO_PORT: `${port}`, RCT_DEVTOOLS_PORT: this.devtools.port.toString(), RADON_IDE_LIB_PATH: libPath, RADON_IDE_VERSION: extensionContext.extension.packageJSON.version, @@ -225,7 +230,7 @@ export class Metro implements Disposable { metroEnv ); } else { - bundlerProcess = this.launchPackager(appRoot, libPath, resetCache, metroEnv); + bundlerProcess = this.launchPackager(appRoot, port, libPath, resetCache, metroEnv); } this.subprocess = bundlerProcess; @@ -262,7 +267,7 @@ export class Metro implements Disposable { this._expoPreludeLineCount = event.lineCount; Logger.debug("Expo prelude line offset was set to: ", this._expoPreludeLineCount); break; - case "RNIDE_initialize_done": + case "initialize_done": this._port = event.port; Logger.info(`Metro started on port ${this._port}`); resolve(); diff --git a/packages/vscode-extension/src/utilities/common.ts b/packages/vscode-extension/src/utilities/common.ts index fda1e129f..60bbfcd5c 100644 --- a/packages/vscode-extension/src/utilities/common.ts +++ b/packages/vscode-extension/src/utilities/common.ts @@ -3,6 +3,7 @@ import fs from "fs"; import { createHash, Hash } from "crypto"; import path, { join } from "path"; import { finished } from "stream/promises"; +import { Server } from "net"; import fetch from "node-fetch"; export const ANDROID_FAIL_ERROR_MESSAGE = "Android failed."; @@ -233,3 +234,32 @@ export async function calculateMD5(fsPath: string, hash: Hash = createHash("md5" } return hash; } + +export async function getOpenPort(): Promise { + const { promise, resolve, reject } = Promise.withResolvers(); + + const server = new Server().listen(0); + + const onListening = () => { + const address = server.address(); + if (!address || typeof address === "string") { + reject(new Error("Failed to reserve an open port")); + return; + } + const port = address.port; + server.close(() => { + resolve(port); + }); + }; + const onError = (err: unknown) => { + reject(err); + }; + + const unsubListening = server.once("listening", onListening); + const unsubError = server.once("error", onError); + + return promise.finally(() => { + unsubListening.off("listening", onListening); + unsubError.off("error", onError); + }); +}