From 7e02cc60883bfc3df2b134177d8073023d3b2513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 19 Sep 2025 22:54:30 +0200 Subject: [PATCH 1/3] Export and use explicit wrapAction instead of monkey-patching commander --- packages/cli-utils/src/actions.ts | 26 +-- packages/cmake-rn/src/cli.ts | 5 +- packages/ferric/src/build.ts | 323 +++++++++++++------------- packages/gyp-to-cmake/src/cli.ts | 34 +-- packages/host/src/node/cli/hermes.ts | 18 +- packages/host/src/node/cli/program.ts | 287 ++++++++++++----------- 6 files changed, 348 insertions(+), 345 deletions(-) diff --git a/packages/cli-utils/src/actions.ts b/packages/cli-utils/src/actions.ts index 8cd15a03..8cc5ea23 100644 --- a/packages/cli-utils/src/actions.ts +++ b/packages/cli-utils/src/actions.ts @@ -4,10 +4,14 @@ import * as commander from "@commander-js/extra-typings"; import { UsageError } from "./errors.js"; -function wrapAction( - fn: (this: Command, ...args: Args) => void | Promise, -) { - return async function (this: Command, ...args: Args) { +export function wrapAction< + Args extends unknown[], + Opts extends commander.OptionValues, + GlobalOpts extends commander.OptionValues, + Command extends commander.Command, + ActionArgs extends unknown[], +>(fn: (this: Command, ...args: ActionArgs) => void | Promise) { + return async function (this: Command, ...args: ActionArgs) { try { await fn.call(this, ...args); } catch (error) { @@ -34,17 +38,3 @@ function wrapAction( } }; } - -import { Command } from "@commander-js/extra-typings"; - -// Patch Command to wrap all actions with our error handler - -// eslint-disable-next-line @typescript-eslint/unbound-method -const originalAction = Command.prototype.action; - -Command.prototype.action = function action( - this: Command, - fn: Parameters[0], -) { - return originalAction.call(this, wrapAction(fn)); -}; diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index d3f2e066..6c102f74 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -10,6 +10,7 @@ import { spawn, oraPromise, assertFixable, + wrapAction, } from "@react-native-node-api/cli-utils"; import { isSupportedTriplet } from "react-native-node-api"; @@ -131,7 +132,7 @@ for (const platform of platforms) { } program = program.action( - async ({ target: requestedTargets, ...baseOptions }) => { + wrapAction(async ({ target: requestedTargets, ...baseOptions }) => { assertFixable( fs.existsSync(path.join(baseOptions.source, "CMakeLists.txt")), `No CMakeLists.txt found in source directory: ${chalk.dim(baseOptions.source)}`, @@ -245,7 +246,7 @@ program = program.action( baseOptions, ); } - }, + }), ); function getTargetsSummary( diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index ec8992ec..d5cbaad7 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -7,6 +7,7 @@ import { Option, oraPromise, assertFixable, + wrapAction, } from "@react-native-node-api/cli-utils"; import { @@ -116,195 +117,197 @@ export const buildCommand = new Command("build") .addOption(configurationOption) .addOption(xcframeworkExtensionOption) .action( - async ({ - target: targetArg, - apple, - android, - ndkVersion, - output: outputPath, - configuration, - xcframeworkExtension, - }) => { - const targets = new Set([...targetArg]); - if (apple) { - for (const target of APPLE_TARGETS) { - targets.add(target); + wrapAction( + async ({ + target: targetArg, + apple, + android, + ndkVersion, + output: outputPath, + configuration, + xcframeworkExtension, + }) => { + const targets = new Set([...targetArg]); + if (apple) { + for (const target of APPLE_TARGETS) { + targets.add(target); + } } - } - if (android) { - for (const target of ANDROID_TARGETS) { - targets.add(target); + if (android) { + for (const target of ANDROID_TARGETS) { + targets.add(target); + } } - } - if (targets.size === 0) { - if (isAndroidSupported()) { - if (process.arch === "arm64") { - targets.add("aarch64-linux-android"); - } else if (process.arch === "x64") { - targets.add("x86_64-linux-android"); + if (targets.size === 0) { + if (isAndroidSupported()) { + if (process.arch === "arm64") { + targets.add("aarch64-linux-android"); + } else if (process.arch === "x64") { + targets.add("x86_64-linux-android"); + } } - } - if (isAppleSupported()) { - if (process.arch === "arm64") { - targets.add("aarch64-apple-ios-sim"); + if (isAppleSupported()) { + if (process.arch === "arm64") { + targets.add("aarch64-apple-ios-sim"); + } } + console.error( + chalk.yellowBright("ℹ"), + chalk.dim( + `Using default targets, pass ${chalk.italic( + "--android", + )}, ${chalk.italic("--apple")} or individual ${chalk.italic( + "--target", + )} options, to avoid this.`, + ), + ); } - console.error( - chalk.yellowBright("ℹ"), - chalk.dim( - `Using default targets, pass ${chalk.italic( - "--android", - )}, ${chalk.italic("--apple")} or individual ${chalk.italic( - "--target", - )} options, to avoid this.`, - ), - ); - } - ensureCargo(); - ensureInstalledTargets(targets); + ensureCargo(); + ensureInstalledTargets(targets); - const appleTargets = filterTargetsByPlatform(targets, "apple"); - const androidTargets = filterTargetsByPlatform(targets, "android"); + const appleTargets = filterTargetsByPlatform(targets, "apple"); + const androidTargets = filterTargetsByPlatform(targets, "android"); - const targetsDescription = - targets.size + - (targets.size === 1 ? " target" : " targets") + - chalk.dim(" (" + [...targets].join(", ") + ")"); - const [appleLibraries, androidLibraries] = await oraPromise( - Promise.all([ - Promise.all( - appleTargets.map( - async (target) => - [target, await build({ configuration, target })] as const, + const targetsDescription = + targets.size + + (targets.size === 1 ? " target" : " targets") + + chalk.dim(" (" + [...targets].join(", ") + ")"); + const [appleLibraries, androidLibraries] = await oraPromise( + Promise.all([ + Promise.all( + appleTargets.map( + async (target) => + [target, await build({ configuration, target })] as const, + ), ), - ), - Promise.all( - androidTargets.map( - async (target) => - [ - target, - await build({ - configuration, + Promise.all( + androidTargets.map( + async (target) => + [ target, - ndkVersion, - androidApiLevel: ANDROID_API_LEVEL, - }), - ] as const, + await build({ + configuration, + target, + ndkVersion, + androidApiLevel: ANDROID_API_LEVEL, + }), + ] as const, + ), ), - ), - ]), - { - text: `Building ${targetsDescription}`, - successText: `Built ${targetsDescription}`, - failText: (error: Error) => `Failed to build: ${error.message}`, - }, - ); + ]), + { + text: `Building ${targetsDescription}`, + successText: `Built ${targetsDescription}`, + failText: (error: Error) => `Failed to build: ${error.message}`, + }, + ); + + if (androidLibraries.length > 0) { + const libraryPathByTriplet = Object.fromEntries( + androidLibraries.map(([target, outputPath]) => [ + ANDROID_TRIPLET_PER_TARGET[target], + outputPath, + ]), + ) as Record; - if (androidLibraries.length > 0) { - const libraryPathByTriplet = Object.fromEntries( - androidLibraries.map(([target, outputPath]) => [ - ANDROID_TRIPLET_PER_TARGET[target], + const androidLibsFilename = determineAndroidLibsFilename( + Object.values(libraryPathByTriplet), + ); + const androidLibsOutputPath = path.resolve( outputPath, - ]), - ) as Record; + androidLibsFilename, + ); - const androidLibsFilename = determineAndroidLibsFilename( - Object.values(libraryPathByTriplet), - ); - const androidLibsOutputPath = path.resolve( - outputPath, - androidLibsFilename, - ); + await oraPromise( + createAndroidLibsDirectory({ + outputPath: androidLibsOutputPath, + libraryPathByTriplet, + autoLink: true, + }), + { + text: "Assembling Android libs directory", + successText: `Android libs directory assembled into ${prettyPath( + androidLibsOutputPath, + )}`, + failText: ({ message }) => + `Failed to assemble Android libs directory: ${message}`, + }, + ); + } + + if (appleLibraries.length > 0) { + const libraryPaths = await combineLibraries(appleLibraries); + const frameworkPaths = libraryPaths.map(createAppleFramework); + const xcframeworkFilename = determineXCFrameworkFilename( + frameworkPaths, + xcframeworkExtension ? ".xcframework" : ".apple.node", + ); + // Create the xcframework + const xcframeworkOutputPath = path.resolve( + outputPath, + xcframeworkFilename, + ); + + await oraPromise( + createXCframework({ + outputPath: xcframeworkOutputPath, + frameworkPaths, + autoLink: true, + }), + { + text: "Assembling XCFramework", + successText: `XCFramework assembled into ${chalk.dim( + path.relative(process.cwd(), xcframeworkOutputPath), + )}`, + failText: ({ message }) => + `Failed to assemble XCFramework: ${message}`, + }, + ); + } + + const libraryName = determineLibraryBasename([ + ...androidLibraries.map(([, outputPath]) => outputPath), + ...appleLibraries.map(([, outputPath]) => outputPath), + ]); + + const declarationsFilename = `${libraryName}.d.ts`; + const declarationsPath = path.join(outputPath, declarationsFilename); await oraPromise( - createAndroidLibsDirectory({ - outputPath: androidLibsOutputPath, - libraryPathByTriplet, - autoLink: true, + generateTypeScriptDeclarations({ + outputFilename: declarationsFilename, + createPath: process.cwd(), + outputPath, }), { - text: "Assembling Android libs directory", - successText: `Android libs directory assembled into ${prettyPath( - androidLibsOutputPath, + text: "Generating TypeScript declarations", + successText: `Generated TypeScript declarations ${prettyPath( + declarationsPath, )}`, - failText: ({ message }) => - `Failed to assemble Android libs directory: ${message}`, + failText: (error) => + `Failed to generate TypeScript declarations: ${error.message}`, }, ); - } - if (appleLibraries.length > 0) { - const libraryPaths = await combineLibraries(appleLibraries); - const frameworkPaths = libraryPaths.map(createAppleFramework); - const xcframeworkFilename = determineXCFrameworkFilename( - frameworkPaths, - xcframeworkExtension ? ".xcframework" : ".apple.node", - ); - - // Create the xcframework - const xcframeworkOutputPath = path.resolve( - outputPath, - xcframeworkFilename, - ); + const entrypointPath = path.join(outputPath, `${libraryName}.js`); await oraPromise( - createXCframework({ - outputPath: xcframeworkOutputPath, - frameworkPaths, - autoLink: true, + generateEntrypoint({ + libraryName, + outputPath: entrypointPath, }), { - text: "Assembling XCFramework", - successText: `XCFramework assembled into ${chalk.dim( - path.relative(process.cwd(), xcframeworkOutputPath), + text: `Generating entrypoint`, + successText: `Generated entrypoint into ${prettyPath( + entrypointPath, )}`, - failText: ({ message }) => - `Failed to assemble XCFramework: ${message}`, + failText: (error) => + `Failed to generate entrypoint: ${error.message}`, }, ); - } - - const libraryName = determineLibraryBasename([ - ...androidLibraries.map(([, outputPath]) => outputPath), - ...appleLibraries.map(([, outputPath]) => outputPath), - ]); - - const declarationsFilename = `${libraryName}.d.ts`; - const declarationsPath = path.join(outputPath, declarationsFilename); - await oraPromise( - generateTypeScriptDeclarations({ - outputFilename: declarationsFilename, - createPath: process.cwd(), - outputPath, - }), - { - text: "Generating TypeScript declarations", - successText: `Generated TypeScript declarations ${prettyPath( - declarationsPath, - )}`, - failText: (error) => - `Failed to generate TypeScript declarations: ${error.message}`, - }, - ); - - const entrypointPath = path.join(outputPath, `${libraryName}.js`); - - await oraPromise( - generateEntrypoint({ - libraryName, - outputPath: entrypointPath, - }), - { - text: `Generating entrypoint`, - successText: `Generated entrypoint into ${prettyPath( - entrypointPath, - )}`, - failText: (error) => - `Failed to generate entrypoint: ${error.message}`, - }, - ); - }, + }, + ), ); async function combineLibraries( diff --git a/packages/gyp-to-cmake/src/cli.ts b/packages/gyp-to-cmake/src/cli.ts index 5556c730..118f0c0d 100644 --- a/packages/gyp-to-cmake/src/cli.ts +++ b/packages/gyp-to-cmake/src/cli.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -import { Command } from "@react-native-node-api/cli-utils"; +import { Command, wrapAction } from "@react-native-node-api/cli-utils"; import { readBindingFile } from "./gyp.js"; import { @@ -67,18 +67,20 @@ export const program = new Command("gyp-to-cmake") "Path to the binding.gyp file or directory to traverse recursively", process.cwd(), ) - .action((targetPath: string, { pathTransforms }) => { - const options: TransformOptions = { - unsupportedBehaviour: "throw", - disallowUnknownProperties: false, - transformWinPathsToPosix: pathTransforms, - }; - const stat = fs.statSync(targetPath); - if (stat.isFile()) { - transformBindingGypFile(targetPath, options); - } else if (stat.isDirectory()) { - transformBindingGypsRecursively(targetPath, options); - } else { - throw new Error(`Expected either a file or a directory: ${targetPath}`); - } - }); + .action( + wrapAction((targetPath: string, { pathTransforms }) => { + const options: TransformOptions = { + unsupportedBehaviour: "throw", + disallowUnknownProperties: false, + transformWinPathsToPosix: pathTransforms, + }; + const stat = fs.statSync(targetPath); + if (stat.isFile()) { + transformBindingGypFile(targetPath, options); + } else if (stat.isDirectory()) { + transformBindingGypsRecursively(targetPath, options); + } else { + throw new Error(`Expected either a file or a directory: ${targetPath}`); + } + }), + ); diff --git a/packages/host/src/node/cli/hermes.ts b/packages/host/src/node/cli/hermes.ts index f3c884b5..c2a165c0 100644 --- a/packages/host/src/node/cli/hermes.ts +++ b/packages/host/src/node/cli/hermes.ts @@ -3,10 +3,12 @@ import fs from "node:fs"; import path from "node:path"; import { + chalk, Command, oraPromise, spawn, - SpawnFailure, + UsageError, + wrapAction, } from "@react-native-node-api/cli-utils"; import { packageDirectorySync } from "pkg-dir"; @@ -24,8 +26,8 @@ export const command = new Command("vendor-hermes") "Don't check timestamps of input files to skip unnecessary rebuilds", false, ) - .action(async (from, { force, silent }) => { - try { + .action( + wrapAction(async (from, { force, silent }) => { const appPackageRoot = packageDirectorySync({ cwd: from }); assert(appPackageRoot, "Failed to find package root"); const reactNativePath = path.dirname( @@ -124,11 +126,5 @@ export const command = new Command("vendor-hermes") }, ); console.log(hermesPath); - } catch (error) { - process.exitCode = 1; - if (error instanceof SpawnFailure) { - error.flushOutput("both"); - } - throw error; - } - }); + }), + ); diff --git a/packages/host/src/node/cli/program.ts b/packages/host/src/node/cli/program.ts index 4e6b7bd7..37950d6d 100644 --- a/packages/host/src/node/cli/program.ts +++ b/packages/host/src/node/cli/program.ts @@ -7,6 +7,7 @@ import { chalk, SpawnFailure, oraPromise, + wrapAction, } from "@react-native-node-api/cli-utils"; import { @@ -70,98 +71,102 @@ program .option("--android", "Link Android modules") .option("--apple", "Link Apple modules") .addOption(pathSuffixOption) - .action(async (pathArg, { force, prune, pathSuffix, android, apple }) => { - console.log("Auto-linking Node-API modules from", chalk.dim(pathArg)); - const platforms: PlatformName[] = []; - if (android) { - platforms.push("android"); - } - if (apple) { - platforms.push("apple"); - } - - if (platforms.length === 0) { - console.error( - `No platform specified, pass one or more of:`, - ...PLATFORMS.map((platform) => chalk.bold(`\n --${platform}`)), - ); - process.exitCode = 1; - return; - } - - for (const platform of platforms) { - const platformDisplayName = getPlatformDisplayName(platform); - const platformOutputPath = getAutolinkPath(platform); - const modules = await oraPromise( - () => - linkModules({ - platform, - fromPath: path.resolve(pathArg), - incremental: !force, - naming: { pathSuffix }, - linker: getLinker(platform), - }), - { - text: `Linking ${platformDisplayName} Node-API modules into ${prettyPath( - platformOutputPath, - )}`, - successText: `Linked ${platformDisplayName} Node-API modules into ${prettyPath( - platformOutputPath, - )}`, - failText: (error) => - `Failed to link ${platformDisplayName} Node-API modules into ${prettyPath( - platformOutputPath, - )}: ${error.message}`, - }, - ); - - if (modules.length === 0) { - console.log("Found no Node-API modules 🤷"); - } - - const failures = modules.filter((result) => "failure" in result); - const linked = modules.filter((result) => "outputPath" in result); + .action( + wrapAction( + async (pathArg, { force, prune, pathSuffix, android, apple }) => { + console.log("Auto-linking Node-API modules from", chalk.dim(pathArg)); + const platforms: PlatformName[] = []; + if (android) { + platforms.push("android"); + } + if (apple) { + platforms.push("apple"); + } - for (const { originalPath, outputPath, skipped } of linked) { - const prettyOutputPath = outputPath - ? "→ " + prettyPath(path.basename(outputPath)) - : ""; - if (skipped) { - console.log( - chalk.greenBright("-"), - "Skipped", - prettyPath(originalPath), - prettyOutputPath, - "(up to date)", - ); - } else { - console.log( - chalk.greenBright("⚭"), - "Linked", - prettyPath(originalPath), - prettyOutputPath, + if (platforms.length === 0) { + console.error( + `No platform specified, pass one or more of:`, + ...PLATFORMS.map((platform) => chalk.bold(`\n --${platform}`)), ); + process.exitCode = 1; + return; } - } - for (const { originalPath, failure } of failures) { - assert(failure instanceof SpawnFailure); - console.error( - "\n", - chalk.redBright("āœ–"), - "Failed to copy", - prettyPath(originalPath), - ); - console.error(failure.message); - failure.flushOutput("both"); - process.exitCode = 1; - } + for (const platform of platforms) { + const platformDisplayName = getPlatformDisplayName(platform); + const platformOutputPath = getAutolinkPath(platform); + const modules = await oraPromise( + () => + linkModules({ + platform, + fromPath: path.resolve(pathArg), + incremental: !force, + naming: { pathSuffix }, + linker: getLinker(platform), + }), + { + text: `Linking ${platformDisplayName} Node-API modules into ${prettyPath( + platformOutputPath, + )}`, + successText: `Linked ${platformDisplayName} Node-API modules into ${prettyPath( + platformOutputPath, + )}`, + failText: (error) => + `Failed to link ${platformDisplayName} Node-API modules into ${prettyPath( + platformOutputPath, + )}: ${error.message}`, + }, + ); - if (prune) { - await pruneLinkedModules(platform, modules); - } - } - }); + if (modules.length === 0) { + console.log("Found no Node-API modules 🤷"); + } + + const failures = modules.filter((result) => "failure" in result); + const linked = modules.filter((result) => "outputPath" in result); + + for (const { originalPath, outputPath, skipped } of linked) { + const prettyOutputPath = outputPath + ? "→ " + prettyPath(path.basename(outputPath)) + : ""; + if (skipped) { + console.log( + chalk.greenBright("-"), + "Skipped", + prettyPath(originalPath), + prettyOutputPath, + "(up to date)", + ); + } else { + console.log( + chalk.greenBright("⚭"), + "Linked", + prettyPath(originalPath), + prettyOutputPath, + ); + } + } + + for (const { originalPath, failure } of failures) { + assert(failure instanceof SpawnFailure); + console.error( + "\n", + chalk.redBright("āœ–"), + "Failed to copy", + prettyPath(originalPath), + ); + console.error(failure.message); + failure.flushOutput("both"); + process.exitCode = 1; + } + + if (prune) { + await pruneLinkedModules(platform, modules); + } + } + }, + ), + ); program .command("list") @@ -169,44 +174,48 @@ program .argument("[from-path]", "Some path inside the app package", process.cwd()) .option("--json", "Output as JSON", false) .addOption(pathSuffixOption) - .action(async (fromArg, { json, pathSuffix }) => { - const rootPath = path.resolve(fromArg); - const dependencies = await findNodeApiModulePathsByDependency({ - fromPath: rootPath, - platform: PLATFORMS, - includeSelf: true, - }); - - if (json) { - console.log(JSON.stringify(dependencies, null, 2)); - } else { - const dependencyCount = Object.keys(dependencies).length; - const xframeworkCount = Object.values(dependencies).reduce( - (acc, { modulePaths }) => acc + modulePaths.length, - 0, - ); - console.log( - "Found", - chalk.greenBright(xframeworkCount), - "Node-API modules in", - chalk.greenBright(dependencyCount), - dependencyCount === 1 ? "package" : "packages", - "from", - prettyPath(rootPath), - ); - for (const [dependencyName, dependency] of Object.entries(dependencies)) { - console.log( - chalk.blueBright(dependencyName), - "→", - prettyPath(dependency.path), + .action( + wrapAction(async (fromArg, { json, pathSuffix }) => { + const rootPath = path.resolve(fromArg); + const dependencies = await findNodeApiModulePathsByDependency({ + fromPath: rootPath, + platform: PLATFORMS, + includeSelf: true, + }); + + if (json) { + console.log(JSON.stringify(dependencies, null, 2)); + } else { + const dependencyCount = Object.keys(dependencies).length; + const xframeworkCount = Object.values(dependencies).reduce( + (acc, { modulePaths }) => acc + modulePaths.length, + 0, ); - logModulePaths( - dependency.modulePaths.map((p) => path.join(dependency.path, p)), - { pathSuffix }, + console.log( + "Found", + chalk.greenBright(xframeworkCount), + "Node-API modules in", + chalk.greenBright(dependencyCount), + dependencyCount === 1 ? "package" : "packages", + "from", + prettyPath(rootPath), ); + for (const [dependencyName, dependency] of Object.entries( + dependencies, + )) { + console.log( + chalk.blueBright(dependencyName), + "→", + prettyPath(dependency.path), + ); + logModulePaths( + dependency.modulePaths.map((p) => path.join(dependency.path, p)), + { pathSuffix }, + ); + } } - } - }); + }), + ); program .command("info ") @@ -214,19 +223,21 @@ program "Utility to print, module path, the hash of a single Android library", ) .addOption(pathSuffixOption) - .action((pathInput, { pathSuffix }) => { - const resolvedModulePath = path.resolve(pathInput); - const normalizedModulePath = normalizeModulePath(resolvedModulePath); - const { packageName, relativePath } = - determineModuleContext(resolvedModulePath); - const libraryName = getLibraryName(resolvedModulePath, { - pathSuffix, - }); - console.log({ - resolvedModulePath, - normalizedModulePath, - packageName, - relativePath, - libraryName, - }); - }); + .action( + wrapAction((pathInput, { pathSuffix }) => { + const resolvedModulePath = path.resolve(pathInput); + const normalizedModulePath = normalizeModulePath(resolvedModulePath); + const { packageName, relativePath } = + determineModuleContext(resolvedModulePath); + const libraryName = getLibraryName(resolvedModulePath, { + pathSuffix, + }); + console.log({ + resolvedModulePath, + normalizedModulePath, + packageName, + relativePath, + libraryName, + }); + }), + ); From dc79b31a0e14f8645b9f9016b26f69f448e49fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 19 Sep 2025 22:54:57 +0200 Subject: [PATCH 2/3] Flush output when SpawnFailures are cause --- packages/cli-utils/src/actions.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/cli-utils/src/actions.ts b/packages/cli-utils/src/actions.ts index 8cc5ea23..0df0b7d2 100644 --- a/packages/cli-utils/src/actions.ts +++ b/packages/cli-utils/src/actions.ts @@ -18,11 +18,18 @@ export function wrapAction< process.exitCode = 1; if (error instanceof SpawnFailure) { error.flushOutput("both"); + } else if ( + error instanceof Error && + error.cause instanceof SpawnFailure + ) { + error.cause.flushOutput("both"); } + // Ensure some visual distance to the previous output + console.error(); if (error instanceof UsageError || error instanceof SpawnFailure) { console.error(chalk.red("ERROR"), error.message); if (error.cause instanceof Error) { - console.error(chalk.red("CAUSE"), error.cause.message); + console.error(chalk.blue("CAUSE"), error.cause.message); } if (error instanceof UsageError && error.fix) { console.error( From 24855c2a793a3006af979fe04280e81be9f5011d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 19 Sep 2025 22:55:23 +0200 Subject: [PATCH 3/3] Update vendor-hermes to use UsageError --- packages/host/src/node/cli/hermes.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/host/src/node/cli/hermes.ts b/packages/host/src/node/cli/hermes.ts index c2a165c0..817b7524 100644 --- a/packages/host/src/node/cli/hermes.ts +++ b/packages/host/src/node/cli/hermes.ts @@ -93,17 +93,12 @@ export const command = new Command("vendor-hermes") }, ); } catch (error) { - if (error instanceof SpawnFailure) { - error.flushOutput("both"); - console.error( - `\nšŸ›‘ React Native uses the ${hermesVersion} tag and cloning our fork failed.`, - `Please see the Node-API package's peer dependency on "react-native" for supported versions.`, - ); - process.exitCode = 1; - return; - } else { - throw error; - } + throw new UsageError("Failed to clone custom Hermes", { + cause: error, + fix: { + instructions: `Check the network connection and ensure this ${chalk.bold("react-native")} version is supported by ${chalk.bold("react-native-node-api")}.`, + }, + }); } } const hermesJsiPath = path.join(hermesPath, "API/jsi/jsi");