Skip to content

Commit 480c2a4

Browse files
authored
refactor: inject bridge server into Metro (#88)
This change moves Harness bridge communication onto Metro so everything runs through a single dev server port instead of a separate bridge port. It keeps the setup simpler and more predictable, reduces extra platform-specific port wiring, and preserves compatibility by keeping webSocketPort around as a deprecated config option for now.
1 parent 4844dc3 commit 480c2a4

File tree

22 files changed

+167
-67
lines changed

22 files changed

+167
-67
lines changed

actions/shared/index.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4388,7 +4388,7 @@ var ConfigSchema = external_exports.object({
43884388
defaultRunner: external_exports.string().optional(),
43894389
host: external_exports.string().min(1, "Host is required").optional(),
43904390
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),
4391-
webSocketPort: external_exports.number().optional().default(3001),
4391+
webSocketPort: external_exports.number().optional().describe("Deprecated. Bridge traffic now uses metroPort and this value is ignored."),
43924392
bridgeTimeout: external_exports.number().min(1e3, "Bridge timeout must be at least 1 second").default(6e4),
43934393
bundleStartTimeout: external_exports.number().min(1e3, "Bundle start timeout must be at least 1 second").default(15e3),
43944394
maxAppRestarts: external_exports.number().min(0, "Max app restarts must be at least 0").default(2),

apps/playground/rn-harness.config.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ export default {
116116
],
117117
defaultRunner: 'android',
118118
bridgeTimeout: 120000,
119-
webSocketPort: 3002,
120119

121120
resetEnvironmentBetweenTestFiles: true,
122121
unstable__enableMetroCache: true,

packages/bridge/src/server.ts

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { WebSocketServer, type WebSocket } from 'ws';
22
import { type BirpcGroup, createBirpcGroup } from 'birpc';
33
import { logger } from '@react-native-harness/tools';
44
import { EventEmitter } from 'node:events';
5+
import type { Server as HttpServer } from 'node:http';
6+
import type { Server as HttpsServer } from 'node:https';
57
import fs from 'node:fs/promises';
68
import os from 'node:os';
79
import path from 'node:path';
@@ -24,8 +26,25 @@ import { matchImageSnapshot } from './image-snapshot.js';
2426
export { DeviceNotRespondingError } from './errors.js';
2527
const bridgeLogger = logger.child('bridge');
2628

27-
export type BridgeServerOptions = {
29+
type BridgeServerStandaloneOptions = {
2830
port: number;
31+
host?: string;
32+
};
33+
34+
type BridgeServerAttachedOptions = {
35+
server: HttpServer | HttpsServer;
36+
path?: string;
37+
};
38+
39+
type BridgeServerNoServerOptions = {
40+
noServer: true;
41+
};
42+
43+
export type BridgeServerOptions = (
44+
| BridgeServerStandaloneOptions
45+
| BridgeServerAttachedOptions
46+
| BridgeServerNoServerOptions
47+
) & {
2948
timeout?: number;
3049
context: HarnessContext;
3150
};
@@ -55,16 +74,43 @@ export type BridgeServer = {
5574
};
5675

5776
export const getBridgeServer = async ({
58-
port,
5977
timeout,
6078
context,
79+
...transport
6180
}: BridgeServerOptions): Promise<BridgeServer> => {
62-
const wss = await new Promise<WebSocketServer>((resolve) => {
63-
const server = new WebSocketServer({ port, host: '0.0.0.0' }, () => {
64-
resolve(server);
65-
});
66-
});
67-
bridgeLogger.debug('bridge server listening on port %d', port);
81+
const wss =
82+
'port' in transport
83+
? await new Promise<WebSocketServer>((resolve) => {
84+
const server = new WebSocketServer(
85+
{
86+
port: transport.port,
87+
host: transport.host ?? '0.0.0.0',
88+
},
89+
() => {
90+
resolve(server);
91+
}
92+
);
93+
})
94+
: new WebSocketServer(
95+
'server' in transport
96+
? {
97+
server: transport.server,
98+
path: transport.path,
99+
}
100+
: {
101+
noServer: true,
102+
}
103+
);
104+
if ('port' in transport) {
105+
bridgeLogger.debug('bridge server listening on port %d', transport.port);
106+
} else if ('server' in transport) {
107+
bridgeLogger.debug(
108+
'bridge server attached to existing HTTP server at path %s',
109+
transport.path ?? '/'
110+
);
111+
} else {
112+
bridgeLogger.debug('bridge server created in noServer mode');
113+
}
68114
const emitter = new EventEmitter();
69115
const clients = new Set<WebSocket>();
70116
const binaryStore = new BinaryStore();

packages/bridge/src/shared.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import type { TestCollectorEvents } from './shared/test-collector.js';
66
import type { BundlerEvents } from './shared/bundler.js';
77
import type { HarnessPlatform } from '@react-native-harness/platforms';
88

9+
export const HARNESS_BRIDGE_PATH = '/__harness';
10+
911
export type FileReference = {
1012
path: string;
1113
};

packages/bundler-metro/src/__tests__/startup.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const createMetroInstance = (
2727
overrides: Partial<MetroInstance> = {}
2828
): MetroInstance => ({
2929
events: getEmitter<ReportableEvent>(),
30+
httpServer: {} as never,
31+
websocketEndpoints: {},
3032
waitUntilHealthy: vi.fn(async () => 'HTTP 200: packager-status:running'),
3133
prewarm: vi.fn(async () => false),
3234
dispose: vi.fn(async () => undefined),

packages/bundler-metro/src/factory.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export const getMetroInstance = async (
9090
options: MetroOptions,
9191
abortSignal: AbortSignal
9292
): Promise<MetroInstance> => {
93-
const { projectRoot, harnessConfig } = options;
93+
const { projectRoot, harnessConfig, websocketEndpoints = {} } = options;
9494
const metroPort = harnessConfig.metroPort;
9595
metroLogger.debug(
9696
'creating Metro instance for %s on port %d',
@@ -131,6 +131,7 @@ export const getMetroInstance = async (
131131
const maybeServer = await Metro.runServer(config, {
132132
waitForBundler: true,
133133
unstable_extraMiddleware: [middleware],
134+
websocketEndpoints,
134135
...(metroBindHost ? { host: metroBindHost } : {}),
135136
watch: process.env.CI ? false : undefined,
136137
});
@@ -150,6 +151,8 @@ export const getMetroInstance = async (
150151

151152
return {
152153
events: reporter,
154+
httpServer: server,
155+
websocketEndpoints,
153156
waitUntilHealthy: async ({ timeoutMs, signal }) =>
154157
waitForMetroStatus({ port: metroPort, timeoutMs, signal }),
155158
prewarm: ({ platform, signal }) => {

packages/bundler-metro/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
export { getMetroInstance } from './factory.js';
2-
export type { MetroInstance, MetroFactory, MetroOptions } from './types.js';
2+
export type {
3+
MetroInstance,
4+
MetroFactory,
5+
MetroOptions,
6+
MetroWebSocketEndpoint,
7+
} from './types.js';
38
export type { Reporter, ReportableEvent } from './reporter.js';
49
export { isMetroCacheReusable } from './paths.js';
510
export {

packages/bundler-metro/src/manifest.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { getHarnessManifestPath } from './paths.js';
66
const getManifestContent = (harnessConfig: HarnessConfig): string => {
77
return `global.RN_HARNESS = {
88
appRegistryComponentName: '${harnessConfig.appRegistryComponentName}',
9-
webSocketPort: ${harnessConfig.webSocketPort},
109
disableViewFlattening: ${harnessConfig.disableViewFlattening},
1110
};`;
1211
};

packages/bundler-metro/src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
import type { Server as HttpServer } from 'node:http';
2+
import type { Server as HttpsServer } from 'node:https';
3+
import type { RunServerOptions } from 'metro';
14
import type { Reporter } from './reporter.js';
25
import type { Config as HarnessConfig } from '@react-native-harness/config';
36

7+
export type MetroWebSocketEndpoints = NonNullable<
8+
RunServerOptions['websocketEndpoints']
9+
>;
10+
export type MetroWebSocketEndpoint = MetroWebSocketEndpoints[string];
11+
412
export type MetroOptions = {
513
projectRoot: string;
614
harnessConfig: HarnessConfig;
15+
websocketEndpoints?: MetroWebSocketEndpoints;
716
};
817

918
export type WaitForMetroHealthOptions = {
@@ -18,6 +27,8 @@ export type PrewarmMetroBundleOptions = {
1827

1928
export type MetroInstance = {
2029
events: Reporter;
30+
httpServer: HttpServer | HttpsServer;
31+
websocketEndpoints: MetroWebSocketEndpoints;
2132
waitUntilHealthy: (options: WaitForMetroHealthOptions) => Promise<string>;
2233
prewarm: (options: PrewarmMetroBundleOptions) => Promise<boolean>;
2334
dispose: () => Promise<void>;

packages/bundler-metro/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
{
1313
"path": "../tools"
1414
},
15+
{
16+
"path": "../bridge"
17+
},
1518
{
1619
"path": "../babel-preset"
1720
},

0 commit comments

Comments
 (0)