diff --git a/private/react-native-fantom/config/jest.config.js b/private/react-native-fantom/config/jest.config.js index a69fc4c3689..adf10408e5c 100644 --- a/private/react-native-fantom/config/jest.config.js +++ b/private/react-native-fantom/config/jest.config.js @@ -35,4 +35,6 @@ module.exports = { watchPathIgnorePatterns: ['/private/react-native-fantom/build/'], globalSetup: '/private/react-native-fantom/runner/global-setup/setup.js', + globalTeardown: + '/private/react-native-fantom/runner/global-setup/teardown.js', }; diff --git a/private/react-native-fantom/runner/createBundle.js b/private/react-native-fantom/runner/createBundle.js new file mode 100644 index 00000000000..89d9d37637a --- /dev/null +++ b/private/react-native-fantom/runner/createBundle.js @@ -0,0 +1,117 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import type {RunBuildOptions} from 'metro'; + +import fs from 'fs'; +import path from 'path'; + +type CreateBundleOptions = { + ...RunBuildOptions, + out: $NonMaybeType, + testPath: string, +}; + +const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..'); + +export default async function createBundle({ + testPath, + entry, + out, + platform, + minify, + dev, + sourceMap, + sourceMapUrl, +}: CreateBundleOptions): Promise { + if (process.env.__FANTOM_METRO_PORT__ == null) { + throw new Error( + 'Could not find Metro server port (process.env.__FANTOM_METRO_PORT__ not set by Fantom)', + ); + } + + const port = Number(process.env.__FANTOM_METRO_PORT__); + if (!Number.isFinite(port) || port <= 0 || port > 65535) { + throw new Error(`Invalid port for Metro server: ${port}`); + } + + const requestPath = path + .relative(PROJECT_ROOT, entry) + .replace(/\.js$/, '.bundle'); + + const bundleURL = new URL(`http://localhost:${port}/${requestPath}`); + + if (platform != null) { + bundleURL.searchParams.append('platform', platform); + } + + if (minify != null) { + bundleURL.searchParams.append('minify', minify ? 'true' : 'false'); + } + + if (dev != null) { + bundleURL.searchParams.append('dev', dev ? 'true' : 'false'); + } + + if (sourceMap != null) { + bundleURL.searchParams.append('sourceMap', sourceMap ? 'true' : 'false'); + } + + if (sourceMapUrl != null) { + bundleURL.searchParams.append('sourceMapUrl', sourceMapUrl); + } + + let bundleResult; + let bundleError; + + // Retry in case Metro hasn't seen the changes in the filesystem yet. + // TODO(T231910841): Remove this when Metro fixes consistency issues when resolving HTTP requests. + let attemps = 0; + do { + if (attemps > 0) { + await sleep(500); + } + + try { + bundleResult = await fetch(bundleURL); + } catch (e) { + bundleError = e; + } + + attemps++; + } while (attemps < 3 && (bundleError || bundleResult?.status === 404)); + + if (bundleError || bundleResult?.ok !== true) { + throw new Error( + `Failed to request bundle from Metro: ${bundleError?.message ?? (await bundleResult?.text()) ?? ''}`, + ); + } + await fs.promises.writeFile(out, await bundleResult.text(), 'utf8'); + + try { + const sourceMapURL = new URL(bundleURL.toString()); + sourceMapURL.pathname = sourceMapURL.pathname.replace(/\.bundle$/, '.map'); + const sourceMapResult = await fetch(sourceMapURL); + if (!sourceMapResult.ok) { + throw new Error(); + } + await fs.promises.writeFile( + out.replace(/\.js$/, '.map'), + await sourceMapResult.text(), + 'utf8', + ); + } catch (e) { + console.error(`Could not fetch source map from Metro for test ${testPath}`); + } +} + +async function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/private/react-native-fantom/runner/global-setup/build.js b/private/react-native-fantom/runner/global-setup/build.js index 54ed59f302e..99f03f77719 100644 --- a/private/react-native-fantom/runner/global-setup/build.js +++ b/private/react-native-fantom/runner/global-setup/build.js @@ -8,6 +8,7 @@ * @format */ +import createBundle from '../createBundle'; import {isCI} from '../EnvironmentOptions'; import {build as buildHermesCompiler} from '../executables/hermesc'; import {build as buildFantomTester} from '../executables/tester'; @@ -15,7 +16,6 @@ import {getNativeBuildOutputPath} from '../executables/utils'; import {HermesVariant} from '../utils'; // $FlowExpectedError[untyped-import] import fs from 'fs'; -import Metro from 'metro'; import os from 'os'; import path from 'path'; @@ -58,10 +58,6 @@ export default async function build(): Promise { } async function warmUpMetro(isOptimizedMode: boolean): Promise { - const metroConfig = await Metro.loadConfig({ - config: path.resolve(__dirname, '..', '..', 'config', 'metro.config.js'), - }); - const entrypointPath = path.resolve( __dirname, '..', @@ -75,7 +71,8 @@ async function warmUpMetro(isOptimizedMode: boolean): Promise { `fantom-warmup-bundle-${Date.now()}.js`, ); - await Metro.runBuild(metroConfig, { + await createBundle({ + testPath: '(warmup bundle - no test path)', entry: entrypointPath, out: bundlePath, platform: 'android', diff --git a/private/react-native-fantom/runner/global-setup/globalSetup.js b/private/react-native-fantom/runner/global-setup/globalSetup.js index bd6b84bacc4..f87eec9848a 100644 --- a/private/react-native-fantom/runner/global-setup/globalSetup.js +++ b/private/react-native-fantom/runner/global-setup/globalSetup.js @@ -10,6 +10,8 @@ import {isOSS, validateEnvironmentVariables} from '../EnvironmentOptions'; import build from './build'; +import Metro from 'metro'; +import path from 'path'; export default async function globalSetup( globalConfig: {...}, @@ -19,7 +21,38 @@ export default async function globalSetup( validateEnvironmentVariables(); + await startMetroServer(); + if (!isOSS) { await build(); } } + +async function startMetroServer() { + const metroConfig = await Metro.loadConfig({ + config: path.resolve(__dirname, '..', '..', 'config', 'metro.config.js'), + }); + + // We need to reuse the same port across runs because can only set environment + // variables for workers in the first one. + // $FlowExpectedError[cannot-write] + metroConfig.server.port = + process.env.__FANTOM_METRO_PORT__ != null + ? Number(process.env.__FANTOM_METRO_PORT__) + : // Any available port + 0; + + const server = await Metro.runServer(metroConfig, { + waitForBundler: true, + watch: true, + }); + + if (process.env.__FANTOM_METRO_PORT__ == null) { + process.env.__FANTOM_METRO_PORT__ = String( + server.httpServer.address().port, + ); + } + + // $FlowExpectedError[prop-missing] + globalThis.__METRO_SERVER__ = server; +} diff --git a/private/react-native-fantom/runner/global-setup/globalTeardown.js b/private/react-native-fantom/runner/global-setup/globalTeardown.js new file mode 100644 index 00000000000..477bd320238 --- /dev/null +++ b/private/react-native-fantom/runner/global-setup/globalTeardown.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import type {RunServerResult} from 'metro'; + +type MetroServer = $NonMaybeType; + +declare var __METRO_SERVER__: ?RunServerResult; + +function getMetroServer(): ?MetroServer { + return typeof __METRO_SERVER__ !== 'undefined' && __METRO_SERVER__ != null + ? __METRO_SERVER__.httpServer + : null; +} + +export default async function globalTeardown( + globalConfig: {...}, + projectConfig: {...}, +): Promise { + const metroServer = getMetroServer(); + if (metroServer) { + await stopMetroServer(metroServer); + } +} + +async function stopMetroServer(metroServer: MetroServer): Promise { + return new Promise((resolve, reject) => { + metroServer.close(error => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); +} diff --git a/private/react-native-fantom/runner/global-setup/teardown.js b/private/react-native-fantom/runner/global-setup/teardown.js new file mode 100644 index 00000000000..6e8913f17cb --- /dev/null +++ b/private/react-native-fantom/runner/global-setup/teardown.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +require('../../../../scripts/shared/babelRegister').registerForMonorepo(); + +module.exports = require('./globalTeardown'); diff --git a/private/react-native-fantom/runner/runner.js b/private/react-native-fantom/runner/runner.js index 36fb632bbc4..18d3b2cacd0 100644 --- a/private/react-native-fantom/runner/runner.js +++ b/private/react-native-fantom/runner/runner.js @@ -20,6 +20,7 @@ import type { HermesVariant, } from './utils'; +import createBundle from './createBundle'; import entrypointTemplate from './entrypoint-template'; import * as EnvironmentOptions from './EnvironmentOptions'; import {run as runHermesCompiler} from './executables/hermesc'; @@ -41,7 +42,6 @@ import fs from 'fs'; // $FlowExpectedError[untyped-import] import {formatResultsErrors} from 'jest-message-util'; import {SnapshotState, buildSnapshotResolver} from 'jest-snapshot'; -import Metro from 'metro'; import nullthrows from 'nullthrows'; import path from 'path'; import readline from 'readline'; @@ -210,10 +210,6 @@ module.exports = async function runTest( const testContents = fs.readFileSync(testPath, 'utf8'); const testConfigs = getFantomTestConfigs(testPath, testContents); - const metroConfig = await Metro.loadConfig({ - config: path.resolve(__dirname, '..', 'config', 'metro.config.js'), - }); - const setupModulePath = path.resolve(__dirname, '../runtime/setup.js'); const featureFlagsModulePath = path.resolve( __dirname, @@ -305,7 +301,8 @@ module.exports = async function runTest( path.basename(testJSBundlePath, '.js') + '.map', ); - await Metro.runBuild(metroConfig, { + await createBundle({ + testPath, entry: entrypointPath, out: testJSBundlePath, platform: 'android', diff --git a/scripts/fantom.sh b/scripts/fantom.sh index e58019f59df..9ae47c1f23b 100755 --- a/scripts/fantom.sh +++ b/scripts/fantom.sh @@ -6,9 +6,14 @@ set -e + if [[ -f "BUCK" && -z "$FANTOM_FORCE_OSS_BUILD" ]]; then - JS_DIR='..' yarn jest --config private/react-native-fantom/config/jest.config.js "$@" + export JS_DIR='..' else yarn workspace @react-native/fantom build - FANTOM_FORCE_OSS_BUILD=1 yarn jest --config private/react-native-fantom/config/jest.config.js "$@" + export FANTOM_FORCE_OSS_BUILD=1 fi + +export NODE_OPTIONS='--max-old-space-size=8192' + +yarn jest --config private/react-native-fantom/config/jest.config.js "$@"