Skip to content

Commit c91ecfb

Browse files
committed
refactor: harden the code
1 parent 841c099 commit c91ecfb

18 files changed

Lines changed: 300 additions & 50 deletions

File tree

packages/bundler-metro/src/factory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const getMetroInstance = async (
6262

6363
const middleware = connect()
6464
.use(nocache())
65-
.use('/', getExpoMiddleware(projectRoot, harnessConfig.entryPoint, metroPort))
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, metroPort: number) =>
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:${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`,
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:${metroPort}`,
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
@@ -3,17 +3,18 @@ import { getResolvedEntryPointWithoutExtension } from './entry-point-utils.js';
33
type PrewarmOptions = {
44
projectRoot: string;
55
entryPoint: string;
6+
port: number;
67
platform: string;
78
dev: boolean;
89
minify: boolean;
910
signal: AbortSignal;
10-
metroPort: number;
1111
};
1212

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:${options.metroPort}/${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: 4 additions & 1 deletion
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()
@@ -24,10 +26,11 @@ export const ConfigSchema = z
2426
host: z.string().min(1, 'Host is required').optional(),
2527
metroPort: z
2628
.number()
29+
.int('Metro port must be an integer')
2730
.min(1, 'Metro port must be at least 1')
2831
.max(65535, 'Metro port must be at most 65535')
2932
.optional()
30-
.default(8081),
33+
.default(DEFAULT_METRO_PORT),
3134
webSocketPort: z.number().optional().default(3001),
3235
bridgeTimeout: z
3336
.number()

packages/jest/src/harness.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,11 @@ 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,
184185
signal,
185-
metroPort: config.metroPort,
186186
});
187187
logMetroPrewarmCompleted(platform);
188188
await appMonitor.start();

