Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pass a concrete port number to Metro #998

Merged
merged 1 commit into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
}