From 17ebe11239a8d3763508c4ced1d733ac6b868675 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 5 Feb 2026 19:17:34 +0100 Subject: [PATCH] feat: add Metro bundle pre-warming --- packages/bundler-metro/src/index.ts | 1 + packages/bundler-metro/src/prewarm.ts | 36 ++++++++++++++++++++++++ packages/config/src/types.ts | 1 + packages/jest/src/harness.ts | 12 ++++++++ packages/jest/src/logs.ts | 6 ++++ packages/platform-android/src/factory.ts | 1 + packages/platform-ios/src/factory.ts | 1 + packages/platform-vega/src/factory.ts | 1 + packages/platform-web/src/factory.ts | 1 + packages/platforms/src/types.ts | 1 + 10 files changed, 61 insertions(+) create mode 100644 packages/bundler-metro/src/prewarm.ts diff --git a/packages/bundler-metro/src/index.ts b/packages/bundler-metro/src/index.ts index 19d3d468..d786fc17 100644 --- a/packages/bundler-metro/src/index.ts +++ b/packages/bundler-metro/src/index.ts @@ -1,3 +1,4 @@ export { getMetroInstance } from './factory.js'; export type { MetroInstance, MetroFactory, MetroOptions } from './types.js'; +export { prewarmMetroBundle } from './prewarm.js'; export type { Reporter, ReportableEvent } from './reporter.js'; diff --git a/packages/bundler-metro/src/prewarm.ts b/packages/bundler-metro/src/prewarm.ts new file mode 100644 index 00000000..15be8438 --- /dev/null +++ b/packages/bundler-metro/src/prewarm.ts @@ -0,0 +1,36 @@ +import { METRO_PORT } from './constants.js'; +import { getResolvedEntryPointWithoutExtension } from './entry-point-utils.js'; + +type PrewarmOptions = { + projectRoot: string; + entryPoint: string; + platform: string; + dev: boolean; + minify: boolean; + signal: AbortSignal; +}; + +export const prewarmMetroBundle = async ( + options: PrewarmOptions +): Promise => { + const { projectRoot, entryPoint, platform, dev, minify, signal } = options; + const resolvedEntryPoint = getResolvedEntryPointWithoutExtension( + projectRoot, + entryPoint + ); + const searchParams = new URLSearchParams({ + platform, + dev: String(dev), + minify: String(minify), + }); + const url = `http://localhost:${METRO_PORT}/${resolvedEntryPoint}.bundle?${searchParams.toString()}`; + + const response = await fetch(url, { signal }); + + if (!response.ok) { + const snippet = (await response.text()).trim(); + throw new Error( + `Metro pre-warm failed (${response.status} ${response.statusText}). ${snippet}` + ); + } +}; diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 916d9aba..b73a6eb9 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -10,6 +10,7 @@ const RunnerSchema = z.object({ ), config: z.record(z.any()), runner: z.string(), + platformId: z.string(), }); export const ConfigSchema = z diff --git a/packages/jest/src/harness.ts b/packages/jest/src/harness.ts index cf7d54f4..3ed8ff5f 100644 --- a/packages/jest/src/harness.ts +++ b/packages/jest/src/harness.ts @@ -13,6 +13,7 @@ import { } from '@react-native-harness/platforms'; import { getMetroInstance, + prewarmMetroBundle, Reporter, ReportableEvent, } from '@react-native-harness/bundler-metro'; @@ -20,6 +21,7 @@ import { InitializationTimeoutError, MaxAppRestartsError } from './errors.js'; import { Config as HarnessConfig } from '@react-native-harness/config'; import { createCrashMonitor, CrashMonitor } from './crash-monitor.js'; import { createClientLogListener } from './client-log-handler.js'; +import { logMetroPrewarmCompleted } from './logs.js'; export type HarnessRunTestsOptions = Exclude; @@ -176,6 +178,16 @@ const getHarnessInternal = async ( } try { + await prewarmMetroBundle({ + projectRoot, + entryPoint: config.entryPoint, + platform: platform.platformId, + dev: true, + minify: false, + signal, + }); + logMetroPrewarmCompleted(platform); + await waitForAppReady({ metroEvents: metroInstance.events, serverBridge, diff --git a/packages/jest/src/logs.ts b/packages/jest/src/logs.ts index a6fa30c8..d9e78b73 100644 --- a/packages/jest/src/logs.ts +++ b/packages/jest/src/logs.ts @@ -25,6 +25,12 @@ export const logTestEnvironmentReady = (runner: HarnessPlatform): void => { log(`${TAG} Runner ${chalk.bold(runner.name)} ready\n`); }; +export const logMetroPrewarmCompleted = (runner: HarnessPlatform): void => { + log( + `${TAG} Metro pre-warm for ${chalk.bold(runner.name)} completed\n` + ); +}; + export const getErrorMessage = (error: HarnessError): string => { return `${ERROR_TAG} ${error.message}\n`; }; diff --git a/packages/platform-android/src/factory.ts b/packages/platform-android/src/factory.ts index d136e4cc..217ff06f 100644 --- a/packages/platform-android/src/factory.ts +++ b/packages/platform-android/src/factory.ts @@ -30,4 +30,5 @@ export const androidPlatform = ( name: config.name, config, runner: import.meta.resolve('./runner.js'), + platformId: 'android', }); diff --git a/packages/platform-ios/src/factory.ts b/packages/platform-ios/src/factory.ts index 09e8b30c..2ae483f0 100644 --- a/packages/platform-ios/src/factory.ts +++ b/packages/platform-ios/src/factory.ts @@ -25,4 +25,5 @@ export const applePlatform = ( name: config.name, config, runner: import.meta.resolve('./runner.js'), + platformId: 'ios', }); diff --git a/packages/platform-vega/src/factory.ts b/packages/platform-vega/src/factory.ts index 55bac26e..915f9245 100644 --- a/packages/platform-vega/src/factory.ts +++ b/packages/platform-vega/src/factory.ts @@ -12,4 +12,5 @@ export const vegaPlatform = ( name: config.name, config, runner: import.meta.resolve('./runner.js'), + platformId: 'vega', }); diff --git a/packages/platform-web/src/factory.ts b/packages/platform-web/src/factory.ts index abc07c8d..26e47ffb 100644 --- a/packages/platform-web/src/factory.ts +++ b/packages/platform-web/src/factory.ts @@ -7,6 +7,7 @@ export const webPlatform = ( name: config.name, config, runner: import.meta.resolve('./runner.js'), + platformId: 'web', }); export const chromium = ( diff --git a/packages/platforms/src/types.ts b/packages/platforms/src/types.ts index d5c2e75c..6050fc48 100644 --- a/packages/platforms/src/types.ts +++ b/packages/platforms/src/types.ts @@ -10,6 +10,7 @@ export type HarnessPlatform> = { name: string; config: TConfig; runner: string; + platformId: string; }; export type RunTarget = {