packages/jest/src/setup.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
getConfig,
33
type Config as HarnessConfig,
4+
ConfigSchema,
45
} from '@react-native-harness/config';
56
import type { Config as JestConfig } from 'jest-runner';
67
import { getHarness } from './harness.js';
@@ -41,7 +42,7 @@ const getHarnessRunner = (
4142

4243
export const setup = async (globalConfig: JestConfig.GlobalConfig) => {
4344
preRunMessage.remove(process.stderr);
44-
const harnessConfig =
45+
let harnessConfig =
4546
global.HARNESS_CONFIG ?? (await getHarnessConfig(globalConfig));
4647

4748
if (global.HARNESS) {
@@ -59,7 +60,10 @@ export const setup = async (globalConfig: JestConfig.GlobalConfig) => {
5960
const cliArgs = getAdditionalCliArgs();
6061

6162
if (cliArgs.metroPort != null) {
62-
harnessConfig.metroPort = cliArgs.metroPort;
63+
harnessConfig = ConfigSchema.parse({
64+
...harnessConfig,
65+
metroPort: cliArgs.metroPort,
66+
});
6367
}
6468

6569
const selectedRunner = getHarnessRunner(harnessConfig, cliArgs);

packages/platform-android/src/adb.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -265,19 +265,3 @@ export const getConnectedDevices = async (): Promise<AdbDevice[]> => {
265265

266266
return devices;
267267
};
268-
269-
export const setDebugHttpHost = async (
270-
adbId: string,
271-
bundleId: string,
272-
host: string
273-
): Promise<void> => {
274-
const prefsPath = `shared_prefs/${bundleId}_preferences.xml`;
275-
const prefsContent = `<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?><map><string name=\\"debug_http_host\\">${host}</string></map>`;
276-
await spawn('adb', [
277-
'-s',
278-
adbId,
279-
'shell',
280-
`run-as ${bundleId} sh -c 'mkdir -p shared_prefs && printf "${prefsContent}" > ${prefsPath}'`,
281-
]);
282-
};
283-

packages/platform-android/src/runner.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@ import {
44
CreateAppMonitorOptions,
55
HarnessPlatformRunner,
66
} from '@react-native-harness/platforms';
7+
import type { Config as HarnessConfig } from '@react-native-harness/config';
78
import {
89
AndroidPlatformConfigSchema,
910
type AndroidPlatformConfig,
1011
} from './config.js';
1112
import { getAdbId } from './adb-id.js';
1213
import * as adb from './adb.js';
14+
import {
15+
applyHarnessDebugHttpHost,
16+
clearHarnessDebugHttpHost,
17+
} from './shared-prefs.js';
1318
import { getDeviceName } from './utils.js';
1419
import { createAndroidAppMonitor } from './app-monitor.js';
1520

1621
const getAndroidRunner = async (
1722
config: AndroidPlatformConfig,
18-
harnessConfig: { metroPort: number; webSocketPort: number }
23+
harnessConfig: HarnessConfig
1924
): Promise<HarnessPlatformRunner> => {
2025
const parsedConfig = AndroidPlatformConfigSchema.parse(config);
2126
const adbId = await getAdbId(parsedConfig.device);
@@ -40,7 +45,7 @@ const getAndroidRunner = async (
4045
adb.reversePort(adbId, 8080),
4146
adb.reversePort(adbId, harnessConfig.webSocketPort),
4247
adb.setHideErrorDialogs(adbId, true),
43-
adb.setDebugHttpHost(adbId, parsedConfig.bundleId, `localhost:${metroPort}`),
48+
applyHarnessDebugHttpHost(adbId, parsedConfig.bundleId, `localhost:${metroPort}`),
4449
]);
4550
const appUid = await adb.getAppUid(adbId, parsedConfig.bundleId);
4651

@@ -69,6 +74,7 @@ const getAndroidRunner = async (
6974
},
7075
dispose: async () => {
7176
await adb.stopApp(adbId, parsedConfig.bundleId);
77+
await clearHarnessDebugHttpHost(adbId, parsedConfig.bundleId);
7278
await adb.setHideErrorDialogs(adbId, false);
7379
},
7480
isAppRunning: async () => {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { spawn, SubprocessError } from '@react-native-harness/tools';
2+
3+
const DEBUG_HTTP_HOST_BLOCK_START =
4+
'<!-- react-native-harness:debug_http_host:start -->';
5+
const DEBUG_HTTP_HOST_BLOCK_END =
6+
'<!-- react-native-harness:debug_http_host:end -->';
7+
8+
const getSharedPrefsPath = (bundleId: string) =>
9+
`shared_prefs/${bundleId}_preferences.xml`;
10+
11+
const getHarnessDebugHttpHostBlock = (host: string) =>
12+
[
13+
DEBUG_HTTP_HOST_BLOCK_START,
14+
`<string name="debug_http_host">${host}</string>`,
15+
DEBUG_HTTP_HOST_BLOCK_END,
16+
].join('\n');
17+
18+
const stripHarnessDebugHttpHostBlock = (content: string): string =>
19+
content.replace(
20+
new RegExp(
21+
`\\s*${DEBUG_HTTP_HOST_BLOCK_START}\\n[\\s\\S]*?\\n${DEBUG_HTTP_HOST_BLOCK_END}\\s*`,
22+
'g'
23+
),
24+
'\n'
25+
);
26+
27+
const normalizeSharedPrefsContent = (content: string | null): string => {
28+
if (!content?.trim()) {
29+
return ['<?xml version="1.0" encoding="utf-8"?>', '<map>', '</map>'].join(
30+
'\n'
31+
);
32+
}
33+
34+
return stripHarnessDebugHttpHostBlock(content).trim();
35+
};
36+
37+
const insertBeforeClosingMap = (content: string, block: string): string => {
38+
if (!content.includes('</map>')) {
39+
throw new Error('Android shared preferences file is missing </map>.');
40+
}
41+
42+
return content.replace(/<\/map>\s*$/, ` ${block.replace(/\n/g, '\n ')}\n</map>`);
43+
};
44+
45+
const readSharedPrefsFile = async (
46+
adbId: string,
47+
bundleId: string
48+
): Promise<string | null> => {
49+
try {
50+
const { stdout } = await spawn('adb', [
51+
'-s',
52+
adbId,
53+
'shell',
54+
`run-as ${bundleId} cat ${getSharedPrefsPath(bundleId)}`,
55+
]);
56+
return stdout;
57+
} catch (error) {
58+
if (error instanceof SubprocessError && error.exitCode === 1) {
59+
return null;
60+
}
61+
62+
throw error;
63+
}
64+
};
65+
66+
const writeSharedPrefsFile = async (
67+
adbId: string,
68+
bundleId: string,
69+
content: string
70+
): Promise<void> => {
71+
await spawn(
72+
'adb',
73+
[
74+
'-s',
75+
adbId,
76+
'shell',
77+
`run-as ${bundleId} sh -c 'mkdir -p shared_prefs && cat > ${getSharedPrefsPath(bundleId)}'`,
78+
],
79+
{ stdin: { string: `${content.trim()}\n` } }
80+
);
81+
};
82+
83+
export const applyHarnessDebugHttpHost = async (
84+
adbId: string,
85+
bundleId: string,
86+
host: string
87+
): Promise<void> => {
88+
const existingContent = await readSharedPrefsFile(adbId, bundleId);
89+
const normalizedContent = normalizeSharedPrefsContent(existingContent);
90+
const nextContent = insertBeforeClosingMap(
91+
normalizedContent,
92+
getHarnessDebugHttpHostBlock(host)
93+
);
94+
await writeSharedPrefsFile(adbId, bundleId, nextContent);
95+
};
96+
97+
export const clearHarnessDebugHttpHost = async (
98+
adbId: string,
99+
bundleId: string
100+
): Promise<void> => {
101+
const existingContent = await readSharedPrefsFile(adbId, bundleId);
102+
103+
if (!existingContent) {
104+
return;
105+
}
106+
107+
const nextContent = stripHarnessDebugHttpHostBlock(existingContent).trim();
108+
109+
if (nextContent === existingContent.trim()) {
110+
return;
111+
}
112+
113+
await writeSharedPrefsFile(adbId, bundleId, nextContent);
114+
};

0 commit comments

Comments
 (0)