From 79e2abd9e7688053502994a1ea79338f5bfd7f43 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Wed, 29 Jan 2025 13:07:21 +0100 Subject: [PATCH] feat(cli): support multiple debug targets (#3376) --- .changeset/calm-adults-provide.md | 5 ++ .yarnrc.yml | 3 + packages/cli/src/helpers/externals.ts | 18 ++++-- packages/cli/src/serve/keyboard.ts | 90 ++++++++++++++++++++++----- packages/cli/src/start.ts | 11 +++- packages/test-app/ios/Podfile.lock | 4 +- 6 files changed, 108 insertions(+), 23 deletions(-) create mode 100644 .changeset/calm-adults-provide.md diff --git a/.changeset/calm-adults-provide.md b/.changeset/calm-adults-provide.md new file mode 100644 index 000000000..0a123721c --- /dev/null +++ b/.changeset/calm-adults-provide.md @@ -0,0 +1,5 @@ +--- +"@rnx-kit/cli": patch +--- + +Support multiple debug targets diff --git a/.yarnrc.yml b/.yarnrc.yml index 57f28af99..1dc47c77c 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -20,6 +20,9 @@ packageExtensions: # https://github.com/microsoft/fluentui/pull/30964 "@types/react": optional: true + "@react-native/dev-middleware@*": + dependencies: + invariant: ^2.2.4 babel-plugin-transform-flow-enums@*: peerDependencies: "@babel/core": ^7.20.0 diff --git a/packages/cli/src/helpers/externals.ts b/packages/cli/src/helpers/externals.ts index 300b71568..e94b756cf 100644 --- a/packages/cli/src/helpers/externals.ts +++ b/packages/cli/src/helpers/externals.ts @@ -1,5 +1,7 @@ import { resolveDependencyChain } from "@rnx-kit/tools-node/package"; import { resolveCommunityCLI } from "@rnx-kit/tools-react-native/context"; +import * as fs from "node:fs"; +import * as path from "node:path"; import type { CliServerApi, CoreDevMiddleware } from "../serve/types"; type ExternalModule = @@ -7,13 +9,21 @@ type ExternalModule = | "@react-native/dev-middleware"; function friendlyRequire(modules: string[], startDir: string): T { + const target = modules.pop(); + if (!target) { + throw new Error("At least one target module is required"); + } + + const resolvedStartDir = fs.lstatSync(startDir).isSymbolicLink() + ? path.resolve(path.dirname(startDir), fs.readlinkSync(startDir)) + : startDir; try { - const modulePath = resolveDependencyChain(modules, startDir); - return require(modulePath) as T; + const finalPackageDir = resolveDependencyChain(modules, resolvedStartDir); + const targetModule = require.resolve(target, { paths: [finalPackageDir] }); + return require(targetModule) as T; } catch (_) { - const module = modules[modules.length - 1]; throw new Error( - `Cannot find module '${module}'. This probably means that ` + + `Cannot find module '${target}'. This probably means that ` + "'@rnx-kit/cli' is not compatible with the version of 'react-native' " + "that you are currently using. Please update to the latest version " + "and try again. If the issue still persists after the update, please " + diff --git a/packages/cli/src/serve/keyboard.ts b/packages/cli/src/serve/keyboard.ts index e329962ba..509f0e4d2 100644 --- a/packages/cli/src/serve/keyboard.ts +++ b/packages/cli/src/serve/keyboard.ts @@ -1,33 +1,93 @@ import { info } from "@rnx-kit/console"; import type { MetroTerminal } from "@rnx-kit/metro-service"; +import * as fs from "node:fs"; +import type { Server } from "node:http"; +import * as path from "node:path"; import readline from "node:readline"; import qrcode from "qrcode"; import type { DevServerMiddleware } from "./types"; -type Options = { +type OpenDebuggerKeyboardHandler = { + handleOpenDebugger: () => Promise; + maybeHandleTargetSelection: (key: string) => boolean; + dismiss: () => void; +}; + +type Params = { devServerUrl: string; help: () => void; messageSocketEndpoint: DevServerMiddleware["messageSocketEndpoint"]; - terminal: MetroTerminal["terminal"]; + metroTerminal: MetroTerminal; + reactNativePath: string; }; -export function attachKeyHandlers({ +function createOpenDebuggerKeyboardHandler({ devServerUrl, - help, - messageSocketEndpoint, - terminal, -}: Options) { + metroTerminal: { reporter }, + reactNativePath, +}: Params): OpenDebuggerKeyboardHandler { + const resolvedPath = fs.lstatSync(reactNativePath).isSymbolicLink() + ? path.resolve( + path.dirname(reactNativePath), + fs.readlinkSync(reactNativePath) + ) + : reactNativePath; + try { + // Available starting with 0.76 + const cliPlugin = require.resolve( + "@react-native/community-cli-plugin/package.json", + { paths: [resolvedPath] } + ); + const { default: OpenDebuggerKeyboardHandler } = require( + `${path.dirname(cliPlugin)}/dist/commands/start/OpenDebuggerKeyboardHandler` + ); + return new OpenDebuggerKeyboardHandler({ devServerUrl, reporter }); + } catch (_) { + return { + handleOpenDebugger: () => { + info("Opening debugger..."); + fetch(devServerUrl + "/open-debugger", { method: "POST" }); + return Promise.resolve(); + }, + maybeHandleTargetSelection: (_: string): boolean => false, + dismiss: () => undefined, + }; + } +} + +export function attachKeyHandlers(server: Server, params: Params) { + const openDebuggerKeyboardHandler = createOpenDebuggerKeyboardHandler(params); + const { + devServerUrl, + help, + messageSocketEndpoint, + metroTerminal: { terminal }, + } = params; + + process.on("SIGINT", () => { + openDebuggerKeyboardHandler.dismiss(); + process.stdin.pause(); + process.stdin.setRawMode(false); + info("Exiting..."); + server.close(); + server.closeAllConnections?.(); // This method was added in Node v18.2.0 + + // Even when we close all connections, clients may keep the server alive. + process.exit(); + }); + process.stdin.setRawMode(true); process.stdin.on("keypress", (_key, data) => { const { ctrl, name } = data; + if (openDebuggerKeyboardHandler.maybeHandleTargetSelection(name)) { + return; + } + if (ctrl === true) { switch (name) { case "c": - info("Exiting..."); - process.exit(); - break; - case "z": - process.emit("SIGTSTP", "SIGTSTP"); + case "d": + process.emit("SIGINT"); break; } } else { @@ -41,11 +101,9 @@ export function attachKeyHandlers({ help(); break; - case "j": { - info("Opening debugger..."); - fetch(devServerUrl + "/open-debugger", { method: "POST" }); + case "j": + openDebuggerKeyboardHandler.handleOpenDebugger(); break; - } case "q": { const url = `${devServerUrl}/index.bundle`; diff --git a/packages/cli/src/start.ts b/packages/cli/src/start.ts index 47cb77c45..9311f2c10 100644 --- a/packages/cli/src/start.ts +++ b/packages/cli/src/start.ts @@ -225,7 +225,16 @@ export async function rnxStart( // in interactive mode, listen for keyboard events from stdin and bind // them to specific actions. if (interactive) { - attachKeyHandlers({ devServerUrl, help, messageSocketEndpoint, terminal }); + attachKeyHandlers(serverInstance, { + devServerUrl, + help, + messageSocketEndpoint, + metroTerminal: { + terminal, + reporter: terminalReporter, + }, + reactNativePath: ctx.reactNativePath, + }); } } diff --git a/packages/test-app/ios/Podfile.lock b/packages/test-app/ios/Podfile.lock index aa1f16cf3..f3a5716b5 100644 --- a/packages/test-app/ios/Podfile.lock +++ b/packages/test-app/ios/Podfile.lock @@ -1466,7 +1466,7 @@ PODS: - React-logger (= 0.76.6) - React-perflogger (= 0.76.6) - React-utils (= 0.76.6) - - ReactNativeHost (0.5.2): + - ReactNativeHost (0.5.3): - DoubleConversion - glog - RCT-Folly (= 2024.01.01.00) @@ -1797,7 +1797,7 @@ SPEC CHECKSUMS: React-utils: ca8195b8b6da1142e93c64d872e5f2e563c917fd ReactCodegen: b95bdd8716016cb9a831cfd3f23d9b9de4dc5d8e ReactCommon: 316f528c058ca9bc3a0c430ce46a85bbd69aa2a0 - ReactNativeHost: 8474de088a9569717439270e2f95093cf056eb81 + ReactNativeHost: c5bb63cd50a88da07f07b4604e51ace44c950a61 ReactTestApp-DevSupport: 66f5c681e1dace4fa01723706b4a1491657a8623 ReactTestApp-MSAL: 90d3923624b7a11b06c113bcaa3ffc964e1c9422 ReactTestApp-Resources: 70da1d78d943a1fdff6362ce3f778e5b4560c95a