diff --git a/.changeset/big-plums-write.md b/.changeset/big-plums-write.md new file mode 100644 index 00000000..45bc6c6e --- /dev/null +++ b/.changeset/big-plums-write.md @@ -0,0 +1,5 @@ +--- +"ferric-cli": patch +--- + +Add --verbose, --concurrency, --clean options diff --git a/.changeset/evil-pens-shop.md b/.changeset/evil-pens-shop.md new file mode 100644 index 00000000..63ce2e2a --- /dev/null +++ b/.changeset/evil-pens-shop.md @@ -0,0 +1,5 @@ +--- +"@react-native-node-api/cli-utils": patch +--- + +Add re-export of "p-limit" diff --git a/package-lock.json b/package-lock.json index eaf0b060..3bd68dec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ }, "apps/test-app": { "name": "@react-native-node-api/test-app", - "version": "0.2.0", + "version": "0.2.1", "dependencies": { "@babel/core": "^7.26.10", "@babel/preset-env": "^7.26.9", @@ -15089,13 +15089,14 @@ }, "packages/cli-utils": { "name": "@react-native-node-api/cli-utils", - "version": "0.1.1", + "version": "0.1.2", "dependencies": { "@commander-js/extra-typings": "^14.0.0", "bufout": "^0.3.2", "chalk": "^5.4.1", "commander": "^14.0.1", - "ora": "^8.2.0" + "ora": "^8.2.0", + "p-limit": "^7.2.0" } }, "packages/cli-utils/node_modules/@commander-js/extra-typings": { @@ -15251,6 +15252,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/cli-utils/node_modules/p-limit": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.2.0.tgz", + "integrity": "sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/cli-utils/node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -15300,18 +15316,18 @@ } }, "packages/cmake-file-api": { - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "zod": "^4.1.11" } }, "packages/cmake-rn": { - "version": "0.6.0", + "version": "0.6.1", "dependencies": { - "@react-native-node-api/cli-utils": "0.1.1", - "cmake-file-api": "0.1.0", - "react-native-node-api": "0.7.0", - "weak-node-api": "0.0.2", + "@react-native-node-api/cli-utils": "0.1.2", + "cmake-file-api": "0.1.1", + "react-native-node-api": "0.7.1", + "weak-node-api": "0.0.3", "zod": "^4.1.11" }, "bin": { @@ -15324,12 +15340,12 @@ }, "packages/ferric": { "name": "ferric-cli", - "version": "0.3.8", + "version": "0.3.9", "dependencies": { "@napi-rs/cli": "~3.0.3", - "@react-native-node-api/cli-utils": "0.1.1", - "react-native-node-api": "0.7.0", - "weak-node-api": "0.0.2" + "@react-native-node-api/cli-utils": "0.1.2", + "react-native-node-api": "0.7.1", + "weak-node-api": "0.0.3" }, "bin": { "ferric": "bin/ferric.js" @@ -15343,9 +15359,9 @@ } }, "packages/gyp-to-cmake": { - "version": "0.5.0", + "version": "0.5.1", "dependencies": { - "@react-native-node-api/cli-utils": "0.1.1", + "@react-native-node-api/cli-utils": "0.1.2", "gyp-parser": "^1.0.4", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1" @@ -15356,11 +15372,11 @@ }, "packages/host": { "name": "react-native-node-api", - "version": "0.7.0", + "version": "0.7.1", "license": "MIT", "dependencies": { "@expo/plist": "^0.4.7", - "@react-native-node-api/cli-utils": "0.1.1", + "@react-native-node-api/cli-utils": "0.1.2", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1", "zod": "^4.1.11" @@ -15376,7 +15392,7 @@ "peerDependencies": { "@babel/core": "^7.26.10", "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.79.7 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4 || 0.81.5", - "weak-node-api": "0.0.2" + "weak-node-api": "0.0.3" } }, "packages/node-addon-examples": { @@ -15404,7 +15420,7 @@ } }, "packages/weak-node-api": { - "version": "0.0.2", + "version": "0.0.3", "license": "MIT", "dependencies": { "node-api-headers": "^1.5.0" diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index ac95662a..73da1def 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -14,6 +14,7 @@ "bufout": "^0.3.2", "chalk": "^5.4.1", "commander": "^14.0.1", - "ora": "^8.2.0" + "ora": "^8.2.0", + "p-limit": "^7.2.0" } } diff --git a/packages/cli-utils/src/index.ts b/packages/cli-utils/src/index.ts index 0fa6ff4a..712adfe8 100644 --- a/packages/cli-utils/src/index.ts +++ b/packages/cli-utils/src/index.ts @@ -2,6 +2,7 @@ export * from "@commander-js/extra-typings"; export { default as chalk } from "chalk"; export * from "ora"; export * from "bufout"; +export { default as pLimit } from "p-limit"; export * from "./actions.js"; export * from "./errors.js"; diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index 57f9ec59..36a673ad 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -1,5 +1,6 @@ import path from "node:path"; import fs from "node:fs"; +import os from "node:os"; import { chalk, @@ -9,6 +10,8 @@ import { assertFixable, wrapAction, prettyPath, + pLimit, + spawn, } from "@react-native-node-api/cli-utils"; import { @@ -85,6 +88,10 @@ function getDefaultTargets() { const targetOption = new Option("--target ", "Target triple") .choices(ALL_TARGETS) .default(getDefaultTargets()); +const cleanOption = new Option( + "--clean", + "Delete the target directory before building", +).default(false); const appleTarget = new Option("--apple", "Use all Apple targets"); const androidTarget = new Option("--android", "Use all Android targets"); const ndkVersionOption = new Option( @@ -112,9 +119,29 @@ const appleBundleIdentifierOption = new Option( "Unique CFBundleIdentifier used for Apple framework artifacts", ).default(undefined, "com.callstackincubator.node-api.{libraryName}"); +const concurrencyOption = new Option( + "--concurrency ", + "Limit the number of concurrent tasks", +) + .argParser((value) => parseInt(value, 10)) + .default( + os.availableParallelism(), + `${os.availableParallelism()} or 1 when verbose is enabled`, + ); + +const verboseOption = new Option( + "--verbose", + "Print more output from underlying compiler & tools", +).default(process.env.CI ? true : false, `false in general and true on CI`); + +function logNotice(message: string, ...params: string[]) { + console.log(`${chalk.yellow("ℹ︎")} ${message}`, ...params); +} + export const buildCommand = new Command("build") .description("Build Rust Node-API module") .addOption(targetOption) + .addOption(cleanOption) .addOption(appleTarget) .addOption(androidTarget) .addOption(ndkVersionOption) @@ -122,10 +149,13 @@ export const buildCommand = new Command("build") .addOption(configurationOption) .addOption(xcframeworkExtensionOption) .addOption(appleBundleIdentifierOption) + .addOption(concurrencyOption) + .addOption(verboseOption) .action( wrapAction( async ({ target: targetArg, + clean, apple, android, ndkVersion, @@ -133,7 +163,25 @@ export const buildCommand = new Command("build") configuration, xcframeworkExtension, appleBundleIdentifier, + concurrency, + verbose, }) => { + if (clean) { + await oraPromise( + () => spawn("cargo", ["clean"], { outputMode: "buffered" }), + { + text: "Cleaning target directory", + successText: "Cleaned target directory", + failText: (error) => `Failed to clean target directory: ${error}`, + }, + ); + } + if (verbose && concurrency > 1) { + logNotice( + `Consider passing ${chalk.blue("--concurrency")} 1 when running in verbose mode`, + ); + } + const limit = pLimit(concurrency); const targets = new Set([...targetArg]); if (apple) { for (const target of APPLE_TARGETS) { @@ -159,15 +207,12 @@ export const buildCommand = new Command("build") 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.`, - ), + logNotice( + `Using default targets, pass ${chalk.blue( + "--android", + )}, ${chalk.blue("--apple")} or individual ${chalk.blue( + "--target", + )} options, choose exactly what to target`, ); } ensureCargo(); @@ -180,30 +225,40 @@ export const buildCommand = new Command("build") 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, + appleTargets.map((target) => + limit( + async () => + [ + target, + await build({ configuration, target, verbose }), + ] as const, + ), ), ), Promise.all( - androidTargets.map( - async (target) => - [ - target, - await build({ - configuration, + androidTargets.map((target) => + limit( + async () => + [ target, - ndkVersion, - androidApiLevel: ANDROID_API_LEVEL, - }), - ] as const, + await build({ + configuration, + target, + verbose, + ndkVersion, + androidApiLevel: ANDROID_API_LEVEL, + }), + ] as const, + ), ), ), ]), { + isSilent: verbose, text: `Building ${targetsDescription}`, successText: `Built ${targetsDescription}`, failText: (error: Error) => `Failed to build: ${error.message}`, @@ -225,11 +280,13 @@ export const buildCommand = new Command("build") ); await oraPromise( - createAndroidLibsDirectory({ - outputPath: androidLibsOutputPath, - libraries, - autoLink: true, - }), + limit(() => + createAndroidLibsDirectory({ + outputPath: androidLibsOutputPath, + libraries, + autoLink: true, + }), + ), { text: "Assembling Android libs directory", successText: `Android libs directory assembled into ${prettyPath( @@ -243,14 +300,25 @@ export const buildCommand = new Command("build") if (appleLibraries.length > 0) { const libraryPaths = await combineLibraries(appleLibraries); - const frameworkPaths = await Promise.all( - libraryPaths.map((libraryPath) => - // TODO: Pass true as `versioned` argument for -darwin targets - createAppleFramework({ - libraryPath, - bundleIdentifier: appleBundleIdentifier, - }), + + const frameworkPaths = await oraPromise( + Promise.all( + libraryPaths.map((libraryPath) => + limit(() => + // TODO: Pass true as `versioned` argument for -darwin targets + createAppleFramework({ + libraryPath, + bundleIdentifier: appleBundleIdentifier, + }), + ), + ), ), + { + text: "Creating Apple frameworks", + successText: `Created Apple frameworks`, + failText: ({ message }) => + `Failed to create Apple frameworks: ${message}`, + }, ); const xcframeworkFilename = determineXCFrameworkFilename( frameworkPaths, diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index 4eee45b2..ef06c16b 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -95,6 +95,7 @@ export function ensureCargo() { type BuildOptions = { configuration: "debug" | "release"; + verbose: boolean; } & ( | { target: AndroidTargetName; @@ -109,7 +110,7 @@ type BuildOptions = { ); export async function build(options: BuildOptions) { - const { target, configuration } = options; + const { target, configuration, verbose } = options; const args = ["build", "--target", target]; if (configuration.toLowerCase() === "release") { args.push("--release"); @@ -123,7 +124,8 @@ export async function build(options: BuildOptions) { args.push("-Z", "build-std=std,panic_abort"); } await spawn("cargo", args, { - outputMode: "buffered", + outputMode: verbose ? "inherit" : "buffered", + outputPrefix: verbose ? chalk.dim(`[${target}]`) : undefined, env: { ...process.env, ...getTargetEnvironmentVariables(options),