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 bcd8d15f8aed..799d39aeb94a 100644 --- a/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp +++ b/packages/react-native/ReactCxxPlatform/react/runtime/ReactHost.cpp @@ -136,52 +136,63 @@ 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 (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, + devToolsHttpClientFactory, + [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, + devToolsWebSocketClientFactory, + devToolsHttpClientFactory); + 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( + devToolsWebSocketClientFactory, + devServerHelper_->getPackagerConnectionUrl(), + [this]() { reloadReactInstance(); }, + []() {}); } // Create the React Instance @@ -194,7 +205,7 @@ void ReactHost::createReactInstance() { reactInstanceData_->messageQueueThread, /* allocInOldGenBeforeTTI */ false); - if (reactInstanceConfig_.enableDebugging) { + if (reactInstanceConfig_.enableInspector) { react_native_assert( inspector_ != nullptr && "Inspector is not initialized"); } @@ -240,6 +251,10 @@ void ReactHost::createReactInstance() { auto jsInvoker = std::make_shared( reactInstance_->getRuntimeScheduler()); + if (inspector_ != nullptr) { + inspector_->connectDebugger(devServerHelper_->getInspectorUrl()); + } + auto liveReloadCallback = [this]() { reloadReactInstance(); }; reactInstance_->initializeRuntime( { @@ -258,7 +273,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, @@ -417,7 +433,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(); } @@ -451,9 +467,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 (...) { 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}; }; 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'); diff --git a/private/react-native-fantom/runner/global-setup/globalSetup.js b/private/react-native-fantom/runner/global-setup/globalSetup.js index f87eec9848a0..94bf690a0335 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; + globalThis.__FANTOM_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); + }); } 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; }