Skip to content

Commit 1248abf

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/ui-references
2 parents 2d7265a + a0c5310 commit 1248abf

16 files changed

Lines changed: 399 additions & 61 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { getMetroInstance } from './factory.js';
22
export type { MetroInstance, MetroFactory, MetroOptions } from './types.js';
3+
export { prewarmMetroBundle } from './prewarm.js';
34
export type { Reporter, ReportableEvent } from './reporter.js';
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { METRO_PORT } from './constants.js';
2+
import { getResolvedEntryPointWithoutExtension } from './entry-point-utils.js';
3+
4+
type PrewarmOptions = {
5+
projectRoot: string;
6+
entryPoint: string;
7+
platform: string;
8+
dev: boolean;
9+
minify: boolean;
10+
signal: AbortSignal;
11+
};
12+
13+
export const prewarmMetroBundle = async (
14+
options: PrewarmOptions
15+
): Promise<void> => {
16+
const { projectRoot, entryPoint, platform, dev, minify, signal } = options;
17+
const resolvedEntryPoint = getResolvedEntryPointWithoutExtension(
18+
projectRoot,
19+
entryPoint
20+
);
21+
const searchParams = new URLSearchParams({
22+
platform,
23+
dev: String(dev),
24+
minify: String(minify),
25+
});
26+
const url = `http://localhost:${METRO_PORT}/${resolvedEntryPoint}.bundle?${searchParams.toString()}`;
27+
28+
const response = await fetch(url, { signal });
29+
30+
if (!response.ok) {
31+
const snippet = (await response.text()).trim();
32+
throw new Error(
33+
`Metro pre-warm failed (${response.status} ${response.statusText}). ${snippet}`
34+
);
35+
}
36+
};

packages/config/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const RunnerSchema = z.object({
1010
),
1111
config: z.record(z.any()),
1212
runner: z.string(),
13+
platformId: z.string(),
1314
});
1415

1516
export const ConfigSchema = z

packages/jest/src/harness.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ import {
1313
} from '@react-native-harness/platforms';
1414
import {
1515
getMetroInstance,
16+
prewarmMetroBundle,
1617
Reporter,
1718
ReportableEvent,
1819
} from '@react-native-harness/bundler-metro';
1920
import { InitializationTimeoutError, MaxAppRestartsError } from './errors.js';
2021
import { Config as HarnessConfig } from '@react-native-harness/config';
2122
import { createCrashMonitor, CrashMonitor } from './crash-monitor.js';
2223
import { createClientLogListener } from './client-log-handler.js';
24+
import { logMetroPrewarmCompleted } from './logs.js';
2325

2426
export type HarnessRunTestsOptions = Exclude<TestExecutionOptions, 'platform'>;
2527

@@ -176,6 +178,16 @@ const getHarnessInternal = async (
176178
}
177179

