Skip to content

Commit

Permalink
pass a concrete port number to Metro (#998)
Browse files Browse the repository at this point in the history
Passes a concrete port number (picked by getting an ephemeral port
briefly assigned by the OS and then releasing it) to Metro instead of
"0", which causes issues with Metro's internal implementation.

### How Has This Been Tested: 
Open the various test apps in the `test-apps` repo and verify the
packager connects.
  • Loading branch information
jwajgelt authored Mar 3, 2025
1 parent 3d69569 commit e339c0d
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 39 deletions.
5 changes: 0 additions & 5 deletions packages/vscode-extension/lib/expo_start.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 1 addition & 29 deletions packages/vscode-extension/lib/metro_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 10 additions & 5 deletions packages/vscode-extension/src/project/metro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,7 +51,7 @@ type MetroEvent =
}
| { type: "RNIDE_expo_env_prelude_lines"; lineCount: number }
| {
type: "RNIDE_initialize_done";
type: "initialize_done";
port: number;
}
| {
Expand Down Expand Up @@ -160,6 +161,7 @@ export class Metro implements Disposable {

private launchPackager(
appRootFolder: string,
port: number,
libPath: string,
resetCache: boolean,
metroEnv: typeof process.env
Expand All @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -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();
Expand Down
30 changes: 30 additions & 0 deletions packages/vscode-extension/src/utilities/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down Expand Up @@ -233,3 +234,32 @@ export async function calculateMD5(fsPath: string, hash: Hash = createHash("md5"
}
return hash;
}

export async function getOpenPort(): Promise<number> {
const { promise, resolve, reject } = Promise.withResolvers<number>();

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);
});
}

0 comments on commit e339c0d

Please sign in to comment.