Skip to content
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
2 changes: 1 addition & 1 deletion actions/shared/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4388,7 +4388,7 @@ var ConfigSchema = external_exports.object({
defaultRunner: external_exports.string().optional(),
host: external_exports.string().min(1, "Host is required").optional(),
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),
webSocketPort: external_exports.number().optional().default(3001),
webSocketPort: external_exports.number().optional().describe("Deprecated. Bridge traffic now uses metroPort and this value is ignored."),
bridgeTimeout: external_exports.number().min(1e3, "Bridge timeout must be at least 1 second").default(6e4),
bundleStartTimeout: external_exports.number().min(1e3, "Bundle start timeout must be at least 1 second").default(15e3),
maxAppRestarts: external_exports.number().min(0, "Max app restarts must be at least 0").default(2),
Expand Down
1 change: 0 additions & 1 deletion apps/playground/rn-harness.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ export default {
],
defaultRunner: 'android',
bridgeTimeout: 120000,
webSocketPort: 3002,

resetEnvironmentBetweenTestFiles: true,
unstable__enableMetroCache: true,
Expand Down
62 changes: 54 additions & 8 deletions packages/bridge/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { WebSocketServer, type WebSocket } from 'ws';
import { type BirpcGroup, createBirpcGroup } from 'birpc';
import { logger } from '@react-native-harness/tools';
import { EventEmitter } from 'node:events';
import type { Server as HttpServer } from 'node:http';
import type { Server as HttpsServer } from 'node:https';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
Expand All @@ -24,8 +26,25 @@ import { matchImageSnapshot } from './image-snapshot.js';
export { DeviceNotRespondingError } from './errors.js';
const bridgeLogger = logger.child('bridge');

export type BridgeServerOptions = {
type BridgeServerStandaloneOptions = {
port: number;
host?: string;
};

type BridgeServerAttachedOptions = {
server: HttpServer | HttpsServer;
path?: string;
};

type BridgeServerNoServerOptions = {
noServer: true;
};

export type BridgeServerOptions = (
| BridgeServerStandaloneOptions
| BridgeServerAttachedOptions
| BridgeServerNoServerOptions
) & {
timeout?: number;
context: HarnessContext;
};
Expand Down Expand Up @@ -55,16 +74,43 @@ export type BridgeServer = {
};

export const getBridgeServer = async ({
port,
timeout,
context,
...transport
}: BridgeServerOptions): Promise<BridgeServer> => {
const wss = await new Promise<WebSocketServer>((resolve) => {
const server = new WebSocketServer({ port, host: '0.0.0.0' }, () => {
resolve(server);
});
});
bridgeLogger.debug('bridge server listening on port %d', port);
const wss =
'port' in transport
? await new Promise<WebSocketServer>((resolve) => {
const server = new WebSocketServer(
{
port: transport.port,
host: transport.host ?? '0.0.0.0',
},
() => {
resolve(server);
}
);
})
: new WebSocketServer(
'server' in transport
? {
server: transport.server,
path: transport.path,
}
: {
noServer: true,
}
);
if ('port' in transport) {
bridgeLogger.debug('bridge server listening on port %d', transport.port);
} else if ('server' in transport) {
bridgeLogger.debug(
'bridge server attached to existing HTTP server at path %s',
transport.path ?? '/'
);
} else {
bridgeLogger.debug('bridge server created in noServer mode');
}
const emitter = new EventEmitter();
const clients = new Set<WebSocket>();
const binaryStore = new BinaryStore();
Expand Down
2 changes: 2 additions & 0 deletions packages/bridge/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type { TestCollectorEvents } from './shared/test-collector.js';
import type { BundlerEvents } from './shared/bundler.js';
import type { HarnessPlatform } from '@react-native-harness/platforms';

export const HARNESS_BRIDGE_PATH = '/__harness';

export type FileReference = {
path: string;
};
Expand Down
2 changes: 2 additions & 0 deletions packages/bundler-metro/src/__tests__/startup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const createMetroInstance = (
overrides: Partial<MetroInstance> = {}
): MetroInstance => ({
events: getEmitter<ReportableEvent>(),
httpServer: {} as never,
websocketEndpoints: {},
waitUntilHealthy: vi.fn(async () => 'HTTP 200: packager-status:running'),
prewarm: vi.fn(async () => false),
dispose: vi.fn(async () => undefined),
Expand Down
5 changes: 4 additions & 1 deletion packages/bundler-metro/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const getMetroInstance = async (
options: MetroOptions,
abortSignal: AbortSignal
): Promise<MetroInstance> => {
const { projectRoot, harnessConfig } = options;
const { projectRoot, harnessConfig, websocketEndpoints = {} } = options;
const metroPort = harnessConfig.metroPort;
metroLogger.debug(
'creating Metro instance for %s on port %d',
Expand Down Expand Up @@ -131,6 +131,7 @@ export const getMetroInstance = async (
const maybeServer = await Metro.runServer(config, {
waitForBundler: true,
unstable_extraMiddleware: [middleware],
websocketEndpoints,
...(metroBindHost ? { host: metroBindHost } : {}),
watch: process.env.CI ? false : undefined,
});
Expand All @@ -150,6 +151,8 @@ export const getMetroInstance = async (

return {
events: reporter,
httpServer: server,
websocketEndpoints,
waitUntilHealthy: async ({ timeoutMs, signal }) =>
waitForMetroStatus({ port: metroPort, timeoutMs, signal }),
prewarm: ({ platform, signal }) => {
Expand Down
7 changes: 6 additions & 1 deletion packages/bundler-metro/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
export { getMetroInstance } from './factory.js';
export type { MetroInstance, MetroFactory, MetroOptions } from './types.js';
export type {
MetroInstance,
MetroFactory,
MetroOptions,
MetroWebSocketEndpoint,
} from './types.js';
export type { Reporter, ReportableEvent } from './reporter.js';
export { isMetroCacheReusable } from './paths.js';
export {
Expand Down
1 change: 0 additions & 1 deletion packages/bundler-metro/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { getHarnessManifestPath } from './paths.js';
const getManifestContent = (harnessConfig: HarnessConfig): string => {
return `global.RN_HARNESS = {
appRegistryComponentName: '${harnessConfig.appRegistryComponentName}',
webSocketPort: ${harnessConfig.webSocketPort},
disableViewFlattening: ${harnessConfig.disableViewFlattening},
};`;
};
Expand Down
11 changes: 11 additions & 0 deletions packages/bundler-metro/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import type { Server as HttpServer } from 'node:http';
import type { Server as HttpsServer } from 'node:https';
import type { RunServerOptions } from 'metro';
import type { Reporter } from './reporter.js';
import type { Config as HarnessConfig } from '@react-native-harness/config';

export type MetroWebSocketEndpoints = NonNullable<
RunServerOptions['websocketEndpoints']
>;
export type MetroWebSocketEndpoint = MetroWebSocketEndpoints[string];

export type MetroOptions = {
projectRoot: string;
harnessConfig: HarnessConfig;
websocketEndpoints?: MetroWebSocketEndpoints;
};

export type WaitForMetroHealthOptions = {
Expand All @@ -18,6 +27,8 @@ export type PrewarmMetroBundleOptions = {

export type MetroInstance = {
events: Reporter;
httpServer: HttpServer | HttpsServer;
websocketEndpoints: MetroWebSocketEndpoints;
waitUntilHealthy: (options: WaitForMetroHealthOptions) => Promise<string>;
prewarm: (options: PrewarmMetroBundleOptions) => Promise<boolean>;
dispose: () => Promise<void>;
Expand Down
3 changes: 3 additions & 0 deletions packages/bundler-metro/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
{
"path": "../tools"
},
{
"path": "../bridge"
},
{
"path": "../babel-preset"
},
Expand Down
9 changes: 6 additions & 3 deletions packages/bundler-metro/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@
"include": ["src/**/*.ts"],
"references": [
{
"path": "../babel-preset/tsconfig.lib.json"
"path": "../runtime/tsconfig.lib.json"
},
{
"path": "../config/tsconfig.lib.json"
},
{
"path": "../runtime/tsconfig.lib.json"
"path": "../tools/tsconfig.lib.json"
},
{
"path": "../tools/tsconfig.lib.json"
"path": "../bridge/tsconfig.lib.json"
},
{
"path": "../babel-preset/tsconfig.lib.json"
}
]
}
7 changes: 6 additions & 1 deletion packages/config/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ export const ConfigSchema = z
.max(65535, 'Metro port must be at most 65535')
.optional()
.default(DEFAULT_METRO_PORT),
webSocketPort: z.number().optional().default(3001),
webSocketPort: z
.number()
.optional()
.describe(
'Deprecated. Bridge traffic now uses metroPort and this value is ignored.'
),
bridgeTimeout: z
.number()
.min(1000, 'Bridge timeout must be at least 1 second')
Expand Down
1 change: 0 additions & 1 deletion packages/jest/src/__tests__/harness-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ const createHarnessConfig = (
): HarnessConfig =>
({
appRegistryComponentName: 'App',
webSocketPort: 8081,
disableViewFlattening: false,
bridgeTimeout: 5000,
entryPoint: 'index.js',
Expand Down
17 changes: 17 additions & 0 deletions packages/jest/src/__tests__/harness.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { EventEmitter } from 'node:events';
import { HARNESS_BRIDGE_PATH } from '@react-native-harness/bridge';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Config as HarnessConfig } from '@react-native-harness/config';
import { definePlugin } from '@react-native-harness/plugins';
Expand Down Expand Up @@ -69,6 +70,7 @@ const createBridgeServer = () => {

return {
serverBridge: {
ws: {} as BridgeServer['ws'],
rpc: {
clients: [],
},
Expand Down Expand Up @@ -117,6 +119,8 @@ const createMetroInstance = (
overrides: Partial<MetroInstance> = {}
): MetroInstance => ({
events: createReporter(),
httpServer: {} as never,
websocketEndpoints: {},
waitUntilHealthy: vi.fn(async () => 'HTTP 200: packager-status:running'),
prewarm: vi.fn(async () => false),
dispose: vi.fn(async () => undefined),
Expand Down Expand Up @@ -406,6 +410,19 @@ describe('getHarness', () => {
'/tmp/project'
);

expect(mocks.getBridgeServer).toHaveBeenCalledWith(
expect.objectContaining({
noServer: true,
})
);
expect(mocks.getMetroInstance).toHaveBeenCalledWith(
expect.objectContaining({
websocketEndpoints: {
[HARNESS_BRIDGE_PATH]: serverBridge.ws,
},
}),
expect.any(AbortSignal)
);
await harness.restart('/tmp/restart.harness.ts');

expect(stopApp).toHaveBeenCalledTimes(1);
Expand Down
65 changes: 40 additions & 25 deletions packages/jest/src/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
BridgeServer,
} from '@react-native-harness/bridge/server';
import {
HARNESS_BRIDGE_PATH,
HarnessContext,
type BridgeEvents,
type DeviceDescriptor,
Expand All @@ -20,6 +21,7 @@ import {
isMetroCacheReusable,
waitForMetroBackedAppReady,
type MetroInstance,
type MetroWebSocketEndpoint,
type ReportableEvent,
} from '@react-native-harness/bundler-metro';
import {
Expand Down Expand Up @@ -272,34 +274,47 @@ const getHarnessInternal = async (
trackHook(pluginManager.callHook(name, payload));
};

const serverBridge = await getBridgeServer({
noServer: true,
timeout: config.bridgeTimeout,
context,
});
harnessLogger.debug(
'starting Metro, platform runner, and bridge initialization'
);
const [metroInstance, platformInstance, serverBridge] = await Promise.all([
getMetroInstance({ projectRoot, harnessConfig: config }, signal).then(
(instance) => {
harnessLogger.debug('Metro initialized');
return instance;
}
),
import(platform.runner)
.then((module) => module.default(platform.config, config))
.then((instance) => {
harnessLogger.debug('platform runner initialized');
return instance;
}),
getBridgeServer({
port: config.webSocketPort,
timeout: config.bridgeTimeout,
context,
}).then((bridge) => {
harnessLogger.debug(
'bridge server initialized on port %d',
config.webSocketPort
);
return bridge;
}),
]);
harnessLogger.debug(
'bridge server initialized on Metro websocket path %s',
HARNESS_BRIDGE_PATH
);
const [metroInstance, platformInstance] = await (async () => {
try {
return await Promise.all([
getMetroInstance(
{
projectRoot,
harnessConfig: config,
websocketEndpoints: {
[HARNESS_BRIDGE_PATH]:
serverBridge.ws as unknown as MetroWebSocketEndpoint,
},
},
signal
).then((instance) => {
harnessLogger.debug('Metro initialized');
return instance;
}),
import(platform.runner).then((module) =>
module.default(platform.config, config)
).then((instance) => {
harnessLogger.debug('platform runner initialized');
return instance;
}),
]);
} catch (error) {
serverBridge.dispose();
throw error;
}
})();
const crashArtifactWriter = createCrashArtifactWriter({
runnerName: platform.name,
platformId: platform.platformId,
Expand Down
9 changes: 9 additions & 0 deletions packages/jest/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ export const setup = async (globalConfig: JestConfig.GlobalConfig) => {

const selectedRunner = getHarnessRunner(harnessConfig, cliArgs);

if (
harnessConfig.webSocketPort != null &&
harnessConfig.webSocketPort !== harnessConfig.metroPort
) {
logger.warn(
`Config option "webSocketPort" is deprecated and ignored. Harness now uses metroPort (${harnessConfig.metroPort}) for bridge traffic.`
);
}

if (globalConfig.collectCoverage) {
// This is going to be used by @react-native-harness/babel-preset
// to enable instrumentation of test files.
Expand Down
Loading
Loading