Skip to content

Commit 6b2de9e

Browse files
authored
feat: configurable Metro port (#79)
Adds support for configuring the Metro port in Harness, including CLI overrides via `--metroPort`. Harness now also restores Android and iOS simulator Metro connection settings on cleanup so normal dev-mode launches keep working after a test run.
1 parent 5cc5afc commit 6b2de9e

25 files changed

Lines changed: 600 additions & 51 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@react-native-harness/jest': patch
3+
'@react-native-harness/platform-android': patch
4+
'@react-native-harness/platform-apple': patch
5+
---
6+
7+
Adds support for configuring the Metro port in Harness, including CLI overrides via `--metroPort`. Harness now also restores Android and iOS simulator Metro connection settings on cleanup so normal dev-mode launches keep working after a test run.

actions/shared/index.cjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4209,6 +4209,7 @@ var coerce = {
42094209
var NEVER = INVALID;
42104210

42114211
// ../config/dist/types.js
4212+
var DEFAULT_METRO_PORT = 8081;
42124213
var RunnerSchema = external_exports.object({
42134214
name: external_exports.string().min(1, "Runner name is required").regex(/^[a-zA-Z0-9._-]+$/, "Runner name can only contain alphanumeric characters, dots, underscores, and hyphens"),
42144215
config: external_exports.record(external_exports.any()),
@@ -4221,6 +4222,7 @@ var ConfigSchema = external_exports.object({
42214222
runners: external_exports.array(RunnerSchema).min(1, "At least one runner is required"),
42224223
defaultRunner: external_exports.string().optional(),
42234224
host: external_exports.string().min(1, "Host is required").optional(),
4225+
metroPort: external_exports.number().int("Metro port must be an integer").min(1, "Metro port must be at least 1").max(65535, "Metro port must be at most 65535").optional().default(DEFAULT_METRO_PORT),
42244226
webSocketPort: external_exports.number().optional().default(3001),
42254227
bridgeTimeout: external_exports.number().min(1e3, "Bridge timeout must be at least 1 second").default(6e4),
42264228
/** @deprecated Removed in favor of crash supervisor. Accepted for backwards compatibility. */

packages/bundler-metro/src/constants.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/bundler-metro/src/factory.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import connect from 'connect';
66
import nocache from 'nocache';
77
import { isPortAvailable, getMetroPackage } from './utils.js';
88
import { MetroPortUnavailableError } from './errors.js';
9-
import { METRO_PORT } from './constants.js';
109
import type { MetroInstance, MetroOptions } from './types.js';
1110
import {
1211
type Reporter,
@@ -41,18 +40,19 @@ export const getMetroInstance = async (
4140
abortSignal: AbortSignal
4241
): Promise<MetroInstance> => {
4342
const { projectRoot, harnessConfig } = options;
44-
const isDefaultPortAvailable = await isPortAvailable(METRO_PORT);
43+
const metroPort = harnessConfig.metroPort;
44+
const isMetroPortAvailable = await isPortAvailable(metroPort);
4545

46-
if (!isDefaultPortAvailable) {
47-
throw new MetroPortUnavailableError(METRO_PORT);
46+
if (!isMetroPortAvailable) {
47+
throw new MetroPortUnavailableError(metroPort);
4848
}
4949

5050
const Metro = getMetroPackage(projectRoot);
5151

5252
process.env.RN_HARNESS = 'true';
5353

5454
const projectMetroConfig = await Metro.loadConfig({
55-
port: METRO_PORT,
55+
port: metroPort,
5656
projectRoot,
5757
});
5858
const config = await withRnHarness(projectMetroConfig, true)();
@@ -62,7 +62,7 @@ export const getMetroInstance = async (
6262

6363
const middleware = connect()
6464
.use(nocache())
65-
.use('/', getExpoMiddleware(projectRoot, harnessConfig.entryPoint))
65+
.use('/', getExpoMiddleware(projectRoot, harnessConfig))
6666
.use('/status', getStatusMiddleware(projectRoot));
6767

6868
const ready = waitForBundler(reporter, abortSignal);

packages/bundler-metro/src/middlewares/expo-middleware.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { IncomingMessage, ServerResponse } from 'node:http';
22
import type { NextFunction } from 'connect';
3+
import type { Config as HarnessConfig } from '@react-native-harness/config';
34
import crypto from 'node:crypto';
45
import { getResolvedEntryPointWithoutExtension } from '../entry-point-utils.js';
56

67
export const getExpoMiddleware =
7-
(projectRoot: string, entryPoint: string) =>
8+
(projectRoot: string, harnessConfig: HarnessConfig) =>
89
(req: IncomingMessage, res: ServerResponse, next: NextFunction) => {
910
if (req.url !== '/') {
1011
next();
@@ -14,7 +15,7 @@ export const getExpoMiddleware =
1415
const platform = req.headers['expo-platform'] as string;
1516
const resolvedEntryPoint = getResolvedEntryPointWithoutExtension(
1617
projectRoot,
17-
entryPoint
18+
harnessConfig.entryPoint
1819
);
1920

2021
const manifestJson = JSON.stringify({
@@ -24,7 +25,7 @@ export const getExpoMiddleware =
2425
launchAsset: {
2526
key: 'bundle',
2627
contentType: 'application/javascript',
27-
url: `http://localhost:8081/${resolvedEntryPoint}.bundle?platform=${platform}&dev=true&hot=false&lazy=true&transform.engine=hermes&transform.bytecode=1&transform.routerRoot=app&transform.reactCompiler=true&unstable_transformProfile=hermes-stable`,
28+
url: `http://localhost:${harnessConfig.metroPort}/${resolvedEntryPoint}.bundle?platform=${platform}&dev=true&hot=false&lazy=true&transform.engine=hermes&transform.bytecode=1&transform.routerRoot=app&transform.reactCompiler=true&unstable_transformProfile=hermes-stable`,
2829
},
2930
assets: [],
3031
metadata: {},
@@ -35,7 +36,7 @@ export const getExpoMiddleware =
3536
version: '1.0.0',
3637
},
3738
expoGo: {
38-
debuggerHost: 'localhost:8081',
39+
debuggerHost: `localhost:${harnessConfig.metroPort}`,
3940
developer: {
4041
tool: 'expo-cli',
4142
projectRoot,

packages/bundler-metro/src/prewarm.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { METRO_PORT } from './constants.js';
21
import { getResolvedEntryPointWithoutExtension } from './entry-point-utils.js';
32

43
type PrewarmOptions = {
54
projectRoot: string;
65
entryPoint: string;
6+
port: number;
77
platform: string;
88
dev: boolean;
99
minify: boolean;
@@ -13,7 +13,8 @@ type PrewarmOptions = {
1313
export const prewarmMetroBundle = async (
1414
options: PrewarmOptions
1515
): Promise<void> => {
16-
const { projectRoot, entryPoint, platform, dev, minify, signal } = options;
16+
const { projectRoot, entryPoint, port, platform, dev, minify, signal } =
17+
options;
1718
const resolvedEntryPoint = getResolvedEntryPointWithoutExtension(
1819
projectRoot,
1920
entryPoint
@@ -23,7 +24,7 @@ export const prewarmMetroBundle = async (
2324
dev: String(dev),
2425
minify: String(minify),
2526
});
26-
const url = `http://localhost:${METRO_PORT}/${resolvedEntryPoint}.bundle?${searchParams.toString()}`;
27+
const url = `http://localhost:${port}/${resolvedEntryPoint}.bundle?${searchParams.toString()}`;
2728

2829
const response = await fetch(url, { signal });
2930

packages/config/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export { getConfig } from './reader.js';
22
export type { Config } from './types.js';
3+
export { ConfigSchema, DEFAULT_METRO_PORT } from './types.js';
34
export {
45
ConfigValidationError,
56
ConfigNotFoundError,

packages/config/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { z } from 'zod';
22

3+
export const DEFAULT_METRO_PORT = 8081;
4+
35
const RunnerSchema = z.object({
46
name: z
57
.string()
@@ -22,6 +24,13 @@ export const ConfigSchema = z
2224
runners: z.array(RunnerSchema).min(1, 'At least one runner is required'),
2325
defaultRunner: z.string().optional(),
2426
host: z.string().min(1, 'Host is required').optional(),
27+
metroPort: z
28+
.number()
29+
.int('Metro port must be an integer')
30+
.min(1, 'Metro port must be at least 1')
31+
.max(65535, 'Metro port must be at most 65535')
32+
.optional()
33+
.default(DEFAULT_METRO_PORT),
2534
webSocketPort: z.number().optional().default(3001),
2635
bridgeTimeout: z
2736
.number()

packages/jest/src/cli-args.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { hideBin } from 'yargs/helpers';
33

44
export type HarnessCliArgs = {
55
harnessRunner?: string;
6+
metroPort?: number;
67
};
78

89
export const getAdditionalCliArgs = (): HarnessCliArgs => {
@@ -11,6 +12,10 @@ export const getAdditionalCliArgs = (): HarnessCliArgs => {
1112
type: 'string',
1213
description: 'Specify which Harness runner to use',
1314
})
15+
.option('metroPort', {
16+
type: 'number',
17+
description: 'Override the Metro bundler port',
18+
})
1419
.strict(false)
1520
.help(false)
1621
.version(false)
@@ -19,5 +24,6 @@ export const getAdditionalCliArgs = (): HarnessCliArgs => {
1924

2025
return {
2126
harnessRunner: argv.harnessRunner,
27+
metroPort: argv.metroPort,
2228
};
2329
};

packages/jest/src/harness.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ const getHarnessInternal = async (
178178
await prewarmMetroBundle({
179179
projectRoot,
180180
entryPoint: config.entryPoint,
181+
port: config.metroPort,
181182
platform: platform.platformId,
182183
dev: true,
183184
minify: false,

0 commit comments

Comments
 (0)