diff --git a/.changeset/configurable-metro-port.md b/.changeset/configurable-metro-port.md new file mode 100644 index 00000000..867d541f --- /dev/null +++ b/.changeset/configurable-metro-port.md @@ -0,0 +1,7 @@ +--- +'@react-native-harness/jest': patch +'@react-native-harness/platform-android': patch +'@react-native-harness/platform-apple': patch +--- + +Adds support for configuring the Metro port in Harness, including CLI overrides via `--metroPort`. Harness now also restores Android and iOS simulator Metro connection settings on cleanup so normal dev-mode launches keep working after a test run. diff --git a/actions/shared/index.cjs b/actions/shared/index.cjs index 6794be6f..db86abd5 100644 --- a/actions/shared/index.cjs +++ b/actions/shared/index.cjs @@ -4209,6 +4209,7 @@ var coerce = { var NEVER = INVALID; // ../config/dist/types.js +var DEFAULT_METRO_PORT = 8081; var RunnerSchema = external_exports.object({ name: external_exports.string().min(1, "Runner name is required").regex(/^[a-zA-Z0-9._-]+$/, "Runner name can only contain alphanumeric characters, dots, underscores, and hyphens"), config: external_exports.record(external_exports.any()), @@ -4221,6 +4222,7 @@ var ConfigSchema = external_exports.object({ runners: external_exports.array(RunnerSchema).min(1, "At least one runner is required"), 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), bridgeTimeout: external_exports.number().min(1e3, "Bridge timeout must be at least 1 second").default(6e4), /** @deprecated Removed in favor of crash supervisor. Accepted for backwards compatibility. */ diff --git a/packages/bundler-metro/src/constants.ts b/packages/bundler-metro/src/constants.ts deleted file mode 100644 index 286b4641..00000000 --- a/packages/bundler-metro/src/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const METRO_PORT = 8081; diff --git a/packages/bundler-metro/src/factory.ts b/packages/bundler-metro/src/factory.ts index 0d3b7f9a..32415dfc 100644 --- a/packages/bundler-metro/src/factory.ts +++ b/packages/bundler-metro/src/factory.ts @@ -6,7 +6,6 @@ import connect from 'connect'; import nocache from 'nocache'; import { isPortAvailable, getMetroPackage } from './utils.js'; import { MetroPortUnavailableError } from './errors.js'; -import { METRO_PORT } from './constants.js'; import type { MetroInstance, MetroOptions } from './types.js'; import { type Reporter, @@ -41,10 +40,11 @@ export const getMetroInstance = async ( abortSignal: AbortSignal ): Promise => { const { projectRoot, harnessConfig } = options; - const isDefaultPortAvailable = await isPortAvailable(METRO_PORT); + const metroPort = harnessConfig.metroPort; + const isMetroPortAvailable = await isPortAvailable(metroPort); - if (!isDefaultPortAvailable) { - throw new MetroPortUnavailableError(METRO_PORT); + if (!isMetroPortAvailable) { + throw new MetroPortUnavailableError(metroPort); } const Metro = getMetroPackage(projectRoot); @@ -52,7 +52,7 @@ export const getMetroInstance = async ( process.env.RN_HARNESS = 'true'; const projectMetroConfig = await Metro.loadConfig({ - port: METRO_PORT, + port: metroPort, projectRoot, }); const config = await withRnHarness(projectMetroConfig, true)(); @@ -62,7 +62,7 @@ export const getMetroInstance = async ( const middleware = connect() .use(nocache()) - .use('/', getExpoMiddleware(projectRoot, harnessConfig.entryPoint)) + .use('/', getExpoMiddleware(projectRoot, harnessConfig)) .use('/status', getStatusMiddleware(projectRoot)); const ready = waitForBundler(reporter, abortSignal); diff --git a/packages/bundler-metro/src/middlewares/expo-middleware.ts b/packages/bundler-metro/src/middlewares/expo-middleware.ts index b763f778..7e5d80a7 100644 --- a/packages/bundler-metro/src/middlewares/expo-middleware.ts +++ b/packages/bundler-metro/src/middlewares/expo-middleware.ts @@ -1,10 +1,11 @@ import type { IncomingMessage, ServerResponse } from 'node:http'; import type { NextFunction } from 'connect'; +import type { Config as HarnessConfig } from '@react-native-harness/config'; import crypto from 'node:crypto'; import { getResolvedEntryPointWithoutExtension } from '../entry-point-utils.js'; export const getExpoMiddleware = - (projectRoot: string, entryPoint: string) => + (projectRoot: string, harnessConfig: HarnessConfig) => (req: IncomingMessage, res: ServerResponse, next: NextFunction) => { if (req.url !== '/') { next(); @@ -14,7 +15,7 @@ export const getExpoMiddleware = const platform = req.headers['expo-platform'] as string; const resolvedEntryPoint = getResolvedEntryPointWithoutExtension( projectRoot, - entryPoint + harnessConfig.entryPoint ); const manifestJson = JSON.stringify({ @@ -24,7 +25,7 @@ export const getExpoMiddleware = launchAsset: { key: 'bundle', contentType: 'application/javascript', - url: `http://localhost:8081/${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`, + 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`, }, assets: [], metadata: {}, @@ -35,7 +36,7 @@ export const getExpoMiddleware = version: '1.0.0', }, expoGo: { - debuggerHost: 'localhost:8081', + debuggerHost: `localhost:${harnessConfig.metroPort}`, developer: { tool: 'expo-cli', projectRoot, diff --git a/packages/bundler-metro/src/prewarm.ts b/packages/bundler-metro/src/prewarm.ts index 15be8438..fe7e5492 100644 --- a/packages/bundler-metro/src/prewarm.ts +++ b/packages/bundler-metro/src/prewarm.ts @@ -1,9 +1,9 @@ -import { METRO_PORT } from './constants.js'; import { getResolvedEntryPointWithoutExtension } from './entry-point-utils.js'; type PrewarmOptions = { projectRoot: string; entryPoint: string; + port: number; platform: string; dev: boolean; minify: boolean; @@ -13,7 +13,8 @@ type PrewarmOptions = { export const prewarmMetroBundle = async ( options: PrewarmOptions ): Promise => { - const { projectRoot, entryPoint, platform, dev, minify, signal } = options; + const { projectRoot, entryPoint, port, platform, dev, minify, signal } = + options; const resolvedEntryPoint = getResolvedEntryPointWithoutExtension( projectRoot, entryPoint @@ -23,7 +24,7 @@ export const prewarmMetroBundle = async ( dev: String(dev), minify: String(minify), }); - const url = `http://localhost:${METRO_PORT}/${resolvedEntryPoint}.bundle?${searchParams.toString()}`; + const url = `http://localhost:${port}/${resolvedEntryPoint}.bundle?${searchParams.toString()}`; const response = await fetch(url, { signal }); diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 4034cb99..1f52244d 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -1,5 +1,6 @@ export { getConfig } from './reader.js'; export type { Config } from './types.js'; +export { ConfigSchema, DEFAULT_METRO_PORT } from './types.js'; export { ConfigValidationError, ConfigNotFoundError, diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 7cd40514..89fe7cc8 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -1,5 +1,7 @@ import { z } from 'zod'; +export const DEFAULT_METRO_PORT = 8081; + const RunnerSchema = z.object({ name: z .string() @@ -22,6 +24,13 @@ export const ConfigSchema = z runners: z.array(RunnerSchema).min(1, 'At least one runner is required'), defaultRunner: z.string().optional(), host: z.string().min(1, 'Host is required').optional(), + metroPort: z + .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: z.number().optional().default(3001), bridgeTimeout: z .number() diff --git a/packages/jest/src/cli-args.ts b/packages/jest/src/cli-args.ts index 58be4560..e2964573 100644 --- a/packages/jest/src/cli-args.ts +++ b/packages/jest/src/cli-args.ts @@ -3,6 +3,7 @@ import { hideBin } from 'yargs/helpers'; export type HarnessCliArgs = { harnessRunner?: string; + metroPort?: number; }; export const getAdditionalCliArgs = (): HarnessCliArgs => { @@ -11,6 +12,10 @@ export const getAdditionalCliArgs = (): HarnessCliArgs => { type: 'string', description: 'Specify which Harness runner to use', }) + .option('metroPort', { + type: 'number', + description: 'Override the Metro bundler port', + }) .strict(false) .help(false) .version(false) @@ -19,5 +24,6 @@ export const getAdditionalCliArgs = (): HarnessCliArgs => { return { harnessRunner: argv.harnessRunner, + metroPort: argv.metroPort, }; }; diff --git a/packages/jest/src/harness.ts b/packages/jest/src/harness.ts index 29a09cef..902a4442 100644 --- a/packages/jest/src/harness.ts +++ b/packages/jest/src/harness.ts @@ -178,6 +178,7 @@ const getHarnessInternal = async ( await prewarmMetroBundle({ projectRoot, entryPoint: config.entryPoint, + port: config.metroPort, platform: platform.platformId, dev: true, minify: false, diff --git a/packages/jest/src/setup.ts b/packages/jest/src/setup.ts index 78e69846..d2552179 100644 --- a/packages/jest/src/setup.ts +++ b/packages/jest/src/setup.ts @@ -1,6 +1,7 @@ import { getConfig, type Config as HarnessConfig, + ConfigSchema, } from '@react-native-harness/config'; import type { Config as JestConfig } from 'jest-runner'; import { getHarness } from './harness.js'; @@ -41,7 +42,7 @@ const getHarnessRunner = ( export const setup = async (globalConfig: JestConfig.GlobalConfig) => { preRunMessage.remove(process.stderr); - const harnessConfig = + let harnessConfig = global.HARNESS_CONFIG ?? (await getHarnessConfig(globalConfig)); if (global.HARNESS) { @@ -57,6 +58,14 @@ export const setup = async (globalConfig: JestConfig.GlobalConfig) => { }); const cliArgs = getAdditionalCliArgs(); + + if (cliArgs.metroPort != null) { + harnessConfig = ConfigSchema.parse({ + ...harnessConfig, + metroPort: cliArgs.metroPort, + }); + } + const selectedRunner = getHarnessRunner(harnessConfig, cliArgs); if (globalConfig.collectCoverage) { diff --git a/packages/platform-android/package.json b/packages/platform-android/package.json index 90b0ce77..3d7faf37 100644 --- a/packages/platform-android/package.json +++ b/packages/platform-android/package.json @@ -16,9 +16,9 @@ } }, "dependencies": { + "@react-native-harness/config": "workspace:*", "@react-native-harness/platforms": "workspace:*", "@react-native-harness/tools": "workspace:*", - "@react-native-harness/config": "workspace:*", "zod": "^3.25.67", "tslib": "^2.3.0" }, diff --git a/packages/platform-android/src/__tests__/shared-prefs.test.ts b/packages/platform-android/src/__tests__/shared-prefs.test.ts new file mode 100644 index 00000000..a36d762a --- /dev/null +++ b/packages/platform-android/src/__tests__/shared-prefs.test.ts @@ -0,0 +1,144 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as tools from '@react-native-harness/tools'; +import { + applyHarnessDebugHttpHost, + clearHarnessDebugHttpHost, +} from '../shared-prefs.js'; + +const bundleId = 'com.example.app'; +const adbId = 'emulator-5554'; + +const getWrittenContent = ( + calls: ReadonlyArray +): string => { + const writeCall = calls.find(([, , options]) => { + if (!options || typeof options !== 'object' || !('stdin' in options)) { + return false; + } + + return Boolean(options.stdin); + }); + const options = writeCall?.[2]; + + if (!options || typeof options !== 'object' || !('stdin' in options)) { + throw new Error('Expected write call options.'); + } + + const content = options.stdin; + + if ( + !content || + typeof content !== 'object' || + !('string' in content) || + typeof content.string !== 'string' + ) { + throw new Error('Expected write call with string stdin.'); + } + + return content.string; +}; + +const getWrittenContents = ( + calls: ReadonlyArray +): string[] => + calls + .filter(([, , options]) => { + if (!options || typeof options !== 'object' || !('stdin' in options)) { + return false; + } + + return Boolean(options.stdin); + }) + .map((call) => getWrittenContent([call])); + +describe('Android shared preferences Metro host override', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('handles empty self-closing map files', async () => { + const spawnSpy = vi + .spyOn(tools, 'spawn') + .mockResolvedValueOnce({ + stdout: '\n\n', + } as Awaited>) + .mockResolvedValueOnce({} as Awaited>); + + await applyHarnessDebugHttpHost(adbId, bundleId, 'localhost:9090'); + + expect(getWrittenContent(spawnSpy.mock.calls)).toContain(''); + expect(getWrittenContent(spawnSpy.mock.calls)).toContain(''); + expect(getWrittenContent(spawnSpy.mock.calls)).toContain( + 'localhost:9090' + ); + }); + + it('restores the previous debug host on cleanup', async () => { + const spawnSpy = vi + .spyOn(tools, 'spawn') + .mockResolvedValueOnce({ + stdout: [ + '', + '', + ' 10.0.2.2:8081', + '', + ].join('\n'), + } as Awaited>) + .mockResolvedValueOnce({} as Awaited>) + .mockResolvedValueOnce({ + stdout: [ + '', + '', + ' 10.0.2.2:8081', + ' ', + ' localhost:9090', + ' ', + '', + ].join('\n'), + } as Awaited>) + .mockResolvedValueOnce({} as Awaited>); + + await applyHarnessDebugHttpHost(adbId, bundleId, 'localhost:9090'); + await clearHarnessDebugHttpHost(adbId, bundleId); + + const writes = getWrittenContents(spawnSpy.mock.calls); + const firstWrite = writes[0]; + const secondWrite = writes[1]; + + expect(firstWrite).toEqual( + expect.stringContaining( + '10.0.2.2:8081' + ) + ); + expect(firstWrite).toEqual( + expect.stringContaining( + 'localhost:9090' + ) + ); + expect(firstWrite).toEqual( + expect.not.stringContaining( + '10.0.2.2:8081' + ) + ); + expect(secondWrite).toEqual( + expect.stringContaining( + '10.0.2.2:8081' + ) + ); + expect(secondWrite).toEqual( + expect.not.stringContaining( + '10.0.2.2:8081' + ) + ); + expect(secondWrite).toEqual( + expect.not.stringContaining( + '' + ) + ); + expect(secondWrite).toEqual( + expect.not.stringContaining( + 'localhost:9090' + ) + ); + }); +}); diff --git a/packages/platform-android/src/runner.ts b/packages/platform-android/src/runner.ts index cafc1e4e..8a000d42 100644 --- a/packages/platform-android/src/runner.ts +++ b/packages/platform-android/src/runner.ts @@ -4,19 +4,23 @@ import { CreateAppMonitorOptions, HarnessPlatformRunner, } from '@react-native-harness/platforms'; -import { Config } from '@react-native-harness/config'; +import type { Config as HarnessConfig } from '@react-native-harness/config'; import { AndroidPlatformConfigSchema, type AndroidPlatformConfig, } from './config.js'; import { getAdbId } from './adb-id.js'; import * as adb from './adb.js'; +import { + applyHarnessDebugHttpHost, + clearHarnessDebugHttpHost, +} from './shared-prefs.js'; import { getDeviceName } from './utils.js'; import { createAndroidAppMonitor } from './app-monitor.js'; const getAndroidRunner = async ( config: AndroidPlatformConfig, - harnessConfig: Config + harnessConfig: HarnessConfig ): Promise => { const parsedConfig = AndroidPlatformConfigSchema.parse(config); const adbId = await getAdbId(parsedConfig.device); @@ -34,11 +38,14 @@ const getAndroidRunner = async ( ); } + const metroPort = harnessConfig.metroPort; + await Promise.all([ - adb.reversePort(adbId, 8081), + adb.reversePort(adbId, metroPort), adb.reversePort(adbId, 8080), adb.reversePort(adbId, harnessConfig.webSocketPort), adb.setHideErrorDialogs(adbId, true), + applyHarnessDebugHttpHost(adbId, parsedConfig.bundleId, `localhost:${metroPort}`), ]); const appUid = await adb.getAppUid(adbId, parsedConfig.bundleId); @@ -67,6 +74,7 @@ const getAndroidRunner = async ( }, dispose: async () => { await adb.stopApp(adbId, parsedConfig.bundleId); + await clearHarnessDebugHttpHost(adbId, parsedConfig.bundleId); await adb.setHideErrorDialogs(adbId, false); }, isAppRunning: async () => { diff --git a/packages/platform-android/src/shared-prefs.ts b/packages/platform-android/src/shared-prefs.ts new file mode 100644 index 00000000..9d983fad --- /dev/null +++ b/packages/platform-android/src/shared-prefs.ts @@ -0,0 +1,205 @@ +import { spawn, SubprocessError } from '@react-native-harness/tools'; + +const DEBUG_HTTP_HOST_BLOCK_START = + ''; +const DEBUG_HTTP_HOST_BLOCK_END = + ''; +const DEBUG_HTTP_HOST_BACKUP_KEY = 'harness_debug_http_host_backup'; + +const getSharedPrefsPath = (bundleId: string) => + `shared_prefs/${bundleId}_preferences.xml`; + +const escapeRegExp = (value: string): string => + value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +const escapeXml = (value: string): string => + value + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); + +const unescapeXml = (value: string): string => + value + .replaceAll(''', "'") + .replaceAll('"', '"') + .replaceAll('>', '>') + .replaceAll('<', '<') + .replaceAll('&', '&'); + +const getStringPreferenceRegex = (key: string) => + new RegExp( + `([\\s\\S]*?)<\\/string>`, + 'g' + ); + +const getStringPreferenceValue = ( + content: string, + key: string +): string | null => { + const matches = [...content.matchAll(getStringPreferenceRegex(key))]; + const value = matches.at(-1)?.[1]; + + return value == null ? null : unescapeXml(value); +}; + +const renameStringPreference = ( + content: string, + fromKey: string, + toKey: string +): string => + content.replace( + new RegExp( + `([\\s\\S]*?<\\/string>)`, + 'g' + ), + `$1${toKey}$2` + ); + +const stripStringPreference = (content: string, key: string): string => + content.replace( + new RegExp( + `\\s*[\\s\\S]*?<\\/string>\\s*`, + 'g' + ), + '\n' + ); + +const normalizeEmptyMap = (content: string): string => + content.replace(//g, '\n'); + +const getHarnessDebugHttpHostBlock = (host: string) => + [ + DEBUG_HTTP_HOST_BLOCK_START, + `${escapeXml(host)}`, + DEBUG_HTTP_HOST_BLOCK_END, + ].join('\n'); + +const stripHarnessDebugHttpHostBlock = (content: string): string => + content.replace( + new RegExp( + `\\s*${escapeRegExp( + DEBUG_HTTP_HOST_BLOCK_START + )}\\s*\\n[\\s\\S]*?\\n\\s*${escapeRegExp(DEBUG_HTTP_HOST_BLOCK_END)}\\s*`, + 'g' + ), + '\n' + ); + +const normalizeSharedPrefsContent = (content: string | null): string => { + if (!content?.trim()) { + return ['', '', ''].join( + '\n' + ); + } + + return normalizeEmptyMap(stripHarnessDebugHttpHostBlock(content)).trim(); +}; + +const insertBeforeClosingMap = (content: string, block: string): string => { + if (!content.includes('')) { + throw new Error('Android shared preferences file is missing .'); + } + + return content.replace( + /<\/map>\s*$/, + ` ${block.replace(/\n/g, '\n ')}\n` + ); +}; + +const readSharedPrefsFile = async ( + adbId: string, + bundleId: string +): Promise => { + try { + const { stdout } = await spawn('adb', [ + '-s', + adbId, + 'shell', + `run-as ${bundleId} cat ${getSharedPrefsPath(bundleId)}`, + ]); + return stdout; + } catch (error) { + if (error instanceof SubprocessError && error.exitCode === 1) { + return null; + } + + throw error; + } +}; + +const writeSharedPrefsFile = async ( + adbId: string, + bundleId: string, + content: string +): Promise => { + await spawn( + 'adb', + [ + '-s', + adbId, + 'shell', + `run-as ${bundleId} sh -c 'mkdir -p shared_prefs && cat > ${getSharedPrefsPath( + bundleId + )}'`, + ], + { stdin: { string: `${content.trim()}\n` } } + ); +}; + +export const applyHarnessDebugHttpHost = async ( + adbId: string, + bundleId: string, + host: string +): Promise => { + const existingContent = await readSharedPrefsFile(adbId, bundleId); + const normalizedContent = normalizeSharedPrefsContent(existingContent); + const existingHost = getStringPreferenceValue( + normalizedContent, + 'debug_http_host' + ); + const contentWithBackup = + existingHost == null + ? normalizedContent + : renameStringPreference( + stripStringPreference(normalizedContent, DEBUG_HTTP_HOST_BACKUP_KEY), + 'debug_http_host', + DEBUG_HTTP_HOST_BACKUP_KEY + ); + const nextContent = insertBeforeClosingMap( + contentWithBackup, + getHarnessDebugHttpHostBlock(host) + ); + await writeSharedPrefsFile(adbId, bundleId, nextContent); +}; + +export const clearHarnessDebugHttpHost = async ( + adbId: string, + bundleId: string +): Promise => { + const existingContent = await readSharedPrefsFile(adbId, bundleId); + + if (!existingContent) { + return; + } + + const nextContentWithoutHarnessBlock = + stripHarnessDebugHttpHostBlock(existingContent).trim(); + + if (nextContentWithoutHarnessBlock === existingContent.trim()) { + return; + } + + const restoredContent = renameStringPreference( + nextContentWithoutHarnessBlock, + DEBUG_HTTP_HOST_BACKUP_KEY, + 'debug_http_host' + ); + + await writeSharedPrefsFile( + adbId, + bundleId, + normalizeEmptyMap(restoredContent).trim() + ); +}; diff --git a/packages/platform-android/tsconfig.json b/packages/platform-android/tsconfig.json index 879e5151..65f896de 100644 --- a/packages/platform-android/tsconfig.json +++ b/packages/platform-android/tsconfig.json @@ -4,10 +4,10 @@ "include": [], "references": [ { - "path": "../config" + "path": "../tools" }, { - "path": "../tools" + "path": "../config" }, { "path": "../platforms" diff --git a/packages/platform-android/tsconfig.lib.json b/packages/platform-android/tsconfig.lib.json index ec9fadc9..e3507a11 100644 --- a/packages/platform-android/tsconfig.lib.json +++ b/packages/platform-android/tsconfig.lib.json @@ -12,10 +12,10 @@ "include": ["src/**/*.ts"], "references": [ { - "path": "../config/tsconfig.lib.json" + "path": "../tools/tsconfig.lib.json" }, { - "path": "../tools/tsconfig.lib.json" + "path": "../config/tsconfig.lib.json" }, { "path": "../platforms/tsconfig.lib.json" diff --git a/packages/platform-ios/package.json b/packages/platform-ios/package.json index 94b868a3..058b2767 100644 --- a/packages/platform-ios/package.json +++ b/packages/platform-ios/package.json @@ -16,9 +16,9 @@ } }, "dependencies": { + "@react-native-harness/config": "workspace:*", "@react-native-harness/platforms": "workspace:*", "@react-native-harness/tools": "workspace:*", - "@react-native-harness/config": "workspace:*", "zod": "^3.25.67", "tslib": "^2.3.0" }, diff --git a/packages/platform-ios/src/__tests__/instance.test.ts b/packages/platform-ios/src/__tests__/instance.test.ts index cba91aa9..975863df 100644 --- a/packages/platform-ios/src/__tests__/instance.test.ts +++ b/packages/platform-ios/src/__tests__/instance.test.ts @@ -1,4 +1,8 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { + DEFAULT_METRO_PORT, + type Config as HarnessConfig, +} from '@react-native-harness/config'; import { getApplePhysicalDevicePlatformInstance, getAppleSimulatorPlatformInstance, @@ -7,6 +11,10 @@ import * as simctl from '../xcrun/simctl.js'; import * as devicectl from '../xcrun/devicectl.js'; import * as libimobiledevice from '../libimobiledevice.js'; +const harnessConfig = { + metroPort: DEFAULT_METRO_PORT, +} as HarnessConfig; + describe('iOS platform instance dependency validation', () => { beforeEach(() => { vi.restoreAllMocks(); @@ -19,6 +27,9 @@ describe('iOS platform instance dependency validation', () => { vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid'); vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true); vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted'); + vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue( + undefined + ); const config = { name: 'ios', @@ -27,7 +38,7 @@ describe('iOS platform instance dependency validation', () => { }; await expect( - getAppleSimulatorPlatformInstance(config) + getAppleSimulatorPlatformInstance(config, harnessConfig) ).resolves.toBeDefined(); expect(assertInstalled).not.toHaveBeenCalled(); }); @@ -44,7 +55,7 @@ describe('iOS platform instance dependency validation', () => { }; await expect( - getApplePhysicalDevicePlatformInstance(config) + getApplePhysicalDevicePlatformInstance(config, harnessConfig) ).rejects.toThrow('missing'); expect(assertInstalled).toHaveBeenCalled(); }); @@ -58,6 +69,9 @@ describe('iOS platform instance dependency validation', () => { ); vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true); vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted'); + vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue( + undefined + ); const config = { name: 'ios', @@ -66,7 +80,7 @@ describe('iOS platform instance dependency validation', () => { }; await expect( - getAppleSimulatorPlatformInstance(config) + getAppleSimulatorPlatformInstance(config, harnessConfig) ).resolves.toBeDefined(); expect(getSimulatorId).toHaveBeenCalled(); }); @@ -84,7 +98,7 @@ describe('iOS platform instance dependency validation', () => { }; await expect( - getApplePhysicalDevicePlatformInstance(config) + getApplePhysicalDevicePlatformInstance(config, harnessConfig) ).rejects.toThrow('missing'); expect(getDeviceId).not.toHaveBeenCalled(); }); diff --git a/packages/platform-ios/src/instance.ts b/packages/platform-ios/src/instance.ts index 81a44140..13864667 100644 --- a/packages/platform-ios/src/instance.ts +++ b/packages/platform-ios/src/instance.ts @@ -4,6 +4,10 @@ import { DeviceNotFoundError, HarnessPlatformRunner, } from '@react-native-harness/platforms'; +import { + DEFAULT_METRO_PORT, + type Config as HarnessConfig, +} from '@react-native-harness/config'; import { ApplePlatformConfig, assertAppleDevicePhysical, @@ -19,7 +23,8 @@ import { import { assertLibimobiledeviceInstalled } from './libimobiledevice.js'; export const getAppleSimulatorPlatformInstance = async ( - config: ApplePlatformConfig + config: ApplePlatformConfig, + harnessConfig: HarnessConfig ): Promise => { assertAppleDeviceSimulator(config.device); @@ -47,6 +52,12 @@ export const getAppleSimulatorPlatformInstance = async ( throw new Error('Simulator is not booted'); } + await simctl.applyHarnessJsLocationOverride( + udid, + config.bundleId, + `localhost:${harnessConfig.metroPort}` + ); + return { startApp: async (options) => { await simctl.startApp( @@ -70,6 +81,7 @@ export const getAppleSimulatorPlatformInstance = async ( }, dispose: async () => { await simctl.stopApp(udid, config.bundleId); + await simctl.clearHarnessJsLocationOverride(udid, config.bundleId); }, isAppRunning: async () => { return await simctl.isAppRunning(udid, config.bundleId); @@ -84,11 +96,18 @@ export const getAppleSimulatorPlatformInstance = async ( }; export const getApplePhysicalDevicePlatformInstance = async ( - config: ApplePlatformConfig + config: ApplePlatformConfig, + harnessConfig: HarnessConfig ): Promise => { assertAppleDevicePhysical(config.device); await assertLibimobiledeviceInstalled(); + if (harnessConfig.metroPort !== DEFAULT_METRO_PORT) { + throw new Error( + `Custom Metro port ${harnessConfig.metroPort} is not supported on physical iOS devices. Physical devices always connect to port ${DEFAULT_METRO_PORT}.` + ); + } + const device = await devicectl.getDevice(config.device.name); if (!device) { diff --git a/packages/platform-ios/src/runner.ts b/packages/platform-ios/src/runner.ts index d413d4af..350de6e4 100644 --- a/packages/platform-ios/src/runner.ts +++ b/packages/platform-ios/src/runner.ts @@ -1,5 +1,5 @@ import { HarnessPlatformRunner } from '@react-native-harness/platforms'; -import { Config } from '@react-native-harness/config'; +import type { Config as HarnessConfig } from '@react-native-harness/config'; import { ApplePlatformConfigSchema, type ApplePlatformConfig, @@ -12,15 +12,15 @@ import { const getAppleRunner = async ( config: ApplePlatformConfig, - _harnessConfig: Config + harnessConfig: HarnessConfig ): Promise => { const parsedConfig = ApplePlatformConfigSchema.parse(config); if (isAppleDeviceSimulator(parsedConfig.device)) { - return getAppleSimulatorPlatformInstance(parsedConfig); + return getAppleSimulatorPlatformInstance(parsedConfig, harnessConfig); } - return getApplePhysicalDevicePlatformInstance(parsedConfig); + return getApplePhysicalDevicePlatformInstance(parsedConfig, harnessConfig); }; export default getAppleRunner; diff --git a/packages/platform-ios/src/xcrun/simctl.ts b/packages/platform-ios/src/xcrun/simctl.ts index 4cfa5cca..fd97fc0a 100644 --- a/packages/platform-ios/src/xcrun/simctl.ts +++ b/packages/platform-ios/src/xcrun/simctl.ts @@ -2,7 +2,12 @@ import { type AppleAppLaunchOptions, type CrashArtifactWriter, } from '@react-native-harness/platforms'; -import { logger, spawn, spawnAndForget } from '@react-native-harness/tools'; +import { + logger, + spawn, + spawnAndForget, + SubprocessError, +} from '@react-native-harness/tools'; import fs from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; @@ -280,6 +285,124 @@ export const isAppRunning = async ( } }; +const HARNESS_JS_LOCATION_BACKUP_KEY = + 'react_native_harness_RCT_jsLocation_backup'; +const HARNESS_MISSING_VALUE = '__RN_HARNESS_MISSING__'; + +const getDefaultsValue = async ( + udid: string, + bundleId: string, + key: string +): Promise => { + try { + const { stdout } = await spawn('xcrun', [ + 'simctl', + 'spawn', + udid, + 'defaults', + 'read', + bundleId, + key, + ]); + return stdout.trim() || null; + } catch (error) { + if (error instanceof SubprocessError && error.exitCode === 1) { + return null; + } + + throw error; + } +}; + +const writeDefaultsValue = async ( + udid: string, + bundleId: string, + key: string, + value: string +): Promise => { + await spawn('xcrun', [ + 'simctl', + 'spawn', + udid, + 'defaults', + 'write', + bundleId, + key, + value, + ]); +}; + +const deleteDefaultsValue = async ( + udid: string, + bundleId: string, + key: string +): Promise => { + try { + await spawn('xcrun', [ + 'simctl', + 'spawn', + udid, + 'defaults', + 'delete', + bundleId, + key, + ]); + } catch (error) { + if (error instanceof SubprocessError && error.exitCode === 1) { + return; + } + + throw error; + } +}; + +export const applyHarnessJsLocationOverride = async ( + udid: string, + bundleId: string, + host: string +): Promise => { + const backupValue = await getDefaultsValue( + udid, + bundleId, + HARNESS_JS_LOCATION_BACKUP_KEY + ); + + if (backupValue === null) { + const existingValue = await getDefaultsValue(udid, bundleId, 'RCT_jsLocation'); + await writeDefaultsValue( + udid, + bundleId, + HARNESS_JS_LOCATION_BACKUP_KEY, + existingValue ?? HARNESS_MISSING_VALUE + ); + } + + await writeDefaultsValue(udid, bundleId, 'RCT_jsLocation', host); +}; + +export const clearHarnessJsLocationOverride = async ( + udid: string, + bundleId: string +): Promise => { + const backupValue = await getDefaultsValue( + udid, + bundleId, + HARNESS_JS_LOCATION_BACKUP_KEY + ); + + if (backupValue === null) { + return; + } + + if (backupValue === HARNESS_MISSING_VALUE) { + await deleteDefaultsValue(udid, bundleId, 'RCT_jsLocation'); + } else { + await writeDefaultsValue(udid, bundleId, 'RCT_jsLocation', backupValue); + } + + await deleteDefaultsValue(udid, bundleId, HARNESS_JS_LOCATION_BACKUP_KEY); +}; + export const screenshot = async ( udid: string, destination: string diff --git a/packages/platform-ios/tsconfig.json b/packages/platform-ios/tsconfig.json index 879e5151..65f896de 100644 --- a/packages/platform-ios/tsconfig.json +++ b/packages/platform-ios/tsconfig.json @@ -4,10 +4,10 @@ "include": [], "references": [ { - "path": "../config" + "path": "../tools" }, { - "path": "../tools" + "path": "../config" }, { "path": "../platforms" diff --git a/packages/platform-ios/tsconfig.lib.json b/packages/platform-ios/tsconfig.lib.json index d6cee886..d9b34e70 100644 --- a/packages/platform-ios/tsconfig.lib.json +++ b/packages/platform-ios/tsconfig.lib.json @@ -13,10 +13,10 @@ "include": ["src/**/*.ts"], "references": [ { - "path": "../config/tsconfig.lib.json" + "path": "../tools/tsconfig.lib.json" }, { - "path": "../tools/tsconfig.lib.json" + "path": "../config/tsconfig.lib.json" }, { "path": "../platforms/tsconfig.lib.json" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e19e442b..a3aa8695 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,13 +31,13 @@ importers: version: 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/react-native': specifier: 22.0.4 - version: 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(metro-config@0.83.3)(metro-resolver@0.83.3)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) + version: 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(metro-config@0.83.3)(metro-resolver@0.83.3)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/rollup': specifier: 22.0.4 version: 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@types/babel__core@7.20.5)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3) '@nx/vite': specifier: 22.0.4 - version: 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0)) + version: 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4) '@nx/web': specifier: 22.0.4 version: 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) @@ -64,7 +64,7 @@ importers: version: 4.6.0(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0)) '@vitest/coverage-v8': specifier: ^3.0.5 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0)) + version: 3.2.4(vitest@3.2.4) '@vitest/ui': specifier: ^3.0.0 version: 3.2.4(vitest@3.2.4) @@ -10093,13 +10093,13 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nx/detox@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': + '@nx/detox@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': dependencies: '@nx/devkit': 22.0.4(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/eslint': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/jest': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@types/node@20.19.25)(babel-plugin-macros@3.1.0)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3) '@nx/js': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) - '@nx/react': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) + '@nx/react': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) tslib: 2.8.1 transitivePeerDependencies: - '@babel/core' @@ -10324,12 +10324,12 @@ snapshots: '@nx/nx-win32-x64-msvc@22.0.4': optional: true - '@nx/react-native@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(metro-config@0.83.3)(metro-resolver@0.83.3)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': + '@nx/react-native@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(metro-config@0.83.3)(metro-resolver@0.83.3)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': dependencies: '@nx/devkit': 22.0.4(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/eslint': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/js': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) - '@nx/react': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) + '@nx/react': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) ajv: 8.17.1 enhanced-resolve: 5.18.3 ignore: 5.3.2 @@ -10340,7 +10340,7 @@ snapshots: tsconfig-paths: 4.2.0 tslib: 2.8.1 optionalDependencies: - '@nx/detox': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) + '@nx/detox': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@types/node@20.19.25)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/rollup': 22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@types/babel__core@7.20.5)(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3) transitivePeerDependencies: - '@babel/core' @@ -10375,7 +10375,7 @@ snapshots: - webpack - webpack-cli - '@nx/react@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': + '@nx/react@22.0.4(@babel/core@7.27.4)(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/helpers@0.5.17)(@types/babel__core@7.20.5)(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)(webpack@5.102.1(@swc/core@1.5.29(@swc/helpers@0.5.17)))': dependencies: '@nx/devkit': 22.0.4(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/eslint': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.29.0(jiti@2.4.2))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) @@ -10393,7 +10393,7 @@ snapshots: semver: 7.7.2 tslib: 2.8.1 optionalDependencies: - '@nx/vite': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0)) + '@nx/vite': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4) transitivePeerDependencies: - '@babel/core' - '@babel/traverse' @@ -10454,7 +10454,7 @@ snapshots: - typescript - verdaccio - '@nx/vite@22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))': + '@nx/vite@22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17)))(typescript@5.9.3)(vite@7.2.2(@types/node@20.19.25)(jiti@2.4.2)(terser@5.42.0)(yaml@2.8.0))(vitest@3.2.4)': dependencies: '@nx/devkit': 22.0.4(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) '@nx/js': 22.0.4(@babel/traverse@7.27.4)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))(nx@22.0.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.23)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.17))) @@ -11904,7 +11904,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@22.1.0)(terser@5.42.0)(yaml@2.8.0))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2