178180
try {
181+
await prewarmMetroBundle({
182+
projectRoot,
183+
entryPoint: config.entryPoint,
184+
platform: platform.platformId,
185+
dev: true,
186+
minify: false,
187+
signal,
188+
});
189+
logMetroPrewarmCompleted(platform);
190+
179191
await waitForAppReady({
180192
metroEvents: metroInstance.events,
181193
serverBridge,

packages/jest/src/logs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ export const logTestEnvironmentReady = (runner: HarnessPlatform): void => {
2525
log(`${TAG} Runner ${chalk.bold(runner.name)} ready\n`);
2626
};
2727

28+
export const logMetroPrewarmCompleted = (runner: HarnessPlatform): void => {
29+
log(
30+
`${TAG} Metro pre-warm for ${chalk.bold(runner.name)} completed\n`
31+
);
32+
};
33+
2834
export const getErrorMessage = (error: HarnessError): string => {
2935
return `${ERROR_TAG} ${error.message}\n`;
3036
};

packages/metro/src/resolver.ts

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { HarnessResolver, MetroResolver } from './types';
2+
3+
export const createHarnessResolver = (
4+
resolvers: HarnessResolver[]
5+
): MetroResolver => {
6+
return (context, moduleName, platform) => {
7+
for (const resolver of resolvers) {
8+
const result = resolver(context, moduleName, platform);
9+
if (result != null) {
10+
return result;
11+
}
12+
}
13+
14+
return context.resolveRequest(context, moduleName, platform);
15+
};
16+
};
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import type { MetroConfig } from '@react-native/metro-config';
2+
import type { Config as HarnessConfig } from '@react-native-harness/config';
3+
import { createHarnessResolver } from './composite-resolver';
4+
import { createTsConfigResolver } from './tsconfig-resolver';
5+
import type { HarnessResolver, MetroResolver } from './types';
6+
7+
export const createHarnessEntryPointResolver = (
8+
harnessConfig: HarnessConfig
9+
): HarnessResolver => {
10+
// Can be relative to the project root or absolute, need to normalize it
11+
const resolvedEntryPointPath = require.resolve(harnessConfig.entryPoint, {
12+
paths: [process.cwd()],
13+
});
14+
15+
return (_context, moduleName, _platform) => {
16+
if (moduleName === resolvedEntryPointPath) {
17+
return {
18+
type: 'sourceFile',
19+
filePath: require.resolve('@react-native-harness/runtime/entry-point'),
20+
};
21+
}
22+
23+
if (moduleName === harnessConfig.entryPoint) {
24+
return {
25+
type: 'sourceFile',
26+
filePath: require.resolve('@react-native-harness/runtime/entry-point'),
27+
};
28+
}
29+
30+
if (typeof moduleName === 'string') {
31+
try {
32+
const resolvedModuleName = require.resolve(moduleName, {
33+
paths: [process.cwd()],
34+
});
35+
if (resolvedModuleName === resolvedEntryPointPath) {
36+
return {
37+
type: 'sourceFile',
38+
filePath: require.resolve(
39+
'@react-native-harness/runtime/entry-point'
40+
),
41+
};
42+
}
43+
} catch {
44+
// Ignore and fall through
45+
}
46+
}
47+
48+
return null;
49+
};
50+
};
51+
52+
export const createJestGlobalsResolver = (): HarnessResolver => {
53+
return (_context, moduleName, _platform) => {
54+
// Intercept @jest/globals imports and redirect to mock module
55+
if (moduleName === '@jest/globals') {
56+
return {
57+
type: 'sourceFile',
58+
filePath: require.resolve('../jest-globals-mock'),
59+
};
60+
}
61+
62+
return null;
63+
};
64+
};
65+
66+
export const createJsxRuntimeResolver = (): HarnessResolver => {
67+
const resolvedJsxRuntimePath = require.resolve(
68+
'@react-native-harness/runtime/jsx-runtime'
69+
);
70+
const resolvedJsxDevRuntimePath = require.resolve(
71+
'@react-native-harness/runtime/jsx-dev-runtime'
72+
);
73+
74+
return (_context, moduleName, _platform) => {
75+
if (moduleName === '@react-native-harness/runtime/jsx-runtime') {
76+
return {
77+
type: 'sourceFile',
78+
filePath: resolvedJsxRuntimePath,
79+
};
80+
}
81+
82+
if (moduleName === '@react-native-harness/runtime/jsx-dev-runtime') {
83+
return {
84+
type: 'sourceFile',
85+
filePath: resolvedJsxDevRuntimePath,
86+
};
87+
}
88+
89+
return null;
90+
};
91+
};
92+
93+
export const getHarnessResolver = (
94+
metroConfig: MetroConfig,
95+
harnessConfig: HarnessConfig
96+
): MetroResolver => {
97+
const userResolver = metroConfig.resolver?.resolveRequest;
98+
const resolvers: HarnessResolver[] = [
99+
createHarnessEntryPointResolver(harnessConfig),
100+
createJestGlobalsResolver(),
101+
createJsxRuntimeResolver(),
102+
createTsConfigResolver(process.cwd()),
103+
userResolver,
104+
].filter((resolver): resolver is HarnessResolver => !!resolver);
105+
106+
return createHarnessResolver(resolvers);
107+
};

0 commit comments

Comments
 (0)