From 63942b4329080ff2f1079b3c027f928f3d68ad5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Tue, 12 Aug 2025 02:16:26 -0700 Subject: [PATCH 1/6] Refactor logic to find available port for Metro in Fantom Differential Revision: D79804005 --- .../runner/global-setup/globalSetup.js | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/private/react-native-fantom/runner/global-setup/globalSetup.js b/private/react-native-fantom/runner/global-setup/globalSetup.js index f87eec9848a0..af8871e74d97 100644 --- a/private/react-native-fantom/runner/global-setup/globalSetup.js +++ b/private/react-native-fantom/runner/global-setup/globalSetup.js @@ -11,6 +11,7 @@ import {isOSS, validateEnvironmentVariables} from '../EnvironmentOptions'; import build from './build'; import Metro from 'metro'; +import {Server} from 'net'; import path from 'path'; export default async function globalSetup( @@ -33,26 +34,38 @@ async function startMetroServer() { config: path.resolve(__dirname, '..', '..', 'config', 'metro.config.js'), }); + if (process.env.__FANTOM_METRO_PORT__ == null) { + const availablePort = await findAvailablePort(); + process.env.__FANTOM_METRO_PORT__ = String(availablePort); + } + // 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; + metroConfig.server.port = Number(process.env.__FANTOM_METRO_PORT__); 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; } + +async function findAvailablePort(): Promise { + return new Promise((resolve, reject) => { + const server = new Server(); + server.listen(0, 'localhost', undefined, () => { + const port = server.address().port; + server.close(error => { + if (error != null) { + reject(error); + } else { + resolve(port); + } + }); + }); + server.on('error', reject); + }); +} From c2de971c2f14ec22c3dfc4da212e61b4828c8739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Tue, 12 Aug 2025 02:16:26 -0700 Subject: [PATCH 2/6] Rename global variable with Metro server for Fantom Differential Revision: D79804007 --- .../react-native-fantom/runner/global-setup/globalSetup.js | 2 +- .../runner/global-setup/globalTeardown.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/private/react-native-fantom/runner/global-setup/globalSetup.js b/private/react-native-fantom/runner/global-setup/globalSetup.js index af8871e74d97..94bf690a0335 100644 --- a/private/react-native-fantom/runner/global-setup/globalSetup.js +++ b/private/react-native-fantom/runner/global-setup/globalSetup.js @@ -50,7 +50,7 @@ async function startMetroServer() { }); // $FlowExpectedError[prop-missing] - globalThis.__METRO_SERVER__ = server; + globalThis.__FANTOM_METRO_SERVER__ = server; } async function findAvailablePort(): Promise { diff --git a/private/react-native-fantom/runner/global-setup/globalTeardown.js b/private/react-native-fantom/runner/global-setup/globalTeardown.js index 477bd3202389..1f6c4bd63e50 100644 --- a/private/react-native-fantom/runner/global-setup/globalTeardown.js +++ b/private/react-native-fantom/runner/global-setup/globalTeardown.js @@ -12,11 +12,12 @@ import type {RunServerResult} from 'metro'; type MetroServer = $NonMaybeType; -declare var __METRO_SERVER__: ?RunServerResult; +declare var __FANTOM_METRO_SERVER__: ?RunServerResult; function getMetroServer(): ?MetroServer { - return typeof __METRO_SERVER__ !== 'undefined' && __METRO_SERVER__ != null - ? __METRO_SERVER__.httpServer + return typeof __FANTOM_METRO_SERVER__ !== 'undefined' && + __FANTOM_METRO_SERVER__ != null + ? __FANTOM_METRO_SERVER__.httpServer : null; } From 1cbfc6f26f5da80fe7ce3e53d5a0bec056584cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Tue, 12 Aug 2025 02:16:26 -0700 Subject: [PATCH 3/6] Connect debugger before loading bundle Differential Revision: D79804004 --- .../ReactCxxPlatform/react/runtime/ReactHost.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp index bcd8d15f8aed..b53b2b83bcfd 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp @@ -240,6 +240,10 @@ void ReactHost::createReactInstance() { auto jsInvoker = std::make_shared( reactInstance_->getRuntimeScheduler()); + if (inspector_ != nullptr) { + inspector_->connectDebugger(devServerHelper_->getInspectorUrl()); + } + auto liveReloadCallback = [this]() { reloadReactInstance(); }; reactInstance_->initializeRuntime( { @@ -451,9 +455,6 @@ bool ReactHost::loadScriptFromDevServer() { auto script = std::make_unique(response); reactInstance_->loadScript( std::move(script), devServerHelper_->getBundleUrl()); - if (inspector_ != nullptr) { - inspector_->connectDebugger(devServerHelper_->getInspectorUrl()); - } devServerHelper_->setupHMRClient(); return true; } catch (...) { From 57bc303c939f2c8e4d212baa9430c1da199bf4f6 Mon Sep 17 00:00:00 2001 From: Ruben Norte Date: Tue, 12 Aug 2025 03:38:16 -0700 Subject: [PATCH 4/6] Split ReactInstanceConfig.enableDebugging into enableInspector and enablePackagerBundles Differential Revision: D79804006 --- .../react/runtime/ReactHost.cpp | 96 ++++++++++--------- .../react/runtime/ReactInstanceConfig.h | 10 +- 2 files changed, 55 insertions(+), 51 deletions(-) diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp index b53b2b83bcfd..b35df76cf652 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp @@ -137,51 +137,52 @@ void ReactHost::createReactInstance() { WebSocketClientFactoryKey); // Create devServerHelper - if (reactInstanceConfig_.enableDebugging) { - if (!devServerHelper_) { - devServerHelper_ = std::make_shared( - reactInstanceConfig_.appId, - reactInstanceConfig_.deviceName, - reactInstanceConfig_.devServerHost, - reactInstanceConfig_.devServerPort, - httpClientFactory, - [this]( - const std::string& moduleName, - const std::string& methodName, - folly::dynamic&& args) { - reactInstance_->callFunctionOnModule( - moduleName, methodName, std::move(args)); - }); - } - if (!inspector_) { - inspector_ = std::make_shared( - reactInstanceConfig_.appId, - reactInstanceConfig_.deviceName, - webSocketClientFactory, - httpClientFactory); - inspector_->ensureHostTarget( - [this]() { reloadReactInstance(); }, - [weakDevUIDelegate = std::weak_ptr( - reactInstanceData_->devUIDelegate)]( - bool showDebuggerOverlay, - std::function&& resumeDebuggerFn) { - if (auto debugUIDelegate = weakDevUIDelegate.lock()) { - if (showDebuggerOverlay) { - debugUIDelegate->showDebuggerOverlay( - std::move(resumeDebuggerFn)); - } else { - debugUIDelegate->hideDebuggerOverlay(); - } + if (!devServerHelper_ && + (reactInstanceConfig_.enableInspector || + reactInstanceConfig_.enableDevMode)) { + devServerHelper_ = std::make_shared( + reactInstanceConfig_.appId, + reactInstanceConfig_.deviceName, + reactInstanceConfig_.devServerHost, + reactInstanceConfig_.devServerPort, + httpClientFactory, + [this]( + const std::string& moduleName, + const std::string& methodName, + folly::dynamic&& args) { + reactInstance_->callFunctionOnModule( + moduleName, methodName, std::move(args)); + }); + } + + if (!inspector_ && reactInstanceConfig_.enableInspector) { + inspector_ = std::make_shared( + reactInstanceConfig_.appId, + reactInstanceConfig_.deviceName, + webSocketClientFactory, + httpClientFactory); + inspector_->ensureHostTarget( + [this]() { reloadReactInstance(); }, + [weakDevUIDelegate = + std::weak_ptr(reactInstanceData_->devUIDelegate)]( + bool showDebuggerOverlay, + std::function&& resumeDebuggerFn) { + if (auto debugUIDelegate = weakDevUIDelegate.lock()) { + if (showDebuggerOverlay) { + debugUIDelegate->showDebuggerOverlay(std::move(resumeDebuggerFn)); + } else { + debugUIDelegate->hideDebuggerOverlay(); } - }); - } - if (!packagerConnection_) { - packagerConnection_ = std::make_unique( - webSocketClientFactory, - devServerHelper_->getPackagerConnectionUrl(), - [this]() { reloadReactInstance(); }, - []() {}); - } + } + }); + } + + if (!packagerConnection_ && reactInstanceConfig_.enableDevMode) { + packagerConnection_ = std::make_unique( + webSocketClientFactory, + devServerHelper_->getPackagerConnectionUrl(), + [this]() { reloadReactInstance(); }, + []() {}); } // Create the React Instance @@ -194,7 +195,7 @@ void ReactHost::createReactInstance() { reactInstanceData_->messageQueueThread, /* allocInOldGenBeforeTTI */ false); - if (reactInstanceConfig_.enableDebugging) { + if (reactInstanceConfig_.enableInspector) { react_native_assert( inspector_ != nullptr && "Inspector is not initialized"); } @@ -262,7 +263,8 @@ void ReactHost::createReactInstance() { reactInstanceData_->turboModuleManagerDelegates, jsInvoker = std::move(jsInvoker), logBoxSurfaceDelegate = reactInstanceData_->logBoxSurfaceDelegate, - devServerHelper = devServerHelper_, + devServerHelper = + reactInstanceConfig_.enableDevMode ? devServerHelper_ : nullptr, animatedNodesManagerProvider = reactInstanceData_->animatedNodesManagerProvider, onJsError = reactInstanceData_->onJsError, @@ -421,7 +423,7 @@ bool ReactHost::loadScript( const std::string& bundlePath, const std::string& sourcePath) noexcept { bool isLoaded = false; - if (devServerHelper_) { + if (reactInstanceConfig_.enableDevMode && devServerHelper_) { devServerHelper_->setSourcePath(sourcePath); isLoaded = loadScriptFromDevServer(); } diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactInstanceConfig.h b/packages/react-native/ReactCxxPlatform/react/runtime/ReactInstanceConfig.h index d90b42a35472..43a0accd6583 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactInstanceConfig.h +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactInstanceConfig.h @@ -13,13 +13,15 @@ namespace facebook::react { struct ReactInstanceConfig { + std::string appId; + std::string deviceName; #ifdef REACT_NATIVE_DEBUG - bool enableDebugging{true}; + bool enableDevMode{true}; + bool enableInspector{true}; #else - bool enableDebugging{false}; + bool enableDevMode{false}; + bool enableInspector{false}; #endif - std::string appId; - std::string deviceName; std::string devServerHost{"localhost"}; uint32_t devServerPort{8081}; }; From 11d3f4df2d449baee587120eb8eaea0ec7aa7a0a Mon Sep 17 00:00:00 2001 From: Ruben Norte Date: Tue, 12 Aug 2025 03:38:17 -0700 Subject: [PATCH 5/6] Allow custom factories for HTTP and WebSocket clients for DevTools Differential Revision: D79806934 --- .../react/http/IHttpClient.cpp | 3 +++ .../ReactCxxPlatform/react/http/IHttpClient.h | 2 ++ .../react/http/IWebSocketClient.cpp | 4 ++++ .../react/http/IWebSocketClient.h | 2 ++ .../react/runtime/ReactHost.cpp | 18 ++++++++++++++---- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.cpp b/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.cpp index c19f481ee22d..59774dabb498 100644 --- a/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.cpp +++ b/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.cpp @@ -10,4 +10,7 @@ namespace facebook::react { // NOLINTNEXTLINE(modernize-avoid-c-arrays) extern const char HttpClientFactoryKey[] = "HttpClientFactory"; +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +extern const char DevToolsHttpClientFactoryKey[] = "DevToolsHttpClientFactory"; + } // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.h b/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.h index 9b3d78b5c9d6..5cd56007f88b 100644 --- a/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.h +++ b/packages/react-native/ReactCxxPlatform/react/http/IHttpClient.h @@ -82,6 +82,8 @@ struct IHttpClient { extern const char HttpClientFactoryKey[]; +extern const char DevToolsHttpClientFactoryKey[]; + using HttpClientFactory = std::function()>; HttpClientFactory getHttpClientFactory(); diff --git a/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.cpp b/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.cpp index 93800a35a7b7..838f687e3724 100644 --- a/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.cpp +++ b/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.cpp @@ -10,4 +10,8 @@ namespace facebook::react { // NOLINTNEXTLINE(modernize-avoid-c-arrays) extern const char WebSocketClientFactoryKey[] = "WebSocketClientFactory"; +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +extern const char DevToolsWebSocketClientFactoryKey[] = + "DevToolsWebSocketClientFactory"; + } // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.h b/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.h index 6af9b1efbefa..9424fb76ab00 100644 --- a/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.h +++ b/packages/react-native/ReactCxxPlatform/react/http/IWebSocketClient.h @@ -38,6 +38,8 @@ class IWebSocketClient { extern const char WebSocketClientFactoryKey[]; +extern const char DevToolsWebSocketClientFactoryKey[]; + using WebSocketClientFactory = std::function()>; diff --git a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp index b35df76cf652..799d39aeb94a 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp @@ -136,6 +136,16 @@ void ReactHost::createReactInstance() { reactInstanceData_->contextContainer->at( WebSocketClientFactoryKey); + auto devToolsHttpClientFactory = + reactInstanceData_->contextContainer + ->find(DevToolsHttpClientFactoryKey) + .value_or(httpClientFactory); + + auto devToolsWebSocketClientFactory = + reactInstanceData_->contextContainer + ->find(DevToolsWebSocketClientFactoryKey) + .value_or(webSocketClientFactory); + // Create devServerHelper if (!devServerHelper_ && (reactInstanceConfig_.enableInspector || @@ -145,7 +155,7 @@ void ReactHost::createReactInstance() { reactInstanceConfig_.deviceName, reactInstanceConfig_.devServerHost, reactInstanceConfig_.devServerPort, - httpClientFactory, + devToolsHttpClientFactory, [this]( const std::string& moduleName, const std::string& methodName, @@ -159,8 +169,8 @@ void ReactHost::createReactInstance() { inspector_ = std::make_shared( reactInstanceConfig_.appId, reactInstanceConfig_.deviceName, - webSocketClientFactory, - httpClientFactory); + devToolsWebSocketClientFactory, + devToolsHttpClientFactory); inspector_->ensureHostTarget( [this]() { reloadReactInstance(); }, [weakDevUIDelegate = @@ -179,7 +189,7 @@ void ReactHost::createReactInstance() { if (!packagerConnection_ && reactInstanceConfig_.enableDevMode) { packagerConnection_ = std::make_unique( - webSocketClientFactory, + devToolsWebSocketClientFactory, devServerHelper_->getPackagerConnectionUrl(), [this]() { reloadReactInstance(); }, []() {}); From 6c8f4bf451f5b85fd46e68794e8a0e99165f7bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Tue, 12 Aug 2025 03:41:57 -0700 Subject: [PATCH 6/6] Automatically inject debugger statements in tests in preparation for debug mode (#53205) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53205 Changelog: [internal] This injects a custom Babel transform for Fantom tests that automatically injects `debugger` statements in the generated code. This simplifies debugging by providing a default interruption point in the test setup for the test author to decide what to debug. This has no effect unless the debugger is opened, which isn't happening yet. Reviewed By: rshest Differential Revision: D79996000 --- .../inject-debugger-statements-in-tests.js | 35 +++++++++++++ .../config/metro-babel-transformer.flow.js | 49 +++++++++++++++++++ .../config/metro-babel-transformer.js | 2 +- 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 private/react-native-fantom/config/babel-plugins/inject-debugger-statements-in-tests.js create mode 100644 private/react-native-fantom/config/metro-babel-transformer.flow.js diff --git a/private/react-native-fantom/config/babel-plugins/inject-debugger-statements-in-tests.js b/private/react-native-fantom/config/babel-plugins/inject-debugger-statements-in-tests.js new file mode 100644 index 000000000000..6304b960232e --- /dev/null +++ b/private/react-native-fantom/config/babel-plugins/inject-debugger-statements-in-tests.js @@ -0,0 +1,35 @@ +/** + * 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. + * + * @noflow + * @format + */ + +/** + * This transform injects a single `debugger;` statement at the top of every + * Fantom test, so we can automatically stop on them when debugging. + */ +module.exports = function ({types: t}) { + return { + name: 'inject-debugger-statements-in-tests', + visitor: { + Program(path, state) { + const filename = state.filename || ''; + if ( + (filename.endsWith('-itest.js') || + filename.endsWith('-itest.fb.js')) && + !filename.includes('/.out/') + ) { + // Check if the first statement is already a debugger statement + const first = path.node.body[0]; + if (!first || first.type !== 'DebuggerStatement') { + path.unshiftContainer('body', t.debuggerStatement()); + } + } + }, + }, + }; +}; diff --git a/private/react-native-fantom/config/metro-babel-transformer.flow.js b/private/react-native-fantom/config/metro-babel-transformer.flow.js new file mode 100644 index 000000000000..b3a07cad935f --- /dev/null +++ b/private/react-native-fantom/config/metro-babel-transformer.flow.js @@ -0,0 +1,49 @@ +/** + * 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 { + BabelTransformer, + BabelTransformerArgs, +} from 'metro-babel-transformer'; + +import MetroBabelTransformer from '@react-native/metro-babel-transformer'; +import crypto from 'crypto'; +import fs from 'fs'; + +const transform: BabelTransformer['transform'] = ( + args: BabelTransformerArgs, +) => { + const processedArgs = { + ...args, + plugins: [ + ...(args.plugins ?? []), + // $FlowExpectedError[untyped-import] + require('./babel-plugins/inject-debugger-statements-in-tests'), + ], + }; + return MetroBabelTransformer.transform(processedArgs); +}; + +module.exports = { + ...MetroBabelTransformer, + transform, + getCacheKey(): string { + const key = crypto.createHash('md5'); + const cacheKeyParts = [ + MetroBabelTransformer.getCacheKey?.() ?? '', + fs.readFileSync(__filename), + fs.readFileSync( + require.resolve('./babel-plugins/inject-debugger-statements-in-tests'), + ), + ]; + cacheKeyParts.forEach(part => key.update(part)); + return key.digest('hex'); + }, +}; diff --git a/private/react-native-fantom/config/metro-babel-transformer.js b/private/react-native-fantom/config/metro-babel-transformer.js index 51593b0b4ef6..5b792b337aa1 100644 --- a/private/react-native-fantom/config/metro-babel-transformer.js +++ b/private/react-native-fantom/config/metro-babel-transformer.js @@ -11,4 +11,4 @@ 'use strict'; require('../../../scripts/shared/babelRegister').registerForMonorepo(); -module.exports = require('@react-native/metro-babel-transformer'); +module.exports = require('./metro-babel-transformer.flow');