Skip to content
Open
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
1 change: 0 additions & 1 deletion packages/bundler-metro/src/constants.ts

This file was deleted.

12 changes: 6 additions & 6 deletions packages/bundler-metro/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import connect from 'connect';
import nocache from 'nocache';
import { isPortAvailable, getMetroPackage } from './utils.js';
import { MetroPortUnavailableError } from './errors.js';
import { METRO_PORT } from './constants.js';
import type { MetroInstance, MetroOptions } from './types.js';
import {
type Reporter,
Expand Down Expand Up @@ -41,18 +40,19 @@ export const getMetroInstance = async (
abortSignal: AbortSignal
): Promise<MetroInstance> => {
const { projectRoot, harnessConfig } = options;
const isDefaultPortAvailable = await isPortAvailable(METRO_PORT);
const metroPort = harnessConfig.metroPort;
const isMetroPortAvailable = await isPortAvailable(metroPort);

if (!isDefaultPortAvailable) {
throw new MetroPortUnavailableError(METRO_PORT);
if (!isMetroPortAvailable) {
throw new MetroPortUnavailableError(metroPort);
}

const Metro = getMetroPackage(projectRoot);

process.env.RN_HARNESS = 'true';

const projectMetroConfig = await Metro.loadConfig({
port: METRO_PORT,
port: metroPort,
projectRoot,
});
const config = await withRnHarness(projectMetroConfig, true)();
Expand All @@ -62,7 +62,7 @@ export const getMetroInstance = async (

const middleware = connect()
.use(nocache())
.use('/', getExpoMiddleware(projectRoot, harnessConfig.entryPoint))
.use('/', getExpoMiddleware(projectRoot, harnessConfig.entryPoint, metroPort))
.use('/status', getStatusMiddleware(projectRoot));

const ready = waitForBundler(reporter, abortSignal);
Expand Down
6 changes: 3 additions & 3 deletions packages/bundler-metro/src/middlewares/expo-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import crypto from 'node:crypto';
import { getResolvedEntryPointWithoutExtension } from '../entry-point-utils.js';

export const getExpoMiddleware =
(projectRoot: string, entryPoint: string) =>
(projectRoot: string, entryPoint: string, metroPort: number) =>
(req: IncomingMessage, res: ServerResponse, next: NextFunction) => {
if (req.url !== '/') {
next();
Expand All @@ -24,7 +24,7 @@ export const getExpoMiddleware =
launchAsset: {
key: 'bundle',
contentType: 'application/javascript',
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`,
url: `http://localhost:${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`,
},
assets: [],
metadata: {},
Expand All @@ -35,7 +35,7 @@ export const getExpoMiddleware =
version: '1.0.0',
},
expoGo: {
debuggerHost: 'localhost:8081',
debuggerHost: `localhost:${metroPort}`,
developer: {
tool: 'expo-cli',
projectRoot,
Expand Down
4 changes: 2 additions & 2 deletions packages/bundler-metro/src/prewarm.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { METRO_PORT } from './constants.js';
import { getResolvedEntryPointWithoutExtension } from './entry-point-utils.js';

type PrewarmOptions = {
Expand All @@ -8,6 +7,7 @@ type PrewarmOptions = {
dev: boolean;
minify: boolean;
signal: AbortSignal;
metroPort: number;
};

export const prewarmMetroBundle = async (
Expand All @@ -23,7 +23,7 @@ export const prewarmMetroBundle = async (
dev: String(dev),
minify: String(minify),
});
const url = `http://localhost:${METRO_PORT}/${resolvedEntryPoint}.bundle?${searchParams.toString()}`;
const url = `http://localhost:${options.metroPort}/${resolvedEntryPoint}.bundle?${searchParams.toString()}`;

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

Expand Down
6 changes: 6 additions & 0 deletions packages/config/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export const ConfigSchema = z
runners: z.array(RunnerSchema).min(1, 'At least one runner is required'),
defaultRunner: z.string().optional(),
host: z.string().min(1, 'Host is required').optional(),
metroPort: z
.number()
.min(1, 'Metro port must be at least 1')
.max(65535, 'Metro port must be at most 65535')
.optional()
.default(8081),
webSocketPort: z.number().optional().default(3001),
bridgeTimeout: z
.number()
Expand Down
6 changes: 6 additions & 0 deletions packages/jest/src/cli-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { hideBin } from 'yargs/helpers';

export type HarnessCliArgs = {
harnessRunner?: string;
metroPort?: number;
};

export const getAdditionalCliArgs = (): HarnessCliArgs => {
Expand All @@ -11,6 +12,10 @@ export const getAdditionalCliArgs = (): HarnessCliArgs => {
type: 'string',
description: 'Specify which Harness runner to use',
})
.option('metroPort', {
type: 'number',
description: 'Override the Metro bundler port',
})
.strict(false)
.help(false)
.version(false)
Expand All @@ -19,5 +24,6 @@ export const getAdditionalCliArgs = (): HarnessCliArgs => {

return {
harnessRunner: argv.harnessRunner,
metroPort: argv.metroPort,
};
};
1 change: 1 addition & 0 deletions packages/jest/src/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ const getHarnessInternal = async (
dev: true,
minify: false,
signal,
metroPort: config.metroPort,
});
logMetroPrewarmCompleted(platform);
await appMonitor.start();
Expand Down
5 changes: 5 additions & 0 deletions packages/jest/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ export const setup = async (globalConfig: JestConfig.GlobalConfig) => {
});

const cliArgs = getAdditionalCliArgs();

if (cliArgs.metroPort != null) {
harnessConfig.metroPort = cliArgs.metroPort;
}

const selectedRunner = getHarnessRunner(harnessConfig, cliArgs);

if (globalConfig.collectCoverage) {
Expand Down
1 change: 0 additions & 1 deletion packages/platform-android/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"dependencies": {
"@react-native-harness/platforms": "workspace:*",
"@react-native-harness/tools": "workspace:*",
"@react-native-harness/config": "workspace:*",
"zod": "^3.25.67",
"tslib": "^2.3.0"
},
Expand Down
16 changes: 16 additions & 0 deletions packages/platform-android/src/adb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,19 @@ export const getConnectedDevices = async (): Promise<AdbDevice[]> => {

return devices;
};

export const setDebugHttpHost = async (
adbId: string,
bundleId: string,
host: string
): Promise<void> => {
const prefsPath = `shared_prefs/${bundleId}_preferences.xml`;
const prefsContent = `<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?><map><string name=\\"debug_http_host\\">${host}</string></map>`;
await spawn('adb', [
'-s',
adbId,
'shell',
`run-as ${bundleId} sh -c 'mkdir -p shared_prefs && printf "${prefsContent}" > ${prefsPath}'`,
]);
};

8 changes: 5 additions & 3 deletions packages/platform-android/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
CreateAppMonitorOptions,
HarnessPlatformRunner,
} from '@react-native-harness/platforms';
import { Config } from '@react-native-harness/config';
import {
AndroidPlatformConfigSchema,
type AndroidPlatformConfig,
Expand All @@ -16,7 +15,7 @@ import { createAndroidAppMonitor } from './app-monitor.js';

const getAndroidRunner = async (
config: AndroidPlatformConfig,
harnessConfig: Config
harnessConfig: { metroPort: number; webSocketPort: number }
): Promise<HarnessPlatformRunner> => {
const parsedConfig = AndroidPlatformConfigSchema.parse(config);
const adbId = await getAdbId(parsedConfig.device);
Expand All @@ -34,11 +33,14 @@ const getAndroidRunner = async (
);
}

const metroPort = harnessConfig.metroPort;

await Promise.all([
adb.reversePort(adbId, 8081),
adb.reversePort(adbId, metroPort),
adb.reversePort(adbId, 8080),
adb.reversePort(adbId, harnessConfig.webSocketPort),
adb.setHideErrorDialogs(adbId, true),
adb.setDebugHttpHost(adbId, parsedConfig.bundleId, `localhost:${metroPort}`),
]);
const appUid = await adb.getAppUid(adbId, parsedConfig.bundleId);

Expand Down
3 changes: 0 additions & 3 deletions packages/platform-android/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
"files": [],
"include": [],
"references": [
{
"path": "../config"
},
{
"path": "../tools"
},
Expand Down
3 changes: 0 additions & 3 deletions packages/platform-android/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
},
"include": ["src/**/*.ts"],
"references": [
{
"path": "../config/tsconfig.lib.json"
},
{
"path": "../tools/tsconfig.lib.json"
},
Expand Down
1 change: 0 additions & 1 deletion packages/platform-ios/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"dependencies": {
"@react-native-harness/platforms": "workspace:*",
"@react-native-harness/tools": "workspace:*",
"@react-native-harness/config": "workspace:*",
"zod": "^3.25.67",
"tslib": "^2.3.0"
},
Expand Down
10 changes: 6 additions & 4 deletions packages/platform-ios/src/__tests__/instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('iOS platform instance dependency validation', () => {
vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid');
vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true);
vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted');
vi.spyOn(simctl, 'setJsLocation').mockResolvedValue(undefined);

const config = {
name: 'ios',
Expand All @@ -27,7 +28,7 @@ describe('iOS platform instance dependency validation', () => {
};

await expect(
getAppleSimulatorPlatformInstance(config)
getAppleSimulatorPlatformInstance(config, 8081)
).resolves.toBeDefined();
expect(assertInstalled).not.toHaveBeenCalled();
});
Expand All @@ -44,7 +45,7 @@ describe('iOS platform instance dependency validation', () => {
};

await expect(
getApplePhysicalDevicePlatformInstance(config)
getApplePhysicalDevicePlatformInstance(config, 8081)
).rejects.toThrow('missing');
expect(assertInstalled).toHaveBeenCalled();
});
Expand All @@ -58,6 +59,7 @@ describe('iOS platform instance dependency validation', () => {
);
vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true);
vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted');
vi.spyOn(simctl, 'setJsLocation').mockResolvedValue(undefined);

const config = {
name: 'ios',
Expand All @@ -66,7 +68,7 @@ describe('iOS platform instance dependency validation', () => {
};

await expect(
getAppleSimulatorPlatformInstance(config)
getAppleSimulatorPlatformInstance(config, 8081)
).resolves.toBeDefined();
expect(getSimulatorId).toHaveBeenCalled();
});
Expand All @@ -84,7 +86,7 @@ describe('iOS platform instance dependency validation', () => {
};

await expect(
getApplePhysicalDevicePlatformInstance(config)
getApplePhysicalDevicePlatformInstance(config, 8081)
).rejects.toThrow('missing');
expect(getDeviceId).not.toHaveBeenCalled();
});
Expand Down
18 changes: 16 additions & 2 deletions packages/platform-ios/src/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
import { assertLibimobiledeviceInstalled } from './libimobiledevice.js';

export const getAppleSimulatorPlatformInstance = async (
config: ApplePlatformConfig
config: ApplePlatformConfig,
metroPort: number
): Promise<HarnessPlatformRunner> => {
assertAppleDeviceSimulator(config.device);

Expand Down Expand Up @@ -47,6 +48,12 @@ export const getAppleSimulatorPlatformInstance = async (
throw new Error('Simulator is not booted');
}

await simctl.setJsLocation(
udid,
config.bundleId,
`localhost:${metroPort}`
);

return {
startApp: async (options) => {
await simctl.startApp(
Expand Down Expand Up @@ -84,11 +91,18 @@ export const getAppleSimulatorPlatformInstance = async (
};

export const getApplePhysicalDevicePlatformInstance = async (
config: ApplePlatformConfig
config: ApplePlatformConfig,
metroPort: number
): Promise<HarnessPlatformRunner> => {
assertAppleDevicePhysical(config.device);
await assertLibimobiledeviceInstalled();

if (metroPort !== 8081) {
throw new Error(
`Custom Metro port ${metroPort} is not supported on physical iOS devices. Physical devices always connect to port 8081.`
);
}

const device = await devicectl.getDevice(config.device.name);

if (!device) {
Expand Down
7 changes: 3 additions & 4 deletions packages/platform-ios/src/runner.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { HarnessPlatformRunner } from '@react-native-harness/platforms';
import { Config } from '@react-native-harness/config';
import {
ApplePlatformConfigSchema,
type ApplePlatformConfig,
Expand All @@ -12,15 +11,15 @@ import {

const getAppleRunner = async (
config: ApplePlatformConfig,
_harnessConfig: Config
harnessConfig: { metroPort: number }
): Promise<HarnessPlatformRunner> => {
const parsedConfig = ApplePlatformConfigSchema.parse(config);

if (isAppleDeviceSimulator(parsedConfig.device)) {
return getAppleSimulatorPlatformInstance(parsedConfig);
return getAppleSimulatorPlatformInstance(parsedConfig, harnessConfig.metroPort);
}

return getApplePhysicalDevicePlatformInstance(parsedConfig);
return getApplePhysicalDevicePlatformInstance(parsedConfig, harnessConfig.metroPort);
};

export default getAppleRunner;
17 changes: 17 additions & 0 deletions packages/platform-ios/src/xcrun/simctl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,23 @@ export const isAppRunning = async (
}
};

export const setJsLocation = async (
udid: string,
bundleId: string,
host: string
): Promise<void> => {
await spawn('xcrun', [
'simctl',
'spawn',
udid,
'defaults',
'write',
bundleId,
'RCT_jsLocation',
host,
]);
};

export const screenshot = async (
udid: string,
destination: string
Expand Down
3 changes: 0 additions & 3 deletions packages/platform-ios/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
"files": [],
"include": [],
"references": [
{
"path": "../config"
},
{
"path": "../tools"
},
Expand Down
Loading
Loading