From f82239c1f86dfc66d155fa21d9fcffdf63435153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 19 Sep 2025 16:06:22 +0200 Subject: [PATCH 01/82] Pretty print spawn errors instead of simply rethrowing to commander (#238) --- .changeset/salty-kiwis-turn.md | 5 +++++ packages/cmake-rn/src/cli.ts | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .changeset/salty-kiwis-turn.md diff --git a/.changeset/salty-kiwis-turn.md b/.changeset/salty-kiwis-turn.md new file mode 100644 index 00000000..861aab3d --- /dev/null +++ b/.changeset/salty-kiwis-turn.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": patch +--- + +Pretty print spawn errors instead of simply rethrowing to commander. diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index b9070c31..7f67ffc6 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -217,9 +217,16 @@ program = program.action( } } catch (error) { if (error instanceof SpawnFailure) { + process.exitCode = 1; error.flushOutput("both"); + if (baseOptions.verbose) { + console.error( + `\nFailed running: ${chalk.dim(error.command, ...error.args)}\n`, + ); + } + } else { + throw error; } - throw error; } }, ); From 2ecf8946924d41400095e3a106eb67e7aef068dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 19 Sep 2025 18:28:02 +0200 Subject: [PATCH 02/82] Pass definitions to cmake (#239) --- .changeset/shaggy-dots-deny.md | 5 ++++ packages/cmake-rn/src/cli.ts | 47 +++++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 .changeset/shaggy-dots-deny.md diff --git a/.changeset/shaggy-dots-deny.md b/.changeset/shaggy-dots-deny.md new file mode 100644 index 00000000..1d7139be --- /dev/null +++ b/.changeset/shaggy-dots-deny.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": minor +--- + +Add passing of definitions (-D) to cmake when configuring diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 7f67ffc6..2a0324a5 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -74,6 +74,26 @@ const outPathOption = new Option( "Specify the output directory to store the final build artifacts", ).default(false, "./{build}/{configuration}"); +const defineOption = new Option( + "-D,--define ", + "Define cache variables passed when configuring projects", +).argParser>( + (input, previous = {}) => { + // TODO: Implement splitting of value using a regular expression (using named groups) for the format [:]= + // and return an object keyed by variable name with the string value as value or alternatively an array of [value, type] + const match = input.match( + /^(?[^:=]+)(:(?[^=]+))?=(?.+)$/, + ); + if (!match || !match.groups) { + throw new Error( + `Invalid format for -D/--define argument: ${input}. Expected [:]=`, + ); + } + const { name, type, value } = match.groups; + return { ...previous, [name]: type ? { value, type } : value }; + }, +); + const noAutoLinkOption = new Option( "--no-auto-link", "Don't mark the output as auto-linkable by react-native-node-api", @@ -92,6 +112,7 @@ let program = new Command("cmake-rn") .addOption(buildPathOption) .addOption(outPathOption) .addOption(configurationOption) + .addOption(defineOption) .addOption(cleanOption) .addOption(noAutoLinkOption) .addOption(noWeakNodeApiLinkageOption); @@ -269,14 +290,15 @@ async function configureProject( const { target, buildPath, outputPath } = context; const { verbose, source, weakNodeApiLinkage } = options; - const nodeApiVariables = + const nodeApiDefinitions = weakNodeApiLinkage && isSupportedTriplet(target) ? getWeakNodeApiVariables(target) : // TODO: Make this a part of the platform definition {}; - const declarations = { - ...nodeApiVariables, + const definitions = { + ...nodeApiDefinitions, + ...options.define, CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath, }; @@ -288,7 +310,7 @@ async function configureProject( "-B", buildPath, ...platform.configureArgs(context, options), - ...toDeclarationArguments(declarations), + ...toDefineArguments(definitions), ], { outputMode: verbose ? "inherit" : "buffered", @@ -321,11 +343,18 @@ async function buildProject( ); } -function toDeclarationArguments(declarations: Record) { - return Object.entries(declarations).flatMap(([key, value]) => [ - "-D", - `${key}=${value}`, - ]); +type CmakeTypedDefinition = { value: string; type: string }; + +function toDefineArguments( + declarations: Record, +) { + return Object.entries(declarations).flatMap(([key, definition]) => { + if (typeof definition === "string") { + return ["-D", `${key}=${definition}`]; + } else { + return ["-D", `${key}:${definition.type}=${definition.value}`]; + } + }); } export { program }; From 2a30d8d39227a5bda415b4089095ffad2cbbe166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 19 Sep 2025 21:13:11 +0200 Subject: [PATCH 03/82] Refactor CLIs into using a shared utility package (#241) * Add cli-util package * Use cli-util in all CLIs * Add changeset * Reverting f82239c1f86dfc66d155fa21d9fcffdf63435153 --- .changeset/fresh-nails-repair.md | 8 + configs/tsconfig.cli.json | 12 + package-lock.json | 879 ++++++------------- package.json | 1 + packages/cli-utils/package.json | 14 + packages/cli-utils/src/actions.ts | 50 ++ packages/{ferric => cli-utils}/src/errors.ts | 0 packages/cli-utils/src/index.ts | 7 + packages/cli-utils/tsconfig.json | 3 + packages/cmake-rn/package.json | 6 +- packages/cmake-rn/src/cli.ts | 205 ++--- packages/cmake-rn/src/platforms/android.ts | 4 +- packages/cmake-rn/src/platforms/apple.ts | 4 +- packages/cmake-rn/src/platforms/types.ts | 9 +- packages/ferric/package.json | 8 +- packages/ferric/src/banner.ts | 2 +- packages/ferric/src/build.ts | 333 ++++--- packages/ferric/src/cargo.ts | 12 +- packages/ferric/src/program.ts | 2 +- packages/ferric/src/run.ts | 1 + packages/ferric/src/rustup.ts | 4 +- packages/ferric/src/targets.ts | 4 +- packages/gyp-to-cmake/package.json | 3 +- packages/gyp-to-cmake/src/cli.ts | 3 +- packages/gyp-to-cmake/src/run.ts | 3 +- packages/host/package.json | 6 +- packages/host/src/node/cli/apple.ts | 2 +- packages/host/src/node/cli/hermes.ts | 9 +- packages/host/src/node/cli/link-modules.ts | 4 +- packages/host/src/node/cli/options.ts | 2 +- packages/host/src/node/cli/program.ts | 10 +- packages/host/src/node/cli/run.ts | 1 + packages/host/src/node/path-utils.ts | 6 +- packages/host/src/node/prebuilds/apple.ts | 2 +- tsconfig.json | 1 + 35 files changed, 657 insertions(+), 963 deletions(-) create mode 100644 .changeset/fresh-nails-repair.md create mode 100644 configs/tsconfig.cli.json create mode 100644 packages/cli-utils/package.json create mode 100644 packages/cli-utils/src/actions.ts rename packages/{ferric => cli-utils}/src/errors.ts (100%) create mode 100644 packages/cli-utils/src/index.ts create mode 100644 packages/cli-utils/tsconfig.json diff --git a/.changeset/fresh-nails-repair.md b/.changeset/fresh-nails-repair.md new file mode 100644 index 00000000..2de1fa30 --- /dev/null +++ b/.changeset/fresh-nails-repair.md @@ -0,0 +1,8 @@ +--- +"gyp-to-cmake": patch +"cmake-rn": patch +"ferric-cli": patch +"react-native-node-api": patch +--- + +Refactored CLIs to use a shared utility package diff --git a/configs/tsconfig.cli.json b/configs/tsconfig.cli.json new file mode 100644 index 00000000..4e77cced --- /dev/null +++ b/configs/tsconfig.cli.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "composite": true, + "declarationMap": true, + "outDir": "${configDir}/dist", + "rootDir": "${configDir}/src", + "types": ["node"] + }, + "include": ["${configDir}/src/*.ts"], + "exclude": ["${configDir}/**.test.ts"] +} diff --git a/package-lock.json b/package-lock.json index 98108da6..96a644fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "license": "MIT", "workspaces": [ "apps/test-app", + "packages/cli-utils", "packages/gyp-to-cmake", "packages/cmake-rn", "packages/ferric", @@ -5404,63 +5405,6 @@ "yaml": "^2.2.1" } }, - "node_modules/@react-native-community/cli-doctor/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-native-community/cli-doctor/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@react-native-community/cli-doctor/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -5473,12 +5417,6 @@ "node": ">=10" } }, - "node_modules/@react-native-community/cli-doctor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/@react-native-community/cli-platform-android": { "version": "20.0.2", "resolved": "https://registry.npmjs.org/@react-native-community/cli-platform-android/-/cli-platform-android-20.0.2.tgz", @@ -5550,18 +5488,6 @@ "semver": "^7.5.2" } }, - "node_modules/@react-native-community/cli-tools/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@react-native-community/cli-tools/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5578,15 +5504,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@react-native-community/cli-tools/node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@react-native-community/cli-tools/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5602,29 +5519,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@react-native-community/cli-tools/node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@react-native-community/cli-tools/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5655,19 +5549,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@react-native-community/cli-tools/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@react-native-community/cli-tools/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -5680,12 +5561,6 @@ "node": ">=10" } }, - "node_modules/@react-native-community/cli-tools/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/@react-native-community/cli-tools/node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -5806,6 +5681,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@react-native-node-api/cli-utils": { + "resolved": "packages/cli-utils", + "link": true + }, "node_modules/@react-native-node-api/ferric-example": { "resolved": "packages/ferric-example", "link": true @@ -6678,9 +6557,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.18.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.5.tgz", - "integrity": "sha512-g9BpPfJvxYBXUWI9bV37j6d6LTMNQ88hPwdWWUeYZnMhlo66FIg9gCc1/DZb15QylJSKwOZjwrckvOTWpOiChg==", + "version": "22.18.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz", + "integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -7943,18 +7822,15 @@ } }, "node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "license": "MIT", "dependencies": { - "restore-cursor": "^5.0.0" + "restore-cursor": "^3.1.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/cli-spinners": { @@ -8007,35 +7883,6 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -8647,9 +8494,9 @@ } }, "node_modules/emoji-regex": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", - "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/encodeurl": { @@ -9624,23 +9471,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/gauge/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/gauge/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/gauge/node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -9648,21 +9478,6 @@ "dev": true, "license": "ISC" }, - "node_modules/gauge/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -10295,15 +10110,12 @@ } }, "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/is-nan": { @@ -10923,35 +10735,6 @@ "node": ">=0.10.0" } }, - "node_modules/logkitty/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/logkitty/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/logkitty/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/logkitty/node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -12247,107 +12030,28 @@ } }, "node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "license": "MIT", "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ora/node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/outdent": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", @@ -13317,35 +13021,23 @@ } }, "node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "license": "MIT", "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" }, "node_modules/reusify": { "version": "1.1.0", @@ -13965,20 +13657,17 @@ } }, "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/string-width-cjs": { @@ -13996,12 +13685,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -14011,31 +13694,13 @@ "node": ">=8" } }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=8" } }, "node_modules/strip-ansi": { @@ -14722,38 +14387,6 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "node_modules/wide-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wide-align/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wide-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -14802,64 +14435,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -14963,35 +14538,6 @@ "node": ">=10" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", @@ -15026,34 +14572,39 @@ "url": "https://github.com/sponsors/colinhacks" } }, - "packages/cmake-rn": { - "version": "0.3.1", + "packages/cli-utils": { + "name": "@react-native-node-api/cli-utils", + "version": "0.1.0", "dependencies": { - "@commander-js/extra-typings": "^13.1.0", + "@commander-js/extra-typings": "^14.0.0", "bufout": "^0.3.2", "chalk": "^5.4.1", - "commander": "^13.1.0", - "ora": "^8.2.0", - "react-native-node-api": "0.4.0" - }, - "bin": { - "cmake-rn": "bin/cmake-rn.js" - }, - "peerDependencies": { - "node-addon-api": "^8.3.1", - "node-api-headers": "^1.5.0" + "commander": "^14.0.1", + "ora": "^8.2.0" } }, - "packages/cmake-rn/node_modules/@commander-js/extra-typings": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-13.1.0.tgz", - "integrity": "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==", + "packages/cli-utils/node_modules/@commander-js/extra-typings": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-14.0.0.tgz", + "integrity": "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg==", "license": "MIT", "peerDependencies": { - "commander": "~13.1.0" + "commander": "~14.0.0" } }, - "packages/cmake-rn/node_modules/chalk": { + "packages/cli-utils/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "packages/cli-utils/node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", @@ -15065,107 +14616,223 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "packages/cmake-rn/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "packages/cli-utils/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/ferric": { - "name": "ferric-cli", - "version": "0.3.1", - "dependencies": { - "@commander-js/extra-typings": "^13.1.0", - "@napi-rs/cli": "~3.0.3", - "bufout": "^0.3.2", - "chalk": "^5.4.1", - "commander": "^13.1.0", - "ora": "^8.2.0", - "react-native-node-api": "0.4.0" + "packages/cli-utils/node_modules/commander": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", + "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "packages/cli-utils/node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "license": "MIT" + }, + "packages/cli-utils/node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" }, - "bin": { - "ferric": "bin/ferric.js" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/ferric-example": { - "name": "@react-native-node-api/ferric-example", - "version": "0.1.1", - "devDependencies": { - "ferric-cli": "*" + "packages/cli-utils/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/ferric/node_modules/@commander-js/extra-typings": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-13.1.0.tgz", - "integrity": "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==", + "packages/cli-utils/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", "license": "MIT", - "peerDependencies": { - "commander": "~13.1.0" + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/ferric/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "packages/cli-utils/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "license": "MIT", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/ferric/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "packages/cli-utils/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/gyp-to-cmake": { - "version": "0.2.0", + "packages/cli-utils/node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", "dependencies": { - "@commander-js/extra-typings": "^13.1.0", - "commander": "^13.1.0", - "gyp-parser": "^1.0.4" + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" }, - "bin": { - "gyp-to-cmake": "bin/gyp-to-cmake.js" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/gyp-to-cmake/node_modules/@commander-js/extra-typings": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-13.1.0.tgz", - "integrity": "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==", + "packages/cli-utils/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "license": "MIT", - "peerDependencies": { - "commander": "~13.1.0" + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/gyp-to-cmake/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "packages/cli-utils/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/cli-utils/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "packages/cmake-rn": { + "version": "0.3.2", + "dependencies": { + "@react-native-node-api/cli-utils": "0.1.0", + "react-native-node-api": "0.5.0" + }, + "bin": { + "cmake-rn": "bin/cmake-rn.js" + }, + "peerDependencies": { + "node-addon-api": "^8.3.1", + "node-api-headers": "^1.5.0" + } + }, + "packages/ferric": { + "name": "ferric-cli", + "version": "0.3.2", + "dependencies": { + "@napi-rs/cli": "~3.0.3", + "@react-native-node-api/cli-utils": "0.1.0", + "react-native-node-api": "0.5.0" + }, + "bin": { + "ferric": "bin/ferric.js" + } + }, + "packages/ferric-example": { + "name": "@react-native-node-api/ferric-example", + "version": "0.1.1", + "devDependencies": { + "ferric-cli": "*" + } + }, + "packages/gyp-to-cmake": { + "version": "0.2.0", + "dependencies": { + "@react-native-node-api/cli-utils": "0.1.0", + "gyp-parser": "^1.0.4" + }, + "bin": { + "gyp-to-cmake": "bin/gyp-to-cmake.js" } }, "packages/host": { "name": "react-native-node-api", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "dependencies": { - "@commander-js/extra-typings": "^13.1.0", - "bufout": "^0.3.2", - "chalk": "^5.4.1", - "commander": "^13.1.0", - "ora": "^8.2.0", + "@react-native-node-api/cli-utils": "0.1.0", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1" }, @@ -15184,36 +14851,6 @@ "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4" } }, - "packages/host/node_modules/@commander-js/extra-typings": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-13.1.0.tgz", - "integrity": "sha512-q5P52BYb1hwVWE6dtID7VvuJWrlfbCv4klj7BjUUOqMz4jbSZD4C9fJ9lRjL2jnBGTg+gDDlaXN51rkWcLk4fg==", - "license": "MIT", - "peerDependencies": { - "commander": "~13.1.0" - } - }, - "packages/host/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "packages/host/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "packages/node-addon-examples": { "name": "@react-native-node-api/node-addon-examples", "dependencies": { @@ -15232,7 +14869,7 @@ "cmake-rn": "*", "gyp-to-cmake": "*", "prebuildify": "^6.0.1", - "react-native-node-api": "^0.4.0", + "react-native-node-api": "^0.5.0", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } diff --git a/package.json b/package.json index 083d3d02..c20b5813 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "private": true, "workspaces": [ "apps/test-app", + "packages/cli-utils", "packages/gyp-to-cmake", "packages/cmake-rn", "packages/ferric", diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json new file mode 100644 index 00000000..bd86f925 --- /dev/null +++ b/packages/cli-utils/package.json @@ -0,0 +1,14 @@ +{ + "name": "@react-native-node-api/cli-utils", + "version": "0.1.0", + "description": "Useful utilities for the CLIs in the React Native Node API mono-repo", + "type": "module", + "main": "dist/index.js", + "dependencies": { + "@commander-js/extra-typings": "^14.0.0", + "bufout": "^0.3.2", + "chalk": "^5.4.1", + "commander": "^14.0.1", + "ora": "^8.2.0" + } +} diff --git a/packages/cli-utils/src/actions.ts b/packages/cli-utils/src/actions.ts new file mode 100644 index 00000000..8cd15a03 --- /dev/null +++ b/packages/cli-utils/src/actions.ts @@ -0,0 +1,50 @@ +import { SpawnFailure } from "bufout"; +import chalk from "chalk"; +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) { + try { + await fn.call(this, ...args); + } catch (error) { + process.exitCode = 1; + if (error instanceof SpawnFailure) { + error.flushOutput("both"); + } + 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); + } + if (error instanceof UsageError && error.fix) { + console.error( + chalk.green("FIX"), + error.fix.command + ? chalk.dim("Run: ") + error.fix.command + : error.fix.instructions, + ); + } + } else { + throw error; + } + } + }; +} + +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/ferric/src/errors.ts b/packages/cli-utils/src/errors.ts similarity index 100% rename from packages/ferric/src/errors.ts rename to packages/cli-utils/src/errors.ts diff --git a/packages/cli-utils/src/index.ts b/packages/cli-utils/src/index.ts new file mode 100644 index 00000000..bf9232fa --- /dev/null +++ b/packages/cli-utils/src/index.ts @@ -0,0 +1,7 @@ +export * from "@commander-js/extra-typings"; +export { default as chalk } from "chalk"; +export * from "ora"; +export * from "bufout"; + +export * from "./actions.js"; +export * from "./errors.js"; diff --git a/packages/cli-utils/tsconfig.json b/packages/cli-utils/tsconfig.json new file mode 100644 index 00000000..aa43e9d9 --- /dev/null +++ b/packages/cli-utils/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../configs/tsconfig.cli.json" +} diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index 694cfe84..74b24aea 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -22,11 +22,7 @@ "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" }, "dependencies": { - "@commander-js/extra-typings": "^13.1.0", - "bufout": "^0.3.2", - "chalk": "^5.4.1", - "commander": "^13.1.0", - "ora": "^8.2.0", + "@react-native-node-api/cli-utils": "0.1.0", "react-native-node-api": "0.5.0" }, "peerDependencies": { diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 2a0324a5..477f5feb 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -3,10 +3,14 @@ import path from "node:path"; import fs from "node:fs"; import { EventEmitter } from "node:events"; -import { Command, Option } from "@commander-js/extra-typings"; -import { spawn, SpawnFailure } from "bufout"; -import { oraPromise } from "ora"; -import chalk from "chalk"; +import { + chalk, + Command, + Option, + spawn, + oraPromise, +} from "@react-native-node-api/cli-utils"; +import { isSupportedTriplet } from "react-native-node-api"; import { getWeakNodeApiVariables } from "./weak-node-api.js"; import { @@ -16,7 +20,6 @@ import { platformHasTarget, } from "./platforms.js"; import { BaseOpts, TargetContext, Platform } from "./platforms/types.js"; -import { isSupportedTriplet } from "react-native-node-api"; // We're attaching a lot of listeners when spawning in parallel EventEmitter.defaultMaxListeners = 100; @@ -128,126 +131,110 @@ for (const platform of platforms) { program = program.action( async ({ target: requestedTargets, ...baseOptions }) => { - try { - const buildPath = getBuildPath(baseOptions); - if (baseOptions.clean) { - await fs.promises.rm(buildPath, { recursive: true, force: true }); + const buildPath = getBuildPath(baseOptions); + if (baseOptions.clean) { + await fs.promises.rm(buildPath, { recursive: true, force: true }); + } + const targets = new Set(requestedTargets); + + for (const platform of Object.values(platforms)) { + // Forcing the types a bit here, since the platform id option is dynamically added + if ((baseOptions as Record)[platform.id]) { + for (const target of platform.targets) { + targets.add(target); + } } - const targets = new Set(requestedTargets); + } + if (targets.size === 0) { for (const platform of Object.values(platforms)) { - // Forcing the types a bit here, since the platform id option is dynamically added - if ((baseOptions as Record)[platform.id]) { - for (const target of platform.targets) { + if (platform.isSupportedByHost()) { + for (const target of await platform.defaultTargets()) { targets.add(target); } } } - if (targets.size === 0) { - for (const platform of Object.values(platforms)) { - if (platform.isSupportedByHost()) { - for (const target of await platform.defaultTargets()) { - targets.add(target); - } - } - } - if (targets.size === 0) { - throw new Error( - "Found no default targets: Install some platform specific build tools", - ); - } else { - console.error( - chalk.yellowBright("ℹ"), - "Using default targets", - chalk.dim("(" + [...targets].join(", ") + ")"), - ); - } + throw new Error( + "Found no default targets: Install some platform specific build tools", + ); + } else { + console.error( + chalk.yellowBright("ℹ"), + "Using default targets", + chalk.dim("(" + [...targets].join(", ") + ")"), + ); } + } - if (!baseOptions.out) { - baseOptions.out = path.join(buildPath, baseOptions.configuration); - } + if (!baseOptions.out) { + baseOptions.out = path.join(buildPath, baseOptions.configuration); + } - const targetContexts = [...targets].map((target) => { - const platform = findPlatformForTarget(target); - const targetBuildPath = getTargetBuildPath(buildPath, target); - return { - target, - platform, - buildPath: targetBuildPath, - outputPath: path.join(targetBuildPath, "out"), - options: baseOptions, - }; - }); - - // Configure every triplet project - const targetsSummary = chalk.dim( - `(${getTargetsSummary(targetContexts)})`, - ); - await oraPromise( - Promise.all( - targetContexts.map(({ platform, ...context }) => - configureProject(platform, context, baseOptions), - ), + const targetContexts = [...targets].map((target) => { + const platform = findPlatformForTarget(target); + const targetBuildPath = getTargetBuildPath(buildPath, target); + return { + target, + platform, + buildPath: targetBuildPath, + outputPath: path.join(targetBuildPath, "out"), + options: baseOptions, + }; + }); + + // Configure every triplet project + const targetsSummary = chalk.dim(`(${getTargetsSummary(targetContexts)})`); + await oraPromise( + Promise.all( + targetContexts.map(({ platform, ...context }) => + configureProject(platform, context, baseOptions), ), - { - text: `Configuring projects ${targetsSummary}`, - isSilent: baseOptions.verbose, - successText: `Configured projects ${targetsSummary}`, - failText: ({ message }) => `Failed to configure projects: ${message}`, - }, - ); + ), + { + text: `Configuring projects ${targetsSummary}`, + isSilent: baseOptions.verbose, + successText: `Configured projects ${targetsSummary}`, + failText: ({ message }) => `Failed to configure projects: ${message}`, + }, + ); - // Build every triplet project - await oraPromise( - Promise.all( - targetContexts.map(async ({ platform, ...context }) => { - // Delete any stale build artifacts before building - // This is important, since we might rename the output files - await fs.promises.rm(context.outputPath, { - recursive: true, - force: true, - }); - await buildProject(platform, context, baseOptions); - }), - ), + // Build every triplet project + await oraPromise( + Promise.all( + targetContexts.map(async ({ platform, ...context }) => { + // Delete any stale build artifacts before building + // This is important, since we might rename the output files + await fs.promises.rm(context.outputPath, { + recursive: true, + force: true, + }); + await buildProject(platform, context, baseOptions); + }), + ), + { + text: "Building projects", + isSilent: baseOptions.verbose, + successText: "Built projects", + failText: ({ message }) => `Failed to build projects: ${message}`, + }, + ); + + // Perform post-build steps for each platform in sequence + for (const platform of platforms) { + const relevantTargets = targetContexts.filter(({ target }) => + platformHasTarget(platform, target), + ); + if (relevantTargets.length == 0) { + continue; + } + await platform.postBuild( { - text: "Building projects", - isSilent: baseOptions.verbose, - successText: "Built projects", - failText: ({ message }) => `Failed to build projects: ${message}`, + outputPath: baseOptions.out || baseOptions.source, + targets: relevantTargets, }, + baseOptions, ); - - // Perform post-build steps for each platform in sequence - for (const platform of platforms) { - const relevantTargets = targetContexts.filter(({ target }) => - platformHasTarget(platform, target), - ); - if (relevantTargets.length == 0) { - continue; - } - await platform.postBuild( - { - outputPath: baseOptions.out || baseOptions.source, - targets: relevantTargets, - }, - baseOptions, - ); - } - } catch (error) { - if (error instanceof SpawnFailure) { - process.exitCode = 1; - error.flushOutput("both"); - if (baseOptions.verbose) { - console.error( - `\nFailed running: ${chalk.dim(error.command, ...error.args)}\n`, - ); - } - } else { - throw error; - } } }, ); diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index 63397aa0..ef71afe1 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -import { Option } from "@commander-js/extra-typings"; +import { Option, oraPromise, chalk } from "@react-native-node-api/cli-utils"; import { createAndroidLibsDirectory, determineAndroidLibsFilename, @@ -10,8 +10,6 @@ import { } from "react-native-node-api"; import type { Platform } from "./types.js"; -import { oraPromise } from "ora"; -import chalk from "chalk"; // This should match https://github.com/react-native-community/template/blob/main/template/android/build.gradle#L7 const DEFAULT_NDK_VERSION = "27.1.12297006"; diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index ce091dc4..c92c5b9c 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -2,8 +2,7 @@ import assert from "node:assert/strict"; import path from "node:path"; import fs from "node:fs"; -import { Option } from "@commander-js/extra-typings"; -import { oraPromise } from "ora"; +import { Option, oraPromise, chalk } from "@react-native-node-api/cli-utils"; import { AppleTriplet as Target, createAppleFramework, @@ -12,7 +11,6 @@ import { } from "react-native-node-api"; import type { Platform } from "./types.js"; -import chalk from "chalk"; type XcodeSDKName = | "iphoneos" diff --git a/packages/cmake-rn/src/platforms/types.ts b/packages/cmake-rn/src/platforms/types.ts index fb7c8f5f..bd3e06a2 100644 --- a/packages/cmake-rn/src/platforms/types.ts +++ b/packages/cmake-rn/src/platforms/types.ts @@ -1,12 +1,13 @@ -import * as commander from "@commander-js/extra-typings"; +import * as cli from "@react-native-node-api/cli-utils"; + import type { program } from "../cli.js"; -type InferOptionValues = ReturnType< +type InferOptionValues = ReturnType< Command["opts"] >; type BaseCommand = typeof program; -type ExtendedCommand = commander.Command< +type ExtendedCommand = cli.Command< [], Opts & InferOptionValues, Record // Global opts are not supported @@ -22,7 +23,7 @@ export type TargetContext = { export type Platform< Targets extends string[] = string[], - Opts extends commander.OptionValues = Record, + Opts extends cli.OptionValues = Record, Command = ExtendedCommand, > = { /** diff --git a/packages/ferric/package.json b/packages/ferric/package.json index 1db23db9..85b8fef6 100644 --- a/packages/ferric/package.json +++ b/packages/ferric/package.json @@ -17,11 +17,7 @@ }, "dependencies": { "@napi-rs/cli": "~3.0.3", - "@commander-js/extra-typings": "^13.1.0", - "bufout": "^0.3.2", - "chalk": "^5.4.1", - "commander": "^13.1.0", - "react-native-node-api": "0.5.0", - "ora": "^8.2.0" + "@react-native-node-api/cli-utils": "0.1.0", + "react-native-node-api": "0.5.0" } } diff --git a/packages/ferric/src/banner.ts b/packages/ferric/src/banner.ts index 820b681d..8bf44cc5 100644 --- a/packages/ferric/src/banner.ts +++ b/packages/ferric/src/banner.ts @@ -1,4 +1,4 @@ -import chalk from "chalk"; +import { chalk } from "@react-native-node-api/cli-utils"; const LINES = [ // Pagga on https://www.asciiart.eu/text-to-ascii-art diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index aaf97d77..ec8992ec 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -1,10 +1,13 @@ import path from "node:path"; import fs from "node:fs"; -import { Command, Option } from "@commander-js/extra-typings"; -import chalk from "chalk"; -import { SpawnFailure } from "bufout"; -import { oraPromise } from "ora"; +import { + chalk, + Command, + Option, + oraPromise, + assertFixable, +} from "@react-native-node-api/cli-utils"; import { determineAndroidLibsFilename, @@ -18,7 +21,6 @@ import { prettyPath, } from "react-native-node-api"; -import { UsageError, assertFixable } from "./errors.js"; import { ensureCargo, build } from "./cargo.js"; import { ALL_TARGETS, @@ -123,208 +125,185 @@ export const buildCommand = new Command("build") configuration, xcframeworkExtension, }) => { - try { - const targets = new Set([...targetArg]); - if (apple) { - for (const target of APPLE_TARGETS) { - targets.add(target); - } + 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.`, - ), - ); } - ensureCargo(); - ensureInstalledTargets(targets); + 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); - 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) => - [ + ), + Promise.all( + androidTargets.map( + async (target) => + [ + target, + await build({ + configuration, target, - await build({ - configuration, - target, - ndkVersion, - androidApiLevel: ANDROID_API_LEVEL, - }), - ] as const, - ), + ndkVersion, + androidApiLevel: ANDROID_API_LEVEL, + }), + ] as const, ), - ]), - { - 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; - - 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", - ); + ), + ]), + { + text: `Building ${targetsDescription}`, + successText: `Built ${targetsDescription}`, + failText: (error: Error) => `Failed to build: ${error.message}`, + }, + ); - // Create the xcframework - const xcframeworkOutputPath = path.resolve( + if (androidLibraries.length > 0) { + const libraryPathByTriplet = Object.fromEntries( + androidLibraries.map(([target, outputPath]) => [ + ANDROID_TRIPLET_PER_TARGET[target], 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}`, - }, - ); - } + ]), + ) as Record; - const libraryName = determineLibraryBasename([ - ...androidLibraries.map(([, outputPath]) => outputPath), - ...appleLibraries.map(([, outputPath]) => outputPath), - ]); + const androidLibsFilename = determineAndroidLibsFilename( + Object.values(libraryPathByTriplet), + ); + const androidLibsOutputPath = path.resolve( + outputPath, + androidLibsFilename, + ); - const declarationsFilename = `${libraryName}.d.ts`; - const declarationsPath = path.join(outputPath, declarationsFilename); await oraPromise( - generateTypeScriptDeclarations({ - outputFilename: declarationsFilename, - createPath: process.cwd(), - outputPath, + createAndroidLibsDirectory({ + outputPath: androidLibsOutputPath, + libraryPathByTriplet, + autoLink: true, }), { - text: "Generating TypeScript declarations", - successText: `Generated TypeScript declarations ${prettyPath( - declarationsPath, + text: "Assembling Android libs directory", + successText: `Android libs directory assembled into ${prettyPath( + androidLibsOutputPath, )}`, - failText: (error) => - `Failed to generate TypeScript declarations: ${error.message}`, + failText: ({ message }) => + `Failed to assemble Android libs directory: ${message}`, }, ); + } - const entrypointPath = path.join(outputPath, `${libraryName}.js`); + 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( - generateEntrypoint({ - libraryName, - outputPath: entrypointPath, + createXCframework({ + outputPath: xcframeworkOutputPath, + frameworkPaths, + autoLink: true, }), { - text: `Generating entrypoint`, - successText: `Generated entrypoint into ${prettyPath( - entrypointPath, + text: "Assembling XCFramework", + successText: `XCFramework assembled into ${chalk.dim( + path.relative(process.cwd(), xcframeworkOutputPath), )}`, - failText: (error) => - `Failed to generate entrypoint: ${error.message}`, + failText: ({ message }) => + `Failed to assemble XCFramework: ${message}`, }, ); - } catch (error) { - process.exitCode = 1; - if (error instanceof SpawnFailure) { - error.flushOutput("both"); - } - 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); - } - if (error instanceof UsageError && error.fix) { - console.error( - chalk.green("FIX"), - error.fix.command - ? chalk.dim("Run: ") + error.fix.command - : error.fix.instructions, - ); - } - } else { - throw error; - } } + + 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}`, + }, + ); }, ); diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index da4e8be9..edc88481 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -3,10 +3,14 @@ import cp from "node:child_process"; import fs from "node:fs"; import path from "node:path"; -import { spawn } from "bufout"; -import chalk from "chalk"; +import { + chalk, + assertFixable, + UsageError, + spawn, +} from "@react-native-node-api/cli-utils"; +import { weakNodeApiPath } from "react-native-node-api"; -import { assertFixable, UsageError } from "./errors.js"; import { AndroidTargetName, AppleTargetName, @@ -14,8 +18,6 @@ import { isAppleTarget, } from "./targets.js"; -import { weakNodeApiPath } from "react-native-node-api"; - const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record = { "aarch64-apple-darwin": "macos-arm64_x86_64", // Universal "x86_64-apple-darwin": "macos-arm64_x86_64", // Universal diff --git a/packages/ferric/src/program.ts b/packages/ferric/src/program.ts index f8059c48..422c4ecc 100644 --- a/packages/ferric/src/program.ts +++ b/packages/ferric/src/program.ts @@ -1,4 +1,4 @@ -import { Command } from "@commander-js/extra-typings"; +import { Command } from "@react-native-node-api/cli-utils"; import { printBanner } from "./banner.js"; import { buildCommand } from "./build.js"; diff --git a/packages/ferric/src/run.ts b/packages/ferric/src/run.ts index 7b390d6c..01311284 100644 --- a/packages/ferric/src/run.ts +++ b/packages/ferric/src/run.ts @@ -1,4 +1,5 @@ import EventEmitter from "node:events"; + import { program } from "./program.js"; // We're attaching a lot of listeners when spawning in parallel diff --git a/packages/ferric/src/rustup.ts b/packages/ferric/src/rustup.ts index b246c0c0..11064dd1 100644 --- a/packages/ferric/src/rustup.ts +++ b/packages/ferric/src/rustup.ts @@ -1,6 +1,6 @@ -import cp from "child_process"; +import cp from "node:child_process"; -import { UsageError } from "./errors.js"; +import { UsageError } from "@react-native-node-api/cli-utils"; export function getInstalledTargets() { try { diff --git a/packages/ferric/src/targets.ts b/packages/ferric/src/targets.ts index 5cc7d80f..b7696a10 100644 --- a/packages/ferric/src/targets.ts +++ b/packages/ferric/src/targets.ts @@ -1,6 +1,4 @@ -import chalk from "chalk"; - -import { UsageError } from "./errors.js"; +import { chalk, UsageError } from "@react-native-node-api/cli-utils"; import { getInstalledTargets } from "./rustup.js"; export const ANDROID_TARGETS = [ diff --git a/packages/gyp-to-cmake/package.json b/packages/gyp-to-cmake/package.json index a45bbcc8..e0b5acb4 100644 --- a/packages/gyp-to-cmake/package.json +++ b/packages/gyp-to-cmake/package.json @@ -22,8 +22,7 @@ "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" }, "dependencies": { - "@commander-js/extra-typings": "^13.1.0", - "commander": "^13.1.0", + "@react-native-node-api/cli-utils": "0.1.0", "gyp-parser": "^1.0.4" } } diff --git a/packages/gyp-to-cmake/src/cli.ts b/packages/gyp-to-cmake/src/cli.ts index 133bd6f9..5556c730 100644 --- a/packages/gyp-to-cmake/src/cli.ts +++ b/packages/gyp-to-cmake/src/cli.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -import { Command } from "@commander-js/extra-typings"; + +import { Command } from "@react-native-node-api/cli-utils"; import { readBindingFile } from "./gyp.js"; import { diff --git a/packages/gyp-to-cmake/src/run.ts b/packages/gyp-to-cmake/src/run.ts index c64a70b0..feff7eb2 100644 --- a/packages/gyp-to-cmake/src/run.ts +++ b/packages/gyp-to-cmake/src/run.ts @@ -1,2 +1,3 @@ import { program } from "./cli.js"; -program.parse(process.argv); + +program.parseAsync(process.argv).catch(console.error); diff --git a/packages/host/package.json b/packages/host/package.json index 9ddc3c4a..57d12c9f 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -78,11 +78,7 @@ ], "license": "MIT", "dependencies": { - "@commander-js/extra-typings": "^13.1.0", - "bufout": "^0.3.2", - "chalk": "^5.4.1", - "commander": "^13.1.0", - "ora": "^8.2.0", + "@react-native-node-api/cli-utils": "0.1.0", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1" }, diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index a04405cb..92fe6468 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -3,7 +3,7 @@ import path from "node:path"; import fs from "node:fs"; import os from "node:os"; -import { spawn } from "bufout"; +import { spawn } from "@react-native-node-api/cli-utils"; import { getLatestMtime, getLibraryName } from "../path-utils.js"; import { diff --git a/packages/host/src/node/cli/hermes.ts b/packages/host/src/node/cli/hermes.ts index 4fddebe6..f3c884b5 100644 --- a/packages/host/src/node/cli/hermes.ts +++ b/packages/host/src/node/cli/hermes.ts @@ -2,9 +2,12 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -import { Command } from "@commander-js/extra-typings"; -import { spawn, SpawnFailure } from "bufout"; -import { oraPromise } from "ora"; +import { + Command, + oraPromise, + spawn, + SpawnFailure, +} from "@react-native-node-api/cli-utils"; import { packageDirectorySync } from "pkg-dir"; import { prettyPath } from "../path-utils"; diff --git a/packages/host/src/node/cli/link-modules.ts b/packages/host/src/node/cli/link-modules.ts index 6053fc68..9b44d921 100644 --- a/packages/host/src/node/cli/link-modules.ts +++ b/packages/host/src/node/cli/link-modules.ts @@ -1,7 +1,8 @@ import path from "node:path"; import fs from "node:fs"; -import { SpawnFailure } from "bufout"; +import { chalk, SpawnFailure } from "@react-native-node-api/cli-utils"; + import { findNodeApiModulePathsByDependency, getAutolinkPath, @@ -11,7 +12,6 @@ import { PlatformName, prettyPath, } from "../path-utils"; -import chalk from "chalk"; export type ModuleLinker = ( options: LinkModuleOptions, diff --git a/packages/host/src/node/cli/options.ts b/packages/host/src/node/cli/options.ts index 1f81f306..0944f7c9 100644 --- a/packages/host/src/node/cli/options.ts +++ b/packages/host/src/node/cli/options.ts @@ -1,4 +1,4 @@ -import { Option } from "@commander-js/extra-typings"; +import { Option } from "@react-native-node-api/cli-utils"; import { assertPathSuffix, PATH_SUFFIX_CHOICES } from "../path-utils"; diff --git a/packages/host/src/node/cli/program.ts b/packages/host/src/node/cli/program.ts index 4747a23e..4e6b7bd7 100644 --- a/packages/host/src/node/cli/program.ts +++ b/packages/host/src/node/cli/program.ts @@ -2,10 +2,12 @@ import assert from "node:assert/strict"; import path from "node:path"; import { EventEmitter } from "node:stream"; -import { Command } from "@commander-js/extra-typings"; -import { SpawnFailure } from "bufout"; -import chalk from "chalk"; -import { oraPromise } from "ora"; +import { + Command, + chalk, + SpawnFailure, + oraPromise, +} from "@react-native-node-api/cli-utils"; import { determineModuleContext, diff --git a/packages/host/src/node/cli/run.ts b/packages/host/src/node/cli/run.ts index ee4c5620..da5566fe 100644 --- a/packages/host/src/node/cli/run.ts +++ b/packages/host/src/node/cli/run.ts @@ -1,2 +1,3 @@ import { program } from "./program"; + program.parseAsync(process.argv).catch(console.error); diff --git a/packages/host/src/node/path-utils.ts b/packages/host/src/node/path-utils.ts index ffec330a..1fe6170e 100644 --- a/packages/host/src/node/path-utils.ts +++ b/packages/host/src/node/path-utils.ts @@ -1,12 +1,14 @@ import assert from "node:assert/strict"; import path from "node:path"; import fs from "node:fs"; -import { findDuplicates } from "./duplicates"; -import chalk from "chalk"; import { packageDirectorySync } from "pkg-dir"; import { readPackageSync } from "read-pkg"; import { createRequire } from "node:module"; +import { chalk } from "@react-native-node-api/cli-utils"; + +import { findDuplicates } from "./duplicates"; + // TODO: Change to .apple.node export const PLATFORMS = ["android", "apple"] as const; export type PlatformName = "android" | "apple"; diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index 9b7765fb..693c90b6 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -4,7 +4,7 @@ import path from "node:path"; import os from "node:os"; import cp from "node:child_process"; -import { spawn } from "bufout"; +import { spawn } from "@react-native-node-api/cli-utils"; import { AppleTriplet } from "./triplets.js"; import { determineLibraryBasename } from "../path-utils.js"; diff --git a/tsconfig.json b/tsconfig.json index 77eba035..cde11ae1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ }, "files": ["prettier.config.js", "eslint.config.js"], "references": [ + { "path": "./packages/cli-utils/tsconfig.json" }, { "path": "./packages/host/tsconfig.json" }, { "path": "./packages/gyp-to-cmake/tsconfig.json" }, { "path": "./packages/cmake-rn/tsconfig.json" }, From 9861bad8d71e919b87bce73dc1d886c3e0dde2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 19 Sep 2025 21:34:47 +0200 Subject: [PATCH 04/82] Assert the existence of CMakeList.txt before passing control to CMake (#242) --- .changeset/social-rivers-tie.md | 5 +++++ packages/cmake-rn/src/cli.ts | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 .changeset/social-rivers-tie.md diff --git a/.changeset/social-rivers-tie.md b/.changeset/social-rivers-tie.md new file mode 100644 index 00000000..6748eb41 --- /dev/null +++ b/.changeset/social-rivers-tie.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": patch +--- + +Assert the existence of CMakeList.txt before passing control to CMake diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 477f5feb..d3f2e066 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -9,6 +9,7 @@ import { Option, spawn, oraPromise, + assertFixable, } from "@react-native-node-api/cli-utils"; import { isSupportedTriplet } from "react-native-node-api"; @@ -131,6 +132,14 @@ for (const platform of platforms) { program = program.action( async ({ target: requestedTargets, ...baseOptions }) => { + assertFixable( + fs.existsSync(path.join(baseOptions.source, "CMakeLists.txt")), + `No CMakeLists.txt found in source directory: ${chalk.dim(baseOptions.source)}`, + { + instructions: `Change working directory into a directory with a CMakeLists.txt, create one or specify the correct source directory using --source`, + }, + ); + const buildPath = getBuildPath(baseOptions); if (baseOptions.clean) { await fs.promises.rm(buildPath, { recursive: true, force: true }); From beff0f946b52b7f26796c7a80c280e985a189430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 19 Sep 2025 23:08:13 +0200 Subject: [PATCH 05/82] Refactor CLIs into using a shared utility package (more) (#244) * Export and use explicit wrapAction instead of monkey-patching commander * Flush output when SpawnFailures are cause * Update vendor-hermes to use UsageError --- packages/cli-utils/src/actions.ts | 35 ++- 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 | 35 ++- packages/host/src/node/cli/program.ts | 287 ++++++++++++----------- 6 files changed, 362 insertions(+), 357 deletions(-) diff --git a/packages/cli-utils/src/actions.ts b/packages/cli-utils/src/actions.ts index 8cd15a03..0df0b7d2 100644 --- a/packages/cli-utils/src/actions.ts +++ b/packages/cli-utils/src/actions.ts @@ -4,21 +4,32 @@ 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) { 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( @@ -34,17 +45,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..817b7524 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( @@ -91,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"); @@ -124,11 +121,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 a336f07998090d6b454aad16a4f4efdb77876862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 21 Sep 2025 11:33:07 +0200 Subject: [PATCH 06/82] Rename target to triplet (#243) * Rename target to triplet * Add changeset --- .changeset/rotten-melons-brush.md | 5 + .github/workflows/check.yml | 4 +- packages/cmake-rn/CHANGELOG.md | 2 +- packages/cmake-rn/src/cli.ts | 131 +++++++++++---------- packages/cmake-rn/src/platforms.test.ts | 24 ++-- packages/cmake-rn/src/platforms.ts | 18 +-- packages/cmake-rn/src/platforms/android.ts | 22 ++-- packages/cmake-rn/src/platforms/apple.ts | 26 ++-- packages/cmake-rn/src/platforms/types.ts | 28 ++--- 9 files changed, 136 insertions(+), 124 deletions(-) create mode 100644 .changeset/rotten-melons-brush.md diff --git a/.changeset/rotten-melons-brush.md b/.changeset/rotten-melons-brush.md new file mode 100644 index 00000000..cb313de4 --- /dev/null +++ b/.changeset/rotten-melons-brush.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": minor +--- + +Breaking: Renamed --target to --triplet to free up --target for passing CMake targets diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 53c7f734..1cf4f24e 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -98,7 +98,7 @@ jobs: - run: npm ci - run: npm run bootstrap env: - CMAKE_RN_TARGETS: arm64-apple-ios-sim + CMAKE_RN_TRIPLETS: arm64-apple-ios-sim FERRIC_TARGETS: aarch64-apple-ios-sim - run: npm run pod-install working-directory: apps/test-app @@ -129,7 +129,7 @@ jobs: - run: npm ci - run: npm run bootstrap env: - CMAKE_RN_TARGETS: i686-linux-android + CMAKE_RN_TRIPLETS: i686-linux-android FERRIC_TARGETS: i686-linux-android - name: Clone patched Hermes version shell: bash diff --git a/packages/cmake-rn/CHANGELOG.md b/packages/cmake-rn/CHANGELOG.md index 90871c10..fcfd8ab2 100644 --- a/packages/cmake-rn/CHANGELOG.md +++ b/packages/cmake-rn/CHANGELOG.md @@ -22,7 +22,7 @@ ### Minor Changes -- 8557768: Derive default targets from the CMAKE_RN_TARGETS environment variable +- 8557768: Derive default targets from the CMAKE_RN_TRIPLETS environment variable ### Patch Changes diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 6c102f74..8a8901ca 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -17,11 +17,11 @@ import { isSupportedTriplet } from "react-native-node-api"; import { getWeakNodeApiVariables } from "./weak-node-api.js"; import { platforms, - allTargets, - findPlatformForTarget, - platformHasTarget, + allTriplets as allTriplets, + findPlatformForTriplet, + platformHasTriplet, } from "./platforms.js"; -import { BaseOpts, TargetContext, Platform } from "./platforms/types.js"; +import { BaseOpts, TripletContext, Platform } from "./platforms/types.js"; // We're attaching a lot of listeners when spawning in parallel EventEmitter.defaultMaxListeners = 100; @@ -43,25 +43,28 @@ const configurationOption = new Option("--configuration ") .choices(["Release", "Debug"] as const) .default("Release"); -// TODO: Derive default targets +// TODO: Derive default build triplets // This is especially important when driving the build from within a React Native app package. -const { CMAKE_RN_TARGETS } = process.env; +const { CMAKE_RN_TRIPLETS } = process.env; -const defaultTargets = CMAKE_RN_TARGETS ? CMAKE_RN_TARGETS.split(",") : []; +const defaultTriplets = CMAKE_RN_TRIPLETS ? CMAKE_RN_TRIPLETS.split(",") : []; -for (const target of defaultTargets) { +for (const triplet of defaultTriplets) { assert( - (allTargets as string[]).includes(target), - `Unexpected target in CMAKE_RN_TARGETS: ${target}`, + (allTriplets as string[]).includes(triplet), + `Unexpected triplet in CMAKE_RN_TRIPLETS: ${triplet}`, ); } -const targetOption = new Option("--target ", "Targets to build for") - .choices(allTargets) +const tripletOption = new Option( + "--triplet ", + "Triplets to build for", +) + .choices(allTriplets) .default( - defaultTargets, - "CMAKE_RN_TARGETS environment variable split by ','", + defaultTriplets, + "CMAKE_RN_TRIPLETS environment variable split by ','", ); const buildPathOption = new Option( @@ -111,7 +114,7 @@ const noWeakNodeApiLinkageOption = new Option( let program = new Command("cmake-rn") .description("Build React Native Node API modules with CMake") - .addOption(targetOption) + .addOption(tripletOption) .addOption(verboseOption) .addOption(sourcePathOption) .addOption(buildPathOption) @@ -132,7 +135,7 @@ for (const platform of platforms) { } program = program.action( - wrapAction(async ({ target: requestedTargets, ...baseOptions }) => { + wrapAction(async ({ triplet: requestedTriplets, ...baseOptions }) => { assertFixable( fs.existsSync(path.join(baseOptions.source, "CMakeLists.txt")), `No CMakeLists.txt found in source directory: ${chalk.dim(baseOptions.source)}`, @@ -145,34 +148,34 @@ program = program.action( if (baseOptions.clean) { await fs.promises.rm(buildPath, { recursive: true, force: true }); } - const targets = new Set(requestedTargets); + const triplets = new Set(requestedTriplets); for (const platform of Object.values(platforms)) { // Forcing the types a bit here, since the platform id option is dynamically added if ((baseOptions as Record)[platform.id]) { - for (const target of platform.targets) { - targets.add(target); + for (const triplet of platform.triplets) { + triplets.add(triplet); } } } - if (targets.size === 0) { + if (triplets.size === 0) { for (const platform of Object.values(platforms)) { if (platform.isSupportedByHost()) { - for (const target of await platform.defaultTargets()) { - targets.add(target); + for (const triplet of await platform.defaultTriplets()) { + triplets.add(triplet); } } } - if (targets.size === 0) { + if (triplets.size === 0) { throw new Error( - "Found no default targets: Install some platform specific build tools", + "Found no default build triplets: Install some platform specific build tools", ); } else { console.error( chalk.yellowBright("ℹ"), - "Using default targets", - chalk.dim("(" + [...targets].join(", ") + ")"), + "Using default build triplets", + chalk.dim("(" + [...triplets].join(", ") + ")"), ); } } @@ -181,30 +184,32 @@ program = program.action( baseOptions.out = path.join(buildPath, baseOptions.configuration); } - const targetContexts = [...targets].map((target) => { - const platform = findPlatformForTarget(target); - const targetBuildPath = getTargetBuildPath(buildPath, target); + const tripletContexts = [...triplets].map((triplet) => { + const platform = findPlatformForTriplet(triplet); + const tripletBuildPath = getTripletBuildPath(buildPath, triplet); return { - target, + triplet, platform, - buildPath: targetBuildPath, - outputPath: path.join(targetBuildPath, "out"), + buildPath: tripletBuildPath, + outputPath: path.join(tripletBuildPath, "out"), options: baseOptions, }; }); // Configure every triplet project - const targetsSummary = chalk.dim(`(${getTargetsSummary(targetContexts)})`); + const tripletsSummary = chalk.dim( + `(${getTripletsSummary(tripletContexts)})`, + ); await oraPromise( Promise.all( - targetContexts.map(({ platform, ...context }) => + tripletContexts.map(({ platform, ...context }) => configureProject(platform, context, baseOptions), ), ), { - text: `Configuring projects ${targetsSummary}`, + text: `Configuring projects ${tripletsSummary}`, isSilent: baseOptions.verbose, - successText: `Configured projects ${targetsSummary}`, + successText: `Configured projects ${tripletsSummary}`, failText: ({ message }) => `Failed to configure projects: ${message}`, }, ); @@ -212,7 +217,7 @@ program = program.action( // Build every triplet project await oraPromise( Promise.all( - targetContexts.map(async ({ platform, ...context }) => { + tripletContexts.map(async ({ platform, ...context }) => { // Delete any stale build artifacts before building // This is important, since we might rename the output files await fs.promises.rm(context.outputPath, { @@ -232,16 +237,16 @@ program = program.action( // Perform post-build steps for each platform in sequence for (const platform of platforms) { - const relevantTargets = targetContexts.filter(({ target }) => - platformHasTarget(platform, target), + const relevantTriplets = tripletContexts.filter(({ triplet }) => + platformHasTriplet(platform, triplet), ); - if (relevantTargets.length == 0) { + if (relevantTriplets.length == 0) { continue; } await platform.postBuild( { outputPath: baseOptions.out || baseOptions.source, - targets: relevantTargets, + triplets: relevantTriplets, }, baseOptions, ); @@ -249,19 +254,19 @@ program = program.action( }), ); -function getTargetsSummary( - targetContexts: { target: string; platform: Platform }[], +function getTripletsSummary( + tripletContexts: { triplet: string; platform: Platform }[], ) { - const targetsPerPlatform: Record = {}; - for (const { target, platform } of targetContexts) { - if (!targetsPerPlatform[platform.id]) { - targetsPerPlatform[platform.id] = []; + const tripletsPerPlatform: Record = {}; + for (const { triplet, platform } of tripletContexts) { + if (!tripletsPerPlatform[platform.id]) { + tripletsPerPlatform[platform.id] = []; } - targetsPerPlatform[platform.id].push(target); + tripletsPerPlatform[platform.id].push(triplet); } - return Object.entries(targetsPerPlatform) - .map(([platformId, targets]) => { - return `${platformId}: ${targets.join(", ")}`; + return Object.entries(tripletsPerPlatform) + .map(([platformId, triplets]) => { + return `${platformId}: ${triplets.join(", ")}`; }) .join(" / "); } @@ -272,24 +277,24 @@ function getBuildPath({ build, source }: BaseOpts) { } /** - * Namespaces the output path with a target name + * Namespaces the output path with a triplet name */ -function getTargetBuildPath(buildPath: string, target: unknown) { - assert(typeof target === "string", "Expected target to be a string"); - return path.join(buildPath, target.replace(/;/g, "_")); +function getTripletBuildPath(buildPath: string, triplet: unknown) { + assert(typeof triplet === "string", "Expected triplet to be a string"); + return path.join(buildPath, triplet.replace(/;/g, "_")); } async function configureProject( platform: Platform>, - context: TargetContext, + context: TripletContext, options: BaseOpts, ) { - const { target, buildPath, outputPath } = context; + const { triplet, buildPath, outputPath } = context; const { verbose, source, weakNodeApiLinkage } = options; const nodeApiDefinitions = - weakNodeApiLinkage && isSupportedTriplet(target) - ? getWeakNodeApiVariables(target) + weakNodeApiLinkage && isSupportedTriplet(triplet) + ? getWeakNodeApiVariables(triplet) : // TODO: Make this a part of the platform definition {}; @@ -311,17 +316,17 @@ async function configureProject( ], { outputMode: verbose ? "inherit" : "buffered", - outputPrefix: verbose ? chalk.dim(`[${target}] `) : undefined, + outputPrefix: verbose ? chalk.dim(`[${triplet}] `) : undefined, }, ); } async function buildProject( platform: Platform>, - context: TargetContext, + context: TripletContext, options: BaseOpts, ) { - const { target, buildPath } = context; + const { triplet, buildPath } = context; const { verbose, configuration } = options; await spawn( "cmake", @@ -335,7 +340,7 @@ async function buildProject( ], { outputMode: verbose ? "inherit" : "buffered", - outputPrefix: verbose ? chalk.dim(`[${target}] `) : undefined, + outputPrefix: verbose ? chalk.dim(`[${triplet}] `) : undefined, }, ); } diff --git a/packages/cmake-rn/src/platforms.test.ts b/packages/cmake-rn/src/platforms.test.ts index 2f8c95f7..1d21c4ef 100644 --- a/packages/cmake-rn/src/platforms.test.ts +++ b/packages/cmake-rn/src/platforms.test.ts @@ -3,28 +3,30 @@ import { describe, it } from "node:test"; import { platforms, - platformHasTarget, - findPlatformForTarget, + platformHasTriplet, + findPlatformForTriplet, } from "./platforms.js"; import { Platform } from "./platforms/types.js"; -const mockPlatform = { targets: ["target1", "target2"] } as unknown as Platform; +const mockPlatform = { + triplets: ["triplet1", "triplet2"], +} as unknown as Platform; -describe("platformHasTarget", () => { - it("returns true when platform has target", () => { - assert.equal(platformHasTarget(mockPlatform, "target1"), true); +describe("platformHasTriplet", () => { + it("returns true when platform has triplet", () => { + assert.equal(platformHasTriplet(mockPlatform, "triplet1"), true); }); - it("returns false when platform doesn't have target", () => { - assert.equal(platformHasTarget(mockPlatform, "target3"), false); + it("returns false when platform doesn't have triplet", () => { + assert.equal(platformHasTriplet(mockPlatform, "triplet3"), false); }); }); -describe("findPlatformForTarget", () => { - it("returns platform when target is found", () => { +describe("findPlatformForTriplet", () => { + it("returns platform when triplet is found", () => { assert(platforms.length >= 2, "Expects at least two platforms"); const [platform1, platform2] = platforms; - const platform = findPlatformForTarget(platform1.targets[0]); + const platform = findPlatformForTriplet(platform1.triplets[0]); assert.equal(platform, platform1); assert.notEqual(platform, platform2); }); diff --git a/packages/cmake-rn/src/platforms.ts b/packages/cmake-rn/src/platforms.ts index cf892263..ed82cabb 100644 --- a/packages/cmake-rn/src/platforms.ts +++ b/packages/cmake-rn/src/platforms.ts @@ -5,23 +5,23 @@ import { platform as apple } from "./platforms/apple.js"; import { Platform } from "./platforms/types.js"; export const platforms: Platform[] = [android, apple] as const; -export const allTargets = [...android.targets, ...apple.targets] as const; +export const allTriplets = [...android.triplets, ...apple.triplets] as const; -export function platformHasTarget

( +export function platformHasTriplet

( platform: P, - target: unknown, -): target is P["targets"][number] { - return (platform.targets as unknown[]).includes(target); + triplet: unknown, +): triplet is P["triplets"][number] { + return (platform.triplets as unknown[]).includes(triplet); } -export function findPlatformForTarget(target: unknown) { +export function findPlatformForTriplet(triplet: unknown) { const platform = Object.values(platforms).find((platform) => - platformHasTarget(platform, target), + platformHasTriplet(platform, triplet), ); assert( platform, - `Unable to determine platform from target: ${ - typeof target === "string" ? target : JSON.stringify(target) + `Unable to determine platform from triplet: ${ + typeof triplet === "string" ? triplet : JSON.stringify(triplet) }`, ); return platform; diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index ef71afe1..832f806e 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -6,7 +6,7 @@ import { Option, oraPromise, chalk } from "@react-native-node-api/cli-utils"; import { createAndroidLibsDirectory, determineAndroidLibsFilename, - AndroidTriplet as Target, + AndroidTriplet as Triplet, } from "react-native-node-api"; import type { Platform } from "./types.js"; @@ -22,7 +22,7 @@ export const ANDROID_ARCHITECTURES = { "aarch64-linux-android": "arm64-v8a", "i686-linux-android": "x86", "x86_64-linux-android": "x86_64", -} satisfies Record; +} satisfies Record; const ndkVersionOption = new Option( "--ndk-version ", @@ -36,16 +36,16 @@ const androidSdkVersionOption = new Option( type AndroidOpts = { ndkVersion: string; androidSdkVersion: string }; -export const platform: Platform = { +export const platform: Platform = { id: "android", name: "Android", - targets: [ + triplets: [ "aarch64-linux-android", "armv7a-linux-androideabi", "i686-linux-android", "x86_64-linux-android", ], - defaultTargets() { + defaultTriplets() { if (process.arch === "arm64") { return ["aarch64-linux-android"]; } else if (process.arch === "x64") { @@ -59,7 +59,7 @@ export const platform: Platform = { .addOption(ndkVersionOption) .addOption(androidSdkVersionOption); }, - configureArgs({ target }, { ndkVersion, androidSdkVersion }) { + configureArgs({ triplet }, { ndkVersion, androidSdkVersion }) { const { ANDROID_HOME } = process.env; assert( typeof ANDROID_HOME === "string", @@ -80,7 +80,7 @@ export const platform: Platform = { ndkPath, "build/cmake/android.toolchain.cmake", ); - const architecture = ANDROID_ARCHITECTURES[target]; + const architecture = ANDROID_ARCHITECTURES[triplet]; return [ "-G", @@ -121,11 +121,11 @@ export const platform: Platform = { const { ANDROID_HOME } = process.env; return typeof ANDROID_HOME === "string" && fs.existsSync(ANDROID_HOME); }, - async postBuild({ outputPath, targets }, { autoLink }) { + async postBuild({ outputPath, triplets }, { autoLink }) { // TODO: Include `configuration` in the output path const libraryPathByTriplet = Object.fromEntries( await Promise.all( - targets.map(async ({ target, outputPath }) => { + triplets.map(async ({ triplet, outputPath }) => { assert( fs.existsSync(outputPath), `Expected a directory at ${outputPath}`, @@ -142,10 +142,10 @@ export const platform: Platform = { ) .map((dirent) => path.join(dirent.parentPath, dirent.name)); assert.equal(result.length, 1, "Expected exactly one library file"); - return [target, result[0]] as const; + return [triplet, result[0]] as const; }), ), - ) as Record; + ) as Record; const androidLibsFilename = determineAndroidLibsFilename( Object.values(libraryPathByTriplet), ); diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index c92c5b9c..00e21c72 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -4,7 +4,7 @@ import fs from "node:fs"; import { Option, oraPromise, chalk } from "@react-native-node-api/cli-utils"; import { - AppleTriplet as Target, + AppleTriplet as Triplet, createAppleFramework, createXCframework, determineXCFrameworkFilename, @@ -33,7 +33,7 @@ const XCODE_SDK_NAMES = { "arm64-apple-tvos-sim": "appletvsimulator", "arm64-apple-visionos": "xros", "arm64-apple-visionos-sim": "xrsimulator", -} satisfies Record; +} satisfies Record; type CMakeSystemName = "Darwin" | "iOS" | "tvOS" | "watchOS" | "visionOS"; @@ -48,7 +48,7 @@ const CMAKE_SYSTEM_NAMES = { "arm64-apple-tvos-sim": "tvOS", "arm64-apple-visionos": "visionOS", "arm64-apple-visionos-sim": "visionOS", -} satisfies Record; +} satisfies Record; type AppleArchitecture = "arm64" | "x86_64" | "arm64;x86_64"; @@ -63,7 +63,7 @@ export const APPLE_ARCHITECTURES = { "arm64-apple-tvos-sim": "arm64", "arm64-apple-visionos": "arm64", "arm64-apple-visionos-sim": "arm64", -} satisfies Record; +} satisfies Record; export function createPlistContent(values: Record) { return [ @@ -94,10 +94,10 @@ type AppleOpts = { xcframeworkExtension: boolean; }; -export const platform: Platform = { +export const platform: Platform = { id: "apple", name: "Apple", - targets: [ + triplets: [ "arm64;x86_64-apple-darwin", "arm64-apple-ios", "arm64-apple-ios-sim", @@ -106,22 +106,22 @@ export const platform: Platform = { "arm64-apple-visionos", "arm64-apple-visionos-sim", ], - defaultTargets() { + defaultTriplets() { return process.arch === "arm64" ? ["arm64-apple-ios-sim"] : []; }, amendCommand(command) { return command.addOption(xcframeworkExtensionOption); }, - configureArgs({ target }) { + configureArgs({ triplet }) { return [ "-G", "Xcode", "-D", - `CMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAMES[target]}`, + `CMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAMES[triplet]}`, "-D", - `CMAKE_OSX_SYSROOT=${XCODE_SDK_NAMES[target]}`, + `CMAKE_OSX_SYSROOT=${XCODE_SDK_NAMES[triplet]}`, "-D", - `CMAKE_OSX_ARCHITECTURES=${APPLE_ARCHITECTURES[target]}`, + `CMAKE_OSX_ARCHITECTURES=${APPLE_ARCHITECTURES[triplet]}`, ]; }, buildArgs() { @@ -132,11 +132,11 @@ export const platform: Platform = { return process.platform === "darwin"; }, async postBuild( - { outputPath, targets }, + { outputPath, triplets }, { configuration, autoLink, xcframeworkExtension }, ) { const libraryPaths = await Promise.all( - targets.map(async ({ outputPath }) => { + triplets.map(async ({ outputPath }) => { const configSpecificPath = path.join(outputPath, configuration); assert( fs.existsSync(configSpecificPath), diff --git a/packages/cmake-rn/src/platforms/types.ts b/packages/cmake-rn/src/platforms/types.ts index bd3e06a2..7f4a78da 100644 --- a/packages/cmake-rn/src/platforms/types.ts +++ b/packages/cmake-rn/src/platforms/types.ts @@ -13,16 +13,16 @@ type ExtendedCommand = cli.Command< Record // Global opts are not supported >; -export type BaseOpts = Omit, "target">; +export type BaseOpts = Omit, "triplet">; -export type TargetContext = { - target: Target; +export type TripletContext = { + triplet: Triplet; buildPath: string; outputPath: string; }; export type Platform< - Targets extends string[] = string[], + Triplets extends string[] = string[], Opts extends cli.OptionValues = Record, Command = ExtendedCommand, > = { @@ -35,13 +35,13 @@ export type Platform< */ name: string; /** - * All the targets supported by this platform. + * All the triplets supported by this platform. */ - targets: Readonly; + triplets: Readonly; /** - * Get the limited subset of targets that should be built by default for this platform, to support a development workflow. + * Get the limited subset of triplets that should be built by default for this platform, to support a development workflow. */ - defaultTargets(): Targets[number][] | Promise; + defaultTriplets(): Triplets[number][] | Promise; /** * Implement this to add any platform specific options to the command. */ @@ -51,21 +51,21 @@ export type Platform< */ isSupportedByHost(): boolean | Promise; /** - * Platform specific arguments passed to CMake to configure a target project. + * Platform specific arguments passed to CMake to configure a triplet project. */ configureArgs( - context: TargetContext, + context: TripletContext, options: BaseOpts & Opts, ): string[]; /** - * Platform specific arguments passed to CMake to build a target project. + * Platform specific arguments passed to CMake to build a triplet project. */ buildArgs( - context: TargetContext, + context: TripletContext, options: BaseOpts & Opts, ): string[]; /** - * Called to combine multiple targets into a single prebuilt artefact. + * Called to combine multiple triplets into a single prebuilt artefact. */ postBuild( context: { @@ -73,7 +73,7 @@ export type Platform< * Location of the final prebuilt artefact. */ outputPath: string; - targets: TargetContext[]; + triplets: TripletContext[]; }, options: BaseOpts & Opts, ): Promise; From 633dc3482dd0f1334be84075c9056e31ea039e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 21 Sep 2025 11:34:03 +0200 Subject: [PATCH 07/82] Enabled passing `--target` to `cmake --build` (#245) * Rename target to triplet * Add changeset * Pass --target to CMake when building --- .changeset/swift-loops-create.md | 5 +++++ packages/cmake-rn/src/cli.ts | 7 +++++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/swift-loops-create.md diff --git a/.changeset/swift-loops-create.md b/.changeset/swift-loops-create.md new file mode 100644 index 00000000..9ad19a08 --- /dev/null +++ b/.changeset/swift-loops-create.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": minor +--- + +Pass --target to CMake diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 8a8901ca..7545caf3 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -102,6 +102,11 @@ const defineOption = new Option( }, ); +const targetOption = new Option( + "--target ", + "CMake targets to build", +).default([] as string[], "Build all targets of the CMake project"); + const noAutoLinkOption = new Option( "--no-auto-link", "Don't mark the output as auto-linkable by react-native-node-api", @@ -122,6 +127,7 @@ let program = new Command("cmake-rn") .addOption(configurationOption) .addOption(defineOption) .addOption(cleanOption) + .addOption(targetOption) .addOption(noAutoLinkOption) .addOption(noWeakNodeApiLinkageOption); @@ -335,6 +341,7 @@ async function buildProject( buildPath, "--config", configuration, + ...(options.target.length > 0 ? ["--target", ...options.target] : []), "--", ...platform.buildArgs(context, options), ], From c72970f41427ab2bd38995cd19b760aaff755d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 21 Sep 2025 20:42:31 +0200 Subject: [PATCH 08/82] Check for `REACT_NATIVE_OVERRIDE_HERMES_DIR` earlier (#246) * Move REACT_NATIVE_OVERRIDE_HERMES_DIR out of tasks to fail earlier * Fix tests --- .changeset/red-candles-sell.md | 5 +++++ packages/host/android/build.gradle | 30 ++++++++++++--------------- packages/host/src/node/gradle.test.ts | 11 ++++++---- 3 files changed, 25 insertions(+), 21 deletions(-) create mode 100644 .changeset/red-candles-sell.md diff --git a/.changeset/red-candles-sell.md b/.changeset/red-candles-sell.md new file mode 100644 index 00000000..220321aa --- /dev/null +++ b/.changeset/red-candles-sell.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Move REACT_NATIVE_OVERRIDE_HERMES_DIR out of tasks to fail earlier diff --git a/packages/host/android/build.gradle b/packages/host/android/build.gradle index f4b3ba4c..ed18f9fe 100644 --- a/packages/host/android/build.gradle +++ b/packages/host/android/build.gradle @@ -2,6 +2,18 @@ import java.nio.file.Paths import groovy.json.JsonSlurper import org.gradle.internal.os.OperatingSystem +if (!System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR")) { + throw new GradleException([ + "React Native Node-API needs a custom version of Hermes with Node-API enabled.", + "Run the following in your terminal, to clone Hermes and instruct React Native to use it:", + "", + "export REACT_NATIVE_OVERRIDE_HERMES_DIR=\$(npx react-native-node-api vendor-hermes --silent --force)", + "", + "And follow this guide to build React Native from source:", + "https://reactnative.dev/contributing/how-to-build-from-source#update-your-project-to-build-from-source" + ].join('\n')) +} + buildscript { ext.getExtOrDefault = {name -> return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['NodeApiModules_' + name] @@ -135,22 +147,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } -task checkHermesOverride { - doFirst { - if (!System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR")) { - throw new GradleException([ - "React Native Node-API needs a custom version of Hermes with Node-API enabled.", - "Run the following in your terminal, to clone Hermes and instruct React Native to use it:", - "", - "export REACT_NATIVE_OVERRIDE_HERMES_DIR=\$(npx react-native-node-api vendor-hermes --silent --force)", - "", - "And follow this guide to build React Native from source:", - "https://reactnative.dev/contributing/how-to-build-from-source#update-your-project-to-build-from-source" - ].join('\n')) - } - } -} - def commandLinePrefix = OperatingSystem.current().isWindows() ? ["cmd", "/c", "node"] : [] def cliPath = file("../bin/react-native-node-api.mjs") @@ -169,5 +165,5 @@ task linkNodeApiModules { } } -preBuild.dependsOn checkHermesOverride, linkNodeApiModules +preBuild.dependsOn linkNodeApiModules diff --git a/packages/host/src/node/gradle.test.ts b/packages/host/src/node/gradle.test.ts index 45c6f8e0..5485a173 100644 --- a/packages/host/src/node/gradle.test.ts +++ b/packages/host/src/node/gradle.test.ts @@ -12,11 +12,11 @@ describe( // Skipping these tests by default, as they download a lot and takes a long time { skip: process.env.ENABLE_GRADLE_TESTS !== "true" }, () => { - describe("checkHermesOverride task", () => { + describe("linkNodeApiModules task", () => { it("should fail if REACT_NATIVE_OVERRIDE_HERMES_DIR is not set", () => { const { status, stdout, stderr } = cp.spawnSync( "sh", - ["gradlew", "react-native-node-api:checkHermesOverride"], + ["gradlew", "react-native-node-api:linkNodeApiModules"], { cwd: TEST_APP_ANDROID_PATH, env: { @@ -45,15 +45,18 @@ describe( /And follow this guide to build React Native from source/, ); }); - }); - describe("linkNodeApiModules task", () => { it("should call the CLI to autolink", () => { const { status, stdout, stderr } = cp.spawnSync( "sh", ["gradlew", "react-native-node-api:linkNodeApiModules"], { cwd: TEST_APP_ANDROID_PATH, + env: { + ...process.env, + // We're passing some directory which exists + REACT_NATIVE_OVERRIDE_HERMES_DIR: __dirname, + }, encoding: "utf-8", }, ); From d8872b6b273fa589ceacdceb6dcaa6202bcc2937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 22 Sep 2025 01:13:40 +0200 Subject: [PATCH 09/82] Update build.gradle with a note on shell requirement As per https://github.com/callstackincubator/react-native-node-api/pull/246#discussion_r2366344188 --- packages/host/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/host/android/build.gradle b/packages/host/android/build.gradle index ed18f9fe..169265a0 100644 --- a/packages/host/android/build.gradle +++ b/packages/host/android/build.gradle @@ -5,7 +5,7 @@ import org.gradle.internal.os.OperatingSystem if (!System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR")) { throw new GradleException([ "React Native Node-API needs a custom version of Hermes with Node-API enabled.", - "Run the following in your terminal, to clone Hermes and instruct React Native to use it:", + "Run the following in your Bash- or Zsh-compatible terminal, to clone Hermes and instruct React Native to use it:", "", "export REACT_NATIVE_OVERRIDE_HERMES_DIR=\$(npx react-native-node-api vendor-hermes --silent --force)", "", From fdb03c2ab18177b263e7a685c1eb1e5f033d9568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 22 Sep 2025 02:21:04 +0200 Subject: [PATCH 10/82] Update gradle.test.ts --- packages/host/src/node/gradle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/host/src/node/gradle.test.ts b/packages/host/src/node/gradle.test.ts index 5485a173..9dcb218c 100644 --- a/packages/host/src/node/gradle.test.ts +++ b/packages/host/src/node/gradle.test.ts @@ -34,7 +34,7 @@ describe( ); assert.match( stderr, - /Run the following in your terminal, to clone Hermes and instruct React Native to use it/, + /Run the following in your Bash- or Zsh-compatible terminal, to clone Hermes and instruct React Native to use it/, ); assert.match( stderr, From 172ca061bf09e2cd51e99ea75687a10b2afa2a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 22 Sep 2025 09:51:07 +0200 Subject: [PATCH 11/82] Switch to x86_64 Android emulator and remove custom options (#252) * Bump disk and heap and switch to x86_64 * Add a custom AVD name * Default settings * Removed ADV name again --- .github/workflows/check.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1cf4f24e..373ac614 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -129,8 +129,8 @@ jobs: - run: npm ci - run: npm run bootstrap env: - CMAKE_RN_TRIPLETS: i686-linux-android - FERRIC_TARGETS: i686-linux-android + CMAKE_RN_TRIPLETS: x86_64-linux-android + FERRIC_TARGETS: x86_64-linux-android - name: Clone patched Hermes version shell: bash run: | @@ -163,9 +163,8 @@ jobs: with: api-level: 29 force-avd-creation: false - emulator-options: -no-snapshot-save -no-metrics -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: true - arch: x86 + arch: x86_64 ndk: ${{ env.NDK_VERSION }} cmake: 3.22.1 working-directory: apps/test-app From ff34c453a528e9ef70d6cdddf857d9c3702cc398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 23 Sep 2025 08:37:40 +0200 Subject: [PATCH 12/82] Explicit `weak-node-api` linkage (#249) * Expose includable WEAK_NODE_API_CONFIG to CMake projects * Use the `include(${WEAK_NODE_API_CONFIG})` syntax in our own tests * Turn define arguments into arrays of objects with key-value-pair * Add general documentation on weak-node-api * Make injection of CMAKE_JS_* variables opt-in and provide our own explicit way of linking with Node-API * Add --weak-node-api option to gyp-to-cmake * Use weak-node-api linkage in node-addon-examples * Opt into cmake-js compatibility in node-tests * Fixed bug passing type through defines * Fixed bug when calling cmake-rn in node-tests * Fix missing set_target_properties of .node suffix * Apply suggestion from @Copilot to documentation * Use [] instead of Array * Push into an array instead of creating a new on every --define * Update packages/gyp-to-cmake/src/transformer.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .changeset/cold-symbols-refuse.md | 5 + .changeset/open-ducks-shop.md | 5 + .changeset/three-colts-tie.md | 5 + docs/WEAK-NODE-API.md | 7 ++ packages/cmake-rn/README.md | 21 ++++ packages/cmake-rn/src/cli.ts | 64 ++++++----- packages/cmake-rn/src/weak-node-api.ts | 24 ++++- packages/gyp-to-cmake/src/cli.ts | 43 +++++--- packages/gyp-to-cmake/src/transformer.ts | 102 ++++++++++++------ .../host/weak-node-api/weak-node-api.cmake | 6 ++ packages/node-addon-examples/package.json | 2 +- .../tests/async/CMakeLists.txt | 16 +-- .../tests/buffers/CMakeLists.txt | 16 +-- packages/node-tests/scripts/build-tests.mts | 7 +- 14 files changed, 223 insertions(+), 100 deletions(-) create mode 100644 .changeset/cold-symbols-refuse.md create mode 100644 .changeset/open-ducks-shop.md create mode 100644 .changeset/three-colts-tie.md create mode 100644 docs/WEAK-NODE-API.md create mode 100644 packages/host/weak-node-api/weak-node-api.cmake diff --git a/.changeset/cold-symbols-refuse.md b/.changeset/cold-symbols-refuse.md new file mode 100644 index 00000000..bf01ac33 --- /dev/null +++ b/.changeset/cold-symbols-refuse.md @@ -0,0 +1,5 @@ +--- +"gyp-to-cmake": minor +--- + +Add --weak-node-api option to emit CMake configuration for use with cmake-rn's default way of Node-API linkage. diff --git a/.changeset/open-ducks-shop.md b/.changeset/open-ducks-shop.md new file mode 100644 index 00000000..c1153f5a --- /dev/null +++ b/.changeset/open-ducks-shop.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": minor +--- + +Breaking: `CMAKE_JS_*` defines are no longer injected by default (use --cmake-js to opt-in) diff --git a/.changeset/three-colts-tie.md b/.changeset/three-colts-tie.md new file mode 100644 index 00000000..e575098a --- /dev/null +++ b/.changeset/three-colts-tie.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": minor +--- + +Expose includable WEAK_NODE_API_CONFIG to CMake projects diff --git a/docs/WEAK-NODE-API.md b/docs/WEAK-NODE-API.md new file mode 100644 index 00000000..0664a3b6 --- /dev/null +++ b/docs/WEAK-NODE-API.md @@ -0,0 +1,7 @@ +# The `weak-node-api` library + +Android's dynamic linker imposes restrictions on the access to global symbols (such as the Node-API free functions): A dynamic library must explicitly declare any dependency bringing symbols it needs as `DT_NEEDED`. + +The implementation of Node-API is split between Hermes and our host package and to avoid addons having to explicitly link against either, we've introduced a `weak-node-api` library (published in `react-native-node-api` package). This library exposes only Node-API and will have its implementation injected by the host. + +While technically not a requirement on non-Android platforms, we choose to make this the general approach across React Native platforms. This keeps things aligned across platforms, while exposing just the Node-API without forcing libraries to build with suppression of errors for undefined symbols. diff --git a/packages/cmake-rn/README.md b/packages/cmake-rn/README.md index e4c64b33..6b6aa888 100644 --- a/packages/cmake-rn/README.md +++ b/packages/cmake-rn/README.md @@ -3,3 +3,24 @@ A wrapper around Cmake making it easier to produce prebuilt binaries targeting iOS and Android matching [the prebuilt binary specification](https://github.com/callstackincubator/react-native-node-api/blob/main/docs/PREBUILDS.md). Serves the same purpose as `cmake-js` does for the Node.js community and could potentially be upstreamed into `cmake-js` eventually. + +## Linking against Node-API + +Android's dynamic linker imposes restrictions on the access to global symbols (such as the Node-API free functions): A dynamic library must explicitly declare any dependency bringing symbols it needs as `DT_NEEDED`. + +The implementation of Node-API is split between Hermes and our host package and to avoid addons having to explicitly link against either, we've introduced a `weak-node-api` library (published in `react-native-node-api` package). This library exposes only Node-API and will have its implementation injected by the host. + +To link against `weak-node-api` just include the CMake config exposed through `WEAK_NODE_API_CONFIG` and add `weak-node-api` to the `target_link_libraries` of the addon's library target. + +```cmake +cmake_minimum_required(VERSION 3.15...3.31) +project(tests-buffers) + +include(${WEAK_NODE_API_CONFIG}) + +add_library(addon SHARED addon.c) +target_link_libraries(addon PRIVATE weak-node-api) +target_compile_features(addon PRIVATE cxx_std_20) +``` + +This is different from how `cmake-js` "injects" the Node-API for linking (via `${CMAKE_JS_INC}`, `${CMAKE_JS_SRC}` and `${CMAKE_JS_LIB}`). To allow for interoperability between these tools, we inject these when you pass `--cmake-js` to `cmake-rn`. diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 7545caf3..5afa8932 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -14,7 +14,10 @@ import { } from "@react-native-node-api/cli-utils"; import { isSupportedTriplet } from "react-native-node-api"; -import { getWeakNodeApiVariables } from "./weak-node-api.js"; +import { + getCmakeJSVariables, + getWeakNodeApiVariables, +} from "./weak-node-api.js"; import { platforms, allTriplets as allTriplets, @@ -85,8 +88,8 @@ const outPathOption = new Option( const defineOption = new Option( "-D,--define ", "Define cache variables passed when configuring projects", -).argParser>( - (input, previous = {}) => { +) + .argParser[]>((input, previous = []) => { // TODO: Implement splitting of value using a regular expression (using named groups) for the format [:]= // and return an object keyed by variable name with the string value as value or alternatively an array of [value, type] const match = input.match( @@ -98,9 +101,10 @@ const defineOption = new Option( ); } const { name, type, value } = match.groups; - return { ...previous, [name]: type ? { value, type } : value }; - }, -); + previous.push({ [type ? `${name}:${type}` : name]: value }); + return previous; + }) + .default([]); const targetOption = new Option( "--target ", @@ -117,6 +121,11 @@ const noWeakNodeApiLinkageOption = new Option( "Don't pass the path of the weak-node-api library from react-native-node-api", ); +const cmakeJsOption = new Option( + "--cmake-js", + "Define CMAKE_JS_* variables used for compatibility with cmake-js", +).default(false); + let program = new Command("cmake-rn") .description("Build React Native Node API modules with CMake") .addOption(tripletOption) @@ -129,7 +138,8 @@ let program = new Command("cmake-rn") .addOption(cleanOption) .addOption(targetOption) .addOption(noAutoLinkOption) - .addOption(noWeakNodeApiLinkageOption); + .addOption(noWeakNodeApiLinkageOption) + .addOption(cmakeJsOption); for (const platform of platforms) { const allOption = new Option( @@ -296,19 +306,26 @@ async function configureProject( options: BaseOpts, ) { const { triplet, buildPath, outputPath } = context; - const { verbose, source, weakNodeApiLinkage } = options; + const { verbose, source, weakNodeApiLinkage, cmakeJs } = options; + + // TODO: Make the two following definitions a part of the platform definition const nodeApiDefinitions = weakNodeApiLinkage && isSupportedTriplet(triplet) - ? getWeakNodeApiVariables(triplet) - : // TODO: Make this a part of the platform definition - {}; + ? [getWeakNodeApiVariables(triplet)] + : []; - const definitions = { + const cmakeJsDefinitions = + cmakeJs && isSupportedTriplet(triplet) + ? [getCmakeJSVariables(triplet)] + : []; + + const definitions = [ ...nodeApiDefinitions, + ...cmakeJsDefinitions, ...options.define, - CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath, - }; + { CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath }, + ]; await spawn( "cmake", @@ -352,18 +369,13 @@ async function buildProject( ); } -type CmakeTypedDefinition = { value: string; type: string }; - -function toDefineArguments( - declarations: Record, -) { - return Object.entries(declarations).flatMap(([key, definition]) => { - if (typeof definition === "string") { - return ["-D", `${key}=${definition}`]; - } else { - return ["-D", `${key}:${definition.type}=${definition.value}`]; - } - }); +function toDefineArguments(declarations: Array>) { + return declarations.flatMap((values) => + Object.entries(values).flatMap(([key, definition]) => [ + "-D", + `${key}=${definition}`, + ]), + ); } export { program }; diff --git a/packages/cmake-rn/src/weak-node-api.ts b/packages/cmake-rn/src/weak-node-api.ts index 496905d7..bc8a7407 100644 --- a/packages/cmake-rn/src/weak-node-api.ts +++ b/packages/cmake-rn/src/weak-node-api.ts @@ -41,7 +41,7 @@ export function getWeakNodeApiPath(triplet: SupportedTriplet): string { } } -export function getWeakNodeApiVariables(triplet: SupportedTriplet) { +function getNodeApiIncludePaths() { const includePaths = [getNodeApiHeadersPath(), getNodeAddonHeadersPath()]; for (const includePath of includePaths) { assert( @@ -49,8 +49,28 @@ export function getWeakNodeApiVariables(triplet: SupportedTriplet) { `Include path with a ';' is not supported: ${includePath}`, ); } + return includePaths; +} + +export function getWeakNodeApiVariables( + triplet: SupportedTriplet, +): Record { + return { + // Expose an includable CMake config file declaring the weak-node-api target + WEAK_NODE_API_CONFIG: path.join(weakNodeApiPath, "weak-node-api.cmake"), + WEAK_NODE_API_INC: getNodeApiIncludePaths().join(";"), + WEAK_NODE_API_LIB: getWeakNodeApiPath(triplet), + }; +} + +/** + * For compatibility with cmake-js + */ +export function getCmakeJSVariables( + triplet: SupportedTriplet, +): Record { return { - CMAKE_JS_INC: includePaths.join(";"), + CMAKE_JS_INC: getNodeApiIncludePaths().join(";"), CMAKE_JS_LIB: getWeakNodeApiPath(triplet), }; } diff --git a/packages/gyp-to-cmake/src/cli.ts b/packages/gyp-to-cmake/src/cli.ts index 118f0c0d..52d14984 100644 --- a/packages/gyp-to-cmake/src/cli.ts +++ b/packages/gyp-to-cmake/src/cli.ts @@ -62,25 +62,38 @@ export const program = new Command("gyp-to-cmake") "--no-path-transforms", "Don't transform output from command expansions (replacing '\\' with '/')", ) + .option("--weak-node-api", "Link against the weak-node-api library", false) + .option("--define-napi-version", "Define NAPI_VERSION for all targets", false) + .option("--cpp ", "C++ standard version", "17") .argument( "[path]", "Path to the binding.gyp file or directory to traverse recursively", process.cwd(), ) .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}`); - } - }), + wrapAction( + ( + targetPath: string, + { pathTransforms, cpp, defineNapiVersion, weakNodeApi }, + ) => { + const options: TransformOptions = { + unsupportedBehaviour: "throw", + disallowUnknownProperties: false, + transformWinPathsToPosix: pathTransforms, + compileFeatures: cpp ? [`cxx_std_${cpp}`] : [], + defineNapiVersion, + weakNodeApi, + }; + 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/gyp-to-cmake/src/transformer.ts b/packages/gyp-to-cmake/src/transformer.ts index 01096501..fdf5c604 100644 --- a/packages/gyp-to-cmake/src/transformer.ts +++ b/packages/gyp-to-cmake/src/transformer.ts @@ -12,6 +12,9 @@ export type GypToCmakeListsOptions = { executeCmdExpansions?: boolean; unsupportedBehaviour?: "skip" | "warn" | "throw"; transformWinPathsToPosix?: boolean; + compileFeatures?: string[]; + defineNapiVersion?: boolean; + weakNodeApi?: boolean; }; function isCmdExpansion(value: string) { @@ -34,6 +37,9 @@ export function bindingGypToCmakeLists({ executeCmdExpansions = true, unsupportedBehaviour = "skip", transformWinPathsToPosix = true, + defineNapiVersion = true, + weakNodeApi = false, + compileFeatures = [], }: GypToCmakeListsOptions): string { function mapExpansion(value: string): string[] { if (!isCmdExpansion(value)) { @@ -60,65 +66,97 @@ export function bindingGypToCmakeLists({ } const lines: string[] = [ - "cmake_minimum_required(VERSION 3.15)", + "cmake_minimum_required(VERSION 3.15...3.31)", //"cmake_policy(SET CMP0091 NEW)", //"cmake_policy(SET CMP0042 NEW)", `project(${projectName})`, "", // Declaring a project-wide NAPI_VERSION as a fallback for targets that don't explicitly set it - `add_compile_definitions(NAPI_VERSION=${napiVersion})`, + // This is only needed when using cmake-js, as it is injected by cmake-rn + ...(defineNapiVersion + ? [`add_compile_definitions(NAPI_VERSION=${napiVersion})`] + : []), ]; + if (weakNodeApi) { + lines.push(`include(\${WEAK_NODE_API_CONFIG})`, ""); + } + for (const target of gyp.targets) { - const { target_name: targetName } = target; + const { target_name: targetName, defines = [] } = target; // TODO: Handle "conditions" // TODO: Handle "cflags" // TODO: Handle "ldflags" - const escapedJoinedSources = target.sources + const escapedSources = target.sources .flatMap(mapExpansion) .map(transformPath) - .map(escapeSpaces) - .join(" "); + .map(escapeSpaces); - const escapedJoinedIncludes = (target.include_dirs || []) + const escapedIncludes = (target.include_dirs || []) .flatMap(mapExpansion) .map(transformPath) - .map(escapeSpaces) - .join(" "); + .map(escapeSpaces); - const escapedJoinedDefines = (target.defines || []) + const escapedDefines = defines .flatMap(mapExpansion) .map(transformPath) - .map(escapeSpaces) - .join(" "); + .map(escapeSpaces); + + const libraries = []; + if (weakNodeApi) { + libraries.push("weak-node-api"); + } else { + libraries.push("${CMAKE_JS_LIB}"); + escapedSources.push("${CMAKE_JS_SRC}"); + escapedIncludes.push("${CMAKE_JS_INC}"); + } lines.push( - "", - `add_library(${targetName} SHARED ${escapedJoinedSources} \${CMAKE_JS_SRC})`, + `add_library(${targetName} SHARED ${escapedSources.join(" ")})`, `set_target_properties(${targetName} PROPERTIES PREFIX "" SUFFIX ".node")`, - `target_include_directories(${targetName} PRIVATE ${escapedJoinedIncludes} \${CMAKE_JS_INC})`, - `target_link_libraries(${targetName} PRIVATE \${CMAKE_JS_LIB})`, - `target_compile_features(${targetName} PRIVATE cxx_std_17)`, - ...(escapedJoinedDefines - ? [ - `target_compile_definitions(${targetName} PRIVATE ${escapedJoinedDefines})`, - ] - : []), - // or - // `set_target_properties(${targetName} PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO)`, ); + + if (libraries.length > 0) { + lines.push( + `target_link_libraries(${targetName} PRIVATE ${libraries.join(" ")})`, + ); + } + + if (escapedIncludes.length > 0) { + lines.push( + `target_include_directories(${targetName} PRIVATE ${escapedIncludes.join( + " ", + )})`, + ); + } + + if (escapedDefines.length > 0) { + lines.push( + `target_compile_definitions(${targetName} PRIVATE ${escapedDefines.join(" ")})`, + ); + } + + if (compileFeatures.length > 0) { + lines.push( + `target_compile_features(${targetName} PRIVATE ${compileFeatures.join(" ")})`, + ); + } + + // `set_target_properties(${targetName} PROPERTIES CXX_STANDARD 11 CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS NO)`, } - // Adding this post-amble from the template, although not used by react-native-node-api - lines.push( - "", - "if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET)", - " # Generate node.lib", - " execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})", - "endif()", - ); + if (!weakNodeApi) { + // This is required by cmake-js to generate the import library for node.lib on Windows + lines.push( + "", + "if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET)", + " # Generate node.lib", + " execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})", + "endif()", + ); + } return lines.join("\n"); } diff --git a/packages/host/weak-node-api/weak-node-api.cmake b/packages/host/weak-node-api/weak-node-api.cmake new file mode 100644 index 00000000..2fda647e --- /dev/null +++ b/packages/host/weak-node-api/weak-node-api.cmake @@ -0,0 +1,6 @@ +add_library(weak-node-api SHARED IMPORTED) + +set_target_properties(weak-node-api PROPERTIES + IMPORTED_LOCATION "${WEAK_NODE_API_LIB}" + INTERFACE_INCLUDE_DIRECTORIES "${WEAK_NODE_API_INC}" +) diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index adae7591..db48db3f 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -11,7 +11,7 @@ }, "scripts": { "copy-examples": "tsx scripts/copy-examples.mts", - "gyp-to-cmake": "gyp-to-cmake .", + "gyp-to-cmake": "gyp-to-cmake --weak-node-api .", "build": "tsx scripts/build-examples.mts", "copy-and-build": "node --run copy-examples && node --run gyp-to-cmake && node --run build", "verify": "tsx scripts/verify-prebuilds.mts", diff --git a/packages/node-addon-examples/tests/async/CMakeLists.txt b/packages/node-addon-examples/tests/async/CMakeLists.txt index 31d513c0..a94a7716 100644 --- a/packages/node-addon-examples/tests/async/CMakeLists.txt +++ b/packages/node-addon-examples/tests/async/CMakeLists.txt @@ -1,15 +1,9 @@ -cmake_minimum_required(VERSION 3.15) +cmake_minimum_required(VERSION 3.15...3.31) project(tests-async) -add_compile_definitions(NAPI_VERSION=8) +include(${WEAK_NODE_API_CONFIG}) -add_library(addon SHARED addon.c ${CMAKE_JS_SRC}) +add_library(addon SHARED addon.c) set_target_properties(addon PROPERTIES PREFIX "" SUFFIX ".node") -target_include_directories(addon PRIVATE ${CMAKE_JS_INC}) -target_link_libraries(addon PRIVATE ${CMAKE_JS_LIB}) -target_compile_features(addon PRIVATE cxx_std_17) - -if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) - # Generate node.lib - execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) -endif() \ No newline at end of file +target_link_libraries(addon PRIVATE weak-node-api) +target_compile_features(addon PRIVATE cxx_std_17) \ No newline at end of file diff --git a/packages/node-addon-examples/tests/buffers/CMakeLists.txt b/packages/node-addon-examples/tests/buffers/CMakeLists.txt index 8e8cc950..837359fc 100644 --- a/packages/node-addon-examples/tests/buffers/CMakeLists.txt +++ b/packages/node-addon-examples/tests/buffers/CMakeLists.txt @@ -1,15 +1,9 @@ -cmake_minimum_required(VERSION 3.15) +cmake_minimum_required(VERSION 3.15...3.31) project(tests-buffers) -add_compile_definitions(NAPI_VERSION=8) +include(${WEAK_NODE_API_CONFIG}) -add_library(addon SHARED addon.c ${CMAKE_JS_SRC}) +add_library(addon SHARED addon.c) set_target_properties(addon PROPERTIES PREFIX "" SUFFIX ".node") -target_include_directories(addon PRIVATE ${CMAKE_JS_INC}) -target_link_libraries(addon PRIVATE ${CMAKE_JS_LIB}) -target_compile_features(addon PRIVATE cxx_std_17) - -if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) - # Generate node.lib - execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) -endif() \ No newline at end of file +target_link_libraries(addon PRIVATE weak-node-api) +target_compile_features(addon PRIVATE cxx_std_17) \ No newline at end of file diff --git a/packages/node-tests/scripts/build-tests.mts b/packages/node-tests/scripts/build-tests.mts index de879e8c..1c38cfb8 100644 --- a/packages/node-tests/scripts/build-tests.mts +++ b/packages/node-tests/scripts/build-tests.mts @@ -1,5 +1,5 @@ import path from "node:path"; -import { spawnSync } from "node:child_process"; +import { execSync } from "node:child_process"; import { findCMakeProjects } from "./utils.mjs"; @@ -13,5 +13,8 @@ for (const projectPath of projectPaths) { projectPath, )} to build for React Native`, ); - spawnSync("cmake-rn", [], { cwd: projectPath, stdio: "inherit" }); + execSync("cmake-rn --cmake-js", { + cwd: projectPath, + stdio: "inherit", + }); } From 58d17e6cbfa63f6118482cc9df89eac4f355e92a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:44:20 +0200 Subject: [PATCH 13/82] Version Packages (#240) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/cold-symbols-refuse.md | 5 ----- .changeset/fresh-nails-repair.md | 8 -------- .changeset/open-ducks-shop.md | 5 ----- .changeset/red-candles-sell.md | 5 ----- .changeset/rotten-melons-brush.md | 5 ----- .changeset/salty-kiwis-turn.md | 5 ----- .changeset/shaggy-dots-deny.md | 5 ----- .changeset/social-rivers-tie.md | 5 ----- .changeset/swift-loops-create.md | 5 ----- .changeset/three-colts-tie.md | 5 ----- packages/cmake-rn/CHANGELOG.md | 19 +++++++++++++++++++ packages/cmake-rn/package.json | 4 ++-- packages/ferric/CHANGELOG.md | 9 +++++++++ packages/ferric/package.json | 4 ++-- packages/gyp-to-cmake/CHANGELOG.md | 10 ++++++++++ packages/gyp-to-cmake/package.json | 2 +- packages/host/CHANGELOG.md | 7 +++++++ packages/host/package.json | 2 +- packages/node-tests/package.json | 2 +- 19 files changed, 52 insertions(+), 60 deletions(-) delete mode 100644 .changeset/cold-symbols-refuse.md delete mode 100644 .changeset/fresh-nails-repair.md delete mode 100644 .changeset/open-ducks-shop.md delete mode 100644 .changeset/red-candles-sell.md delete mode 100644 .changeset/rotten-melons-brush.md delete mode 100644 .changeset/salty-kiwis-turn.md delete mode 100644 .changeset/shaggy-dots-deny.md delete mode 100644 .changeset/social-rivers-tie.md delete mode 100644 .changeset/swift-loops-create.md delete mode 100644 .changeset/three-colts-tie.md diff --git a/.changeset/cold-symbols-refuse.md b/.changeset/cold-symbols-refuse.md deleted file mode 100644 index bf01ac33..00000000 --- a/.changeset/cold-symbols-refuse.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"gyp-to-cmake": minor ---- - -Add --weak-node-api option to emit CMake configuration for use with cmake-rn's default way of Node-API linkage. diff --git a/.changeset/fresh-nails-repair.md b/.changeset/fresh-nails-repair.md deleted file mode 100644 index 2de1fa30..00000000 --- a/.changeset/fresh-nails-repair.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"gyp-to-cmake": patch -"cmake-rn": patch -"ferric-cli": patch -"react-native-node-api": patch ---- - -Refactored CLIs to use a shared utility package diff --git a/.changeset/open-ducks-shop.md b/.changeset/open-ducks-shop.md deleted file mode 100644 index c1153f5a..00000000 --- a/.changeset/open-ducks-shop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"cmake-rn": minor ---- - -Breaking: `CMAKE_JS_*` defines are no longer injected by default (use --cmake-js to opt-in) diff --git a/.changeset/red-candles-sell.md b/.changeset/red-candles-sell.md deleted file mode 100644 index 220321aa..00000000 --- a/.changeset/red-candles-sell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Move REACT_NATIVE_OVERRIDE_HERMES_DIR out of tasks to fail earlier diff --git a/.changeset/rotten-melons-brush.md b/.changeset/rotten-melons-brush.md deleted file mode 100644 index cb313de4..00000000 --- a/.changeset/rotten-melons-brush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"cmake-rn": minor ---- - -Breaking: Renamed --target to --triplet to free up --target for passing CMake targets diff --git a/.changeset/salty-kiwis-turn.md b/.changeset/salty-kiwis-turn.md deleted file mode 100644 index 861aab3d..00000000 --- a/.changeset/salty-kiwis-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"cmake-rn": patch ---- - -Pretty print spawn errors instead of simply rethrowing to commander. diff --git a/.changeset/shaggy-dots-deny.md b/.changeset/shaggy-dots-deny.md deleted file mode 100644 index 1d7139be..00000000 --- a/.changeset/shaggy-dots-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"cmake-rn": minor ---- - -Add passing of definitions (-D) to cmake when configuring diff --git a/.changeset/social-rivers-tie.md b/.changeset/social-rivers-tie.md deleted file mode 100644 index 6748eb41..00000000 --- a/.changeset/social-rivers-tie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"cmake-rn": patch ---- - -Assert the existence of CMakeList.txt before passing control to CMake diff --git a/.changeset/swift-loops-create.md b/.changeset/swift-loops-create.md deleted file mode 100644 index 9ad19a08..00000000 --- a/.changeset/swift-loops-create.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"cmake-rn": minor ---- - -Pass --target to CMake diff --git a/.changeset/three-colts-tie.md b/.changeset/three-colts-tie.md deleted file mode 100644 index e575098a..00000000 --- a/.changeset/three-colts-tie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"cmake-rn": minor ---- - -Expose includable WEAK_NODE_API_CONFIG to CMake projects diff --git a/packages/cmake-rn/CHANGELOG.md b/packages/cmake-rn/CHANGELOG.md index fcfd8ab2..e5f7e17a 100644 --- a/packages/cmake-rn/CHANGELOG.md +++ b/packages/cmake-rn/CHANGELOG.md @@ -1,5 +1,24 @@ # cmake-rn +## 0.4.0 + +### Minor Changes + +- ff34c45: Breaking: `CMAKE_JS_*` defines are no longer injected by default (use --cmake-js to opt-in) +- a336f07: Breaking: Renamed --target to --triplet to free up --target for passing CMake targets +- 2ecf894: Add passing of definitions (-D) to cmake when configuring +- 633dc34: Pass --target to CMake +- ff34c45: Expose includable WEAK_NODE_API_CONFIG to CMake projects + +### Patch Changes + +- 2a30d8d: Refactored CLIs to use a shared utility package +- f82239c: Pretty print spawn errors instead of simply rethrowing to commander. +- 9861bad: Assert the existence of CMakeList.txt before passing control to CMake +- Updated dependencies [2a30d8d] +- Updated dependencies [c72970f] + - react-native-node-api@0.5.1 + ## 0.3.2 ### Patch Changes diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index 74b24aea..f78644da 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -1,6 +1,6 @@ { "name": "cmake-rn", - "version": "0.3.2", + "version": "0.4.0", "description": "Build React Native Node API modules with CMake", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -23,7 +23,7 @@ }, "dependencies": { "@react-native-node-api/cli-utils": "0.1.0", - "react-native-node-api": "0.5.0" + "react-native-node-api": "0.5.1" }, "peerDependencies": { "node-addon-api": "^8.3.1", diff --git a/packages/ferric/CHANGELOG.md b/packages/ferric/CHANGELOG.md index bfcae846..8e3ba40b 100644 --- a/packages/ferric/CHANGELOG.md +++ b/packages/ferric/CHANGELOG.md @@ -1,5 +1,14 @@ # ferric-cli +## 0.3.3 + +### Patch Changes + +- 2a30d8d: Refactored CLIs to use a shared utility package +- Updated dependencies [2a30d8d] +- Updated dependencies [c72970f] + - react-native-node-api@0.5.1 + ## 0.3.2 ### Patch Changes diff --git a/packages/ferric/package.json b/packages/ferric/package.json index 85b8fef6..f114d4e3 100644 --- a/packages/ferric/package.json +++ b/packages/ferric/package.json @@ -1,6 +1,6 @@ { "name": "ferric-cli", - "version": "0.3.2", + "version": "0.3.3", "description": "Rust Node-API Modules for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -18,6 +18,6 @@ "dependencies": { "@napi-rs/cli": "~3.0.3", "@react-native-node-api/cli-utils": "0.1.0", - "react-native-node-api": "0.5.0" + "react-native-node-api": "0.5.1" } } diff --git a/packages/gyp-to-cmake/CHANGELOG.md b/packages/gyp-to-cmake/CHANGELOG.md index bd29ad20..51ecfba8 100644 --- a/packages/gyp-to-cmake/CHANGELOG.md +++ b/packages/gyp-to-cmake/CHANGELOG.md @@ -1,5 +1,15 @@ # gyp-to-cmake +## 0.3.0 + +### Minor Changes + +- ff34c45: Add --weak-node-api option to emit CMake configuration for use with cmake-rn's default way of Node-API linkage. + +### Patch Changes + +- 2a30d8d: Refactored CLIs to use a shared utility package + ## 0.2.0 ### Minor Changes diff --git a/packages/gyp-to-cmake/package.json b/packages/gyp-to-cmake/package.json index e0b5acb4..702a1d1f 100644 --- a/packages/gyp-to-cmake/package.json +++ b/packages/gyp-to-cmake/package.json @@ -1,6 +1,6 @@ { "name": "gyp-to-cmake", - "version": "0.2.0", + "version": "0.3.0", "description": "Convert binding.gyp files to CMakeLists.txt", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { diff --git a/packages/host/CHANGELOG.md b/packages/host/CHANGELOG.md index 4dbf62bb..2b9a1b1c 100644 --- a/packages/host/CHANGELOG.md +++ b/packages/host/CHANGELOG.md @@ -1,5 +1,12 @@ # react-native-node-api +## 0.5.1 + +### Patch Changes + +- 2a30d8d: Refactored CLIs to use a shared utility package +- c72970f: Move REACT_NATIVE_OVERRIDE_HERMES_DIR out of tasks to fail earlier + ## 0.5.0 ### Minor Changes diff --git a/packages/host/package.json b/packages/host/package.json index 57d12c9f..6a547aff 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -1,6 +1,6 @@ { "name": "react-native-node-api", - "version": "0.5.0", + "version": "0.5.1", "description": "Node-API for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { diff --git a/packages/node-tests/package.json b/packages/node-tests/package.json index c708c5ee..cf77504f 100644 --- a/packages/node-tests/package.json +++ b/packages/node-tests/package.json @@ -22,7 +22,7 @@ "cmake-rn": "*", "gyp-to-cmake": "*", "prebuildify": "^6.0.1", - "react-native-node-api": "^0.5.0", + "react-native-node-api": "^0.5.1", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } From 9c6d6062f87453bdefaa98901915aacb02b86e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 28 Sep 2025 19:21:15 +0200 Subject: [PATCH 14/82] Add `cmake-file-api` package, implementing a TypeScript wrapper of the CMake file-based API (#257) * Update shared TS configs * Add new package scaffold * Add copilot instructions * Add package specific instructions * Add a test task * Fix readIndex * Add codemodel * Add target object kind * Add cache object kind * Add cmakeFiles object kind * Add toolchains object kind * Add configureLog object kind * Add separate query functions * Add reply error index * Add an index * Update version mechanism * Make ClientStatefulQueryReply more DRY --- .github/copilot-instructions.md | 77 + .vscode/tasks.json | 14 + configs/tsconfig.node-tests.json | 17 + .../{tsconfig.cli.json => tsconfig.node.json} | 4 +- package-lock.json | 40 +- package.json | 7 +- packages/cli-utils/tsconfig.json | 2 +- packages/cmake-file-api/README.md | 7 + .../cmake-file-api/copilot-instructions.md | 259 +++ .../docs/cmake-file-api.7.rst.txt | 1864 +++++++++++++++++ packages/cmake-file-api/package.json | 33 + packages/cmake-file-api/src/index.ts | 26 + packages/cmake-file-api/src/query.test.ts | 228 ++ packages/cmake-file-api/src/query.ts | 93 + packages/cmake-file-api/src/reply.test.ts | 1075 ++++++++++ packages/cmake-file-api/src/reply.ts | 240 +++ packages/cmake-file-api/src/schemas.ts | 7 + .../src/schemas/ReplyIndexV1.ts | 116 + .../src/schemas/objects/CacheV2.ts | 28 + .../src/schemas/objects/CmakeFilesV1.ts | 45 + .../src/schemas/objects/CodemodelV2.ts | 77 + .../src/schemas/objects/ConfigureLogV1.ts | 17 + .../src/schemas/objects/TargetV2.ts | 253 +++ .../src/schemas/objects/ToolchainsV1.ts | 37 + packages/cmake-file-api/tsconfig.json | 3 + packages/cmake-file-api/tsconfig.tests.json | 8 + tsconfig.json | 2 + 27 files changed, 4563 insertions(+), 16 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 .vscode/tasks.json create mode 100644 configs/tsconfig.node-tests.json rename configs/{tsconfig.cli.json => tsconfig.node.json} (73%) create mode 100644 packages/cmake-file-api/README.md create mode 100644 packages/cmake-file-api/copilot-instructions.md create mode 100644 packages/cmake-file-api/docs/cmake-file-api.7.rst.txt create mode 100644 packages/cmake-file-api/package.json create mode 100644 packages/cmake-file-api/src/index.ts create mode 100644 packages/cmake-file-api/src/query.test.ts create mode 100644 packages/cmake-file-api/src/query.ts create mode 100644 packages/cmake-file-api/src/reply.test.ts create mode 100644 packages/cmake-file-api/src/reply.ts create mode 100644 packages/cmake-file-api/src/schemas.ts create mode 100644 packages/cmake-file-api/src/schemas/ReplyIndexV1.ts create mode 100644 packages/cmake-file-api/src/schemas/objects/CacheV2.ts create mode 100644 packages/cmake-file-api/src/schemas/objects/CmakeFilesV1.ts create mode 100644 packages/cmake-file-api/src/schemas/objects/CodemodelV2.ts create mode 100644 packages/cmake-file-api/src/schemas/objects/ConfigureLogV1.ts create mode 100644 packages/cmake-file-api/src/schemas/objects/TargetV2.ts create mode 100644 packages/cmake-file-api/src/schemas/objects/ToolchainsV1.ts create mode 100644 packages/cmake-file-api/tsconfig.json create mode 100644 packages/cmake-file-api/tsconfig.tests.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..bcd409ce --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,77 @@ +# Copilot Instructions for React Native Node-API + +This is a **monorepo** that brings Node-API support to React Native, enabling native addons written in C/C++/Rust to run on React Native across iOS and Android. + +## Package-Specific Instructions + +**IMPORTANT**: Before working on any package, always check for and read package-specific `copilot-instructions.md` files in the package directory. These contain critical preferences and patterns for that specific package. + +## Architecture Overview + +**Core Flow**: JS `require("./addon.node")` → Babel transform → `requireNodeAddon()` TurboModule call → native library loading → Node-API module initialization + +### Package Architecture + +See the [README.md](../README.md#packages) for detailed descriptions of each package and their roles in the system. Key packages include: + +- `packages/host` - Core Node-API runtime and Babel plugin +- `packages/cmake-rn` - CMake wrapper for native builds +- `packages/cmake-file-api` - TypeScript wrapper for CMake File API with Zod validation +- `packages/ferric` - Rust/Cargo wrapper with napi-rs integration +- `packages/gyp-to-cmake` - Legacy binding.gyp compatibility +- `apps/test-app` - Integration testing harness + +## Critical Build Dependencies + +- **Custom Hermes**: Currently depends on a patched Hermes with Node-API support (see [facebook/hermes#1377](https://github.com/facebook/hermes/pull/1377)) +- **Prebuilt Binary Spec**: All tools must output to the exact naming scheme: + - Android: `*.android.node/` with jniLibs structure + `react-native-node-api-module` marker file + - iOS: `*.apple.node` (XCFramework renamed) + marker file + +## Essential Workflows + +### Development Setup + +```bash +npm ci && npm run build # Install deps and build all packages +npm run bootstrap # Build native components (weak-node-api, examples) +``` + +### Package Development + +- **TypeScript project references**: Use `tsc --build` for incremental compilation +- **Workspace scripts**: Most build/test commands use npm workspaces (`--workspace` flag) +- **Focus on Node.js packages**: AI development primarily targets the Node.js tooling packages rather than native mobile code +- **No TypeScript type asserts**: You have to ask explicitly and justify if you want to add `as` type assertions. + +## Key Patterns + +### Babel Transformation + +The core magic happens in `packages/host/src/node/babel-plugin/plugin.ts`: + +```js +// Input: require("./addon.node") +// Output: require("react-native-node-api").requireNodeAddon("pkg-name--addon") +``` + +### CMake Integration + +For linking against Node-API in CMakeLists.txt: + +```cmake +include(${WEAK_NODE_API_CONFIG}) +target_link_libraries(addon PRIVATE weak-node-api) +``` + +### Cross-Platform Naming + +Library names use double-dash separation: `package-name--path-component--addon-name` + +### Testing + +- **Individual packages**: Some packages have VS Code test tasks and others have their own `npm test` scripts for focused iteration (e.g., `npm test --workspace cmake-rn`). Use the latter only if the former is missing. +- **Cross-package**: Use root-level `npm test` for cross-package testing once individual package tests pass +- **Mobile integration**: Available but not the primary AI development focus - ask the developer to run those tests as needed + +**Documentation**: Integration details, platform setup, and toolchain configuration are covered in existing repo documentation files. diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..f0c624e1 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,14 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Test cmake-file-api", + "command": "node", + "args": ["--run", "test"], + "options": { + "cwd": "${workspaceFolder}/packages/cmake-file-api" + }, + "group": "test" + } + ] +} diff --git a/configs/tsconfig.node-tests.json b/configs/tsconfig.node-tests.json new file mode 100644 index 00000000..642ba5ac --- /dev/null +++ b/configs/tsconfig.node-tests.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "declarationMap": false + }, + "include": ["${configDir}/src/**/*.test.ts"], + "exclude": [] + /* + "references": [ + { + "path": "./tsconfig.json" + } + ] + */ +} diff --git a/configs/tsconfig.cli.json b/configs/tsconfig.node.json similarity index 73% rename from configs/tsconfig.cli.json rename to configs/tsconfig.node.json index 4e77cced..ff37e16c 100644 --- a/configs/tsconfig.cli.json +++ b/configs/tsconfig.node.json @@ -7,6 +7,6 @@ "rootDir": "${configDir}/src", "types": ["node"] }, - "include": ["${configDir}/src/*.ts"], - "exclude": ["${configDir}/**.test.ts"] + "include": ["${configDir}/src/"], + "exclude": ["${configDir}/**/*.test.ts"] } diff --git a/package-lock.json b/package-lock.json index 96a644fc..02024acc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,15 +7,16 @@ "name": "@react-native-node-api/root", "license": "MIT", "workspaces": [ - "apps/test-app", "packages/cli-utils", - "packages/gyp-to-cmake", + "packages/cmake-file-api", "packages/cmake-rn", "packages/ferric", + "packages/gyp-to-cmake", "packages/host", "packages/node-addon-examples", "packages/node-tests", - "packages/ferric-example" + "packages/ferric-example", + "apps/test-app" ], "devDependencies": { "@changesets/cli": "^2.29.5", @@ -7909,6 +7910,10 @@ "node": ">=0.8" } }, + "node_modules/cmake-file-api": { + "resolved": "packages/cmake-file-api", + "link": true + }, "node_modules/cmake-js": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-7.3.1.tgz", @@ -14784,11 +14789,26 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "packages/cmake-file-api": { + "version": "0.1.0", + "dependencies": { + "zod": "^4.1.11" + } + }, + "packages/cmake-file-api/node_modules/zod": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", + "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/cmake-rn": { - "version": "0.3.2", + "version": "0.4.0", "dependencies": { "@react-native-node-api/cli-utils": "0.1.0", - "react-native-node-api": "0.5.0" + "react-native-node-api": "0.5.1" }, "bin": { "cmake-rn": "bin/cmake-rn.js" @@ -14800,11 +14820,11 @@ }, "packages/ferric": { "name": "ferric-cli", - "version": "0.3.2", + "version": "0.3.3", "dependencies": { "@napi-rs/cli": "~3.0.3", "@react-native-node-api/cli-utils": "0.1.0", - "react-native-node-api": "0.5.0" + "react-native-node-api": "0.5.1" }, "bin": { "ferric": "bin/ferric.js" @@ -14818,7 +14838,7 @@ } }, "packages/gyp-to-cmake": { - "version": "0.2.0", + "version": "0.3.0", "dependencies": { "@react-native-node-api/cli-utils": "0.1.0", "gyp-parser": "^1.0.4" @@ -14829,7 +14849,7 @@ }, "packages/host": { "name": "react-native-node-api", - "version": "0.5.0", + "version": "0.5.1", "license": "MIT", "dependencies": { "@react-native-node-api/cli-utils": "0.1.0", @@ -14869,7 +14889,7 @@ "cmake-rn": "*", "gyp-to-cmake": "*", "prebuildify": "^6.0.1", - "react-native-node-api": "^0.5.0", + "react-native-node-api": "^0.5.1", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } diff --git a/package.json b/package.json index c20b5813..699dbe3f 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,16 @@ "type": "module", "private": true, "workspaces": [ - "apps/test-app", "packages/cli-utils", - "packages/gyp-to-cmake", + "packages/cmake-file-api", "packages/cmake-rn", "packages/ferric", + "packages/gyp-to-cmake", "packages/host", "packages/node-addon-examples", "packages/node-tests", - "packages/ferric-example" + "packages/ferric-example", + "apps/test-app" ], "homepage": "https://github.com/callstackincubator/react-native-node-api#readme", "scripts": { diff --git a/packages/cli-utils/tsconfig.json b/packages/cli-utils/tsconfig.json index aa43e9d9..f183b9a9 100644 --- a/packages/cli-utils/tsconfig.json +++ b/packages/cli-utils/tsconfig.json @@ -1,3 +1,3 @@ { - "extends": "../../configs/tsconfig.cli.json" + "extends": "../../configs/tsconfig.node.json" } diff --git a/packages/cmake-file-api/README.md b/packages/cmake-file-api/README.md new file mode 100644 index 00000000..50dce7ea --- /dev/null +++ b/packages/cmake-file-api/README.md @@ -0,0 +1,7 @@ +# CMake File API (unofficial) + +The CMake File API provides an interface for querying CMake's configuration and project information. + +The API is based on files, where queries are written by client tools and read by CMake and replies are then written by CMake and read by client tools. The API is versioned, and the current version is v1 and these files are located in a directory named `.cmake/api/v1` in the build directory. + +This package provides a TypeScript interface to create query files and read replies and is intended to serve the same purpose to the TypeScript community that the [`cmake-file-api` crate](https://crates.io/crates/cmake-file-api), serves to the Rust community. diff --git a/packages/cmake-file-api/copilot-instructions.md b/packages/cmake-file-api/copilot-instructions.md new file mode 100644 index 00000000..c6def984 --- /dev/null +++ b/packages/cmake-file-api/copilot-instructions.md @@ -0,0 +1,259 @@ +# Copilot Instructions for cmake-file-api Package + +This package provides a TypeScript wrapper around the CMake File API using Zod schemas for validation. + +## Code Style Preferences + +### Node.js Built-ins + +- **Always** use Node.js built-ins with the `node:` prefix (e.g., `node:fs`, `node:path`, `node:assert/strict`) +- Prefer async APIs where possible (e.g., `fs.promises.readFile`, `fs.promises.writeFile`) + +### Schema Validation + +- Use **Zod** for all schema validation with strict typing +- Follow the CMake File API v1 specification precisely - read the documentation in `docs/cmake-file-api.7.rst.txt` +- Use `z.enum()` instead of generic strings for known enumeration values +- Make record values optional when they might not exist (e.g., `z.record(key, value.optional())`) +- **Keep schema files clean** - avoid inline comments except when strictly necessary for clarity +- **Use `index = z.number().int().min(0)`** for all *Index and *Indexes fields (they are documented as "unsigned integer 0-based index" in the CMake File API) + +### TypeScript Patterns + +- **No TypeScript type assertions (`as`)** unless explicitly justified +- Use destructuring to extract values from objects instead of accessing object properties repeatedly +- Prefer explicit assertions with meaningful messages over implicit type assumptions + +### Testing + +- Use Node.js built-in test runner and run test using the "Test cmake-file-api" task in VS Code +- **Prefer `assert.deepStrictEqual(result, mockData)`** over individual field assertions for schema validation +- Create comprehensive test cases that validate the complete schema structure +- Use proper type guards with assertions when dealing with optional values +- Test both positive cases (valid data) and ensure schemas properly validate structure + +### Error Handling + +- Use `assert` from `node:assert/strict` for runtime validation +- Provide descriptive error messages that help with debugging +- Handle CMake File API error objects properly (they have an `error` field instead of the expected structure) + +## Architecture Patterns + +### Schema Organization + +- Export schemas with versioned names (e.g., `ReplyFileReferenceV1`, `IndexReplyV1`) +- Organize related schemas in dedicated files under `src/schemas/` +- Keep the main API functions in `src/reply.ts` and `src/query.ts` + +### Minor Version Schema Pattern + +When implementing schemas that support multiple minor versions (as documented in CMake File API), use this hierarchical extension pattern: + +1. **Base Version Schema**: Create the earliest version as the base (e.g., `DirectoryV2_0`) +2. **Extended Version Schemas**: Use `.extend()` to add new fields for later versions (e.g., `DirectoryV2_3 = DirectoryV2_0.extend({...})`) +3. **Hierarchical Composition**: Parent schemas should also follow this pattern (e.g., `ConfigurationV2_3 = ConfigurationV2_0.extend({...})`) +4. **Version Constraints**: Use `minor: z.number().max(X)` for earlier versions and `minor: z.number().min(X)` for later versions +5. **Union Export**: Combine all versions using `z.union([SchemaV2_0, SchemaV2_3])` and export as the main schema name + +This pattern ensures: + +- Type safety across different minor versions +- Proper validation based on version numbers +- Clear inheritance hierarchy +- Backward compatibility support + +### Context-Dependent Object Versioning + +For objects that don't contain version information themselves (like Target objects), use the versioned extension pattern and export a union of all versions. Keep it DRY by versioning nested schemas separately: + +```typescript +// Version nested schemas separately to avoid duplication +const SourceV2_0 = z.object({ + path: z.string(), + // ... base fields +}); + +const SourceV2_5 = SourceV2_0.extend({ + fileSetIndex: index.optional(), // Added in v2.5 +}); + +const CompileGroupV2_0 = z.object({ + sourceIndexes: z.array(index), + language: z.string(), + // ... base fields +}); + +const CompileGroupV2_1 = CompileGroupV2_0.extend({ + precompileHeaders: z.array(PrecompileHeader).optional(), // Added in v2.1 +}); + +// Build main object versions using versioned nested schemas +const TargetV2_0 = z.object({ + // ... base fields + sources: z.array(SourceV2_0).optional(), + compileGroups: z.array(CompileGroupV2_0).optional(), +}); + +const TargetV2_1 = TargetV2_0.extend({ + compileGroups: z.array(CompileGroupV2_1).optional(), // Use versioned nested schema +}); + +const TargetV2_5 = TargetV2_2.extend({ + fileSets: z.array(FileSet).optional(), + sources: z.array(SourceV2_5).optional(), // Use versioned nested schema +}); + +// Export union of all versions for flexible validation +export const TargetV2 = z.union([ + TargetV2_0, + TargetV2_1, + TargetV2_2, + TargetV2_5, + TargetV2_6, + TargetV2_7, + TargetV2_8, +]); + +// Also export individual versions for specific use cases +export { + TargetV2_0, + TargetV2_1, + TargetV2_2, + TargetV2_5, + TargetV2_6, + TargetV2_7, + TargetV2_8, +}; +``` + +Then, reader functions should accept an optional schema parameter defaulting to the latest version: + +```typescript +export async function readTarget( + filePath: string, + schema: z.ZodSchema = TargetV2_8, // Default to latest version +) { + // ... implementation +} +``` + +This approach provides flexibility while maintaining type safety, avoiding code duplication, and allowing callers to specify the exact version schema when needed. + +### Function Design + +- Functions should be async where file I/O is involved +- Use clear, descriptive function names that indicate their purpose +- Validate file paths and extensions before processing +- Parse and validate JSON using Zod schemas rather than manual type checking + +### Documentation References + +- Always refer to the official CMake File API documentation +- The specification is available in `docs/cmake-file-api.7.rst.txt` +- When implementing Object Kinds, check the docs for exact field requirements and optional properties. Pay attention to indention in the document as it indicates nested structures. + +## Example Patterns + +### Good Schema Pattern + +```typescript +const index = z.number().int().min(0); + +export const MySchemaV1 = z.object({ + kind: z.enum(["validValue1", "validValue2"]), + optionalField: z.string().optional(), + parentIndex: index.optional(), // For *Index fields + childIndexes: z.array(index).optional(), // For *Indexes fields + requiredNested: z.object({ + major: z.number(), + minor: z.number(), + }), +}); +``` + +### Good Minor Version Schema Pattern + +```typescript +// Base version schema (earliest version) +const ItemV2_0 = z.object({ + name: z.string(), + type: z.enum(["TYPE1", "TYPE2"]), + paths: z.object({ + source: z.string(), + build: z.string(), + }), +}); + +// Extended version schema (adds fields introduced in v2.3) +const ItemV2_3 = ItemV2_0.extend({ + jsonFile: z.string(), + metadata: z + .object({ + version: z.string(), + }) + .optional(), +}); + +// Parent schema versions +const ContainerV2_0 = z.object({ + kind: z.literal("container"), + version: z.object({ + major: z.literal(2), + minor: z.number().max(2), // Versions 2.0-2.2 + }), + items: z.array(ItemV2_0), +}); + +const ContainerV2_3 = ContainerV2_0.extend({ + version: z.object({ + major: z.literal(2), + minor: z.number().min(3), // Versions 2.3+ + }), + items: z.array(ItemV2_3), +}); + +// Union export for all versions +export const ContainerV2 = z.union([ContainerV2_0, ContainerV2_3]); +``` + +### Good Function Pattern + +```typescript +export async function readSomething(filePath: string) { + assert( + path.basename(filePath).startsWith("expected-") && + path.extname(filePath) === ".json", + "Expected a path to an expected-*.json file", + ); + const content = await fs.promises.readFile(filePath, "utf-8"); + const { field1, field2 } = MySchemaV1.parse(JSON.parse(content)); + // Use destructured values directly + return { field1, field2 }; +} +``` + +### Good Test Pattern + +```typescript +it("validates complete structure", async function (context) { + const mockData = { + // Complete, realistic test data based on CMake File API docs + field1: "expectedValue", + field2: { nested: "structure" }, + optionalField: "presentValue", + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["example-file.json", mockData], + ]); + const result = await readSomething(path.join(tmpPath, "example-file.json")); + + // Prefer deepStrictEqual for complete schema validation + assert.deepStrictEqual(result, mockData); + + // Only use individual assertions when testing specific edge cases + // const optionalValue = result.optionalField; + // assert(optionalValue, "Expected optional field to exist in this test case"); +}); +``` diff --git a/packages/cmake-file-api/docs/cmake-file-api.7.rst.txt b/packages/cmake-file-api/docs/cmake-file-api.7.rst.txt new file mode 100644 index 00000000..4211c572 --- /dev/null +++ b/packages/cmake-file-api/docs/cmake-file-api.7.rst.txt @@ -0,0 +1,1864 @@ +The following is a snapshot of https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html + +.. cmake-manual-description: CMake File-Based API + +cmake-file-api(7) +***************** + +.. only:: html + + .. contents:: + +Introduction +============ + +CMake provides a file-based API that clients may use to get semantic +information about the buildsystems CMake generates. Clients may use +the API by writing query files to a specific location in a build tree +to request zero or more `Object Kinds`_. When CMake generates the +buildsystem in that build tree it will read the query files and write +reply files for the client to read. + +The file-based API uses a ``/.cmake/api/`` directory at the top +of a build tree. The API is versioned to support changes to the layout +of files within the API directory. API file layout versioning is +orthogonal to the versioning of `Object Kinds`_ used in replies. +This version of CMake supports only one API version, `API v1`_. + +.. versionadded:: 3.27 + Projects may also submit queries for the current run using the + :command:`cmake_file_api` command. + +.. _`file-api v1`: + +API v1 +====== + +API v1 is housed in the ``/.cmake/api/v1/`` directory. +It has the following subdirectories: + +``query/`` + Holds query files written by clients. + These may be `v1 Shared Stateless Query Files`_, + `v1 Client Stateless Query Files`_, or `v1 Client Stateful Query Files`_. + +``reply/`` + Holds reply files written by CMake when it runs to generate a build system. + Clients may read reply files only when referenced by a reply index: + + ``index-*.json`` + A `v1 Reply Index File`_ written when CMake generates a build system. + + ``error-*.json`` + .. versionadded:: 4.1 + + A `v1 Reply Error Index`_ written when CMake fails to generate a build + system due to an error. + + Clients may look for and read a reply index at any time. + Clients may optionally create the ``reply/`` directory at any time + and monitor it for the appearance of a new reply index. + CMake owns all reply files. Clients must never remove them. + +.. versionadded:: 3.31 + Users can add query files to ``api/v1/query`` inside the + :envvar:`CMAKE_CONFIG_DIR` to create user-wide queries for all CMake projects. + +v1 Shared Stateless Query Files +------------------------------- + +Shared stateless query files allow clients to share requests for +major versions of the `Object Kinds`_ and get all requested versions +recognized by the CMake that runs. + +Clients may create shared requests by creating empty files in the +``v1/query/`` directory. The form is:: + + /.cmake/api/v1/query/-v + +where ```` is one of the `Object Kinds`_, ``-v`` is literal, +and ```` is the major version number. + +Files of this form are stateless shared queries not owned by any specific +client. Once created they should not be removed without external client +coordination or human intervention. + +v1 Client Stateless Query Files +------------------------------- + +Client stateless query files allow clients to create owned requests for +major versions of the `Object Kinds`_ and get all requested versions +recognized by the CMake that runs. + +Clients may create owned requests by creating empty files in +client-specific query subdirectories. The form is:: + + /.cmake/api/v1/query/client-/-v + +where ``client-`` is literal, ```` is a string uniquely +identifying the client, ```` is one of the `Object Kinds`_, +``-v`` is literal, and ```` is the major version number. +Each client must choose a unique ```` identifier via its +own means. + +Files of this form are stateless queries owned by the client ````. +The owning client may remove them at any time. + +v1 Client Stateful Query Files +------------------------------ + +Stateful query files allow clients to request a list of versions of +each of the `Object Kinds`_ and get only the most recent version +recognized by the CMake that runs. + +Clients may create owned stateful queries by creating ``query.json`` +files in client-specific query subdirectories. The form is:: + + /.cmake/api/v1/query/client-/query.json + +where ``client-`` is literal, ```` is a string uniquely +identifying the client, and ``query.json`` is literal. Each client +must choose a unique ```` identifier via its own means. + +``query.json`` files are stateful queries owned by the client ````. +The owning client may update or remove them at any time. When a +given client installation is updated it may then update the stateful +query it writes to build trees to request newer object versions. +This can be used to avoid asking CMake to generate multiple object +versions unnecessarily. + +A ``query.json`` file must contain a JSON object: + +.. code-block:: json + + { + "requests": [ + { "kind": "" , "version": 1 }, + { "kind": "" , "version": { "major": 1, "minor": 2 } }, + { "kind": "" , "version": [2, 1] }, + { "kind": "" , "version": [2, { "major": 1, "minor": 2 }] }, + { "kind": "" , "version": 1, "client": {} }, + { "kind": "..." } + ], + "client": {} + } + +The members are: + +``requests`` + A JSON array containing zero or more requests. Each request is + a JSON object with members: + + ``kind`` + Specifies one of the `Object Kinds`_ to be included in the reply. + + ``version`` + Indicates the version(s) of the object kind that the client + understands. Versions have major and minor components following + semantic version conventions. The value must be + + * a JSON integer specifying a (non-negative) major version number, or + * a JSON object containing ``major`` and (optionally) ``minor`` + members specifying non-negative integer version components, or + * a JSON array whose elements are each one of the above. + + ``client`` + Optional member reserved for use by the client. This value is + preserved in the reply written for the client in the + `v1 Reply Index File`_ but is otherwise ignored. Clients may use + this to pass custom information with a request through to its reply. + + For each requested object kind CMake will choose the *first* version + that it recognizes for that kind among those listed in the request. + The response will use the selected *major* version with the highest + *minor* version known to the running CMake for that major version. + Therefore clients should list all supported major versions in + preferred order along with the minimal minor version required + for each major version. + +``client`` + Optional member reserved for use by the client. This value is + preserved in the reply written for the client in the + `v1 Reply Index File`_ but is otherwise ignored. Clients may use + this to pass custom information with a query through to its reply. + +Other ``query.json`` top-level members are reserved for future use. +If present they are ignored for forward compatibility. + +v1 Reply Index File +------------------- + +CMake writes an ``index-*.json`` file to the ``v1/reply/`` directory +when it successfully generates a build system. Clients must read the +reply index file first and may read other `v1 Reply Files`_ only by +following references. The form of the reply index file name is:: + + /.cmake/api/v1/reply/index-.json + +where ``index-`` is literal and ```` is an unspecified +name selected by CMake. Whenever a new index file is generated it +is given a new name and any old one is deleted. During the short +time between these steps there may be multiple index files present; +the one with the largest name in lexicographic order is the current +index file. + +The reply index file contains a JSON object: + +.. code-block:: json + + { + "cmake": { + "version": { + "major": 3, "minor": 14, "patch": 0, "suffix": "", + "string": "3.14.0", "isDirty": false + }, + "paths": { + "cmake": "/prefix/bin/cmake", + "ctest": "/prefix/bin/ctest", + "cpack": "/prefix/bin/cpack", + "root": "/prefix/share/cmake-3.14" + }, + "generator": { + "multiConfig": false, + "name": "Unix Makefiles" + } + }, + "objects": [ + { "kind": "", + "version": { "major": 1, "minor": 0 }, + "jsonFile": "" }, + { "...": "..." } + ], + "reply": { + "-v": { "kind": "", + "version": { "major": 1, "minor": 0 }, + "jsonFile": "" }, + "": { "error": "unknown query file" }, + "...": {}, + "client-": { + "-v": { "kind": "", + "version": { "major": 1, "minor": 0 }, + "jsonFile": "" }, + "": { "error": "unknown query file" }, + "...": {}, + "query.json": { + "requests": [ {}, {}, {} ], + "responses": [ + { "kind": "", + "version": { "major": 1, "minor": 0 }, + "jsonFile": "" }, + { "error": "unknown query file" }, + { "...": {} } + ], + "client": {} + } + } + } + } + +The members are: + +``cmake`` + A JSON object containing information about the instance of CMake that + generated the reply. It contains members: + + ``version`` + A JSON object specifying the version of CMake with members: + + ``major``, ``minor``, ``patch`` + Integer values specifying the major, minor, and patch version components. + ``suffix`` + A string specifying the version suffix, if any, e.g. ``g0abc3``. + ``string`` + A string specifying the full version in the format + ``..[-]``. + ``isDirty`` + A boolean indicating whether the version was built from a version + controlled source tree with local modifications. + + ``paths`` + A JSON object specifying paths to things that come with CMake. + It has members for :program:`cmake`, :program:`ctest`, and :program:`cpack` + whose values are JSON strings specifying the absolute path to each tool, + represented with forward slashes. It also has a ``root`` member for + the absolute path to the directory containing CMake resources like the + ``Modules/`` directory (see :variable:`CMAKE_ROOT`). + + ``generator`` + A JSON object describing the CMake generator used for the build. + It has members: + + ``multiConfig`` + A boolean specifying whether the generator supports multiple output + configurations. + ``name`` + A string specifying the name of the generator. + ``platform`` + If the generator supports :variable:`CMAKE_GENERATOR_PLATFORM`, + this is a string specifying the generator platform name. + +``objects`` + A JSON array listing all versions of all `Object Kinds`_ generated + as part of the reply. Each array entry is a + `v1 Reply File Reference`_. + +``reply`` + A JSON object mirroring the content of the ``query/`` directory + that CMake loaded to produce the reply. The members are of the form + + ``-v`` + A member of this form appears for each of the + `v1 Shared Stateless Query Files`_ that CMake recognized as a + request for object kind ```` with major version ````. + The value is + + * a `v1 Reply File Reference`_ to the corresponding reply file for + that object kind and version, or + * in a `v1 Reply Error Index`_, a JSON object with a single ``error`` + member containing a string with an error message. + + ```` + A member of this form appears for each of the + `v1 Shared Stateless Query Files`_ that CMake did not recognize. + The value is a JSON object with a single ``error`` member + containing a string with an error message indicating that the + query file is unknown. + + ``client-`` + A member of this form appears for each client-owned directory + holding `v1 Client Stateless Query Files`_. + The value is a JSON object mirroring the content of the + ``query/client-/`` directory. The members are of the form: + + ``-v`` + A member of this form appears for each of the + `v1 Client Stateless Query Files`_ that CMake recognized as a + request for object kind ```` with major version ````. + The value is + + * a `v1 Reply File Reference`_ to the corresponding reply file for + that object kind and version, or + * in a `v1 Reply Error Index`_, a JSON object with a single ``error`` + member containing a string with an error message. + + ```` + A member of this form appears for each of the + `v1 Client Stateless Query Files`_ that CMake did not recognize. + The value is a JSON object with a single ``error`` member + containing a string with an error message indicating that the + query file is unknown. + + ``query.json`` + This member appears for clients using + `v1 Client Stateful Query Files`_. + If the ``query.json`` file failed to read or parse as a JSON object, + this member is a JSON object with a single ``error`` member + containing a string with an error message. Otherwise, this member + is a JSON object mirroring the content of the ``query.json`` file. + The members are: + + ``client`` + A copy of the ``query.json`` file ``client`` member, if it exists. + + ``requests`` + A copy of the ``query.json`` file ``requests`` member, if it exists. + + ``responses`` + If the ``query.json`` file ``requests`` member is missing or invalid, + this member is a JSON object with a single ``error`` member + containing a string with an error message. Otherwise, this member + contains a JSON array with a response for each entry of the + ``requests`` array, in the same order. Each response is + + * a `v1 Reply File Reference`_ to the corresponding reply file for + the requested object kind and selected version, or + * a JSON object with a single ``error`` member containing a string + with an error message. + +After reading the reply index file, clients may read the other +`v1 Reply Files`_ it references. + +v1 Reply File Reference +^^^^^^^^^^^^^^^^^^^^^^^ + +The reply index file represents each reference to another reply file +using a JSON object with members: + +``kind`` + A string specifying one of the `Object Kinds`_. +``version`` + A JSON object with members ``major`` and ``minor`` specifying + integer version components of the object kind. +``jsonFile`` + A JSON string specifying a path relative to the reply index file + to another JSON file containing the object. + +.. _`file-api reply error index`: + +v1 Reply Error Index +^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 4.1 + +CMake writes an ``error-*.json`` file to the ``v1/reply/`` directory +when it fails to generate a build system. This reply error index +follows the same naming pattern, syntax, and semantics of a +`v1 Reply Index File`_, with the following exceptions: + +* The ``index-`` prefix is replaced by an ``error-`` prefix. + +* When a new error index is generated, old index files are *not* + deleted. If a `v1 Reply Index File`_ exists, it indexes replies + from the most recent successful run. If multiple ``index-*.json`` + and/or ``error-*.json`` files are present, the one with the largest + name in lexicographic order, excluding the ``index-`` or ``error-`` + prefix, is the current index. + +* Only a subset of `Object Kinds`_ are provided: + + `configureLog `_ + .. versionadded:: 4.1 + + Index entries for other object kinds contain an ``error`` message + instead of a `v1 Reply File Reference`_. + +v1 Reply Files +-------------- + +Reply files containing specific `Object Kinds`_ are written by CMake. +The names of these files are unspecified and must not be interpreted +by clients. Clients must first read the `v1 Reply Index File`_ and +follow references to the names of the desired response objects. + +Reply files (including the index file) will never be replaced by +files of the same name but different content. This allows a client +to read the files concurrently with a running CMake that may generate +a new reply. However, after generating a new reply CMake will attempt +to remove reply files from previous runs that it did not just write. +If a client attempts to read a reply file referenced by the index but +finds the file missing, that means a concurrent CMake has generated +a new reply. The client may simply start again by reading the new +reply index file. + +.. _`file-api object kinds`: + +Object Kinds +============ + +The CMake file-based API reports semantic information about the build +system using the following kinds of JSON objects. Each kind of object +is versioned independently using semantic versioning with major and +minor components. Every kind of object has the form: + +.. code-block:: json + + { + "kind": "", + "version": { "major": 1, "minor": 0 }, + "...": {} + } + +The ``kind`` member is a string specifying the object kind name. +The ``version`` member is a JSON object with ``major`` and ``minor`` +members specifying integer components of the object kind's version. +Additional top-level members are specific to each object kind. + +Object Kind "codemodel" +----------------------- + +The ``codemodel`` object kind describes the build system structure as +modeled by CMake. + +There is only one ``codemodel`` object major version, version 2. +Version 1 does not exist to avoid confusion with that from +:manual:`cmake-server(7)` mode. + +"codemodel" version 2 +^^^^^^^^^^^^^^^^^^^^^ + +``codemodel`` object version 2 is a JSON object: + +.. code-block:: json + + { + "kind": "codemodel", + "version": { "major": 2, "minor": 8 }, + "paths": { + "source": "/path/to/top-level-source-dir", + "build": "/path/to/top-level-build-dir" + }, + "configurations": [ + { + "name": "Debug", + "directories": [ + { + "source": ".", + "build": ".", + "childIndexes": [ 1 ], + "projectIndex": 0, + "targetIndexes": [ 0 ], + "hasInstallRule": true, + "minimumCMakeVersion": { + "string": "3.14" + }, + "jsonFile": "" + }, + { + "source": "sub", + "build": "sub", + "parentIndex": 0, + "projectIndex": 0, + "targetIndexes": [ 1 ], + "minimumCMakeVersion": { + "string": "3.14" + }, + "jsonFile": "" + } + ], + "projects": [ + { + "name": "MyProject", + "directoryIndexes": [ 0, 1 ], + "targetIndexes": [ 0, 1 ] + } + ], + "targets": [ + { + "name": "MyExecutable", + "directoryIndex": 0, + "projectIndex": 0, + "jsonFile": "" + }, + { + "name": "MyLibrary", + "directoryIndex": 1, + "projectIndex": 0, + "jsonFile": "" + } + ] + } + ] + } + +The members specific to ``codemodel`` objects are: + +``paths`` + A JSON object containing members: + + ``source`` + A string specifying the absolute path to the top-level source directory, + represented with forward slashes. + + ``build`` + A string specifying the absolute path to the top-level build directory, + represented with forward slashes. + +``configurations`` + A JSON array of entries corresponding to available build configurations. + On single-configuration generators there is one entry for the value + of the :variable:`CMAKE_BUILD_TYPE` variable. For multi-configuration + generators there is an entry for each configuration listed in the + :variable:`CMAKE_CONFIGURATION_TYPES` variable. + Each entry is a JSON object containing members: + + ``name`` + A string specifying the name of the configuration, e.g. ``Debug``. + + ``directories`` + A JSON array of entries each corresponding to a build system directory + whose source directory contains a ``CMakeLists.txt`` file. The first + entry corresponds to the top-level directory. Each entry is a + JSON object containing members: + + ``source`` + A string specifying the path to the source directory, represented + with forward slashes. If the directory is inside the top-level + source directory then the path is specified relative to that + directory (with ``.`` for the top-level source directory itself). + Otherwise the path is absolute. + + ``build`` + A string specifying the path to the build directory, represented + with forward slashes. If the directory is inside the top-level + build directory then the path is specified relative to that + directory (with ``.`` for the top-level build directory itself). + Otherwise the path is absolute. + + ``parentIndex`` + Optional member that is present when the directory is not top-level. + The value is an unsigned integer 0-based index of another entry in + the main ``directories`` array that corresponds to the parent + directory that added this directory as a subdirectory. + + ``childIndexes`` + Optional member that is present when the directory has subdirectories. + The value is a JSON array of entries corresponding to child directories + created by the :command:`add_subdirectory` or :command:`subdirs` + command. Each entry is an unsigned integer 0-based index of another + entry in the main ``directories`` array. + + ``projectIndex`` + An unsigned integer 0-based index into the main ``projects`` array + indicating the build system project to which the this directory belongs. + + ``targetIndexes`` + Optional member that is present when the directory itself has targets, + excluding those belonging to subdirectories. The value is a JSON + array of entries corresponding to the targets. Each entry is an + unsigned integer 0-based index into the main ``targets`` array. + + ``minimumCMakeVersion`` + Optional member present when a minimum required version of CMake is + known for the directory. This is the ```` version given to the + most local call to the :command:`cmake_minimum_required(VERSION)` + command in the directory itself or one of its ancestors. + The value is a JSON object with one member: + + ``string`` + A string specifying the minimum required version in the format:: + + .[.[.]][] + + Each component is an unsigned integer and the suffix may be an + arbitrary string. + + ``hasInstallRule`` + Optional member that is present with boolean value ``true`` when + the directory or one of its subdirectories contains any + :command:`install` rules, i.e. whether a ``make install`` + or equivalent rule is available. + + ``jsonFile`` + A JSON string specifying a path relative to the codemodel file + to another JSON file containing a + `"codemodel" version 2 "directory" object`_. + + This field was added in codemodel version 2.3. + + ``projects`` + A JSON array of entries corresponding to the top-level project + and sub-projects defined in the build system. Each (sub-)project + corresponds to a source directory whose ``CMakeLists.txt`` file + calls the :command:`project` command with a project name different + from its parent directory. The first entry corresponds to the + top-level project. + + Each entry is a JSON object containing members: + + ``name`` + A string specifying the name given to the :command:`project` command. + + ``parentIndex`` + Optional member that is present when the project is not top-level. + The value is an unsigned integer 0-based index of another entry in + the main ``projects`` array that corresponds to the parent project + that added this project as a sub-project. + + ``childIndexes`` + Optional member that is present when the project has sub-projects. + The value is a JSON array of entries corresponding to the sub-projects. + Each entry is an unsigned integer 0-based index of another + entry in the main ``projects`` array. + + ``directoryIndexes`` + A JSON array of entries corresponding to build system directories + that are part of the project. The first entry corresponds to the + top-level directory of the project. Each entry is an unsigned + integer 0-based index into the main ``directories`` array. + + ``targetIndexes`` + Optional member that is present when the project itself has targets, + excluding those belonging to sub-projects. The value is a JSON + array of entries corresponding to the targets. Each entry is an + unsigned integer 0-based index into the main ``targets`` array. + + ``targets`` + A JSON array of entries corresponding to the build system targets. + Such targets are created by calls to :command:`add_executable`, + :command:`add_library`, and :command:`add_custom_target`, excluding + imported targets and interface libraries (which do not generate any + build rules). Each entry is a JSON object containing members: + + ``name`` + A string specifying the target name. + + ``id`` + A string uniquely identifying the target. This matches the ``id`` + field in the file referenced by ``jsonFile``. + + ``directoryIndex`` + An unsigned integer 0-based index into the main ``directories`` array + indicating the build system directory in which the target is defined. + + ``projectIndex`` + An unsigned integer 0-based index into the main ``projects`` array + indicating the build system project in which the target is defined. + + ``jsonFile`` + A JSON string specifying a path relative to the codemodel file + to another JSON file containing a + `"codemodel" version 2 "target" object`_. + +"codemodel" version 2 "directory" object +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A codemodel "directory" object is referenced by a `"codemodel" version 2`_ +object's ``directories`` array. Each "directory" object is a JSON object +with members: + +``paths`` + A JSON object containing members: + + ``source`` + A string specifying the path to the source directory, represented + with forward slashes. If the directory is inside the top-level + source directory then the path is specified relative to that + directory (with ``.`` for the top-level source directory itself). + Otherwise the path is absolute. + + ``build`` + A string specifying the path to the build directory, represented + with forward slashes. If the directory is inside the top-level + build directory then the path is specified relative to that + directory (with ``.`` for the top-level build directory itself). + Otherwise the path is absolute. + +``installers`` + A JSON array of entries corresponding to :command:`install` rules. + Each entry is a JSON object containing members: + + ``component`` + A string specifying the component selected by the corresponding + :command:`install` command invocation. + + ``destination`` + Optional member that is present for specific ``type`` values below. + The value is a string specifying the install destination path. + The path may be absolute or relative to the install prefix. + + ``paths`` + Optional member that is present for specific ``type`` values below. + The value is a JSON array of entries corresponding to the paths + (files or directories) to be installed. Each entry is one of: + + * A string specifying the path from which a file or directory + is to be installed. The portion of the path not preceded by + a ``/`` also specifies the path (name) to which the file + or directory is to be installed under the destination. + + * A JSON object with members: + + ``from`` + A string specifying the path from which a file or directory + is to be installed. + + ``to`` + A string specifying the path to which the file or directory + is to be installed under the destination. + + In both cases the paths are represented with forward slashes. If + the "from" path is inside the top-level directory documented by the + corresponding ``type`` value, then the path is specified relative + to that directory. Otherwise the path is absolute. + + ``type`` + A string specifying the type of installation rule. The value is one + of the following, with some variants providing additional members: + + ``file`` + An :command:`install(FILES)` or :command:`install(PROGRAMS)` call. + The ``destination`` and ``paths`` members are populated, with paths + under the top-level *source* directory expressed relative to it. + The ``isOptional`` member may exist. + This type has no additional members. + + ``directory`` + An :command:`install(DIRECTORY)` call. + The ``destination`` and ``paths`` members are populated, with paths + under the top-level *source* directory expressed relative to it. + The ``isOptional`` member may exist. + This type has no additional members. + + ``target`` + An :command:`install(TARGETS)` call. + The ``destination`` and ``paths`` members are populated, with paths + under the top-level *build* directory expressed relative to it. + The ``isOptional`` member may exist. + This type has additional members ``targetId``, ``targetIndex``, + ``targetIsImportLibrary``, and ``targetInstallNamelink``. + + ``export`` + An :command:`install(EXPORT)` call. + The ``destination`` and ``paths`` members are populated, with paths + under the top-level *build* directory expressed relative to it. + The ``paths`` entries refer to files generated automatically by + CMake for installation, and their actual values are considered + private implementation details. + This type has additional members ``exportName`` and ``exportTargets``. + + ``script`` + An :command:`install(SCRIPT)` call. + This type has additional member ``scriptFile``. + + ``code`` + An :command:`install(CODE)` call. + This type has no additional members. + + ``importedRuntimeArtifacts`` + An :command:`install(IMPORTED_RUNTIME_ARTIFACTS)` call. + The ``destination`` member is populated. The ``isOptional`` member may + exist. This type has no additional members. + + ``runtimeDependencySet`` + An :command:`install(RUNTIME_DEPENDENCY_SET)` call or an + :command:`install(TARGETS)` call with ``RUNTIME_DEPENDENCIES``. The + ``destination`` member is populated. This type has additional members + ``runtimeDependencySetName`` and ``runtimeDependencySetType``. + + ``fileSet`` + An :command:`install(TARGETS)` call with ``FILE_SET``. + The ``destination`` and ``paths`` members are populated. + The ``isOptional`` member may exist. + This type has additional members ``fileSetName``, ``fileSetType``, + ``fileSetDirectories``, and ``fileSetTarget``. + + This type was added in codemodel version 2.4. + + ``cxxModuleBmi`` + An :command:`install(TARGETS)` call with ``CXX_MODULES_BMI``. + The ``destination`` member is populated and the ``isOptional`` member + may exist. This type has an additional ``cxxModuleBmiTarget`` member. + + This type was added in codemodel version 2.5. + + ``isExcludeFromAll`` + Optional member that is present with boolean value ``true`` when + :command:`install` is called with the ``EXCLUDE_FROM_ALL`` option. + + ``isForAllComponents`` + Optional member that is present with boolean value ``true`` when + :command:`install(SCRIPT|CODE)` is called with the + ``ALL_COMPONENTS`` option. + + ``isOptional`` + Optional member that is present with boolean value ``true`` when + :command:`install` is called with the ``OPTIONAL`` option. + This is allowed when ``type`` is ``file``, ``directory``, or ``target``. + + ``targetId`` + Optional member that is present when ``type`` is ``target``. + The value is a string uniquely identifying the target to be installed. + This matches the ``id`` member of the target in the main + "codemodel" object's ``targets`` array. + + ``targetIndex`` + Optional member that is present when ``type`` is ``target``. + The value is an unsigned integer 0-based index into the main "codemodel" + object's ``targets`` array for the target to be installed. + + ``targetIsImportLibrary`` + Optional member that is present when ``type`` is ``target`` and + the installer is for a Windows DLL import library file or for an + AIX linker import file. If present, it has boolean value ``true``. + + ``targetInstallNamelink`` + Optional member that is present when ``type`` is ``target`` and + the installer corresponds to a target that may use symbolic links + to implement the :prop_tgt:`VERSION` and :prop_tgt:`SOVERSION` + target properties. + The value is a string indicating how the installer is supposed to + handle the symlinks: ``skip`` means the installer should skip the + symlinks and install only the real file, and ``only`` means the + installer should install only the symlinks and not the real file. + In all cases the ``paths`` member lists what it actually installs. + + ``exportName`` + Optional member that is present when ``type`` is ``export``. + The value is a string specifying the name of the export. + + ``exportTargets`` + Optional member that is present when ``type`` is ``export``. + The value is a JSON array of entries corresponding to the targets + included in the export. Each entry is a JSON object with members: + + ``id`` + A string uniquely identifying the target. This matches + the ``id`` member of the target in the main "codemodel" + object's ``targets`` array. + + ``index`` + An unsigned integer 0-based index into the main "codemodel" + object's ``targets`` array for the target. + + ``runtimeDependencySetName`` + Optional member that is present when ``type`` is ``runtimeDependencySet`` + and the installer was created by an + :command:`install(RUNTIME_DEPENDENCY_SET)` call. The value is a string + specifying the name of the runtime dependency set that was installed. + + ``runtimeDependencySetType`` + Optional member that is present when ``type`` is ``runtimeDependencySet``. + The value is a string with one of the following values: + + ``library`` + Indicates that this installer installs dependencies that are not macOS + frameworks. + + ``framework`` + Indicates that this installer installs dependencies that are macOS + frameworks. + + ``fileSetName`` + Optional member that is present when ``type`` is ``fileSet``. The value is + a string with the name of the file set. + + This field was added in codemodel version 2.4. + + ``fileSetType`` + Optional member that is present when ``type`` is ``fileSet``. The value is + a string with the type of the file set. + + This field was added in codemodel version 2.4. + + ``fileSetDirectories`` + Optional member that is present when ``type`` is ``fileSet``. The value + is a list of strings with the file set's base directories (determined by + genex-evaluation of :prop_tgt:`HEADER_DIRS` or + :prop_tgt:`HEADER_DIRS_`). + + This field was added in codemodel version 2.4. + + ``fileSetTarget`` + Optional member that is present when ``type`` is ``fileSet``. The value + is a JSON object with members: + + ``id`` + A string uniquely identifying the target. This matches + the ``id`` member of the target in the main "codemodel" + object's ``targets`` array. + + ``index`` + An unsigned integer 0-based index into the main "codemodel" + object's ``targets`` array for the target. + + This field was added in codemodel version 2.4. + + ``cxxModuleBmiTarget`` + Optional member that is present when ``type`` is ``cxxModuleBmi``. + The value is a JSON object with members: + + ``id`` + A string uniquely identifying the target. This matches + the ``id`` member of the target in the main "codemodel" + object's ``targets`` array. + + ``index`` + An unsigned integer 0-based index into the main "codemodel" + object's ``targets`` array for the target. + + This field was added in codemodel version 2.5. + + ``scriptFile`` + Optional member that is present when ``type`` is ``script``. + The value is a string specifying the path to the script file on disk, + represented with forward slashes. If the file is inside the top-level + source directory then the path is specified relative to that directory. + Otherwise the path is absolute. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`install` or other command invocation that added this + installer is available. The value is an unsigned integer 0-based + index into the ``backtraceGraph`` member's ``nodes`` array. + +``backtraceGraph`` + A `"codemodel" version 2 "backtrace graph"`_ whose nodes are referenced + from ``backtrace`` members elsewhere in this "directory" object. + +"codemodel" version 2 "target" object +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A codemodel "target" object is referenced by a `"codemodel" version 2`_ +object's ``targets`` array. Each "target" object is a JSON object +with members: + +``name`` + A string specifying the logical name of the target. + +``id`` + A string uniquely identifying the target. The format is unspecified + and should not be interpreted by clients. + +``type`` + A string specifying the type of the target. The value is one of + ``EXECUTABLE``, ``STATIC_LIBRARY``, ``SHARED_LIBRARY``, + ``MODULE_LIBRARY``, ``OBJECT_LIBRARY``, ``INTERFACE_LIBRARY``, + or ``UTILITY``. + +``backtrace`` + Optional member that is present when a CMake language backtrace to + the command in the source code that created the target is available. + The value is an unsigned integer 0-based index into the + ``backtraceGraph`` member's ``nodes`` array. + +``folder`` + Optional member that is present when the :prop_tgt:`FOLDER` target + property is set. The value is a JSON object with one member: + + ``name`` + A string specifying the name of the target folder. + +``paths`` + A JSON object containing members: + + ``source`` + A string specifying the path to the target's source directory, + represented with forward slashes. If the directory is inside the + top-level source directory then the path is specified relative to + that directory (with ``.`` for the top-level source directory itself). + Otherwise the path is absolute. + + ``build`` + A string specifying the path to the target's build directory, + represented with forward slashes. If the directory is inside the + top-level build directory then the path is specified relative to + that directory (with ``.`` for the top-level build directory itself). + Otherwise the path is absolute. + +``nameOnDisk`` + Optional member that is present for executable and library targets + that are linked or archived into a single primary artifact. + The value is a string specifying the file name of that artifact on disk. + +``artifacts`` + Optional member that is present for executable and library targets + that produce artifacts on disk meant for consumption by dependents. + The value is a JSON array of entries corresponding to the artifacts. + Each entry is a JSON object containing one member: + + ``path`` + A string specifying the path to the file on disk, represented with + forward slashes. If the file is inside the top-level build directory + then the path is specified relative to that directory. + Otherwise the path is absolute. + +``isGeneratorProvided`` + Optional member that is present with boolean value ``true`` if the + target is provided by CMake's build system generator rather than by + a command in the source code. + +``install`` + Optional member that is present when the target has an :command:`install` + rule. The value is a JSON object with members: + + ``prefix`` + A JSON object specifying the installation prefix. It has one member: + + ``path`` + A string specifying the value of :variable:`CMAKE_INSTALL_PREFIX`. + + ``destinations`` + A JSON array of entries specifying an install destination path. + Each entry is a JSON object with members: + + ``path`` + A string specifying the install destination path. The path may + be absolute or relative to the install prefix. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`install` command invocation that specified this + destination is available. The value is an unsigned integer 0-based + index into the ``backtraceGraph`` member's ``nodes`` array. + +``launchers`` + Optional member that is present on executable targets that have + at least one launcher specified by the project. The value is a + JSON array of entries corresponding to the specified launchers. + Each entry is a JSON object with members: + + ``command`` + A string specifying the path to the launcher on disk, represented + with forward slashes. If the file is inside the top-level source + directory then the path is specified relative to that directory. + + ``arguments`` + Optional member that is present when the launcher command has + arguments preceding the executable to be launched. The value + is a JSON array of strings representing the arguments. + + ``type`` + A string specifying the type of launcher. The value is one of + the following: + + ``emulator`` + An emulator for the target platform when cross-compiling. + See the :prop_tgt:`CROSSCOMPILING_EMULATOR` target property. + + ``test`` + A start program for the execution of tests. + See the :prop_tgt:`TEST_LAUNCHER` target property. + + This field was added in codemodel version 2.7. + +``link`` + Optional member that is present for executables and shared library + targets that link into a runtime binary. The value is a JSON object + with members describing the link step: + + ``language`` + A string specifying the language (e.g. ``C``, ``CXX``, ``Fortran``) + of the toolchain is used to invoke the linker. + + ``commandFragments`` + Optional member that is present when fragments of the link command + line invocation are available. The value is a JSON array of entries + specifying ordered fragments. Each entry is a JSON object with members: + + ``fragment`` + A string specifying a fragment of the link command line invocation. + The value is encoded in the build system's native shell format. + + ``role`` + A string specifying the role of the fragment's content: + + * ``flags``: link flags. + * ``libraries``: link library file paths or flags. + * ``libraryPath``: library search path flags. + * ``frameworkPath``: macOS framework search path flags. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`target_link_libraries`, :command:`target_link_options`, + or other command invocation that added this link fragment is available. + The value is an unsigned integer 0-based index into the ``backtraceGraph`` + member's ``nodes`` array. + + ``lto`` + Optional member that is present with boolean value ``true`` + when link-time optimization (a.k.a. interprocedural optimization + or link-time code generation) is enabled. + + ``sysroot`` + Optional member that is present when the :variable:`CMAKE_SYSROOT_LINK` + or :variable:`CMAKE_SYSROOT` variable is defined. The value is a + JSON object with one member: + + ``path`` + A string specifying the absolute path to the sysroot, represented + with forward slashes. + +``archive`` + Optional member that is present for static library targets. The value + is a JSON object with members describing the archive step: + + ``commandFragments`` + Optional member that is present when fragments of the archiver command + line invocation are available. The value is a JSON array of entries + specifying the fragments. Each entry is a JSON object with members: + + ``fragment`` + A string specifying a fragment of the archiver command line invocation. + The value is encoded in the build system's native shell format. + + ``role`` + A string specifying the role of the fragment's content: + + * ``flags``: archiver flags. + + ``lto`` + Optional member that is present with boolean value ``true`` + when link-time optimization (a.k.a. interprocedural optimization + or link-time code generation) is enabled. + +``debugger`` + Optional member that is present when the target has one of the + following fields set. + The value is a JSON object of entries corresponding to + debugger specific values set. + + This field was added in codemodel version 2.8. + + ``workingDirectory`` + Optional member that is present when the + :prop_tgt:`DEBUGGER_WORKING_DIRECTORY` target property is set. + The member will also be present in :ref:`Visual Studio Generators` + when :prop_tgt:`VS_DEBUGGER_WORKING_DIRECTORY` is set. + + This field was added in codemodel version 2.8. + +``dependencies`` + Optional member that is present when the target depends on other targets. + The value is a JSON array of entries corresponding to the dependencies. + Each entry is a JSON object with members: + + ``id`` + A string uniquely identifying the target on which this target depends. + This matches the main ``id`` member of the other target. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`add_dependencies`, :command:`target_link_libraries`, + or other command invocation that created this dependency is + available. The value is an unsigned integer 0-based index into + the ``backtraceGraph`` member's ``nodes`` array. + +``fileSets`` + An optional member that is present when a target defines one or more + file sets. The value is a JSON array of entries corresponding to the + target's file sets. Each entry is a JSON object with members: + + ``name`` + A string specifying the name of the file set. + + ``type`` + A string specifying the type of the file set. See + :command:`target_sources` supported file set types. + + ``visibility`` + A string specifying the visibility of the file set; one of ``PUBLIC``, + ``PRIVATE``, or ``INTERFACE``. + + ``baseDirectories`` + A JSON array of strings, each specifying a base directory containing + sources in the file set. If the directory is inside the top-level source + directory then the path is specified relative to that directory. + Otherwise the path is absolute. + + This field was added in codemodel version 2.5. + +``sources`` + A JSON array of entries corresponding to the target's source files. + Each entry is a JSON object with members: + + ``path`` + A string specifying the path to the source file on disk, represented + with forward slashes. If the file is inside the top-level source + directory then the path is specified relative to that directory. + Otherwise the path is absolute. + + ``compileGroupIndex`` + Optional member that is present when the source is compiled. + The value is an unsigned integer 0-based index into the + ``compileGroups`` array. + + ``sourceGroupIndex`` + Optional member that is present when the source is part of a source + group either via the :command:`source_group` command or by default. + The value is an unsigned integer 0-based index into the + ``sourceGroups`` array. + + ``isGenerated`` + Optional member that is present with boolean value ``true`` if + the source is :prop_sf:`GENERATED`. + + ``fileSetIndex`` + Optional member that is present when the source is part of a file set. + The value is an unsigned integer 0-based index into the ``fileSets`` + array. + + This field was added in codemodel version 2.5. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`target_sources`, :command:`add_executable`, + :command:`add_library`, :command:`add_custom_target`, or other + command invocation that added this source to the target is + available. The value is an unsigned integer 0-based index into + the ``backtraceGraph`` member's ``nodes`` array. + +``sourceGroups`` + Optional member that is present when sources are grouped together by + the :command:`source_group` command or by default. The value is a + JSON array of entries corresponding to the groups. Each entry is + a JSON object with members: + + ``name`` + A string specifying the name of the source group. + + ``sourceIndexes`` + A JSON array listing the sources belonging to the group. + Each entry is an unsigned integer 0-based index into the + main ``sources`` array for the target. + +``compileGroups`` + Optional member that is present when the target has sources that compile. + The value is a JSON array of entries corresponding to groups of sources + that all compile with the same settings. Each entry is a JSON object + with members: + + ``sourceIndexes`` + A JSON array listing the sources belonging to the group. + Each entry is an unsigned integer 0-based index into the + main ``sources`` array for the target. + + ``language`` + A string specifying the language (e.g. ``C``, ``CXX``, ``Fortran``) + of the toolchain is used to compile the source file. + + ``languageStandard`` + Optional member that is present when the language standard is set + explicitly (e.g. via :prop_tgt:`CXX_STANDARD`) or implicitly by + compile features. Each entry is a JSON object with two members: + + ``backtraces`` + Optional member that is present when a CMake language backtrace to + the ``_STANDARD`` setting is available. If the language + standard was set implicitly by compile features those are used as + the backtrace(s). It's possible for multiple compile features to + require the same language standard so there could be multiple + backtraces. The value is a JSON array with each entry being an + unsigned integer 0-based index into the ``backtraceGraph`` + member's ``nodes`` array. + + ``standard`` + String representing the language standard. + + This field was added in codemodel version 2.2. + + ``compileCommandFragments`` + Optional member that is present when fragments of the compiler command + line invocation are available. The value is a JSON array of entries + specifying ordered fragments. Each entry is a JSON object with + one member: + + ``fragment`` + A string specifying a fragment of the compile command line invocation. + The value is encoded in the build system's native shell format. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the command invocation that added this fragment is available. + The value is an unsigned integer 0-based index into the + ``backtraceGraph`` member's ``nodes`` array. + + ``includes`` + Optional member that is present when there are include directories. + The value is a JSON array with an entry for each directory. Each + entry is a JSON object with members: + + ``path`` + A string specifying the path to the include directory, + represented with forward slashes. + + ``isSystem`` + Optional member that is present with boolean value ``true`` if + the include directory is marked as a system include directory. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`target_include_directories` or other command invocation + that added this include directory is available. The value is + an unsigned integer 0-based index into the ``backtraceGraph`` + member's ``nodes`` array. + + ``frameworks`` + Optional member that is present when, on Apple platforms, there are + frameworks. The value is a JSON array with an entry for each directory. + Each entry is a JSON object with members: + + ``path`` + A string specifying the path to the framework directory, + represented with forward slashes. + + ``isSystem`` + Optional member that is present with boolean value ``true`` if + the framework is marked as a system one. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`target_link_libraries` or other command invocation + that added this framework is available. The value is + an unsigned integer 0-based index into the ``backtraceGraph`` + member's ``nodes`` array. + + This field was added in codemodel version 2.6. + + ``precompileHeaders`` + Optional member that is present when :command:`target_precompile_headers` + or other command invocations set :prop_tgt:`PRECOMPILE_HEADERS` on the + target. The value is a JSON array with an entry for each header. Each + entry is a JSON object with members: + + ``header`` + Full path to the precompile header file. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`target_precompile_headers` or other command invocation + that added this precompiled header is available. The value is an + unsigned integer 0-based index into the ``backtraceGraph`` member's + ``nodes`` array. + + This field was added in codemodel version 2.1. + + ``defines`` + Optional member that is present when there are preprocessor definitions. + The value is a JSON array with an entry for each definition. Each + entry is a JSON object with members: + + ``define`` + A string specifying the preprocessor definition in the format + ``[=]``, e.g. ``DEF`` or ``DEF=1``. + + ``backtrace`` + Optional member that is present when a CMake language backtrace to + the :command:`target_compile_definitions` or other command invocation + that added this preprocessor definition is available. The value is + an unsigned integer 0-based index into the ``backtraceGraph`` + member's ``nodes`` array. + + ``sysroot`` + Optional member that is present when the + :variable:`CMAKE_SYSROOT_COMPILE` or :variable:`CMAKE_SYSROOT` + variable is defined. The value is a JSON object with one member: + + ``path`` + A string specifying the absolute path to the sysroot, represented + with forward slashes. + +``backtraceGraph`` + A `"codemodel" version 2 "backtrace graph"`_ whose nodes are referenced + from ``backtrace`` members elsewhere in this "target" object. + +"codemodel" version 2 "backtrace graph" +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``backtraceGraph`` member of a `"codemodel" version 2 "directory" object`_, +or `"codemodel" version 2 "target" object`_ is a JSON object describing a +graph of backtraces. Its nodes are referenced from ``backtrace`` members +elsewhere in the containing object. The backtrace graph object members are: + +``nodes`` + A JSON array listing nodes in the backtrace graph. Each entry + is a JSON object with members: + + ``file`` + An unsigned integer 0-based index into the backtrace ``files`` array. + + ``line`` + An optional member present when the node represents a line within + the file. The value is an unsigned integer 1-based line number. + + ``command`` + An optional member present when the node represents a command + invocation within the file. The value is an unsigned integer + 0-based index into the backtrace ``commands`` array. + + ``parent`` + An optional member present when the node is not the bottom of + the call stack. The value is an unsigned integer 0-based index + of another entry in the backtrace ``nodes`` array. + +``commands`` + A JSON array listing command names referenced by backtrace nodes. + Each entry is a string specifying a command name. + +``files`` + A JSON array listing CMake language files referenced by backtrace nodes. + Each entry is a string specifying the path to a file, represented + with forward slashes. If the file is inside the top-level source + directory then the path is specified relative to that directory. + Otherwise the path is absolute. + +.. _`file-api configureLog`: + +Object Kind "configureLog" +-------------------------- + +.. versionadded:: 3.26 + +The ``configureLog`` object kind describes the location and contents of +a :manual:`cmake-configure-log(7)` file. + +There is only one ``configureLog`` object major version, version 1. + +"configureLog" version 1 +^^^^^^^^^^^^^^^^^^^^^^^^ + +``configureLog`` object version 1 is a JSON object: + +.. code-block:: json + + { + "kind": "configureLog", + "version": { "major": 1, "minor": 0 }, + "path": "/path/to/top-level-build-dir/CMakeFiles/CMakeConfigureLog.yaml", + "eventKindNames": [ "try_compile-v1", "try_run-v1" ] + } + +The members specific to ``configureLog`` objects are: + +``path`` + A string specifying the path to the configure log file. + Clients must read the log file from this path, which may be + different than the path documented by :manual:`cmake-configure-log(7)`. + The log file may not exist if no events are logged. + +``eventKindNames`` + A JSON array whose entries are each a JSON string naming one + of the :manual:`cmake-configure-log(7)` versioned event kinds. + At most one version of each configure log event kind will be listed. + Although the configure log may contain other (versioned) event kinds, + clients must ignore those that are not listed in this field. + +Object Kind "cache" +------------------- + +The ``cache`` object kind lists cache entries. These are the +:ref:`CMake Language Variables` stored in the persistent cache +(``CMakeCache.txt``) for the build tree. + +There is only one ``cache`` object major version, version 2. +Version 1 does not exist to avoid confusion with that from +:manual:`cmake-server(7)` mode. + +"cache" version 2 +^^^^^^^^^^^^^^^^^ + +``cache`` object version 2 is a JSON object: + +.. code-block:: json + + { + "kind": "cache", + "version": { "major": 2, "minor": 0 }, + "entries": [ + { + "name": "BUILD_SHARED_LIBS", + "value": "ON", + "type": "BOOL", + "properties": [ + { + "name": "HELPSTRING", + "value": "Build shared libraries" + } + ] + }, + { + "name": "CMAKE_GENERATOR", + "value": "Unix Makefiles", + "type": "INTERNAL", + "properties": [ + { + "name": "HELPSTRING", + "value": "Name of generator." + } + ] + } + ] + } + +The members specific to ``cache`` objects are: + +``entries`` + A JSON array whose entries are each a JSON object specifying a + cache entry. The members of each entry are: + + ``name`` + A string specifying the name of the entry. + + ``value`` + A string specifying the value of the entry. + + ``type`` + A string specifying the type of the entry used by + :manual:`cmake-gui(1)` to choose a widget for editing. + + ``properties`` + A JSON array of entries specifying associated + :ref:`cache entry properties `. + Each entry is a JSON object containing members: + + ``name`` + A string specifying the name of the cache entry property. + + ``value`` + A string specifying the value of the cache entry property. + +Object Kind "cmakeFiles" +------------------------ + +The ``cmakeFiles`` object kind lists files used by CMake while +configuring and generating the build system. These include the +``CMakeLists.txt`` files as well as included ``.cmake`` files. + +There is only one ``cmakeFiles`` object major version, version 1. + +"cmakeFiles" version 1 +^^^^^^^^^^^^^^^^^^^^^^ + +``cmakeFiles`` object version 1 is a JSON object: + +.. code-block:: json + + { + "kind": "cmakeFiles", + "version": { "major": 1, "minor": 1 }, + "paths": { + "build": "/path/to/top-level-build-dir", + "source": "/path/to/top-level-source-dir" + }, + "inputs": [ + { + "path": "CMakeLists.txt" + }, + { + "isGenerated": true, + "path": "/path/to/top-level-build-dir/.../CMakeSystem.cmake" + }, + { + "isExternal": true, + "path": "/path/to/external/third-party/module.cmake" + }, + { + "isCMake": true, + "isExternal": true, + "path": "/path/to/cmake/Modules/CMakeGenericSystem.cmake" + } + ], + "globsDependent": [ + { + "expression": "src/*.cxx", + "recurse": true, + "files": [ + "src/foo.cxx", + "src/bar.cxx" + ] + } + ] + } + +The members specific to ``cmakeFiles`` objects are: + +``paths`` + A JSON object containing members: + + ``source`` + A string specifying the absolute path to the top-level source directory, + represented with forward slashes. + + ``build`` + A string specifying the absolute path to the top-level build directory, + represented with forward slashes. + +``inputs`` + A JSON array whose entries are each a JSON object specifying an input + file used by CMake when configuring and generating the build system. + The members of each entry are: + + ``path`` + A string specifying the path to an input file to CMake, represented + with forward slashes. If the file is inside the top-level source + directory then the path is specified relative to that directory. + Otherwise the path is absolute. + + ``isGenerated`` + Optional member that is present with boolean value ``true`` + if the path specifies a file that is under the top-level + build directory and the build is out-of-source. + This member is not available on in-source builds. + + ``isExternal`` + Optional member that is present with boolean value ``true`` + if the path specifies a file that is not under the top-level + source or build directories. + + ``isCMake`` + Optional member that is present with boolean value ``true`` + if the path specifies a file in the CMake installation. + +``globsDependent`` + Optional member that is present when the project calls :command:`file(GLOB)` + or :command:`file(GLOB_RECURSE)` with the ``CONFIGURE_DEPENDS`` option. + The value is a JSON array of JSON objects, each specifying a globbing + expression and the list of paths it matched. If the globbing expression + no longer matches the same list of paths, CMake considers the build system + to be out of date. + + This field was added in ``cmakeFiles`` version 1.1. + + The members of each entry are: + + ``expression`` + A string specifying the globbing expression. + + ``recurse`` + Optional member that is present with boolean value ``true`` + if the entry corresponds to a :command:`file(GLOB_RECURSE)` call. + Otherwise the entry corresponds to a :command:`file(GLOB)` call. + + ``listDirectories`` + Optional member that is present with boolean value ``true`` if + :command:`file(GLOB)` was called without ``LIST_DIRECTORIES false`` or + :command:`file(GLOB_RECURSE)` was called with ``LIST_DIRECTORIES true``. + + ``followSymlinks`` + Optional member that is present with boolean value ``true`` if + :command:`file(GLOB)` was called with the ``FOLLOW_SYMLINKS`` option. + + ``relative`` + Optional member that is present if :command:`file(GLOB)` was called + with the ``RELATIVE `` option. The value is a string containing + the ```` given. + + ``paths`` + A JSON array of strings specifying the paths matched by the call + to :command:`file(GLOB)` or :command:`file(GLOB_RECURSE)`. + +Object Kind "toolchains" +------------------------ + +The ``toolchains`` object kind lists properties of the toolchains used during +the build. These include the language, compiler path, ID, and version. + +There is only one ``toolchains`` object major version, version 1. + +"toolchains" version 1 +^^^^^^^^^^^^^^^^^^^^^^ + +``toolchains`` object version 1 is a JSON object: + +.. code-block:: json + + { + "kind": "toolchains", + "version": { "major": 1, "minor": 0 }, + "toolchains": [ + { + "language": "C", + "compiler": { + "path": "/usr/bin/cc", + "id": "GNU", + "version": "9.3.0", + "implicit": { + "includeDirectories": [ + "/usr/lib/gcc/x86_64-linux-gnu/9/include", + "/usr/local/include", + "/usr/include/x86_64-linux-gnu", + "/usr/include" + ], + "linkDirectories": [ + "/usr/lib/gcc/x86_64-linux-gnu/9", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib", + "/lib/x86_64-linux-gnu", + "/lib" + ], + "linkFrameworkDirectories": [], + "linkLibraries": [ "gcc", "gcc_s", "c", "gcc", "gcc_s" ] + } + }, + "sourceFileExtensions": [ "c", "m" ] + }, + { + "language": "CXX", + "compiler": { + "path": "/usr/bin/c++", + "id": "GNU", + "version": "9.3.0", + "implicit": { + "includeDirectories": [ + "/usr/include/c++/9", + "/usr/include/x86_64-linux-gnu/c++/9", + "/usr/include/c++/9/backward", + "/usr/lib/gcc/x86_64-linux-gnu/9/include", + "/usr/local/include", + "/usr/include/x86_64-linux-gnu", + "/usr/include" + ], + "linkDirectories": [ + "/usr/lib/gcc/x86_64-linux-gnu/9", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib", + "/lib/x86_64-linux-gnu", + "/lib" + ], + "linkFrameworkDirectories": [], + "linkLibraries": [ + "stdc++", "m", "gcc_s", "gcc", "c", "gcc_s", "gcc" + ] + } + }, + "sourceFileExtensions": [ + "C", "M", "c++", "cc", "cpp", "cxx", "mm", "CPP" + ] + } + ] + } + +The members specific to ``toolchains`` objects are: + +``toolchains`` + A JSON array whose entries are each a JSON object specifying a toolchain + associated with a particular language. The members of each entry are: + + ``language`` + A JSON string specifying the toolchain language, like C or CXX. Language + names are the same as language names that can be passed to the + :command:`project` command. Because CMake only supports a single toolchain + per language, this field can be used as a key. + + ``compiler`` + A JSON object containing members: + + ``path`` + Optional member that is present when the + :variable:`CMAKE__COMPILER` variable is defined for the current + language. Its value is a JSON string holding the path to the compiler. + + ``id`` + Optional member that is present when the + :variable:`CMAKE__COMPILER_ID` variable is defined for the current + language. Its value is a JSON string holding the ID (GNU, MSVC, etc.) of + the compiler. + + ``version`` + Optional member that is present when the + :variable:`CMAKE__COMPILER_VERSION` variable is defined for the + current language. Its value is a JSON string holding the version of the + compiler. + + ``target`` + Optional member that is present when the + :variable:`CMAKE__COMPILER_TARGET` variable is defined for the + current language. Its value is a JSON string holding the cross-compiling + target of the compiler. + + ``implicit`` + A JSON object containing members: + + ``includeDirectories`` + Optional member that is present when the + :variable:`CMAKE__IMPLICIT_INCLUDE_DIRECTORIES` variable is + defined for the current language. Its value is a JSON array of JSON + strings where each string holds a path to an implicit include + directory for the compiler. + + ``linkDirectories`` + Optional member that is present when the + :variable:`CMAKE__IMPLICIT_LINK_DIRECTORIES` variable is + defined for the current language. Its value is a JSON array of JSON + strings where each string holds a path to an implicit link directory + for the compiler. + + ``linkFrameworkDirectories`` + Optional member that is present when the + :variable:`CMAKE__IMPLICIT_LINK_FRAMEWORK_DIRECTORIES` variable + is defined for the current language. Its value is a JSON array of JSON + strings where each string holds a path to an implicit link framework + directory for the compiler. + + ``linkLibraries`` + Optional member that is present when the + :variable:`CMAKE__IMPLICIT_LINK_LIBRARIES` variable is defined + for the current language. Its value is a JSON array of JSON strings + where each string holds a path to an implicit link library for the + compiler. + + ``sourceFileExtensions`` + Optional member that is present when the + :variable:`CMAKE__SOURCE_FILE_EXTENSIONS` variable is defined for + the current language. Its value is a JSON array of JSON strings where + each string holds a file extension (without the leading dot) for the + language. diff --git a/packages/cmake-file-api/package.json b/packages/cmake-file-api/package.json new file mode 100644 index 00000000..b641dffc --- /dev/null +++ b/packages/cmake-file-api/package.json @@ -0,0 +1,33 @@ +{ + "name": "cmake-file-api", + "version": "0.1.0", + "type": "module", + "description": "TypeScript wrapper around the CMake File API", + "homepage": "https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html", + "scripts": { + "build": "tsc --build", + "lint": "eslint 'src/**/*.ts'", + "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" + }, + "files": [ + "docs/", + "dist/", + "!*.test.d.ts", + "!*.test.d.ts.map" + ], + "exports": { + ".": "./dist/index.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/callstackincubator/react-native-node-api", + "directory": "packages/cmake-file-api" + }, + "author": { + "name": "Kræn Hansen", + "url": "https://github.com/kraenhansen" + }, + "dependencies": { + "zod": "^4.1.11" + } +} diff --git a/packages/cmake-file-api/src/index.ts b/packages/cmake-file-api/src/index.ts new file mode 100644 index 00000000..a0f3fa3a --- /dev/null +++ b/packages/cmake-file-api/src/index.ts @@ -0,0 +1,26 @@ +export { + createSharedStatelessQuery, + createClientStatelessQuery, + createClientStatefulQuery, + type VersionSpec, + type QueryRequest, + type StatefulQuery, +} from "./query.js"; + +export { + readReplyIndex, + isReplyErrorIndexPath, + readReplyErrorIndex, + readCodemodel, + readTarget, + readCache, + readCmakeFiles, + readToolchains, + readConfigureLog, + findCurrentReplyIndexPath, + readCurrentSharedCodemodel, + readCurrentTargets, + readCurrentTargetsDeep, +} from "./reply.js"; + +export * from "./schemas.js"; diff --git a/packages/cmake-file-api/src/query.test.ts b/packages/cmake-file-api/src/query.test.ts new file mode 100644 index 00000000..5995cd23 --- /dev/null +++ b/packages/cmake-file-api/src/query.test.ts @@ -0,0 +1,228 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, it, type TestContext } from "node:test"; + +import { + createSharedStatelessQuery, + createClientStatelessQuery, + createClientStatefulQuery, + type StatefulQuery, +} from "./query.js"; + +function createTempBuildDir(context: TestContext) { + const tmpPath = fs.mkdtempSync(path.join(os.tmpdir(), "cmake-api-test-")); + + context.after(() => { + fs.rmSync(tmpPath, { recursive: true, force: true }); + }); + + return tmpPath; +} + +describe("createSharedStatelessQuery", () => { + it("creates a shared stateless query file", async function (context) { + const buildPath = createTempBuildDir(context); + + await createSharedStatelessQuery(buildPath, "codemodel", "2"); + + const queryPath = path.join(buildPath, ".cmake/api/v1/query/codemodel-v2"); + assert(fs.existsSync(queryPath), "Query file should exist"); + + const content = fs.readFileSync(queryPath, "utf-8"); + assert.strictEqual(content, "", "Query file should be empty"); + }); + + it("creates directory structure recursively", async function (context) { + const buildPath = createTempBuildDir(context); + + await createSharedStatelessQuery(buildPath, "cache", "2"); + + const queryDir = path.join(buildPath, ".cmake/api/v1/query"); + assert(fs.existsSync(queryDir), "Query directory should exist"); + + const queryPath = path.join(queryDir, "cache-v2"); + assert(fs.existsSync(queryPath), "Query file should exist"); + }); + + it("supports all object kinds", async function (context) { + const buildPath = createTempBuildDir(context); + const kinds = [ + "codemodel", + "configureLog", + "cache", + "cmakeFiles", + "toolchains", + ] as const; + + for (const kind of kinds) { + await createSharedStatelessQuery(buildPath, kind, "1"); + + const queryPath = path.join(buildPath, `.cmake/api/v1/query/${kind}-v1`); + assert(fs.existsSync(queryPath), `Query file for ${kind} should exist`); + } + }); +}); + +describe("createClientStatelessQuery", () => { + it("creates a client stateless query file", async function (context) { + const buildPath = createTempBuildDir(context); + + await createClientStatelessQuery(buildPath, "my-client", "codemodel", "2"); + + const queryPath = path.join( + buildPath, + ".cmake/api/v1/query/client-my-client/codemodel-v2", + ); + assert(fs.existsSync(queryPath), "Client query file should exist"); + + const content = fs.readFileSync(queryPath, "utf-8"); + assert.strictEqual(content, "", "Client query file should be empty"); + }); + + it("creates client directory structure", async function (context) { + const buildPath = createTempBuildDir(context); + + await createClientStatelessQuery(buildPath, "test-client", "cache", "2"); + + const clientDir = path.join( + buildPath, + ".cmake/api/v1/query/client-test-client", + ); + assert(fs.existsSync(clientDir), "Client directory should exist"); + + const queryPath = path.join(clientDir, "cache-v2"); + assert(fs.existsSync(queryPath), "Client query file should exist"); + }); + + it("supports multiple clients", async function (context) { + const buildPath = createTempBuildDir(context); + + await createClientStatelessQuery(buildPath, "client-a", "codemodel", "2"); + await createClientStatelessQuery(buildPath, "client-b", "cache", "2"); + + const clientAPath = path.join( + buildPath, + ".cmake/api/v1/query/client-client-a/codemodel-v2", + ); + const clientBPath = path.join( + buildPath, + ".cmake/api/v1/query/client-client-b/cache-v2", + ); + + assert(fs.existsSync(clientAPath), "Client A query should exist"); + assert(fs.existsSync(clientBPath), "Client B query should exist"); + }); +}); + +describe("createClientStatefulQuery", () => { + it("creates a client stateful query file with simple request", async function (context) { + const buildPath = createTempBuildDir(context); + + const query: StatefulQuery = { + requests: [{ kind: "codemodel", version: 2 }], + }; + + await createClientStatefulQuery(buildPath, "my-client", query); + + const queryPath = path.join( + buildPath, + ".cmake/api/v1/query/client-my-client/query.json", + ); + assert(fs.existsSync(queryPath), "Stateful query file should exist"); + + const content = fs.readFileSync(queryPath, "utf-8"); + const parsed = JSON.parse(content) as StatefulQuery; + + assert.deepStrictEqual(parsed, query, "Parsed query should match input"); + }); + + it("creates stateful query with complex version specifications", async function (context) { + const buildPath = createTempBuildDir(context); + + const query: StatefulQuery = { + requests: [ + { + kind: "codemodel", + version: [2, { major: 1, minor: 5 }], + }, + { + kind: "cache", + version: { major: 2, minor: 0 }, + }, + { + kind: "toolchains", + }, + ], + client: { name: "test-tool", version: "1.0.0" }, + }; + + await createClientStatefulQuery(buildPath, "advanced-client", query); + + const queryPath = path.join( + buildPath, + ".cmake/api/v1/query/client-advanced-client/query.json", + ); + const content = fs.readFileSync(queryPath, "utf-8"); + const parsed = JSON.parse(content) as StatefulQuery; + + assert.deepStrictEqual(parsed, query, "Complex query should be preserved"); + }); + + it("creates well-formatted JSON", async function (context) { + const buildPath = createTempBuildDir(context); + + const query: StatefulQuery = { + requests: [ + { kind: "codemodel", version: 2 }, + { kind: "cache", version: 2 }, + ], + }; + + await createClientStatefulQuery(buildPath, "format-test", query); + + const queryPath = path.join( + buildPath, + ".cmake/api/v1/query/client-format-test/query.json", + ); + const content = fs.readFileSync(queryPath, "utf-8"); + + // Should be pretty-printed with 2-space indentation + assert(content.includes(" "), "JSON should be indented"); + assert(content.includes("\n"), "JSON should have newlines"); + + // Should be valid JSON + assert.doesNotThrow(() => JSON.parse(content), "Should be valid JSON"); + }); + + it("supports client-specific data in requests", async function (context) { + const buildPath = createTempBuildDir(context); + + const query: StatefulQuery = { + requests: [ + { + kind: "codemodel", + version: 2, + client: { requestId: "req-001", priority: "high" }, + }, + ], + client: { sessionId: "session-123" }, + }; + + await createClientStatefulQuery(buildPath, "custom-client", query); + + const queryPath = path.join( + buildPath, + ".cmake/api/v1/query/client-custom-client/query.json", + ); + const content = fs.readFileSync(queryPath, "utf-8"); + const parsed = JSON.parse(content) as StatefulQuery; + + assert.deepStrictEqual(parsed.requests[0]?.client, { + requestId: "req-001", + priority: "high", + }); + assert.deepStrictEqual(parsed.client, { sessionId: "session-123" }); + }); +}); diff --git a/packages/cmake-file-api/src/query.ts b/packages/cmake-file-api/src/query.ts new file mode 100644 index 00000000..ccb3a2d8 --- /dev/null +++ b/packages/cmake-file-api/src/query.ts @@ -0,0 +1,93 @@ +import fs from "node:fs"; +import path from "node:path"; + +/** + * Creates a shared stateless query file for the specified object kind and major version. + * These are stateless shared queries not owned by any specific client. + * + * @param buildPath Path to the build directory + * @param kind Object kind to query for + * @param majorVersion Major version number as string + */ +export async function createSharedStatelessQuery( + buildPath: string, + kind: "codemodel" | "configureLog" | "cache" | "cmakeFiles" | "toolchains", + majorVersion: string, +) { + const queryPath = path.join( + buildPath, + `.cmake/api/v1/query/${kind}-v${majorVersion}`, + ); + await fs.promises.mkdir(path.dirname(queryPath), { recursive: true }); + await fs.promises.writeFile(queryPath, ""); +} + +/** + * Creates a client stateless query file for the specified client, object kind and major version. + * These are stateless queries owned by the specified client. + * + * @param buildPath Path to the build directory + * @param clientName Unique identifier for the client + * @param kind Object kind to query for + * @param majorVersion Major version number as string + */ +export async function createClientStatelessQuery( + buildPath: string, + clientName: string, + kind: "codemodel" | "configureLog" | "cache" | "cmakeFiles" | "toolchains", + majorVersion: string, +) { + const queryPath = path.join( + buildPath, + `.cmake/api/v1/query/client-${clientName}/${kind}-v${majorVersion}`, + ); + await fs.promises.mkdir(path.dirname(queryPath), { recursive: true }); + await fs.promises.writeFile(queryPath, ""); +} + +/** + * Version specification for stateful queries + */ +export type VersionSpec = + | number // major version only + | { major: number; minor?: number } // major with optional minor + | (number | { major: number; minor?: number })[]; // array of version specs + +/** + * Request specification for stateful queries + */ +export interface QueryRequest { + kind: "codemodel" | "configureLog" | "cache" | "cmakeFiles" | "toolchains"; + version?: VersionSpec; + client?: unknown; // Reserved for client use +} + +/** + * Stateful query specification + */ +export interface StatefulQuery { + requests: QueryRequest[]; + client?: unknown; // Reserved for client use +} + +/** + * Creates a client stateful query file (query.json) for the specified client. + * These are stateful queries owned by the specified client that can request + * specific versions and get only the most recent version recognized by CMake. + * + * @param buildPath Path to the build directory + * @param clientName Unique identifier for the client + * @param query Stateful query specification + */ +export async function createClientStatefulQuery( + buildPath: string, + clientName: string, + query: StatefulQuery, +) { + const queryPath = path.join( + buildPath, + `.cmake/api/v1/query/client-${clientName}/query.json`, + ); + await fs.promises.mkdir(path.dirname(queryPath), { recursive: true }); + await fs.promises.writeFile(queryPath, JSON.stringify(query, null, 2)); +} diff --git a/packages/cmake-file-api/src/reply.test.ts b/packages/cmake-file-api/src/reply.test.ts new file mode 100644 index 00000000..87c1a7bd --- /dev/null +++ b/packages/cmake-file-api/src/reply.test.ts @@ -0,0 +1,1075 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, it, type TestContext } from "node:test"; + +import { + findCurrentReplyIndexPath, + readReplyIndex, + readCodemodel, + readCurrentSharedCodemodel, + readTarget, + readCache, + readCmakeFiles, + readToolchains, + readConfigureLog, + isReplyErrorIndexPath, + readReplyErrorIndex, +} from "./reply.js"; + +function createTempDir(context: TestContext, prefix = "test-") { + const tmpPath = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + + context.after(() => { + fs.rmSync(tmpPath, { recursive: true, force: true }); + }); + + return tmpPath; +} + +function createMockReplyDirectory( + context: TestContext, + replyFiles: [string, Record][], +) { + const tmpPath = createTempDir(context); + + for (const [fileName, content] of replyFiles) { + const filePath = path.join(tmpPath, fileName); + fs.writeFileSync(filePath, JSON.stringify(content), { + encoding: "utf-8", + }); + } + + return tmpPath; +} + +describe("findCurrentReplyIndexPath", () => { + it("returns the correct path when only index files are present", async function (context) { + const tmpPath = createMockReplyDirectory(context, [ + ["index-a.json", {}], + ["index-b.json", {}], + ]); + const result = await findCurrentReplyIndexPath(tmpPath); + assert.strictEqual(result, path.join(tmpPath, "index-b.json")); + }); + + it("returns the correct path when only error files are present", async function (context) { + const tmpPath = createMockReplyDirectory(context, [ + ["error-a.json", {}], + ["error-b.json", {}], + ]); + const result = await findCurrentReplyIndexPath(tmpPath); + assert.strictEqual(result, path.join(tmpPath, "error-b.json")); + }); + + it("returns the correct path when both index and error files are present", async function (context) { + const tmpPath = createMockReplyDirectory(context, [ + ["index-a.json", {}], + ["error-b.json", {}], + ]); + const result = await findCurrentReplyIndexPath(tmpPath); + assert.strictEqual(result, path.join(tmpPath, "error-b.json")); + }); + + it("returns the correct path when both index and error files are present (reversed)", async function (context) { + const tmpPath = createMockReplyDirectory(context, [ + ["error-a.json", {}], + ["index-b.json", {}], + ]); + const result = await findCurrentReplyIndexPath(tmpPath); + assert.strictEqual(result, path.join(tmpPath, "index-b.json")); + }); +}); + +describe("readIndex", () => { + it("reads a well-formed index file with complete structure", async function (context) { + const mockIndex = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { + multiConfig: false, + name: "Unix Makefiles", + // Note: platform is optional according to docs - omitted here like in the example + }, + }, + objects: [ + { + kind: "codemodel", + version: { major: 2, minor: 0 }, + jsonFile: "codemodel-v2-12345.json", + }, + { + kind: "cache", + version: { major: 2, minor: 0 }, + jsonFile: "cache-v2-67890.json", + }, + ], + reply: { + "codemodel-v2": { + kind: "codemodel", + version: { major: 2, minor: 0 }, + jsonFile: "codemodel-v2-12345.json", + }, + "cache-v2": { + kind: "cache", + version: { major: 2, minor: 0 }, + jsonFile: "cache-v2-67890.json", + }, + "unknown-kind-v1": { + error: "unknown query file", + }, + "client-test-client": { + "codemodel-v2": { + kind: "codemodel", + version: { major: 2, minor: 0 }, + jsonFile: "codemodel-v2-12345.json", + }, + "unknown-v1": { + error: "unknown query file", + }, + }, + }, + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["index-a.json", mockIndex], + ]); + const result = await readReplyIndex(path.join(tmpPath, "index-a.json")); + + // Verify the entire structure matches our mock data + assert.deepStrictEqual(result, mockIndex); + }); + + it("reads index file with generator platform", async function (context) { + const mockIndexWithPlatform = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { + multiConfig: true, + name: "Visual Studio 16 2019", + platform: "x64", // Present when generator supports CMAKE_GENERATOR_PLATFORM + }, + }, + objects: [], + reply: {}, + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["index-b.json", mockIndexWithPlatform], + ]); + const result = await readReplyIndex(path.join(tmpPath, "index-b.json")); + + // Verify the entire structure matches our mock data + assert.deepStrictEqual(result, mockIndexWithPlatform); + }); +}); + +describe("readCodeModel", () => { + it("reads a well-formed codemodel file", async function (context) { + const mockCodemodel = { + kind: "codemodel", + version: { major: 2, minor: 3 }, + paths: { + source: "/path/to/source", + build: "/path/to/build", + }, + configurations: [ + { + name: "Debug", + directories: [ + { + source: ".", + build: ".", + childIndexes: [], + projectIndex: 0, + targetIndexes: [0], + hasInstallRule: true, + minimumCMakeVersion: { + string: "3.14", + }, + jsonFile: "directory-debug.json", + }, + ], + projects: [ + { + name: "MyProject", + directoryIndexes: [0], + targetIndexes: [0], + }, + ], + targets: [ + { + name: "MyExecutable", + id: "MyExecutable::@6890a9b7b1a1a2e4d6b9", + directoryIndex: 0, + projectIndex: 0, + jsonFile: "target-MyExecutable.json", + }, + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["codemodel-v2-12345.json", mockCodemodel], + ]); + const result = await readCodemodel( + path.join(tmpPath, "codemodel-v2-12345.json"), + ); + + // Verify the entire structure matches our mock data + assert.deepStrictEqual(result, mockCodemodel); + }); +}); + +describe("readTarget", () => { + // Base objects for reusable test data + const baseTarget = { + name: "MyTarget", + id: "MyTarget::@6890a9b7b1a1a2e4d6b9", + type: "EXECUTABLE" as const, + paths: { + source: ".", + build: ".", + }, + }; + + const baseCompileGroup = { + sourceIndexes: [0], + language: "CXX", + includes: [ + { + path: "/usr/include", + isSystem: true, + backtrace: 1, + }, + ], + defines: [ + { + define: "NDEBUG", + backtrace: 2, + }, + ], + }; + + const baseSource = { + path: "main.cpp", + compileGroupIndex: 0, + isGenerated: false, + backtrace: 1, + }; + + it("validates TargetV2_0 schema (base version)", async function (context) { + const targetV2_0 = { + ...baseTarget, + sources: [baseSource], + compileGroups: [baseCompileGroup], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_0.json", targetV2_0], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_0.json"), + "2.0", + ); + + assert.deepStrictEqual(result, targetV2_0); + }); + + it("validates TargetV2_1 schema (added precompileHeaders)", async function (context) { + const targetV2_1 = { + ...baseTarget, + sources: [baseSource], + compileGroups: [ + { + ...baseCompileGroup, + precompileHeaders: [ + { + header: "pch.h", + backtrace: 3, + }, + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_1.json", targetV2_1], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_1.json"), + "2.1", + ); + + assert.deepStrictEqual(result, targetV2_1); + }); + + it("validates TargetV2_2 schema (added languageStandard)", async function (context) { + const targetV2_2 = { + ...baseTarget, + sources: [baseSource], + compileGroups: [ + { + ...baseCompileGroup, + precompileHeaders: [ + { + header: "pch.h", + backtrace: 3, + }, + ], + languageStandard: { + backtraces: [4], + standard: "17", + }, + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_2.json", targetV2_2], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_2.json"), + "2.2", + ); + + assert.deepStrictEqual(result, targetV2_2); + }); + + it("validates TargetV2_5 schema (added fileSets and fileSetIndex)", async function (context) { + const targetV2_5 = { + ...baseTarget, + fileSets: [ + { + name: "HEADERS", + type: "HEADERS", + visibility: "PUBLIC" as const, + baseDirectories: ["."], + }, + ], + sources: [ + { + ...baseSource, + fileSetIndex: 0, + }, + ], + compileGroups: [ + { + ...baseCompileGroup, + precompileHeaders: [ + { + header: "pch.h", + backtrace: 3, + }, + ], + languageStandard: { + backtraces: [4], + standard: "17", + }, + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_5.json", targetV2_5], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_5.json"), + "2.5", + ); + + assert.deepStrictEqual(result, targetV2_5); + }); + + it("validates TargetV2_6 schema (added frameworks)", async function (context) { + const targetV2_6 = { + ...baseTarget, + fileSets: [ + { + name: "HEADERS", + type: "HEADERS", + visibility: "PUBLIC" as const, + baseDirectories: ["."], + }, + ], + sources: [ + { + ...baseSource, + fileSetIndex: 0, + }, + ], + compileGroups: [ + { + ...baseCompileGroup, + precompileHeaders: [ + { + header: "pch.h", + backtrace: 3, + }, + ], + languageStandard: { + backtraces: [4], + standard: "17", + }, + frameworks: [ + { + path: "/System/Library/Frameworks/Foundation.framework", + isSystem: true, + backtrace: 5, + }, + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_6.json", targetV2_6], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_6.json"), + "2.6", + ); + + assert.deepStrictEqual(result, targetV2_6); + }); + + it("validates TargetV2_7 schema (added launchers)", async function (context) { + const targetV2_7 = { + ...baseTarget, + launchers: [ + { + command: "/usr/bin/gdb", + arguments: ["--args"], + type: "test" as const, + }, + ], + fileSets: [ + { + name: "HEADERS", + type: "HEADERS", + visibility: "PUBLIC" as const, + baseDirectories: ["."], + }, + ], + sources: [ + { + ...baseSource, + fileSetIndex: 0, + }, + ], + compileGroups: [ + { + ...baseCompileGroup, + precompileHeaders: [ + { + header: "pch.h", + backtrace: 3, + }, + ], + languageStandard: { + backtraces: [4], + standard: "17", + }, + frameworks: [ + { + path: "/System/Library/Frameworks/Foundation.framework", + isSystem: true, + backtrace: 5, + }, + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_7.json", targetV2_7], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_7.json"), + "2.7", + ); + + assert.deepStrictEqual(result, targetV2_7); + }); + + it("validates TargetV2_8 schema (added debugger)", async function (context) { + const targetV2_8 = { + ...baseTarget, + debugger: { + workingDirectory: "/path/to/debug", + }, + launchers: [ + { + command: "/usr/bin/gdb", + arguments: ["--args"], + type: "test" as const, + }, + ], + fileSets: [ + { + name: "HEADERS", + type: "HEADERS", + visibility: "PUBLIC" as const, + baseDirectories: ["."], + }, + ], + sources: [ + { + ...baseSource, + fileSetIndex: 0, + }, + ], + compileGroups: [ + { + ...baseCompileGroup, + precompileHeaders: [ + { + header: "pch.h", + backtrace: 3, + }, + ], + languageStandard: { + backtraces: [4], + standard: "17", + }, + frameworks: [ + { + path: "/System/Library/Frameworks/Foundation.framework", + isSystem: true, + backtrace: 5, + }, + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["target-v2_8.json", targetV2_8], + ]); + const result = await readTarget( + path.join(tmpPath, "target-v2_8.json"), + "2.8", + ); + + assert.deepStrictEqual(result, targetV2_8); + }); +}); + +describe("readCache", () => { + it("reads a well-formed cache file", async function (context) { + const mockCache = { + kind: "cache", + version: { major: 2, minor: 0 }, + entries: [ + { + name: "BUILD_SHARED_LIBS", + value: "ON", + type: "BOOL", + properties: [ + { + name: "HELPSTRING", + value: "Build shared libraries", + }, + ], + }, + { + name: "CMAKE_GENERATOR", + value: "Unix Makefiles", + type: "INTERNAL", + properties: [ + { + name: "HELPSTRING", + value: "Name of generator.", + }, + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["cache-v2.json", mockCache], + ]); + const result = await readCache(path.join(tmpPath, "cache-v2.json"), "2.0"); + + // Verify the entire structure matches our mock data + assert.deepStrictEqual(result, mockCache); + }); +}); + +describe("readCmakeFiles", () => { + // Base objects for reusable test data + const baseCmakeFiles = { + kind: "cmakeFiles" as const, + paths: { + build: "/path/to/top-level-build-dir", + source: "/path/to/top-level-source-dir", + }, + inputs: [ + { + path: "CMakeLists.txt", + }, + { + isExternal: true, + path: "/path/to/external/third-party/module.cmake", + }, + ], + }; + + it("validates CmakeFilesV1_0 schema (base version)", async function (context) { + const cmakeFilesV1_0 = { + ...baseCmakeFiles, + version: { major: 1, minor: 0 }, + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["cmakeFiles-v1_0.json", cmakeFilesV1_0], + ]); + const result = await readCmakeFiles( + path.join(tmpPath, "cmakeFiles-v1_0.json"), + "1.0", + ); + + assert.deepStrictEqual(result, cmakeFilesV1_0); + }); + + it("validates CmakeFilesV1_1 schema (added globsDependent)", async function (context) { + const cmakeFilesV1_1 = { + ...baseCmakeFiles, + version: { major: 1, minor: 1 }, + globsDependent: [ + { + expression: "src/*.cxx", + recurse: true, + paths: ["src/foo.cxx", "src/bar.cxx"], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["cmakeFiles-v1_1.json", cmakeFilesV1_1], + ]); + const result = await readCmakeFiles( + path.join(tmpPath, "cmakeFiles-v1_1.json"), + "1.1", + ); + + assert.deepStrictEqual(result, cmakeFilesV1_1); + }); +}); + +describe("readToolchains", () => { + it("reads a well-formed toolchains file", async function (context) { + const mockToolchains = { + kind: "toolchains", + version: { major: 1, minor: 0 }, + toolchains: [ + { + language: "C", + compiler: { + path: "/usr/bin/cc", + id: "GNU", + version: "9.3.0", + implicit: { + includeDirectories: [ + "/usr/lib/gcc/x86_64-linux-gnu/9/include", + "/usr/local/include", + "/usr/include/x86_64-linux-gnu", + "/usr/include", + ], + linkDirectories: [ + "/usr/lib/gcc/x86_64-linux-gnu/9", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib", + "/lib/x86_64-linux-gnu", + "/lib", + ], + linkFrameworkDirectories: [], + linkLibraries: ["gcc", "gcc_s", "c", "gcc", "gcc_s"], + }, + }, + sourceFileExtensions: ["c", "m"], + }, + { + language: "CXX", + compiler: { + path: "/usr/bin/c++", + id: "GNU", + version: "9.3.0", + implicit: { + includeDirectories: [ + "/usr/include/c++/9", + "/usr/include/x86_64-linux-gnu/c++/9", + "/usr/include/c++/9/backward", + "/usr/lib/gcc/x86_64-linux-gnu/9/include", + "/usr/local/include", + "/usr/include/x86_64-linux-gnu", + "/usr/include", + ], + linkDirectories: [ + "/usr/lib/gcc/x86_64-linux-gnu/9", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib", + "/lib/x86_64-linux-gnu", + "/lib", + ], + linkFrameworkDirectories: [], + linkLibraries: [ + "stdc++", + "m", + "gcc_s", + "gcc", + "c", + "gcc_s", + "gcc", + ], + }, + }, + sourceFileExtensions: [ + "C", + "M", + "c++", + "cc", + "cpp", + "cxx", + "mm", + "CPP", + ], + }, + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["toolchains-v1.json", mockToolchains], + ]); + const result = await readToolchains( + path.join(tmpPath, "toolchains-v1.json"), + "1.0", + ); + + // Verify the entire structure matches our mock data + assert.deepStrictEqual(result, mockToolchains); + }); +}); + +describe("readConfigureLog", () => { + it("reads a well-formed configureLog file", async function (context) { + const mockConfigureLog = { + kind: "configureLog", + version: { major: 1, minor: 0 }, + path: "/path/to/build/dir/CMakeFiles/CMakeConfigureLog.yaml", + eventKindNames: [ + "message", + "try_compile-v1", + "try_run-v1", + "detect-c_compiler-v1", + "detect-cxx_compiler-v1", + ], + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["configureLog-v1.json", mockConfigureLog], + ]); + const result = await readConfigureLog( + path.join(tmpPath, "configureLog-v1.json"), + "1.0", + ); + assert.deepStrictEqual(result, mockConfigureLog); + }); +}); + +describe("Reply Error Index Support", () => { + describe("isReplyErrorIndexPath", () => { + it("identifies reply error index files correctly", function () { + assert.strictEqual( + isReplyErrorIndexPath("/path/to/error-12345.json"), + true, + ); + assert.strictEqual( + isReplyErrorIndexPath("/path/to/index-12345.json"), + false, + ); + assert.strictEqual(isReplyErrorIndexPath("error-abc.json"), true); + assert.strictEqual(isReplyErrorIndexPath("index-abc.json"), false); + }); + }); + + describe("readReplyErrorIndex", () => { + it("reads a well-formed reply error index file", async function (context) { + const mockReplyErrorIndex = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { + multiConfig: false, + name: "Unix Makefiles", + }, + }, + objects: [ + { + kind: "configureLog", + version: { major: 1, minor: 0 }, + jsonFile: "configureLog-v1-12345.json", + }, + ], + reply: { + "configureLog-v1": { + kind: "configureLog", + version: { major: 1, minor: 0 }, + jsonFile: "configureLog-v1-12345.json", + }, + "codemodel-v2": { + error: "CMake failed to generate build system", + }, + "cache-v2": { + error: "CMake failed to generate build system", + }, + }, + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["error-12345.json", mockReplyErrorIndex], + ]); + const result = await readReplyErrorIndex( + path.join(tmpPath, "error-12345.json"), + ); + + assert.deepStrictEqual(result, mockReplyErrorIndex); + }); + + it("rejects non-reply error index files", async function (context) { + const tmpPath = createMockReplyDirectory(context, [ + ["index-12345.json", {}], + ]); + + await assert.rejects( + () => readReplyErrorIndex(path.join(tmpPath, "index-12345.json")), + /Expected a path to an error-\*\.json file/, + ); + }); + + it("rejects reply error index with unsupported object kind in objects array", async function (context) { + const invalidReplyErrorIndex = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { + multiConfig: false, + name: "Unix Makefiles", + }, + }, + objects: [ + { + kind: "codemodel", // Invalid: only configureLog is supported + version: { major: 2, minor: 0 }, + jsonFile: "codemodel-v2-12345.json", + }, + ], + reply: {}, + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["error-12345.json", invalidReplyErrorIndex], + ]); + + await assert.rejects( + () => readReplyErrorIndex(path.join(tmpPath, "error-12345.json")), + (error: Error) => { + return error.message.includes( + 'Invalid input: expected \\"configureLog\\"', + ); + }, + ); + }); + + it("rejects reply error index with unsupported object kind in client stateful query responses", async function (context) { + const invalidReplyErrorIndex = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { + multiConfig: false, + name: "Unix Makefiles", + }, + }, + objects: [], + reply: { + "client-test": { + "query.json": { + client: {}, + requests: [ + { + kind: "codemodel", + version: { major: 2, minor: 0 }, + }, + ], + responses: [ + { + kind: "codemodel", // Invalid: only configureLog is supported in error index + version: { major: 2, minor: 0 }, + jsonFile: "codemodel-v2-12345.json", + }, + ], + }, + }, + }, + }; + + const tmpPath = createMockReplyDirectory(context, [ + ["error-12345.json", invalidReplyErrorIndex], + ]); + + await assert.rejects( + () => readReplyErrorIndex(path.join(tmpPath, "error-12345.json")), + (error: Error) => { + return error.message.includes( + 'Invalid input: expected \\"configureLog\\"', + ); + }, + ); + }); + }); + + describe("readCurrentCodemodel with error index handling", () => { + it("throws descriptive error when current index is a reply error index", async function (context) { + const mockReplyErrorIndex = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { multiConfig: false, name: "Unix Makefiles" }, + }, + objects: [], + reply: { + "codemodel-v2": { error: "Build system generation failed" }, + }, + }; + + const buildPath = createTempDir(context, "build-test-"); + const replyPath = path.join(buildPath, ".cmake/api/v1/reply"); + await fs.promises.mkdir(replyPath, { recursive: true }); + + fs.writeFileSync( + path.join(replyPath, "error-12345.json"), + JSON.stringify(mockReplyErrorIndex), + ); + + await assert.rejects( + () => readCurrentSharedCodemodel(buildPath), + /CMake failed to generate build system\. Error in codemodel: Build system generation failed/, + ); + }); + + it("throws generic error when reply error index has no codemodel entry", async function (context) { + const mockReplyErrorIndex = { + cmake: { + version: { + major: 3, + minor: 26, + patch: 0, + suffix: "", + string: "3.26.0", + isDirty: false, + }, + paths: { + cmake: "/usr/bin/cmake", + ctest: "/usr/bin/ctest", + cpack: "/usr/bin/cpack", + root: "/usr/share/cmake", + }, + generator: { multiConfig: false, name: "Unix Makefiles" }, + }, + objects: [], + reply: {}, + }; + + const buildPath = createTempDir(context, "build-test-"); + const replyPath = path.join(buildPath, ".cmake/api/v1/reply"); + await fs.promises.mkdir(replyPath, { recursive: true }); + + fs.writeFileSync( + path.join(replyPath, "error-12345.json"), + JSON.stringify(mockReplyErrorIndex), + ); + + await assert.rejects( + () => readCurrentSharedCodemodel(buildPath), + /CMake failed to generate build system\. No codemodel available in error index\./, + ); + }); + }); +}); diff --git a/packages/cmake-file-api/src/reply.ts b/packages/cmake-file-api/src/reply.ts new file mode 100644 index 00000000..0a8a1a02 --- /dev/null +++ b/packages/cmake-file-api/src/reply.ts @@ -0,0 +1,240 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +import * as z from "zod"; + +import * as schemas from "./schemas.js"; + +/** + * As per https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html#v1-reply-error-index + */ +export async function findCurrentReplyIndexPath(replyPath: string) { + // If multiple index-*.json and/or error-*.json files are present, + // the one with the largest name in lexicographic order, + // excluding the index- or error- prefix, is the current index. + + const fileNames = ( + await Promise.all([ + Array.fromAsync( + fs.promises.glob("error-*.json", { + withFileTypes: false, + cwd: replyPath, + }), + ), + Array.fromAsync( + fs.promises.glob("index-*.json", { + withFileTypes: false, + cwd: replyPath, + }), + ), + ]) + ).flat(); + + const [currentIndexFileName] = fileNames + .sort((a, b) => { + const strippedA = a.replace(/^(error|index)-/, ""); + const strippedB = b.replace(/^(error|index)-/, ""); + return strippedA.localeCompare(strippedB); + }) + .reverse(); + + assert( + currentIndexFileName, + `No index-*.json or error-*.json files found in ${replyPath}`, + ); + + return path.join(replyPath, currentIndexFileName); +} + +export async function readReplyIndex(filePath: string) { + assert( + path.basename(filePath).startsWith("index-") && + path.extname(filePath) === ".json", + "Expected a path to a index-*.json file", + ); + const content = await fs.promises.readFile(filePath, "utf-8"); + return schemas.IndexReplyV1.parse(JSON.parse(content)); +} + +export function isReplyErrorIndexPath(filePath: string): boolean { + return ( + path.basename(filePath).startsWith("error-") && + path.extname(filePath) === ".json" + ); +} + +export async function readReplyErrorIndex(filePath: string) { + assert( + isReplyErrorIndexPath(filePath), + "Expected a path to an error-*.json file", + ); + const content = await fs.promises.readFile(filePath, "utf-8"); + return schemas.ReplyErrorIndex.parse(JSON.parse(content)); +} + +export async function readCodemodel(filePath: string) { + assert( + path.basename(filePath).startsWith("codemodel-") && + path.extname(filePath) === ".json", + "Expected a path to a codemodel-*.json file", + ); + const content = await fs.promises.readFile(filePath, "utf-8"); + return schemas.CodemodelV2.parse(JSON.parse(content)); +} + +/** + * Call {@link createSharedStatelessQuery} to create a shared codemodel query before reading the current shared codemodel. + */ +export async function readCurrentSharedCodemodel(buildPath: string) { + const replyPath = path.join(buildPath, `.cmake/api/v1/reply`); + const replyIndexPath = await findCurrentReplyIndexPath(replyPath); + + // Check if this is an error index - they don't contain codemodel data + if (isReplyErrorIndexPath(replyIndexPath)) { + const errorIndex = await readReplyErrorIndex(replyIndexPath); + const { reply } = errorIndex; + const codemodelFile = reply["codemodel-v2"]; + + if ( + codemodelFile && + "error" in codemodelFile && + typeof codemodelFile.error === "string" + ) { + throw new Error( + `CMake failed to generate build system. Error in codemodel: ${codemodelFile.error}`, + ); + } + + throw new Error( + "CMake failed to generate build system. No codemodel available in error index.", + ); + } + + const index = await readReplyIndex(replyIndexPath); + const { reply } = index; + const { "codemodel-v2": codemodelFile } = reply; + assert( + codemodelFile, + "Expected a codemodel-v2 reply file - was a query created?", + ); + if ("error" in codemodelFile && typeof codemodelFile.error === "string") { + throw new Error( + `Error reading codemodel-v2 reply file: ${codemodelFile.error}`, + ); + } + + // Use ReplyFileReference schema to validate and parse the codemodel file + const { kind, jsonFile } = schemas.ReplyFileReferenceV1.parse(codemodelFile); + assert(kind === "codemodel", "Expected a codemodel file reference"); + + const codemodelPath = path.join(buildPath, `.cmake/api/v1/reply`, jsonFile); + return readCodemodel(codemodelPath); +} + +export async function readCurrentTargets( + buildPath: string, + configuration: string, +) { + const { configurations } = await readCurrentSharedCodemodel(buildPath); + const relevantConfig = + configurations.length === 1 + ? configurations[0] + : configurations.find((config) => config.name === configuration); + assert( + relevantConfig, + `Unable to locate "${configuration}" configuration found`, + ); + return relevantConfig.targets; +} + +export async function readTarget( + targetPath: string, + version: keyof typeof schemas.targetSchemaPerVersion, +): Promise> { + assert( + path.basename(targetPath).startsWith("target-") && + path.extname(targetPath) === ".json", + "Expected a path to a target-*.json file", + ); + const content = await fs.promises.readFile(targetPath, "utf-8"); + return schemas.targetSchemaPerVersion[version].parse(JSON.parse(content)); +} + +export async function readCurrentTargetsDeep( + buildPath: string, + configuration: string, + version: keyof typeof schemas.targetSchemaPerVersion, +): Promise[]> { + const targets = await readCurrentTargets(buildPath, configuration); + return Promise.all( + targets.map((target) => { + const targetPath = path.join( + buildPath, + `.cmake/api/v1/reply`, + target.jsonFile, + ); + return readTarget(targetPath, version); + }), + ); +} + +export async function readCache( + cachePath: string, + version: keyof typeof schemas.cacheSchemaPerVersion, +): Promise> { + assert( + path.basename(cachePath).startsWith("cache-") && + path.extname(cachePath) === ".json", + "Expected a path to a cache-*.json file", + ); + const content = await fs.promises.readFile(cachePath, "utf-8"); + return schemas.cacheSchemaPerVersion[version].parse(JSON.parse(content)); +} + +export async function readCmakeFiles( + cmakeFilesPath: string, + version: keyof typeof schemas.cmakeFilesSchemaPerVersion, +): Promise< + z.infer<(typeof schemas.cmakeFilesSchemaPerVersion)[typeof version]> +> { + assert( + path.basename(cmakeFilesPath).startsWith("cmakeFiles-") && + path.extname(cmakeFilesPath) === ".json", + "Expected a path to a cmakeFiles-*.json file", + ); + const content = await fs.promises.readFile(cmakeFilesPath, "utf-8"); + return schemas.cmakeFilesSchemaPerVersion[version].parse(JSON.parse(content)); +} + +export async function readToolchains( + toolchainsPath: string, + version: keyof typeof schemas.toolchainsSchemaPerVersion, +): Promise< + z.infer<(typeof schemas.toolchainsSchemaPerVersion)[typeof version]> +> { + assert( + path.basename(toolchainsPath).startsWith("toolchains-") && + path.extname(toolchainsPath) === ".json", + "Expected a path to a toolchains-*.json file", + ); + const content = await fs.promises.readFile(toolchainsPath, "utf-8"); + return schemas.toolchainsSchemaPerVersion[version].parse(JSON.parse(content)); +} + +export async function readConfigureLog( + configureLogPath: string, + version: keyof typeof schemas.configureLogSchemaPerVersion, +): Promise< + z.infer<(typeof schemas.configureLogSchemaPerVersion)[typeof version]> +> { + assert( + path.basename(configureLogPath).startsWith("configureLog-") && + path.extname(configureLogPath) === ".json", + "Expected a path to a configureLog-*.json file", + ); + const content = await fs.promises.readFile(configureLogPath, "utf-8"); + return schemas.configureLogSchemaPerVersion[version].parse( + JSON.parse(content), + ); +} diff --git a/packages/cmake-file-api/src/schemas.ts b/packages/cmake-file-api/src/schemas.ts new file mode 100644 index 00000000..3984bee5 --- /dev/null +++ b/packages/cmake-file-api/src/schemas.ts @@ -0,0 +1,7 @@ +export * from "./schemas/ReplyIndexV1.js"; +export * from "./schemas/objects/CodemodelV2.js"; +export * from "./schemas/objects/TargetV2.js"; +export * from "./schemas/objects/CacheV2.js"; +export * from "./schemas/objects/CmakeFilesV1.js"; +export * from "./schemas/objects/ToolchainsV1.js"; +export * from "./schemas/objects/ConfigureLogV1.js"; diff --git a/packages/cmake-file-api/src/schemas/ReplyIndexV1.ts b/packages/cmake-file-api/src/schemas/ReplyIndexV1.ts new file mode 100644 index 00000000..272ba4ce --- /dev/null +++ b/packages/cmake-file-api/src/schemas/ReplyIndexV1.ts @@ -0,0 +1,116 @@ +import * as z from "zod"; + +export const ReplyFileReferenceV1 = z.object({ + kind: z.enum([ + "codemodel", + "configureLog", + "cache", + "cmakeFiles", + "toolchains", + ]), + version: z.object({ + major: z.number(), + minor: z.number(), + }), + jsonFile: z.string(), +}); + +const ReplyErrorObject = z.object({ + error: z.string(), +}); + +const VersionNumber = z.number(); + +const VersionObject = z.object({ + major: z.number(), + minor: z.number().optional(), +}); + +const VersionSpec = z.union([ + VersionNumber, + VersionObject, + z.array(z.union([VersionNumber, VersionObject])), +]); + +const QueryRequest = z.object({ + kind: z.string(), + version: VersionSpec.optional(), + client: z.unknown().optional(), +}); + +const ClientStatefulQueryReply = z.object({ + client: z.unknown().optional(), + requests: z.array(QueryRequest).optional(), + responses: z.array(ReplyFileReferenceV1).optional(), +}); + +export const IndexReplyV1 = z.object({ + cmake: z.object({ + version: z.object({ + major: z.number(), + minor: z.number(), + patch: z.number(), + suffix: z.string(), + string: z.string(), + isDirty: z.boolean(), + }), + paths: z.object({ + cmake: z.string(), + ctest: z.string(), + cpack: z.string(), + root: z.string(), + }), + generator: z.object({ + multiConfig: z.boolean(), + name: z.string(), + platform: z.string().optional(), + }), + }), + objects: z.array(ReplyFileReferenceV1), + reply: z.record( + z.string(), + z + .union([ + ReplyFileReferenceV1, + ReplyErrorObject, + z.record( + z.string(), + z.union([ + ReplyFileReferenceV1, + ReplyErrorObject, + ClientStatefulQueryReply, + ]), + ), + ]) + .optional(), + ), +}); + +const ReplyErrorIndexFileReference = ReplyFileReferenceV1.extend({ + kind: z.enum(["configureLog"]), +}); + +const ClientStatefulQueryReplyForErrorIndex = ClientStatefulQueryReply.extend({ + responses: z.array(ReplyErrorIndexFileReference).optional(), +}); + +export const ReplyErrorIndex = IndexReplyV1.extend({ + objects: z.array(ReplyErrorIndexFileReference), + reply: z.record( + z.string(), + z + .union([ + ReplyErrorIndexFileReference, + ReplyErrorObject, + z.record( + z.string(), + z.union([ + ReplyErrorIndexFileReference, + ReplyErrorObject, + ClientStatefulQueryReplyForErrorIndex, + ]), + ), + ]) + .optional(), + ), +}); diff --git a/packages/cmake-file-api/src/schemas/objects/CacheV2.ts b/packages/cmake-file-api/src/schemas/objects/CacheV2.ts new file mode 100644 index 00000000..a85d6caf --- /dev/null +++ b/packages/cmake-file-api/src/schemas/objects/CacheV2.ts @@ -0,0 +1,28 @@ +import * as z from "zod"; + +const CacheEntryProperty = z.object({ + name: z.string(), + value: z.string(), +}); + +const CacheEntry = z.object({ + name: z.string(), + value: z.string(), + type: z.string(), + properties: z.array(CacheEntryProperty), +}); + +export const CacheV2_0 = z.object({ + kind: z.literal("cache"), + version: z.object({ + major: z.literal(2), + minor: z.number().int().nonnegative(), + }), + entries: z.array(CacheEntry), +}); + +export const CacheV2 = z.union([CacheV2_0]); + +export const cacheSchemaPerVersion = { + "2.0": CacheV2_0, +} as const satisfies Record; diff --git a/packages/cmake-file-api/src/schemas/objects/CmakeFilesV1.ts b/packages/cmake-file-api/src/schemas/objects/CmakeFilesV1.ts new file mode 100644 index 00000000..f23bd999 --- /dev/null +++ b/packages/cmake-file-api/src/schemas/objects/CmakeFilesV1.ts @@ -0,0 +1,45 @@ +import * as z from "zod"; + +const CmakeFilesInput = z.object({ + path: z.string(), + isGenerated: z.boolean().optional(), + isExternal: z.boolean().optional(), + isCMake: z.boolean().optional(), +}); + +const CmakeFilesGlobDependent = z.object({ + expression: z.string(), + recurse: z.boolean().optional(), + listDirectories: z.boolean().optional(), + followSymlinks: z.boolean().optional(), + relative: z.string().optional(), + paths: z.array(z.string()), +}); + +export const CmakeFilesV1_0 = z.object({ + kind: z.literal("cmakeFiles"), + version: z.object({ + major: z.literal(1), + minor: z.number().max(0), + }), + paths: z.object({ + source: z.string(), + build: z.string(), + }), + inputs: z.array(CmakeFilesInput), +}); + +export const CmakeFilesV1_1 = CmakeFilesV1_0.extend({ + version: z.object({ + major: z.literal(1), + minor: z.number().min(1), + }), + globsDependent: z.array(CmakeFilesGlobDependent).optional(), +}); + +export const CmakeFilesV1 = z.union([CmakeFilesV1_0, CmakeFilesV1_1]); + +export const cmakeFilesSchemaPerVersion = { + "1.0": CmakeFilesV1_0, + "1.1": CmakeFilesV1_1, +} as const satisfies Record; diff --git a/packages/cmake-file-api/src/schemas/objects/CodemodelV2.ts b/packages/cmake-file-api/src/schemas/objects/CodemodelV2.ts new file mode 100644 index 00000000..17fc202f --- /dev/null +++ b/packages/cmake-file-api/src/schemas/objects/CodemodelV2.ts @@ -0,0 +1,77 @@ +import * as z from "zod"; + +const index = z.number().int().nonnegative(); + +const MinimumCMakeVersion = z.object({ + string: z.string(), +}); + +const DirectoryV2_0 = z.object({ + source: z.string(), + build: z.string(), + parentIndex: index.optional(), + childIndexes: z.array(index).optional(), + projectIndex: index, + targetIndexes: z.array(index).optional(), + minimumCMakeVersion: MinimumCMakeVersion.optional(), + hasInstallRule: z.boolean().optional(), +}); + +const DirectoryV2_3 = DirectoryV2_0.extend({ + jsonFile: z.string(), +}); + +const Project = z.object({ + name: z.string(), + parentIndex: index.optional(), + childIndexes: z.array(index).optional(), + directoryIndexes: z.array(index), + targetIndexes: z.array(index).optional(), +}); + +const Target = z.object({ + name: z.string(), + id: z.string(), + directoryIndex: index, + projectIndex: index, + jsonFile: z.string(), +}); + +const ConfigurationV2_0 = z.object({ + name: z.string(), + directories: z.array(DirectoryV2_0), + projects: z.array(Project), + targets: z.array(Target), +}); + +const ConfigurationV2_3 = ConfigurationV2_0.extend({ + directories: z.array(DirectoryV2_3), +}); + +export const CodemodelV2_0 = z.object({ + kind: z.literal("codemodel"), + version: z.object({ + major: z.literal(2), + minor: z.number().max(2), + }), + paths: z.object({ + source: z.string(), + build: z.string(), + }), + configurations: z.array(ConfigurationV2_0), +}); + +export const CodemodelV2_3 = CodemodelV2_0.extend({ + version: z.object({ + major: z.literal(2), + minor: z.number().min(3), + }), + configurations: z.array(ConfigurationV2_3), +}); + +export const CodemodelV2 = z.union([CodemodelV2_0, CodemodelV2_3]); + +export const codemodelFilesSchemaPerVersion = { + "2.0": CodemodelV2_0, + "2.3": CodemodelV2_3, +} as const satisfies Record; diff --git a/packages/cmake-file-api/src/schemas/objects/ConfigureLogV1.ts b/packages/cmake-file-api/src/schemas/objects/ConfigureLogV1.ts new file mode 100644 index 00000000..6f6688ef --- /dev/null +++ b/packages/cmake-file-api/src/schemas/objects/ConfigureLogV1.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export const ConfigureLogV1_0 = z.object({ + kind: z.literal("configureLog"), + version: z.object({ + major: z.literal(1), + minor: z.literal(0), + }), + path: z.string(), + eventKindNames: z.array(z.string()), +}); + +export const ConfigureLogV1 = z.union([ConfigureLogV1_0]); + +export const configureLogSchemaPerVersion = { + "1.0": ConfigureLogV1_0, +} as const satisfies Record; diff --git a/packages/cmake-file-api/src/schemas/objects/TargetV2.ts b/packages/cmake-file-api/src/schemas/objects/TargetV2.ts new file mode 100644 index 00000000..da34e479 --- /dev/null +++ b/packages/cmake-file-api/src/schemas/objects/TargetV2.ts @@ -0,0 +1,253 @@ +import * as z from "zod"; + +const index = z.number().int().nonnegative(); + +const Artifact = z.object({ + path: z.string(), +}); + +const Folder = z.object({ + name: z.string(), +}); + +const InstallPrefix = z.object({ + path: z.string(), +}); + +const InstallDestination = z.object({ + path: z.string(), + backtrace: index.optional(), +}); + +const Install = z.object({ + prefix: InstallPrefix, + destinations: z.array(InstallDestination), +}); + +const Launcher = z.object({ + command: z.string(), + arguments: z.array(z.string()).optional(), + type: z.enum(["emulator", "test"]), +}); + +const LinkCommandFragment = z.object({ + fragment: z.string(), + role: z.enum(["flags", "libraries", "libraryPath", "frameworkPath"]), + backtrace: index.optional(), +}); + +const Sysroot = z.object({ + path: z.string(), +}); + +const Link = z.object({ + language: z.string(), + commandFragments: z.array(LinkCommandFragment).optional(), + lto: z.boolean().optional(), + sysroot: Sysroot.optional(), +}); + +const ArchiveCommandFragment = z.object({ + fragment: z.string(), + role: z.enum(["flags"]), +}); + +const Archive = z.object({ + commandFragments: z.array(ArchiveCommandFragment).optional(), + lto: z.boolean().optional(), +}); + +const Debugger = z.object({ + workingDirectory: z.string().optional(), +}); + +const Dependency = z.object({ + id: z.string(), + backtrace: index.optional(), +}); + +const FileSet = z.object({ + name: z.string(), + type: z.string(), + visibility: z.enum(["PUBLIC", "PRIVATE", "INTERFACE"]), + baseDirectories: z.array(z.string()), +}); + +const SourceGroup = z.object({ + name: z.string(), + sourceIndexes: z.array(index), +}); + +const LanguageStandard = z.object({ + backtraces: z.array(index).optional(), + standard: z.string(), +}); + +const CompileCommandFragment = z.object({ + fragment: z.string(), + backtrace: index.optional(), +}); + +const Include = z.object({ + path: z.string(), + isSystem: z.boolean().optional(), + backtrace: index.optional(), +}); + +const Framework = z.object({ + path: z.string(), + isSystem: z.boolean().optional(), + backtrace: index.optional(), +}); + +const PrecompileHeader = z.object({ + header: z.string(), + backtrace: index.optional(), +}); + +const Define = z.object({ + define: z.string(), + backtrace: index.optional(), +}); + +const BacktraceNode = z.object({ + file: index, + line: z.number().int().positive().optional(), + command: index.optional(), + parent: index.optional(), +}); + +const BacktraceGraph = z.object({ + nodes: z.array(BacktraceNode), + commands: z.array(z.string()), + files: z.array(z.string()), +}); + +// Versioned nested schemas +const SourceV2_0 = z.object({ + path: z.string(), + compileGroupIndex: index.optional(), + sourceGroupIndex: index.optional(), + isGenerated: z.boolean().optional(), + backtrace: index.optional(), +}); + +const SourceV2_5 = SourceV2_0.extend({ + fileSetIndex: index.optional(), +}); + +const CompileGroupV2_0 = z.object({ + sourceIndexes: z.array(index), + language: z.string(), + compileCommandFragments: z.array(CompileCommandFragment).optional(), + includes: z.array(Include).optional(), + defines: z.array(Define).optional(), + sysroot: Sysroot.optional(), +}); + +const CompileGroupV2_1 = CompileGroupV2_0.extend({ + precompileHeaders: z.array(PrecompileHeader).optional(), +}); + +const CompileGroupV2_2 = CompileGroupV2_1.extend({ + languageStandard: LanguageStandard.optional(), +}); + +const CompileGroupV2_6 = CompileGroupV2_2.extend({ + frameworks: z.array(Framework).optional(), +}); + +// Base version (v2.0) - Original target fields +const TargetV2_0 = z.object({ + name: z.string(), + id: z.string(), + type: z.enum([ + "EXECUTABLE", + "STATIC_LIBRARY", + "SHARED_LIBRARY", + "MODULE_LIBRARY", + "OBJECT_LIBRARY", + "INTERFACE_LIBRARY", + "UTILITY", + ]), + backtrace: index.optional(), + folder: Folder.optional(), + paths: z.object({ + source: z.string(), + build: z.string(), + }), + nameOnDisk: z.string().optional(), + artifacts: z.array(Artifact).optional(), + isGeneratorProvided: z.boolean().optional(), + install: Install.optional(), + link: Link.optional(), + archive: Archive.optional(), + dependencies: z.array(Dependency).optional(), + sources: z.array(SourceV2_0).optional(), + sourceGroups: z.array(SourceGroup).optional(), + compileGroups: z.array(CompileGroupV2_0).optional(), + backtraceGraph: BacktraceGraph.optional(), +}); + +// v2.1+ - Added precompileHeaders +const TargetV2_1 = TargetV2_0.extend({ + compileGroups: z.array(CompileGroupV2_1).optional(), +}); + +// v2.2+ - Added languageStandard +const TargetV2_2 = TargetV2_1.extend({ + compileGroups: z.array(CompileGroupV2_2).optional(), +}); + +// v2.5+ - Added fileSets and fileSetIndex in sources +const TargetV2_5 = TargetV2_2.extend({ + fileSets: z.array(FileSet).optional(), + sources: z.array(SourceV2_5).optional(), +}); + +// v2.6+ - Added frameworks +const TargetV2_6 = TargetV2_5.extend({ + compileGroups: z.array(CompileGroupV2_6).optional(), +}); + +// v2.7+ - Added launchers +const TargetV2_7 = TargetV2_6.extend({ + launchers: z.array(Launcher).optional(), +}); + +// v2.8+ - Added debugger +const TargetV2_8 = TargetV2_7.extend({ + debugger: Debugger.optional(), +}); + +// Export union of all versions for flexible validation +export const TargetV2 = z.union([ + TargetV2_0, + TargetV2_1, + TargetV2_2, + TargetV2_5, + TargetV2_6, + TargetV2_7, + TargetV2_8, +]); + +// Also export individual versions for specific use cases +export { + TargetV2_0, + TargetV2_1, + TargetV2_2, + TargetV2_5, + TargetV2_6, + TargetV2_7, + TargetV2_8, +}; + +export const targetSchemaPerVersion = { + "2.0": TargetV2_0, + "2.1": TargetV2_1, + "2.2": TargetV2_2, + "2.5": TargetV2_5, + "2.6": TargetV2_6, + "2.7": TargetV2_7, + "2.8": TargetV2_8, +} as const satisfies Record; diff --git a/packages/cmake-file-api/src/schemas/objects/ToolchainsV1.ts b/packages/cmake-file-api/src/schemas/objects/ToolchainsV1.ts new file mode 100644 index 00000000..1e3f79c2 --- /dev/null +++ b/packages/cmake-file-api/src/schemas/objects/ToolchainsV1.ts @@ -0,0 +1,37 @@ +import * as z from "zod"; + +const ToolchainCompilerImplicit = z.object({ + includeDirectories: z.array(z.string()).optional(), + linkDirectories: z.array(z.string()).optional(), + linkFrameworkDirectories: z.array(z.string()).optional(), + linkLibraries: z.array(z.string()).optional(), +}); + +const ToolchainCompiler = z.object({ + path: z.string().optional(), + id: z.string().optional(), + version: z.string().optional(), + target: z.string().optional(), + implicit: ToolchainCompilerImplicit, +}); + +const Toolchain = z.object({ + language: z.string(), + compiler: ToolchainCompiler, + sourceFileExtensions: z.array(z.string()).optional(), +}); + +export const ToolchainsV1_0 = z.object({ + kind: z.literal("toolchains"), + version: z.object({ + major: z.literal(1), + minor: z.number().int().nonnegative(), + }), + toolchains: z.array(Toolchain), +}); + +export const ToolchainsV1 = z.union([ToolchainsV1_0]); + +export const toolchainsSchemaPerVersion = { + "1.0": ToolchainsV1_0, +} as const satisfies Record; diff --git a/packages/cmake-file-api/tsconfig.json b/packages/cmake-file-api/tsconfig.json new file mode 100644 index 00000000..f183b9a9 --- /dev/null +++ b/packages/cmake-file-api/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../configs/tsconfig.node.json" +} diff --git a/packages/cmake-file-api/tsconfig.tests.json b/packages/cmake-file-api/tsconfig.tests.json new file mode 100644 index 00000000..c203a437 --- /dev/null +++ b/packages/cmake-file-api/tsconfig.tests.json @@ -0,0 +1,8 @@ +{ + "extends": "../../configs/tsconfig.node-tests.json", + "references": [ + { + "path": "./tsconfig.json" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index cde11ae1..a733c3ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,8 @@ "files": ["prettier.config.js", "eslint.config.js"], "references": [ { "path": "./packages/cli-utils/tsconfig.json" }, + { "path": "./packages/cmake-file-api/tsconfig.json" }, + { "path": "./packages/cmake-file-api/tsconfig.tests.json" }, { "path": "./packages/host/tsconfig.json" }, { "path": "./packages/gyp-to-cmake/tsconfig.json" }, { "path": "./packages/cmake-rn/tsconfig.json" }, From a23af5a4f765fa583e7ea77d4136c1e1d8170151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 29 Sep 2025 07:39:37 +0200 Subject: [PATCH 15/82] Use CMake file API to read shared library target paths (#254) * Use CMake file API to read shared library target paths * Refactor createAppleFramework into an async function * Correcting createAndroidLibsDirectory types * Support multiple shared library outputs * Fix precondition for exactly one artifact * Take array of triplet + path when creating Android prebuilds * Use bufout spawn in createAppleFramework --- .changeset/evil-vans-love.md | 5 + package-lock.json | 1 + packages/cmake-rn/package.json | 1 + packages/cmake-rn/src/cli.ts | 3 + packages/cmake-rn/src/platforms/android.ts | 104 ++++++++++-------- packages/cmake-rn/src/platforms/apple.ts | 111 +++++++++++--------- packages/ferric/src/build.ts | 18 ++-- packages/host/src/node/prebuilds/android.ts | 8 +- packages/host/src/node/prebuilds/apple.ts | 25 ++--- 9 files changed, 154 insertions(+), 122 deletions(-) create mode 100644 .changeset/evil-vans-love.md diff --git a/.changeset/evil-vans-love.md b/.changeset/evil-vans-love.md new file mode 100644 index 00000000..873d1d86 --- /dev/null +++ b/.changeset/evil-vans-love.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": patch +--- + +Use CMake file API to read shared library target paths diff --git a/package-lock.json b/package-lock.json index 02024acc..16c9a3d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14808,6 +14808,7 @@ "version": "0.4.0", "dependencies": { "@react-native-node-api/cli-utils": "0.1.0", + "cmake-file-api": "0.1.0", "react-native-node-api": "0.5.1" }, "bin": { diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index f78644da..91fa1bde 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@react-native-node-api/cli-utils": "0.1.0", + "cmake-file-api": "0.1.0", "react-native-node-api": "0.5.1" }, "peerDependencies": { diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 5afa8932..a7d56530 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -13,6 +13,7 @@ import { wrapAction, } from "@react-native-node-api/cli-utils"; import { isSupportedTriplet } from "react-native-node-api"; +import * as cmakeFileApi from "cmake-file-api"; import { getCmakeJSVariables, @@ -327,6 +328,8 @@ async function configureProject( { CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath }, ]; + await cmakeFileApi.createSharedStatelessQuery(buildPath, "codemodel", "2"); + await spawn( "cmake", [ diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index 832f806e..655c1d7b 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -5,9 +5,9 @@ import path from "node:path"; import { Option, oraPromise, chalk } from "@react-native-node-api/cli-utils"; import { createAndroidLibsDirectory, - determineAndroidLibsFilename, AndroidTriplet as Triplet, } from "react-native-node-api"; +import * as cmakeFileApi from "cmake-file-api"; import type { Platform } from "./types.js"; @@ -121,50 +121,64 @@ export const platform: Platform = { const { ANDROID_HOME } = process.env; return typeof ANDROID_HOME === "string" && fs.existsSync(ANDROID_HOME); }, - async postBuild({ outputPath, triplets }, { autoLink }) { - // TODO: Include `configuration` in the output path - const libraryPathByTriplet = Object.fromEntries( - await Promise.all( - triplets.map(async ({ triplet, outputPath }) => { - assert( - fs.existsSync(outputPath), - `Expected a directory at ${outputPath}`, - ); - // Expect binary file(s), either .node or .so - const dirents = await fs.promises.readdir(outputPath, { - withFileTypes: true, - }); - const result = dirents - .filter( - (dirent) => - dirent.isFile() && - (dirent.name.endsWith(".so") || dirent.name.endsWith(".node")), - ) - .map((dirent) => path.join(dirent.parentPath, dirent.name)); - assert.equal(result.length, 1, "Expected exactly one library file"); - return [triplet, result[0]] as const; - }), - ), - ) as Record; - const androidLibsFilename = determineAndroidLibsFilename( - Object.values(libraryPathByTriplet), - ); - const androidLibsOutputPath = path.resolve(outputPath, androidLibsFilename); + async postBuild({ outputPath, triplets }, { autoLink, configuration }) { + const prebuilds: Record< + string, + { triplet: Triplet; libraryPath: string }[] + > = {}; - await oraPromise( - createAndroidLibsDirectory({ - outputPath: androidLibsOutputPath, - libraryPathByTriplet, - autoLink, - }), - { - text: "Assembling Android libs directory", - successText: `Android libs directory assembled into ${chalk.dim( - path.relative(process.cwd(), androidLibsOutputPath), - )}`, - failText: ({ message }) => - `Failed to assemble Android libs directory: ${message}`, - }, - ); + for (const { triplet, buildPath } of triplets) { + assert(fs.existsSync(buildPath), `Expected a directory at ${buildPath}`); + const targets = await cmakeFileApi.readCurrentTargetsDeep( + buildPath, + configuration, + "2.0", + ); + const sharedLibraries = targets.filter( + (target) => target.type === "SHARED_LIBRARY", + ); + assert.equal( + sharedLibraries.length, + 1, + "Expected exactly one shared library", + ); + const [sharedLibrary] = sharedLibraries; + const { artifacts } = sharedLibrary; + assert( + artifacts && artifacts.length === 1, + "Expected exactly one artifact", + ); + const [artifact] = artifacts; + // Add prebuild entry, creating a new entry if needed + if (!(sharedLibrary.name in prebuilds)) { + prebuilds[sharedLibrary.name] = []; + } + prebuilds[sharedLibrary.name].push({ + triplet, + libraryPath: path.join(buildPath, artifact.path), + }); + } + + for (const [libraryName, libraries] of Object.entries(prebuilds)) { + const prebuildOutputPath = path.resolve( + outputPath, + `${libraryName}.android.node`, + ); + await oraPromise( + createAndroidLibsDirectory({ + outputPath: prebuildOutputPath, + libraries, + autoLink, + }), + { + text: `Assembling Android libs directory (${libraryName})`, + successText: `Android libs directory (${libraryName}) assembled into ${chalk.dim( + path.relative(process.cwd(), prebuildOutputPath), + )}`, + failText: ({ message }) => + `Failed to assemble Android libs directory (${libraryName}): ${message}`, + }, + ); + } }, }; diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index 00e21c72..c93d3d92 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -7,10 +7,10 @@ import { AppleTriplet as Triplet, createAppleFramework, createXCframework, - determineXCFrameworkFilename, } from "react-native-node-api"; import type { Platform } from "./types.js"; +import * as cmakeFileApi from "cmake-file-api"; type XcodeSDKName = | "iphoneos" @@ -135,56 +135,63 @@ export const platform: Platform = { { outputPath, triplets }, { configuration, autoLink, xcframeworkExtension }, ) { - const libraryPaths = await Promise.all( - triplets.map(async ({ outputPath }) => { - const configSpecificPath = path.join(outputPath, configuration); - assert( - fs.existsSync(configSpecificPath), - `Expected a directory at ${configSpecificPath}`, - ); - // Expect binary file(s), either .node or .dylib - const files = await fs.promises.readdir(configSpecificPath); - const result = files.map(async (file) => { - const filePath = path.join(configSpecificPath, file); - if (filePath.endsWith(".dylib")) { - return filePath; - } else if (file.endsWith(".node")) { - // Rename the file to .dylib for xcodebuild to accept it - const newFilePath = filePath.replace(/\.node$/, ".dylib"); - await fs.promises.rename(filePath, newFilePath); - return newFilePath; - } else { - throw new Error( - `Expected a .node or .dylib file, but found ${file}`, - ); - } - }); - assert.equal(result.length, 1, "Expected exactly one library file"); - return await result[0]; - }), - ); - 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, - }), - { - text: "Assembling XCFramework", - successText: `XCFramework assembled into ${chalk.dim( - path.relative(process.cwd(), xcframeworkOutputPath), - )}`, - failText: ({ message }) => `Failed to assemble XCFramework: ${message}`, - }, - ); + const prebuilds: Record = {}; + for (const { buildPath } of triplets) { + assert(fs.existsSync(buildPath), `Expected a directory at ${buildPath}`); + const targets = await cmakeFileApi.readCurrentTargetsDeep( + buildPath, + configuration, + "2.0", + ); + const sharedLibraries = targets.filter( + (target) => target.type === "SHARED_LIBRARY", + ); + assert.equal( + sharedLibraries.length, + 1, + "Expected exactly one shared library", + ); + const [sharedLibrary] = sharedLibraries; + const { artifacts } = sharedLibrary; + assert( + artifacts && artifacts.length === 1, + "Expected exactly one artifact", + ); + const [artifact] = artifacts; + // Add prebuild entry, creating a new entry if needed + if (!(sharedLibrary.name in prebuilds)) { + prebuilds[sharedLibrary.name] = []; + } + prebuilds[sharedLibrary.name].push(path.join(buildPath, artifact.path)); + } + + const extension = xcframeworkExtension ? ".xcframework" : ".apple.node"; + + for (const [libraryName, libraryPaths] of Object.entries(prebuilds)) { + const frameworkPaths = await Promise.all( + libraryPaths.map(createAppleFramework), + ); + // Create the xcframework + const xcframeworkOutputPath = path.resolve( + outputPath, + `${libraryName}${extension}`, + ); + + await oraPromise( + createXCframework({ + outputPath: xcframeworkOutputPath, + frameworkPaths, + autoLink, + }), + { + text: `Assembling XCFramework (${libraryName})`, + successText: `XCFramework (${libraryName}) assembled into ${chalk.dim( + path.relative(process.cwd(), xcframeworkOutputPath), + )}`, + failText: ({ message }) => + `Failed to assemble XCFramework (${libraryName}): ${message}`, + }, + ); + } }, }; diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index d5cbaad7..2bd0bded 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -204,15 +204,13 @@ export const buildCommand = new Command("build") ); if (androidLibraries.length > 0) { - const libraryPathByTriplet = Object.fromEntries( - androidLibraries.map(([target, outputPath]) => [ - ANDROID_TRIPLET_PER_TARGET[target], - outputPath, - ]), - ) as Record; + const libraries = androidLibraries.map(([target, outputPath]) => ({ + triplet: ANDROID_TRIPLET_PER_TARGET[target], + libraryPath: outputPath, + })); const androidLibsFilename = determineAndroidLibsFilename( - Object.values(libraryPathByTriplet), + libraries.map(({ libraryPath }) => libraryPath), ); const androidLibsOutputPath = path.resolve( outputPath, @@ -222,7 +220,7 @@ export const buildCommand = new Command("build") await oraPromise( createAndroidLibsDirectory({ outputPath: androidLibsOutputPath, - libraryPathByTriplet, + libraries, autoLink: true, }), { @@ -238,7 +236,9 @@ export const buildCommand = new Command("build") if (appleLibraries.length > 0) { const libraryPaths = await combineLibraries(appleLibraries); - const frameworkPaths = libraryPaths.map(createAppleFramework); + const frameworkPaths = await Promise.all( + libraryPaths.map(createAppleFramework), + ); const xcframeworkFilename = determineXCFrameworkFilename( frameworkPaths, xcframeworkExtension ? ".xcframework" : ".apple.node", diff --git a/packages/host/src/node/prebuilds/android.ts b/packages/host/src/node/prebuilds/android.ts index b24b422b..ec26408b 100644 --- a/packages/host/src/node/prebuilds/android.ts +++ b/packages/host/src/node/prebuilds/android.ts @@ -32,24 +32,24 @@ export function determineAndroidLibsFilename(libraryPaths: string[]) { type AndroidLibsDirectoryOptions = { outputPath: string; - libraryPathByTriplet: Record; + libraries: { triplet: AndroidTriplet; libraryPath: string }[]; autoLink: boolean; }; export async function createAndroidLibsDirectory({ outputPath, - libraryPathByTriplet, + libraries, autoLink, }: AndroidLibsDirectoryOptions) { // Delete and recreate any existing output directory await fs.promises.rm(outputPath, { recursive: true, force: true }); await fs.promises.mkdir(outputPath, { recursive: true }); - for (const [triplet, libraryPath] of Object.entries(libraryPathByTriplet)) { + for (const { triplet, libraryPath } of libraries) { assert( fs.existsSync(libraryPath), `Library not found: ${libraryPath} for triplet ${triplet}`, ); - const arch = ANDROID_ARCHITECTURES[triplet as AndroidTriplet]; + const arch = ANDROID_ARCHITECTURES[triplet]; const archOutputPath = path.join(outputPath, arch); await fs.promises.mkdir(archOutputPath, { recursive: true }); // Strip the ".node" extension from the library name diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index 693c90b6..f3b1efde 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -2,7 +2,6 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; -import cp from "node:child_process"; import { spawn } from "@react-native-node-api/cli-utils"; @@ -45,7 +44,7 @@ type XCframeworkOptions = { autoLink: boolean; }; -export function createAppleFramework(libraryPath: string) { +export async function createAppleFramework(libraryPath: string) { assert(fs.existsSync(libraryPath), `Library not found: ${libraryPath}`); // Write a info.plist file to the framework const libraryName = path.basename(libraryPath, path.extname(libraryPath)); @@ -54,11 +53,11 @@ export function createAppleFramework(libraryPath: string) { `${libraryName}.framework`, ); // Create the framework from scratch - fs.rmSync(frameworkPath, { recursive: true, force: true }); - fs.mkdirSync(frameworkPath); - fs.mkdirSync(path.join(frameworkPath, "Headers")); + await fs.promises.rm(frameworkPath, { recursive: true, force: true }); + await fs.promises.mkdir(frameworkPath); + await fs.promises.mkdir(path.join(frameworkPath, "Headers")); // Create an empty Info.plist file - fs.writeFileSync( + await fs.promises.writeFile( path.join(frameworkPath, "Info.plist"), createPlistContent({ CFBundleDevelopmentRegion: "en", @@ -75,13 +74,15 @@ export function createAppleFramework(libraryPath: string) { ); const newLibraryPath = path.join(frameworkPath, libraryName); // TODO: Consider copying the library instead of renaming it - fs.renameSync(libraryPath, newLibraryPath); + await fs.promises.rename(libraryPath, newLibraryPath); // Update the name of the library - cp.spawnSync("install_name_tool", [ - "-id", - `@rpath/${libraryName}.framework/${libraryName}`, - newLibraryPath, - ]); + await spawn( + "install_name_tool", + ["-id", `@rpath/${libraryName}.framework/${libraryName}`, newLibraryPath], + { + outputMode: "buffered", + }, + ); return frameworkPath; } From 31838bdaf512da3e50370f506212d86f1af62bec Mon Sep 17 00:00:00 2001 From: Jamie Birch <14055146+shirakaba@users.noreply.github.com> Date: Thu, 16 Oct 2025 22:54:01 +0900 Subject: [PATCH 16/82] Handle Info.plist lookup in versioned frameworks (#261) * handle Info.plist lookup in versioned frameworks * whoops * extract out readInfoPlist() helper * rethrow with context * add tests --- packages/host/src/node/cli/apple.test.ts | 50 ++++++++++++++++ packages/host/src/node/cli/apple.ts | 75 +++++++++++++++++++++--- 2 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 packages/host/src/node/cli/apple.test.ts diff --git a/packages/host/src/node/cli/apple.test.ts b/packages/host/src/node/cli/apple.test.ts new file mode 100644 index 00000000..191baafe --- /dev/null +++ b/packages/host/src/node/cli/apple.test.ts @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import path from "node:path"; +import { readInfoPlist } from "./apple"; +import { setupTempDirectory } from "../test-utils"; + +describe("apple", () => { + describe("Info.plist lookup", () => { + it("should find Info.plist files in unversioned frameworks", async (context) => { + const infoPlistContents = `...`; + const infoPlistSubPath = "Info.plist"; + const tempDirectoryPath = setupTempDirectory(context, { + [infoPlistSubPath]: infoPlistContents, + }); + + const result = await readInfoPlist(tempDirectoryPath); + + assert.strictEqual(result.contents, infoPlistContents); + assert.strictEqual( + result.infoPlistPath, + path.join(tempDirectoryPath, infoPlistSubPath), + ); + }); + + it("should find Info.plist files in versioned frameworks", async (context) => { + const infoPlistContents = `...`; + const infoPlistSubPath = "Versions/Current/Resources/Info.plist"; + const tempDirectoryPath = setupTempDirectory(context, { + [infoPlistSubPath]: infoPlistContents, + }); + + const result = await readInfoPlist(tempDirectoryPath); + + assert.strictEqual(result.contents, infoPlistContents); + assert.strictEqual( + result.infoPlistPath, + path.join(tempDirectoryPath, infoPlistSubPath), + ); + }); + + it("should throw if Info.plist is missing from framework", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, {}); + + await assert.rejects( + async () => readInfoPlist(tempDirectoryPath), + /Unable to read Info.plist for framework at path ".*?", as an Info.plist file couldn't be found./, + ); + }); + }); +}); diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index 92fe6468..20a9065c 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -12,8 +12,67 @@ import { LinkModuleResult, } from "./link-modules.js"; +function determineInfoPlistPath(frameworkPath: string) { + const checkedPaths = new Array(); + + // First, assume it is an "unversioned" framework that keeps its Info.plist in + // the root. This is the convention for iOS, tvOS, and friends. + let infoPlistPath = path.join(frameworkPath, "Info.plist"); + + if (fs.existsSync(infoPlistPath)) { + return infoPlistPath; + } + checkedPaths.push(infoPlistPath); + + // Next, assume it is a "versioned" framework that keeps its Info.plist + // under a subdirectory. This is the convention for macOS. + infoPlistPath = path.join( + frameworkPath, + "Versions/Current/Resources/Info.plist", + ); + + if (fs.existsSync(infoPlistPath)) { + return infoPlistPath; + } + checkedPaths.push(infoPlistPath); + + throw new Error( + [ + `Unable to locate an Info.plist file within framework. Checked the following paths:`, + ...checkedPaths.map((checkedPath) => `- ${checkedPath}`), + ].join("\n"), + ); +} + +/** + * Resolves the Info.plist file within a framework and reads its contents. + */ +export async function readInfoPlist(frameworkPath: string) { + let infoPlistPath: string; + try { + infoPlistPath = determineInfoPlistPath(frameworkPath); + } catch (cause) { + throw new Error( + `Unable to read Info.plist for framework at path "${frameworkPath}", as an Info.plist file couldn't be found.`, + { cause }, + ); + } + + let contents: string; + try { + contents = await fs.promises.readFile(infoPlistPath, "utf-8"); + } catch (cause) { + throw new Error( + `Unable to read Info.plist for framework at path "${frameworkPath}", due to a file system error.`, + { cause }, + ); + } + + return { infoPlistPath, contents }; +} + type UpdateInfoPlistOptions = { - filePath: string; + frameworkPath: string; oldLibraryName: string; newLibraryName: string; }; @@ -22,17 +81,15 @@ type UpdateInfoPlistOptions = { * Update the Info.plist file of an xcframework to use the new library name. */ export async function updateInfoPlist({ - filePath, + frameworkPath, oldLibraryName, newLibraryName, }: UpdateInfoPlistOptions) { - const infoPlistContents = await fs.promises.readFile(filePath, "utf-8"); + const { infoPlistPath, contents } = await readInfoPlist(frameworkPath); + // TODO: Use a proper plist parser - const updatedContents = infoPlistContents.replaceAll( - oldLibraryName, - newLibraryName, - ); - await fs.promises.writeFile(filePath, updatedContents, "utf-8"); + const updatedContents = contents.replaceAll(oldLibraryName, newLibraryName); + await fs.promises.writeFile(infoPlistPath, updatedContents, "utf-8"); } export async function linkXcframework({ @@ -126,7 +183,7 @@ export async function linkXcframework({ ); // Update the Info.plist file for the framework await updateInfoPlist({ - filePath: path.join(newFrameworkPath, "Info.plist"), + frameworkPath: newFrameworkPath, oldLibraryName, newLibraryName, }); From 2b9a538d217b09f4e2dd41dfdec171cb0a2ce50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 17 Oct 2025 19:51:26 +0200 Subject: [PATCH 17/82] Add changeset for #261 --- .changeset/old-worlds-stay.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/old-worlds-stay.md diff --git a/.changeset/old-worlds-stay.md b/.changeset/old-worlds-stay.md new file mode 100644 index 00000000..d99af932 --- /dev/null +++ b/.changeset/old-worlds-stay.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Handle Info.plist lookup in versioned frameworks From 5569a6cede158956362edf1835c193aa756b1fe2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 20:00:55 +0200 Subject: [PATCH 18/82] Version Packages (#259) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/evil-vans-love.md | 5 ----- .changeset/old-worlds-stay.md | 5 ----- packages/cmake-rn/CHANGELOG.md | 8 ++++++++ packages/cmake-rn/package.json | 4 ++-- packages/ferric/CHANGELOG.md | 7 +++++++ packages/ferric/package.json | 4 ++-- packages/host/CHANGELOG.md | 6 ++++++ packages/host/package.json | 2 +- packages/node-tests/package.json | 2 +- 9 files changed, 27 insertions(+), 16 deletions(-) delete mode 100644 .changeset/evil-vans-love.md delete mode 100644 .changeset/old-worlds-stay.md diff --git a/.changeset/evil-vans-love.md b/.changeset/evil-vans-love.md deleted file mode 100644 index 873d1d86..00000000 --- a/.changeset/evil-vans-love.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"cmake-rn": patch ---- - -Use CMake file API to read shared library target paths diff --git a/.changeset/old-worlds-stay.md b/.changeset/old-worlds-stay.md deleted file mode 100644 index d99af932..00000000 --- a/.changeset/old-worlds-stay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Handle Info.plist lookup in versioned frameworks diff --git a/packages/cmake-rn/CHANGELOG.md b/packages/cmake-rn/CHANGELOG.md index e5f7e17a..b88e0e6a 100644 --- a/packages/cmake-rn/CHANGELOG.md +++ b/packages/cmake-rn/CHANGELOG.md @@ -1,5 +1,13 @@ # cmake-rn +## 0.4.1 + +### Patch Changes + +- a23af5a: Use CMake file API to read shared library target paths +- Updated dependencies [2b9a538] + - react-native-node-api@0.5.2 + ## 0.4.0 ### Minor Changes diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index 91fa1bde..31ddc702 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -1,6 +1,6 @@ { "name": "cmake-rn", - "version": "0.4.0", + "version": "0.4.1", "description": "Build React Native Node API modules with CMake", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -24,7 +24,7 @@ "dependencies": { "@react-native-node-api/cli-utils": "0.1.0", "cmake-file-api": "0.1.0", - "react-native-node-api": "0.5.1" + "react-native-node-api": "0.5.2" }, "peerDependencies": { "node-addon-api": "^8.3.1", diff --git a/packages/ferric/CHANGELOG.md b/packages/ferric/CHANGELOG.md index 8e3ba40b..21bbc965 100644 --- a/packages/ferric/CHANGELOG.md +++ b/packages/ferric/CHANGELOG.md @@ -1,5 +1,12 @@ # ferric-cli +## 0.3.4 + +### Patch Changes + +- Updated dependencies [2b9a538] + - react-native-node-api@0.5.2 + ## 0.3.3 ### Patch Changes diff --git a/packages/ferric/package.json b/packages/ferric/package.json index f114d4e3..1e08b5a3 100644 --- a/packages/ferric/package.json +++ b/packages/ferric/package.json @@ -1,6 +1,6 @@ { "name": "ferric-cli", - "version": "0.3.3", + "version": "0.3.4", "description": "Rust Node-API Modules for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -18,6 +18,6 @@ "dependencies": { "@napi-rs/cli": "~3.0.3", "@react-native-node-api/cli-utils": "0.1.0", - "react-native-node-api": "0.5.1" + "react-native-node-api": "0.5.2" } } diff --git a/packages/host/CHANGELOG.md b/packages/host/CHANGELOG.md index 2b9a1b1c..f721628d 100644 --- a/packages/host/CHANGELOG.md +++ b/packages/host/CHANGELOG.md @@ -1,5 +1,11 @@ # react-native-node-api +## 0.5.2 + +### Patch Changes + +- 2b9a538: Handle Info.plist lookup in versioned frameworks + ## 0.5.1 ### Patch Changes diff --git a/packages/host/package.json b/packages/host/package.json index 6a547aff..9ba3b46d 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -1,6 +1,6 @@ { "name": "react-native-node-api", - "version": "0.5.1", + "version": "0.5.2", "description": "Node-API for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { diff --git a/packages/node-tests/package.json b/packages/node-tests/package.json index cf77504f..cfdad1a7 100644 --- a/packages/node-tests/package.json +++ b/packages/node-tests/package.json @@ -22,7 +22,7 @@ "cmake-rn": "*", "gyp-to-cmake": "*", "prebuildify": "^6.0.1", - "react-native-node-api": "^0.5.1", + "react-native-node-api": "^0.5.2", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } From b91415bcfe920031fcd379ed59e8982f3ffa245c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 17 Oct 2025 22:53:42 +0200 Subject: [PATCH 19/82] Removed the docs from cmake-file-api archive --- packages/cmake-file-api/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cmake-file-api/package.json b/packages/cmake-file-api/package.json index b641dffc..656939c6 100644 --- a/packages/cmake-file-api/package.json +++ b/packages/cmake-file-api/package.json @@ -10,7 +10,6 @@ "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" }, "files": [ - "docs/", "dist/", "!*.test.d.ts", "!*.test.d.ts.map" From d8e90a88d83b877b9161d5941ad9a96ed79d2f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 18 Oct 2025 01:21:51 +0200 Subject: [PATCH 20/82] Filter CMake targets by target name when passed (#264) --- .changeset/cold-showers-arrive.md | 5 +++++ packages/cmake-rn/src/platforms/android.ts | 9 +++++++-- packages/cmake-rn/src/platforms/apple.ts | 6 ++++-- 3 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 .changeset/cold-showers-arrive.md diff --git a/.changeset/cold-showers-arrive.md b/.changeset/cold-showers-arrive.md new file mode 100644 index 00000000..de063bcb --- /dev/null +++ b/.changeset/cold-showers-arrive.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": patch +--- + +Filter CMake targets by target name when passed diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index 655c1d7b..dbf9d887 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -121,7 +121,10 @@ export const platform: Platform = { const { ANDROID_HOME } = process.env; return typeof ANDROID_HOME === "string" && fs.existsSync(ANDROID_HOME); }, - async postBuild({ outputPath, triplets }, { autoLink, configuration }) { + async postBuild( + { outputPath, triplets }, + { autoLink, configuration, target }, + ) { const prebuilds: Record< string, { triplet: Triplet; libraryPath: string }[] @@ -135,7 +138,9 @@ export const platform: Platform = { "2.0", ); const sharedLibraries = targets.filter( - (target) => target.type === "SHARED_LIBRARY", + ({ type, name }) => + type === "SHARED_LIBRARY" && + (target.length === 0 || target.includes(name)), ); assert.equal( sharedLibraries.length, diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index c93d3d92..849ccfe5 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -133,7 +133,7 @@ export const platform: Platform = { }, async postBuild( { outputPath, triplets }, - { configuration, autoLink, xcframeworkExtension }, + { configuration, autoLink, xcframeworkExtension, target }, ) { const prebuilds: Record = {}; for (const { buildPath } of triplets) { @@ -144,7 +144,9 @@ export const platform: Platform = { "2.0", ); const sharedLibraries = targets.filter( - (target) => target.type === "SHARED_LIBRARY", + ({ type, name }) => + type === "SHARED_LIBRARY" && + (target.length === 0 || target.includes(name)), ); assert.equal( sharedLibraries.length, From 9f1a30112ed0d12a32985f288fb3485fa55e4b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 18 Oct 2025 09:49:53 +0200 Subject: [PATCH 21/82] Fix requireNodeAddon return type (#267) --- .changeset/bumpy-things-poke.md | 5 +++++ .../host/src/react-native/NativeNodeApiHost.ts | 8 -------- packages/host/src/react-native/index.ts | 15 +++++++++++++-- 3 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 .changeset/bumpy-things-poke.md delete mode 100644 packages/host/src/react-native/NativeNodeApiHost.ts diff --git a/.changeset/bumpy-things-poke.md b/.changeset/bumpy-things-poke.md new file mode 100644 index 00000000..dd1de884 --- /dev/null +++ b/.changeset/bumpy-things-poke.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Fix requireNodeAddon return type diff --git a/packages/host/src/react-native/NativeNodeApiHost.ts b/packages/host/src/react-native/NativeNodeApiHost.ts deleted file mode 100644 index fdf04dc8..00000000 --- a/packages/host/src/react-native/NativeNodeApiHost.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { TurboModule } from "react-native"; -import { TurboModuleRegistry } from "react-native"; - -export interface Spec extends TurboModule { - requireNodeAddon(libraryName: string): void; -} - -export default TurboModuleRegistry.getEnforcing("NodeApiHost"); diff --git a/packages/host/src/react-native/index.ts b/packages/host/src/react-native/index.ts index c34ae8b7..0a077c75 100644 --- a/packages/host/src/react-native/index.ts +++ b/packages/host/src/react-native/index.ts @@ -1,3 +1,14 @@ -import native from "./NativeNodeApiHost"; +import { type TurboModule, TurboModuleRegistry } from "react-native"; -export const requireNodeAddon = native.requireNodeAddon.bind(native); +export interface Spec extends TurboModule { + requireNodeAddon(libraryName: string): T; +} + +const native = TurboModuleRegistry.getEnforcing("NodeApiHost"); + +/** + * Loads a native Node-API addon by filename. + */ +export function requireNodeAddon(libraryName: string): T { + return native.requireNodeAddon(libraryName); +} From 04196788819c48840512ab15919e2cac8a509d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 18 Oct 2025 09:50:04 +0200 Subject: [PATCH 22/82] Omit test.d.ts files from packages (#268) --- packages/cmake-rn/package.json | 4 +++- packages/host/package.json | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index 31ddc702..cd81d9d5 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -14,7 +14,9 @@ }, "files": [ "bin", - "dist" + "dist", + "!dist/**/*.test.d.ts", + "!dist/**/*.test.d.ts.map" ], "scripts": { "build": "tsc", diff --git a/packages/host/package.json b/packages/host/package.json index 9ba3b46d..8031a554 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -26,6 +26,8 @@ "logo.svg", "bin", "dist", + "!dist/**/*.test.d.ts", + "!dist/**/*.test.d.ts.map", "cpp", "android", "!android/.cxx", From b1d0dde32c5d8b3a85a67ef41b7f083c355762b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 21 Oct 2025 07:13:15 +0200 Subject: [PATCH 23/82] Adding a git clean to clean script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 699dbe3f..d0189f61 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "homepage": "https://github.com/callstackincubator/react-native-node-api#readme", "scripts": { "build": "tsc --build", - "clean": "tsc --build --clean", + "clean": "tsc --build --clean && git clean -fdx -e node_modules", "dev": "tsc --build --watch", "lint": "eslint .", "prettier:check": "prettier --experimental-cli --check .", From 9640d63d4c6b1f3e0b9790ba5cb8ba90f92ff544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 21 Oct 2025 11:21:39 +0200 Subject: [PATCH 24/82] Fix `node-tests` package to propagate errors (#276) * Fix node-tests asserts being swallowed * Add script to run mocha-remote and metro alone --- apps/test-app/package.json | 1 + packages/node-tests/rolldown.config.mts | 12 ++++++++++++ packages/node-tests/scripts/generate-entrypoint.mts | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/test-app/package.json b/apps/test-app/package.json index 10c33840..08f55a35 100644 --- a/apps/test-app/package.json +++ b/apps/test-app/package.json @@ -7,6 +7,7 @@ "android": "react-native run-android --no-packager --active-arch-only", "ios": "react-native run-ios --no-packager", "pod-install": "cd ios && pod install", + "mocha-and-metro": "mocha-remote --exit-on-error -- node --run metro", "test:android": "mocha-remote --exit-on-error -- concurrently --kill-others-on-fail --passthrough-arguments npm:metro 'npm:android -- {@}' --", "test:android:allTests": "MOCHA_REMOTE_CONTEXT=allTests node --run test:android -- ", "test:android:nodeAddonExamples": "MOCHA_REMOTE_CONTEXT=nodeAddonExamples node --run test:android -- ", diff --git a/packages/node-tests/rolldown.config.mts b/packages/node-tests/rolldown.config.mts index c1b7f327..884b18c7 100644 --- a/packages/node-tests/rolldown.config.mts +++ b/packages/node-tests/rolldown.config.mts @@ -64,6 +64,18 @@ function testSuiteConfig(suitePath: string): RolldownOptions[] { delimiters: ["", ""], }, ), + replacePlugin( + { + // Replace the default export to return a function instead of initializing the addon immediately + // This allows the test runner to intercept any errors which would normally be thrown when importing + // to work around Metro's `guardedLoadModule` swallowing errors during module initialization + // See https://github.com/facebook/metro/blob/34bb8913ec4b5b02690b39d2246599faf094f721/packages/metro-runtime/src/polyfills/require.js#L348-L353 + "export default require_test();": "export default require_test;", + }, + { + delimiters: ["", ""], + }, + ), aliasPlugin({ entries: [ { diff --git a/packages/node-tests/scripts/generate-entrypoint.mts b/packages/node-tests/scripts/generate-entrypoint.mts index a0cf8c8d..49e6478a 100644 --- a/packages/node-tests/scripts/generate-entrypoint.mts +++ b/packages/node-tests/scripts/generate-entrypoint.mts @@ -36,7 +36,7 @@ function suiteToString(suite: TestSuite, indent = 1): string { return Object.entries(suite) .map(([key, value]) => { if (typeof value === "string") { - return `${padding}"${key}": () => require("./${value}")`; + return `${padding}"${key}": require("./${value}")`; } else { return `${padding}"${key}": {\n${suiteToString( value, From 0c3e8ba5336cbe785d6e29fcdeb95e2e0fece719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 21 Oct 2025 12:30:49 +0200 Subject: [PATCH 25/82] Fix expansion of options in `--build` and `--out` (#278) * Fix expansion of options in --build and --out * Attempt at correcting paths on windows --- .changeset/fresh-frogs-enter.md | 5 +++++ packages/cmake-rn/src/cli.ts | 35 +++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 .changeset/fresh-frogs-enter.md diff --git a/.changeset/fresh-frogs-enter.md b/.changeset/fresh-frogs-enter.md new file mode 100644 index 00000000..76c39869 --- /dev/null +++ b/.changeset/fresh-frogs-enter.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": patch +--- + +Fix expansion of options in --build and --out diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index a7d56530..10bf9f63 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -74,7 +74,7 @@ const tripletOption = new Option( const buildPathOption = new Option( "--build ", "Specify the build directory to store the configured CMake project", -); +).default("{source}/build"); const cleanOption = new Option( "--clean", @@ -84,7 +84,7 @@ const cleanOption = new Option( const outPathOption = new Option( "--out ", "Specify the output directory to store the final build artifacts", -).default(false, "./{build}/{configuration}"); +).default("{build}/{configuration}"); const defineOption = new Option( "-D,--define ", @@ -151,8 +151,27 @@ for (const platform of platforms) { program = platform.amendCommand(program); } +function expandTemplate( + input: string, + values: Record, +): string { + return input.replaceAll(/{([^}]+)}/g, (_, key: string) => + typeof values[key] === "string" ? values[key] : "", + ); +} + program = program.action( wrapAction(async ({ triplet: requestedTriplets, ...baseOptions }) => { + baseOptions.build = path.resolve( + process.cwd(), + expandTemplate(baseOptions.build, baseOptions), + ); + baseOptions.out = path.resolve( + process.cwd(), + expandTemplate(baseOptions.out, baseOptions), + ); + const { out, build: buildPath } = baseOptions; + assertFixable( fs.existsSync(path.join(baseOptions.source, "CMakeLists.txt")), `No CMakeLists.txt found in source directory: ${chalk.dim(baseOptions.source)}`, @@ -161,7 +180,6 @@ program = program.action( }, ); - const buildPath = getBuildPath(baseOptions); if (baseOptions.clean) { await fs.promises.rm(buildPath, { recursive: true, force: true }); } @@ -197,10 +215,6 @@ program = program.action( } } - if (!baseOptions.out) { - baseOptions.out = path.join(buildPath, baseOptions.configuration); - } - const tripletContexts = [...triplets].map((triplet) => { const platform = findPlatformForTriplet(triplet); const tripletBuildPath = getTripletBuildPath(buildPath, triplet); @@ -262,7 +276,7 @@ program = program.action( } await platform.postBuild( { - outputPath: baseOptions.out || baseOptions.source, + outputPath: out, triplets: relevantTriplets, }, baseOptions, @@ -288,11 +302,6 @@ function getTripletsSummary( .join(" / "); } -function getBuildPath({ build, source }: BaseOpts) { - // TODO: Add configuration (debug vs release) - return path.resolve(process.cwd(), build || path.join(source, "build")); -} - /** * Namespaces the output path with a triplet name */ From 6996e734b514ddd6506b319c1d9986821b812847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 21 Oct 2025 12:32:06 +0200 Subject: [PATCH 26/82] Move `prettyPath` to `cli-utils` package (#279) * Move prettyPath to cli-utils * Use prettyPath more * Removed unused Option --- packages/cli-utils/src/index.ts | 1 + packages/cli-utils/src/paths.ts | 8 ++++++++ packages/cmake-rn/src/platforms/android.ts | 10 ++++++---- packages/cmake-rn/src/platforms/apple.ts | 10 ++++++---- packages/ferric/src/build.ts | 6 ++---- packages/gyp-to-cmake/src/cli.ts | 16 +++++++++++----- packages/host/src/node/cli/hermes.ts | 3 +-- packages/host/src/node/cli/link-modules.ts | 7 +++++-- packages/host/src/node/cli/program.ts | 2 +- packages/host/src/node/index.ts | 2 +- packages/host/src/node/path-utils.ts | 8 +------- 11 files changed, 43 insertions(+), 30 deletions(-) create mode 100644 packages/cli-utils/src/paths.ts diff --git a/packages/cli-utils/src/index.ts b/packages/cli-utils/src/index.ts index bf9232fa..0fa6ff4a 100644 --- a/packages/cli-utils/src/index.ts +++ b/packages/cli-utils/src/index.ts @@ -5,3 +5,4 @@ export * from "bufout"; export * from "./actions.js"; export * from "./errors.js"; +export * from "./paths.js"; diff --git a/packages/cli-utils/src/paths.ts b/packages/cli-utils/src/paths.ts new file mode 100644 index 00000000..6ff8bd2e --- /dev/null +++ b/packages/cli-utils/src/paths.ts @@ -0,0 +1,8 @@ +import chalk from "chalk"; +import path from "node:path"; + +export function prettyPath(p: string) { + return chalk.dim( + path.relative(process.cwd(), p) || chalk.italic("current directory"), + ); +} diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index dbf9d887..b7b758c3 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -2,7 +2,11 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -import { Option, oraPromise, chalk } from "@react-native-node-api/cli-utils"; +import { + Option, + oraPromise, + prettyPath, +} from "@react-native-node-api/cli-utils"; import { createAndroidLibsDirectory, AndroidTriplet as Triplet, @@ -177,9 +181,7 @@ export const platform: Platform = { }), { text: `Assembling Android libs directory (${libraryName})`, - successText: `Android libs directory (${libraryName}) assembled into ${chalk.dim( - path.relative(process.cwd(), prebuildOutputPath), - )}`, + successText: `Android libs directory (${libraryName}) assembled into ${prettyPath(prebuildOutputPath)}`, failText: ({ message }) => `Failed to assemble Android libs directory (${libraryName}): ${message}`, }, diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index 849ccfe5..91f5e7fd 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -2,7 +2,11 @@ import assert from "node:assert/strict"; import path from "node:path"; import fs from "node:fs"; -import { Option, oraPromise, chalk } from "@react-native-node-api/cli-utils"; +import { + Option, + oraPromise, + prettyPath, +} from "@react-native-node-api/cli-utils"; import { AppleTriplet as Triplet, createAppleFramework, @@ -187,9 +191,7 @@ export const platform: Platform = { }), { text: `Assembling XCFramework (${libraryName})`, - successText: `XCFramework (${libraryName}) assembled into ${chalk.dim( - path.relative(process.cwd(), xcframeworkOutputPath), - )}`, + successText: `XCFramework (${libraryName}) assembled into ${prettyPath(xcframeworkOutputPath)}`, failText: ({ message }) => `Failed to assemble XCFramework (${libraryName}): ${message}`, }, diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index 2bd0bded..fdf961e3 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -8,6 +8,7 @@ import { oraPromise, assertFixable, wrapAction, + prettyPath, } from "@react-native-node-api/cli-utils"; import { @@ -19,7 +20,6 @@ import { createXCframework, createUniversalAppleLibrary, determineLibraryBasename, - prettyPath, } from "react-native-node-api"; import { ensureCargo, build } from "./cargo.js"; @@ -258,9 +258,7 @@ export const buildCommand = new Command("build") }), { text: "Assembling XCFramework", - successText: `XCFramework assembled into ${chalk.dim( - path.relative(process.cwd(), xcframeworkOutputPath), - )}`, + successText: `XCFramework assembled into ${prettyPath(xcframeworkOutputPath)}`, failText: ({ message }) => `Failed to assemble XCFramework: ${message}`, }, diff --git a/packages/gyp-to-cmake/src/cli.ts b/packages/gyp-to-cmake/src/cli.ts index 52d14984..1045ba45 100644 --- a/packages/gyp-to-cmake/src/cli.ts +++ b/packages/gyp-to-cmake/src/cli.ts @@ -1,7 +1,10 @@ import fs from "node:fs"; import path from "node:path"; - -import { Command, wrapAction } from "@react-native-node-api/cli-utils"; +import { + Command, + prettyPath, + wrapAction, +} from "@react-native-node-api/cli-utils"; import { readBindingFile } from "./gyp.js"; import { @@ -29,15 +32,18 @@ export function transformBindingGypFile( ...restOfOptions }: TransformOptions, ) { - console.log("Transforming", gypPath); - const gyp = readBindingFile(gypPath, disallowUnknownProperties); const parentPath = path.dirname(gypPath); + const cmakeListsPath = path.join(parentPath, "CMakeLists.txt"); + console.log( + `Transforming ${prettyPath(gypPath)} → ${prettyPath(cmakeListsPath)}`, + ); + + const gyp = readBindingFile(gypPath, disallowUnknownProperties); const result = bindingGypToCmakeLists({ gyp, projectName, ...restOfOptions, }); - const cmakeListsPath = path.join(parentPath, "CMakeLists.txt"); fs.writeFileSync(cmakeListsPath, result, "utf-8"); } diff --git a/packages/host/src/node/cli/hermes.ts b/packages/host/src/node/cli/hermes.ts index 817b7524..541a324e 100644 --- a/packages/host/src/node/cli/hermes.ts +++ b/packages/host/src/node/cli/hermes.ts @@ -9,11 +9,10 @@ import { spawn, UsageError, wrapAction, + prettyPath, } from "@react-native-node-api/cli-utils"; import { packageDirectorySync } from "pkg-dir"; -import { prettyPath } from "../path-utils"; - const HOST_PACKAGE_ROOT = path.resolve(__dirname, "../../.."); // FIXME: make this configurable with reasonable fallback before public release const HERMES_GIT_URL = "https://github.com/kraenhansen/hermes.git"; diff --git a/packages/host/src/node/cli/link-modules.ts b/packages/host/src/node/cli/link-modules.ts index 9b44d921..8b3bf2e0 100644 --- a/packages/host/src/node/cli/link-modules.ts +++ b/packages/host/src/node/cli/link-modules.ts @@ -1,7 +1,11 @@ import path from "node:path"; import fs from "node:fs"; -import { chalk, SpawnFailure } from "@react-native-node-api/cli-utils"; +import { + chalk, + SpawnFailure, + prettyPath, +} from "@react-native-node-api/cli-utils"; import { findNodeApiModulePathsByDependency, @@ -10,7 +14,6 @@ import { logModulePaths, NamingStrategy, PlatformName, - prettyPath, } from "../path-utils"; export type ModuleLinker = ( diff --git a/packages/host/src/node/cli/program.ts b/packages/host/src/node/cli/program.ts index 37950d6d..cba9994a 100644 --- a/packages/host/src/node/cli/program.ts +++ b/packages/host/src/node/cli/program.ts @@ -8,6 +8,7 @@ import { SpawnFailure, oraPromise, wrapAction, + prettyPath, } from "@react-native-node-api/cli-utils"; import { @@ -19,7 +20,6 @@ import { normalizeModulePath, PlatformName, PLATFORMS, - prettyPath, } from "../path-utils"; import { command as vendorHermes } from "./hermes"; diff --git a/packages/host/src/node/index.ts b/packages/host/src/node/index.ts index 878eeb01..dd914402 100644 --- a/packages/host/src/node/index.ts +++ b/packages/host/src/node/index.ts @@ -22,6 +22,6 @@ export { determineXCFrameworkFilename, } from "./prebuilds/apple.js"; -export { determineLibraryBasename, prettyPath } from "./path-utils.js"; +export { determineLibraryBasename } from "./path-utils.js"; export { weakNodeApiPath } from "./weak-node-api.js"; diff --git a/packages/host/src/node/path-utils.ts b/packages/host/src/node/path-utils.ts index 1fe6170e..9ac1da93 100644 --- a/packages/host/src/node/path-utils.ts +++ b/packages/host/src/node/path-utils.ts @@ -5,7 +5,7 @@ import { packageDirectorySync } from "pkg-dir"; import { readPackageSync } from "read-pkg"; import { createRequire } from "node:module"; -import { chalk } from "@react-native-node-api/cli-utils"; +import { chalk, prettyPath } from "@react-native-node-api/cli-utils"; import { findDuplicates } from "./duplicates"; @@ -194,12 +194,6 @@ export function getLibraryName(modulePath: string, naming: NamingStrategy) { )}`; } -export function prettyPath(p: string) { - return chalk.dim( - path.relative(process.cwd(), p) || chalk.italic("current directory"), - ); -} - export function resolvePackageRoot( requireFromPackageRoot: NodeJS.Require, packageName: string, From 5016ed22dfeed5d9a12b57f80653dcb20bbaa077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 21 Oct 2025 13:10:59 +0200 Subject: [PATCH 27/82] Add package name stripping the "scope" part from library names (#277) * Rename PathSuffixChoice to LibraryNamingChoice and add packageName option defaulting to strip * Add changeset * Add fixes from code review --- .changeset/orange-bananas-obey.md | 5 + apps/test-app/babel.config.js | 2 +- packages/host/src/node/babel-plugin/plugin.ts | 30 ++++-- packages/host/src/node/cli/options.ts | 21 ++++- packages/host/src/node/cli/program.ts | 26 +++-- packages/host/src/node/path-utils.test.ts | 48 ++++++++++ packages/host/src/node/path-utils.ts | 94 +++++++++++++++---- 7 files changed, 188 insertions(+), 38 deletions(-) create mode 100644 .changeset/orange-bananas-obey.md diff --git a/.changeset/orange-bananas-obey.md b/.changeset/orange-bananas-obey.md new file mode 100644 index 00000000..68c5aa45 --- /dev/null +++ b/.changeset/orange-bananas-obey.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": minor +--- + +Scope is now stripped from package names when renaming libraries while linking diff --git a/apps/test-app/babel.config.js b/apps/test-app/babel.config.js index 8ea846e2..1eecb8fa 100644 --- a/apps/test-app/babel.config.js +++ b/apps/test-app/babel.config.js @@ -1,5 +1,5 @@ module.exports = { presets: ["module:@react-native/babel-preset"], - // plugins: [['module:react-native-node-api/babel-plugin', { stripPathSuffix: true }]], + // plugins: [['module:react-native-node-api/babel-plugin', { packageName: "strip", pathSuffix: "strip" }]], plugins: ["module:react-native-node-api/babel-plugin"], }; diff --git a/packages/host/src/node/babel-plugin/plugin.ts b/packages/host/src/node/babel-plugin/plugin.ts index 6b9937cb..45e269e9 100644 --- a/packages/host/src/node/babel-plugin/plugin.ts +++ b/packages/host/src/node/babel-plugin/plugin.ts @@ -9,11 +9,22 @@ import { isNodeApiModule, findNodeAddonForBindings, NamingStrategy, - PathSuffixChoice, - assertPathSuffix, + LibraryNamingChoice, + assertLibraryNamingChoice, } from "../path-utils"; export type PluginOptions = { + /** + * Controls how the package name is transformed into a library name. + * The transformation is needed to disambiguate and avoid conflicts between addons with the same name (but in different sub-paths or packages). + * + * As an example, if the package name is `@my-org/my-pkg` and the path of the addon within the package is `build/Release/my-addon.node` (and `pathSuffix` is set to `"strip"`): + * - `"omit"`: Only the path within the package is used and the library name will be `my-addon`. + * - `"strip"`: Scope / org gets stripped and the library name will be `my-pkg--my-addon`. + * - `"keep"`: The org and name is kept and the library name will be `my-org--my-pkg--my-addon`. + */ + packageName?: LibraryNamingChoice; + /** * Controls how the path of the addon inside a package is transformed into a library name. * The transformation is needed to disambiguate and avoid conflicts between addons with the same name (but in different sub-paths or packages). @@ -23,13 +34,16 @@ export type PluginOptions = { * - `"strip"` (default): Path gets stripped to its basename and the library name will be `my-pkg--my-addon`. * - `"keep"`: The full path is kept and the library name will be `my-pkg--build-Release-my-addon`. */ - pathSuffix?: PathSuffixChoice; + pathSuffix?: LibraryNamingChoice; }; function assertOptions(opts: unknown): asserts opts is PluginOptions { assert(typeof opts === "object" && opts !== null, "Expected an object"); if ("pathSuffix" in opts) { - assertPathSuffix(opts.pathSuffix); + assertLibraryNamingChoice(opts.pathSuffix); + } + if ("packageName" in opts) { + assertLibraryNamingChoice(opts.packageName); } } @@ -57,7 +71,7 @@ export function plugin(): PluginObj { visitor: { CallExpression(p) { assertOptions(this.opts); - const { pathSuffix = "strip" } = this.opts; + const { pathSuffix = "strip", packageName = "strip" } = this.opts; if (typeof this.filename !== "string") { // This transformation only works when the filename is known return; @@ -80,6 +94,7 @@ export function plugin(): PluginObj { const resolvedPath = findNodeAddonForBindings(id, from); if (typeof resolvedPath === "string") { replaceWithRequireNodeAddon(p.parentPath, resolvedPath, { + packageName, pathSuffix, }); } @@ -89,7 +104,10 @@ export function plugin(): PluginObj { isNodeApiModule(path.join(from, id)) ) { const relativePath = path.join(from, id); - replaceWithRequireNodeAddon(p, relativePath, { pathSuffix }); + replaceWithRequireNodeAddon(p, relativePath, { + packageName, + pathSuffix, + }); } } }, diff --git a/packages/host/src/node/cli/options.ts b/packages/host/src/node/cli/options.ts index 0944f7c9..eb059b42 100644 --- a/packages/host/src/node/cli/options.ts +++ b/packages/host/src/node/cli/options.ts @@ -1,15 +1,28 @@ import { Option } from "@react-native-node-api/cli-utils"; -import { assertPathSuffix, PATH_SUFFIX_CHOICES } from "../path-utils"; +import { + assertLibraryNamingChoice, + LIBRARY_NAMING_CHOICES, +} from "../path-utils"; -const { NODE_API_PATH_SUFFIX } = process.env; +const { NODE_API_PACKAGE_NAME, NODE_API_PATH_SUFFIX } = process.env; +if (typeof NODE_API_PACKAGE_NAME === "string") { + assertLibraryNamingChoice(NODE_API_PACKAGE_NAME); +} if (typeof NODE_API_PATH_SUFFIX === "string") { - assertPathSuffix(NODE_API_PATH_SUFFIX); + assertLibraryNamingChoice(NODE_API_PATH_SUFFIX); } +export const packageNameOption = new Option( + "--package-name ", + "Controls how the package name is transformed into a library name", +) + .choices(LIBRARY_NAMING_CHOICES) + .default(NODE_API_PACKAGE_NAME || "strip"); + export const pathSuffixOption = new Option( "--path-suffix ", "Controls how the path of the addon inside a package is transformed into a library name", ) - .choices(PATH_SUFFIX_CHOICES) + .choices(LIBRARY_NAMING_CHOICES) .default(NODE_API_PATH_SUFFIX || "strip"); diff --git a/packages/host/src/node/cli/program.ts b/packages/host/src/node/cli/program.ts index cba9994a..6fd037b3 100644 --- a/packages/host/src/node/cli/program.ts +++ b/packages/host/src/node/cli/program.ts @@ -23,7 +23,7 @@ import { } from "../path-utils"; import { command as vendorHermes } from "./hermes"; -import { pathSuffixOption } from "./options"; +import { packageNameOption, pathSuffixOption } from "./options"; import { linkModules, pruneLinkedModules, ModuleLinker } from "./link-modules"; import { linkXcframework } from "./apple"; import { linkAndroidDir } from "./android"; @@ -70,10 +70,14 @@ program ) .option("--android", "Link Android modules") .option("--apple", "Link Apple modules") + .addOption(packageNameOption) .addOption(pathSuffixOption) .action( wrapAction( - async (pathArg, { force, prune, pathSuffix, android, apple }) => { + async ( + pathArg, + { force, prune, pathSuffix, android, apple, packageName }, + ) => { console.log("Auto-linking Node-API modules from", chalk.dim(pathArg)); const platforms: PlatformName[] = []; if (android) { @@ -101,7 +105,7 @@ program platform, fromPath: path.resolve(pathArg), incremental: !force, - naming: { pathSuffix }, + naming: { packageName, pathSuffix }, linker: getLinker(platform), }), { @@ -173,9 +177,10 @@ program .description("Lists Node-API modules") .argument("[from-path]", "Some path inside the app package", process.cwd()) .option("--json", "Output as JSON", false) + .addOption(packageNameOption) .addOption(pathSuffixOption) .action( - wrapAction(async (fromArg, { json, pathSuffix }) => { + wrapAction(async (fromArg, { json, pathSuffix, packageName }) => { const rootPath = path.resolve(fromArg); const dependencies = await findNodeApiModulePathsByDependency({ fromPath: rootPath, @@ -210,7 +215,7 @@ program ); logModulePaths( dependency.modulePaths.map((p) => path.join(dependency.path, p)), - { pathSuffix }, + { packageName, pathSuffix }, ); } } @@ -222,21 +227,22 @@ program .description( "Utility to print, module path, the hash of a single Android library", ) + .addOption(packageNameOption) .addOption(pathSuffixOption) .action( - wrapAction((pathInput, { pathSuffix }) => { + wrapAction((pathInput, { pathSuffix, packageName }) => { const resolvedModulePath = path.resolve(pathInput); const normalizedModulePath = normalizeModulePath(resolvedModulePath); - const { packageName, relativePath } = - determineModuleContext(resolvedModulePath); + const context = determineModuleContext(resolvedModulePath); const libraryName = getLibraryName(resolvedModulePath, { + packageName, pathSuffix, }); console.log({ resolvedModulePath, normalizedModulePath, - packageName, - relativePath, + packageName: context.packageName, + relativePath: context.relativePath, libraryName, }); }), diff --git a/packages/host/src/node/path-utils.test.ts b/packages/host/src/node/path-utils.test.ts index cc6b307b..e780f802 100644 --- a/packages/host/src/node/path-utils.test.ts +++ b/packages/host/src/node/path-utils.test.ts @@ -208,6 +208,7 @@ describe("getLibraryName", () => { }); assert.equal( getLibraryName(path.join(tempDirectoryPath, "addon"), { + packageName: "keep", pathSuffix: "keep", }), "my-package--addon", @@ -215,6 +216,7 @@ describe("getLibraryName", () => { assert.equal( getLibraryName(path.join(tempDirectoryPath, "sub-directory/addon"), { + packageName: "keep", pathSuffix: "keep", }), "my-package--sub-directory-addon", @@ -230,6 +232,7 @@ describe("getLibraryName", () => { }); assert.equal( getLibraryName(path.join(tempDirectoryPath, "addon"), { + packageName: "keep", pathSuffix: "strip", }), "my-package--addon", @@ -237,6 +240,7 @@ describe("getLibraryName", () => { assert.equal( getLibraryName(path.join(tempDirectoryPath, "sub-directory", "addon"), { + packageName: "keep", pathSuffix: "strip", }), "my-package--addon", @@ -252,6 +256,7 @@ describe("getLibraryName", () => { }); assert.equal( getLibraryName(path.join(tempDirectoryPath, "addon"), { + packageName: "keep", pathSuffix: "omit", }), "my-package", @@ -259,11 +264,54 @@ describe("getLibraryName", () => { assert.equal( getLibraryName(path.join(tempDirectoryPath, "sub-directory", "addon"), { + packageName: "keep", pathSuffix: "omit", }), "my-package", ); }); + + it("keeps and escapes scope from package name", (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "package.json": `{ "name": "@my-org/my-package" }`, + "addon.apple.node/addon.node": "// This is supposed to be a binary file", + }); + assert.equal( + getLibraryName(path.join(tempDirectoryPath, "addon"), { + packageName: "keep", + pathSuffix: "strip", + }), + "my-org__my-package--addon", + ); + }); + + it("strips scope from package name", (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "package.json": `{ "name": "@my-org/my-package" }`, + "addon.apple.node/addon.node": "// This is supposed to be a binary file", + }); + assert.equal( + getLibraryName(path.join(tempDirectoryPath, "addon"), { + packageName: "strip", + pathSuffix: "strip", + }), + "my-package--addon", + ); + }); + + it("omits scope from package name", (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "package.json": `{ "name": "@my-org/my-package" }`, + "addon.apple.node/addon.node": "// This is supposed to be a binary file", + }); + assert.equal( + getLibraryName(path.join(tempDirectoryPath, "addon"), { + packageName: "omit", + pathSuffix: "strip", + }), + "addon", + ); + }); }); describe("findPackageDependencyPaths", () => { diff --git a/packages/host/src/node/path-utils.ts b/packages/host/src/node/path-utils.ts index 9ac1da93..b5cc9c7d 100644 --- a/packages/host/src/node/path-utils.ts +++ b/packages/host/src/node/path-utils.ts @@ -18,22 +18,33 @@ export const PLATFORM_EXTENSIONS = { apple: ".apple.node", } as const satisfies Record; -export type PlatformExtentions = (typeof PLATFORM_EXTENSIONS)[PlatformName]; +export type PlatformExtensions = (typeof PLATFORM_EXTENSIONS)[PlatformName]; -export const PATH_SUFFIX_CHOICES = ["strip", "keep", "omit"] as const; -export type PathSuffixChoice = (typeof PATH_SUFFIX_CHOICES)[number]; +export const LIBRARY_NAMING_CHOICES = ["strip", "keep", "omit"] as const; +export type LibraryNamingChoice = (typeof LIBRARY_NAMING_CHOICES)[number]; -export function assertPathSuffix( +export function assertLibraryNamingChoice( value: unknown, -): asserts value is PathSuffixChoice { +): asserts value is LibraryNamingChoice { assert(typeof value === "string", `Expected a string, got ${typeof value}`); assert( - (PATH_SUFFIX_CHOICES as readonly string[]).includes(value), - `Expected one of ${PATH_SUFFIX_CHOICES.join(", ")}`, + (LIBRARY_NAMING_CHOICES as readonly string[]).includes(value), + `Expected one of ${LIBRARY_NAMING_CHOICES.join(", ")}`, ); } export type NamingStrategy = { + /** + * Controls how the package name is transformed into a library name. + * The transformation is needed to disambiguate and avoid conflicts between addons with the same name (but in different sub-paths or packages). + * + * As an example, if the package name is `@my-org/my-pkg` and the path of the addon within the package is `build/Release/my-addon.node` (and `pathSuffix` is set to `"strip"`): + * - `"omit"`: Only the path within the package is used and the library name will be `my-addon`. + * - `"strip"`: Scope / org gets stripped and the library name will be `my-pkg--my-addon`. + * - `"keep"`: The org and name is kept and the library name will be `my-org--my-pkg--my-addon`. + */ + packageName: LibraryNamingChoice; + /** * Controls how the path of the addon inside a package is transformed into a library name. * The transformation is needed to disambiguate and avoid conflicts between addons with the same name (but in different sub-paths or packages). @@ -43,7 +54,7 @@ export type NamingStrategy = { * - `"strip"`: Path gets stripped to its basename and the library name will be `my-pkg--my-addon`. * - `"keep"`: The full path is kept and the library name will be `my-pkg--build-Release-my-addon`. */ - pathSuffix: PathSuffixChoice; + pathSuffix: LibraryNamingChoice; }; // Cache mapping package directory to package name across calls @@ -176,22 +187,71 @@ export function normalizeModulePath(modulePath: string) { } export function escapePath(modulePath: string) { - return modulePath.replace(/[^a-zA-Z0-9]/g, "-"); + return ( + modulePath + // Replace any non-alphanumeric character with a dash + .replace(/[^a-zA-Z0-9-_]/g, "-") + ); +} + +export function transformPackageName( + packageName: string, + strategy: LibraryNamingChoice, +) { + if (strategy === "omit") { + return ""; + } else if (packageName.startsWith("@")) { + const [first, ...rest] = packageName.split("/"); + assert(rest.length > 0, `Invalid scoped package name (${packageName})`); + if (strategy === "strip") { + return escapePath(rest.join("/")); + } else { + // Stripping away the @ and using double underscore to separate scope and name is common practice in other projects (like DefinitelyTyped) + return escapePath(`${first.replace(/^@/, "")}__${rest.join("/")}`); + } + } else { + return escapePath(packageName); + } +} + +export function transformPathSuffix( + relativePath: string, + strategy: LibraryNamingChoice, +) { + if (strategy === "omit") { + return ""; + } else if (strategy === "strip") { + return escapePath(path.basename(relativePath)); + } else { + return escapePath(relativePath.replaceAll(/[/\\]/g, "-")); + } } /** * Get the name of the library which will be used when the module is linked in. */ export function getLibraryName(modulePath: string, naming: NamingStrategy) { + assert( + naming.packageName !== "omit" || naming.pathSuffix !== "omit", + "Both packageName and pathSuffix cannot be 'omit' at the same time", + ); const { packageName, relativePath } = determineModuleContext(modulePath); - const escapedPackageName = escapePath(packageName); - return naming.pathSuffix === "omit" - ? escapedPackageName - : `${escapedPackageName}--${escapePath( - naming.pathSuffix === "strip" - ? path.basename(relativePath) - : relativePath, - )}`; + const transformedPackageName = transformPackageName( + packageName, + naming.packageName, + ); + const transformedRelativePath = transformPathSuffix( + relativePath, + naming.pathSuffix, + ); + const parts = []; + if (transformedPackageName) { + parts.push(transformedPackageName); + } + if (transformedRelativePath) { + parts.push(transformedRelativePath); + } + return parts.join("--"); } export function resolvePackageRoot( From ac3ee3448932a8a751dde5b6e5aae91def5cc90f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 21 Oct 2025 13:20:05 +0200 Subject: [PATCH 28/82] Convert binary plist files to xml and use a proper parser when linking (#275) * Convert binary plist files to xml and use a proper parser when linking * Add assert for platform in updateInfoPlist * Changed updateInfoPlist to assert old library name --- package-lock.json | 30 ++++ packages/host/package.json | 1 + packages/host/src/node/cli/apple.test.ts | 164 ++++++++++++++++++++-- packages/host/src/node/cli/apple.ts | 57 ++++---- packages/host/src/node/prebuilds/apple.ts | 18 +-- 5 files changed, 215 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16c9a3d5..48911022 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2914,6 +2914,17 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@expo/plist": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.7.tgz", + "integrity": "sha512-dGxqHPvCZKeRKDU1sJZMmuyVtcASuSYh1LPFVaM1DuffqPL36n6FMEL0iUqq2Tx3xhWk8wCnWl34IKplUjJDdA==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.2.3", + "xmlbuilder": "^15.1.1" + } + }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -6879,6 +6890,15 @@ "integrity": "sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA==", "license": "MIT" }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -14474,6 +14494,15 @@ "async-limiter": "~1.0.0" } }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -14853,6 +14882,7 @@ "version": "0.5.1", "license": "MIT", "dependencies": { + "@expo/plist": "^0.4.7", "@react-native-node-api/cli-utils": "0.1.0", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1" diff --git a/packages/host/package.json b/packages/host/package.json index 8031a554..caab011d 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -80,6 +80,7 @@ ], "license": "MIT", "dependencies": { + "@expo/plist": "^0.4.7", "@react-native-node-api/cli-utils": "0.1.0", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1" diff --git a/packages/host/src/node/cli/apple.test.ts b/packages/host/src/node/cli/apple.test.ts index 191baafe..051e7cee 100644 --- a/packages/host/src/node/cli/apple.test.ts +++ b/packages/host/src/node/cli/apple.test.ts @@ -1,50 +1,184 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import path from "node:path"; -import { readInfoPlist } from "./apple"; +import fs from "node:fs"; + +import { + determineInfoPlistPath, + readInfoPlist, + updateInfoPlist, +} from "./apple"; import { setupTempDirectory } from "../test-utils"; describe("apple", () => { - describe("Info.plist lookup", () => { - it("should find Info.plist files in unversioned frameworks", async (context) => { + describe("determineInfoPlistPath", () => { + it("should find Info.plist files in unversioned frameworks", (context) => { const infoPlistContents = `...`; const infoPlistSubPath = "Info.plist"; const tempDirectoryPath = setupTempDirectory(context, { [infoPlistSubPath]: infoPlistContents, }); - const result = await readInfoPlist(tempDirectoryPath); - - assert.strictEqual(result.contents, infoPlistContents); assert.strictEqual( - result.infoPlistPath, + determineInfoPlistPath(tempDirectoryPath), path.join(tempDirectoryPath, infoPlistSubPath), ); }); - it("should find Info.plist files in versioned frameworks", async (context) => { + it("should find Info.plist files in versioned frameworks", (context) => { const infoPlistContents = `...`; const infoPlistSubPath = "Versions/Current/Resources/Info.plist"; const tempDirectoryPath = setupTempDirectory(context, { [infoPlistSubPath]: infoPlistContents, }); - const result = await readInfoPlist(tempDirectoryPath); - - assert.strictEqual(result.contents, infoPlistContents); assert.strictEqual( - result.infoPlistPath, + determineInfoPlistPath(tempDirectoryPath), path.join(tempDirectoryPath, infoPlistSubPath), ); }); - it("should throw if Info.plist is missing from framework", async (context) => { + it("should throw if Info.plist is missing from framework", (context) => { + const tempDirectoryPath = setupTempDirectory(context, {}); + + assert.throws( + () => determineInfoPlistPath(tempDirectoryPath), + /Unable to locate an Info.plist file within framework./, + ); + }); + }); + + describe("readInfoPlist", () => { + it("should read Info.plist contents", async (context) => { + const infoPlistContents = ` + + + + + CFBundleExecutable + ExecutableFileName + CFBundleIconFile + AppIcon + + + `; + const infoPlistSubPath = "Info.plist"; + const tempDirectoryPath = setupTempDirectory(context, { + [infoPlistSubPath]: infoPlistContents, + }); + const infoPlistPath = path.join(tempDirectoryPath, infoPlistSubPath); + + const contents = await readInfoPlist(infoPlistPath); + assert.deepEqual(contents, { + CFBundleExecutable: "ExecutableFileName", + CFBundleIconFile: "AppIcon", + }); + }); + + it("should throw if Info.plist doesn't exist", async (context) => { const tempDirectoryPath = setupTempDirectory(context, {}); + const infoPlistPath = path.join(tempDirectoryPath, "Info.plist"); await assert.rejects( - async () => readInfoPlist(tempDirectoryPath), - /Unable to read Info.plist for framework at path ".*?", as an Info.plist file couldn't be found./, + () => readInfoPlist(infoPlistPath), + /Unable to read Info.plist at path/, ); }); }); + + describe("updateInfoPlist", () => { + it( + "updates an xml plist", + { skip: process.platform !== "darwin" }, + async (context) => { + const infoPlistSubPath = "Info.plist"; + const tempDirectoryPath = setupTempDirectory(context, { + [infoPlistSubPath]: ` + + + + + CFBundleExecutable + addon + + + `, + }); + + await updateInfoPlist({ + frameworkPath: tempDirectoryPath, + oldLibraryName: "addon", + newLibraryName: "new-addon-name", + }); + + const contents = await fs.promises.readFile( + path.join(tempDirectoryPath, infoPlistSubPath), + "utf-8", + ); + assert.match(contents, /<\?xml version="1.0" encoding="UTF-8"\?>/); + assert.match( + contents, + /CFBundleExecutable<\/key>\s*new-addon-name<\/string>/, + ); + }, + ); + + it( + "converts a binary plist to xml", + { skip: process.platform !== "darwin" }, + async (context) => { + const tempDirectoryPath = setupTempDirectory(context, {}); + // Write a binary plist file + const binaryPlistContents = Buffer.from( + // Generated running "base64 -i " on a plist file from a framework in the node-examples package + "YnBsaXN0MDDfEBUBAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4cICEiIyQiJSYnJChfEBNCdWlsZE1hY2hpbmVPU0J1aWxkXxAZQ0ZCdW5kbGVEZXZlbG9wbWVudFJlZ2lvbl8QEkNGQnVuZGxlRXhlY3V0YWJsZV8QEkNGQnVuZGxlSWRlbnRpZmllcl8QHUNGQnVuZGxlSW5mb0RpY3Rpb25hcnlWZXJzaW9uXxATQ0ZCdW5kbGVQYWNrYWdlVHlwZV8QGkNGQnVuZGxlU2hvcnRWZXJzaW9uU3RyaW5nXxARQ0ZCdW5kbGVTaWduYXR1cmVfEBpDRkJ1bmRsZVN1cHBvcnRlZFBsYXRmb3Jtc18QD0NGQnVuZGxlVmVyc2lvbl8QFUNTUmVzb3VyY2VzRmlsZU1hcHBlZFpEVENvbXBpbGVyXxAPRFRQbGF0Zm9ybUJ1aWxkXkRUUGxhdGZvcm1OYW1lXxARRFRQbGF0Zm9ybVZlcnNpb25aRFRTREtCdWlsZFlEVFNES05hbWVXRFRYY29kZVxEVFhjb2RlQnVpbGRfEBBNaW5pbXVtT1NWZXJzaW9uXlVJRGV2aWNlRmFtaWx5VjI0RzIzMVdFbmdsaXNoVWFkZG9uXxAPZXhhbXBsZV82LmFkZG9uUzYuMFRGTVdLUzEuMFQ/Pz8/oR9fEA9pUGhvbmVTaW11bGF0b3IJXxAiY29tLmFwcGxlLmNvbXBpbGVycy5sbHZtLmNsYW5nLjFfMFYyMkMxNDZfEA9pcGhvbmVzaW11bGF0b3JUMTguMl8QE2lwaG9uZXNpbXVsYXRvcjE4LjJUMTYyMFgxNkM1MDMyYaEpEAEACAA1AEsAZwB8AJEAsQDHAOQA+AEVAScBPwFKAVwBawF/AYoBlAGcAakBvAHLAdIB2gHgAfIB9gH7Af8CBAIGAhgCGQI+AkUCVwJcAnICdwKAAoIAAAAAAAACAQAAAAAAAAAqAAAAAAAAAAAAAAAAAAAChA==", + "base64", + ); + const binaryPlistPath = path.join(tempDirectoryPath, "Info.plist"); + await fs.promises.writeFile(binaryPlistPath, binaryPlistContents); + + await updateInfoPlist({ + frameworkPath: tempDirectoryPath, + oldLibraryName: "addon", + newLibraryName: "new-addon-name", + }); + + const contents = await fs.promises.readFile(binaryPlistPath, "utf-8"); + assert.match(contents, /<\?xml version="1.0" encoding="UTF-8"\?>/); + assert.match( + contents, + /CFBundleExecutable<\/key>\s*new-addon-name<\/string>/, + ); + }, + ); + + it( + "throws when not on darwin", + { skip: process.platform === "darwin" }, + async (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + ["Info.plist"]: '', + }); + + await assert.rejects( + () => + updateInfoPlist({ + frameworkPath: tempDirectoryPath, + oldLibraryName: "addon", + newLibraryName: "new-addon-name", + }), + (err) => { + assert(err instanceof Error); + assert.match(err.message, /Failed to convert Info.plist at path/); + assert(err.cause instanceof Error); + assert.match( + err.cause.message, + /Updating Info.plist files are not supported on this platform/, + ); + return true; + }, + ); + }, + ); + }); }); diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index 20a9065c..687b8ccf 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -3,6 +3,7 @@ import path from "node:path"; import fs from "node:fs"; import os from "node:os"; +import plist from "@expo/plist"; import { spawn } from "@react-native-node-api/cli-utils"; import { getLatestMtime, getLibraryName } from "../path-utils.js"; @@ -12,7 +13,7 @@ import { LinkModuleResult, } from "./link-modules.js"; -function determineInfoPlistPath(frameworkPath: string) { +export function determineInfoPlistPath(frameworkPath: string) { const checkedPaths = new Array(); // First, assume it is an "unversioned" framework that keeps its Info.plist in @@ -47,28 +48,15 @@ function determineInfoPlistPath(frameworkPath: string) { /** * Resolves the Info.plist file within a framework and reads its contents. */ -export async function readInfoPlist(frameworkPath: string) { - let infoPlistPath: string; +export async function readInfoPlist(infoPlistPath: string) { try { - infoPlistPath = determineInfoPlistPath(frameworkPath); + const contents = await fs.promises.readFile(infoPlistPath, "utf-8"); + return plist.parse(contents) as Record; } catch (cause) { - throw new Error( - `Unable to read Info.plist for framework at path "${frameworkPath}", as an Info.plist file couldn't be found.`, - { cause }, - ); - } - - let contents: string; - try { - contents = await fs.promises.readFile(infoPlistPath, "utf-8"); - } catch (cause) { - throw new Error( - `Unable to read Info.plist for framework at path "${frameworkPath}", due to a file system error.`, - { cause }, - ); + throw new Error(`Unable to read Info.plist at path "${infoPlistPath}"`, { + cause, + }); } - - return { infoPlistPath, contents }; } type UpdateInfoPlistOptions = { @@ -85,11 +73,32 @@ export async function updateInfoPlist({ oldLibraryName, newLibraryName, }: UpdateInfoPlistOptions) { - const { infoPlistPath, contents } = await readInfoPlist(frameworkPath); + const infoPlistPath = determineInfoPlistPath(frameworkPath); + + // Convert to XML format if needed + try { + assert( + process.platform === "darwin", + "Updating Info.plist files are not supported on this platform", + ); + await spawn("plutil", ["-convert", "xml1", infoPlistPath], { + outputMode: "inherit", + }); + } catch (error) { + throw new Error( + `Failed to convert Info.plist at path "${infoPlistPath}" to XML format`, + { cause: error }, + ); + } - // TODO: Use a proper plist parser - const updatedContents = contents.replaceAll(oldLibraryName, newLibraryName); - await fs.promises.writeFile(infoPlistPath, updatedContents, "utf-8"); + const contents = await readInfoPlist(infoPlistPath); + assert.equal( + contents.CFBundleExecutable, + oldLibraryName, + "Unexpected CFBundleExecutable value in Info.plist", + ); + contents.CFBundleExecutable = newLibraryName; + await fs.promises.writeFile(infoPlistPath, plist.build(contents), "utf-8"); } export async function linkXcframework({ diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index f3b1efde..33422b85 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -3,6 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import os from "node:os"; +import plist from "@expo/plist"; import { spawn } from "@react-native-node-api/cli-utils"; import { AppleTriplet } from "./triplets.js"; @@ -23,21 +24,6 @@ export const APPLE_ARCHITECTURES = { "arm64-apple-visionos-sim": "arm64", } satisfies Record; -export function createPlistContent(values: Record) { - return [ - '', - '', - '', - " ", - ...Object.entries(values).flatMap(([key, value]) => [ - ` ${key}`, - ` ${value}`, - ]), - " ", - "", - ].join("\n"); -} - type XCframeworkOptions = { frameworkPaths: string[]; outputPath: string; @@ -59,7 +45,7 @@ export async function createAppleFramework(libraryPath: string) { // Create an empty Info.plist file await fs.promises.writeFile( path.join(frameworkPath, "Info.plist"), - createPlistContent({ + plist.build({ CFBundleDevelopmentRegion: "en", CFBundleExecutable: libraryName, CFBundleIdentifier: `com.callstackincubator.node-api.${libraryName}`, From 427673f75a720f0a04e0fc78296a97e93d5ce247 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 21 Oct 2025 21:18:49 +0200 Subject: [PATCH 29/82] Use apple frameworks (#274) * Use Xcode to build frameworks * Generate CMakeLists.txt with frameworks enabled * Handling non-framework dylibs too * Dereference framework directories * Use framework in weak-node-api * Update cmake configs in addon-examples/tests * Assert a symbolic link before dereferencingDirectory * Add "-" to list of chars not escaped from bundle ids * Remove unused comments * Add FRAMEWORK in cmake-rn README.md --- package-lock.json | 26 +- packages/cmake-rn/README.md | 17 + packages/cmake-rn/package.json | 3 +- packages/cmake-rn/src/cli.ts | 163 +++------ packages/cmake-rn/src/helpers.ts | 8 + packages/cmake-rn/src/platforms/android.ts | 124 +++++-- packages/cmake-rn/src/platforms/apple.ts | 336 ++++++++++++++---- packages/cmake-rn/src/platforms/types.ts | 39 +- packages/cmake-rn/src/weak-node-api.ts | 10 +- packages/ferric/src/build.ts | 5 +- packages/gyp-to-cmake/package.json | 4 +- packages/gyp-to-cmake/src/cli.ts | 55 ++- packages/gyp-to-cmake/src/transformer.ts | 62 +++- packages/host/src/node/index.ts | 5 +- packages/host/src/node/path-utils.test.ts | 43 +++ packages/host/src/node/path-utils.ts | 15 + packages/host/src/node/prebuilds/apple.ts | 9 +- packages/host/weak-node-api/CMakeLists.txt | 9 +- .../tests/async/CMakeLists.txt | 21 +- .../tests/buffers/CMakeLists.txt | 21 +- 20 files changed, 701 insertions(+), 274 deletions(-) create mode 100644 packages/cmake-rn/src/helpers.ts diff --git a/package-lock.json b/package-lock.json index 48911022..1efe4481 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14834,11 +14834,12 @@ } }, "packages/cmake-rn": { - "version": "0.4.0", + "version": "0.4.1", "dependencies": { "@react-native-node-api/cli-utils": "0.1.0", "cmake-file-api": "0.1.0", - "react-native-node-api": "0.5.1" + "react-native-node-api": "0.5.2", + "zod": "^4.1.11" }, "bin": { "cmake-rn": "bin/cmake-rn.js" @@ -14848,13 +14849,22 @@ "node-api-headers": "^1.5.0" } }, + "packages/cmake-rn/node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/ferric": { "name": "ferric-cli", - "version": "0.3.3", + "version": "0.3.4", "dependencies": { "@napi-rs/cli": "~3.0.3", "@react-native-node-api/cli-utils": "0.1.0", - "react-native-node-api": "0.5.1" + "react-native-node-api": "0.5.2" }, "bin": { "ferric": "bin/ferric.js" @@ -14871,7 +14881,9 @@ "version": "0.3.0", "dependencies": { "@react-native-node-api/cli-utils": "0.1.0", - "gyp-parser": "^1.0.4" + "gyp-parser": "^1.0.4", + "pkg-dir": "^8.0.0", + "read-pkg": "^9.0.1" }, "bin": { "gyp-to-cmake": "bin/gyp-to-cmake.js" @@ -14879,7 +14891,7 @@ }, "packages/host": { "name": "react-native-node-api", - "version": "0.5.1", + "version": "0.5.2", "license": "MIT", "dependencies": { "@expo/plist": "^0.4.7", @@ -14920,7 +14932,7 @@ "cmake-rn": "*", "gyp-to-cmake": "*", "prebuildify": "^6.0.1", - "react-native-node-api": "^0.5.1", + "react-native-node-api": "^0.5.2", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } diff --git a/packages/cmake-rn/README.md b/packages/cmake-rn/README.md index 6b6aa888..ba41aecb 100644 --- a/packages/cmake-rn/README.md +++ b/packages/cmake-rn/README.md @@ -16,11 +16,28 @@ To link against `weak-node-api` just include the CMake config exposed through `W cmake_minimum_required(VERSION 3.15...3.31) project(tests-buffers) +# Defines the "weak-node-api" target include(${WEAK_NODE_API_CONFIG}) add_library(addon SHARED addon.c) target_link_libraries(addon PRIVATE weak-node-api) target_compile_features(addon PRIVATE cxx_std_20) + +if(APPLE) + # Build frameworks when building for Apple (optional) + set_target_properties(addon PROPERTIES + FRAMEWORK TRUE + MACOSX_FRAMEWORK_IDENTIFIER async_test.addon + MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 + MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 + XCODE_ATTRIBUTE_SKIP_INSTALL NO + ) +else() + set_target_properties(addon PROPERTIES + PREFIX "" + SUFFIX .node + ) +endif() ``` This is different from how `cmake-js` "injects" the Node-API for linking (via `${CMAKE_JS_INC}`, `${CMAKE_JS_SRC}` and `${CMAKE_JS_LIB}`). To allow for interoperability between these tools, we inject these when you pass `--cmake-js` to `cmake-rn`. diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index cd81d9d5..107c6962 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -26,7 +26,8 @@ "dependencies": { "@react-native-node-api/cli-utils": "0.1.0", "cmake-file-api": "0.1.0", - "react-native-node-api": "0.5.2" + "react-native-node-api": "0.5.2", + "zod": "^4.1.11" }, "peerDependencies": { "node-addon-api": "^8.3.1", diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 10bf9f63..2639c13d 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -12,20 +12,14 @@ import { assertFixable, wrapAction, } from "@react-native-node-api/cli-utils"; -import { isSupportedTriplet } from "react-native-node-api"; -import * as cmakeFileApi from "cmake-file-api"; -import { - getCmakeJSVariables, - getWeakNodeApiVariables, -} from "./weak-node-api.js"; import { platforms, allTriplets as allTriplets, findPlatformForTriplet, platformHasTriplet, } from "./platforms.js"; -import { BaseOpts, TripletContext, Platform } from "./platforms/types.js"; +import { Platform } from "./platforms/types.js"; // We're attaching a lot of listeners when spawning in parallel EventEmitter.defaultMaxListeners = 100; @@ -170,17 +164,17 @@ program = program.action( process.cwd(), expandTemplate(baseOptions.out, baseOptions), ); - const { out, build: buildPath } = baseOptions; + const { verbose, clean, source, out, build: buildPath } = baseOptions; assertFixable( - fs.existsSync(path.join(baseOptions.source, "CMakeLists.txt")), - `No CMakeLists.txt found in source directory: ${chalk.dim(baseOptions.source)}`, + fs.existsSync(path.join(source, "CMakeLists.txt")), + `No CMakeLists.txt found in source directory: ${chalk.dim(source)}`, { instructions: `Change working directory into a directory with a CMakeLists.txt, create one or specify the correct source directory using --source`, }, ); - if (baseOptions.clean) { + if (clean) { await fs.promises.rm(buildPath, { recursive: true, force: true }); } const triplets = new Set(requestedTriplets); @@ -217,13 +211,17 @@ program = program.action( const tripletContexts = [...triplets].map((triplet) => { const platform = findPlatformForTriplet(triplet); - const tripletBuildPath = getTripletBuildPath(buildPath, triplet); + return { triplet, platform, - buildPath: tripletBuildPath, - outputPath: path.join(tripletBuildPath, "out"), - options: baseOptions, + async spawn(command: string, args: string[], cwd?: string) { + await spawn(command, args, { + outputMode: verbose ? "inherit" : "buffered", + outputPrefix: verbose ? chalk.dim(`[${triplet}] `) : undefined, + cwd, + }); + }, }; }); @@ -231,11 +229,29 @@ program = program.action( const tripletsSummary = chalk.dim( `(${getTripletsSummary(tripletContexts)})`, ); + + // Perform configure steps for each platform in sequence await oraPromise( Promise.all( - tripletContexts.map(({ platform, ...context }) => - configureProject(platform, context, baseOptions), - ), + platforms.map(async (platform) => { + const relevantTriplets = tripletContexts.filter(({ triplet }) => + platformHasTriplet(platform, triplet), + ); + if (relevantTriplets.length > 0) { + await platform.configure( + relevantTriplets, + baseOptions, + (command, args, cwd) => + spawn(command, args, { + outputMode: verbose ? "inherit" : "buffered", + outputPrefix: verbose + ? chalk.dim(`[${platform.name}] `) + : undefined, + cwd, + }), + ); + } + }), ), { text: `Configuring projects ${tripletsSummary}`, @@ -249,13 +265,14 @@ program = program.action( await oraPromise( Promise.all( tripletContexts.map(async ({ platform, ...context }) => { - // Delete any stale build artifacts before building - // This is important, since we might rename the output files - await fs.promises.rm(context.outputPath, { - recursive: true, - force: true, - }); - await buildProject(platform, context, baseOptions); + // TODO: Consider if this is still important 😬 + // // Delete any stale build artifacts before building + // // This is important, since we might rename the output files + // await fs.promises.rm(context.outputPath, { + // recursive: true, + // force: true, + // }); + await platform.build(context, baseOptions); }), ), { @@ -274,13 +291,7 @@ program = program.action( if (relevantTriplets.length == 0) { continue; } - await platform.postBuild( - { - outputPath: out, - triplets: relevantTriplets, - }, - baseOptions, - ); + await platform.postBuild(out, relevantTriplets, baseOptions); } }), ); @@ -302,92 +313,4 @@ function getTripletsSummary( .join(" / "); } -/** - * Namespaces the output path with a triplet name - */ -function getTripletBuildPath(buildPath: string, triplet: unknown) { - assert(typeof triplet === "string", "Expected triplet to be a string"); - return path.join(buildPath, triplet.replace(/;/g, "_")); -} - -async function configureProject( - platform: Platform>, - context: TripletContext, - options: BaseOpts, -) { - const { triplet, buildPath, outputPath } = context; - const { verbose, source, weakNodeApiLinkage, cmakeJs } = options; - - // TODO: Make the two following definitions a part of the platform definition - - const nodeApiDefinitions = - weakNodeApiLinkage && isSupportedTriplet(triplet) - ? [getWeakNodeApiVariables(triplet)] - : []; - - const cmakeJsDefinitions = - cmakeJs && isSupportedTriplet(triplet) - ? [getCmakeJSVariables(triplet)] - : []; - - const definitions = [ - ...nodeApiDefinitions, - ...cmakeJsDefinitions, - ...options.define, - { CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath }, - ]; - - await cmakeFileApi.createSharedStatelessQuery(buildPath, "codemodel", "2"); - - await spawn( - "cmake", - [ - "-S", - source, - "-B", - buildPath, - ...platform.configureArgs(context, options), - ...toDefineArguments(definitions), - ], - { - outputMode: verbose ? "inherit" : "buffered", - outputPrefix: verbose ? chalk.dim(`[${triplet}] `) : undefined, - }, - ); -} - -async function buildProject( - platform: Platform>, - context: TripletContext, - options: BaseOpts, -) { - const { triplet, buildPath } = context; - const { verbose, configuration } = options; - await spawn( - "cmake", - [ - "--build", - buildPath, - "--config", - configuration, - ...(options.target.length > 0 ? ["--target", ...options.target] : []), - "--", - ...platform.buildArgs(context, options), - ], - { - outputMode: verbose ? "inherit" : "buffered", - outputPrefix: verbose ? chalk.dim(`[${triplet}] `) : undefined, - }, - ); -} - -function toDefineArguments(declarations: Array>) { - return declarations.flatMap((values) => - Object.entries(values).flatMap(([key, definition]) => [ - "-D", - `${key}=${definition}`, - ]), - ); -} - export { program }; diff --git a/packages/cmake-rn/src/helpers.ts b/packages/cmake-rn/src/helpers.ts new file mode 100644 index 00000000..ac88cc92 --- /dev/null +++ b/packages/cmake-rn/src/helpers.ts @@ -0,0 +1,8 @@ +export function toDefineArguments(declarations: Array>) { + return declarations.flatMap((values) => + Object.entries(values).flatMap(([key, definition]) => [ + "-D", + `${key}=${definition}`, + ]), + ); +} diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index b7b758c3..479fdc92 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -14,6 +14,11 @@ import { import * as cmakeFileApi from "cmake-file-api"; import type { Platform } from "./types.js"; +import { toDefineArguments } from "../helpers.js"; +import { + getCmakeJSVariables, + getWeakNodeApiVariables, +} from "../weak-node-api.js"; // This should match https://github.com/react-native-community/template/blob/main/template/android/build.gradle#L7 const DEFAULT_NDK_VERSION = "27.1.12297006"; @@ -40,6 +45,10 @@ const androidSdkVersionOption = new Option( type AndroidOpts = { ndkVersion: string; androidSdkVersion: string }; +function getBuildPath(baseBuildPath: string, triplet: Triplet) { + return path.join(baseBuildPath, triplet); +} + export const platform: Platform = { id: "android", name: "Android", @@ -63,7 +72,19 @@ export const platform: Platform = { .addOption(ndkVersionOption) .addOption(androidSdkVersionOption); }, - configureArgs({ triplet }, { ndkVersion, androidSdkVersion }) { + async configure( + triplets, + { + configuration, + ndkVersion, + androidSdkVersion, + source, + define, + build, + weakNodeApiLinkage, + cmakeJs, + }, + ) { const { ANDROID_HOME } = process.env; assert( typeof ANDROID_HOME === "string", @@ -84,57 +105,84 @@ export const platform: Platform = { ndkPath, "build/cmake/android.toolchain.cmake", ); - const architecture = ANDROID_ARCHITECTURES[triplet]; - - return [ - "-G", - "Ninja", - "--toolchain", - toolchainPath, - "-D", - "CMAKE_SYSTEM_NAME=Android", - // "-D", - // `CPACK_SYSTEM_NAME=Android-${architecture}`, - // "-D", - // `CMAKE_INSTALL_PREFIX=${installPath}`, - // "-D", - // `CMAKE_BUILD_TYPE=${configuration}`, - "-D", - "CMAKE_MAKE_PROGRAM=ninja", - // "-D", - // "CMAKE_C_COMPILER_LAUNCHER=ccache", - // "-D", - // "CMAKE_CXX_COMPILER_LAUNCHER=ccache", - "-D", - `ANDROID_NDK=${ndkPath}`, - "-D", - `ANDROID_ABI=${architecture}`, - "-D", - "ANDROID_TOOLCHAIN=clang", - "-D", - `ANDROID_PLATFORM=${androidSdkVersion}`, - "-D", - // TODO: Make this configurable - "ANDROID_STL=c++_shared", + + const commonDefinitions = [ + ...define, + { + CMAKE_BUILD_TYPE: configuration, + CMAKE_SYSTEM_NAME: "Android", + // "CMAKE_INSTALL_PREFIX": installPath, + CMAKE_MAKE_PROGRAM: "ninja", + // "-D", + // "CMAKE_C_COMPILER_LAUNCHER=ccache", + // "-D", + // "CMAKE_CXX_COMPILER_LAUNCHER=ccache", + ANDROID_NDK: ndkPath, + ANDROID_TOOLCHAIN: "clang", + ANDROID_PLATFORM: androidSdkVersion, + // TODO: Make this configurable + ANDROID_STL: "c++_shared", + }, ]; + + await Promise.all( + triplets.map(async ({ triplet, spawn }) => { + const buildPath = getBuildPath(build, triplet); + const outputPath = path.join(buildPath, "out"); + // We want to use the CMake File API to query information later + await cmakeFileApi.createSharedStatelessQuery( + buildPath, + "codemodel", + "2", + ); + + await spawn("cmake", [ + "-S", + source, + "-B", + buildPath, + "-G", + "Ninja", + "--toolchain", + toolchainPath, + ...toDefineArguments([ + ...(weakNodeApiLinkage ? [getWeakNodeApiVariables(triplet)] : []), + ...(cmakeJs ? [getCmakeJSVariables(triplet)] : []), + ...commonDefinitions, + { + // "CPACK_SYSTEM_NAME": `Android-${architecture}`, + CMAKE_LIBRARY_OUTPUT_DIRECTORY: outputPath, + ANDROID_ABI: ANDROID_ARCHITECTURES[triplet], + }, + ]), + ]); + }), + ); }, - buildArgs() { - return []; + async build({ triplet, spawn }, { target, build }) { + const buildPath = getBuildPath(build, triplet); + await spawn("cmake", [ + "--build", + buildPath, + ...(target.length > 0 ? ["--target", ...target] : []), + ]); }, isSupportedByHost() { const { ANDROID_HOME } = process.env; return typeof ANDROID_HOME === "string" && fs.existsSync(ANDROID_HOME); }, async postBuild( - { outputPath, triplets }, - { autoLink, configuration, target }, + outputPath, + triplets, + { autoLink, configuration, target, build }, ) { const prebuilds: Record< string, { triplet: Triplet; libraryPath: string }[] > = {}; - for (const { triplet, buildPath } of triplets) { + for (const { triplet } of triplets) { + const buildPath = getBuildPath(build, triplet); assert(fs.existsSync(buildPath), `Expected a directory at ${buildPath}`); const targets = await cmakeFileApi.readCurrentTargetsDeep( buildPath, diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index 91f5e7fd..6edc6065 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import path from "node:path"; import fs from "node:fs"; +import cp from "node:child_process"; import { Option, @@ -11,10 +12,41 @@ import { AppleTriplet as Triplet, createAppleFramework, createXCframework, + dereferenceDirectory, } from "react-native-node-api"; import type { Platform } from "./types.js"; import * as cmakeFileApi from "cmake-file-api"; +import { toDefineArguments } from "../helpers.js"; +import { + getCmakeJSVariables, + getWeakNodeApiVariables, +} from "../weak-node-api.js"; + +import * as z from "zod"; + +const XcodeListOutput = z.object({ + project: z.object({ + configurations: z.array(z.string()), + name: z.string(), + schemes: z.array(z.string()), + targets: z.array(z.string()), + }), +}); + +function listXcodeProject(cwd: string): z.infer { + const result = cp.spawnSync("xcodebuild", ["-list", "-json"], { + encoding: "utf-8", + cwd, + }); + assert.equal( + result.status, + 0, + `Failed to run xcodebuild -list: ${result.stderr}`, + ); + const parsed = JSON.parse(result.stdout) as unknown; + return XcodeListOutput.parse(parsed); +} type XcodeSDKName = | "iphoneos" @@ -54,6 +86,20 @@ const CMAKE_SYSTEM_NAMES = { "arm64-apple-visionos-sim": "visionOS", } satisfies Record; +const DESTINATION_BY_TRIPLET = { + "arm64-apple-ios": "generic/platform=iOS", + "arm64-apple-ios-sim": "generic/platform=iOS Simulator", + "arm64-apple-tvos": "generic/platform=tvOS", + // "x86_64-apple-tvos": "generic/platform=tvOS", + "arm64-apple-tvos-sim": "generic/platform=tvOS Simulator", + "arm64-apple-visionos": "generic/platform=visionOS", + "arm64-apple-visionos-sim": "generic/platform=visionOS Simulator", + // TODO: Verify that the three following destinations are correct and actually work + "x86_64-apple-darwin": "generic/platform=macOS,arch=x86_64", + "arm64-apple-darwin": "generic/platform=macOS,arch=arm64", + "arm64;x86_64-apple-darwin": "generic/platform=macOS", +} satisfies Record; + type AppleArchitecture = "arm64" | "x86_64" | "arm64;x86_64"; export const APPLE_ARCHITECTURES = { @@ -84,11 +130,6 @@ export function createPlistContent(values: Record) { ].join("\n"); } -export function getAppleBuildArgs() { - // We expect the final application to sign these binaries - return ["CODE_SIGNING_ALLOWED=NO"]; -} - const xcframeworkExtensionOption = new Option( "--xcframework-extension", "Don't rename the xcframework to .apple.node", @@ -98,6 +139,34 @@ type AppleOpts = { xcframeworkExtension: boolean; }; +function getBuildPath(baseBuildPath: string, triplet: Triplet) { + return path.join(baseBuildPath, triplet.replace(/;/g, "_")); +} + +async function readCmakeSharedLibraryTarget( + buildPath: string, + configuration: string, + target: string[], +) { + const targets = await cmakeFileApi.readCurrentTargetsDeep( + buildPath, + configuration, + "2.0", + ); + const sharedLibraries = targets.filter( + ({ type, name }) => + type === "SHARED_LIBRARY" && + (target.length === 0 || target.includes(name)), + ); + assert.equal( + sharedLibraries.length, + 1, + "Expected exactly one shared library", + ); + const [sharedLibrary] = sharedLibraries; + return sharedLibrary; +} + export const platform: Platform = { id: "apple", name: "Apple", @@ -116,86 +185,229 @@ export const platform: Platform = { amendCommand(command) { return command.addOption(xcframeworkExtensionOption); }, - configureArgs({ triplet }) { - return [ - "-G", - "Xcode", - "-D", - `CMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAMES[triplet]}`, - "-D", - `CMAKE_OSX_SYSROOT=${XCODE_SDK_NAMES[triplet]}`, - "-D", - `CMAKE_OSX_ARCHITECTURES=${APPLE_ARCHITECTURES[triplet]}`, - ]; + async configure( + triplets, + { source, build, define, weakNodeApiLinkage, cmakeJs }, + spawn, + ) { + // Ideally, we would generate a single Xcode project supporting all architectures / platforms + // However, CMake's Xcode generator does not support that well, so we generate one project per triplet + // Specifically, the linking of weak-node-api breaks, since the sdk / arch specific framework + // from the xcframework is picked at configure time, not at build time. + // See https://gitlab.kitware.com/cmake/cmake/-/issues/21752#note_1717047 for more information. + await Promise.all( + triplets.map(async ({ triplet }) => { + const buildPath = getBuildPath(build, triplet); + // We want to use the CMake File API to query information later + // TODO: Or do we? + await cmakeFileApi.createSharedStatelessQuery( + buildPath, + "codemodel", + "2", + ); + await spawn("cmake", [ + "-S", + source, + "-B", + buildPath, + "-G", + "Xcode", + ...toDefineArguments([ + ...define, + ...(weakNodeApiLinkage ? [getWeakNodeApiVariables("apple")] : []), + ...(cmakeJs ? [getCmakeJSVariables("apple")] : []), + { + CMAKE_SYSTEM_NAME: CMAKE_SYSTEM_NAMES[triplet], + CMAKE_OSX_SYSROOT: XCODE_SDK_NAMES[triplet], + CMAKE_OSX_ARCHITECTURES: APPLE_ARCHITECTURES[triplet], + }, + { + // Setting the output directories works around an issue with Xcode generator + // where an unexpanded variable would emitted in the artifact paths. + // This is okay, since we're generating per triplet build directories anyway. + // https://gitlab.kitware.com/cmake/cmake/-/issues/24161 + CMAKE_LIBRARY_OUTPUT_DIRECTORY: path.join(buildPath, "out"), + CMAKE_ARCHIVE_OUTPUT_DIRECTORY: path.join(buildPath, "out"), + }, + ]), + ]); + }), + ); }, - buildArgs() { + async build({ spawn, triplet }, { build, target, configuration }) { // We expect the final application to sign these binaries - return ["CODE_SIGNING_ALLOWED=NO"]; + if (target.length > 1) { + throw new Error("Building for multiple targets is not supported yet"); + } + + const buildPath = getBuildPath(build, triplet); + + const sharedLibrary = await readCmakeSharedLibraryTarget( + buildPath, + configuration, + target, + ); + + const isFramework = sharedLibrary.nameOnDisk?.includes(".framework/"); + + if (isFramework) { + const { project } = listXcodeProject(buildPath); + + const schemes = project.schemes.filter( + (scheme) => scheme !== "ALL_BUILD" && scheme !== "ZERO_CHECK", + ); + + assert( + schemes.length === 1, + `Expected exactly one buildable scheme, got ${schemes.join(", ")}`, + ); + + const [scheme] = schemes; + + if (target.length === 1) { + assert.equal( + scheme, + target[0], + "Expected the only scheme to match the requested target", + ); + } + + await spawn( + "xcodebuild", + [ + "archive", + "-scheme", + scheme, + "-configuration", + configuration, + "-destination", + DESTINATION_BY_TRIPLET[triplet], + ], + buildPath, + ); + await spawn( + "xcodebuild", + [ + "install", + "-scheme", + scheme, + "-configuration", + configuration, + "-destination", + DESTINATION_BY_TRIPLET[triplet], + ], + buildPath, + ); + } else { + await spawn("cmake", [ + "--build", + buildPath, + "--config", + configuration, + ...(target.length > 0 ? ["--target", ...target] : []), + "--", + + // Skip code-signing (needed when building free dynamic libraries) + // TODO: Make this configurable + "CODE_SIGNING_ALLOWED=NO", + ]); + // Create a framework + const { artifacts } = sharedLibrary; + assert( + artifacts && artifacts.length === 1, + "Expected exactly one artifact", + ); + const [artifact] = artifacts; + await createAppleFramework( + path.join(buildPath, artifact.path), + triplet.endsWith("-darwin"), + ); + } }, isSupportedByHost: function (): boolean | Promise { return process.platform === "darwin"; }, async postBuild( - { outputPath, triplets }, - { configuration, autoLink, xcframeworkExtension, target }, + outputPath, + triplets, + { configuration, autoLink, xcframeworkExtension, target, build }, ) { - const prebuilds: Record = {}; - for (const { buildPath } of triplets) { + const libraryNames = new Set(); + const frameworkPaths: string[] = []; + for (const { triplet } of triplets) { + const buildPath = getBuildPath(build, triplet); assert(fs.existsSync(buildPath), `Expected a directory at ${buildPath}`); - const targets = await cmakeFileApi.readCurrentTargetsDeep( + const sharedLibrary = await readCmakeSharedLibraryTarget( buildPath, configuration, - "2.0", - ); - const sharedLibraries = targets.filter( - ({ type, name }) => - type === "SHARED_LIBRARY" && - (target.length === 0 || target.includes(name)), - ); - assert.equal( - sharedLibraries.length, - 1, - "Expected exactly one shared library", + target, ); - const [sharedLibrary] = sharedLibraries; const { artifacts } = sharedLibrary; assert( artifacts && artifacts.length === 1, "Expected exactly one artifact", ); const [artifact] = artifacts; - // Add prebuild entry, creating a new entry if needed - if (!(sharedLibrary.name in prebuilds)) { - prebuilds[sharedLibrary.name] = []; + libraryNames.add(sharedLibrary.name); + // Locate the path of the framework, if a free dynamic library was built + if (artifact.path.includes(".framework/")) { + frameworkPaths.push(path.dirname(path.join(buildPath, artifact.path))); + } else { + const libraryName = path.basename( + artifact.path, + path.extname(artifact.path), + ); + const frameworkPath = path.join( + buildPath, + path.dirname(artifact.path), + `${libraryName}.framework`, + ); + assert( + fs.existsSync(frameworkPath), + `Expected to find a framework at: ${frameworkPath}`, + ); + frameworkPaths.push(frameworkPath); } - prebuilds[sharedLibrary.name].push(path.join(buildPath, artifact.path)); } + // Make sure none of the frameworks are symlinks + // We do this before creating an xcframework to avoid symlink paths being invalidated + // as the xcframework might be moved to a different location + await Promise.all( + frameworkPaths.map(async (frameworkPath) => { + const stat = await fs.promises.lstat(frameworkPath); + if (stat.isSymbolicLink()) { + await dereferenceDirectory(frameworkPath); + } + }), + ); + const extension = xcframeworkExtension ? ".xcframework" : ".apple.node"; - for (const [libraryName, libraryPaths] of Object.entries(prebuilds)) { - const frameworkPaths = await Promise.all( - libraryPaths.map(createAppleFramework), - ); - // Create the xcframework - const xcframeworkOutputPath = path.resolve( - outputPath, - `${libraryName}${extension}`, - ); + assert( + libraryNames.size === 1, + "Expected all libraries to have the same name", + ); + const [libraryName] = libraryNames; - await oraPromise( - createXCframework({ - outputPath: xcframeworkOutputPath, - frameworkPaths, - autoLink, - }), - { - text: `Assembling XCFramework (${libraryName})`, - successText: `XCFramework (${libraryName}) assembled into ${prettyPath(xcframeworkOutputPath)}`, - failText: ({ message }) => - `Failed to assemble XCFramework (${libraryName}): ${message}`, - }, - ); - } + // Create the xcframework + const xcframeworkOutputPath = path.resolve( + outputPath, + `${libraryName}${extension}`, + ); + + await oraPromise( + createXCframework({ + outputPath: xcframeworkOutputPath, + frameworkPaths, + autoLink, + }), + { + text: `Assembling XCFramework (${libraryName})`, + successText: `XCFramework (${libraryName}) assembled into ${prettyPath(xcframeworkOutputPath)}`, + failText: ({ message }) => + `Failed to assemble XCFramework (${libraryName}): ${message}`, + }, + ); }, }; diff --git a/packages/cmake-rn/src/platforms/types.ts b/packages/cmake-rn/src/platforms/types.ts index 7f4a78da..d3cd9b9f 100644 --- a/packages/cmake-rn/src/platforms/types.ts +++ b/packages/cmake-rn/src/platforms/types.ts @@ -17,10 +17,18 @@ export type BaseOpts = Omit, "triplet">; export type TripletContext = { triplet: Triplet; - buildPath: string; - outputPath: string; + /** + * Spawn a command in the context of this triplet + */ + spawn: Spawn; }; +export type Spawn = ( + command: string, + args: string[], + cwd?: string, +) => Promise; + export type Platform< Triplets extends string[] = string[], Opts extends cli.OptionValues = Record, @@ -51,30 +59,29 @@ export type Platform< */ isSupportedByHost(): boolean | Promise; /** - * Platform specific arguments passed to CMake to configure a triplet project. + * Configure all projects for this platform. */ - configureArgs( - context: TripletContext, + configure( + triplets: TripletContext[], options: BaseOpts & Opts, - ): string[]; + spawn: Spawn, + ): Promise; /** - * Platform specific arguments passed to CMake to build a triplet project. + * Platform specific command to build a triplet project. */ - buildArgs( + build( context: TripletContext, options: BaseOpts & Opts, - ): string[]; + ): Promise; /** * Called to combine multiple triplets into a single prebuilt artefact. */ postBuild( - context: { - /** - * Location of the final prebuilt artefact. - */ - outputPath: string; - triplets: TripletContext[]; - }, + /** + * Location of the final prebuilt artefact. + */ + outputPath: string, + triplets: TripletContext[], options: BaseOpts & Opts, ): Promise; }; diff --git a/packages/cmake-rn/src/weak-node-api.ts b/packages/cmake-rn/src/weak-node-api.ts index bc8a7407..ee6cb154 100644 --- a/packages/cmake-rn/src/weak-node-api.ts +++ b/packages/cmake-rn/src/weak-node-api.ts @@ -16,8 +16,10 @@ export function toCmakePath(input: string) { return input.split(path.win32.sep).join(path.posix.sep); } -export function getWeakNodeApiPath(triplet: SupportedTriplet): string { - if (isAppleTriplet(triplet)) { +export function getWeakNodeApiPath( + triplet: SupportedTriplet | "apple", +): string { + if (triplet === "apple" || isAppleTriplet(triplet)) { const xcframeworkPath = path.join( weakNodeApiPath, "weak-node-api.xcframework", @@ -53,7 +55,7 @@ function getNodeApiIncludePaths() { } export function getWeakNodeApiVariables( - triplet: SupportedTriplet, + triplet: SupportedTriplet | "apple", ): Record { return { // Expose an includable CMake config file declaring the weak-node-api target @@ -67,7 +69,7 @@ export function getWeakNodeApiVariables( * For compatibility with cmake-js */ export function getCmakeJSVariables( - triplet: SupportedTriplet, + triplet: SupportedTriplet | "apple", ): Record { return { CMAKE_JS_INC: getNodeApiIncludePaths().join(";"), diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index fdf961e3..a3eb1684 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -237,7 +237,10 @@ export const buildCommand = new Command("build") if (appleLibraries.length > 0) { const libraryPaths = await combineLibraries(appleLibraries); const frameworkPaths = await Promise.all( - libraryPaths.map(createAppleFramework), + libraryPaths.map((libraryPath) => + // TODO: Pass true as `versioned` argument for -darwin targets + createAppleFramework(libraryPath), + ), ); const xcframeworkFilename = determineXCFrameworkFilename( frameworkPaths, diff --git a/packages/gyp-to-cmake/package.json b/packages/gyp-to-cmake/package.json index 702a1d1f..415fe62b 100644 --- a/packages/gyp-to-cmake/package.json +++ b/packages/gyp-to-cmake/package.json @@ -23,6 +23,8 @@ }, "dependencies": { "@react-native-node-api/cli-utils": "0.1.0", - "gyp-parser": "^1.0.4" + "gyp-parser": "^1.0.4", + "pkg-dir": "^8.0.0", + "read-pkg": "^9.0.1" } } diff --git a/packages/gyp-to-cmake/src/cli.ts b/packages/gyp-to-cmake/src/cli.ts index 1045ba45..45cefbaa 100644 --- a/packages/gyp-to-cmake/src/cli.ts +++ b/packages/gyp-to-cmake/src/cli.ts @@ -1,7 +1,12 @@ +import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; +import { packageDirectorySync } from "pkg-dir"; +import { readPackageSync } from "read-pkg"; + import { Command, + Option, prettyPath, wrapAction, } from "@react-native-node-api/cli-utils"; @@ -12,23 +17,25 @@ import { type GypToCmakeListsOptions, } from "./transformer.js"; -export type TransformOptions = Omit< - GypToCmakeListsOptions, - "gyp" | "projectName" -> & { +export type TransformOptions = Omit & { disallowUnknownProperties: boolean; - projectName?: string; }; export function generateProjectName(gypPath: string) { - return path.dirname(gypPath).replaceAll(path.sep, "-"); + const packagePath = packageDirectorySync({ cwd: path.dirname(gypPath) }); + assert(packagePath, "Expected the binding.gyp file to be inside a package"); + const { name } = readPackageSync({ cwd: packagePath }); + return name + .replace(/^@/g, "") + .replace(/\//g, "--") + .replace(/[^a-zA-Z0-9_-]/g, "_"); } export function transformBindingGypFile( gypPath: string, { disallowUnknownProperties, - projectName = generateProjectName(gypPath), + projectName, ...restOfOptions }: TransformOptions, ) { @@ -49,7 +56,7 @@ export function transformBindingGypFile( export function transformBindingGypsRecursively( directoryPath: string, - options: TransformOptions, + options: Omit, ) { const entries = fs.readdirSync(directoryPath, { withFileTypes: true }); for (const entry of entries) { @@ -57,11 +64,19 @@ export function transformBindingGypsRecursively( if (entry.isDirectory()) { transformBindingGypsRecursively(fullPath, options); } else if (entry.isFile() && entry.name === "binding.gyp") { - transformBindingGypFile(fullPath, options); + transformBindingGypFile(fullPath, { + ...options, + projectName: generateProjectName(fullPath), + }); } } } +const projectNameOption = new Option( + "--project-name ", + "Project name to use in CMakeLists.txt", +).default(undefined, "Uses name from the surrounding package.json"); + export const program = new Command("gyp-to-cmake") .description("Transform binding.gyp to CMakeLists.txt") .option( @@ -70,7 +85,12 @@ export const program = new Command("gyp-to-cmake") ) .option("--weak-node-api", "Link against the weak-node-api library", false) .option("--define-napi-version", "Define NAPI_VERSION for all targets", false) + .option( + "--no-apple-framework", + "Disable emitting target properties to produce Apple frameworks", + ) .option("--cpp ", "C++ standard version", "17") + .addOption(projectNameOption) .argument( "[path]", "Path to the binding.gyp file or directory to traverse recursively", @@ -80,19 +100,30 @@ export const program = new Command("gyp-to-cmake") wrapAction( ( targetPath: string, - { pathTransforms, cpp, defineNapiVersion, weakNodeApi }, + { + pathTransforms, + cpp, + defineNapiVersion, + weakNodeApi, + appleFramework, + projectName, + }, ) => { - const options: TransformOptions = { + const options: Omit = { unsupportedBehaviour: "throw", disallowUnknownProperties: false, transformWinPathsToPosix: pathTransforms, compileFeatures: cpp ? [`cxx_std_${cpp}`] : [], defineNapiVersion, weakNodeApi, + appleFramework, }; const stat = fs.statSync(targetPath); if (stat.isFile()) { - transformBindingGypFile(targetPath, options); + transformBindingGypFile(targetPath, { + ...options, + projectName: projectName ?? generateProjectName(targetPath), + }); } else if (stat.isDirectory()) { transformBindingGypsRecursively(targetPath, options); } else { diff --git a/packages/gyp-to-cmake/src/transformer.ts b/packages/gyp-to-cmake/src/transformer.ts index fdf5c604..3c9b65d8 100644 --- a/packages/gyp-to-cmake/src/transformer.ts +++ b/packages/gyp-to-cmake/src/transformer.ts @@ -15,6 +15,7 @@ export type GypToCmakeListsOptions = { compileFeatures?: string[]; defineNapiVersion?: boolean; weakNodeApi?: boolean; + appleFramework?: boolean; }; function isCmdExpansion(value: string) { @@ -26,6 +27,10 @@ function escapeSpaces(source: string) { return source.replace(/ /g, "\\ "); } +function escapeBundleIdentifier(identifier: string) { + return identifier.replaceAll("__", ".").replace(/[^A-Za-z0-9.-_]/g, "-"); +} + /** * @see {@link https://github.com/cmake-js/cmake-js?tab=readme-ov-file#usage} for details on the template used * @returns The contents of a CMakeLists.txt file @@ -39,6 +44,7 @@ export function bindingGypToCmakeLists({ transformWinPathsToPosix = true, defineNapiVersion = true, weakNodeApi = false, + appleFramework = true, compileFeatures = [], }: GypToCmakeListsOptions): string { function mapExpansion(value: string): string[] { @@ -113,10 +119,58 @@ export function bindingGypToCmakeLists({ escapedIncludes.push("${CMAKE_JS_INC}"); } - lines.push( - `add_library(${targetName} SHARED ${escapedSources.join(" ")})`, - `set_target_properties(${targetName} PROPERTIES PREFIX "" SUFFIX ".node")`, - ); + function setTargetPropertiesLines( + properties: Record, + indent = "", + ): string[] { + return [ + `${indent}set_target_properties(${targetName} PROPERTIES`, + ...Object.entries(properties).map( + ([key, value]) => `${indent} ${key} ${value ? value : '""'}`, + ), + `${indent} )`, + ]; + } + + lines.push(`add_library(${targetName} SHARED ${escapedSources.join(" ")})`); + + if (appleFramework) { + lines.push( + "", + 'option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON)', + "", + "if(APPLE AND BUILD_APPLE_FRAMEWORK)", + ...setTargetPropertiesLines( + { + FRAMEWORK: "TRUE", + MACOSX_FRAMEWORK_IDENTIFIER: escapeBundleIdentifier( + `${projectName}.${targetName}`, + ), + MACOSX_FRAMEWORK_SHORT_VERSION_STRING: "1.0", + MACOSX_FRAMEWORK_BUNDLE_VERSION: "1.0", + XCODE_ATTRIBUTE_SKIP_INSTALL: "NO", + }, + " ", + ), + "else()", + ...setTargetPropertiesLines( + { + PREFIX: "", + SUFFIX: ".node", + }, + " ", + ), + "endif()", + "", + ); + } else { + lines.push( + ...setTargetPropertiesLines({ + PREFIX: "", + SUFFIX: ".node", + }), + ); + } if (libraries.length > 0) { lines.push( diff --git a/packages/host/src/node/index.ts b/packages/host/src/node/index.ts index dd914402..3071f233 100644 --- a/packages/host/src/node/index.ts +++ b/packages/host/src/node/index.ts @@ -22,6 +22,9 @@ export { determineXCFrameworkFilename, } from "./prebuilds/apple.js"; -export { determineLibraryBasename } from "./path-utils.js"; +export { + determineLibraryBasename, + dereferenceDirectory, +} from "./path-utils.js"; export { weakNodeApiPath } from "./weak-node-api.js"; diff --git a/packages/host/src/node/path-utils.test.ts b/packages/host/src/node/path-utils.test.ts index e780f802..7a65147b 100644 --- a/packages/host/src/node/path-utils.test.ts +++ b/packages/host/src/node/path-utils.test.ts @@ -13,6 +13,7 @@ import { isNodeApiModule, stripExtension, findNodeApiModulePathsByDependency, + dereferenceDirectory, } from "./path-utils.js"; import { setupTempDirectory } from "./test-utils.js"; @@ -550,3 +551,45 @@ describe("findNodeAddonForBindings()", () => { }); } }); + +describe("dereferenceDirectory", () => { + describe("when directory contains symlinks", () => { + it("should dereference symlinks", async (context) => { + // Create a temp directory with a symlink + const tempDir = setupTempDirectory(context, { + "original/file.txt": "Hello, world!", + }); + const originalPath = path.join(tempDir, "original"); + const symlinkPath = path.join(tempDir, "link"); + // Create a link to the original directory + fs.symlinkSync(originalPath, symlinkPath, "dir"); + // And an internal link + fs.symlinkSync( + path.join(originalPath, "file.txt"), + path.join(originalPath, "linked-file.txt"), + "file", + ); + + { + // Verify that outer link is no longer a link + const stat = await fs.promises.lstat(symlinkPath); + assert(stat.isSymbolicLink()); + } + + await dereferenceDirectory(symlinkPath); + + { + // Verify that outer link is no longer a link + const stat = await fs.promises.lstat(symlinkPath); + assert(!stat.isSymbolicLink()); + } + + // Verify that the internal link is still a link to a readable file + const internalLinkPath = path.join(symlinkPath, "linked-file.txt"); + const internalLinkStat = await fs.promises.lstat(internalLinkPath); + assert(internalLinkStat.isSymbolicLink()); + const content = await fs.promises.readFile(internalLinkPath, "utf8"); + assert.equal(content, "Hello, world!"); + }); + }); +}); diff --git a/packages/host/src/node/path-utils.ts b/packages/host/src/node/path-utils.ts index b5cc9c7d..f898d566 100644 --- a/packages/host/src/node/path-utils.ts +++ b/packages/host/src/node/path-utils.ts @@ -543,3 +543,18 @@ export function findNodeAddonForBindings(id: string, fromDir: string) { } return undefined; } + +export async function dereferenceDirectory(dirPath: string) { + const tempPath = dirPath + ".tmp"; + const stat = await fs.promises.lstat(dirPath); + assert(stat.isSymbolicLink(), `Expected a symbolic link at: ${dirPath}`); + // Move the existing framework out of the way + await fs.promises.rename(dirPath, tempPath); + // Only dereference the symlink at tempPath (not recursively) + const realPath = await fs.promises.realpath(tempPath); + await fs.promises.cp(realPath, dirPath, { + recursive: true, + verbatimSymlinks: true, + }); + await fs.promises.unlink(tempPath); +} diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index 33422b85..dce6353b 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -30,7 +30,14 @@ type XCframeworkOptions = { autoLink: boolean; }; -export async function createAppleFramework(libraryPath: string) { +export async function createAppleFramework( + libraryPath: string, + versioned = false, +) { + if (versioned) { + // TODO: Add support for generating a Versions/Current/Resources/Info.plist convention framework + throw new Error("Creating versioned frameworks is not supported yet"); + } assert(fs.existsSync(libraryPath), `Library not found: ${libraryPath}`); // Write a info.plist file to the framework const libraryName = path.basename(libraryPath, path.extname(libraryPath)); diff --git a/packages/host/weak-node-api/CMakeLists.txt b/packages/host/weak-node-api/CMakeLists.txt index 49e9bf85..08422d62 100644 --- a/packages/host/weak-node-api/CMakeLists.txt +++ b/packages/host/weak-node-api/CMakeLists.txt @@ -3,13 +3,18 @@ project(weak-node-api) add_library(${PROJECT_NAME} SHARED weak_node_api.cpp - ${CMAKE_JS_SRC} ) # Stripping the prefix from the library name # to make sure the name of the XCFramework will match the name of the library if(APPLE) - set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") + set_target_properties(${PROJECT_NAME} PROPERTIES + FRAMEWORK TRUE + MACOSX_FRAMEWORK_IDENTIFIER com.callstack.${PROJECT_NAME} + MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 + MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 + XCODE_ATTRIBUTE_SKIP_INSTALL NO + ) endif() target_include_directories(${PROJECT_NAME} diff --git a/packages/node-addon-examples/tests/async/CMakeLists.txt b/packages/node-addon-examples/tests/async/CMakeLists.txt index a94a7716..659e3461 100644 --- a/packages/node-addon-examples/tests/async/CMakeLists.txt +++ b/packages/node-addon-examples/tests/async/CMakeLists.txt @@ -1,9 +1,26 @@ cmake_minimum_required(VERSION 3.15...3.31) -project(tests-async) +project(async_test) include(${WEAK_NODE_API_CONFIG}) add_library(addon SHARED addon.c) -set_target_properties(addon PROPERTIES PREFIX "" SUFFIX ".node") + +option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON) + +if(APPLE AND BUILD_APPLE_FRAMEWORK) + set_target_properties(addon PROPERTIES + FRAMEWORK TRUE + MACOSX_FRAMEWORK_IDENTIFIER async_test.addon + MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 + MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 + XCODE_ATTRIBUTE_SKIP_INSTALL NO + ) +elseif(APPLE) + set_target_properties(addon PROPERTIES + PREFIX "" + SUFFIX .node + ) +endif() + target_link_libraries(addon PRIVATE weak-node-api) target_compile_features(addon PRIVATE cxx_std_17) \ No newline at end of file diff --git a/packages/node-addon-examples/tests/buffers/CMakeLists.txt b/packages/node-addon-examples/tests/buffers/CMakeLists.txt index 837359fc..bca19bce 100644 --- a/packages/node-addon-examples/tests/buffers/CMakeLists.txt +++ b/packages/node-addon-examples/tests/buffers/CMakeLists.txt @@ -1,9 +1,26 @@ cmake_minimum_required(VERSION 3.15...3.31) -project(tests-buffers) +project(buffers_test) include(${WEAK_NODE_API_CONFIG}) add_library(addon SHARED addon.c) -set_target_properties(addon PROPERTIES PREFIX "" SUFFIX ".node") + +option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON) + +if(APPLE AND BUILD_APPLE_FRAMEWORK) + set_target_properties(addon PROPERTIES + FRAMEWORK TRUE + MACOSX_FRAMEWORK_IDENTIFIER buffers_test.addon + MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 + MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 + XCODE_ATTRIBUTE_SKIP_INSTALL NO + ) +elseif(APPLE) + set_target_properties(addon PROPERTIES + PREFIX "" + SUFFIX .node + ) +endif() + target_link_libraries(addon PRIVATE weak-node-api) target_compile_features(addon PRIVATE cxx_std_17) \ No newline at end of file From 5156d3515895634ce95370f7a21405b391d37f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 21 Oct 2025 21:25:20 +0200 Subject: [PATCH 30/82] Add missing changesets --- .changeset/brown-breads-glow.md | 7 +++++++ .changeset/salty-ghosts-work.md | 9 +++++++++ 2 files changed, 16 insertions(+) create mode 100644 .changeset/brown-breads-glow.md create mode 100644 .changeset/salty-ghosts-work.md diff --git a/.changeset/brown-breads-glow.md b/.changeset/brown-breads-glow.md new file mode 100644 index 00000000..d888cc4a --- /dev/null +++ b/.changeset/brown-breads-glow.md @@ -0,0 +1,7 @@ +--- +"cmake-rn": minor +"gyp-to-cmake": minor +"react-native-node-api": minor +--- + +Use of CMake targets producing Apple frameworks instead of free dylibs is now supported diff --git a/.changeset/salty-ghosts-work.md b/.changeset/salty-ghosts-work.md new file mode 100644 index 00000000..85c5636f --- /dev/null +++ b/.changeset/salty-ghosts-work.md @@ -0,0 +1,9 @@ +--- +"@react-native-node-api/cli-utils": patch +"cmake-rn": patch +"ferric-cli": patch +"gyp-to-cmake": patch +"react-native-node-api": patch +--- + +Refactored moving prettyPath util to CLI utils package From acd06f2971101c6ab1e9f032a0abb67202067ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 23 Oct 2025 08:23:31 +0200 Subject: [PATCH 31/82] Link Xcframework using plists files instead of re-creating them (#281) * Update existing xcframework instead of re-creating it when linking * Add changeset * Support linking of both flat and versioned frameworks * Delete leftover magic files * Trigger CI again * Fix parsing of xcframework info and add tests * Use Versions/Current/Resources as suggested Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .changeset/blue-parts-cheer.md | 5 + package-lock.json | 31 +- packages/host/package.json | 6 +- packages/host/src/node/cli/apple.test.ts | 372 ++++++++++++------- packages/host/src/node/cli/apple.ts | 442 ++++++++++++++--------- 5 files changed, 517 insertions(+), 339 deletions(-) create mode 100644 .changeset/blue-parts-cheer.md diff --git a/.changeset/blue-parts-cheer.md b/.changeset/blue-parts-cheer.md new file mode 100644 index 00000000..782aa96f --- /dev/null +++ b/.changeset/blue-parts-cheer.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Linking Node-API addons for Apple platforms is no longer re-creating Xcframeworks diff --git a/package-lock.json b/package-lock.json index 1efe4481..ccc7603b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14597,10 +14597,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -14824,15 +14823,6 @@ "zod": "^4.1.11" } }, - "packages/cmake-file-api/node_modules/zod": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", - "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "packages/cmake-rn": { "version": "0.4.1", "dependencies": { @@ -14849,15 +14839,6 @@ "node-api-headers": "^1.5.0" } }, - "packages/cmake-rn/node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "packages/ferric": { "name": "ferric-cli", "version": "0.3.4", @@ -14897,7 +14878,8 @@ "@expo/plist": "^0.4.7", "@react-native-node-api/cli-utils": "0.1.0", "pkg-dir": "^8.0.0", - "read-pkg": "^9.0.1" + "read-pkg": "^9.0.1", + "zod": "^4.1.11" }, "bin": { "react-native-node-api": "bin/react-native-node-api.mjs" @@ -14906,8 +14888,7 @@ "@babel/core": "^7.26.10", "@babel/types": "^7.27.0", "fswin": "^3.24.829", - "node-api-headers": "^1.5.0", - "zod": "^3.24.3" + "node-api-headers": "^1.5.0" }, "peerDependencies": { "@babel/core": "^7.26.10", diff --git a/packages/host/package.json b/packages/host/package.json index caab011d..914a7664 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -83,14 +83,14 @@ "@expo/plist": "^0.4.7", "@react-native-node-api/cli-utils": "0.1.0", "pkg-dir": "^8.0.0", - "read-pkg": "^9.0.1" + "read-pkg": "^9.0.1", + "zod": "^4.1.11" }, "devDependencies": { "@babel/core": "^7.26.10", "@babel/types": "^7.27.0", "fswin": "^3.24.829", - "node-api-headers": "^1.5.0", - "zod": "^3.24.3" + "node-api-headers": "^1.5.0" }, "peerDependencies": { "@babel/core": "^7.26.10", diff --git a/packages/host/src/node/cli/apple.test.ts b/packages/host/src/node/cli/apple.test.ts index 051e7cee..7914a408 100644 --- a/packages/host/src/node/cli/apple.test.ts +++ b/packages/host/src/node/cli/apple.test.ts @@ -2,54 +2,19 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import path from "node:path"; import fs from "node:fs"; +import cp from "node:child_process"; import { - determineInfoPlistPath, - readInfoPlist, - updateInfoPlist, + linkFlatFramework, + readAndParsePlist, + readFrameworkInfo, + readXcframeworkInfo, } from "./apple"; import { setupTempDirectory } from "../test-utils"; -describe("apple", () => { - describe("determineInfoPlistPath", () => { - it("should find Info.plist files in unversioned frameworks", (context) => { - const infoPlistContents = `...`; - const infoPlistSubPath = "Info.plist"; - const tempDirectoryPath = setupTempDirectory(context, { - [infoPlistSubPath]: infoPlistContents, - }); - - assert.strictEqual( - determineInfoPlistPath(tempDirectoryPath), - path.join(tempDirectoryPath, infoPlistSubPath), - ); - }); - - it("should find Info.plist files in versioned frameworks", (context) => { - const infoPlistContents = `...`; - const infoPlistSubPath = "Versions/Current/Resources/Info.plist"; - const tempDirectoryPath = setupTempDirectory(context, { - [infoPlistSubPath]: infoPlistContents, - }); - - assert.strictEqual( - determineInfoPlistPath(tempDirectoryPath), - path.join(tempDirectoryPath, infoPlistSubPath), - ); - }); - - it("should throw if Info.plist is missing from framework", (context) => { - const tempDirectoryPath = setupTempDirectory(context, {}); - - assert.throws( - () => determineInfoPlistPath(tempDirectoryPath), - /Unable to locate an Info.plist file within framework./, - ); - }); - }); - +describe("apple", { skip: process.platform !== "darwin" }, () => { describe("readInfoPlist", () => { - it("should read Info.plist contents", async (context) => { + it("should read Info.plist contents, plus extra keys not in schema", async (context) => { const infoPlistContents = ` @@ -57,8 +22,10 @@ describe("apple", () => { CFBundleExecutable ExecutableFileName - CFBundleIconFile - AppIcon + CFBundlePackageType + FMWK + CFBundleInfoDictionaryVersion + 6.0 `; @@ -68,10 +35,11 @@ describe("apple", () => { }); const infoPlistPath = path.join(tempDirectoryPath, infoPlistSubPath); - const contents = await readInfoPlist(infoPlistPath); + const contents = await readAndParsePlist(infoPlistPath); assert.deepEqual(contents, { CFBundleExecutable: "ExecutableFileName", - CFBundleIconFile: "AppIcon", + CFBundlePackageType: "FMWK", + CFBundleInfoDictionaryVersion: "6.0", }); }); @@ -80,104 +48,246 @@ describe("apple", () => { const infoPlistPath = path.join(tempDirectoryPath, "Info.plist"); await assert.rejects( - () => readInfoPlist(infoPlistPath), - /Unable to read Info.plist at path/, + () => readAndParsePlist(infoPlistPath), + /Expected an Info.plist/, ); }); }); - describe("updateInfoPlist", () => { - it( - "updates an xml plist", - { skip: process.platform !== "darwin" }, - async (context) => { - const infoPlistSubPath = "Info.plist"; - const tempDirectoryPath = setupTempDirectory(context, { - [infoPlistSubPath]: ` - - - + describe("readXcframeworkInfo", () => { + it("should read xcframework Info.plist contents, plus extra keys not in schema", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "foo.xcframework": { + "Info.plist": ` + + + - CFBundleExecutable - addon + AvailableLibraries + + + BinaryPath + hello.framework/hello + LibraryIdentifier + tvos-arm64 + LibraryPath + hello.framework + SupportedArchitectures + + arm64 + + SupportedPlatform + tvos + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 - - `, - }); + + `, + }, + }); - await updateInfoPlist({ - frameworkPath: tempDirectoryPath, - oldLibraryName: "addon", - newLibraryName: "new-addon-name", - }); + const result = await readXcframeworkInfo( + path.join(tempDirectoryPath, "foo.xcframework", "Info.plist"), + ); - const contents = await fs.promises.readFile( - path.join(tempDirectoryPath, infoPlistSubPath), - "utf-8", - ); - assert.match(contents, /<\?xml version="1.0" encoding="UTF-8"\?>/); - assert.match( - contents, - /CFBundleExecutable<\/key>\s*new-addon-name<\/string>/, - ); - }, - ); + assert.deepEqual(result, { + AvailableLibraries: [ + { + BinaryPath: "hello.framework/hello", + LibraryIdentifier: "tvos-arm64", + LibraryPath: "hello.framework", + SupportedArchitectures: ["arm64"], + SupportedPlatform: "tvos", + }, + ], + CFBundlePackageType: "XFWK", + XCFrameworkFormatVersion: "1.0", + }); + }); + }); - it( - "converts a binary plist to xml", - { skip: process.platform !== "darwin" }, - async (context) => { - const tempDirectoryPath = setupTempDirectory(context, {}); - // Write a binary plist file - const binaryPlistContents = Buffer.from( - // Generated running "base64 -i " on a plist file from a framework in the node-examples package - "YnBsaXN0MDDfEBUBAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4cICEiIyQiJSYnJChfEBNCdWlsZE1hY2hpbmVPU0J1aWxkXxAZQ0ZCdW5kbGVEZXZlbG9wbWVudFJlZ2lvbl8QEkNGQnVuZGxlRXhlY3V0YWJsZV8QEkNGQnVuZGxlSWRlbnRpZmllcl8QHUNGQnVuZGxlSW5mb0RpY3Rpb25hcnlWZXJzaW9uXxATQ0ZCdW5kbGVQYWNrYWdlVHlwZV8QGkNGQnVuZGxlU2hvcnRWZXJzaW9uU3RyaW5nXxARQ0ZCdW5kbGVTaWduYXR1cmVfEBpDRkJ1bmRsZVN1cHBvcnRlZFBsYXRmb3Jtc18QD0NGQnVuZGxlVmVyc2lvbl8QFUNTUmVzb3VyY2VzRmlsZU1hcHBlZFpEVENvbXBpbGVyXxAPRFRQbGF0Zm9ybUJ1aWxkXkRUUGxhdGZvcm1OYW1lXxARRFRQbGF0Zm9ybVZlcnNpb25aRFRTREtCdWlsZFlEVFNES05hbWVXRFRYY29kZVxEVFhjb2RlQnVpbGRfEBBNaW5pbXVtT1NWZXJzaW9uXlVJRGV2aWNlRmFtaWx5VjI0RzIzMVdFbmdsaXNoVWFkZG9uXxAPZXhhbXBsZV82LmFkZG9uUzYuMFRGTVdLUzEuMFQ/Pz8/oR9fEA9pUGhvbmVTaW11bGF0b3IJXxAiY29tLmFwcGxlLmNvbXBpbGVycy5sbHZtLmNsYW5nLjFfMFYyMkMxNDZfEA9pcGhvbmVzaW11bGF0b3JUMTguMl8QE2lwaG9uZXNpbXVsYXRvcjE4LjJUMTYyMFgxNkM1MDMyYaEpEAEACAA1AEsAZwB8AJEAsQDHAOQA+AEVAScBPwFKAVwBawF/AYoBlAGcAakBvAHLAdIB2gHgAfIB9gH7Af8CBAIGAhgCGQI+AkUCVwJcAnICdwKAAoIAAAAAAAACAQAAAAAAAAAqAAAAAAAAAAAAAAAAAAAChA==", - "base64", - ); - const binaryPlistPath = path.join(tempDirectoryPath, "Info.plist"); - await fs.promises.writeFile(binaryPlistPath, binaryPlistContents); + describe("readFrameworkInfo", () => { + it("should read framework Info.plist contents, plus extra keys not in schema", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "foo.framework": { + "Info.plist": ` + + + + + CFBundlePackageType + FMWK + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + example-0--hello + CFBundleIdentifier + example_0.hello + CFBundleSupportedPlatforms + + XRSimulator + + + + `, + }, + }); - await updateInfoPlist({ - frameworkPath: tempDirectoryPath, - oldLibraryName: "addon", - newLibraryName: "new-addon-name", - }); + const result = await readFrameworkInfo( + path.join(tempDirectoryPath, "foo.framework", "Info.plist"), + ); - const contents = await fs.promises.readFile(binaryPlistPath, "utf-8"); - assert.match(contents, /<\?xml version="1.0" encoding="UTF-8"\?>/); - assert.match( - contents, - /CFBundleExecutable<\/key>\s*new-addon-name<\/string>/, - ); - }, - ); + assert.deepEqual(result, { + CFBundlePackageType: "FMWK", + CFBundleInfoDictionaryVersion: "6.0", + CFBundleExecutable: "example-0--hello", + CFBundleIdentifier: "example_0.hello", + CFBundleSupportedPlatforms: ["XRSimulator"], + }); + }); + }); - it( - "throws when not on darwin", - { skip: process.platform === "darwin" }, - async (context) => { - const tempDirectoryPath = setupTempDirectory(context, { - ["Info.plist"]: '', - }); - - await assert.rejects( - () => - updateInfoPlist({ - frameworkPath: tempDirectoryPath, - oldLibraryName: "addon", - newLibraryName: "new-addon-name", - }), - (err) => { - assert(err instanceof Error); - assert.match(err.message, /Failed to convert Info.plist at path/); - assert(err.cause instanceof Error); - assert.match( - err.cause.message, - /Updating Info.plist files are not supported on this platform/, - ); - return true; - }, + describe("linkFlatFramework", () => { + it("updates an xml plist, preserving extra keys", async (context) => { + const infoPlistSubPath = "Info.plist"; + const tempDirectoryPath = setupTempDirectory(context, { + "foo.framework": { + [infoPlistSubPath]: ` + + + + + CFBundleExecutable + addon + CFBundlePackageType + FMWK + CFBundleInfoDictionaryVersion + 6.0 + MyExtraKey + MyExtraValue + + + `, + }, + }); + + // Create a dummy binary file + cp.spawnSync("clang", [ + "-dynamiclib", + "-o", + path.join(tempDirectoryPath, "foo.framework", "addon"), + "-xc", + "/dev/null", + ]); + + await linkFlatFramework({ + frameworkPath: path.join(tempDirectoryPath, "foo.framework"), + newLibraryName: "new-addon-name", + }); + + const contents = await fs.promises.readFile( + path.join( + tempDirectoryPath, + "new-addon-name.framework", + infoPlistSubPath, + ), + "utf-8", + ); + assert.match(contents, /<\?xml version="1.0" encoding="UTF-8"\?>/); + assert.match( + contents, + /CFBundleExecutable<\/key>\s*new-addon-name<\/string>/, + ); + + // Assert the install name was updated correctly + const { stdout: otoolOutput } = cp.spawnSync( + "otool", + [ + "-L", + path.join( + tempDirectoryPath, + "new-addon-name.framework", + "new-addon-name", + ), + ], + { encoding: "utf-8" }, + ); + assert.match( + otoolOutput, + /@rpath\/new-addon-name.framework\/new-addon-name/, + ); + + // It should preserve extra keys + assert.match( + contents, + /MyExtraKey<\/key>\s*MyExtraValue<\/string>/, + ); + }); + + it("converts a binary plist to xml", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, {}); + await fs.promises.mkdir(path.join(tempDirectoryPath, "foo.framework")); + // Write a binary plist file + const binaryPlistContents = Buffer.from( + // Generated running "base64 -i " on a plist file from a framework in the node-examples package + "YnBsaXN0MDDfEBUBAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4cICEiIyQiJSYnJChfEBNCdWlsZE1hY2hpbmVPU0J1aWxkXxAZQ0ZCdW5kbGVEZXZlbG9wbWVudFJlZ2lvbl8QEkNGQnVuZGxlRXhlY3V0YWJsZV8QEkNGQnVuZGxlSWRlbnRpZmllcl8QHUNGQnVuZGxlSW5mb0RpY3Rpb25hcnlWZXJzaW9uXxATQ0ZCdW5kbGVQYWNrYWdlVHlwZV8QGkNGQnVuZGxlU2hvcnRWZXJzaW9uU3RyaW5nXxARQ0ZCdW5kbGVTaWduYXR1cmVfEBpDRkJ1bmRsZVN1cHBvcnRlZFBsYXRmb3Jtc18QD0NGQnVuZGxlVmVyc2lvbl8QFUNTUmVzb3VyY2VzRmlsZU1hcHBlZFpEVENvbXBpbGVyXxAPRFRQbGF0Zm9ybUJ1aWxkXkRUUGxhdGZvcm1OYW1lXxARRFRQbGF0Zm9ybVZlcnNpb25aRFRTREtCdWlsZFlEVFNES05hbWVXRFRYY29kZVxEVFhjb2RlQnVpbGRfEBBNaW5pbXVtT1NWZXJzaW9uXlVJRGV2aWNlRmFtaWx5VjI0RzIzMVdFbmdsaXNoVWFkZG9uXxAPZXhhbXBsZV82LmFkZG9uUzYuMFRGTVdLUzEuMFQ/Pz8/oR9fEA9pUGhvbmVTaW11bGF0b3IJXxAiY29tLmFwcGxlLmNvbXBpbGVycy5sbHZtLmNsYW5nLjFfMFYyMkMxNDZfEA9pcGhvbmVzaW11bGF0b3JUMTguMl8QE2lwaG9uZXNpbXVsYXRvcjE4LjJUMTYyMFgxNkM1MDMyYaEpEAEACAA1AEsAZwB8AJEAsQDHAOQA+AEVAScBPwFKAVwBawF/AYoBlAGcAakBvAHLAdIB2gHgAfIB9gH7Af8CBAIGAhgCGQI+AkUCVwJcAnICdwKAAoIAAAAAAAACAQAAAAAAAAAqAAAAAAAAAAAAAAAAAAAChA==", + "base64", + ); + await fs.promises.writeFile( + path.join(tempDirectoryPath, "foo.framework", "Info.plist"), + binaryPlistContents, + ); + + // Create a dummy binary file + cp.spawnSync("clang", [ + "-dynamiclib", + "-o", + path.join(tempDirectoryPath, "foo.framework", "addon"), + "-xc", + "/dev/null", + ]); + + await linkFlatFramework({ + frameworkPath: path.join(tempDirectoryPath, "foo.framework"), + newLibraryName: "new-addon-name", + }); + + const contents = await fs.promises.readFile( + path.join(tempDirectoryPath, "new-addon-name.framework", "Info.plist"), + "utf-8", + ); + assert.match(contents, /<\?xml version="1.0" encoding="UTF-8"\?>/); + assert.match( + contents, + /CFBundleExecutable<\/key>\s*new-addon-name<\/string>/, + ); + }); + }); +}); + +describe("apple on non-darwin", { skip: process.platform === "darwin" }, () => { + it("throws", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + ["Info.plist"]: '', + }); + + await assert.rejects( + () => + linkFlatFramework({ + frameworkPath: path.join(tempDirectoryPath, "Info.plist"), + newLibraryName: "new-addon-name", + }), + (err) => { + assert(err instanceof Error); + assert.match( + err.message, + /Linking Apple addons are only supported on macOS/, ); + return true; }, ); }); diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index 687b8ccf..0ccbd82a 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -1,9 +1,10 @@ import assert from "node:assert/strict"; import path from "node:path"; import fs from "node:fs"; -import os from "node:os"; import plist from "@expo/plist"; +import * as zod from "zod"; + import { spawn } from "@react-native-node-api/cli-utils"; import { getLatestMtime, getLibraryName } from "../path-utils.js"; @@ -13,92 +14,228 @@ import { LinkModuleResult, } from "./link-modules.js"; -export function determineInfoPlistPath(frameworkPath: string) { - const checkedPaths = new Array(); +/** + * Reads and parses a plist file, converting it to XML format if needed. + */ +export async function readAndParsePlist(plistPath: string): Promise { + assert(fs.existsSync(plistPath), `Expected an Info.plist: ${plistPath}`); + // Try reading the file to see if it is already in XML format + try { + const contents = await fs.promises.readFile(plistPath, "utf-8"); + if (contents.startsWith(" `- ${checkedPath}`), - ].join("\n"), - ); +export async function writeXcframeworkInfo( + xcframeworkPath: string, + info: zod.infer, +) { + const infoPlistPath = path.join(xcframeworkPath, "Info.plist"); + const infoPlistXml = plist.build(info); + await fs.promises.writeFile(infoPlistPath, infoPlistXml, "utf-8"); } -/** - * Resolves the Info.plist file within a framework and reads its contents. - */ -export async function readInfoPlist(infoPlistPath: string) { - try { - const contents = await fs.promises.readFile(infoPlistPath, "utf-8"); - return plist.parse(contents) as Record; - } catch (cause) { - throw new Error(`Unable to read Info.plist at path "${infoPlistPath}"`, { - cause, - }); - } +const FrameworkInfoSchema = zod.looseObject({ + CFBundlePackageType: zod.literal("FMWK"), + CFBundleInfoDictionaryVersion: zod.literal("6.0"), + CFBundleExecutable: zod.string(), +}); + +export async function readFrameworkInfo(infoPlistPath: string) { + const infoPlist = await readAndParsePlist(infoPlistPath); + return FrameworkInfoSchema.parse(infoPlist); } -type UpdateInfoPlistOptions = { +export async function writeFrameworkInfo( + infoPlistPath: string, + info: zod.infer, +) { + const infoPlistXml = plist.build(info); + await fs.promises.writeFile(infoPlistPath, infoPlistXml, "utf-8"); +} + +type LinkFrameworkOptions = { frameworkPath: string; - oldLibraryName: string; newLibraryName: string; }; -/** - * Update the Info.plist file of an xcframework to use the new library name. - */ -export async function updateInfoPlist({ +export async function linkFramework({ frameworkPath, - oldLibraryName, newLibraryName, -}: UpdateInfoPlistOptions) { - const infoPlistPath = determineInfoPlistPath(frameworkPath); - - // Convert to XML format if needed - try { - assert( - process.platform === "darwin", - "Updating Info.plist files are not supported on this platform", - ); - await spawn("plutil", ["-convert", "xml1", infoPlistPath], { - outputMode: "inherit", - }); - } catch (error) { - throw new Error( - `Failed to convert Info.plist at path "${infoPlistPath}" to XML format`, - { cause: error }, - ); +}: LinkFrameworkOptions) { + assert.equal( + process.platform, + "darwin", + "Linking Apple frameworks are only supported on macOS", + ); + assert( + fs.existsSync(frameworkPath), + `Expected framework at '${frameworkPath}'`, + ); + if (fs.existsSync(path.join(frameworkPath, "Versions"))) { + await linkVersionedFramework({ frameworkPath, newLibraryName }); + } else { + await linkFlatFramework({ frameworkPath, newLibraryName }); } +} + +export async function linkFlatFramework({ + frameworkPath, + newLibraryName, +}: LinkFrameworkOptions) { + assert.equal( + process.platform, + "darwin", + "Linking Apple addons are only supported on macOS", + ); + const frameworkInfoPath = path.join(frameworkPath, "Info.plist"); + const frameworkInfo = await readFrameworkInfo(frameworkInfoPath); + // Update install name + await spawn( + "install_name_tool", + [ + "-id", + `@rpath/${newLibraryName}.framework/${newLibraryName}`, + frameworkInfo.CFBundleExecutable, + ], + { + outputMode: "buffered", + cwd: frameworkPath, + }, + ); + await writeFrameworkInfo(frameworkInfoPath, { + ...frameworkInfo, + CFBundleExecutable: newLibraryName, + }); + // Rename the actual binary + await fs.promises.rename( + path.join(frameworkPath, frameworkInfo.CFBundleExecutable), + path.join(frameworkPath, newLibraryName), + ); + // Rename the framework directory + await fs.promises.rename( + frameworkPath, + path.join(path.dirname(frameworkPath), `${newLibraryName}.framework`), + ); +} - const contents = await readInfoPlist(infoPlistPath); +export async function linkVersionedFramework({ + frameworkPath, + newLibraryName, +}: LinkFrameworkOptions) { assert.equal( - contents.CFBundleExecutable, - oldLibraryName, - "Unexpected CFBundleExecutable value in Info.plist", + process.platform, + "darwin", + "Linking Apple addons are only supported on macOS", + ); + const frameworkInfoPath = path.join( + frameworkPath, + "Versions", + "Current", + "Resources", + "Info.plist", + ); + const frameworkInfo = await readFrameworkInfo(frameworkInfoPath); + // Update install name + await spawn( + "install_name_tool", + [ + "-id", + `@rpath/${newLibraryName}.framework/${newLibraryName}`, + frameworkInfo.CFBundleExecutable, + ], + { + outputMode: "buffered", + cwd: frameworkPath, + }, + ); + await writeFrameworkInfo(frameworkInfoPath, { + ...frameworkInfo, + CFBundleExecutable: newLibraryName, + }); + // Rename the actual binary + const existingBinaryPath = path.join( + frameworkPath, + frameworkInfo.CFBundleExecutable, + ); + const stat = await fs.promises.lstat(existingBinaryPath); + assert( + stat.isSymbolicLink(), + `Expected binary to be a symlink: ${existingBinaryPath}`, + ); + const realBinaryPath = await fs.promises.realpath(existingBinaryPath); + const newRealBinaryPath = path.join( + path.dirname(realBinaryPath), + newLibraryName, + ); + // Rename the real binary file + await fs.promises.rename(realBinaryPath, newRealBinaryPath); + // Remove the old binary symlink + await fs.promises.unlink(existingBinaryPath); + // Create a new symlink with the new name + const newBinarySymlinkTarget = path.join( + "Versions", + "Current", + newLibraryName, + ); + assert( + fs.existsSync(path.join(frameworkPath, newBinarySymlinkTarget)), + "Expected new binary to exist", + ); + await fs.promises.symlink( + newBinarySymlinkTarget, + path.join(frameworkPath, newLibraryName), + ); + + // Rename the framework directory + await fs.promises.rename( + frameworkPath, + path.join(path.dirname(frameworkPath), `${newLibraryName}.framework`), ); - contents.CFBundleExecutable = newLibraryName; - await fs.promises.writeFile(infoPlistPath, plist.build(contents), "utf-8"); } export async function linkXcframework({ @@ -107,123 +244,68 @@ export async function linkXcframework({ incremental, naming, }: LinkModuleOptions): Promise { + assert.equal( + process.platform, + "darwin", + "Linking Apple addons are only supported on macOS", + ); // Copy the xcframework to the output directory and rename the framework and binary const newLibraryName = getLibraryName(modulePath, naming); const outputPath = getLinkedModuleOutputPath(platform, modulePath, naming); - const tempPath = await fs.promises.mkdtemp( - path.join(os.tmpdir(), `react-native-node-api-${newLibraryName}-`), - ); - try { - if (incremental && fs.existsSync(outputPath)) { - const moduleModified = getLatestMtime(modulePath); - const outputModified = getLatestMtime(outputPath); - if (moduleModified < outputModified) { - return { - originalPath: modulePath, - libraryName: newLibraryName, - outputPath, - skipped: true, - }; - } - } - // Delete any existing xcframework (or xcodebuild will try to amend it) - await fs.promises.rm(outputPath, { recursive: true, force: true }); - await fs.promises.cp(modulePath, tempPath, { recursive: true }); - - // Following extracted function mimics `glob("*/*.framework/")` - function globFrameworkDirs( - startPath: string, - fn: (parentPath: string, name: string) => Promise, - ) { - return fs - .readdirSync(startPath, { withFileTypes: true }) - .filter((tripletEntry) => tripletEntry.isDirectory()) - .flatMap((tripletEntry) => { - const tripletPath = path.join(startPath, tripletEntry.name); - return fs - .readdirSync(tripletPath, { withFileTypes: true }) - .filter( - (frameworkEntry) => - frameworkEntry.isDirectory() && - path.extname(frameworkEntry.name) === ".framework", - ) - .flatMap( - async (frameworkEntry) => - await fn(tripletPath, frameworkEntry.name), - ); - }); - } - const frameworkPaths = await Promise.all( - globFrameworkDirs(tempPath, async (tripletPath, frameworkEntryName) => { - const frameworkPath = path.join(tripletPath, frameworkEntryName); - const oldLibraryName = path.basename(frameworkEntryName, ".framework"); - const oldLibraryPath = path.join(frameworkPath, oldLibraryName); - const newFrameworkPath = path.join( - tripletPath, - `${newLibraryName}.framework`, - ); - const newLibraryPath = path.join(newFrameworkPath, newLibraryName); - assert( - fs.existsSync(oldLibraryPath), - `Expected a library at '${oldLibraryPath}'`, - ); - // Rename the library - await fs.promises.rename( - oldLibraryPath, - // Cannot use newLibraryPath here, because the framework isn't renamed yet - path.join(frameworkPath, newLibraryName), - ); - // Rename the framework - await fs.promises.rename(frameworkPath, newFrameworkPath); - // Expect the library in the new location - assert(fs.existsSync(newLibraryPath)); - // Update the binary - await spawn( - "install_name_tool", - [ - "-id", - `@rpath/${newLibraryName}.framework/${newLibraryName}`, - newLibraryPath, - ], - { - outputMode: "buffered", - }, - ); - // Update the Info.plist file for the framework - await updateInfoPlist({ - frameworkPath: newFrameworkPath, - oldLibraryName, - newLibraryName, - }); - return newFrameworkPath; - }), - ); - - // Create a new xcframework from the renamed frameworks - await spawn( - "xcodebuild", - [ - "-create-xcframework", - ...frameworkPaths.flatMap((frameworkPath) => [ - "-framework", - frameworkPath, - ]), - "-output", + if (incremental && fs.existsSync(outputPath)) { + const moduleModified = getLatestMtime(modulePath); + const outputModified = getLatestMtime(outputPath); + if (moduleModified < outputModified) { + return { + originalPath: modulePath, + libraryName: newLibraryName, outputPath, - ], - { - outputMode: "buffered", - }, - ); - - return { - originalPath: modulePath, - libraryName: newLibraryName, - outputPath, - skipped: false, - }; - } finally { - await fs.promises.rm(tempPath, { recursive: true, force: true }); + skipped: true, + }; + } } + // Delete any existing xcframework (or xcodebuild will try to amend it) + await fs.promises.rm(outputPath, { recursive: true, force: true }); + // Copy the existing xcframework to the output path + await fs.promises.cp(modulePath, outputPath, { + recursive: true, + verbatimSymlinks: true, + }); + + const info = await readXcframeworkInfo(path.join(outputPath, "Info.plist")); + + await Promise.all( + info.AvailableLibraries.map(async (framework) => { + const frameworkPath = path.join( + outputPath, + framework.LibraryIdentifier, + framework.LibraryPath, + ); + await linkFramework({ frameworkPath, newLibraryName }); + }), + ); + + await writeXcframeworkInfo(outputPath, { + ...info, + AvailableLibraries: info.AvailableLibraries.map((library) => { + return { + ...library, + LibraryPath: `${newLibraryName}.framework`, + BinaryPath: `${newLibraryName}.framework/${newLibraryName}`, + }; + }), + }); + + // Delete any leftover "magic file" + await fs.promises.rm(path.join(outputPath, "react-native-node-api-module"), { + force: true, + }); + + return { + originalPath: modulePath, + libraryName: newLibraryName, + outputPath, + skipped: false, + }; } From d6fb7285b44510d20b36ca53de157881ffaf94c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 08:40:27 +0200 Subject: [PATCH 32/82] Version Packages (#266) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/blue-parts-cheer.md | 5 ----- .changeset/brown-breads-glow.md | 7 ------- .changeset/bumpy-things-poke.md | 5 ----- .changeset/cold-showers-arrive.md | 5 ----- .changeset/fresh-frogs-enter.md | 5 ----- .changeset/orange-bananas-obey.md | 5 ----- .changeset/salty-ghosts-work.md | 9 --------- packages/cli-utils/CHANGELOG.md | 7 +++++++ packages/cli-utils/package.json | 2 +- packages/cmake-rn/CHANGELOG.md | 19 +++++++++++++++++++ packages/cmake-rn/package.json | 6 +++--- packages/ferric/CHANGELOG.md | 13 +++++++++++++ packages/ferric/package.json | 6 +++--- packages/gyp-to-cmake/CHANGELOG.md | 12 ++++++++++++ packages/gyp-to-cmake/package.json | 4 ++-- packages/host/CHANGELOG.md | 15 +++++++++++++++ packages/host/package.json | 4 ++-- packages/node-tests/package.json | 2 +- 18 files changed, 78 insertions(+), 53 deletions(-) delete mode 100644 .changeset/blue-parts-cheer.md delete mode 100644 .changeset/brown-breads-glow.md delete mode 100644 .changeset/bumpy-things-poke.md delete mode 100644 .changeset/cold-showers-arrive.md delete mode 100644 .changeset/fresh-frogs-enter.md delete mode 100644 .changeset/orange-bananas-obey.md delete mode 100644 .changeset/salty-ghosts-work.md create mode 100644 packages/cli-utils/CHANGELOG.md diff --git a/.changeset/blue-parts-cheer.md b/.changeset/blue-parts-cheer.md deleted file mode 100644 index 782aa96f..00000000 --- a/.changeset/blue-parts-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Linking Node-API addons for Apple platforms is no longer re-creating Xcframeworks diff --git a/.changeset/brown-breads-glow.md b/.changeset/brown-breads-glow.md deleted file mode 100644 index d888cc4a..00000000 --- a/.changeset/brown-breads-glow.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"cmake-rn": minor -"gyp-to-cmake": minor -"react-native-node-api": minor ---- - -Use of CMake targets producing Apple frameworks instead of free dylibs is now supported diff --git a/.changeset/bumpy-things-poke.md b/.changeset/bumpy-things-poke.md deleted file mode 100644 index dd1de884..00000000 --- a/.changeset/bumpy-things-poke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Fix requireNodeAddon return type diff --git a/.changeset/cold-showers-arrive.md b/.changeset/cold-showers-arrive.md deleted file mode 100644 index de063bcb..00000000 --- a/.changeset/cold-showers-arrive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"cmake-rn": patch ---- - -Filter CMake targets by target name when passed diff --git a/.changeset/fresh-frogs-enter.md b/.changeset/fresh-frogs-enter.md deleted file mode 100644 index 76c39869..00000000 --- a/.changeset/fresh-frogs-enter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"cmake-rn": patch ---- - -Fix expansion of options in --build and --out diff --git a/.changeset/orange-bananas-obey.md b/.changeset/orange-bananas-obey.md deleted file mode 100644 index 68c5aa45..00000000 --- a/.changeset/orange-bananas-obey.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": minor ---- - -Scope is now stripped from package names when renaming libraries while linking diff --git a/.changeset/salty-ghosts-work.md b/.changeset/salty-ghosts-work.md deleted file mode 100644 index 85c5636f..00000000 --- a/.changeset/salty-ghosts-work.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@react-native-node-api/cli-utils": patch -"cmake-rn": patch -"ferric-cli": patch -"gyp-to-cmake": patch -"react-native-node-api": patch ---- - -Refactored moving prettyPath util to CLI utils package diff --git a/packages/cli-utils/CHANGELOG.md b/packages/cli-utils/CHANGELOG.md new file mode 100644 index 00000000..a5290fa7 --- /dev/null +++ b/packages/cli-utils/CHANGELOG.md @@ -0,0 +1,7 @@ +# @react-native-node-api/cli-utils + +## 0.1.1 + +### Patch Changes + +- 5156d35: Refactored moving prettyPath util to CLI utils package diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index bd86f925..af9743b6 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -1,6 +1,6 @@ { "name": "@react-native-node-api/cli-utils", - "version": "0.1.0", + "version": "0.1.1", "description": "Useful utilities for the CLIs in the React Native Node API mono-repo", "type": "module", "main": "dist/index.js", diff --git a/packages/cmake-rn/CHANGELOG.md b/packages/cmake-rn/CHANGELOG.md index b88e0e6a..51f23972 100644 --- a/packages/cmake-rn/CHANGELOG.md +++ b/packages/cmake-rn/CHANGELOG.md @@ -1,5 +1,24 @@ # cmake-rn +## 0.5.0 + +### Minor Changes + +- 5156d35: Use of CMake targets producing Apple frameworks instead of free dylibs is now supported + +### Patch Changes + +- d8e90a8: Filter CMake targets by target name when passed +- 0c3e8ba: Fix expansion of options in --build and --out +- 5156d35: Refactored moving prettyPath util to CLI utils package +- Updated dependencies [acd06f2] +- Updated dependencies [5156d35] +- Updated dependencies [9f1a301] +- Updated dependencies [5016ed2] +- Updated dependencies [5156d35] + - react-native-node-api@0.6.0 + - @react-native-node-api/cli-utils@0.1.1 + ## 0.4.1 ### Patch Changes diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index 107c6962..10cdcae5 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -1,6 +1,6 @@ { "name": "cmake-rn", - "version": "0.4.1", + "version": "0.5.0", "description": "Build React Native Node API modules with CMake", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -24,9 +24,9 @@ "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" }, "dependencies": { - "@react-native-node-api/cli-utils": "0.1.0", + "@react-native-node-api/cli-utils": "0.1.1", "cmake-file-api": "0.1.0", - "react-native-node-api": "0.5.2", + "react-native-node-api": "0.6.0", "zod": "^4.1.11" }, "peerDependencies": { diff --git a/packages/ferric/CHANGELOG.md b/packages/ferric/CHANGELOG.md index 21bbc965..2d6c39d1 100644 --- a/packages/ferric/CHANGELOG.md +++ b/packages/ferric/CHANGELOG.md @@ -1,5 +1,18 @@ # ferric-cli +## 0.3.5 + +### Patch Changes + +- 5156d35: Refactored moving prettyPath util to CLI utils package +- Updated dependencies [acd06f2] +- Updated dependencies [5156d35] +- Updated dependencies [9f1a301] +- Updated dependencies [5016ed2] +- Updated dependencies [5156d35] + - react-native-node-api@0.6.0 + - @react-native-node-api/cli-utils@0.1.1 + ## 0.3.4 ### Patch Changes diff --git a/packages/ferric/package.json b/packages/ferric/package.json index 1e08b5a3..0fef421e 100644 --- a/packages/ferric/package.json +++ b/packages/ferric/package.json @@ -1,6 +1,6 @@ { "name": "ferric-cli", - "version": "0.3.4", + "version": "0.3.5", "description": "Rust Node-API Modules for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -17,7 +17,7 @@ }, "dependencies": { "@napi-rs/cli": "~3.0.3", - "@react-native-node-api/cli-utils": "0.1.0", - "react-native-node-api": "0.5.2" + "@react-native-node-api/cli-utils": "0.1.1", + "react-native-node-api": "0.6.0" } } diff --git a/packages/gyp-to-cmake/CHANGELOG.md b/packages/gyp-to-cmake/CHANGELOG.md index 51ecfba8..9572c06d 100644 --- a/packages/gyp-to-cmake/CHANGELOG.md +++ b/packages/gyp-to-cmake/CHANGELOG.md @@ -1,5 +1,17 @@ # gyp-to-cmake +## 0.4.0 + +### Minor Changes + +- 5156d35: Use of CMake targets producing Apple frameworks instead of free dylibs is now supported + +### Patch Changes + +- 5156d35: Refactored moving prettyPath util to CLI utils package +- Updated dependencies [5156d35] + - @react-native-node-api/cli-utils@0.1.1 + ## 0.3.0 ### Minor Changes diff --git a/packages/gyp-to-cmake/package.json b/packages/gyp-to-cmake/package.json index 415fe62b..cc687164 100644 --- a/packages/gyp-to-cmake/package.json +++ b/packages/gyp-to-cmake/package.json @@ -1,6 +1,6 @@ { "name": "gyp-to-cmake", - "version": "0.3.0", + "version": "0.4.0", "description": "Convert binding.gyp files to CMakeLists.txt", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -22,7 +22,7 @@ "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" }, "dependencies": { - "@react-native-node-api/cli-utils": "0.1.0", + "@react-native-node-api/cli-utils": "0.1.1", "gyp-parser": "^1.0.4", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1" diff --git a/packages/host/CHANGELOG.md b/packages/host/CHANGELOG.md index f721628d..d25e9765 100644 --- a/packages/host/CHANGELOG.md +++ b/packages/host/CHANGELOG.md @@ -1,5 +1,20 @@ # react-native-node-api +## 0.6.0 + +### Minor Changes + +- 5156d35: Use of CMake targets producing Apple frameworks instead of free dylibs is now supported +- 5016ed2: Scope is now stripped from package names when renaming libraries while linking + +### Patch Changes + +- acd06f2: Linking Node-API addons for Apple platforms is no longer re-creating Xcframeworks +- 9f1a301: Fix requireNodeAddon return type +- 5156d35: Refactored moving prettyPath util to CLI utils package +- Updated dependencies [5156d35] + - @react-native-node-api/cli-utils@0.1.1 + ## 0.5.2 ### Patch Changes diff --git a/packages/host/package.json b/packages/host/package.json index 914a7664..36dd2eb0 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -1,6 +1,6 @@ { "name": "react-native-node-api", - "version": "0.5.2", + "version": "0.6.0", "description": "Node-API for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -81,7 +81,7 @@ "license": "MIT", "dependencies": { "@expo/plist": "^0.4.7", - "@react-native-node-api/cli-utils": "0.1.0", + "@react-native-node-api/cli-utils": "0.1.1", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1", "zod": "^4.1.11" diff --git a/packages/node-tests/package.json b/packages/node-tests/package.json index cfdad1a7..89c280c1 100644 --- a/packages/node-tests/package.json +++ b/packages/node-tests/package.json @@ -22,7 +22,7 @@ "cmake-rn": "*", "gyp-to-cmake": "*", "prebuildify": "^6.0.1", - "react-native-node-api": "^0.5.2", + "react-native-node-api": "^0.6.0", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } From bb9a78c4cc2a0463e983ce1698d6605763e624d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 23 Oct 2025 15:20:57 +0200 Subject: [PATCH 33/82] Fix visualizing duplicate library names (#282) * Fix visualizing duplicate links * Remove outdated comment * Add changeset --- .changeset/polite-bikes-stay.md | 5 ++++ packages/host/src/node/cli/link-modules.ts | 25 +++++++--------- packages/host/src/node/cli/program.ts | 12 ++++---- packages/host/src/node/duplicates.ts | 12 -------- packages/host/src/node/path-utils.ts | 35 +++++++++++----------- 5 files changed, 39 insertions(+), 50 deletions(-) create mode 100644 .changeset/polite-bikes-stay.md delete mode 100644 packages/host/src/node/duplicates.ts diff --git a/.changeset/polite-bikes-stay.md b/.changeset/polite-bikes-stay.md new file mode 100644 index 00000000..1a457c7f --- /dev/null +++ b/.changeset/polite-bikes-stay.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Fixed visualizing duplicate library names diff --git a/packages/host/src/node/cli/link-modules.ts b/packages/host/src/node/cli/link-modules.ts index 8b3bf2e0..a2d274f6 100644 --- a/packages/host/src/node/cli/link-modules.ts +++ b/packages/host/src/node/cli/link-modules.ts @@ -11,9 +11,10 @@ import { findNodeApiModulePathsByDependency, getAutolinkPath, getLibraryName, - logModulePaths, + visualizeLibraryMap, NamingStrategy, PlatformName, + getLibraryMap, } from "../path-utils"; export type ModuleLinker = ( @@ -78,9 +79,14 @@ export async function linkModules({ ), ); - if (hasDuplicateLibraryNames(absoluteModulePaths, naming)) { - logModulePaths(absoluteModulePaths, naming); - throw new Error("Found conflicting library names"); + const libraryMap = getLibraryMap(absoluteModulePaths, naming); + const duplicates = new Map( + Array.from(libraryMap.entries()).filter(([, paths]) => paths.length > 1), + ); + + if (duplicates.size > 0) { + const visualized = visualizeLibraryMap(duplicates); + throw new Error("Found conflicting library names:\n" + visualized); } return Promise.all( @@ -133,17 +139,6 @@ export async function pruneLinkedModules( ); } -export function hasDuplicateLibraryNames( - modulePaths: string[], - naming: NamingStrategy, -): boolean { - const libraryNames = modulePaths.map((modulePath) => { - return getLibraryName(modulePath, naming); - }); - const uniqueNames = new Set(libraryNames); - return uniqueNames.size !== libraryNames.length; -} - export function getLinkedModuleOutputPath( platform: PlatformName, modulePath: string, diff --git a/packages/host/src/node/cli/program.ts b/packages/host/src/node/cli/program.ts index 6fd037b3..169ca85b 100644 --- a/packages/host/src/node/cli/program.ts +++ b/packages/host/src/node/cli/program.ts @@ -16,10 +16,11 @@ import { findNodeApiModulePathsByDependency, getAutolinkPath, getLibraryName, - logModulePaths, + visualizeLibraryMap, normalizeModulePath, PlatformName, PLATFORMS, + getLibraryMap, } from "../path-utils"; import { command as vendorHermes } from "./hermes"; @@ -115,10 +116,10 @@ program successText: `Linked ${platformDisplayName} Node-API modules into ${prettyPath( platformOutputPath, )}`, - failText: (error) => + failText: () => `Failed to link ${platformDisplayName} Node-API modules into ${prettyPath( platformOutputPath, - )}: ${error.message}`, + )}`, }, ); @@ -209,14 +210,15 @@ program dependencies, )) { console.log( - chalk.blueBright(dependencyName), + "\n" + chalk.blueBright(dependencyName), "→", prettyPath(dependency.path), ); - logModulePaths( + const libraryMap = getLibraryMap( dependency.modulePaths.map((p) => path.join(dependency.path, p)), { packageName, pathSuffix }, ); + console.log(visualizeLibraryMap(libraryMap)); } } }), diff --git a/packages/host/src/node/duplicates.ts b/packages/host/src/node/duplicates.ts deleted file mode 100644 index 5e8be7f2..00000000 --- a/packages/host/src/node/duplicates.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function findDuplicates(values: string[]) { - const seen = new Set(); - const duplicates = new Set(); - for (const value of values) { - if (seen.has(value)) { - duplicates.add(value); - } else { - seen.add(value); - } - } - return duplicates; -} diff --git a/packages/host/src/node/path-utils.ts b/packages/host/src/node/path-utils.ts index f898d566..60fd0ffa 100644 --- a/packages/host/src/node/path-utils.ts +++ b/packages/host/src/node/path-utils.ts @@ -7,8 +7,6 @@ import { createRequire } from "node:module"; import { chalk, prettyPath } from "@react-native-node-api/cli-utils"; -import { findDuplicates } from "./duplicates"; - // TODO: Change to .apple.node export const PLATFORMS = ["android", "apple"] as const; export type PlatformName = "android" | "apple"; @@ -267,32 +265,33 @@ export function resolvePackageRoot( } } -export function logModulePaths( - modulePaths: string[], - // TODO: Default to iterating and printing for all supported naming strategies - naming: NamingStrategy, -) { - const pathsPerName = new Map(); +/** + * Module paths per library name. + */ +export type LibraryMap = Map; + +export function getLibraryMap(modulePaths: string[], naming: NamingStrategy) { + const result = new Map(); for (const modulePath of modulePaths) { const libraryName = getLibraryName(modulePath, naming); - const existingPaths = pathsPerName.get(libraryName) ?? []; + const existingPaths = result.get(libraryName) ?? []; existingPaths.push(modulePath); - pathsPerName.set(libraryName, existingPaths); + result.set(libraryName, existingPaths); } + return result; +} - const allModulePaths = modulePaths.map((modulePath) => modulePath); - const duplicatePaths = findDuplicates(allModulePaths); - for (const [libraryName, modulePaths] of pathsPerName) { - console.log( +export function visualizeLibraryMap(libraryMap: LibraryMap) { + const result = []; + for (const [libraryName, modulePaths] of libraryMap) { + result.push( chalk.greenBright(`${libraryName}`), ...modulePaths.flatMap((modulePath) => { - const line = duplicatePaths.has(modulePath) - ? chalk.redBright(prettyPath(modulePath)) - : prettyPath(modulePath); - return `\n ↳ ${line}`; + return ` ↳ ${prettyPath(modulePath)}`; }), ); } + return result.join("\n"); } /** From 5c9321bf9a0a75d48277b50bc9091c2867b1480a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 23 Oct 2025 18:06:40 +0200 Subject: [PATCH 34/82] Add `--strip` option to strip debug symbols from outputs (#286) * Refactored getting the NDK path and CMake toolchain into functions * Add strip option to cmake-rn * Locate and call NDK strip tool on Android libraries * Add stripping to Apple libraries * Add changeset --- .changeset/great-kings-clean.md | 5 ++ packages/cmake-rn/src/cli.ts | 6 ++ packages/cmake-rn/src/platforms/android.ts | 81 ++++++++++++++++------ packages/cmake-rn/src/platforms/apple.ts | 17 ++++- 4 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 .changeset/great-kings-clean.md diff --git a/.changeset/great-kings-clean.md b/.changeset/great-kings-clean.md new file mode 100644 index 00000000..d8f7fb75 --- /dev/null +++ b/.changeset/great-kings-clean.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": patch +--- + +Add `--strip` option to strip debug symbols from outputs diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 2639c13d..f2e45f47 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -106,6 +106,11 @@ const targetOption = new Option( "CMake targets to build", ).default([] as string[], "Build all targets of the CMake project"); +const stripOption = new Option( + "--strip", + "Strip debug symbols from the final binaries", +).default(false); + const noAutoLinkOption = new Option( "--no-auto-link", "Don't mark the output as auto-linkable by react-native-node-api", @@ -132,6 +137,7 @@ let program = new Command("cmake-rn") .addOption(defineOption) .addOption(cleanOption) .addOption(targetOption) + .addOption(stripOption) .addOption(noAutoLinkOption) .addOption(noWeakNodeApiLinkageOption) .addOption(cmakeJsOption); diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index 479fdc92..f058802a 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -49,6 +49,44 @@ function getBuildPath(baseBuildPath: string, triplet: Triplet) { return path.join(baseBuildPath, triplet); } +function getNdkPath(ndkVersion: string) { + const { ANDROID_HOME } = process.env; + assert(typeof ANDROID_HOME === "string", "Missing env variable ANDROID_HOME"); + assert( + fs.existsSync(ANDROID_HOME), + `Expected the Android SDK at ${ANDROID_HOME}`, + ); + const installNdkCommand = `sdkmanager --install "ndk;${ndkVersion}"`; + const ndkPath = path.resolve(ANDROID_HOME, "ndk", ndkVersion); + assert( + fs.existsSync(ndkPath), + `Missing Android NDK v${ndkVersion} (at ${ndkPath}) - run: ${installNdkCommand}`, + ); + return ndkPath; +} + +function getNdkToolchainPath(ndkPath: string) { + const toolchainPath = path.join( + ndkPath, + "build/cmake/android.toolchain.cmake", + ); + assert( + fs.existsSync(toolchainPath), + `No CMake toolchain found in ${toolchainPath}`, + ); + return toolchainPath; +} + +function getNdkLlvmBinPath(ndkPath: string) { + const prebuiltPath = path.join(ndkPath, "toolchains/llvm/prebuilt"); + const platforms = fs.readdirSync(prebuiltPath); + assert( + platforms.length === 1, + `Expected a single llvm prebuilt toolchain in ${prebuiltPath}`, + ); + return path.join(prebuiltPath, platforms[0], "bin"); +} + export const platform: Platform = { id: "android", name: "Android", @@ -85,26 +123,8 @@ export const platform: Platform = { cmakeJs, }, ) { - const { ANDROID_HOME } = process.env; - assert( - typeof ANDROID_HOME === "string", - "Missing env variable ANDROID_HOME", - ); - assert( - fs.existsSync(ANDROID_HOME), - `Expected the Android SDK at ${ANDROID_HOME}`, - ); - const installNdkCommand = `sdkmanager --install "ndk;${ndkVersion}"`; - const ndkPath = path.resolve(ANDROID_HOME, "ndk", ndkVersion); - assert( - fs.existsSync(ndkPath), - `Missing Android NDK v${ndkVersion} (at ${ndkPath}) - run: ${installNdkCommand}`, - ); - - const toolchainPath = path.join( - ndkPath, - "build/cmake/android.toolchain.cmake", - ); + const ndkPath = getNdkPath(ndkVersion); + const toolchainPath = getNdkToolchainPath(ndkPath); const commonDefinitions = [ ...define, @@ -174,14 +194,14 @@ export const platform: Platform = { async postBuild( outputPath, triplets, - { autoLink, configuration, target, build }, + { autoLink, configuration, target, build, strip, ndkVersion }, ) { const prebuilds: Record< string, { triplet: Triplet; libraryPath: string }[] > = {}; - for (const { triplet } of triplets) { + for (const { spawn, triplet } of triplets) { const buildPath = getBuildPath(build, triplet); assert(fs.existsSync(buildPath), `Expected a directory at ${buildPath}`); const targets = await cmakeFileApi.readCurrentTargetsDeep( @@ -210,9 +230,24 @@ export const platform: Platform = { if (!(sharedLibrary.name in prebuilds)) { prebuilds[sharedLibrary.name] = []; } + const libraryPath = path.join(buildPath, artifact.path); + assert( + fs.existsSync(libraryPath), + `Expected built library at ${libraryPath}`, + ); + + if (strip) { + const llvmBinPath = getNdkLlvmBinPath(getNdkPath(ndkVersion)); + const stripToolPath = path.join(llvmBinPath, `llvm-strip`); + assert( + fs.existsSync(stripToolPath), + `Expected llvm-strip to exist at ${stripToolPath}`, + ); + await spawn(stripToolPath, [libraryPath]); + } prebuilds[sharedLibrary.name].push({ triplet, - libraryPath: path.join(buildPath, artifact.path), + libraryPath, }); } diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index 6edc6065..3c09ab18 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -330,11 +330,11 @@ export const platform: Platform = { async postBuild( outputPath, triplets, - { configuration, autoLink, xcframeworkExtension, target, build }, + { configuration, autoLink, xcframeworkExtension, target, build, strip }, ) { const libraryNames = new Set(); const frameworkPaths: string[] = []; - for (const { triplet } of triplets) { + for (const { spawn, triplet } of triplets) { const buildPath = getBuildPath(build, triplet); assert(fs.existsSync(buildPath), `Expected a directory at ${buildPath}`); const sharedLibrary = await readCmakeSharedLibraryTarget( @@ -348,10 +348,21 @@ export const platform: Platform = { "Expected exactly one artifact", ); const [artifact] = artifacts; + + const artifactPath = path.join(buildPath, artifact.path); + + if (strip) { + // -r: All relocation entries. + // -S: All symbol table entries. + // -T: All text relocation entries. + // -x: All local symbols. + await spawn("strip", ["-rSTx", artifactPath]); + } + libraryNames.add(sharedLibrary.name); // Locate the path of the framework, if a free dynamic library was built if (artifact.path.includes(".framework/")) { - frameworkPaths.push(path.dirname(path.join(buildPath, artifact.path))); + frameworkPaths.push(path.dirname(artifactPath)); } else { const libraryName = path.basename( artifact.path, From 5c3de897ee0e172eebad37c2aa11ac95dbc0b46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 23 Oct 2025 22:23:34 +0200 Subject: [PATCH 35/82] Enable "RelWithDebInfo" and "MinSizeRel" configurations and support Apple debug symbols when building and linking (#284) * Add RelWithDebInfo and MinSizeRel configurations * Locate and include debug symbols when creating an Xcframework * Use RelWithDebInfo when building examples * Use dsymutil to rebuild DWARF debug symbols * Add changesets * Include config in Android build directory names * Add MinSizeRel and RelWithDebInfo to list of path prefix candidates in babel plugin --- .changeset/flat-suits-cross.md | 5 ++ .changeset/hot-poems-attack.md | 5 ++ .changeset/light-buttons-leave.md | 5 ++ packages/cmake-rn/src/cli.ts | 3 +- packages/cmake-rn/src/platforms/android.ts | 22 ++++--- packages/host/src/node/cli/apple.ts | 59 +++++++++++++++---- packages/host/src/node/path-utils.ts | 6 ++ packages/host/src/node/prebuilds/apple.ts | 14 ++++- .../scripts/build-examples.mts | 13 ++-- 9 files changed, 102 insertions(+), 30 deletions(-) create mode 100644 .changeset/flat-suits-cross.md create mode 100644 .changeset/hot-poems-attack.md create mode 100644 .changeset/light-buttons-leave.md diff --git a/.changeset/flat-suits-cross.md b/.changeset/flat-suits-cross.md new file mode 100644 index 00000000..2b1535cd --- /dev/null +++ b/.changeset/flat-suits-cross.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Rebuild any dSYM directory when linking frameworks. diff --git a/.changeset/hot-poems-attack.md b/.changeset/hot-poems-attack.md new file mode 100644 index 00000000..dee83154 --- /dev/null +++ b/.changeset/hot-poems-attack.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": patch +--- + +Locate and include debug symbols when creating an Xcframework. diff --git a/.changeset/light-buttons-leave.md b/.changeset/light-buttons-leave.md new file mode 100644 index 00000000..ff0767e0 --- /dev/null +++ b/.changeset/light-buttons-leave.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": patch +--- + +Allow passing "RelWithDebInfo" and "MinSizeRel" as --configuration diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index f2e45f47..0ddc2dd7 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -36,9 +36,8 @@ const sourcePathOption = new Option( "Specify the source directory containing a CMakeLists.txt file", ).default(process.cwd()); -// TODO: Add "MinSizeRel" and "RelWithDebInfo" const configurationOption = new Option("--configuration ") - .choices(["Release", "Debug"] as const) + .choices(["Release", "Debug", "RelWithDebInfo", "MinSizeRel"] as const) .default("Release"); // TODO: Derive default build triplets diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index f058802a..baceb452 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -13,7 +13,7 @@ import { } from "react-native-node-api"; import * as cmakeFileApi from "cmake-file-api"; -import type { Platform } from "./types.js"; +import type { BaseOpts, Platform } from "./types.js"; import { toDefineArguments } from "../helpers.js"; import { getCmakeJSVariables, @@ -45,8 +45,12 @@ const androidSdkVersionOption = new Option( type AndroidOpts = { ndkVersion: string; androidSdkVersion: string }; -function getBuildPath(baseBuildPath: string, triplet: Triplet) { - return path.join(baseBuildPath, triplet); +function getBuildPath( + baseBuildPath: string, + triplet: Triplet, + configuration: BaseOpts["configuration"], +) { + return path.join(baseBuildPath, triplet + "-" + configuration); } function getNdkPath(ndkVersion: string) { @@ -147,7 +151,7 @@ export const platform: Platform = { await Promise.all( triplets.map(async ({ triplet, spawn }) => { - const buildPath = getBuildPath(build, triplet); + const buildPath = getBuildPath(build, triplet, configuration); const outputPath = path.join(buildPath, "out"); // We want to use the CMake File API to query information later await cmakeFileApi.createSharedStatelessQuery( @@ -161,6 +165,8 @@ export const platform: Platform = { source, "-B", buildPath, + // Ideally, we would use the "Ninja Multi-Config" generator here, + // but it doesn't support the "RelWithDebInfo" configuration on Android. "-G", "Ninja", "--toolchain", @@ -179,8 +185,8 @@ export const platform: Platform = { }), ); }, - async build({ triplet, spawn }, { target, build }) { - const buildPath = getBuildPath(build, triplet); + async build({ triplet, spawn }, { target, build, configuration }) { + const buildPath = getBuildPath(build, triplet, configuration); await spawn("cmake", [ "--build", buildPath, @@ -201,8 +207,8 @@ export const platform: Platform = { { triplet: Triplet; libraryPath: string }[] > = {}; - for (const { spawn, triplet } of triplets) { - const buildPath = getBuildPath(build, triplet); + for (const { triplet, spawn } of triplets) { + const buildPath = getBuildPath(build, triplet, configuration); assert(fs.existsSync(buildPath), `Expected a directory at ${buildPath}`); const targets = await cmakeFileApi.readCurrentTargetsDeep( buildPath, diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index 0ccbd82a..c5eec200 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -59,6 +59,7 @@ const XcframeworkInfoSchema = zod.looseObject({ BinaryPath: zod.string(), LibraryIdentifier: zod.string(), LibraryPath: zod.string(), + DebugSymbolsPath: zod.string().optional(), }), ), CFBundlePackageType: zod.literal("XFWK"), @@ -100,13 +101,12 @@ export async function writeFrameworkInfo( type LinkFrameworkOptions = { frameworkPath: string; + debugSymbolsPath?: string; newLibraryName: string; }; -export async function linkFramework({ - frameworkPath, - newLibraryName, -}: LinkFrameworkOptions) { +export async function linkFramework(options: LinkFrameworkOptions) { + const { frameworkPath } = options; assert.equal( process.platform, "darwin", @@ -117,14 +117,15 @@ export async function linkFramework({ `Expected framework at '${frameworkPath}'`, ); if (fs.existsSync(path.join(frameworkPath, "Versions"))) { - await linkVersionedFramework({ frameworkPath, newLibraryName }); + await linkVersionedFramework(options); } else { - await linkFlatFramework({ frameworkPath, newLibraryName }); + await linkFlatFramework(options); } } export async function linkFlatFramework({ frameworkPath, + debugSymbolsPath, newLibraryName, }: LinkFrameworkOptions) { assert.equal( @@ -151,16 +152,44 @@ export async function linkFlatFramework({ ...frameworkInfo, CFBundleExecutable: newLibraryName, }); + // Rename the actual binary await fs.promises.rename( path.join(frameworkPath, frameworkInfo.CFBundleExecutable), path.join(frameworkPath, newLibraryName), ); // Rename the framework directory - await fs.promises.rename( - frameworkPath, - path.join(path.dirname(frameworkPath), `${newLibraryName}.framework`), + const newFrameworkPath = path.join( + path.dirname(frameworkPath), + `${newLibraryName}.framework`, ); + await fs.promises.rename(frameworkPath, newFrameworkPath); + + if (debugSymbolsPath) { + const frameworkDebugSymbolsPath = path.join( + debugSymbolsPath, + `${path.basename(frameworkPath)}.dSYM`, + ); + if (fs.existsSync(frameworkDebugSymbolsPath)) { + // Remove existing DWARF data + await fs.promises.rm(frameworkDebugSymbolsPath, { + recursive: true, + force: true, + }); + // Rebuild DWARF data + await spawn( + "dsymutil", + [ + path.join(newFrameworkPath, newLibraryName), + "-o", + path.join(debugSymbolsPath, newLibraryName + ".dSYM"), + ], + { + outputMode: "buffered", + }, + ); + } + } } export async function linkVersionedFramework({ @@ -282,7 +311,17 @@ export async function linkXcframework({ framework.LibraryIdentifier, framework.LibraryPath, ); - await linkFramework({ frameworkPath, newLibraryName }); + await linkFramework({ + frameworkPath, + newLibraryName, + debugSymbolsPath: framework.DebugSymbolsPath + ? path.join( + outputPath, + framework.LibraryIdentifier, + framework.DebugSymbolsPath, + ) + : undefined, + }); }), ); diff --git a/packages/host/src/node/path-utils.ts b/packages/host/src/node/path-utils.ts index 60fd0ffa..a2a954a6 100644 --- a/packages/host/src/node/path-utils.ts +++ b/packages/host/src/node/path-utils.ts @@ -521,11 +521,17 @@ export function getLatestMtime(fromPath: string): number { // https://github.com/TooTallNate/node-bindings/blob/v1.3.0/bindings.js#L21 const nodeBindingsSubdirs = [ "./", + "./build/MinSizeRel", + "./build/RelWithDebInfo", "./build/Release", "./build/Debug", "./build", + "./out/MinSizeRel", + "./out/RelWithDebInfo", "./out/Release", "./out/Debug", + "./MinSizeRel", + "./RelWithDebInfo", "./Release", "./Debug", ]; diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index dce6353b..22be74fa 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -99,7 +99,19 @@ export async function createXCframework({ "xcodebuild", [ "-create-xcframework", - ...frameworkPaths.flatMap((p) => ["-framework", p]), + ...frameworkPaths.flatMap((frameworkPath) => { + const debugSymbolPath = frameworkPath + ".dSYM"; + if (fs.existsSync(debugSymbolPath)) { + return [ + "-framework", + frameworkPath, + "-debug-symbols", + debugSymbolPath, + ]; + } else { + return ["-framework", frameworkPath]; + } + }), "-output", xcodeOutputPath, ], diff --git a/packages/node-addon-examples/scripts/build-examples.mts b/packages/node-addon-examples/scripts/build-examples.mts index fd590eb6..bc447e71 100644 --- a/packages/node-addon-examples/scripts/build-examples.mts +++ b/packages/node-addon-examples/scripts/build-examples.mts @@ -6,14 +6,9 @@ const projectDirectories = findCMakeProjects(); for (const projectDirectory of projectDirectories) { console.log(`Running "cmake-rn" in ${projectDirectory}`); - execSync( - "cmake-rn", - // "cmake-rn --android --apple", - // "cmake-rn --triplet aarch64-linux-android --triplet arm64-apple-ios-sim", - { - cwd: projectDirectory, - stdio: "inherit", - }, - ); + execSync("cmake-rn --configuration RelWithDebInfo", { + cwd: projectDirectory, + stdio: "inherit", + }); console.log(); } From 2ddc21d21ca50cc5ca47c3afebb6cc4d1ee24104 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:31:42 +0200 Subject: [PATCH 36/82] Version Packages (#285) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/flat-suits-cross.md | 5 ----- .changeset/great-kings-clean.md | 5 ----- .changeset/hot-poems-attack.md | 5 ----- .changeset/light-buttons-leave.md | 5 ----- .changeset/polite-bikes-stay.md | 5 ----- packages/cmake-rn/CHANGELOG.md | 11 +++++++++++ packages/cmake-rn/package.json | 4 ++-- packages/ferric/CHANGELOG.md | 8 ++++++++ packages/ferric/package.json | 4 ++-- packages/host/CHANGELOG.md | 7 +++++++ packages/host/package.json | 2 +- packages/node-tests/package.json | 2 +- 12 files changed, 32 insertions(+), 31 deletions(-) delete mode 100644 .changeset/flat-suits-cross.md delete mode 100644 .changeset/great-kings-clean.md delete mode 100644 .changeset/hot-poems-attack.md delete mode 100644 .changeset/light-buttons-leave.md delete mode 100644 .changeset/polite-bikes-stay.md diff --git a/.changeset/flat-suits-cross.md b/.changeset/flat-suits-cross.md deleted file mode 100644 index 2b1535cd..00000000 --- a/.changeset/flat-suits-cross.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Rebuild any dSYM directory when linking frameworks. diff --git a/.changeset/great-kings-clean.md b/.changeset/great-kings-clean.md deleted file mode 100644 index d8f7fb75..00000000 --- a/.changeset/great-kings-clean.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"cmake-rn": patch ---- - -Add `--strip` option to strip debug symbols from outputs diff --git a/.changeset/hot-poems-attack.md b/.changeset/hot-poems-attack.md deleted file mode 100644 index dee83154..00000000 --- a/.changeset/hot-poems-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"cmake-rn": patch ---- - -Locate and include debug symbols when creating an Xcframework. diff --git a/.changeset/light-buttons-leave.md b/.changeset/light-buttons-leave.md deleted file mode 100644 index ff0767e0..00000000 --- a/.changeset/light-buttons-leave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"cmake-rn": patch ---- - -Allow passing "RelWithDebInfo" and "MinSizeRel" as --configuration diff --git a/.changeset/polite-bikes-stay.md b/.changeset/polite-bikes-stay.md deleted file mode 100644 index 1a457c7f..00000000 --- a/.changeset/polite-bikes-stay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Fixed visualizing duplicate library names diff --git a/packages/cmake-rn/CHANGELOG.md b/packages/cmake-rn/CHANGELOG.md index 51f23972..7a1bc4e2 100644 --- a/packages/cmake-rn/CHANGELOG.md +++ b/packages/cmake-rn/CHANGELOG.md @@ -1,5 +1,16 @@ # cmake-rn +## 0.5.1 + +### Patch Changes + +- 5c9321b: Add `--strip` option to strip debug symbols from outputs +- 5c3de89: Locate and include debug symbols when creating an Xcframework. +- 5c3de89: Allow passing "RelWithDebInfo" and "MinSizeRel" as --configuration +- Updated dependencies [5c3de89] +- Updated dependencies [bb9a78c] + - react-native-node-api@0.6.1 + ## 0.5.0 ### Minor Changes diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index 10cdcae5..d534d60d 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -1,6 +1,6 @@ { "name": "cmake-rn", - "version": "0.5.0", + "version": "0.5.1", "description": "Build React Native Node API modules with CMake", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -26,7 +26,7 @@ "dependencies": { "@react-native-node-api/cli-utils": "0.1.1", "cmake-file-api": "0.1.0", - "react-native-node-api": "0.6.0", + "react-native-node-api": "0.6.1", "zod": "^4.1.11" }, "peerDependencies": { diff --git a/packages/ferric/CHANGELOG.md b/packages/ferric/CHANGELOG.md index 2d6c39d1..fd58cc73 100644 --- a/packages/ferric/CHANGELOG.md +++ b/packages/ferric/CHANGELOG.md @@ -1,5 +1,13 @@ # ferric-cli +## 0.3.6 + +### Patch Changes + +- Updated dependencies [5c3de89] +- Updated dependencies [bb9a78c] + - react-native-node-api@0.6.1 + ## 0.3.5 ### Patch Changes diff --git a/packages/ferric/package.json b/packages/ferric/package.json index 0fef421e..4cb3f87e 100644 --- a/packages/ferric/package.json +++ b/packages/ferric/package.json @@ -1,6 +1,6 @@ { "name": "ferric-cli", - "version": "0.3.5", + "version": "0.3.6", "description": "Rust Node-API Modules for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -18,6 +18,6 @@ "dependencies": { "@napi-rs/cli": "~3.0.3", "@react-native-node-api/cli-utils": "0.1.1", - "react-native-node-api": "0.6.0" + "react-native-node-api": "0.6.1" } } diff --git a/packages/host/CHANGELOG.md b/packages/host/CHANGELOG.md index d25e9765..188c5d3f 100644 --- a/packages/host/CHANGELOG.md +++ b/packages/host/CHANGELOG.md @@ -1,5 +1,12 @@ # react-native-node-api +## 0.6.1 + +### Patch Changes + +- 5c3de89: Rebuild any dSYM directory when linking frameworks. +- bb9a78c: Fixed visualizing duplicate library names + ## 0.6.0 ### Minor Changes diff --git a/packages/host/package.json b/packages/host/package.json index 36dd2eb0..87118caf 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -1,6 +1,6 @@ { "name": "react-native-node-api", - "version": "0.6.0", + "version": "0.6.1", "description": "Node-API for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { diff --git a/packages/node-tests/package.json b/packages/node-tests/package.json index 89c280c1..eec9e5ed 100644 --- a/packages/node-tests/package.json +++ b/packages/node-tests/package.json @@ -22,7 +22,7 @@ "cmake-rn": "*", "gyp-to-cmake": "*", "prebuildify": "^6.0.1", - "react-native-node-api": "^0.6.0", + "react-native-node-api": "^0.6.1", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } From f00d2bb8f6484a520bfeb62620f5bc151a44002b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 24 Oct 2025 11:06:32 +0200 Subject: [PATCH 37/82] Add license file and update contributors --- LICENSE.md | 21 +++++++++++++++++++++ package.json | 8 ++++++++ 2 files changed, 29 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..bf17f03d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025-present, Callstack and React Native Node API contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package.json b/package.json index d0189f61..eb2f9dff 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,14 @@ { "name": "Jamie Birch", "url": "https://github.com/shirakaba" + }, + { + "name": "Mariusz Pasiński", + "url": "https://github.com/mani3xis" + }, + { + "name": "Kamil Paradowski", + "url": "https://github.com/paradowstack" } ], "license": "MIT", From 7536c6cf234e56f80d5f54a6c1846ca47546a2ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 26 Oct 2025 21:16:28 +0100 Subject: [PATCH 38/82] Add a `--react-native-package` option to `vendor-hermes` command (#287) * Add --react-native-package option to "vendor-hermes" command * Allow overriding REACT_NATIVE_OVERRIDE_HERMES_DIR externally --- .changeset/chatty-states-build.md | 5 +++++ packages/host/scripts/patch-hermes.rb | 23 +++++++++++++------- packages/host/src/node/cli/hermes.ts | 31 +++++++++++++++++++++------ 3 files changed, 45 insertions(+), 14 deletions(-) create mode 100644 .changeset/chatty-states-build.md diff --git a/.changeset/chatty-states-build.md b/.changeset/chatty-states-build.md new file mode 100644 index 00000000..1be4d07f --- /dev/null +++ b/.changeset/chatty-states-build.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Add --react-native-package option to "vendor-hermes" command, allowing caller to choose the package to download hermes into diff --git a/packages/host/scripts/patch-hermes.rb b/packages/host/scripts/patch-hermes.rb index 9e1faaf5..a6cb11f7 100644 --- a/packages/host/scripts/patch-hermes.rb +++ b/packages/host/scripts/patch-hermes.rb @@ -4,13 +4,20 @@ raise "React Native Node-API cannot reliably patch JSI when React Native Core is prebuilt." end -VENDORED_HERMES_DIR ||= `npx react-native-node-api vendor-hermes --silent '#{Pod::Config.instance.installation_root}'`.strip -if Dir.exist?(VENDORED_HERMES_DIR) - Pod::UI.info "Hermes vendored into #{VENDORED_HERMES_DIR.inspect}" -else - raise "Hermes patching failed. Please check the output above for errors." +if ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].nil? + VENDORED_HERMES_DIR ||= `npx react-native-node-api vendor-hermes --silent '#{Pod::Config.instance.installation_root}'`.strip + # Signal the patched Hermes to React Native + ENV['BUILD_FROM_SOURCE'] = 'true' + ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'] = VENDORED_HERMES_DIR +elsif Dir.exist?(ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR']) + # Setting an override path implies building from source + ENV['BUILD_FROM_SOURCE'] = 'true' end -# Signal the patched Hermes to React Native -ENV['BUILD_FROM_SOURCE'] = 'true' -ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'] = VENDORED_HERMES_DIR +if !ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].empty? + if Dir.exist?(ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR']) + Pod::UI.info "[Node-API] Using overridden Hermes in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}" + else + raise "Hermes patching failed: Expected override to exist in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}" + end +end diff --git a/packages/host/src/node/cli/hermes.ts b/packages/host/src/node/cli/hermes.ts index 541a324e..6bd3daa3 100644 --- a/packages/host/src/node/cli/hermes.ts +++ b/packages/host/src/node/cli/hermes.ts @@ -5,18 +5,24 @@ import path from "node:path"; import { chalk, Command, + Option, oraPromise, spawn, UsageError, wrapAction, prettyPath, } from "@react-native-node-api/cli-utils"; -import { packageDirectorySync } from "pkg-dir"; +import { packageDirectory } from "pkg-dir"; +import { readPackage } from "read-pkg"; -const HOST_PACKAGE_ROOT = path.resolve(__dirname, "../../.."); // FIXME: make this configurable with reasonable fallback before public release const HERMES_GIT_URL = "https://github.com/kraenhansen/hermes.git"; +const platformOption = new Option( + "--react-native-package ", + "The React Native package to vendor Hermes into", +).default("react-native"); + export const command = new Command("vendor-hermes") .argument("[from]", "Path to a file inside the app package", process.cwd()) .option("--silent", "Don't print anything except the final path", false) @@ -25,12 +31,20 @@ export const command = new Command("vendor-hermes") "Don't check timestamps of input files to skip unnecessary rebuilds", false, ) + .addOption(platformOption) .action( - wrapAction(async (from, { force, silent }) => { - const appPackageRoot = packageDirectorySync({ cwd: from }); + wrapAction(async (from, { force, silent, reactNativePackage }) => { + const appPackageRoot = await packageDirectory({ cwd: from }); assert(appPackageRoot, "Failed to find package root"); + + const { dependencies = {} } = await readPackage({ cwd: appPackageRoot }); + assert( + Object.keys(dependencies).includes(reactNativePackage), + `Expected app to have a dependency on the '${reactNativePackage}' package`, + ); + const reactNativePath = path.dirname( - require.resolve("react-native/package.json", { + require.resolve(reactNativePackage + "/package.json", { // Ensures we'll be patching the React Native package actually used by the app paths: [appPackageRoot], }), @@ -40,6 +54,11 @@ export const command = new Command("vendor-hermes") "sdks", ".hermesversion", ); + assert( + fs.existsSync(hermesVersionPath), + `Expected a file with a Hermes version at ${prettyPath(hermesVersionPath)}`, + ); + const hermesVersion = fs.readFileSync(hermesVersionPath, "utf8").trim(); if (!silent) { console.log(`Using Hermes version: ${hermesVersion}`); @@ -50,7 +69,7 @@ export const command = new Command("vendor-hermes") "ReactCommon/jsi/jsi/", ); - const hermesPath = path.join(HOST_PACKAGE_ROOT, "hermes"); + const hermesPath = path.join(reactNativePath, "sdks", "node-api-hermes"); if (force && fs.existsSync(hermesPath)) { await oraPromise( fs.promises.rm(hermesPath, { recursive: true, force: true }), From 07ea9dc02eec117e80ce9eca54f0131ab492dd8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 26 Oct 2025 22:50:42 +0100 Subject: [PATCH 39/82] Add x86_64 and universal simulator slices (#288) * Delete unused APPLE_ARCHITECTURES from host package * Add x86_64 and universal simulator triplets to host * Provide per-triplet constants * Drive-by delete unused createPlistContent * Declare support for relevant triplets * Support multiple purposes when picking default triplets * Add changeset * Incorporate review * Update Ferric to link against the universal iOS simulator framework * Update CI to build universal ios sim framework * Renamed purpose to mode and values to be more semantic * Assert triplet platform is supported by host --- .changeset/bright-parts-roll.md | 6 ++ .github/workflows/check.yml | 2 +- packages/cmake-rn/src/cli.ts | 11 ++- packages/cmake-rn/src/platforms/android.ts | 20 ++-- packages/cmake-rn/src/platforms/apple.ts | 98 +++++++++++++++----- packages/cmake-rn/src/platforms/types.ts | 13 ++- packages/ferric/src/cargo.ts | 2 +- packages/host/src/node/prebuilds/apple.ts | 16 ---- packages/host/src/node/prebuilds/triplets.ts | 13 ++- 9 files changed, 124 insertions(+), 57 deletions(-) create mode 100644 .changeset/bright-parts-roll.md diff --git a/.changeset/bright-parts-roll.md b/.changeset/bright-parts-roll.md new file mode 100644 index 00000000..4d27aef2 --- /dev/null +++ b/.changeset/bright-parts-roll.md @@ -0,0 +1,6 @@ +--- +"cmake-rn": patch +"react-native-node-api": patch +--- + +Add x86_64 and universal simulator triplets diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 373ac614..1955f9d2 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -98,7 +98,7 @@ jobs: - run: npm ci - run: npm run bootstrap env: - CMAKE_RN_TRIPLETS: arm64-apple-ios-sim + CMAKE_RN_TRIPLETS: arm64;x86_64-apple-ios-sim FERRIC_TARGETS: aarch64-apple-ios-sim - run: npm run pod-install working-directory: apps/test-app diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 0ddc2dd7..a96279ed 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -187,7 +187,7 @@ program = program.action( for (const platform of Object.values(platforms)) { // Forcing the types a bit here, since the platform id option is dynamically added if ((baseOptions as Record)[platform.id]) { - for (const triplet of platform.triplets) { + for (const triplet of await platform.defaultTriplets("all")) { triplets.add(triplet); } } @@ -196,7 +196,9 @@ program = program.action( if (triplets.size === 0) { for (const platform of Object.values(platforms)) { if (platform.isSupportedByHost()) { - for (const triplet of await platform.defaultTriplets()) { + for (const triplet of await platform.defaultTriplets( + "current-development", + )) { triplets.add(triplet); } } @@ -217,6 +219,11 @@ program = program.action( const tripletContexts = [...triplets].map((triplet) => { const platform = findPlatformForTriplet(triplet); + assert( + platform.isSupportedByHost(), + `Triplet '${triplet}' cannot be built, as the '${platform.name}' platform is not supported on a '${process.platform}' host.`, + ); + return { triplet, platform, diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index baceb452..4e505ca8 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -100,13 +100,21 @@ export const platform: Platform = { "i686-linux-android", "x86_64-linux-android", ], - defaultTriplets() { - if (process.arch === "arm64") { - return ["aarch64-linux-android"]; - } else if (process.arch === "x64") { - return ["x86_64-linux-android"]; + defaultTriplets(mode) { + if (mode === "all") { + return [...this.triplets]; + } else if (mode === "current-development") { + // We're applying a heuristic to determine the current simulators + // TODO: Run a command to probe the currently running emulators instead + if (process.arch === "arm64") { + return ["aarch64-linux-android"]; + } else if (process.arch === "x64") { + return ["x86_64-linux-android"]; + } else { + return []; + } } else { - return []; + throw new Error(`Unexpected mode: ${mode as string}`); } }, amendCommand(command) { diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index 3c09ab18..8f3de38a 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -62,13 +62,22 @@ const XCODE_SDK_NAMES = { "x86_64-apple-darwin": "macosx", "arm64-apple-darwin": "macosx", "arm64;x86_64-apple-darwin": "macosx", + "arm64-apple-ios": "iphoneos", "arm64-apple-ios-sim": "iphonesimulator", - "arm64-apple-tvos": "appletvos", + "x86_64-apple-ios-sim": "iphonesimulator", + "arm64;x86_64-apple-ios-sim": "iphonesimulator", + // "x86_64-apple-tvos": "appletvos", + "arm64-apple-tvos": "appletvos", + "x86_64-apple-tvos-sim": "appletvsimulator", "arm64-apple-tvos-sim": "appletvsimulator", + "arm64;x86_64-apple-tvos-sim": "appletvsimulator", + "arm64-apple-visionos": "xros", "arm64-apple-visionos-sim": "xrsimulator", + "x86_64-apple-visionos-sim": "xrsimulator", + "arm64;x86_64-apple-visionos-sim": "xrsimulator", } satisfies Record; type CMakeSystemName = "Darwin" | "iOS" | "tvOS" | "watchOS" | "visionOS"; @@ -77,27 +86,44 @@ const CMAKE_SYSTEM_NAMES = { "x86_64-apple-darwin": "Darwin", "arm64-apple-darwin": "Darwin", "arm64;x86_64-apple-darwin": "Darwin", + "arm64-apple-ios": "iOS", "arm64-apple-ios-sim": "iOS", - "arm64-apple-tvos": "tvOS", + "x86_64-apple-ios-sim": "iOS", + "arm64;x86_64-apple-ios-sim": "iOS", + // "x86_64-apple-tvos": "appletvos", + "arm64-apple-tvos": "tvOS", "arm64-apple-tvos-sim": "tvOS", + "x86_64-apple-tvos-sim": "tvOS", + "arm64;x86_64-apple-tvos-sim": "tvOS", + "arm64-apple-visionos": "visionOS", + "x86_64-apple-visionos-sim": "visionOS", "arm64-apple-visionos-sim": "visionOS", + "arm64;x86_64-apple-visionos-sim": "visionOS", } satisfies Record; const DESTINATION_BY_TRIPLET = { + "x86_64-apple-darwin": "generic/platform=macOS", + "arm64-apple-darwin": "generic/platform=macOS", + "arm64;x86_64-apple-darwin": "generic/platform=macOS", + "arm64-apple-ios": "generic/platform=iOS", "arm64-apple-ios-sim": "generic/platform=iOS Simulator", + "x86_64-apple-ios-sim": "generic/platform=iOS Simulator", + "arm64;x86_64-apple-ios-sim": "generic/platform=iOS Simulator", + "arm64-apple-tvos": "generic/platform=tvOS", // "x86_64-apple-tvos": "generic/platform=tvOS", + "x86_64-apple-tvos-sim": "generic/platform=tvOS Simulator", "arm64-apple-tvos-sim": "generic/platform=tvOS Simulator", + "arm64;x86_64-apple-tvos-sim": "generic/platform=tvOS Simulator", + "arm64-apple-visionos": "generic/platform=visionOS", "arm64-apple-visionos-sim": "generic/platform=visionOS Simulator", - // TODO: Verify that the three following destinations are correct and actually work - "x86_64-apple-darwin": "generic/platform=macOS,arch=x86_64", - "arm64-apple-darwin": "generic/platform=macOS,arch=arm64", - "arm64;x86_64-apple-darwin": "generic/platform=macOS", + "x86_64-apple-visionos-sim": "generic/platform=visionOS Simulator", + "arm64;x86_64-apple-visionos-sim": "generic/platform=visionOS Simulator", } satisfies Record; type AppleArchitecture = "arm64" | "x86_64" | "arm64;x86_64"; @@ -106,30 +132,24 @@ export const APPLE_ARCHITECTURES = { "x86_64-apple-darwin": "x86_64", "arm64-apple-darwin": "arm64", "arm64;x86_64-apple-darwin": "arm64;x86_64", + "arm64-apple-ios": "arm64", "arm64-apple-ios-sim": "arm64", - "arm64-apple-tvos": "arm64", + "x86_64-apple-ios-sim": "x86_64", + "arm64;x86_64-apple-ios-sim": "arm64;x86_64", + // "x86_64-apple-tvos": "x86_64", + "arm64-apple-tvos": "arm64", "arm64-apple-tvos-sim": "arm64", + "x86_64-apple-tvos-sim": "x86_64", + "arm64;x86_64-apple-tvos-sim": "arm64;x86_64", + "arm64-apple-visionos": "arm64", + "x86_64-apple-visionos-sim": "x86_64", "arm64-apple-visionos-sim": "arm64", + "arm64;x86_64-apple-visionos-sim": "arm64;x86_64", } satisfies Record; -export function createPlistContent(values: Record) { - return [ - '', - '', - '', - "", - ...Object.entries(values).flatMap(([key, value]) => [ - `${key}`, - `${value}`, - ]), - "", - "", - ].join("\n"); -} - const xcframeworkExtensionOption = new Option( "--xcframework-extension", "Don't rename the xcframework to .apple.node", @@ -171,16 +191,46 @@ export const platform: Platform = { id: "apple", name: "Apple", triplets: [ + "arm64-apple-darwin", + "x86_64-apple-darwin", "arm64;x86_64-apple-darwin", + "arm64-apple-ios", "arm64-apple-ios-sim", + "x86_64-apple-ios-sim", + "arm64;x86_64-apple-ios-sim", + "arm64-apple-tvos", + "x86_64-apple-tvos-sim", "arm64-apple-tvos-sim", + "arm64;x86_64-apple-tvos-sim", + "arm64-apple-visionos", + "x86_64-apple-visionos-sim", "arm64-apple-visionos-sim", + "arm64;x86_64-apple-visionos-sim", ], - defaultTriplets() { - return process.arch === "arm64" ? ["arm64-apple-ios-sim"] : []; + defaultTriplets(mode) { + if (mode === "all") { + return [ + "arm64;x86_64-apple-darwin", + + "arm64-apple-ios", + "arm64;x86_64-apple-ios-sim", + + "arm64-apple-tvos", + "arm64;x86_64-apple-tvos-sim", + + "arm64-apple-visionos", + "arm64;x86_64-apple-visionos-sim", + ]; + } else if (mode === "current-development") { + // We're applying a heuristic to determine the current simulators + // TODO: Run a command to probe the currently running simulators instead + return ["arm64;x86_64-apple-ios-sim"]; + } else { + throw new Error(`Unexpected mode: ${mode as string}`); + } }, amendCommand(command) { return command.addOption(xcframeworkExtensionOption); diff --git a/packages/cmake-rn/src/platforms/types.ts b/packages/cmake-rn/src/platforms/types.ts index d3cd9b9f..6944d4bf 100644 --- a/packages/cmake-rn/src/platforms/types.ts +++ b/packages/cmake-rn/src/platforms/types.ts @@ -33,6 +33,7 @@ export type Platform< Triplets extends string[] = string[], Opts extends cli.OptionValues = Record, Command = ExtendedCommand, + Triplet extends string = Triplets[number], > = { /** * Used to identify the platform in the CLI. @@ -47,9 +48,11 @@ export type Platform< */ triplets: Readonly; /** - * Get the limited subset of triplets that should be built by default for this platform, to support a development workflow. + * Get the limited subset of triplets that should be built by default for this platform. */ - defaultTriplets(): Triplets[number][] | Promise; + defaultTriplets( + mode: "current-development" | "all", + ): Triplet[] | Promise; /** * Implement this to add any platform specific options to the command. */ @@ -62,7 +65,7 @@ export type Platform< * Configure all projects for this platform. */ configure( - triplets: TripletContext[], + triplets: TripletContext[], options: BaseOpts & Opts, spawn: Spawn, ): Promise; @@ -70,7 +73,7 @@ export type Platform< * Platform specific command to build a triplet project. */ build( - context: TripletContext, + context: TripletContext, options: BaseOpts & Opts, ): Promise; /** @@ -81,7 +84,7 @@ export type Platform< * Location of the final prebuilt artefact. */ outputPath: string, - triplets: TripletContext[], + triplets: TripletContext[], options: BaseOpts & Opts, ): Promise; }; diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index edc88481..b4a06f16 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -22,7 +22,7 @@ const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record = { "aarch64-apple-darwin": "macos-arm64_x86_64", // Universal "x86_64-apple-darwin": "macos-arm64_x86_64", // Universal "aarch64-apple-ios": "ios-arm64", - "aarch64-apple-ios-sim": "ios-arm64-simulator", + "aarch64-apple-ios-sim": "ios-arm64_x86_64-simulator", // Universal // "aarch64-apple-ios-macabi": "", // Catalyst // "x86_64-apple-ios": "ios-x86_64", // "x86_64-apple-ios-macabi": "ios-x86_64-simulator", diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index 22be74fa..9054965e 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -6,24 +6,8 @@ import os from "node:os"; import plist from "@expo/plist"; import { spawn } from "@react-native-node-api/cli-utils"; -import { AppleTriplet } from "./triplets.js"; import { determineLibraryBasename } from "../path-utils.js"; -type AppleArchitecture = "arm64" | "x86_64" | "arm64;x86_64"; - -export const APPLE_ARCHITECTURES = { - "x86_64-apple-darwin": "x86_64", - "arm64-apple-darwin": "arm64", - "arm64;x86_64-apple-darwin": "arm64;x86_64", - "arm64-apple-ios": "arm64", - "arm64-apple-ios-sim": "arm64", - "arm64-apple-tvos": "arm64", - // "x86_64-apple-tvos": "x86_64", - "arm64-apple-tvos-sim": "arm64", - "arm64-apple-visionos": "arm64", - "arm64-apple-visionos-sim": "arm64", -} satisfies Record; - type XCframeworkOptions = { frameworkPaths: string[]; outputPath: string; diff --git a/packages/host/src/node/prebuilds/triplets.ts b/packages/host/src/node/prebuilds/triplets.ts index 7471b15b..1d361c0a 100644 --- a/packages/host/src/node/prebuilds/triplets.ts +++ b/packages/host/src/node/prebuilds/triplets.ts @@ -11,16 +11,25 @@ export const ANDROID_TRIPLETS = [ export type AndroidTriplet = (typeof ANDROID_TRIPLETS)[number]; export const APPLE_TRIPLETS = [ - "arm64;x86_64-apple-darwin", "x86_64-apple-darwin", "arm64-apple-darwin", + "arm64;x86_64-apple-darwin", + "arm64-apple-ios", + "x86_64-apple-ios-sim", "arm64-apple-ios-sim", + "arm64;x86_64-apple-ios-sim", + "arm64-apple-tvos", - "arm64-apple-tvos-sim", // "x86_64-apple-tvos", + "x86_64-apple-tvos-sim", + "arm64-apple-tvos-sim", + "arm64;x86_64-apple-tvos-sim", + "arm64-apple-visionos", + "x86_64-apple-visionos-sim", "arm64-apple-visionos-sim", + "arm64;x86_64-apple-visionos-sim", ] as const; export type AppleTriplet = (typeof APPLE_TRIPLETS)[number]; From 9411a8c5f9b8d6beab72d80b54e45ba8e85e8ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 26 Oct 2025 23:23:03 +0100 Subject: [PATCH 40/82] Ferric x86 iOS simulator (#292) * Make the build command default * Refactor creation of universal apple libraries and add support for ios libraries too * Assert weak node api framework before passing it to Rust * Assert paths passed when creating a universal library * Add the x86_64-apple-ios target * Add changesets --- .changeset/better-pets-help.md | 5 +++ .changeset/large-hornets-burn.md | 5 +++ packages/ferric/src/build.ts | 52 ++++++++++++++++------- packages/ferric/src/cargo.ts | 8 +++- packages/ferric/src/program.ts | 2 +- packages/ferric/src/targets.ts | 4 +- packages/host/src/node/prebuilds/apple.ts | 8 +++- 7 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 .changeset/better-pets-help.md create mode 100644 .changeset/large-hornets-burn.md diff --git a/.changeset/better-pets-help.md b/.changeset/better-pets-help.md new file mode 100644 index 00000000..904acf2c --- /dev/null +++ b/.changeset/better-pets-help.md @@ -0,0 +1,5 @@ +--- +"ferric-cli": patch +--- + +Add x86_64 ios simulator target and output universal libraries for iOS simulators. diff --git a/.changeset/large-hornets-burn.md b/.changeset/large-hornets-burn.md new file mode 100644 index 00000000..c7ad76f4 --- /dev/null +++ b/.changeset/large-hornets-burn.md @@ -0,0 +1,5 @@ +--- +"ferric-cli": patch +--- + +It's no longer required to pass "build" to ferric, as this is default now diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index a3eb1684..4dd26d87 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -311,34 +311,54 @@ export const buildCommand = new Command("build") ), ); +async function createUniversalAppleLibraries(libraryPathGroups: string[][]) { + const result = await oraPromise( + Promise.all( + libraryPathGroups.map(async (libraryPaths) => { + if (libraryPaths.length === 0) { + return []; + } else if (libraryPaths.length === 1) { + return libraryPaths; + } else { + return [await createUniversalAppleLibrary(libraryPaths)]; + } + }), + ), + { + text: "Combining arch-specific libraries into universal libraries", + successText: "Combined arch-specific libraries into universal libraries", + failText: (error) => + `Failed to combine arch-specific libraries: ${error.message}`, + }, + ); + return result.flat(); +} + async function combineLibraries( libraries: Readonly<[AppleTargetName, string]>[], ): Promise { const result = []; const darwinLibraries = []; + const iosSimulatorLibraries = []; for (const [target, libraryPath] of libraries) { if (target.endsWith("-darwin")) { darwinLibraries.push(libraryPath); + } else if ( + target === "aarch64-apple-ios-sim" || + target === "x86_64-apple-ios" // Simulator despite name missing -sim suffix + ) { + iosSimulatorLibraries.push(libraryPath); } else { result.push(libraryPath); } } - if (darwinLibraries.length === 0) { - return result; - } else if (darwinLibraries.length === 1) { - return [...result, darwinLibraries[0]]; - } else { - const universalPath = await oraPromise( - createUniversalAppleLibrary(darwinLibraries), - { - text: "Combining Darwin libraries into a universal library", - successText: "Combined Darwin libraries into a universal library", - failText: (error) => - `Failed to combine Darwin libraries: ${error.message}`, - }, - ); - return [...result, universalPath]; - } + + const combinedLibraryPaths = await createUniversalAppleLibraries([ + darwinLibraries, + iosSimulatorLibraries, + ]); + + return [...result, ...combinedLibraryPaths]; } export function isAndroidSupported() { diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index b4a06f16..7b665b98 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -21,10 +21,12 @@ import { const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record = { "aarch64-apple-darwin": "macos-arm64_x86_64", // Universal "x86_64-apple-darwin": "macos-arm64_x86_64", // Universal + "aarch64-apple-ios": "ios-arm64", "aarch64-apple-ios-sim": "ios-arm64_x86_64-simulator", // Universal + "x86_64-apple-ios": "ios-arm64_x86_64-simulator", // Universal + // "aarch64-apple-ios-macabi": "", // Catalyst - // "x86_64-apple-ios": "ios-x86_64", // "x86_64-apple-ios-macabi": "ios-x86_64-simulator", // "aarch64-apple-tvos": "tvos-arm64", // "aarch64-apple-tvos-sim": "tvos-arm64-simulator", @@ -216,6 +218,10 @@ export function getTargetEnvironmentVariables({ }; } else if (isAppleTarget(target)) { const weakNodeApiFrameworkPath = getWeakNodeApiFrameworkPath(target); + assert( + fs.existsSync(weakNodeApiFrameworkPath), + `Expected weak-node-api framework at ${weakNodeApiFrameworkPath}`, + ); return { CARGO_ENCODED_RUSTFLAGS: [ "-L", diff --git a/packages/ferric/src/program.ts b/packages/ferric/src/program.ts index 422c4ecc..d47d0479 100644 --- a/packages/ferric/src/program.ts +++ b/packages/ferric/src/program.ts @@ -6,4 +6,4 @@ import { buildCommand } from "./build.js"; export const program = new Command("ferric") .hook("preAction", () => printBanner()) .description("Rust Node-API Modules for React Native") - .addCommand(buildCommand); + .addCommand(buildCommand, { isDefault: true }); diff --git a/packages/ferric/src/targets.ts b/packages/ferric/src/targets.ts index b7696a10..0e548de6 100644 --- a/packages/ferric/src/targets.ts +++ b/packages/ferric/src/targets.ts @@ -16,10 +16,12 @@ export type AndroidTargetName = (typeof ANDROID_TARGETS)[number]; export const APPLE_TARGETS = [ "aarch64-apple-darwin", "x86_64-apple-darwin", + "aarch64-apple-ios", "aarch64-apple-ios-sim", + "x86_64-apple-ios", // Simulator (despite the missing -sim suffix) + // "aarch64-apple-ios-macabi", // Catalyst - // "x86_64-apple-ios", // "x86_64-apple-ios-macabi", // Catalyst // TODO: Re-enabled these when we know how to install them 🙈 diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index 9054965e..1aeb9658 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -131,11 +131,17 @@ export function determineXCFrameworkFilename( } export async function createUniversalAppleLibrary(libraryPaths: string[]) { + assert( + libraryPaths.length > 0, + "Expected at least one library to create a universal library", + ); // Determine the output path const filenames = new Set(libraryPaths.map((p) => path.basename(p))); assert( filenames.size === 1, - "Expected all darwin libraries to have the same name", + `Expected libraries to have the same name, but got: ${[...filenames].join( + ", ", + )}`, ); const [filename] = filenames; const lipoParentPath = fs.realpathSync( From 8398f76ec7420144dce8479a4d90331f411098d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 27 Oct 2025 18:29:57 +0100 Subject: [PATCH 41/82] Add visionOS and tvOS triplets to Ferric (#294) * Refactored "ensureInstalledTargets" into "ensureAvailableTargets" * Assert nightly toolchain * Add visionos target * Add job to test building Ferric Apple triplets * Add tvos targets * Rename "build-weak-node-api:all-triplets" to "build-weak-node-api:all" and add ":apple" and ":android" scripts * Refactor scripts into a common "prepare-weak-node-api" script * Add missing targets --- .github/workflows/check.yml | 51 +++++++++++++++++- packages/ferric/src/build.ts | 11 +++- packages/ferric/src/cargo.ts | 22 ++++++-- packages/ferric/src/targets.ts | 96 ++++++++++++++++++++++++++-------- packages/host/package.json | 9 ++-- 5 files changed, 157 insertions(+), 32 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1955f9d2..ec9c7cee 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -152,7 +152,7 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - name: Build weak-node-api for all architectures - run: npm run build-weak-node-api -- --android + run: npm run build-weak-node-api:android working-directory: packages/host - name: Build ferric-example for all architectures run: npm run build -- --android @@ -188,3 +188,52 @@ jobs: with: name: emulator-logcat path: apps/test-app/emulator-logcat.txt + test-ferric-apple-triplets: + if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Ferric 🦀') + name: Test ferric Apple triplets + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/jod + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: "17" + distribution: "temurin" + - run: rustup target add x86_64-apple-darwin x86_64-apple-ios aarch64-apple-ios aarch64-apple-ios-sim + - run: rustup toolchain install nightly --component rust-src + - run: npm ci + - run: npm run build + # Build weak-node-api for all Apple architectures + - run: | + npm run prepare-weak-node-api + npm run build-weak-node-api:apple + working-directory: packages/host + # Build Ferric example for all Apple architectures + - run: npx ferric --apple + working-directory: packages/ferric-example + - name: Inspect the structure of the prebuilt binary + run: lipo -info ferric_example.apple.node/*/libferric_example.framework/libferric_example > lipo-info.txt + working-directory: packages/ferric-example + - name: Upload lipo info + uses: actions/upload-artifact@v4 + with: + name: lipo-info + path: packages/ferric-example/lipo-info.txt + - name: Verify Apple triplet builds + run: | + # Create expected fixture content + cat > expected-lipo-info.txt << 'EOF' + Architectures in the fat file: ferric_example.apple.node/ios-arm64_x86_64-simulator/libferric_example.framework/libferric_example are: x86_64 arm64 + Architectures in the fat file: ferric_example.apple.node/macos-arm64_x86_64/libferric_example.framework/libferric_example are: x86_64 arm64 + Architectures in the fat file: ferric_example.apple.node/tvos-arm64_x86_64-simulator/libferric_example.framework/libferric_example are: x86_64 arm64 + Non-fat file: ferric_example.apple.node/ios-arm64/libferric_example.framework/libferric_example is architecture: arm64 + Non-fat file: ferric_example.apple.node/tvos-arm64/libferric_example.framework/libferric_example is architecture: arm64 + Non-fat file: ferric_example.apple.node/xros-arm64-simulator/libferric_example.framework/libferric_example is architecture: arm64 + Non-fat file: ferric_example.apple.node/xros-arm64/libferric_example.framework/libferric_example is architecture: arm64 + EOF + # Compare with expected fixture (will fail if files differ) + diff expected-lipo-info.txt lipo-info.txt + working-directory: packages/ferric-example diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index 4dd26d87..6587347e 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -29,7 +29,7 @@ import { AndroidTargetName, APPLE_TARGETS, AppleTargetName, - ensureInstalledTargets, + ensureAvailableTargets, filterTargetsByPlatform, } from "./targets.js"; import { generateTypeScriptDeclarations } from "./napi-rs.js"; @@ -164,7 +164,7 @@ export const buildCommand = new Command("build") ); } ensureCargo(); - ensureInstalledTargets(targets); + ensureAvailableTargets(targets); const appleTargets = filterTargetsByPlatform(targets, "apple"); const androidTargets = filterTargetsByPlatform(targets, "android"); @@ -340,6 +340,7 @@ async function combineLibraries( const result = []; const darwinLibraries = []; const iosSimulatorLibraries = []; + const tvosSimulatorLibraries = []; for (const [target, libraryPath] of libraries) { if (target.endsWith("-darwin")) { darwinLibraries.push(libraryPath); @@ -348,6 +349,11 @@ async function combineLibraries( target === "x86_64-apple-ios" // Simulator despite name missing -sim suffix ) { iosSimulatorLibraries.push(libraryPath); + } else if ( + target === "aarch64-apple-tvos-sim" || + target === "x86_64-apple-tvos" // Simulator despite name missing -sim suffix + ) { + tvosSimulatorLibraries.push(libraryPath); } else { result.push(libraryPath); } @@ -356,6 +362,7 @@ async function combineLibraries( const combinedLibraryPaths = await createUniversalAppleLibraries([ darwinLibraries, iosSimulatorLibraries, + tvosSimulatorLibraries, ]); return [...result, ...combinedLibraryPaths]; diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index 7b665b98..9afa0770 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -16,6 +16,7 @@ import { AppleTargetName, isAndroidTarget, isAppleTarget, + isThirdTierTarget, } from "./targets.js"; const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record = { @@ -26,12 +27,17 @@ const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record = { "aarch64-apple-ios-sim": "ios-arm64_x86_64-simulator", // Universal "x86_64-apple-ios": "ios-arm64_x86_64-simulator", // Universal + "aarch64-apple-visionos": "xros-arm64", + "aarch64-apple-visionos-sim": "xros-arm64_x86_64-simulator", // Universal + // The x86_64 target for vision simulator isn't supported + // see https://doc.rust-lang.org/rustc/platform-support.html + + "aarch64-apple-tvos": "tvos-arm64", + "aarch64-apple-tvos-sim": "tvos-arm64_x86_64-simulator", + "x86_64-apple-tvos": "tvos-arm64_x86_64-simulator", + // "aarch64-apple-ios-macabi": "", // Catalyst // "x86_64-apple-ios-macabi": "ios-x86_64-simulator", - // "aarch64-apple-tvos": "tvos-arm64", - // "aarch64-apple-tvos-sim": "tvos-arm64-simulator", - // "aarch64-apple-visionos": "xros-arm64", - // "aarch64-apple-visionos-sim": "xros-arm64-simulator", }; const ANDROID_ARCH_PR_TARGET: Record = { @@ -84,6 +90,14 @@ export async function build(options: BuildOptions) { if (configuration.toLowerCase() === "release") { args.push("--release"); } + if (isThirdTierTarget(target)) { + // Use the nightly toolchain for third tier targets + args.splice(0, 0, "+nightly"); + // Passing the nightly "build-std" to + // > Enable Cargo to compile the standard library itself as part of a crate graph compilation + // See https://doc.rust-lang.org/rustc/platform-support/apple-visionos.html#building-the-target + args.push("-Z", "build-std=std,panic_abort"); + } await spawn("cargo", args, { outputMode: "buffered", env: { diff --git a/packages/ferric/src/targets.ts b/packages/ferric/src/targets.ts index 0e548de6..513c27e1 100644 --- a/packages/ferric/src/targets.ts +++ b/packages/ferric/src/targets.ts @@ -1,4 +1,6 @@ -import { chalk, UsageError } from "@react-native-node-api/cli-utils"; +import cp from "node:child_process"; + +import { assertFixable } from "@react-native-node-api/cli-utils"; import { getInstalledTargets } from "./rustup.js"; export const ANDROID_TARGETS = [ @@ -24,25 +26,23 @@ export const APPLE_TARGETS = [ // "aarch64-apple-ios-macabi", // Catalyst // "x86_64-apple-ios-macabi", // Catalyst - // TODO: Re-enabled these when we know how to install them 🙈 - /* - "aarch64-apple-tvos", - "aarch64-apple-tvos-sim", "aarch64-apple-visionos", "aarch64-apple-visionos-sim", - */ + + "aarch64-apple-tvos", + // "arm64e-apple-tvos", + "aarch64-apple-tvos-sim", + "x86_64-apple-tvos", // Simulator (despite the missing -sim suffix) // "aarch64-apple-watchos", // "aarch64-apple-watchos-sim", // "arm64_32-apple-watchos", // "arm64e-apple-darwin", // "arm64e-apple-ios", - // "arm64e-apple-tvos", // "armv7k-apple-watchos", // "armv7s-apple-ios", // "i386-apple-ios", // "i686-apple-darwin", - // "x86_64-apple-tvos", // "x86_64-apple-watchos-sim", // "x86_64h-apple-darwin", ] as const; @@ -51,24 +51,72 @@ export type AppleTargetName = (typeof APPLE_TARGETS)[number]; export const ALL_TARGETS = [...ANDROID_TARGETS, ...APPLE_TARGETS] as const; export type TargetName = (typeof ALL_TARGETS)[number]; +const THIRD_TIER_TARGETS: Set = new Set([ + "aarch64-apple-visionos", + "aarch64-apple-visionos-sim", + + "aarch64-apple-tvos", + "aarch64-apple-tvos-sim", + "x86_64-apple-tvos", +]); + +export function assertNightlyToolchain() { + const toolchainLines = cp + .execFileSync("rustup", ["toolchain", "list"], { + encoding: "utf-8", + }) + .split("\n"); + + const nightlyLines = toolchainLines.filter((line) => + line.startsWith("nightly-"), + ); + assertFixable( + nightlyLines.length > 0, + "You need to use a nightly Rust toolchain", + { + command: "rustup toolchain install nightly --component rust-src", + }, + ); + + const componentLines = cp + .execFileSync("rustup", ["component", "list", "--toolchain", "nightly"], { + encoding: "utf-8", + }) + .split("\n"); + assertFixable( + componentLines.some((line) => line === "rust-src (installed)"), + "You need to install the rust-src component for the nightly Rust toolchain", + { + command: "rustup toolchain install nightly --component rust-src", + }, + ); +} + /** - * Ensure the targets are installed into the Rust toolchain + * Ensure the targets are either installed into the Rust toolchain or available via nightly Rust toolchain. * We do this up-front because the error message and fix is very unclear from the failure when missing. */ -export function ensureInstalledTargets(expectedTargets: Set) { +export function ensureAvailableTargets(expectedTargets: Set) { const installedTargets = getInstalledTargets(); - const missingTargets = new Set([ - ...[...expectedTargets].filter((target) => !installedTargets.has(target)), - ]); - if (missingTargets.size > 0) { - // TODO: Ask the user if they want to run this - throw new UsageError( - `You're missing ${ - missingTargets.size - } targets - to fix this, run:\n\n${chalk.italic( - `rustup target add ${[...missingTargets].join(" ")}`, - )}`, - ); + + const missingInstallableTargets = expectedTargets + .difference(installedTargets) + .difference(THIRD_TIER_TARGETS); + + assertFixable( + missingInstallableTargets.size === 0, + `You need to add these targets to your toolchain: ${[ + ...missingInstallableTargets, + ].join(", ")}`, + { + command: `rustup target add ${[...missingInstallableTargets].join(" ")}`, + }, + ); + + const expectedThirdTierTargets = + expectedTargets.intersection(THIRD_TIER_TARGETS); + if (expectedThirdTierTargets.size > 0) { + assertNightlyToolchain(); } } @@ -82,6 +130,10 @@ export function isAppleTarget(target: TargetName): target is AppleTargetName { return APPLE_TARGETS.includes(target as (typeof APPLE_TARGETS)[number]); } +export function isThirdTierTarget(target: TargetName): boolean { + return THIRD_TIER_TARGETS.has(target); +} + export function filterTargetsByPlatform( targets: Set, platform: "android", diff --git a/packages/host/package.json b/packages/host/package.json index 87118caf..c12e5963 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -46,12 +46,15 @@ "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", "generate-weak-node-api": "tsx scripts/generate-weak-node-api.ts", "generate-weak-node-api-injector": "tsx scripts/generate-weak-node-api-injector.ts", + "prepare-weak-node-api": "node --run copy-node-api-headers && node --run generate-weak-node-api-injector && node --run generate-weak-node-api", "build-weak-node-api": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension --source ./weak-node-api --out ./weak-node-api", - "build-weak-node-api:all-triplets": "cmake-rn --android --apple --no-auto-link --no-weak-node-api-linkage --xcframework-extension --source ./weak-node-api --out ./weak-node-api", + "build-weak-node-api:android": "node --run build-weak-node-api -- --android", + "build-weak-node-api:apple": "node --run build-weak-node-api -- --apple", + "build-weak-node-api:all": "node --run build-weak-node-api -- --android --apple", "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", "test:gradle": "ENABLE_GRADLE_TESTS=true node --run test", - "bootstrap": "node --run copy-node-api-headers && node --run generate-weak-node-api-injector && node --run generate-weak-node-api && node --run build-weak-node-api", - "prerelease": "node --run copy-node-api-headers && node --run generate-weak-node-api-injector && node --run generate-weak-node-api && node --run build-weak-node-api:all-triplets" + "bootstrap": "node --run prepare-weak-node-api && node --run build-weak-node-api", + "prerelease": "node --run prepare-weak-node-api && node --run build-weak-node-api:all" }, "keywords": [ "react-native", From b66117676d111b9f9c94ea7db93dafa899b5ebcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 27 Oct 2025 18:31:43 +0100 Subject: [PATCH 42/82] Add changeset --- .changeset/mighty-regions-clean.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mighty-regions-clean.md diff --git a/.changeset/mighty-regions-clean.md b/.changeset/mighty-regions-clean.md new file mode 100644 index 00000000..eae04696 --- /dev/null +++ b/.changeset/mighty-regions-clean.md @@ -0,0 +1,5 @@ +--- +"ferric-cli": patch +--- + +Add support for visionOS and tvOS targets From bdc172e5e944a8fff6770e0ba816aaf95d454563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 27 Oct 2025 22:34:43 +0100 Subject: [PATCH 43/82] Add explicit support for React Native v0.79.7 --- .changeset/tame-bugs-shave.md | 5 +++++ packages/host/package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/tame-bugs-shave.md diff --git a/.changeset/tame-bugs-shave.md b/.changeset/tame-bugs-shave.md new file mode 100644 index 00000000..c8181e01 --- /dev/null +++ b/.changeset/tame-bugs-shave.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Add explicit support for React Native v0.79.7 diff --git a/packages/host/package.json b/packages/host/package.json index c12e5963..18246b53 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -97,6 +97,6 @@ }, "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.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4" + "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" } } From 4672e01384d5a6462bc29177568c44fc01e96a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 2 Nov 2025 13:02:47 +0100 Subject: [PATCH 44/82] Warn on "pod install" with the new architecture disabled (#300) --- .changeset/wicked-tables-deny.md | 5 +++++ packages/host/react-native-node-api.podspec | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 .changeset/wicked-tables-deny.md diff --git a/.changeset/wicked-tables-deny.md b/.changeset/wicked-tables-deny.md new file mode 100644 index 00000000..a1db092b --- /dev/null +++ b/.changeset/wicked-tables-deny.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Warn on "pod install" with the new architecture disabled diff --git a/packages/host/react-native-node-api.podspec b/packages/host/react-native-node-api.podspec index 5066e82d..74107aab 100644 --- a/packages/host/react-native-node-api.podspec +++ b/packages/host/react-native-node-api.podspec @@ -17,6 +17,10 @@ unless defined?(@xcframeworks_copied) @xcframeworks_copied = true end +if ENV['RCT_NEW_ARCH_ENABLED'] == '0' + Pod::UI.warn "React Native Node-API doesn't support the legacy architecture (but RCT_NEW_ARCH_ENABLED == '0')" +end + Pod::Spec.new do |s| s.name = package["name"] s.version = package["version"] From 51eacadc06ea4db7323085e118b611be128bab72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 28 Oct 2025 14:58:09 +0100 Subject: [PATCH 45/82] Commit update from bootstrapping --- packages/node-addon-examples/tests/async/CMakeLists.txt | 6 +++--- packages/node-addon-examples/tests/buffers/CMakeLists.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/node-addon-examples/tests/async/CMakeLists.txt b/packages/node-addon-examples/tests/async/CMakeLists.txt index 659e3461..2b6b2b81 100644 --- a/packages/node-addon-examples/tests/async/CMakeLists.txt +++ b/packages/node-addon-examples/tests/async/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15...3.31) -project(async_test) +project(async-test) include(${WEAK_NODE_API_CONFIG}) @@ -10,12 +10,12 @@ option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON) if(APPLE AND BUILD_APPLE_FRAMEWORK) set_target_properties(addon PROPERTIES FRAMEWORK TRUE - MACOSX_FRAMEWORK_IDENTIFIER async_test.addon + MACOSX_FRAMEWORK_IDENTIFIER async-test.addon MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 XCODE_ATTRIBUTE_SKIP_INSTALL NO ) -elseif(APPLE) +else() set_target_properties(addon PROPERTIES PREFIX "" SUFFIX .node diff --git a/packages/node-addon-examples/tests/buffers/CMakeLists.txt b/packages/node-addon-examples/tests/buffers/CMakeLists.txt index bca19bce..8d7ac2d2 100644 --- a/packages/node-addon-examples/tests/buffers/CMakeLists.txt +++ b/packages/node-addon-examples/tests/buffers/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15...3.31) -project(buffers_test) +project(buffers-test) include(${WEAK_NODE_API_CONFIG}) @@ -10,12 +10,12 @@ option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON) if(APPLE AND BUILD_APPLE_FRAMEWORK) set_target_properties(addon PROPERTIES FRAMEWORK TRUE - MACOSX_FRAMEWORK_IDENTIFIER buffers_test.addon + MACOSX_FRAMEWORK_IDENTIFIER buffers-test.addon MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 XCODE_ATTRIBUTE_SKIP_INSTALL NO ) -elseif(APPLE) +else() set_target_properties(addon PROPERTIES PREFIX "" SUFFIX .node From c6986981ed9cf66d65b58bb7db6dbd736ccb6449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 2 Nov 2025 13:55:49 +0100 Subject: [PATCH 46/82] Move and simplify Apple host TurboModule (#301) * Moved "ios" into a shared "apple" directory * Simplify Apple host module provider * Add changeset --- .changeset/sad-poets-smoke.md | 5 +++ .../host/apple/NodeApiHostModuleProvider.mm | 21 +++++++++ .../host/ios/NodeApiHostModuleProvider.mm | 44 ------------------- packages/host/react-native-node-api.podspec | 2 +- 4 files changed, 27 insertions(+), 45 deletions(-) create mode 100644 .changeset/sad-poets-smoke.md create mode 100644 packages/host/apple/NodeApiHostModuleProvider.mm delete mode 100644 packages/host/ios/NodeApiHostModuleProvider.mm diff --git a/.changeset/sad-poets-smoke.md b/.changeset/sad-poets-smoke.md new file mode 100644 index 00000000..8c5c1990 --- /dev/null +++ b/.changeset/sad-poets-smoke.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Moved and simplify Apple host TurboModule diff --git a/packages/host/apple/NodeApiHostModuleProvider.mm b/packages/host/apple/NodeApiHostModuleProvider.mm new file mode 100644 index 00000000..b01c5306 --- /dev/null +++ b/packages/host/apple/NodeApiHostModuleProvider.mm @@ -0,0 +1,21 @@ +#import "CxxNodeApiHostModule.hpp" +#import "WeakNodeApiInjector.hpp" + +#import +@interface NodeApiHost : NSObject + +@end + +@implementation NodeApiHost ++ (void)load { + callstack::nodeapihost::injectIntoWeakNodeApi(); + + facebook::react::registerCxxModuleToGlobalModuleMap( + callstack::nodeapihost::CxxNodeApiHostModule::kModuleName, + [](std::shared_ptr jsInvoker) { + return std::make_shared( + jsInvoker); + }); +} + +@end \ No newline at end of file diff --git a/packages/host/ios/NodeApiHostModuleProvider.mm b/packages/host/ios/NodeApiHostModuleProvider.mm deleted file mode 100644 index d4ecd94f..00000000 --- a/packages/host/ios/NodeApiHostModuleProvider.mm +++ /dev/null @@ -1,44 +0,0 @@ -#import "CxxNodeApiHostModule.hpp" -#import "WeakNodeApiInjector.hpp" - -#define USE_CXX_TURBO_MODULE_UTILS 0 -#if defined(__has_include) -#if __has_include() -#undef USE_CXX_TURBO_MODULE_UTILS -#define USE_CXX_TURBO_MODULE_UTILS 1 -#endif -#endif - -#if USE_CXX_TURBO_MODULE_UTILS -#import -@interface NodeApiHost : NSObject -#else -#import -@interface NodeApiHost : NSObject -#endif // USE_CXX_TURBO_MODULE_UTILS - -@end - -@implementation NodeApiHost -#if USE_CXX_TURBO_MODULE_UTILS -+ (void)load { - callstack::nodeapihost::injectIntoWeakNodeApi(); - - facebook::react::registerCxxModuleToGlobalModuleMap( - callstack::nodeapihost::CxxNodeApiHostModule::kModuleName, - [](std::shared_ptr jsInvoker) { - return std::make_shared( - jsInvoker); - }); -} -#else -RCT_EXPORT_MODULE() - -- (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared( - params.jsInvoker); -} -#endif // USE_CXX_TURBO_MODULE_UTILS - -@end \ No newline at end of file diff --git a/packages/host/react-native-node-api.podspec b/packages/host/react-native-node-api.podspec index 74107aab..13e941b7 100644 --- a/packages/host/react-native-node-api.podspec +++ b/packages/host/react-native-node-api.podspec @@ -32,7 +32,7 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://github.com/callstackincubator/react-native-node-api.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "weak-node-api/include/*.h", "weak-node-api/*.hpp" + s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "weak-node-api/include/*.h", "weak-node-api/*.hpp" s.public_header_files = "weak-node-api/include/*.h" s.vendored_frameworks = "auto-linked/apple/*.xcframework", "weak-node-api/weak-node-api.xcframework" From e9ca2aa88c1e01e6b8430d738d2d055d27666eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 2 Nov 2025 16:36:34 +0100 Subject: [PATCH 47/82] Restore Apple framework symlinks (#302) * Set up files in private packages * Reconstruct missing symbolic links if needed * Restore weak-node-api symlinks * Improve robustness of restoreFrameworkLinks By not expecting the current version name to be "A" * Add tests for restoreFrameworkLinks --- package-lock.json | 2 + packages/ferric-example/package.json | 8 +- packages/host/src/node/cli/apple.test.ts | 104 ++++++++++++++++++ packages/host/src/node/cli/apple.ts | 51 +++++++++ packages/host/src/node/cli/program.ts | 36 +++++- packages/node-addon-examples/.gitignore | 1 + packages/node-addon-examples/package.json | 10 ++ packages/node-addon-examples/tests/.gitignore | 1 - packages/node-tests/package.json | 7 ++ 9 files changed, 217 insertions(+), 3 deletions(-) delete mode 100644 packages/node-addon-examples/tests/.gitignore diff --git a/package-lock.json b/package-lock.json index ccc7603b..76bde18c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14897,6 +14897,7 @@ }, "packages/node-addon-examples": { "name": "@react-native-node-api/node-addon-examples", + "version": "0.1.0", "dependencies": { "assert": "^2.1.0" }, @@ -14909,6 +14910,7 @@ }, "packages/node-tests": { "name": "@react-native-node-api/node-tests", + "version": "0.1.0", "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", diff --git a/packages/ferric-example/package.json b/packages/ferric-example/package.json index 73678bb3..0695c049 100644 --- a/packages/ferric-example/package.json +++ b/packages/ferric-example/package.json @@ -1,8 +1,8 @@ { "name": "@react-native-node-api/ferric-example", + "version": "0.1.1", "private": true, "type": "commonjs", - "version": "0.1.1", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { "type": "git", @@ -11,6 +11,12 @@ }, "main": "ferric_example.js", "types": "ferric_example.d.ts", + "files": [ + "ferric_example.js", + "ferric_example.d.ts", + "ferric_example.apple.node", + "ferric_example.android.node" + ], "scripts": { "build": "ferric build", "bootstrap": "node --run build" diff --git a/packages/host/src/node/cli/apple.test.ts b/packages/host/src/node/cli/apple.test.ts index 7914a408..7d32afde 100644 --- a/packages/host/src/node/cli/apple.test.ts +++ b/packages/host/src/node/cli/apple.test.ts @@ -9,6 +9,7 @@ import { readAndParsePlist, readFrameworkInfo, readXcframeworkInfo, + restoreFrameworkLinks, } from "./apple"; import { setupTempDirectory } from "../test-utils"; @@ -267,6 +268,109 @@ describe("apple", { skip: process.platform !== "darwin" }, () => { ); }); }); + + describe("restoreFrameworkLinks", () => { + it("restores a versioned framework", async (context) => { + const infoPlistContents = ` + + + + + CFBundlePackageType + FMWK + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + example-addon + + + `; + + const tempDirectoryPath = setupTempDirectory(context, { + "foo.framework": { + Versions: { + A: { + Resources: { + "Info.plist": infoPlistContents, + }, + "example-addon": "", + }, + }, + }, + }); + + const frameworkPath = path.join(tempDirectoryPath, "foo.framework"); + const currentVersionPath = path.join( + frameworkPath, + "Versions", + "Current", + ); + const binaryLinkPath = path.join(frameworkPath, "example-addon"); + const realBinaryPath = path.join( + frameworkPath, + "Versions", + "A", + "example-addon", + ); + + async function assertVersionedFramework() { + const currentStat = await fs.promises.lstat(currentVersionPath); + assert( + currentStat.isSymbolicLink(), + "Expected Current symlink to be restored", + ); + assert.equal( + await fs.promises.realpath(currentVersionPath), + path.join(frameworkPath, "Versions", "A"), + ); + + const binaryStat = await fs.promises.lstat(binaryLinkPath); + assert( + binaryStat.isSymbolicLink(), + "Expected binary symlink to be restored", + ); + assert.equal( + await fs.promises.realpath(binaryLinkPath), + realBinaryPath, + ); + } + + await restoreFrameworkLinks(frameworkPath); + await assertVersionedFramework(); + + // Calling again to expect a no-op + await restoreFrameworkLinks(frameworkPath); + await assertVersionedFramework(); + }); + + it("throws on a flat framework", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "foo.framework": { + "Info.plist": ` + + + + + CFBundlePackageType + FMWK + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + example-addon + + + `, + }, + }); + + const frameworkPath = path.join(tempDirectoryPath, "foo.framework"); + + await assert.rejects( + () => restoreFrameworkLinks(frameworkPath), + /Expected "Versions" directory inside versioned framework/, + ); + }); + }); }); describe("apple on non-darwin", { skip: process.platform === "darwin" }, () => { diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index c5eec200..3ff64cef 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -192,6 +192,54 @@ export async function linkFlatFramework({ } } +/** + * NPM packages aren't preserving internal symlinks inside versioned frameworks. + * This function attempts to restore those. + */ +export async function restoreFrameworkLinks(frameworkPath: string) { + // Reconstruct missing symbolic links if needed + const versionsPath = path.join(frameworkPath, "Versions"); + const versionCurrentPath = path.join(versionsPath, "Current"); + + assert( + fs.existsSync(versionsPath), + `Expected "Versions" directory inside versioned framework '${frameworkPath}'`, + ); + + if (!fs.existsSync(versionCurrentPath)) { + const versionDirectoryEntries = await fs.promises.readdir(versionsPath, { + withFileTypes: true, + }); + const versionDirectoryPaths = versionDirectoryEntries + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => path.join(dirent.parentPath, dirent.name)); + assert.equal( + versionDirectoryPaths.length, + 1, + `Expected a single directory in ${versionsPath}, found ${JSON.stringify(versionDirectoryPaths)}`, + ); + const [versionDirectoryPath] = versionDirectoryPaths; + await fs.promises.symlink( + path.relative(path.dirname(versionCurrentPath), versionDirectoryPath), + versionCurrentPath, + ); + } + + const { CFBundleExecutable } = await readFrameworkInfo( + path.join(versionCurrentPath, "Resources", "Info.plist"), + ); + + const libraryRealPath = path.join(versionCurrentPath, CFBundleExecutable); + const libraryLinkPath = path.join(frameworkPath, CFBundleExecutable); + // Reconstruct missing symbolic links if needed + if (fs.existsSync(libraryRealPath) && !fs.existsSync(libraryLinkPath)) { + await fs.promises.symlink( + path.relative(path.dirname(libraryLinkPath), libraryRealPath), + libraryLinkPath, + ); + } +} + export async function linkVersionedFramework({ frameworkPath, newLibraryName, @@ -201,6 +249,9 @@ export async function linkVersionedFramework({ "darwin", "Linking Apple addons are only supported on macOS", ); + + await restoreFrameworkLinks(frameworkPath); + const frameworkInfoPath = path.join( frameworkPath, "Versions", diff --git a/packages/host/src/node/cli/program.ts b/packages/host/src/node/cli/program.ts index 169ca85b..85b9016f 100644 --- a/packages/host/src/node/cli/program.ts +++ b/packages/host/src/node/cli/program.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import path from "node:path"; import { EventEmitter } from "node:stream"; +import fs from "node:fs"; import { Command, @@ -26,8 +27,9 @@ import { import { command as vendorHermes } from "./hermes"; import { packageNameOption, pathSuffixOption } from "./options"; import { linkModules, pruneLinkedModules, ModuleLinker } from "./link-modules"; -import { linkXcframework } from "./apple"; +import { linkXcframework, restoreFrameworkLinks } from "./apple"; import { linkAndroidDir } from "./android"; +import { weakNodeApiPath } from "../weak-node-api"; // We're attaching a lot of listeners when spawning in parallel EventEmitter.defaultMaxListeners = 100; @@ -169,6 +171,38 @@ program await pruneLinkedModules(platform, modules); } } + + if (apple) { + await oraPromise( + async () => { + const xcframeworkPath = path.join( + weakNodeApiPath, + "weak-node-api.xcframework", + ); + await Promise.all( + [ + path.join(xcframeworkPath, "macos-x86_64"), + path.join(xcframeworkPath, "macos-arm64"), + path.join(xcframeworkPath, "macos-arm64_x86_64"), + ].map(async (slicePath) => { + const frameworkPath = path.join( + slicePath, + "weak-node-api.framework", + ); + if (fs.existsSync(frameworkPath)) { + await restoreFrameworkLinks(frameworkPath); + } + }), + ); + }, + { + text: "Restoring weak-node-api symlinks", + successText: "Restored weak-node-api symlinks", + failText: (error) => + `Failed to restore weak-node-api symlinks: ${error.message}`, + }, + ); + } }, ), ); diff --git a/packages/node-addon-examples/.gitignore b/packages/node-addon-examples/.gitignore index d838da98..7470cb91 100644 --- a/packages/node-addon-examples/.gitignore +++ b/packages/node-addon-examples/.gitignore @@ -1 +1,2 @@ examples/ +build/ diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index db48db3f..010d2f31 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -1,7 +1,17 @@ { "name": "@react-native-node-api/node-addon-examples", + "version": "0.1.0", "type": "commonjs", "main": "dist/index.js", + "files": [ + "dist", + "examples/**/package.json", + "examples/**/*.js", + "tests/**/package.json", + "tests/**/*.js", + "**/*.apple.node/**", + "**/*.android.node/**" + ], "private": true, "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { diff --git a/packages/node-addon-examples/tests/.gitignore b/packages/node-addon-examples/tests/.gitignore deleted file mode 100644 index 378eac25..00000000 --- a/packages/node-addon-examples/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build diff --git a/packages/node-tests/package.json b/packages/node-tests/package.json index eec9e5ed..fd1d497b 100644 --- a/packages/node-tests/package.json +++ b/packages/node-tests/package.json @@ -1,8 +1,15 @@ { "name": "@react-native-node-api/node-tests", + "version": "0.1.0", "description": "Harness for running the Node.js tests from https://github.com/nodejs/node/tree/main/test", "type": "commonjs", "main": "tests.generated.js", + "files": [ + "dist", + "tests/**/*.js", + "**/*.apple.node/**", + "**/*.android.node/**" + ], "private": true, "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { From 2c54ae31560fa463a4fbd515457e0b8f54eebf98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 2 Nov 2025 16:37:01 +0100 Subject: [PATCH 48/82] Moved patching of JSI headers to a separate function (#304) --- packages/host/src/node/cli/hermes.ts | 49 ++++++++++++++++++---------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/packages/host/src/node/cli/hermes.ts b/packages/host/src/node/cli/hermes.ts index 6bd3daa3..4b41692c 100644 --- a/packages/host/src/node/cli/hermes.ts +++ b/packages/host/src/node/cli/hermes.ts @@ -23,6 +23,32 @@ const platformOption = new Option( "The React Native package to vendor Hermes into", ).default("react-native"); +type PatchJSIHeadersOptions = { + reactNativePath: string; + hermesJsiPath: string; + silent: boolean; +}; + +async function patchJsiHeaders({ + reactNativePath, + hermesJsiPath, + silent, +}: PatchJSIHeadersOptions) { + const reactNativeJsiPath = path.join(reactNativePath, "ReactCommon/jsi/jsi/"); + await oraPromise( + fs.promises.cp(hermesJsiPath, reactNativeJsiPath, { + recursive: true, + }), + { + text: `Copying JSI from patched Hermes to React Native`, + successText: "Copied JSI from patched Hermes to React Native", + failText: (err) => + `Failed to copy JSI from Hermes to React Native: ${err.message}`, + isEnabled: !silent, + }, + ); +} + export const command = new Command("vendor-hermes") .argument("[from]", "Path to a file inside the app package", process.cwd()) .option("--silent", "Don't print anything except the final path", false) @@ -64,11 +90,6 @@ export const command = new Command("vendor-hermes") console.log(`Using Hermes version: ${hermesVersion}`); } - const reactNativeJsiPath = path.join( - reactNativePath, - "ReactCommon/jsi/jsi/", - ); - const hermesPath = path.join(reactNativePath, "sdks", "node-api-hermes"); if (force && fs.existsSync(hermesPath)) { await oraPromise( @@ -125,19 +146,11 @@ export const command = new Command("vendor-hermes") fs.existsSync(hermesJsiPath), `Hermes JSI path does not exist: ${hermesJsiPath}`, ); - - await oraPromise( - fs.promises.cp(hermesJsiPath, reactNativeJsiPath, { - recursive: true, - }), - { - text: `Copying JSI from patched Hermes to React Native`, - successText: "Copied JSI from patched Hermes to React Native", - failText: (err) => - `Failed to copy JSI from Hermes to React Native: ${err.message}`, - isEnabled: !silent, - }, - ); + await patchJsiHeaders({ + reactNativePath, + hermesJsiPath, + silent, + }); console.log(hermesPath); }), ); From 1d636d6e2c195c22e937ce4231c6884cd8080c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 2 Nov 2025 19:36:05 +0100 Subject: [PATCH 49/82] Fallback on non-universal libraries when getting path for weak-node-api framework. (#303) --- packages/ferric/src/cargo.ts | 58 ++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index 9afa0770..4671a47c 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -19,22 +19,46 @@ import { isThirdTierTarget, } from "./targets.js"; -const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record = { - "aarch64-apple-darwin": "macos-arm64_x86_64", // Universal - "x86_64-apple-darwin": "macos-arm64_x86_64", // Universal +/** + * A per apple target mapping to a list of xcframework slices in order of priority + */ +const APPLE_XCFRAMEWORK_SLICES_PER_TARGET: Record = { + "aarch64-apple-darwin": [ + "macos-arm64_x86_64", // Universal + "macos-arm64", + ], + "x86_64-apple-darwin": [ + "macos-arm64_x86_64", // Universal + "macos-x86_64", + ], - "aarch64-apple-ios": "ios-arm64", - "aarch64-apple-ios-sim": "ios-arm64_x86_64-simulator", // Universal - "x86_64-apple-ios": "ios-arm64_x86_64-simulator", // Universal + "aarch64-apple-ios": ["ios-arm64"], + "aarch64-apple-ios-sim": [ + "ios-arm64_x86_64-simulator", // Universal + "ios-arm64-simulator", + ], + "x86_64-apple-ios": [ + "ios-arm64_x86_64-simulator", // Universal + "ios-x86_64-simulator", + ], - "aarch64-apple-visionos": "xros-arm64", - "aarch64-apple-visionos-sim": "xros-arm64_x86_64-simulator", // Universal + "aarch64-apple-visionos": ["xros-arm64"], + "aarch64-apple-visionos-sim": [ + "xros-arm64_x86_64-simulator", // Universal + "xros-arm64-simulator", + ], // The x86_64 target for vision simulator isn't supported // see https://doc.rust-lang.org/rustc/platform-support.html - "aarch64-apple-tvos": "tvos-arm64", - "aarch64-apple-tvos-sim": "tvos-arm64_x86_64-simulator", - "x86_64-apple-tvos": "tvos-arm64_x86_64-simulator", + "aarch64-apple-tvos": ["tvos-arm64"], + "aarch64-apple-tvos-sim": [ + "tvos-arm64_x86_64-simulator", // Universal + "tvos-arm64-simulator", + ], + "x86_64-apple-tvos": [ + "tvos-arm64_x86_64-simulator", // Universal + "tvos-x86_64-simulator", + ], // "aarch64-apple-ios-macabi": "", // Catalyst // "x86_64-apple-ios-macabi": "ios-x86_64-simulator", @@ -145,11 +169,19 @@ export function getTargetAndroidPlatform(target: AndroidTargetName) { } export function getWeakNodeApiFrameworkPath(target: AppleTargetName) { - return joinPathAndAssertExistence( + const xcframeworkPath = joinPathAndAssertExistence( weakNodeApiPath, "weak-node-api.xcframework", - APPLE_XCFRAMEWORK_CHILDS_PER_TARGET[target], ); + const result = APPLE_XCFRAMEWORK_SLICES_PER_TARGET[target].find((slice) => { + const candidatePath = path.join(xcframeworkPath, slice); + return fs.existsSync(candidatePath); + }); + assert( + result, + `No matching slice found in weak-node-api.xcframework for target ${target}`, + ); + return joinPathAndAssertExistence(xcframeworkPath, result); } export function getWeakNodeApiAndroidLibraryPath(target: AndroidTargetName) { From a2fd4221a4f0e3e056448fe4bcdd565daf45c4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 2 Nov 2025 20:30:56 +0100 Subject: [PATCH 50/82] MacOS test app and fix hermes vendoring from `react-native-macos` (#297) * Remove platform restriction from host package's podspec * Add fallback for watchFolders in the metro config * Update Podspec to detect React Native package name * Add script to init a MacOS test app Enable Hermes and Fabric Patch react_native_post_install to pass react native path Don't pod install when initializing Patch macos app with scripts and source files Re-arm the init script Move linked deps into install command to make --install-links effective Using regular linking for monorepo deps Include original dependencies Enable new arch in Podfile Fix comment from review * Add job in the check workflow to initialize and build the MacOS test app No need to Setup Android SDK Debugging with Copilot Pass --mode when building from CLI Install CMake 3.22 Fix bootstrap issue Trying a higher CMake version Remove debug info from workflow Build universal Darwin libraries Add missing x86_64-apple-darwin Rust target to macOS CI job (#298) * Initial plan * Add missing x86_64-apple-darwin Rust target to macOS job Co-authored-by: kraenhansen <1243959+kraenhansen@users.noreply.github.com> * Add only missing x86_64-apple-darwin target (aarch64 is host) Co-authored-by: kraenhansen <1243959+kraenhansen@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: kraenhansen <1243959+kraenhansen@users.noreply.github.com> Run MacOS test app Use package script to run all tests --- .changeset/silly-mice-warn.md | 5 + .github/workflows/check.yml | 33 ++++ .gitignore | 3 + apps/test-app/metro.config.js | 13 +- eslint.config.js | 2 + package-lock.json | 27 +-- package.json | 4 +- packages/host/react-native-node-api.podspec | 1 - packages/host/scripts/patch-hermes.rb | 12 +- scripts/init-macos-test-app.ts | 190 ++++++++++++++++++++ tsconfig.json | 1 + tsconfig.scripts.json | 10 ++ 12 files changed, 284 insertions(+), 17 deletions(-) create mode 100644 .changeset/silly-mice-warn.md create mode 100644 scripts/init-macos-test-app.ts create mode 100644 tsconfig.scripts.json diff --git a/.changeset/silly-mice-warn.md b/.changeset/silly-mice-warn.md new file mode 100644 index 00000000..7ebdc578 --- /dev/null +++ b/.changeset/silly-mice-warn.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Detects "pod install" from React Native MacOS apps and vendors Hermes accordingly diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ec9c7cee..a221cfc4 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -107,6 +107,39 @@ jobs: # TODO: Enable release mode when it works # run: npm run test:ios -- --mode Release working-directory: apps/test-app + test-macos: + # Disabling this on main for now, as initializing the template takes a long time and + # we don't have macOS-specific code yet + if: contains(github.event.pull_request.labels.*.name, 'MacOS 💻') + name: Test app (macOS) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/jod + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: "17" + distribution: "temurin" + # Install CMake 3 since 4.x may have compatibility issues with Hermes build system + - name: Install compatible CMake version + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: "3.31.2" + - run: rustup target add x86_64-apple-darwin + - run: npm ci + - run: npm run bootstrap + env: + CMAKE_RN_TRIPLETS: arm64;x86_64-apple-darwin + FERRIC_TARGETS: aarch64-apple-darwin,x86_64-apple-darwin + - run: npm run init-macos-test-app + - run: pod install --project-directory=macos + working-directory: apps/macos-test-app + - name: Run MacOS test app + run: npm run test:allTests -- --mode Release + working-directory: apps/macos-test-app test-android: if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Android 🤖') name: Test app (Android) diff --git a/.gitignore b/.gitignore index 42f6c1a8..6c7c7bf3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ node_modules/ dist/ *.tsbuildinfo + +# Treading the MacOS app as ephemeral +apps/macos-test-app diff --git a/apps/test-app/metro.config.js b/apps/test-app/metro.config.js index 2c321b49..95e22157 100644 --- a/apps/test-app/metro.config.js +++ b/apps/test-app/metro.config.js @@ -1,5 +1,6 @@ const { makeMetroConfig } = require("@rnx-kit/metro-config"); -module.exports = makeMetroConfig({ + +const config = makeMetroConfig({ transformer: { getTransformOptions: async () => ({ transform: { @@ -9,3 +10,13 @@ module.exports = makeMetroConfig({ }), }, }); + +if (config.watchFolders.length === 0) { + // This patch is needed to locate packages in the monorepo from the MacOS app + // which is intentionally kept outside of the workspaces configuration to prevent + // duplicate react-native version and pollution of the package lock. + const path = require("node:path"); + config.watchFolders.push(path.resolve(__dirname, "../..")); +} + +module.exports = config; diff --git a/eslint.config.js b/eslint.config.js index c0af837e..e1bacb32 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -47,6 +47,7 @@ export default tseslint.config( { files: [ "apps/test-app/*.js", + "apps/macos-test-app/*.js", "packages/node-addon-examples/**/*.js", "packages/host/babel-plugin.js", "packages/host/react-native.config.js", @@ -68,6 +69,7 @@ export default tseslint.config( }, { files: [ + "**/metro.config.js", "packages/gyp-to-cmake/bin/*.js", "packages/host/bin/*.mjs", "packages/host/scripts/*.mjs", diff --git a/package-lock.json b/package-lock.json index 76bde18c..9604cb48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "globals": "^16.0.0", "prettier": "^3.6.2", "react-native": "0.81.4", + "read-pkg": "^9.0.1", "tsx": "^4.20.5", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" @@ -14607,7 +14608,7 @@ }, "packages/cli-utils": { "name": "@react-native-node-api/cli-utils", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "@commander-js/extra-typings": "^14.0.0", "bufout": "^0.3.2", @@ -14824,11 +14825,11 @@ } }, "packages/cmake-rn": { - "version": "0.4.1", + "version": "0.5.1", "dependencies": { - "@react-native-node-api/cli-utils": "0.1.0", + "@react-native-node-api/cli-utils": "0.1.1", "cmake-file-api": "0.1.0", - "react-native-node-api": "0.5.2", + "react-native-node-api": "0.6.1", "zod": "^4.1.11" }, "bin": { @@ -14841,11 +14842,11 @@ }, "packages/ferric": { "name": "ferric-cli", - "version": "0.3.4", + "version": "0.3.6", "dependencies": { "@napi-rs/cli": "~3.0.3", - "@react-native-node-api/cli-utils": "0.1.0", - "react-native-node-api": "0.5.2" + "@react-native-node-api/cli-utils": "0.1.1", + "react-native-node-api": "0.6.1" }, "bin": { "ferric": "bin/ferric.js" @@ -14859,9 +14860,9 @@ } }, "packages/gyp-to-cmake": { - "version": "0.3.0", + "version": "0.4.0", "dependencies": { - "@react-native-node-api/cli-utils": "0.1.0", + "@react-native-node-api/cli-utils": "0.1.1", "gyp-parser": "^1.0.4", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1" @@ -14872,11 +14873,11 @@ }, "packages/host": { "name": "react-native-node-api", - "version": "0.5.2", + "version": "0.6.1", "license": "MIT", "dependencies": { "@expo/plist": "^0.4.7", - "@react-native-node-api/cli-utils": "0.1.0", + "@react-native-node-api/cli-utils": "0.1.1", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1", "zod": "^4.1.11" @@ -14892,7 +14893,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.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4" + "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" } }, "packages/node-addon-examples": { @@ -14915,7 +14916,7 @@ "cmake-rn": "*", "gyp-to-cmake": "*", "prebuildify": "^6.0.1", - "react-native-node-api": "^0.5.2", + "react-native-node-api": "^0.6.1", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } diff --git a/package.json b/package.json index eb2f9dff..be2dbf31 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "test": "npm test --workspace react-native-node-api --workspace cmake-rn --workspace gyp-to-cmake --workspace node-addon-examples", "bootstrap": "node --run build && npm run bootstrap --workspaces --if-present", "prerelease": "node --run build && npm run prerelease --workspaces --if-present", - "release": "changeset publish" + "release": "changeset publish", + "init-macos-test-app": "node scripts/init-macos-test-app.ts" }, "author": { "name": "Callstack", @@ -64,6 +65,7 @@ "globals": "^16.0.0", "prettier": "^3.6.2", "react-native": "0.81.4", + "read-pkg": "^9.0.1", "tsx": "^4.20.5", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" diff --git a/packages/host/react-native-node-api.podspec b/packages/host/react-native-node-api.podspec index 13e941b7..5ed4072a 100644 --- a/packages/host/react-native-node-api.podspec +++ b/packages/host/react-native-node-api.podspec @@ -29,7 +29,6 @@ Pod::Spec.new do |s| s.license = package["license"] s.authors = package["author"] - s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://github.com/callstackincubator/react-native-node-api.git", :tag => "#{s.version}" } s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "weak-node-api/include/*.h", "weak-node-api/*.hpp" diff --git a/packages/host/scripts/patch-hermes.rb b/packages/host/scripts/patch-hermes.rb index a6cb11f7..76252154 100644 --- a/packages/host/scripts/patch-hermes.rb +++ b/packages/host/scripts/patch-hermes.rb @@ -4,8 +4,18 @@ raise "React Native Node-API cannot reliably patch JSI when React Native Core is prebuilt." end +def get_react_native_package + if caller.any? { |frame| frame.include?("node_modules/react-native-macos/") } + return "react-native-macos" + elsif caller.any? { |frame| frame.include?("node_modules/react-native/") } + return "react-native" + else + raise "Unable to determine React Native package from call stack." + end +end + if ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].nil? - VENDORED_HERMES_DIR ||= `npx react-native-node-api vendor-hermes --silent '#{Pod::Config.instance.installation_root}'`.strip + VENDORED_HERMES_DIR ||= `npx react-native-node-api vendor-hermes --react-native-package '#{get_react_native_package()}' --silent '#{Pod::Config.instance.installation_root}'`.strip # Signal the patched Hermes to React Native ENV['BUILD_FROM_SOURCE'] = 'true' ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'] = VENDORED_HERMES_DIR diff --git a/scripts/init-macos-test-app.ts b/scripts/init-macos-test-app.ts new file mode 100644 index 00000000..7d3abd1e --- /dev/null +++ b/scripts/init-macos-test-app.ts @@ -0,0 +1,190 @@ +import assert from "node:assert/strict"; +import cp from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { readPackage } from "read-pkg"; + +const REACT_NATIVE_VERSION = "0.79.6"; +const ROOT_PATH = path.join(import.meta.dirname, ".."); +const APP_PATH = path.join(ROOT_PATH, "apps", "macos-test-app"); +const OTHER_APP_PATH = path.join(ROOT_PATH, "apps", "test-app"); + +function exec(command: string, args: string[], options: cp.SpawnOptions = {}) { + const { status } = cp.spawnSync(command, args, { + stdio: "inherit", + ...options, + }); + assert.equal(status, 0, `Failed to execute '${command}'`); +} + +async function deletePreviousApp() { + if (fs.existsSync(APP_PATH)) { + console.log("Deleting existing app directory"); + await fs.promises.rm(APP_PATH, { recursive: true, force: true }); + } +} + +async function initializeReactNativeTemplate() { + console.log("Initializing community template"); + exec("npx", [ + "@react-native-community/cli", + "init", + "MacOSTestApp", + "--skip-install", + "--skip-git-init", + // "--platform-name", + // "react-native-macos", + "--version", + REACT_NATIVE_VERSION, + "--directory", + APP_PATH, + ]); + + // Clean up + const CLEANUP_PATHS = [ + "ios", + "android", + "__tests__", + ".prettierrc.js", + ".gitignore", + ]; + + for (const cleanupPath of CLEANUP_PATHS) { + await fs.promises.rm(path.join(APP_PATH, cleanupPath), { + recursive: true, + force: true, + }); + } +} + +async function patchPackageJson() { + console.log("Patching package.json scripts"); + const packageJson = await readPackage({ cwd: APP_PATH }); + const otherPackageJson = await readPackage({ cwd: OTHER_APP_PATH }); + + packageJson.scripts = { + ...packageJson.scripts, + metro: "react-native start --reset-cache --no-interactive", + "mocha-and-metro": "mocha-remote --exit-on-error -- node --run metro", + premacos: "killall 'MacOSTestApp' || true", + macos: "react-native run-macos --no-packager", + test: "mocha-remote --exit-on-error -- concurrently --passthrough-arguments --kill-others-on-fail npm:metro 'npm:macos -- {@}' --", + "test:allTests": "MOCHA_REMOTE_CONTEXT=allTests node --run test -- ", + "test:nodeAddonExamples": + "MOCHA_REMOTE_CONTEXT=nodeAddonExamples node --run test -- ", + "test:nodeTests": "MOCHA_REMOTE_CONTEXT=nodeTests node --run test -- ", + "test:ferricExample": + "MOCHA_REMOTE_CONTEXT=ferricExample node --run test -- ", + }; + + const transferredDependencies = new Set([ + "@rnx-kit/metro-config", + "mocha-remote-cli", + "mocha-remote-react-native", + ]); + + const { dependencies: otherDependencies = {} } = otherPackageJson; + + packageJson.dependencies = { + ...packageJson.dependencies, + "react-native-macos-init": "^2.1.3", + "@react-native-node-api/node-addon-examples": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "node-addon-examples"), + ), + "@react-native-node-api/node-tests": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "node-tests"), + ), + "@react-native-node-api/ferric-example": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "ferric-example"), + ), + "react-native-node-api": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "host"), + ), + ...Object.fromEntries( + Object.entries(otherDependencies).filter(([name]) => + transferredDependencies.has(name), + ), + ), + }; + + await fs.promises.writeFile( + path.join(APP_PATH, "package.json"), + JSON.stringify(packageJson, null, 2), + "utf8", + ); +} + +function installDependencies() { + console.log("Installing dependencies"); + exec("npm", ["install", "--prefer-offline"], { + cwd: APP_PATH, + }); +} + +function initializeReactNativeMacOSTemplate() { + console.log("Initializing react-native-macos template"); + exec("npx", ["react-native-macos-init"], { + cwd: APP_PATH, + }); +} + +async function patchPodfile() { + console.log("Patching Podfile"); + const replacements = [ + [ + // As per https://github.com/microsoft/react-native-macos/issues/2723#issuecomment-3392930688 + "require_relative '../node_modules/react-native-macos/scripts/react_native_pods'\nrequire_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'", + "require_relative '../node_modules/react-native-macos/scripts/cocoapods/autolinking'", + ], + [ + ":hermes_enabled => false,", + // Adding the new_arch_enabled here as it's not a part of the template + ":hermes_enabled => true,\n :new_arch_enabled => true,", + ], + [ + ":fabric_enabled => ENV['RCT_NEW_ARCH_ENABLED'] == '1',", + ":fabric_enabled => true,", + ], + [ + "react_native_post_install(installer)", + "react_native_post_install(installer, '../node_modules/react-native-macos')", + ], + ]; + + const podfilePath = path.join(APP_PATH, "macos", "Podfile"); + let podfileContents = await fs.promises.readFile(podfilePath, "utf8"); + for (const [searchValue, replaceValue] of replacements) { + podfileContents = podfileContents.replace(searchValue, replaceValue); + } + await fs.promises.writeFile(podfilePath, podfileContents, "utf8"); +} + +async function copySourceFiles() { + console.log("Copying source files from test-app into macos-test-app:"); + const FILE_NAMES = [ + "App.tsx", + // Adds the babel plugin needed to transform require calls + "babel.config.js", + // Adds the ability to reference symlinked packages + "metro.config.js", + ]; + for (const fileName of FILE_NAMES) { + console.log(`↳ ${fileName}`); + await fs.promises.copyFile( + path.join(OTHER_APP_PATH, fileName), + path.join(APP_PATH, fileName), + ); + } +} + +await deletePreviousApp(); +await initializeReactNativeTemplate(); +await patchPackageJson(); +installDependencies(); +initializeReactNativeMacOSTemplate(); +await patchPodfile(); +await copySourceFiles(); diff --git a/tsconfig.json b/tsconfig.json index a733c3ff..4ca25b74 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ }, "files": ["prettier.config.js", "eslint.config.js"], "references": [ + { "path": "./tsconfig.scripts.json" }, { "path": "./packages/cli-utils/tsconfig.json" }, { "path": "./packages/cmake-file-api/tsconfig.json" }, { "path": "./packages/cmake-file-api/tsconfig.tests.json" }, diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json new file mode 100644 index 00000000..88041106 --- /dev/null +++ b/tsconfig.scripts.json @@ -0,0 +1,10 @@ +{ + "extends": "./configs/tsconfig.node.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "declarationMap": false, + "rootDir": "scripts" + }, + "include": ["scripts"] +} From 6a3267c1ea213b65c90972edbfc2d72dc352199c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 00:31:22 +0100 Subject: [PATCH 51/82] Version Packages (#293) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/better-pets-help.md | 5 ----- .changeset/bright-parts-roll.md | 6 ------ .changeset/chatty-states-build.md | 5 ----- .changeset/large-hornets-burn.md | 5 ----- .changeset/mighty-regions-clean.md | 5 ----- .changeset/sad-poets-smoke.md | 5 ----- .changeset/silly-mice-warn.md | 5 ----- .changeset/tame-bugs-shave.md | 5 ----- .changeset/wicked-tables-deny.md | 5 ----- packages/cmake-rn/CHANGELOG.md | 13 +++++++++++++ packages/cmake-rn/package.json | 4 ++-- packages/ferric/CHANGELOG.md | 15 +++++++++++++++ packages/ferric/package.json | 4 ++-- packages/host/CHANGELOG.md | 11 +++++++++++ packages/host/package.json | 2 +- 15 files changed, 44 insertions(+), 51 deletions(-) delete mode 100644 .changeset/better-pets-help.md delete mode 100644 .changeset/bright-parts-roll.md delete mode 100644 .changeset/chatty-states-build.md delete mode 100644 .changeset/large-hornets-burn.md delete mode 100644 .changeset/mighty-regions-clean.md delete mode 100644 .changeset/sad-poets-smoke.md delete mode 100644 .changeset/silly-mice-warn.md delete mode 100644 .changeset/tame-bugs-shave.md delete mode 100644 .changeset/wicked-tables-deny.md diff --git a/.changeset/better-pets-help.md b/.changeset/better-pets-help.md deleted file mode 100644 index 904acf2c..00000000 --- a/.changeset/better-pets-help.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"ferric-cli": patch ---- - -Add x86_64 ios simulator target and output universal libraries for iOS simulators. diff --git a/.changeset/bright-parts-roll.md b/.changeset/bright-parts-roll.md deleted file mode 100644 index 4d27aef2..00000000 --- a/.changeset/bright-parts-roll.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"cmake-rn": patch -"react-native-node-api": patch ---- - -Add x86_64 and universal simulator triplets diff --git a/.changeset/chatty-states-build.md b/.changeset/chatty-states-build.md deleted file mode 100644 index 1be4d07f..00000000 --- a/.changeset/chatty-states-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Add --react-native-package option to "vendor-hermes" command, allowing caller to choose the package to download hermes into diff --git a/.changeset/large-hornets-burn.md b/.changeset/large-hornets-burn.md deleted file mode 100644 index c7ad76f4..00000000 --- a/.changeset/large-hornets-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"ferric-cli": patch ---- - -It's no longer required to pass "build" to ferric, as this is default now diff --git a/.changeset/mighty-regions-clean.md b/.changeset/mighty-regions-clean.md deleted file mode 100644 index eae04696..00000000 --- a/.changeset/mighty-regions-clean.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"ferric-cli": patch ---- - -Add support for visionOS and tvOS targets diff --git a/.changeset/sad-poets-smoke.md b/.changeset/sad-poets-smoke.md deleted file mode 100644 index 8c5c1990..00000000 --- a/.changeset/sad-poets-smoke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Moved and simplify Apple host TurboModule diff --git a/.changeset/silly-mice-warn.md b/.changeset/silly-mice-warn.md deleted file mode 100644 index 7ebdc578..00000000 --- a/.changeset/silly-mice-warn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Detects "pod install" from React Native MacOS apps and vendors Hermes accordingly diff --git a/.changeset/tame-bugs-shave.md b/.changeset/tame-bugs-shave.md deleted file mode 100644 index c8181e01..00000000 --- a/.changeset/tame-bugs-shave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Add explicit support for React Native v0.79.7 diff --git a/.changeset/wicked-tables-deny.md b/.changeset/wicked-tables-deny.md deleted file mode 100644 index a1db092b..00000000 --- a/.changeset/wicked-tables-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Warn on "pod install" with the new architecture disabled diff --git a/packages/cmake-rn/CHANGELOG.md b/packages/cmake-rn/CHANGELOG.md index 7a1bc4e2..d0072ace 100644 --- a/packages/cmake-rn/CHANGELOG.md +++ b/packages/cmake-rn/CHANGELOG.md @@ -1,5 +1,18 @@ # cmake-rn +## 0.5.2 + +### Patch Changes + +- 07ea9dc: Add x86_64 and universal simulator triplets +- Updated dependencies [07ea9dc] +- Updated dependencies [7536c6c] +- Updated dependencies [c698698] +- Updated dependencies [a2fd422] +- Updated dependencies [bdc172e] +- Updated dependencies [4672e01] + - react-native-node-api@0.6.2 + ## 0.5.1 ### Patch Changes diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index d534d60d..9afb6e43 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -1,6 +1,6 @@ { "name": "cmake-rn", - "version": "0.5.1", + "version": "0.5.2", "description": "Build React Native Node API modules with CMake", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -26,7 +26,7 @@ "dependencies": { "@react-native-node-api/cli-utils": "0.1.1", "cmake-file-api": "0.1.0", - "react-native-node-api": "0.6.1", + "react-native-node-api": "0.6.2", "zod": "^4.1.11" }, "peerDependencies": { diff --git a/packages/ferric/CHANGELOG.md b/packages/ferric/CHANGELOG.md index fd58cc73..6ef161d9 100644 --- a/packages/ferric/CHANGELOG.md +++ b/packages/ferric/CHANGELOG.md @@ -1,5 +1,20 @@ # ferric-cli +## 0.3.7 + +### Patch Changes + +- 9411a8c: Add x86_64 ios simulator target and output universal libraries for iOS simulators. +- 9411a8c: It's no longer required to pass "build" to ferric, as this is default now +- b661176: Add support for visionOS and tvOS targets +- Updated dependencies [07ea9dc] +- Updated dependencies [7536c6c] +- Updated dependencies [c698698] +- Updated dependencies [a2fd422] +- Updated dependencies [bdc172e] +- Updated dependencies [4672e01] + - react-native-node-api@0.6.2 + ## 0.3.6 ### Patch Changes diff --git a/packages/ferric/package.json b/packages/ferric/package.json index 4cb3f87e..b098e373 100644 --- a/packages/ferric/package.json +++ b/packages/ferric/package.json @@ -1,6 +1,6 @@ { "name": "ferric-cli", - "version": "0.3.6", + "version": "0.3.7", "description": "Rust Node-API Modules for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -18,6 +18,6 @@ "dependencies": { "@napi-rs/cli": "~3.0.3", "@react-native-node-api/cli-utils": "0.1.1", - "react-native-node-api": "0.6.1" + "react-native-node-api": "0.6.2" } } diff --git a/packages/host/CHANGELOG.md b/packages/host/CHANGELOG.md index 188c5d3f..9252fd34 100644 --- a/packages/host/CHANGELOG.md +++ b/packages/host/CHANGELOG.md @@ -1,5 +1,16 @@ # react-native-node-api +## 0.6.2 + +### Patch Changes + +- 07ea9dc: Add x86_64 and universal simulator triplets +- 7536c6c: Add --react-native-package option to "vendor-hermes" command, allowing caller to choose the package to download hermes into +- c698698: Moved and simplify Apple host TurboModule +- a2fd422: Detects "pod install" from React Native MacOS apps and vendors Hermes accordingly +- bdc172e: Add explicit support for React Native v0.79.7 +- 4672e01: Warn on "pod install" with the new architecture disabled + ## 0.6.1 ### Patch Changes diff --git a/packages/host/package.json b/packages/host/package.json index 18246b53..e93a13b8 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -1,6 +1,6 @@ { "name": "react-native-node-api", - "version": "0.6.1", + "version": "0.6.2", "description": "Node-API for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { From 0763a4664b3faf3e85669d5350ea6f6064a232d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 3 Nov 2025 12:18:24 +0100 Subject: [PATCH 52/82] Update react-native peer dependency version range --- packages/host/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/host/package.json b/packages/host/package.json index e93a13b8..448ac91a 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -97,6 +97,6 @@ }, "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" + "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" } } From 0c89ed6bba49a07a938a6f19ea90eeac18a68327 Mon Sep 17 00:00:00 2001 From: owl352 <64574305+owl352@users.noreply.github.com> Date: Tue, 4 Nov 2025 02:02:44 +0300 Subject: [PATCH 53/82] Update package.json (#309) --- packages/host/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/host/package.json b/packages/host/package.json index 448ac91a..1edcc6ba 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -32,7 +32,7 @@ "android", "!android/.cxx", "!android/build", - "ios", + "apple", "include", "babel-plugin.js", "scripts/patch-hermes.rb", From eca721e097b703ea1648d26c9ffca9b156e1acd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 4 Nov 2025 14:51:30 +0100 Subject: [PATCH 54/82] Remove --force from vendor hermes command (#310) --- .changeset/tender-laws-admire.md | 5 +++++ packages/host/android/build.gradle | 2 +- packages/host/src/node/gradle.test.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/tender-laws-admire.md diff --git a/.changeset/tender-laws-admire.md b/.changeset/tender-laws-admire.md new file mode 100644 index 00000000..4aa54b2b --- /dev/null +++ b/.changeset/tender-laws-admire.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Don't instruct users to pass --force when vendoring hermes diff --git a/packages/host/android/build.gradle b/packages/host/android/build.gradle index 169265a0..41c7b7bd 100644 --- a/packages/host/android/build.gradle +++ b/packages/host/android/build.gradle @@ -7,7 +7,7 @@ if (!System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR")) { "React Native Node-API needs a custom version of Hermes with Node-API enabled.", "Run the following in your Bash- or Zsh-compatible terminal, to clone Hermes and instruct React Native to use it:", "", - "export REACT_NATIVE_OVERRIDE_HERMES_DIR=\$(npx react-native-node-api vendor-hermes --silent --force)", + "export REACT_NATIVE_OVERRIDE_HERMES_DIR=\$(npx react-native-node-api vendor-hermes --silent)", "", "And follow this guide to build React Native from source:", "https://reactnative.dev/contributing/how-to-build-from-source#update-your-project-to-build-from-source" diff --git a/packages/host/src/node/gradle.test.ts b/packages/host/src/node/gradle.test.ts index 9dcb218c..e08ee16e 100644 --- a/packages/host/src/node/gradle.test.ts +++ b/packages/host/src/node/gradle.test.ts @@ -38,7 +38,7 @@ describe( ); assert.match( stderr, - /export REACT_NATIVE_OVERRIDE_HERMES_DIR=\$\(npx react-native-node-api vendor-hermes --silent --force\)/, + /export REACT_NATIVE_OVERRIDE_HERMES_DIR=\$\(npx react-native-node-api vendor-hermes --silent\)/, ); assert.match( stderr, From 61fff3f9b54fb9793323d2a6f4d7f0dcef8c382c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 4 Nov 2025 19:32:26 +0100 Subject: [PATCH 55/82] Escape bundle identifiers when creating Apple frameworks and allow passing `--apple-bundle-identifier` (#316) * Escapes library names to match a CFBundleIdentifier * Allow passing --apple-bundle-identifier --- .changeset/gold-beans-jump.md | 7 ++++++ .changeset/long-regions-yawn.md | 5 ++++ packages/cmake-rn/src/platforms/apple.ts | 24 +++++++++++++----- packages/ferric/src/build.ts | 12 ++++++++- .../host/src/node/prebuilds/apple.test.ts | 17 +++++++++++++ packages/host/src/node/prebuilds/apple.ts | 25 ++++++++++++++++--- 6 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 .changeset/gold-beans-jump.md create mode 100644 .changeset/long-regions-yawn.md create mode 100644 packages/host/src/node/prebuilds/apple.test.ts diff --git a/.changeset/gold-beans-jump.md b/.changeset/gold-beans-jump.md new file mode 100644 index 00000000..ecc9f902 --- /dev/null +++ b/.changeset/gold-beans-jump.md @@ -0,0 +1,7 @@ +--- +"cmake-rn": patch +"ferric-cli": patch +"react-native-node-api": patch +--- + +Allow passing --apple-bundle-identifier to specify the bundle identifiers used when creating Apple frameworks. diff --git a/.changeset/long-regions-yawn.md b/.changeset/long-regions-yawn.md new file mode 100644 index 00000000..b0d515e7 --- /dev/null +++ b/.changeset/long-regions-yawn.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": minor +--- + +Ensure proper escaping when generating a bundle identifier while creating an Apple framework diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index 8f3de38a..c61c1e81 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -155,8 +155,14 @@ const xcframeworkExtensionOption = new Option( "Don't rename the xcframework to .apple.node", ).default(false); +const appleBundleIdentifierOption = new Option( + "--apple-bundle-identifier ", + "Unique CFBundleIdentifier used for Apple framework artifacts", +).default(undefined, "com.callstackincubator.node-api.{libraryName}"); + type AppleOpts = { xcframeworkExtension: boolean; + appleBundleIdentifier?: string; }; function getBuildPath(baseBuildPath: string, triplet: Triplet) { @@ -233,7 +239,9 @@ export const platform: Platform = { } }, amendCommand(command) { - return command.addOption(xcframeworkExtensionOption); + return command + .addOption(xcframeworkExtensionOption) + .addOption(appleBundleIdentifierOption); }, async configure( triplets, @@ -284,7 +292,10 @@ export const platform: Platform = { }), ); }, - async build({ spawn, triplet }, { build, target, configuration }) { + async build( + { spawn, triplet }, + { build, target, configuration, appleBundleIdentifier }, + ) { // We expect the final application to sign these binaries if (target.length > 1) { throw new Error("Building for multiple targets is not supported yet"); @@ -368,10 +379,11 @@ export const platform: Platform = { "Expected exactly one artifact", ); const [artifact] = artifacts; - await createAppleFramework( - path.join(buildPath, artifact.path), - triplet.endsWith("-darwin"), - ); + await createAppleFramework({ + libraryPath: path.join(buildPath, artifact.path), + versioned: triplet.endsWith("-darwin"), + bundleIdentifier: appleBundleIdentifier, + }); } }, isSupportedByHost: function (): boolean | Promise { diff --git a/packages/ferric/src/build.ts b/packages/ferric/src/build.ts index 6587347e..57f9ec59 100644 --- a/packages/ferric/src/build.ts +++ b/packages/ferric/src/build.ts @@ -107,6 +107,11 @@ const configurationOption = new Option( .choices(["debug", "release"]) .default("debug"); +const appleBundleIdentifierOption = new Option( + "--apple-bundle-identifier ", + "Unique CFBundleIdentifier used for Apple framework artifacts", +).default(undefined, "com.callstackincubator.node-api.{libraryName}"); + export const buildCommand = new Command("build") .description("Build Rust Node-API module") .addOption(targetOption) @@ -116,6 +121,7 @@ export const buildCommand = new Command("build") .addOption(outputPathOption) .addOption(configurationOption) .addOption(xcframeworkExtensionOption) + .addOption(appleBundleIdentifierOption) .action( wrapAction( async ({ @@ -126,6 +132,7 @@ export const buildCommand = new Command("build") output: outputPath, configuration, xcframeworkExtension, + appleBundleIdentifier, }) => { const targets = new Set([...targetArg]); if (apple) { @@ -239,7 +246,10 @@ export const buildCommand = new Command("build") const frameworkPaths = await Promise.all( libraryPaths.map((libraryPath) => // TODO: Pass true as `versioned` argument for -darwin targets - createAppleFramework(libraryPath), + createAppleFramework({ + libraryPath, + bundleIdentifier: appleBundleIdentifier, + }), ), ); const xcframeworkFilename = determineXCFrameworkFilename( diff --git a/packages/host/src/node/prebuilds/apple.test.ts b/packages/host/src/node/prebuilds/apple.test.ts new file mode 100644 index 00000000..4139831e --- /dev/null +++ b/packages/host/src/node/prebuilds/apple.test.ts @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { escapeBundleIdentifier } from "./apple"; + +describe("escapeBundleIdentifier", () => { + it("escapes and passes through values as expected", () => { + assert.equal( + escapeBundleIdentifier("abc-def-123-789.-"), + "abc-def-123-789.-", + ); + assert.equal(escapeBundleIdentifier("abc_def"), "abc-def"); + assert.equal(escapeBundleIdentifier("abc\ndef"), "abc-def"); + assert.equal(escapeBundleIdentifier("\0abc"), "-abc"); + assert.equal(escapeBundleIdentifier("🤷"), "--"); // An emoji takes up two chars + }); +}); diff --git a/packages/host/src/node/prebuilds/apple.ts b/packages/host/src/node/prebuilds/apple.ts index 1aeb9658..23f0848b 100644 --- a/packages/host/src/node/prebuilds/apple.ts +++ b/packages/host/src/node/prebuilds/apple.ts @@ -14,10 +14,25 @@ type XCframeworkOptions = { autoLink: boolean; }; -export async function createAppleFramework( - libraryPath: string, +/** + * Escapes any input to match a CFBundleIdentifier + * See https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleidentifier + */ +export function escapeBundleIdentifier(input: string) { + return input.replace(/[^A-Za-z0-9-.]/g, "-"); +} + +type CreateAppleFrameworkOptions = { + libraryPath: string; + versioned?: boolean; + bundleIdentifier?: string; +}; + +export async function createAppleFramework({ + libraryPath, versioned = false, -) { + bundleIdentifier, +}: CreateAppleFrameworkOptions) { if (versioned) { // TODO: Add support for generating a Versions/Current/Resources/Info.plist convention framework throw new Error("Creating versioned frameworks is not supported yet"); @@ -39,7 +54,9 @@ export async function createAppleFramework( plist.build({ CFBundleDevelopmentRegion: "en", CFBundleExecutable: libraryName, - CFBundleIdentifier: `com.callstackincubator.node-api.${libraryName}`, + CFBundleIdentifier: escapeBundleIdentifier( + bundleIdentifier ?? `com.callstackincubator.node-api.${libraryName}`, + ), CFBundleInfoDictionaryVersion: "6.0", CFBundleName: libraryName, CFBundlePackageType: "FMWK", From 683bab7fe5eedac1343898b0c78f27e3ed320fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 5 Nov 2025 11:20:13 +0100 Subject: [PATCH 56/82] Skip deleting temp directories when requested --- packages/host/src/node/test-utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/host/src/node/test-utils.ts b/packages/host/src/node/test-utils.ts index fb7abaf5..78b4d9a1 100644 --- a/packages/host/src/node/test-utils.ts +++ b/packages/host/src/node/test-utils.ts @@ -25,7 +25,9 @@ export function setupTempDirectory(context: TestContext, files: FileMap) { ); context.after(() => { - fs.rmSync(tempDirectoryPath, { recursive: true, force: true }); + if (!process.env.KEEP_TEMP_DIRS) { + fs.rmSync(tempDirectoryPath, { recursive: true, force: true }); + } }); writeFiles(tempDirectoryPath, files); From 5dea205128fb20d906e1a719de2bf25d0af7ba7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 5 Nov 2025 11:34:23 +0100 Subject: [PATCH 57/82] Add changeset for #309, a follow-up to #301 --- .changeset/poor-lemons-yell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/poor-lemons-yell.md diff --git a/.changeset/poor-lemons-yell.md b/.changeset/poor-lemons-yell.md new file mode 100644 index 00000000..ff49220e --- /dev/null +++ b/.changeset/poor-lemons-yell.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Add "apple" folder into the package (follow-up to #301) From 60fae96bc462b455efcfd947ba1ef6979b573998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 6 Nov 2025 09:27:45 +0100 Subject: [PATCH 58/82] Separate `weak-node-api` package (#308) * Don't export weakNodeApiPath from host * Move weak-node-api into its own package * Add a podspec to weak-node-api * Delete node-api headers from the host package * Remove symlink restoring of weak-node-api from host * Give preference to a Debug build of weak-node-api * Combined two buildFeatures * Error if weak-node-api is included outside of cmake-rn (for now) * Temporary work-around deep-referencing into weak-node-api * Use `find_package` instead of `include` to locate "weak-node-api" * Updates to the package lock * Moved weak-node-api into a peer dependency as it's supposed to be installed by the app * Reset weak-node-api version to 0.0.1 as published * Update readme and package description --- .changeset/eight-walls-push.md | 5 ++ .changeset/quick-poets-greet.md | 7 ++ .changeset/spotty-beers-repeat.md | 5 ++ .changeset/tired-words-relate.md | 5 ++ .github/workflows/check.yml | 18 +++-- .gitignore | 3 + apps/test-app/package.json | 3 +- package-lock.json | 40 ++++++++---- package.json | 3 +- packages/cmake-rn/README.md | 4 +- packages/cmake-rn/src/weak-node-api.ts | 26 ++++---- packages/ferric/src/cargo.ts | 13 ++-- packages/gyp-to-cmake/src/transformer.ts | 2 +- packages/host/.gitignore | 7 -- packages/host/android/CMakeLists.txt | 7 +- packages/host/android/build.gradle | 27 +++++--- packages/host/package.json | 29 ++------- packages/host/react-native-node-api.podspec | 7 +- ...ts => generate-weak-node-api-injector.mts} | 7 +- packages/host/src/node/cli/program.ts | 36 +--------- packages/host/src/node/index.ts | 2 - packages/host/src/node/weak-node-api.ts | 10 --- packages/host/tsconfig.node-scripts.json | 9 ++- packages/host/tsconfig.node.json | 7 +- .../host/weak-node-api/weak-node-api.cmake | 6 -- .../tests/async/CMakeLists.txt | 2 +- .../tests/buffers/CMakeLists.txt | 2 +- packages/weak-node-api/.gitignore | 11 ++++ .../{host => }/weak-node-api/CMakeLists.txt | 2 +- packages/weak-node-api/README.md | 19 ++++++ packages/weak-node-api/package.json | 65 +++++++++++++++++++ .../scripts/copy-node-api-headers.ts | 2 +- .../scripts/generate-weak-node-api.ts | 13 ++-- packages/weak-node-api/src/index.ts | 2 + .../src}/node-api-functions.ts | 0 .../src/restore-xcframework-symlinks.ts | 42 ++++++++++++ packages/weak-node-api/src/weak-node-api.ts | 26 ++++++++ packages/weak-node-api/tsconfig.json | 10 +++ .../weak-node-api/tsconfig.node-scripts.json | 13 ++++ packages/weak-node-api/tsconfig.node.json | 12 ++++ .../types/node-api-headers/index.d.ts | 0 .../weak-node-api/weak-node-api-config.cmake | 39 +++++++++++ packages/weak-node-api/weak-node-api.podspec | 34 ++++++++++ tsconfig.json | 3 +- 44 files changed, 421 insertions(+), 164 deletions(-) create mode 100644 .changeset/eight-walls-push.md create mode 100644 .changeset/quick-poets-greet.md create mode 100644 .changeset/spotty-beers-repeat.md create mode 100644 .changeset/tired-words-relate.md rename packages/host/scripts/{generate-weak-node-api-injector.ts => generate-weak-node-api-injector.mts} (94%) delete mode 100644 packages/host/src/node/weak-node-api.ts delete mode 100644 packages/host/weak-node-api/weak-node-api.cmake create mode 100644 packages/weak-node-api/.gitignore rename packages/{host => }/weak-node-api/CMakeLists.txt (96%) create mode 100644 packages/weak-node-api/README.md create mode 100644 packages/weak-node-api/package.json rename packages/{host => weak-node-api}/scripts/copy-node-api-headers.ts (82%) rename packages/{host => weak-node-api}/scripts/generate-weak-node-api.ts (87%) create mode 100644 packages/weak-node-api/src/index.ts rename packages/{host/scripts => weak-node-api/src}/node-api-functions.ts (100%) create mode 100644 packages/weak-node-api/src/restore-xcframework-symlinks.ts create mode 100644 packages/weak-node-api/src/weak-node-api.ts create mode 100644 packages/weak-node-api/tsconfig.json create mode 100644 packages/weak-node-api/tsconfig.node-scripts.json create mode 100644 packages/weak-node-api/tsconfig.node.json rename packages/{host => weak-node-api}/types/node-api-headers/index.d.ts (100%) create mode 100644 packages/weak-node-api/weak-node-api-config.cmake create mode 100644 packages/weak-node-api/weak-node-api.podspec diff --git a/.changeset/eight-walls-push.md b/.changeset/eight-walls-push.md new file mode 100644 index 00000000..58793ae4 --- /dev/null +++ b/.changeset/eight-walls-push.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Moved weak-node-api into a separate "weak-node-api" package. diff --git a/.changeset/quick-poets-greet.md b/.changeset/quick-poets-greet.md new file mode 100644 index 00000000..4b53b523 --- /dev/null +++ b/.changeset/quick-poets-greet.md @@ -0,0 +1,7 @@ +--- +"gyp-to-cmake": minor +"cmake-rn": minor +"react-native-node-api": minor +--- + +Use `find_package` instead of `include` to locate "weak-node-api" diff --git a/.changeset/spotty-beers-repeat.md b/.changeset/spotty-beers-repeat.md new file mode 100644 index 00000000..cfa21deb --- /dev/null +++ b/.changeset/spotty-beers-repeat.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": minor +--- + +No longer exporting weakNodeApiPath, import from "weak-node-api" instead diff --git a/.changeset/tired-words-relate.md b/.changeset/tired-words-relate.md new file mode 100644 index 00000000..e1a3ec02 --- /dev/null +++ b/.changeset/tired-words-relate.md @@ -0,0 +1,5 @@ +--- +"weak-node-api": patch +--- + +Initial release! diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a221cfc4..f24d9c04 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -40,9 +40,9 @@ jobs: - run: rustup target add x86_64-linux-android - run: npm ci - run: npm run build - # Bootstrap host package to get weak-node-api and ferric-example to get types + # Bootstrap weak-node-api and ferric-example to get types # TODO: Solve this by adding an option to ferric to build only types or by committing the types into the repo as a fixture for an "init" command - - run: npm run bootstrap --workspace react-native-node-api + - run: npm run bootstrap --workspace weak-node-api - run: npm run bootstrap --workspace @react-native-node-api/ferric-example - run: npm run lint env: @@ -184,9 +184,8 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Build weak-node-api for all architectures - run: npm run build-weak-node-api:android - working-directory: packages/host + - name: Build weak-node-api for all Android architectures + run: npm run build-weak-node-api:android --workspace weak-node-api - name: Build ferric-example for all architectures run: npm run build -- --android working-directory: packages/ferric-example @@ -239,11 +238,10 @@ jobs: - run: rustup toolchain install nightly --component rust-src - run: npm ci - run: npm run build - # Build weak-node-api for all Apple architectures - - run: | - npm run prepare-weak-node-api - npm run build-weak-node-api:apple - working-directory: packages/host + - name: Build weak-node-api for all Apple architectures + run: | + npm run prepare-weak-node-api --workspace weak-node-api + npm run build-weak-node-api:apple --workspace weak-node-api # Build Ferric example for all Apple architectures - run: npx ferric --apple working-directory: packages/ferric-example diff --git a/.gitignore b/.gitignore index 6c7c7bf3..6a69674c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ dist/ # Treading the MacOS app as ephemeral apps/macos-test-app + +# Cache used by the rust analyzer +target/rust-analyzer/ diff --git a/apps/test-app/package.json b/apps/test-app/package.json index 08f55a35..2f41e8c4 100644 --- a/apps/test-app/package.json +++ b/apps/test-app/package.json @@ -42,6 +42,7 @@ "react": "19.1.0", "react-native": "0.81.4", "react-native-node-api": "*", - "react-native-test-app": "^4.4.7" + "react-native-test-app": "^4.4.7", + "weak-node-api": "*" } } diff --git a/package-lock.json b/package-lock.json index 9604cb48..845f156c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "workspaces": [ "packages/cli-utils", "packages/cmake-file-api", + "packages/weak-node-api", "packages/cmake-rn", "packages/ferric", "packages/gyp-to-cmake", @@ -32,7 +33,7 @@ "prettier": "^3.6.2", "react-native": "0.81.4", "read-pkg": "^9.0.1", - "tsx": "^4.20.5", + "tsx": "^4.20.6", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" } @@ -63,7 +64,8 @@ "react": "19.1.0", "react-native": "0.81.4", "react-native-node-api": "*", - "react-native-test-app": "^4.4.7" + "react-native-test-app": "^4.4.7", + "weak-node-api": "*" } }, "node_modules/@actions/core": { @@ -14006,9 +14008,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.20.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", - "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", "dependencies": { @@ -14355,6 +14357,10 @@ "defaults": "^1.0.3" } }, + "node_modules/weak-node-api": { + "resolved": "packages/weak-node-api", + "link": true + }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", @@ -14825,11 +14831,11 @@ } }, "packages/cmake-rn": { - "version": "0.5.1", + "version": "0.5.2", "dependencies": { "@react-native-node-api/cli-utils": "0.1.1", "cmake-file-api": "0.1.0", - "react-native-node-api": "0.6.1", + "react-native-node-api": "0.6.2", "zod": "^4.1.11" }, "bin": { @@ -14842,11 +14848,11 @@ }, "packages/ferric": { "name": "ferric-cli", - "version": "0.3.6", + "version": "0.3.7", "dependencies": { "@napi-rs/cli": "~3.0.3", "@react-native-node-api/cli-utils": "0.1.1", - "react-native-node-api": "0.6.1" + "react-native-node-api": "0.6.2" }, "bin": { "ferric": "bin/ferric.js" @@ -14873,7 +14879,7 @@ }, "packages/host": { "name": "react-native-node-api", - "version": "0.6.1", + "version": "0.6.2", "license": "MIT", "dependencies": { "@expo/plist": "^0.4.7", @@ -14888,12 +14894,12 @@ "devDependencies": { "@babel/core": "^7.26.10", "@babel/types": "^7.27.0", - "fswin": "^3.24.829", - "node-api-headers": "^1.5.0" + "fswin": "^3.24.829" }, "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" + "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.1" } }, "packages/node-addon-examples": { @@ -14920,6 +14926,14 @@ "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } + }, + "packages/weak-node-api": { + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "node-api-headers": "^1.5.0", + "zod": "^4.1.11" + } } } } diff --git a/package.json b/package.json index be2dbf31..668f6c31 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "workspaces": [ "packages/cli-utils", "packages/cmake-file-api", + "packages/weak-node-api", "packages/cmake-rn", "packages/ferric", "packages/gyp-to-cmake", @@ -66,7 +67,7 @@ "prettier": "^3.6.2", "react-native": "0.81.4", "read-pkg": "^9.0.1", - "tsx": "^4.20.5", + "tsx": "^4.20.6", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" } diff --git a/packages/cmake-rn/README.md b/packages/cmake-rn/README.md index ba41aecb..61772ce2 100644 --- a/packages/cmake-rn/README.md +++ b/packages/cmake-rn/README.md @@ -10,14 +10,14 @@ Android's dynamic linker imposes restrictions on the access to global symbols (s The implementation of Node-API is split between Hermes and our host package and to avoid addons having to explicitly link against either, we've introduced a `weak-node-api` library (published in `react-native-node-api` package). This library exposes only Node-API and will have its implementation injected by the host. -To link against `weak-node-api` just include the CMake config exposed through `WEAK_NODE_API_CONFIG` and add `weak-node-api` to the `target_link_libraries` of the addon's library target. +To link against `weak-node-api` just use `find_package` to import the `weak-node-api` target and add it to the `target_link_libraries` of the addon's library target. ```cmake cmake_minimum_required(VERSION 3.15...3.31) project(tests-buffers) # Defines the "weak-node-api" target -include(${WEAK_NODE_API_CONFIG}) +find_package(weak-node-api REQUIRED CONFIG) add_library(addon SHARED addon.c) target_link_libraries(addon PRIVATE weak-node-api) diff --git a/packages/cmake-rn/src/weak-node-api.ts b/packages/cmake-rn/src/weak-node-api.ts index ee6cb154..44b2e79c 100644 --- a/packages/cmake-rn/src/weak-node-api.ts +++ b/packages/cmake-rn/src/weak-node-api.ts @@ -6,9 +6,14 @@ import { isAndroidTriplet, isAppleTriplet, SupportedTriplet, - weakNodeApiPath, } from "react-native-node-api"; +import { + applePrebuildPath, + androidPrebuildPath, + weakNodeApiCmakePath, +} from "weak-node-api"; + import { ANDROID_ARCHITECTURES } from "./platforms/android.js"; import { getNodeAddonHeadersPath, getNodeApiHeadersPath } from "./headers.js"; @@ -20,19 +25,14 @@ export function getWeakNodeApiPath( triplet: SupportedTriplet | "apple", ): string { if (triplet === "apple" || isAppleTriplet(triplet)) { - const xcframeworkPath = path.join( - weakNodeApiPath, - "weak-node-api.xcframework", - ); assert( - fs.existsSync(xcframeworkPath), - `Expected an XCFramework at ${xcframeworkPath}`, + fs.existsSync(applePrebuildPath), + `Expected an XCFramework at ${applePrebuildPath}`, ); - return xcframeworkPath; + return applePrebuildPath; } else if (isAndroidTriplet(triplet)) { const libraryPath = path.join( - weakNodeApiPath, - "weak-node-api.android.node", + androidPrebuildPath, ANDROID_ARCHITECTURES[triplet], "libweak-node-api.so", ); @@ -58,8 +58,10 @@ export function getWeakNodeApiVariables( triplet: SupportedTriplet | "apple", ): Record { return { - // Expose an includable CMake config file declaring the weak-node-api target - WEAK_NODE_API_CONFIG: path.join(weakNodeApiPath, "weak-node-api.cmake"), + // Enable use of `find_package(weak-node-api REQUIRED CONFIG)` + "weak-node-api_DIR": path.dirname(weakNodeApiCmakePath), + // Enable use of `include(${WEAK_NODE_API_CONFIG})` + WEAK_NODE_API_CONFIG: weakNodeApiCmakePath, WEAK_NODE_API_INC: getNodeApiIncludePaths().join(";"), WEAK_NODE_API_LIB: getWeakNodeApiPath(triplet), }; diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index 4671a47c..4eee45b2 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -9,7 +9,7 @@ import { UsageError, spawn, } from "@react-native-node-api/cli-utils"; -import { weakNodeApiPath } from "react-native-node-api"; +import { applePrebuildPath, androidPrebuildPath } from "weak-node-api"; import { AndroidTargetName, @@ -169,25 +169,20 @@ export function getTargetAndroidPlatform(target: AndroidTargetName) { } export function getWeakNodeApiFrameworkPath(target: AppleTargetName) { - const xcframeworkPath = joinPathAndAssertExistence( - weakNodeApiPath, - "weak-node-api.xcframework", - ); const result = APPLE_XCFRAMEWORK_SLICES_PER_TARGET[target].find((slice) => { - const candidatePath = path.join(xcframeworkPath, slice); + const candidatePath = path.join(applePrebuildPath, slice); return fs.existsSync(candidatePath); }); assert( result, `No matching slice found in weak-node-api.xcframework for target ${target}`, ); - return joinPathAndAssertExistence(xcframeworkPath, result); + return joinPathAndAssertExistence(applePrebuildPath, result); } export function getWeakNodeApiAndroidLibraryPath(target: AndroidTargetName) { return joinPathAndAssertExistence( - weakNodeApiPath, - "weak-node-api.android.node", + androidPrebuildPath, ANDROID_ARCH_PR_TARGET[target], ); } diff --git a/packages/gyp-to-cmake/src/transformer.ts b/packages/gyp-to-cmake/src/transformer.ts index 3c9b65d8..a5660eba 100644 --- a/packages/gyp-to-cmake/src/transformer.ts +++ b/packages/gyp-to-cmake/src/transformer.ts @@ -85,7 +85,7 @@ export function bindingGypToCmakeLists({ ]; if (weakNodeApi) { - lines.push(`include(\${WEAK_NODE_API_CONFIG})`, ""); + lines.push(`find_package(weak-node-api REQUIRED CONFIG)`, ""); } for (const target of gyp.targets) { diff --git a/packages/host/.gitignore b/packages/host/.gitignore index b3f12e5b..5ba3e2fe 100644 --- a/packages/host/.gitignore +++ b/packages/host/.gitignore @@ -16,12 +16,5 @@ include/ android/.cxx/ android/build/ -# Everything in weak-node-api is generated, except for the configurations -# Generated and built bia `npm run build-weak-node-api-injector` -/weak-node-api/build/ -/weak-node-api/*.xcframework -/weak-node-api/*.android.node -/weak-node-api/weak_node_api.cpp -/weak-node-api/weak_node_api.hpp # Generated via `npm run generate-weak-node-api-injector` /cpp/WeakNodeApiInjector.cpp diff --git a/packages/host/android/CMakeLists.txt b/packages/host/android/CMakeLists.txt index 3e9fd392..e4c183f7 100644 --- a/packages/host/android/CMakeLists.txt +++ b/packages/host/android/CMakeLists.txt @@ -5,12 +5,7 @@ set(CMAKE_CXX_STANDARD 20) find_package(ReactAndroid REQUIRED CONFIG) find_package(hermes-engine REQUIRED CONFIG) - -add_library(weak-node-api INTERFACE) -target_include_directories(weak-node-api INTERFACE - ../weak-node-api - ../weak-node-api/include -) +find_package(weak-node-api REQUIRED CONFIG) add_library(node-api-host SHARED src/main/cpp/OnLoad.cpp diff --git a/packages/host/android/build.gradle b/packages/host/android/build.gradle index 41c7b7bd..2f6b6be8 100644 --- a/packages/host/android/build.gradle +++ b/packages/host/android/build.gradle @@ -14,6 +14,17 @@ if (!System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR")) { ].join('\n')) } +def findWeakNodeApiDir() { + def searchDir = rootDir.toPath() + do { + def p = searchDir.resolve("node_modules/weak-node-api") + if (p.toFile().exists()) { + return p.toRealPath().toString() + } + } while (searchDir = searchDir.getParent()) + throw new GradleException("Could not find `weak-node-api`"); +} + buildscript { ext.getExtOrDefault = {name -> return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['NodeApiModules_' + name] @@ -61,8 +72,11 @@ android { sourceSets { main { manifest.srcFile "src/main/AndroidManifestNew.xml" - // Include the weak-node-api to enable a dynamic load - jniLibs.srcDirs += ["../weak-node-api/weak-node-api.android.node"] + // Include the weak-node-api native libraries directly + jniLibs.srcDirs += [ + "../../weak-node-api/build/Debug/weak-node-api.android.node", + "../../weak-node-api/build/Release/weak-node-api.android.node" + ] } } } @@ -71,7 +85,8 @@ android { compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") buildFeatures { - prefab = true + buildConfig true + prefab true } defaultConfig { @@ -82,7 +97,7 @@ android { cmake { targets "node-api-host" cppFlags "-frtti -fexceptions -Wall -fstack-protector-all" - arguments "-DANDROID_STL=c++_shared" + arguments "-DANDROID_STL=c++_shared", "-Dweak-node-api_DIR=${findWeakNodeApiDir()}" abiFilters (*reactNativeArchitectures()) buildTypes { @@ -103,10 +118,6 @@ android { } } - buildFeatures { - buildConfig true - } - buildTypes { debug { jniDebuggable true diff --git a/packages/host/package.json b/packages/host/package.json index 1edcc6ba..2f1199bf 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -43,33 +43,18 @@ ], "scripts": { "build": "tsc --build", - "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", - "generate-weak-node-api": "tsx scripts/generate-weak-node-api.ts", - "generate-weak-node-api-injector": "tsx scripts/generate-weak-node-api-injector.ts", - "prepare-weak-node-api": "node --run copy-node-api-headers && node --run generate-weak-node-api-injector && node --run generate-weak-node-api", - "build-weak-node-api": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension --source ./weak-node-api --out ./weak-node-api", - "build-weak-node-api:android": "node --run build-weak-node-api -- --android", - "build-weak-node-api:apple": "node --run build-weak-node-api -- --apple", - "build-weak-node-api:all": "node --run build-weak-node-api -- --android --apple", + "generate-weak-node-api-injector": "node scripts/generate-weak-node-api-injector.mts", "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", "test:gradle": "ENABLE_GRADLE_TESTS=true node --run test", - "bootstrap": "node --run prepare-weak-node-api && node --run build-weak-node-api", - "prerelease": "node --run prepare-weak-node-api && node --run build-weak-node-api:all" + "bootstrap": "node --run generate-weak-node-api-injector", + "prerelease": "node --run generate-weak-node-api-injector" }, "keywords": [ - "react-native", "node-api", "napi", - "node-api", "node-addon-api", "native", - "addon", - "module", - "c", - "c++", - "bindings", - "buildtools", - "cmake" + "addon" ], "author": { "name": "Callstack", @@ -92,11 +77,11 @@ "devDependencies": { "@babel/core": "^7.26.10", "@babel/types": "^7.27.0", - "fswin": "^3.24.829", - "node-api-headers": "^1.5.0" + "fswin": "^3.24.829" }, "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" + "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.1" } } diff --git a/packages/host/react-native-node-api.podspec b/packages/host/react-native-node-api.podspec index 5ed4072a..ab4b0e75 100644 --- a/packages/host/react-native-node-api.podspec +++ b/packages/host/react-native-node-api.podspec @@ -31,10 +31,11 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/callstackincubator/react-native-node-api.git", :tag => "#{s.version}" } - s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "weak-node-api/include/*.h", "weak-node-api/*.hpp" - s.public_header_files = "weak-node-api/include/*.h" + s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}" - s.vendored_frameworks = "auto-linked/apple/*.xcframework", "weak-node-api/weak-node-api.xcframework" + s.dependency "weak-node-api" + + s.vendored_frameworks = "auto-linked/apple/*.xcframework" s.script_phase = { :name => 'Copy Node-API xcframeworks', :execution_position => :before_compile, diff --git a/packages/host/scripts/generate-weak-node-api-injector.ts b/packages/host/scripts/generate-weak-node-api-injector.mts similarity index 94% rename from packages/host/scripts/generate-weak-node-api-injector.ts rename to packages/host/scripts/generate-weak-node-api-injector.mts index d5adfd83..71acfe86 100644 --- a/packages/host/scripts/generate-weak-node-api-injector.ts +++ b/packages/host/scripts/generate-weak-node-api-injector.mts @@ -2,9 +2,9 @@ import fs from "node:fs"; import path from "node:path"; import cp from "node:child_process"; -import { FunctionDecl, getNodeApiFunctions } from "./node-api-functions"; +import { type FunctionDecl, getNodeApiFunctions } from "weak-node-api"; -export const CPP_SOURCE_PATH = path.join(__dirname, "../cpp"); +export const CPP_SOURCE_PATH = path.join(import.meta.dirname, "../cpp"); // TODO: Remove when all runtime Node API functions are implemented const IMPLEMENTED_RUNTIME_FUNCTIONS = [ @@ -28,9 +28,10 @@ const IMPLEMENTED_RUNTIME_FUNCTIONS = [ export function generateSource(functions: FunctionDecl[]) { return ` // This file is generated by react-native-node-api - #include #include #include + + #include #include #include diff --git a/packages/host/src/node/cli/program.ts b/packages/host/src/node/cli/program.ts index 85b9016f..169ca85b 100644 --- a/packages/host/src/node/cli/program.ts +++ b/packages/host/src/node/cli/program.ts @@ -1,7 +1,6 @@ import assert from "node:assert/strict"; import path from "node:path"; import { EventEmitter } from "node:stream"; -import fs from "node:fs"; import { Command, @@ -27,9 +26,8 @@ import { import { command as vendorHermes } from "./hermes"; import { packageNameOption, pathSuffixOption } from "./options"; import { linkModules, pruneLinkedModules, ModuleLinker } from "./link-modules"; -import { linkXcframework, restoreFrameworkLinks } from "./apple"; +import { linkXcframework } from "./apple"; import { linkAndroidDir } from "./android"; -import { weakNodeApiPath } from "../weak-node-api"; // We're attaching a lot of listeners when spawning in parallel EventEmitter.defaultMaxListeners = 100; @@ -171,38 +169,6 @@ program await pruneLinkedModules(platform, modules); } } - - if (apple) { - await oraPromise( - async () => { - const xcframeworkPath = path.join( - weakNodeApiPath, - "weak-node-api.xcframework", - ); - await Promise.all( - [ - path.join(xcframeworkPath, "macos-x86_64"), - path.join(xcframeworkPath, "macos-arm64"), - path.join(xcframeworkPath, "macos-arm64_x86_64"), - ].map(async (slicePath) => { - const frameworkPath = path.join( - slicePath, - "weak-node-api.framework", - ); - if (fs.existsSync(frameworkPath)) { - await restoreFrameworkLinks(frameworkPath); - } - }), - ); - }, - { - text: "Restoring weak-node-api symlinks", - successText: "Restored weak-node-api symlinks", - failText: (error) => - `Failed to restore weak-node-api symlinks: ${error.message}`, - }, - ); - } }, ), ); diff --git a/packages/host/src/node/index.ts b/packages/host/src/node/index.ts index 3071f233..1c4c69a9 100644 --- a/packages/host/src/node/index.ts +++ b/packages/host/src/node/index.ts @@ -26,5 +26,3 @@ export { determineLibraryBasename, dereferenceDirectory, } from "./path-utils.js"; - -export { weakNodeApiPath } from "./weak-node-api.js"; diff --git a/packages/host/src/node/weak-node-api.ts b/packages/host/src/node/weak-node-api.ts deleted file mode 100644 index 02e3befe..00000000 --- a/packages/host/src/node/weak-node-api.ts +++ /dev/null @@ -1,10 +0,0 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import path from "node:path"; - -export const weakNodeApiPath = path.resolve(__dirname, "../../weak-node-api"); - -assert( - fs.existsSync(weakNodeApiPath), - `Expected Weak Node API path to exist: ${weakNodeApiPath}`, -); diff --git a/packages/host/tsconfig.node-scripts.json b/packages/host/tsconfig.node-scripts.json index 4e11d816..b3771a38 100644 --- a/packages/host/tsconfig.node-scripts.json +++ b/packages/host/tsconfig.node-scripts.json @@ -7,6 +7,11 @@ "rootDir": "scripts", "types": ["node"] }, - "include": ["scripts/**/*.ts", "types/**/*.d.ts"], - "exclude": [] + "include": ["scripts/**/*.mts", "types/**/*.d.ts"], + "exclude": [], + "references": [ + { + "path": "../weak-node-api/tsconfig.node.json" + } + ] } diff --git a/packages/host/tsconfig.node.json b/packages/host/tsconfig.node.json index bf847c8c..e0982db2 100644 --- a/packages/host/tsconfig.node.json +++ b/packages/host/tsconfig.node.json @@ -8,5 +8,10 @@ "types": ["node"] }, "include": ["src/node/**/*.ts", "types/**/*.d.ts"], - "exclude": ["**/*.test.ts"] + "exclude": ["**/*.test.ts"], + "references": [ + { + "path": "../weak-node-api/tsconfig.node.json" + } + ] } diff --git a/packages/host/weak-node-api/weak-node-api.cmake b/packages/host/weak-node-api/weak-node-api.cmake deleted file mode 100644 index 2fda647e..00000000 --- a/packages/host/weak-node-api/weak-node-api.cmake +++ /dev/null @@ -1,6 +0,0 @@ -add_library(weak-node-api SHARED IMPORTED) - -set_target_properties(weak-node-api PROPERTIES - IMPORTED_LOCATION "${WEAK_NODE_API_LIB}" - INTERFACE_INCLUDE_DIRECTORIES "${WEAK_NODE_API_INC}" -) diff --git a/packages/node-addon-examples/tests/async/CMakeLists.txt b/packages/node-addon-examples/tests/async/CMakeLists.txt index 2b6b2b81..67e5448b 100644 --- a/packages/node-addon-examples/tests/async/CMakeLists.txt +++ b/packages/node-addon-examples/tests/async/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.15...3.31) project(async-test) -include(${WEAK_NODE_API_CONFIG}) +find_package(weak-node-api REQUIRED CONFIG) add_library(addon SHARED addon.c) diff --git a/packages/node-addon-examples/tests/buffers/CMakeLists.txt b/packages/node-addon-examples/tests/buffers/CMakeLists.txt index 8d7ac2d2..da615db2 100644 --- a/packages/node-addon-examples/tests/buffers/CMakeLists.txt +++ b/packages/node-addon-examples/tests/buffers/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.15...3.31) project(buffers-test) -include(${WEAK_NODE_API_CONFIG}) +find_package(weak-node-api REQUIRED CONFIG) add_library(addon SHARED addon.c) diff --git a/packages/weak-node-api/.gitignore b/packages/weak-node-api/.gitignore new file mode 100644 index 00000000..30ea0716 --- /dev/null +++ b/packages/weak-node-api/.gitignore @@ -0,0 +1,11 @@ + +# Everything in weak-node-api is generated, except for the configurations +# Generated and built via `npm run bootstrap` +/build/ +/*.xcframework +/*.android.node +/generated/weak_node_api.cpp +/generated/weak_node_api.hpp + +# Copied from node-api-headers by scripts/copy-node-api-headers.ts +/include/ diff --git a/packages/host/weak-node-api/CMakeLists.txt b/packages/weak-node-api/CMakeLists.txt similarity index 96% rename from packages/host/weak-node-api/CMakeLists.txt rename to packages/weak-node-api/CMakeLists.txt index 08422d62..de2784e0 100644 --- a/packages/host/weak-node-api/CMakeLists.txt +++ b/packages/weak-node-api/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.15) project(weak-node-api) add_library(${PROJECT_NAME} SHARED - weak_node_api.cpp + generated/weak_node_api.cpp ) # Stripping the prefix from the library name diff --git a/packages/weak-node-api/README.md b/packages/weak-node-api/README.md new file mode 100644 index 00000000..073c9121 --- /dev/null +++ b/packages/weak-node-api/README.md @@ -0,0 +1,19 @@ +# Weak Node-API + +A clean linkable interface for Node-API and with runtime-injectable implementation. + +This package is part of the [Node-API for React Native](https://github.com/callstackincubator/react-native-node-api) project, which brings Node-API support to React Native applications. However, it can be used independently in any context where an indirect / weak Node-API implementation is needed. + +## Why is this needed? + +Android's dynamic linker restricts access to global symbols—dynamic libraries must explicitly declare dependencies as `DT_NEEDED` to access symbols. In the context of React Native, the Node-API implementation is split between Hermes and a host runtime, native addons built for Android would otherwise need to explicitly link against both - which is not ideal for multiple reasons. + +This library provides a solution by: + +- Exposing only Node-API functions without implementation +- Allowing runtime injection of the actual implementation by the host +- Eliminating the need for addons to suppress undefined symbol errors + +## Is this usable in the context of Node.js? + +While originally designed for React Native's split Node-API implementation, this approach could potentially be adapted for Node.js scenarios where addons need to link with undefined symbols allowed. Usage patterns and examples for Node.js contexts are being explored and this pattern could eventually be upstreamed to Node.js itself, benefiting the broader Node-API ecosystem. diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json new file mode 100644 index 00000000..63cfb410 --- /dev/null +++ b/packages/weak-node-api/package.json @@ -0,0 +1,65 @@ +{ + "name": "weak-node-api", + "version": "0.0.1", + "description": "A linkable and runtime-injectable Node-API", + "homepage": "https://github.com/callstackincubator/react-native-node-api", + "repository": { + "type": "git", + "url": "git+https://github.com/callstackincubator/react-native-node-api.git", + "directory": "packages/weak-node-api" + }, + "main": "dist/index.js", + "type": "module", + "files": [ + "dist", + "!dist/**/*.test.d.ts", + "!dist/**/*.test.d.ts.map", + "include", + "build/Debug", + "build/Release", + "*.podspec", + "*.cmake" + ], + "scripts": { + "build": "tsc --build", + "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", + "generate-weak-node-api": "tsx scripts/generate-weak-node-api.ts", + "prepare-weak-node-api": "node --run copy-node-api-headers && node --run generate-weak-node-api", + "build-weak-node-api": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension", + "build-weak-node-api:android": "node --run build-weak-node-api -- --android", + "build-weak-node-api:apple": "node --run build-weak-node-api -- --apple", + "build-weak-node-api:all": "node --run build-weak-node-api -- --android --apple", + "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", + "bootstrap": "node --run prepare-weak-node-api && node --run build-weak-node-api", + "prerelease": "node --run prepare-weak-node-api && node --run build-weak-node-api:all" + }, + "keywords": [ + "react-native", + "napi", + "node-api", + "node-addon-api", + "native", + "addon", + "module", + "c", + "c++", + "bindings", + "buildtools", + "cmake" + ], + "author": { + "name": "Callstack", + "url": "https://github.com/callstackincubator" + }, + "contributors": [ + { + "name": "Kræn Hansen", + "url": "https://github.com/kraenhansen" + } + ], + "license": "MIT", + "devDependencies": { + "node-api-headers": "^1.5.0", + "zod": "^4.1.11" + } +} diff --git a/packages/host/scripts/copy-node-api-headers.ts b/packages/weak-node-api/scripts/copy-node-api-headers.ts similarity index 82% rename from packages/host/scripts/copy-node-api-headers.ts rename to packages/weak-node-api/scripts/copy-node-api-headers.ts index 9632627e..2bfd43dc 100644 --- a/packages/host/scripts/copy-node-api-headers.ts +++ b/packages/weak-node-api/scripts/copy-node-api-headers.ts @@ -3,7 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { include_dir as includeSourcePath } from "node-api-headers"; -const includeDestinationPath = path.join(__dirname, "../weak-node-api/include"); +const includeDestinationPath = path.join(import.meta.dirname, "../include"); assert(fs.existsSync(includeSourcePath), `Expected ${includeSourcePath}`); console.log(`Copying ${includeSourcePath} to ${includeDestinationPath}`); fs.cpSync(includeSourcePath, includeDestinationPath, { recursive: true }); diff --git a/packages/host/scripts/generate-weak-node-api.ts b/packages/weak-node-api/scripts/generate-weak-node-api.ts similarity index 87% rename from packages/host/scripts/generate-weak-node-api.ts rename to packages/weak-node-api/scripts/generate-weak-node-api.ts index 1090de41..b9a8736c 100644 --- a/packages/host/scripts/generate-weak-node-api.ts +++ b/packages/weak-node-api/scripts/generate-weak-node-api.ts @@ -2,9 +2,12 @@ import fs from "node:fs"; import path from "node:path"; import cp from "node:child_process"; -import { FunctionDecl, getNodeApiFunctions } from "./node-api-functions"; +import { + FunctionDecl, + getNodeApiFunctions, +} from "../src/node-api-functions.js"; -export const WEAK_NODE_API_PATH = path.join(__dirname, "../weak-node-api"); +export const OUTPUT_PATH = path.join(import.meta.dirname, "../generated"); /** * Generates source code for a version script for the given Node API version. @@ -67,17 +70,17 @@ export function generateSource(functions: FunctionDecl[]) { } async function run() { - await fs.promises.mkdir(WEAK_NODE_API_PATH, { recursive: true }); + await fs.promises.mkdir(OUTPUT_PATH, { recursive: true }); const nodeApiFunctions = getNodeApiFunctions(); const header = generateHeader(nodeApiFunctions); - const headerPath = path.join(WEAK_NODE_API_PATH, "weak_node_api.hpp"); + const headerPath = path.join(OUTPUT_PATH, "weak_node_api.hpp"); await fs.promises.writeFile(headerPath, header, "utf-8"); cp.spawnSync("clang-format", ["-i", headerPath], { stdio: "inherit" }); const source = generateSource(nodeApiFunctions); - const sourcePath = path.join(WEAK_NODE_API_PATH, "weak_node_api.cpp"); + const sourcePath = path.join(OUTPUT_PATH, "weak_node_api.cpp"); await fs.promises.writeFile(sourcePath, source, "utf-8"); cp.spawnSync("clang-format", ["-i", sourcePath], { stdio: "inherit" }); } diff --git a/packages/weak-node-api/src/index.ts b/packages/weak-node-api/src/index.ts new file mode 100644 index 00000000..fbd5337a --- /dev/null +++ b/packages/weak-node-api/src/index.ts @@ -0,0 +1,2 @@ +export * from "./weak-node-api.js"; +export * from "./node-api-functions.js"; diff --git a/packages/host/scripts/node-api-functions.ts b/packages/weak-node-api/src/node-api-functions.ts similarity index 100% rename from packages/host/scripts/node-api-functions.ts rename to packages/weak-node-api/src/node-api-functions.ts diff --git a/packages/weak-node-api/src/restore-xcframework-symlinks.ts b/packages/weak-node-api/src/restore-xcframework-symlinks.ts new file mode 100644 index 00000000..41373809 --- /dev/null +++ b/packages/weak-node-api/src/restore-xcframework-symlinks.ts @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +import { applePrebuildPath } from "./weak-node-api.js"; + +async function restoreVersionedFrameworkSymlinks(frameworkPath: string) { + const currentLinkPath = path.join(frameworkPath, "Versions", "Current"); + + if (!fs.existsSync(currentLinkPath)) { + await fs.promises.symlink("A", currentLinkPath); + } + + const binaryLinkPath = path.join(frameworkPath, "weak-node-api"); + + if (!fs.existsSync(binaryLinkPath)) { + await fs.promises.symlink("Versions/Current/weak-node-api", binaryLinkPath); + } + + const resourcesLinkPath = path.join(frameworkPath, "Resources"); + + if (!fs.existsSync(resourcesLinkPath)) { + await fs.promises.symlink("Versions/Current/Resources", resourcesLinkPath); + } +} + +if (process.platform === "darwin") { + assert( + fs.existsSync(applePrebuildPath), + `Expected an Xcframework at ${applePrebuildPath}`, + ); + + const macosFrameworkPath = path.join( + applePrebuildPath, + "macos-arm64_x86_64", + "weak-node-api.framework", + ); + + if (fs.existsSync(macosFrameworkPath)) { + await restoreVersionedFrameworkSymlinks(macosFrameworkPath); + } +} diff --git a/packages/weak-node-api/src/weak-node-api.ts b/packages/weak-node-api/src/weak-node-api.ts new file mode 100644 index 00000000..87802461 --- /dev/null +++ b/packages/weak-node-api/src/weak-node-api.ts @@ -0,0 +1,26 @@ +import path from "node:path"; +import fs from "node:fs"; + +export const weakNodeApiPath = path.resolve(import.meta.dirname, ".."); + +const debugOutputPath = path.resolve(weakNodeApiPath, "build", "Debug"); +const releaseOutputPath = path.resolve(weakNodeApiPath, "build", "Release"); + +export const outputPath = fs.existsSync(debugOutputPath) + ? debugOutputPath + : releaseOutputPath; + +export const applePrebuildPath = path.resolve( + outputPath, + "weak-node-api.xcframework", +); + +export const androidPrebuildPath = path.resolve( + outputPath, + "weak-node-api.android.node", +); + +export const weakNodeApiCmakePath = path.resolve( + weakNodeApiPath, + "weak-node-api-config.cmake", +); diff --git a/packages/weak-node-api/tsconfig.json b/packages/weak-node-api/tsconfig.json new file mode 100644 index 00000000..f08015f8 --- /dev/null +++ b/packages/weak-node-api/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true + }, + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.node-scripts.json" } + ] +} diff --git a/packages/weak-node-api/tsconfig.node-scripts.json b/packages/weak-node-api/tsconfig.node-scripts.json new file mode 100644 index 00000000..4bc586fa --- /dev/null +++ b/packages/weak-node-api/tsconfig.node-scripts.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "rootDir": "scripts", + "types": ["node"] + }, + "include": ["scripts/**/*.ts", "types/**/*.d.ts"], + "exclude": [], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/weak-node-api/tsconfig.node.json b/packages/weak-node-api/tsconfig.node.json new file mode 100644 index 00000000..0028899f --- /dev/null +++ b/packages/weak-node-api/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "composite": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*.ts", "types/**/*.d.ts"], + "exclude": ["**/*.test.ts"] +} diff --git a/packages/host/types/node-api-headers/index.d.ts b/packages/weak-node-api/types/node-api-headers/index.d.ts similarity index 100% rename from packages/host/types/node-api-headers/index.d.ts rename to packages/weak-node-api/types/node-api-headers/index.d.ts diff --git a/packages/weak-node-api/weak-node-api-config.cmake b/packages/weak-node-api/weak-node-api-config.cmake new file mode 100644 index 00000000..7d4d4a05 --- /dev/null +++ b/packages/weak-node-api/weak-node-api-config.cmake @@ -0,0 +1,39 @@ + +# Get the current file directory +get_filename_component(WEAK_NODE_API_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) + +if(NOT DEFINED WEAK_NODE_API_LIB) + # Auto-detect library path for Android NDK builds + if(ANDROID) + # Define the library path pattern for Android + set(WEAK_NODE_API_LIB_PATH "weak-node-api.android.node/${ANDROID_ABI}/libweak-node-api.so") + + # Try Debug first, then Release using the packaged Android node structure + set(WEAK_NODE_API_LIB_DEBUG "${WEAK_NODE_API_CMAKE_DIR}/build/Debug/${WEAK_NODE_API_LIB_PATH}") + set(WEAK_NODE_API_LIB_RELEASE "${WEAK_NODE_API_CMAKE_DIR}/build/Release/${WEAK_NODE_API_LIB_PATH}") + + if(EXISTS "${WEAK_NODE_API_LIB_DEBUG}") + set(WEAK_NODE_API_LIB "${WEAK_NODE_API_LIB_DEBUG}") + message(STATUS "Using Debug weak-node-api library: ${WEAK_NODE_API_LIB}") + elseif(EXISTS "${WEAK_NODE_API_LIB_RELEASE}") + set(WEAK_NODE_API_LIB "${WEAK_NODE_API_LIB_RELEASE}") + message(STATUS "Using Release weak-node-api library: ${WEAK_NODE_API_LIB}") + else() + message(FATAL_ERROR "Could not find weak-node-api library for Android ABI ${ANDROID_ABI}. Expected at:\n ${WEAK_NODE_API_LIB_DEBUG}\n ${WEAK_NODE_API_LIB_RELEASE}") + endif() + else() + message(FATAL_ERROR "WEAK_NODE_API_LIB is not set") + endif() +endif() + +if(NOT DEFINED WEAK_NODE_API_INC) + set(WEAK_NODE_API_INC "${WEAK_NODE_API_CMAKE_DIR}/include;${WEAK_NODE_API_CMAKE_DIR}/generated") + message(STATUS "Using weak-node-api include directories: ${WEAK_NODE_API_INC}") +endif() + +add_library(weak-node-api SHARED IMPORTED) + +set_target_properties(weak-node-api PROPERTIES + IMPORTED_LOCATION "${WEAK_NODE_API_LIB}" + INTERFACE_INCLUDE_DIRECTORIES "${WEAK_NODE_API_INC}" +) diff --git a/packages/weak-node-api/weak-node-api.podspec b/packages/weak-node-api/weak-node-api.podspec new file mode 100644 index 00000000..236c6ec2 --- /dev/null +++ b/packages/weak-node-api/weak-node-api.podspec @@ -0,0 +1,34 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +# We need to restore symlinks in the versioned framework directories, +# as these are not preserved when in the archive uploaded to NPM +unless defined?(@restored) + RESTORE_COMMAND = "node '#{File.join(__dir__, "dist/restore-xcframework-symlinks.js")}'" + Pod::UI.info("[weak-node-api] ".green + "Restoring symbolic links in Xcframework") + system(RESTORE_COMMAND) or raise "Failed to restore symlinks in Xcframework" + # Setting a flag to avoid running this command on every require + @restored = true +end + +Pod::Spec.new do |s| + s.name = package["name"] + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.source = { :git => "https://github.com/callstackincubator/react-native-node-api.git", :tag => "#{s.version}" } + + # TODO: These headers could be included in the Xcframework? + # (tracked by https://github.com/callstackincubator/react-native-node-api/issues/315) + s.source_files = "generated/*.hpp", "include/*.h" + s.public_header_files = "generated/*.hpp", "include/*.h" + + s.vendored_frameworks = "build/*/weak-node-api.xcframework" + + # Avoiding the header dir to allow for idiomatic Node-API includes + s.header_dir = nil +end \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 4ca25b74..2d49bdb1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ { "path": "./packages/cmake-rn/tsconfig.json" }, { "path": "./packages/ferric/tsconfig.json" }, { "path": "./packages/node-addon-examples/tsconfig.json" }, - { "path": "./packages/node-tests/tsconfig.json" } + { "path": "./packages/node-tests/tsconfig.json" }, + { "path": "./packages/weak-node-api/tsconfig.json" } ] } From 9a5f8e6c2ec6a7c546c15cf4335e198becdf1934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 6 Nov 2025 10:34:41 +0100 Subject: [PATCH 59/82] Include headers in `weak-node-api` Xcframework (#320) * Add weak-node-api headers to the Apple framework * Refactor restoring symlinks * Restore Headers symlink too --- packages/host/src/node/cli/apple.test.ts | 12 +-- packages/host/src/node/cli/apple.ts | 88 +++++++++++-------- packages/weak-node-api/CMakeLists.txt | 35 ++++++-- .../src/restore-xcframework-symlinks.ts | 53 ++++++++--- packages/weak-node-api/weak-node-api.podspec | 3 - 5 files changed, 120 insertions(+), 71 deletions(-) diff --git a/packages/host/src/node/cli/apple.test.ts b/packages/host/src/node/cli/apple.test.ts index 7d32afde..797ce905 100644 --- a/packages/host/src/node/cli/apple.test.ts +++ b/packages/host/src/node/cli/apple.test.ts @@ -9,7 +9,7 @@ import { readAndParsePlist, readFrameworkInfo, readXcframeworkInfo, - restoreFrameworkLinks, + restoreVersionedFrameworkSymlinks, } from "./apple"; import { setupTempDirectory } from "../test-utils"; @@ -269,7 +269,7 @@ describe("apple", { skip: process.platform !== "darwin" }, () => { }); }); - describe("restoreFrameworkLinks", () => { + describe("restoreVersionedFrameworkSymlinks", () => { it("restores a versioned framework", async (context) => { const infoPlistContents = ` @@ -335,11 +335,11 @@ describe("apple", { skip: process.platform !== "darwin" }, () => { ); } - await restoreFrameworkLinks(frameworkPath); + await restoreVersionedFrameworkSymlinks(frameworkPath); await assertVersionedFramework(); // Calling again to expect a no-op - await restoreFrameworkLinks(frameworkPath); + await restoreVersionedFrameworkSymlinks(frameworkPath); await assertVersionedFramework(); }); @@ -366,8 +366,8 @@ describe("apple", { skip: process.platform !== "darwin" }, () => { const frameworkPath = path.join(tempDirectoryPath, "foo.framework"); await assert.rejects( - () => restoreFrameworkLinks(frameworkPath), - /Expected "Versions" directory inside versioned framework/, + () => restoreVersionedFrameworkSymlinks(frameworkPath), + /Expected 'Versions' directory inside versioned framework/, ); }); }); diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index 3ff64cef..31aa6779 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -192,52 +192,62 @@ export async function linkFlatFramework({ } } -/** - * NPM packages aren't preserving internal symlinks inside versioned frameworks. - * This function attempts to restore those. - */ -export async function restoreFrameworkLinks(frameworkPath: string) { - // Reconstruct missing symbolic links if needed - const versionsPath = path.join(frameworkPath, "Versions"); - const versionCurrentPath = path.join(versionsPath, "Current"); +async function restoreSymlink(target: string, linkPath: string) { + if ( + !fs.existsSync(linkPath) && + fs.existsSync(path.resolve(path.dirname(linkPath), target)) + ) { + await fs.promises.symlink(target, linkPath); + } +} +async function guessCurrentFrameworkVersion(frameworkPath: string) { + const versionsPath = path.join(frameworkPath, "Versions"); assert( fs.existsSync(versionsPath), - `Expected "Versions" directory inside versioned framework '${frameworkPath}'`, + "Expected 'Versions' directory inside versioned framework", ); - if (!fs.existsSync(versionCurrentPath)) { - const versionDirectoryEntries = await fs.promises.readdir(versionsPath, { - withFileTypes: true, - }); - const versionDirectoryPaths = versionDirectoryEntries - .filter((dirent) => dirent.isDirectory()) - .map((dirent) => path.join(dirent.parentPath, dirent.name)); - assert.equal( - versionDirectoryPaths.length, - 1, - `Expected a single directory in ${versionsPath}, found ${JSON.stringify(versionDirectoryPaths)}`, - ); - const [versionDirectoryPath] = versionDirectoryPaths; - await fs.promises.symlink( - path.relative(path.dirname(versionCurrentPath), versionDirectoryPath), - versionCurrentPath, - ); - } + const versionDirectoryEntries = await fs.promises.readdir(versionsPath, { + withFileTypes: true, + }); + const versions = versionDirectoryEntries + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + assert.equal( + versions.length, + 1, + `Expected exactly one directory in ${versionsPath}, found ${JSON.stringify(versions)}`, + ); + const [version] = versions; + return version; +} - const { CFBundleExecutable } = await readFrameworkInfo( - path.join(versionCurrentPath, "Resources", "Info.plist"), +/** + * NPM packages aren't preserving internal symlinks inside versioned frameworks. + * This function attempts to restore those. + */ +export async function restoreVersionedFrameworkSymlinks(frameworkPath: string) { + const currentVersionName = await guessCurrentFrameworkVersion(frameworkPath); + const currentVersionPath = path.join(frameworkPath, "Versions", "Current"); + await restoreSymlink(currentVersionName, currentVersionPath); + await restoreSymlink( + "Versions/Current/Resources", + path.join(frameworkPath, "Resources"), + ); + await restoreSymlink( + "Versions/Current/Headers", + path.join(frameworkPath, "Headers"), ); - const libraryRealPath = path.join(versionCurrentPath, CFBundleExecutable); - const libraryLinkPath = path.join(frameworkPath, CFBundleExecutable); - // Reconstruct missing symbolic links if needed - if (fs.existsSync(libraryRealPath) && !fs.existsSync(libraryLinkPath)) { - await fs.promises.symlink( - path.relative(path.dirname(libraryLinkPath), libraryRealPath), - libraryLinkPath, - ); - } + const { CFBundleExecutable: executableName } = await readFrameworkInfo( + path.join(currentVersionPath, "Resources", "Info.plist"), + ); + + await restoreSymlink( + path.join("Versions", "Current", executableName), + path.join(frameworkPath, executableName), + ); } export async function linkVersionedFramework({ @@ -250,7 +260,7 @@ export async function linkVersionedFramework({ "Linking Apple addons are only supported on macOS", ); - await restoreFrameworkLinks(frameworkPath); + await restoreVersionedFrameworkSymlinks(frameworkPath); const frameworkInfoPath = path.join( frameworkPath, diff --git a/packages/weak-node-api/CMakeLists.txt b/packages/weak-node-api/CMakeLists.txt index de2784e0..e53c71cd 100644 --- a/packages/weak-node-api/CMakeLists.txt +++ b/packages/weak-node-api/CMakeLists.txt @@ -1,26 +1,43 @@ -cmake_minimum_required(VERSION 3.15) +cmake_minimum_required(VERSION 3.19) project(weak-node-api) -add_library(${PROJECT_NAME} SHARED - generated/weak_node_api.cpp +# Read version from package.json +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/package.json" PACKAGE_JSON) +string(JSON PACKAGE_VERSION GET ${PACKAGE_JSON} version) + +add_library(${PROJECT_NAME} SHARED) + +set(INCLUDE_DIR "include") +set(GENERATED_SOURCE_DIR "generated") + +target_sources(${PROJECT_NAME} + PUBLIC + ${GENERATED_SOURCE_DIR}/weak_node_api.cpp + PUBLIC FILE_SET HEADERS + BASE_DIRS ${GENERATED_SOURCE_DIR} ${INCLUDE_DIR} FILES + ${GENERATED_SOURCE_DIR}/weak_node_api.hpp + ${INCLUDE_DIR}/js_native_api_types.h + ${INCLUDE_DIR}/js_native_api.h + ${INCLUDE_DIR}/node_api_types.h + ${INCLUDE_DIR}/node_api.h ) +get_target_property(PUBLIC_HEADER_FILES ${PROJECT_NAME} HEADER_SET) + # Stripping the prefix from the library name # to make sure the name of the XCFramework will match the name of the library if(APPLE) set_target_properties(${PROJECT_NAME} PROPERTIES FRAMEWORK TRUE MACOSX_FRAMEWORK_IDENTIFIER com.callstack.${PROJECT_NAME} - MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 - MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 + MACOSX_FRAMEWORK_SHORT_VERSION_STRING ${PACKAGE_VERSION} + MACOSX_FRAMEWORK_BUNDLE_VERSION ${PACKAGE_VERSION} + VERSION ${PACKAGE_VERSION} XCODE_ATTRIBUTE_SKIP_INSTALL NO + PUBLIC_HEADER "${PUBLIC_HEADER_FILES}" ) endif() -target_include_directories(${PROJECT_NAME} - PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include -) target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) target_compile_definitions(${PROJECT_NAME} PRIVATE NAPI_VERSION=8) diff --git a/packages/weak-node-api/src/restore-xcframework-symlinks.ts b/packages/weak-node-api/src/restore-xcframework-symlinks.ts index 41373809..575d9ce3 100644 --- a/packages/weak-node-api/src/restore-xcframework-symlinks.ts +++ b/packages/weak-node-api/src/restore-xcframework-symlinks.ts @@ -4,24 +4,49 @@ import path from "node:path"; import { applePrebuildPath } from "./weak-node-api.js"; -async function restoreVersionedFrameworkSymlinks(frameworkPath: string) { - const currentLinkPath = path.join(frameworkPath, "Versions", "Current"); - - if (!fs.existsSync(currentLinkPath)) { - await fs.promises.symlink("A", currentLinkPath); +async function restoreSymlink(target: string, path: string) { + if (!fs.existsSync(path)) { + await fs.promises.symlink(target, path); } +} - const binaryLinkPath = path.join(frameworkPath, "weak-node-api"); +async function guessCurrentFrameworkVersion(frameworkPath: string) { + const versionsPath = path.join(frameworkPath, "Versions"); + assert(fs.existsSync(versionsPath)); - if (!fs.existsSync(binaryLinkPath)) { - await fs.promises.symlink("Versions/Current/weak-node-api", binaryLinkPath); - } - - const resourcesLinkPath = path.join(frameworkPath, "Resources"); + const versionDirectoryEntries = await fs.promises.readdir(versionsPath, { + withFileTypes: true, + }); + const versions = versionDirectoryEntries + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); + assert.equal( + versions.length, + 1, + `Expected exactly one directory in ${versionsPath}, found ${JSON.stringify(versions)}`, + ); + const [version] = versions; + return version; +} - if (!fs.existsSync(resourcesLinkPath)) { - await fs.promises.symlink("Versions/Current/Resources", resourcesLinkPath); - } +async function restoreVersionedFrameworkSymlinks(frameworkPath: string) { + const currentVersionName = await guessCurrentFrameworkVersion(frameworkPath); + await restoreSymlink( + currentVersionName, + path.join(frameworkPath, "Versions", "Current"), + ); + await restoreSymlink( + "Versions/Current/weak-node-api", + path.join(frameworkPath, "weak-node-api"), + ); + await restoreSymlink( + "Versions/Current/Resources", + path.join(frameworkPath, "Resources"), + ); + await restoreSymlink( + "Versions/Current/Headers", + path.join(frameworkPath, "Headers"), + ); } if (process.platform === "darwin") { diff --git a/packages/weak-node-api/weak-node-api.podspec b/packages/weak-node-api/weak-node-api.podspec index 236c6ec2..26e69f74 100644 --- a/packages/weak-node-api/weak-node-api.podspec +++ b/packages/weak-node-api/weak-node-api.podspec @@ -22,11 +22,8 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/callstackincubator/react-native-node-api.git", :tag => "#{s.version}" } - # TODO: These headers could be included in the Xcframework? - # (tracked by https://github.com/callstackincubator/react-native-node-api/issues/315) s.source_files = "generated/*.hpp", "include/*.h" s.public_header_files = "generated/*.hpp", "include/*.h" - s.vendored_frameworks = "build/*/weak-node-api.xcframework" # Avoiding the header dir to allow for idiomatic Node-API includes From 8418503d2b06f6ae51655b9effcdaca0574992d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 6 Nov 2025 11:11:24 +0100 Subject: [PATCH 60/82] Add test to `weak-node-api` (#321) * Add scaffold for testing * Implemented two simple tests * Add test job in the check workflow * Using the NAPI_NO_RETURN definition instead of __attribute__((noreturn)) * Using unreachable instead of noreturn * Use C++20 --- .github/workflows/check.yml | 26 ++++++++++ packages/weak-node-api/.gitignore | 3 ++ packages/weak-node-api/CMakeLists.txt | 9 +++- .../scripts/generate-weak-node-api.ts | 48 ++++++++++++------- packages/weak-node-api/tests/CMakeLists.txt | 27 +++++++++++ packages/weak-node-api/tests/test_inject.cpp | 25 ++++++++++ 6 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 packages/weak-node-api/tests/CMakeLists.txt create mode 100644 packages/weak-node-api/tests/test_inject.cpp diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index f24d9c04..ecff59bd 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -76,6 +76,32 @@ jobs: - run: npm ci - run: npm run bootstrap - run: npm test + weak-node-api-tests: + if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'weak-node-api') + strategy: + fail-fast: false + matrix: + runner: + - ubuntu-latest + - windows-latest + - macos-latest + runs-on: ${{ matrix.runner }} + name: Weak Node-API tests (${{ matrix.runner }}) + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/jod + - run: npm ci + - run: npm run build + - name: Prepare weak-node-api + run: npm run prepare-weak-node-api --workspace weak-node-api + - name: Build and run weak-node-api C++ tests + run: | + cmake -S . -B build -DBUILD_TESTS=ON + cmake --build build + ctest --test-dir build --output-on-failure + working-directory: packages/weak-node-api test-ios: if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Apple 🍎') name: Test app (iOS) diff --git a/packages/weak-node-api/.gitignore b/packages/weak-node-api/.gitignore index 30ea0716..5cc9e939 100644 --- a/packages/weak-node-api/.gitignore +++ b/packages/weak-node-api/.gitignore @@ -9,3 +9,6 @@ # Copied from node-api-headers by scripts/copy-node-api-headers.ts /include/ + +# Clang cache +/.cache/ diff --git a/packages/weak-node-api/CMakeLists.txt b/packages/weak-node-api/CMakeLists.txt index e53c71cd..d61630f2 100644 --- a/packages/weak-node-api/CMakeLists.txt +++ b/packages/weak-node-api/CMakeLists.txt @@ -38,10 +38,17 @@ if(APPLE) ) endif() -target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17) +# C++20 is needed to use designated initializers +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_20) target_compile_definitions(${PROJECT_NAME} PRIVATE NAPI_VERSION=8) target_compile_options(${PROJECT_NAME} PRIVATE $<$:/W4 /WX> $<$>:-Wall -Wextra -Werror> ) + +option(BUILD_TESTS "Build the tests" OFF) +if(BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/packages/weak-node-api/scripts/generate-weak-node-api.ts b/packages/weak-node-api/scripts/generate-weak-node-api.ts index b9a8736c..b47aed27 100644 --- a/packages/weak-node-api/scripts/generate-weak-node-api.ts +++ b/packages/weak-node-api/scripts/generate-weak-node-api.ts @@ -18,13 +18,24 @@ export function generateHeader(functions: FunctionDecl[]) { "#include ", // Node-API "#include ", // fprintf() "#include ", // abort() + "", + // Ideally we would have just used NAPI_NO_RETURN, but + // __declspec(noreturn) (when building with Microsoft Visual C++) cannot be used on members of a struct + // TODO: If we targeted C++23 we could use std::unreachable() + "#if defined(__GNUC__)", + "#define WEAK_NODE_API_UNREACHABLE __builtin_unreachable();", + "#else", + "#define WEAK_NODE_API_UNREACHABLE __assume(0);", + "#endif", + "", // Generate the struct of function pointers "struct WeakNodeApiHost {", - ...functions.map( - ({ returnType, noReturn, name, argumentTypes }) => - `${returnType} ${ - noReturn ? " __attribute__((noreturn))" : "" - }(*${name})(${argumentTypes.join(", ")});`, + ...functions.map(({ returnType, name, argumentTypes }) => + [ + returnType, + // Signature + `(*${name})(${argumentTypes.join(", ")});`, + ].join(" "), ), "};", "typedef void(*InjectHostFunction)(const WeakNodeApiHost&);", @@ -46,25 +57,26 @@ export function generateSource(functions: FunctionDecl[]) { "};", ``, // Generate function calling into the host - ...functions.flatMap(({ returnType, noReturn, name, argumentTypes }) => { + ...functions.flatMap(({ returnType, name, argumentTypes, noReturn }) => { return [ - `extern "C" ${returnType} ${ - noReturn ? " __attribute__((noreturn))" : "" - }${name}(${argumentTypes - .map((type, index) => `${type} arg${index}`) - .join(", ")}) {`, + 'extern "C"', + returnType, + name, + "(", + argumentTypes.map((type, index) => `${type} arg${index}`).join(", "), + ") {", `if (g_host.${name} == nullptr) {`, ` fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n");`, " abort();", "}", - (returnType === "void" ? "" : "return ") + - "g_host." + - name + - "(" + - argumentTypes.map((_, index) => `arg${index}`).join(", ") + - ");", + returnType === "void" ? "" : "return ", + `g_host.${name}`, + "(", + argumentTypes.map((_, index) => `arg${index}`).join(", "), + ");", + noReturn ? "WEAK_NODE_API_UNREACHABLE" : "", "};", - ]; + ].join(" "); }), ].join("\n"); } diff --git a/packages/weak-node-api/tests/CMakeLists.txt b/packages/weak-node-api/tests/CMakeLists.txt new file mode 100644 index 00000000..89b19f84 --- /dev/null +++ b/packages/weak-node-api/tests/CMakeLists.txt @@ -0,0 +1,27 @@ +Include(FetchContent) + +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.11.0 +) + +FetchContent_MakeAvailable(Catch2) + +add_executable(weak-node-api-tests + test_inject.cpp +) +target_link_libraries(weak-node-api-tests + PRIVATE + weak-node-api + Catch2::Catch2WithMain +) + +target_compile_features(weak-node-api-tests PRIVATE cxx_std_20) +target_compile_definitions(weak-node-api-tests PRIVATE NAPI_VERSION=8) + +# As per https://github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md#catchcmake-and-catchaddtestscmake +list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) +include(CTest) +include(Catch) +catch_discover_tests(weak-node-api-tests) diff --git a/packages/weak-node-api/tests/test_inject.cpp b/packages/weak-node-api/tests/test_inject.cpp new file mode 100644 index 00000000..e2101c8c --- /dev/null +++ b/packages/weak-node-api/tests/test_inject.cpp @@ -0,0 +1,25 @@ +#include +#include + +TEST_CASE("inject_weak_node_api_host") { + SECTION("is callable") { + WeakNodeApiHost host{}; + inject_weak_node_api_host(host); + } + + SECTION("propagates calls to napi_create_object") { + static bool called = false; + auto my_create_object = [](napi_env env, + napi_value *result) -> napi_status { + called = true; + return napi_status::napi_ok; + }; + WeakNodeApiHost host{.napi_create_object = my_create_object}; + inject_weak_node_api_host(host); + + napi_value result; + napi_create_object({}, &result); + + REQUIRE(called); + } +} From ecd6c540aa6fb30246b3fc8979b0973d76b81792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 7 Nov 2025 05:58:59 +0100 Subject: [PATCH 61/82] Run depcheck on CI and fix issues from missing dependencies (#323) * Add depcheck script * Fix missing depcheck issues * Run depcheck on CI --- .github/workflows/check.yml | 1 + configs/tsconfig.node-tests.json | 7 - package-lock.json | 570 +++++++++++++++++- package.json | 2 + packages/cmake-rn/package.json | 3 +- packages/ferric/package.json | 3 +- packages/node-tests/package.json | 1 - packages/weak-node-api/package.json | 4 +- .../scripts/copy-node-api-headers.ts | 4 +- .../weak-node-api/src/node-api-functions.ts | 43 +- .../types/node-api-headers/index.d.ts | 27 +- scripts/depcheck.ts | 81 +++ 12 files changed, 688 insertions(+), 58 deletions(-) create mode 100644 scripts/depcheck.ts diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ecff59bd..d85f17bd 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -48,6 +48,7 @@ jobs: env: DEBUG: eslint:eslint - run: npm run prettier:check + - run: npm run depcheck unit-tests: strategy: fail-fast: false diff --git a/configs/tsconfig.node-tests.json b/configs/tsconfig.node-tests.json index 642ba5ac..2b914535 100644 --- a/configs/tsconfig.node-tests.json +++ b/configs/tsconfig.node-tests.json @@ -7,11 +7,4 @@ }, "include": ["${configDir}/src/**/*.test.ts"], "exclude": [] - /* - "references": [ - { - "path": "./tsconfig.json" - } - ] - */ } diff --git a/package-lock.json b/package-lock.json index 845f156c..b16b1d3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@tsconfig/node22": "^22.0.0", "@tsconfig/react-native": "3.0.6", "@types/node": "^22", + "depcheck": "^1.4.7", "eslint": "^9.32.0", "eslint-config-prettier": "^10.1.8", "globals": "^16.0.0", @@ -388,9 +389,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -433,12 +434,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.4" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -1931,13 +1932,13 @@ } }, "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -6565,6 +6566,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mocha": { "version": "10.0.10", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", @@ -6586,6 +6594,13 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "license": "MIT" }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", @@ -6893,6 +6908,67 @@ "integrity": "sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA==", "license": "MIT" }, + "node_modules/@vue/compiler-core": { + "version": "3.5.23", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.23.tgz", + "integrity": "sha512-nW7THWj5HOp085ROk65LwaoxuzDsjIxr485F4iu63BoxsXoSqKqmsUUoP4A7Gl67DgIgi0zJ8JFgHfvny/74MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.23", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.23", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.23.tgz", + "integrity": "sha512-AT8RMw0vEzzzO0JU5gY0F6iCzaWUIh/aaRVordzMBKXRpoTllTT4kocHDssByPsvodNCfump/Lkdow2mT/O5KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.23", + "@vue/shared": "3.5.23" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.23", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.23.tgz", + "integrity": "sha512-3QTEUo4qg7FtQwaDJa8ou1CUikx5WTtZlY61rRRDu3lK2ZKrGoAGG8mvDgOpDsQ4A1bez9s+WtBB6DS2KuFCPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.23", + "@vue/compiler-dom": "3.5.23", + "@vue/compiler-ssr": "3.5.23", + "@vue/shared": "3.5.23", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.23", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.23.tgz", + "integrity": "sha512-Hld2xphbMjXs9Q9WKxPf2EqmE+Rq/FEDnK/wUBtmYq74HCV4XDdSCheAaB823OQXIIFGq9ig/RbAZkF9s4U0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.23", + "@vue/shared": "3.5.23" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.23", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.23.tgz", + "integrity": "sha512-0YZ1DYuC5o/YJPf6pFdt2KYxVGDxkDbH/1NYJnVJWUkzr8ituBEmFVQRNX2gCaAsFEjEDnLkWpgqlZA7htgS/g==", + "dev": true, + "license": "MIT" + }, "node_modules/@xmldom/xmldom": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", @@ -7122,6 +7198,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -7132,6 +7218,16 @@ "node": ">=8" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -7669,6 +7765,15 @@ "node": ">=4" } }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -8424,6 +8529,182 @@ "dev": true, "license": "MIT" }, + "node_modules/depcheck": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-1.4.7.tgz", + "integrity": "sha512-1lklS/bV5chOxwNKA/2XUUk/hPORp8zihZsXflr8x0kLwmcZ9Y9BsS6Hs3ssvA+2wUVbG0U2Ciqvm1SokNjPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.23.0", + "@babel/traverse": "^7.23.2", + "@vue/compiler-sfc": "^3.3.4", + "callsite": "^1.0.0", + "camelcase": "^6.3.0", + "cosmiconfig": "^7.1.0", + "debug": "^4.3.4", + "deps-regex": "^0.2.0", + "findup-sync": "^5.0.0", + "ignore": "^5.2.4", + "is-core-module": "^2.12.0", + "js-yaml": "^3.14.1", + "json5": "^2.2.3", + "lodash": "^4.17.21", + "minimatch": "^7.4.6", + "multimatch": "^5.0.0", + "please-upgrade-node": "^3.2.0", + "readdirp": "^3.6.0", + "require-package-name": "^2.0.1", + "resolve": "^1.22.3", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "yargs": "^16.2.0" + }, + "bin": { + "depcheck": "bin/depcheck.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/depcheck/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/depcheck/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/depcheck/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/depcheck/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/depcheck/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/depcheck/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/depcheck/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8433,6 +8714,13 @@ "node": ">= 0.8" } }, + "node_modules/deps-regex": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/deps-regex/-/deps-regex-0.2.0.tgz", + "integrity": "sha512-PwuBojGMQAYbWkMXOY9Pd/NWCDNHVH12pnS7WHqZkTSeMESe4hwnKKRp0yR87g37113x4JPbo/oIvXY+s/f56Q==", + "dev": true, + "license": "MIT" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -8443,6 +8731,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -8560,6 +8858,19 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -8979,6 +9290,13 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -9035,6 +9353,19 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", @@ -9271,6 +9602,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -9641,6 +9988,51 @@ "node": ">=10.13.0" } }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "16.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", @@ -9801,6 +10193,19 @@ "hermes-estree": "0.29.1" } }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/hosted-git-info": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", @@ -10672,6 +11077,13 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", @@ -10825,6 +11237,16 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -11704,6 +12126,26 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", @@ -11713,6 +12155,25 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -12225,6 +12686,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12351,6 +12822,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-compare": "^1.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -12360,6 +12841,35 @@ "node": ">= 0.4" } }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prebuildify": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/prebuildify/-/prebuildify-6.0.1.tgz", @@ -13009,6 +13519,13 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "license": "ISC" }, + "node_modules/require-package-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", + "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -13029,6 +13546,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -13225,6 +13756,13 @@ "semver": "bin/semver.js" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT" + }, "node_modules/send": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", @@ -13538,6 +14076,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", diff --git a/package.json b/package.json index 668f6c31..8ddb8b6c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "clean": "tsc --build --clean && git clean -fdx -e node_modules", "dev": "tsc --build --watch", "lint": "eslint .", + "depcheck": "node scripts/depcheck.ts", "prettier:check": "prettier --experimental-cli --check .", "prettier:write": "prettier --experimental-cli --write .", "test": "npm test --workspace react-native-node-api --workspace cmake-rn --workspace gyp-to-cmake --workspace node-addon-examples", @@ -61,6 +62,7 @@ "@tsconfig/node22": "^22.0.0", "@tsconfig/react-native": "3.0.6", "@types/node": "^22", + "depcheck": "^1.4.7", "eslint": "^9.32.0", "eslint-config-prettier": "^10.1.8", "globals": "^16.0.0", diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index 9afb6e43..db5a34fb 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -27,7 +27,8 @@ "@react-native-node-api/cli-utils": "0.1.1", "cmake-file-api": "0.1.0", "react-native-node-api": "0.6.2", - "zod": "^4.1.11" + "zod": "^4.1.11", + "weak-node-api": "0.0.1" }, "peerDependencies": { "node-addon-api": "^8.3.1", diff --git a/packages/ferric/package.json b/packages/ferric/package.json index b098e373..8c4488f9 100644 --- a/packages/ferric/package.json +++ b/packages/ferric/package.json @@ -18,6 +18,7 @@ "dependencies": { "@napi-rs/cli": "~3.0.3", "@react-native-node-api/cli-utils": "0.1.1", - "react-native-node-api": "0.6.2" + "react-native-node-api": "0.6.2", + "weak-node-api": "0.0.1" } } diff --git a/packages/node-tests/package.json b/packages/node-tests/package.json index fd1d497b..76dc1cc4 100644 --- a/packages/node-tests/package.json +++ b/packages/node-tests/package.json @@ -28,7 +28,6 @@ "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", - "prebuildify": "^6.0.1", "react-native-node-api": "^0.6.1", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json index 63cfb410..de1c5d1b 100644 --- a/packages/weak-node-api/package.json +++ b/packages/weak-node-api/package.json @@ -58,8 +58,10 @@ } ], "license": "MIT", + "dependencies": { + "node-api-headers": "^1.5.0" + }, "devDependencies": { - "node-api-headers": "^1.5.0", "zod": "^4.1.11" } } diff --git a/packages/weak-node-api/scripts/copy-node-api-headers.ts b/packages/weak-node-api/scripts/copy-node-api-headers.ts index 2bfd43dc..8d90174b 100644 --- a/packages/weak-node-api/scripts/copy-node-api-headers.ts +++ b/packages/weak-node-api/scripts/copy-node-api-headers.ts @@ -1,7 +1,9 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -import { include_dir as includeSourcePath } from "node-api-headers"; + +import { nodeApiHeaders } from "../src/node-api-functions.js"; +const { include_dir: includeSourcePath } = nodeApiHeaders; const includeDestinationPath = path.join(import.meta.dirname, "../include"); assert(fs.existsSync(includeSourcePath), `Expected ${includeSourcePath}`); diff --git a/packages/weak-node-api/src/node-api-functions.ts b/packages/weak-node-api/src/node-api-functions.ts index c554a053..4290a471 100644 --- a/packages/weak-node-api/src/node-api-functions.ts +++ b/packages/weak-node-api/src/node-api-functions.ts @@ -1,14 +1,39 @@ import assert from "node:assert/strict"; import path from "node:path"; import cp from "node:child_process"; - -import { - type NodeApiVersion, - symbols, - include_dir as nodeApiIncludePath, -} from "node-api-headers"; import { z } from "zod"; +import * as rawHeaders from "node-api-headers"; + +const SymbolsPerInterface = z.object({ + js_native_api_symbols: z.array(z.string()), + node_api_symbols: z.array(z.string()), +}); + +const NodeApiHeaders = z.object({ + include_dir: z.string(), + def_paths: z.object({ + js_native_api_def: z.string(), + node_api_def: z.string(), + }), + symbols: z.object({ + v1: SymbolsPerInterface, + v2: SymbolsPerInterface, + v3: SymbolsPerInterface, + v4: SymbolsPerInterface, + v5: SymbolsPerInterface, + v6: SymbolsPerInterface, + v7: SymbolsPerInterface, + v8: SymbolsPerInterface, + v9: SymbolsPerInterface, + v10: SymbolsPerInterface, + }), +}); + +type NodeApiVersion = keyof z.infer["symbols"]; + +export const nodeApiHeaders = NodeApiHeaders.parse(rawHeaders); + const clangAstDump = z.object({ kind: z.literal("TranslationUnitDecl"), inner: z.array( @@ -42,8 +67,8 @@ export function getNodeApiHeaderAST(version: NodeApiVersion) { // Parse and analyze the source file but not compile it "-fsyntax-only", // Include from the node-api-headers package - `-I${nodeApiIncludePath}`, - path.join(nodeApiIncludePath, "node_api.h"), + `-I${nodeApiHeaders.include_dir}`, + path.join(nodeApiHeaders.include_dir, "node_api.h"), ], { encoding: "utf-8", @@ -71,7 +96,7 @@ export function getNodeApiFunctions(version: NodeApiVersion = "v8") { assert(Array.isArray(root.inner)); const foundSymbols = new Set(); - const symbolsPerInterface = symbols[version]; + const symbolsPerInterface = nodeApiHeaders.symbols[version]; const engineSymbols = new Set(symbolsPerInterface.js_native_api_symbols); const runtimeSymbols = new Set(symbolsPerInterface.node_api_symbols); const allSymbols = new Set([...engineSymbols, ...runtimeSymbols]); diff --git a/packages/weak-node-api/types/node-api-headers/index.d.ts b/packages/weak-node-api/types/node-api-headers/index.d.ts index dc6ab254..4444171b 100644 --- a/packages/weak-node-api/types/node-api-headers/index.d.ts +++ b/packages/weak-node-api/types/node-api-headers/index.d.ts @@ -1,29 +1,4 @@ module "node-api-headers" { - type SymbolsPerInterface = { - js_native_api_symbols: string[]; - node_api_symbols: string[]; - }; - type Exported = { - include_dir: string; - def_paths: { - js_native_api_def: string; - node_api_def: string; - }; - symbols: { - v1: SymbolsPerInterface; - v2: SymbolsPerInterface; - v3: SymbolsPerInterface; - v4: SymbolsPerInterface; - v5: SymbolsPerInterface; - v6: SymbolsPerInterface; - v7: SymbolsPerInterface; - v8: SymbolsPerInterface; - v9: SymbolsPerInterface; - v10: SymbolsPerInterface; - }; - }; - export type NodeApiVersion = keyof Exported["symbols"]; - - const exported: Exported; + declare const exported: unknown; export = exported; } diff --git a/scripts/depcheck.ts b/scripts/depcheck.ts new file mode 100644 index 00000000..a16097a5 --- /dev/null +++ b/scripts/depcheck.ts @@ -0,0 +1,81 @@ +import path from "node:path"; +import assert from "node:assert/strict"; +import cp from "node:child_process"; +import fs from "node:fs"; + +import depcheck from "depcheck"; + +function getWorkspaces() { + const workspaces = JSON.parse( + cp.execFileSync("npm", ["query", ".workspace"], { encoding: "utf8" }), + ) as unknown; + assert(Array.isArray(workspaces)); + for (const workspace of workspaces) { + assert(typeof workspace === "object" && workspace !== null); + } + return workspaces as Record[]; +} + +const rootDir = path.resolve(import.meta.dirname, ".."); +const root = await depcheck(rootDir, {}); + +const rootPackage = JSON.parse( + await fs.promises.readFile(path.join(rootDir, "package.json"), { + encoding: "utf8", + }), +) as unknown; + +assert( + typeof rootPackage === "object" && + rootPackage !== null && + "devDependencies" in rootPackage && + typeof rootPackage.devDependencies === "object" && + rootPackage.devDependencies !== null, +); + +const rootDevDependencies = new Set(Object.keys(rootPackage.devDependencies)); +for (const packageName of [...rootDevDependencies.values()]) { + rootDevDependencies.add(`@types/${packageName}`); +} + +for (const { + name: workspaceName, + path: workspacePath, + private: workspacePrivate, +} of getWorkspaces()) { + assert(typeof workspaceName === "string"); + assert(typeof workspacePath === "string"); + assert( + typeof workspacePrivate === "boolean" || + typeof workspacePrivate === "undefined", + ); + if (workspacePrivate) { + console.warn(`Skipping private package '${workspaceName}'`); + continue; + } + const result = await depcheck(workspacePath, { + ignoreMatches: [...rootDevDependencies], + }); + for (const [name, filePaths] of Object.entries(result.missing)) { + if (!rootDevDependencies.has(name)) { + console.error(`Missing '${name}' in '${workspaceName}':`); + for (const filePath of filePaths) { + console.error("↳", path.relative(workspacePath, filePath)); + } + console.error(); + process.exitCode = 1; + } + } + for (const name of result.dependencies) { + console.error(`Unused dependency '${name}' in '${workspaceName}'`); + console.error(); + process.exitCode = 1; + } + for (const name of result.devDependencies) { + console.error(`Unused dev-dependency '${name}' in '${workspaceName}'`); + console.error(); + process.exitCode = 1; + } +} + +assert.deepEqual(root.dependencies, [], "Found unused dependencies"); From c98947e5e91f1223c8bdb5ce5844d25840e9cbb1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 09:40:03 +0100 Subject: [PATCH 62/82] Version Packages (#311) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/eight-walls-push.md | 5 ----- .changeset/gold-beans-jump.md | 7 ------- .changeset/long-regions-yawn.md | 5 ----- .changeset/poor-lemons-yell.md | 5 ----- .changeset/quick-poets-greet.md | 7 ------- .changeset/spotty-beers-repeat.md | 5 ----- .changeset/tender-laws-admire.md | 5 ----- .changeset/tired-words-relate.md | 5 ----- packages/cmake-rn/CHANGELOG.md | 20 ++++++++++++++++++++ packages/cmake-rn/package.json | 6 +++--- packages/ferric/CHANGELOG.md | 16 ++++++++++++++++ packages/ferric/package.json | 6 +++--- packages/gyp-to-cmake/CHANGELOG.md | 6 ++++++ packages/gyp-to-cmake/package.json | 2 +- packages/host/CHANGELOG.md | 17 +++++++++++++++++ packages/host/package.json | 4 ++-- packages/node-tests/package.json | 2 +- packages/weak-node-api/CHANGELOG.md | 7 +++++++ packages/weak-node-api/package.json | 2 +- 19 files changed, 77 insertions(+), 55 deletions(-) delete mode 100644 .changeset/eight-walls-push.md delete mode 100644 .changeset/gold-beans-jump.md delete mode 100644 .changeset/long-regions-yawn.md delete mode 100644 .changeset/poor-lemons-yell.md delete mode 100644 .changeset/quick-poets-greet.md delete mode 100644 .changeset/spotty-beers-repeat.md delete mode 100644 .changeset/tender-laws-admire.md delete mode 100644 .changeset/tired-words-relate.md create mode 100644 packages/weak-node-api/CHANGELOG.md diff --git a/.changeset/eight-walls-push.md b/.changeset/eight-walls-push.md deleted file mode 100644 index 58793ae4..00000000 --- a/.changeset/eight-walls-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Moved weak-node-api into a separate "weak-node-api" package. diff --git a/.changeset/gold-beans-jump.md b/.changeset/gold-beans-jump.md deleted file mode 100644 index ecc9f902..00000000 --- a/.changeset/gold-beans-jump.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"cmake-rn": patch -"ferric-cli": patch -"react-native-node-api": patch ---- - -Allow passing --apple-bundle-identifier to specify the bundle identifiers used when creating Apple frameworks. diff --git a/.changeset/long-regions-yawn.md b/.changeset/long-regions-yawn.md deleted file mode 100644 index b0d515e7..00000000 --- a/.changeset/long-regions-yawn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": minor ---- - -Ensure proper escaping when generating a bundle identifier while creating an Apple framework diff --git a/.changeset/poor-lemons-yell.md b/.changeset/poor-lemons-yell.md deleted file mode 100644 index ff49220e..00000000 --- a/.changeset/poor-lemons-yell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Add "apple" folder into the package (follow-up to #301) diff --git a/.changeset/quick-poets-greet.md b/.changeset/quick-poets-greet.md deleted file mode 100644 index 4b53b523..00000000 --- a/.changeset/quick-poets-greet.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"gyp-to-cmake": minor -"cmake-rn": minor -"react-native-node-api": minor ---- - -Use `find_package` instead of `include` to locate "weak-node-api" diff --git a/.changeset/spotty-beers-repeat.md b/.changeset/spotty-beers-repeat.md deleted file mode 100644 index cfa21deb..00000000 --- a/.changeset/spotty-beers-repeat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": minor ---- - -No longer exporting weakNodeApiPath, import from "weak-node-api" instead diff --git a/.changeset/tender-laws-admire.md b/.changeset/tender-laws-admire.md deleted file mode 100644 index 4aa54b2b..00000000 --- a/.changeset/tender-laws-admire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"react-native-node-api": patch ---- - -Don't instruct users to pass --force when vendoring hermes diff --git a/.changeset/tired-words-relate.md b/.changeset/tired-words-relate.md deleted file mode 100644 index e1a3ec02..00000000 --- a/.changeset/tired-words-relate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"weak-node-api": patch ---- - -Initial release! diff --git a/packages/cmake-rn/CHANGELOG.md b/packages/cmake-rn/CHANGELOG.md index d0072ace..3c7a5f72 100644 --- a/packages/cmake-rn/CHANGELOG.md +++ b/packages/cmake-rn/CHANGELOG.md @@ -1,5 +1,25 @@ # cmake-rn +## 0.6.0 + +### Minor Changes + +- 60fae96: Use `find_package` instead of `include` to locate "weak-node-api" + +### Patch Changes + +- 61fff3f: Allow passing --apple-bundle-identifier to specify the bundle identifiers used when creating Apple frameworks. +- Updated dependencies [60fae96] +- Updated dependencies [61fff3f] +- Updated dependencies [61fff3f] +- Updated dependencies [5dea205] +- Updated dependencies [60fae96] +- Updated dependencies [60fae96] +- Updated dependencies [eca721e] +- Updated dependencies [60fae96] + - react-native-node-api@0.7.0 + - weak-node-api@0.0.2 + ## 0.5.2 ### Patch Changes diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index db5a34fb..df59dbb1 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -1,6 +1,6 @@ { "name": "cmake-rn", - "version": "0.5.2", + "version": "0.6.0", "description": "Build React Native Node API modules with CMake", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -26,9 +26,9 @@ "dependencies": { "@react-native-node-api/cli-utils": "0.1.1", "cmake-file-api": "0.1.0", - "react-native-node-api": "0.6.2", + "react-native-node-api": "0.7.0", "zod": "^4.1.11", - "weak-node-api": "0.0.1" + "weak-node-api": "0.0.2" }, "peerDependencies": { "node-addon-api": "^8.3.1", diff --git a/packages/ferric/CHANGELOG.md b/packages/ferric/CHANGELOG.md index 6ef161d9..7d4a69c3 100644 --- a/packages/ferric/CHANGELOG.md +++ b/packages/ferric/CHANGELOG.md @@ -1,5 +1,21 @@ # ferric-cli +## 0.3.8 + +### Patch Changes + +- 61fff3f: Allow passing --apple-bundle-identifier to specify the bundle identifiers used when creating Apple frameworks. +- Updated dependencies [60fae96] +- Updated dependencies [61fff3f] +- Updated dependencies [61fff3f] +- Updated dependencies [5dea205] +- Updated dependencies [60fae96] +- Updated dependencies [60fae96] +- Updated dependencies [eca721e] +- Updated dependencies [60fae96] + - react-native-node-api@0.7.0 + - weak-node-api@0.0.2 + ## 0.3.7 ### Patch Changes diff --git a/packages/ferric/package.json b/packages/ferric/package.json index 8c4488f9..274718c5 100644 --- a/packages/ferric/package.json +++ b/packages/ferric/package.json @@ -1,6 +1,6 @@ { "name": "ferric-cli", - "version": "0.3.7", + "version": "0.3.8", "description": "Rust Node-API Modules for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -18,7 +18,7 @@ "dependencies": { "@napi-rs/cli": "~3.0.3", "@react-native-node-api/cli-utils": "0.1.1", - "react-native-node-api": "0.6.2", - "weak-node-api": "0.0.1" + "react-native-node-api": "0.7.0", + "weak-node-api": "0.0.2" } } diff --git a/packages/gyp-to-cmake/CHANGELOG.md b/packages/gyp-to-cmake/CHANGELOG.md index 9572c06d..86b34c6b 100644 --- a/packages/gyp-to-cmake/CHANGELOG.md +++ b/packages/gyp-to-cmake/CHANGELOG.md @@ -1,5 +1,11 @@ # gyp-to-cmake +## 0.5.0 + +### Minor Changes + +- 60fae96: Use `find_package` instead of `include` to locate "weak-node-api" + ## 0.4.0 ### Minor Changes diff --git a/packages/gyp-to-cmake/package.json b/packages/gyp-to-cmake/package.json index cc687164..9dd09db4 100644 --- a/packages/gyp-to-cmake/package.json +++ b/packages/gyp-to-cmake/package.json @@ -1,6 +1,6 @@ { "name": "gyp-to-cmake", - "version": "0.4.0", + "version": "0.5.0", "description": "Convert binding.gyp files to CMakeLists.txt", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { diff --git a/packages/host/CHANGELOG.md b/packages/host/CHANGELOG.md index 9252fd34..c29638f8 100644 --- a/packages/host/CHANGELOG.md +++ b/packages/host/CHANGELOG.md @@ -1,5 +1,22 @@ # react-native-node-api +## 0.7.0 + +### Minor Changes + +- 61fff3f: Ensure proper escaping when generating a bundle identifier while creating an Apple framework +- 60fae96: Use `find_package` instead of `include` to locate "weak-node-api" +- 60fae96: No longer exporting weakNodeApiPath, import from "weak-node-api" instead + +### Patch Changes + +- 60fae96: Moved weak-node-api into a separate "weak-node-api" package. +- 61fff3f: Allow passing --apple-bundle-identifier to specify the bundle identifiers used when creating Apple frameworks. +- 5dea205: Add "apple" folder into the package (follow-up to #301) +- eca721e: Don't instruct users to pass --force when vendoring hermes +- Updated dependencies [60fae96] + - weak-node-api@0.0.2 + ## 0.6.2 ### Patch Changes diff --git a/packages/host/package.json b/packages/host/package.json index 2f1199bf..400ecfc8 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -1,6 +1,6 @@ { "name": "react-native-node-api", - "version": "0.6.2", + "version": "0.7.0", "description": "Node-API for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -82,6 +82,6 @@ "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.1" + "weak-node-api": "0.0.2" } } diff --git a/packages/node-tests/package.json b/packages/node-tests/package.json index 76dc1cc4..2c0a9c6c 100644 --- a/packages/node-tests/package.json +++ b/packages/node-tests/package.json @@ -28,7 +28,7 @@ "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", - "react-native-node-api": "^0.6.1", + "react-native-node-api": "^0.7.0", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } diff --git a/packages/weak-node-api/CHANGELOG.md b/packages/weak-node-api/CHANGELOG.md new file mode 100644 index 00000000..88a11828 --- /dev/null +++ b/packages/weak-node-api/CHANGELOG.md @@ -0,0 +1,7 @@ +# weak-node-api + +## 0.0.2 + +### Patch Changes + +- 60fae96: Initial release! diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json index de1c5d1b..79256293 100644 --- a/packages/weak-node-api/package.json +++ b/packages/weak-node-api/package.json @@ -1,6 +1,6 @@ { "name": "weak-node-api", - "version": "0.0.1", + "version": "0.0.2", "description": "A linkable and runtime-injectable Node-API", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { From 7ff2c2b561def7166b983745be75e43b5e8e0706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 7 Nov 2025 12:08:44 +0100 Subject: [PATCH 63/82] Publint (#325) * Add publint to root package * Fix issues * Add missing "generated" folder to weak-node-api files * Add a changeset package script for discoverability * Add changesets * Run publint in CI * Run publint only in the non-private packages --- .changeset/old-poems-sing.md | 9 ++ .changeset/wild-boats-lay.md | 5 + .github/workflows/check.yml | 1 + apps/test-app/package.json | 1 + package-lock.json | 200 +++++++++------------------ package.json | 3 + packages/cli-utils/package.json | 7 +- packages/cmake-file-api/package.json | 2 +- packages/host/package.json | 3 +- packages/weak-node-api/package.json | 5 +- scripts/run-in-published.ts | 32 +++++ 11 files changed, 128 insertions(+), 140 deletions(-) create mode 100644 .changeset/old-poems-sing.md create mode 100644 .changeset/wild-boats-lay.md create mode 100644 scripts/run-in-published.ts diff --git a/.changeset/old-poems-sing.md b/.changeset/old-poems-sing.md new file mode 100644 index 00000000..f774682f --- /dev/null +++ b/.changeset/old-poems-sing.md @@ -0,0 +1,9 @@ +--- +"cmake-file-api": patch +"weak-node-api": patch +"@react-native-node-api/cli-utils": patch +"@react-native-node-api/test-app": patch +"react-native-node-api": patch +--- + +Fix minor package issues. diff --git a/.changeset/wild-boats-lay.md b/.changeset/wild-boats-lay.md new file mode 100644 index 00000000..302b14b3 --- /dev/null +++ b/.changeset/wild-boats-lay.md @@ -0,0 +1,5 @@ +--- +"weak-node-api": patch +--- + +Add missing "generated" directory diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index d85f17bd..17cfdac4 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -49,6 +49,7 @@ jobs: DEBUG: eslint:eslint - run: npm run prettier:check - run: npm run depcheck + - run: npm run publint unit-tests: strategy: fail-fast: false diff --git a/apps/test-app/package.json b/apps/test-app/package.json index 2f41e8c4..39518dd6 100644 --- a/apps/test-app/package.json +++ b/apps/test-app/package.json @@ -1,6 +1,7 @@ { "name": "@react-native-node-api/test-app", "private": true, + "type": "commonjs", "version": "0.2.0", "scripts": { "metro": "react-native start --no-interactive", diff --git a/package-lock.json b/package-lock.json index b16b1d3a..eaf0b060 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "eslint-config-prettier": "^10.1.8", "globals": "^16.0.0", "prettier": "^3.6.2", + "publint": "^0.3.15", "react-native": "0.81.4", "read-pkg": "^9.0.1", "tsx": "^4.20.6", @@ -5319,6 +5320,19 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/@publint/pack": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.2.tgz", + "integrity": "sha512-S+9ANAvUmjutrshV4jZjaiG8XQyuJIZ8a4utWmN/vW1sgQ9IfBnPndwkmQYw53QmouOIytT874u65HEmu6H5jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, "node_modules/@react-native-community/cli": { "version": "20.0.2", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.0.2.tgz", @@ -8834,16 +8848,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -9731,13 +9735,6 @@ "node": ">= 0.6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true, - "license": "MIT" - }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -11801,13 +11798,6 @@ "node": ">=10" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true, - "license": "MIT" - }, "node_modules/mocha": { "version": "11.7.2", "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.2.tgz", @@ -12199,32 +12189,6 @@ "node": ">=12.0.0" } }, - "node_modules/node-abi": { - "version": "3.77.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", - "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", @@ -12870,37 +12834,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/prebuildify": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/prebuildify/-/prebuildify-6.0.1.tgz", - "integrity": "sha512-8Y2oOOateom/s8dNBsGIcnm6AxPmLH4/nanQzL5lQMU+sC0CMhzARZHizwr36pUPLdvBnOkCNQzxg4djuFSgIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "mkdirp-classic": "^0.5.3", - "node-abi": "^3.3.0", - "npm-run-path": "^3.1.0", - "pump": "^3.0.0", - "tar-fs": "^2.1.0" - }, - "bin": { - "prebuildify": "bin.js" - } - }, - "node_modules/prebuildify/node_modules/npm-run-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", - "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -12982,17 +12915,35 @@ "dev": true, "license": "MIT" }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "node_modules/publint": { + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.15.tgz", + "integrity": "sha512-xPbRAPW+vqdiaKy5sVVY0uFAu3LaviaPO3pZ9FaRx59l9+U/RKR1OEbLhkug87cwiVKxPXyB4txsv5cad67u+A==", "dev": true, "license": "MIT", "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "@publint/pack": "^0.1.2", + "package-manager-detector": "^1.3.0", + "picocolors": "^1.1.1", + "sade": "^1.8.1" + }, + "bin": { + "publint": "src/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" } }, + "node_modules/publint/node_modules/package-manager-detector": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.5.0.tgz", + "integrity": "sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==", + "dev": true, + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -13698,6 +13649,19 @@ "tslib": "^2.1.0" } }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -14389,43 +14353,6 @@ "node": ">=10" } }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true, - "license": "ISC" - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/tar/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -15379,11 +15306,12 @@ } }, "packages/cmake-rn": { - "version": "0.5.2", + "version": "0.6.0", "dependencies": { "@react-native-node-api/cli-utils": "0.1.1", "cmake-file-api": "0.1.0", - "react-native-node-api": "0.6.2", + "react-native-node-api": "0.7.0", + "weak-node-api": "0.0.2", "zod": "^4.1.11" }, "bin": { @@ -15396,11 +15324,12 @@ }, "packages/ferric": { "name": "ferric-cli", - "version": "0.3.7", + "version": "0.3.8", "dependencies": { "@napi-rs/cli": "~3.0.3", "@react-native-node-api/cli-utils": "0.1.1", - "react-native-node-api": "0.6.2" + "react-native-node-api": "0.7.0", + "weak-node-api": "0.0.2" }, "bin": { "ferric": "bin/ferric.js" @@ -15414,7 +15343,7 @@ } }, "packages/gyp-to-cmake": { - "version": "0.4.0", + "version": "0.5.0", "dependencies": { "@react-native-node-api/cli-utils": "0.1.1", "gyp-parser": "^1.0.4", @@ -15427,7 +15356,7 @@ }, "packages/host": { "name": "react-native-node-api", - "version": "0.6.2", + "version": "0.7.0", "license": "MIT", "dependencies": { "@expo/plist": "^0.4.7", @@ -15447,7 +15376,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.1" + "weak-node-api": "0.0.2" } }, "packages/node-addon-examples": { @@ -15469,17 +15398,18 @@ "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", - "prebuildify": "^6.0.1", - "react-native-node-api": "^0.6.1", + "react-native-node-api": "^0.7.0", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } }, "packages/weak-node-api": { - "version": "0.0.1", + "version": "0.0.2", "license": "MIT", + "dependencies": { + "node-api-headers": "^1.5.0" + }, "devDependencies": { - "node-api-headers": "^1.5.0", "zod": "^4.1.11" } } diff --git a/package.json b/package.json index 8ddb8b6c..7c4400dc 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,13 @@ "dev": "tsc --build --watch", "lint": "eslint .", "depcheck": "node scripts/depcheck.ts", + "publint": "node scripts/run-in-published.ts npx publint --strict", "prettier:check": "prettier --experimental-cli --check .", "prettier:write": "prettier --experimental-cli --write .", "test": "npm test --workspace react-native-node-api --workspace cmake-rn --workspace gyp-to-cmake --workspace node-addon-examples", "bootstrap": "node --run build && npm run bootstrap --workspaces --if-present", "prerelease": "node --run build && npm run prerelease --workspaces --if-present", + "changeset": "changeset", "release": "changeset publish", "init-macos-test-app": "node scripts/init-macos-test-app.ts" }, @@ -67,6 +69,7 @@ "eslint-config-prettier": "^10.1.8", "globals": "^16.0.0", "prettier": "^3.6.2", + "publint": "^0.3.15", "react-native": "0.81.4", "read-pkg": "^9.0.1", "tsx": "^4.20.6", diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index af9743b6..05d9be6c 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -3,7 +3,12 @@ "version": "0.1.1", "description": "Useful utilities for the CLIs in the React Native Node API mono-repo", "type": "module", - "main": "dist/index.js", + "files": [ + "dist" + ], + "exports": { + ".": "./dist/index.js" + }, "dependencies": { "@commander-js/extra-typings": "^14.0.0", "bufout": "^0.3.2", diff --git a/packages/cmake-file-api/package.json b/packages/cmake-file-api/package.json index 656939c6..4958f523 100644 --- a/packages/cmake-file-api/package.json +++ b/packages/cmake-file-api/package.json @@ -19,7 +19,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/callstackincubator/react-native-node-api", + "url": "git+https://github.com/callstackincubator/react-native-node-api.git", "directory": "packages/cmake-file-api" }, "author": { diff --git a/packages/host/package.json b/packages/host/package.json index 400ecfc8..a071e6f8 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -19,8 +19,7 @@ "react-native": "./dist/react-native/index.js" }, "./babel-plugin": "./dist/node/babel-plugin/index.js", - "./cli": "./dist/node/cli/run.js", - "./weak-node-api": "./dist/node/weak-node-api.js" + "./cli": "./dist/node/cli/run.js" }, "files": [ "logo.svg", diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json index 79256293..c5d85c6d 100644 --- a/packages/weak-node-api/package.json +++ b/packages/weak-node-api/package.json @@ -8,13 +8,16 @@ "url": "git+https://github.com/callstackincubator/react-native-node-api.git", "directory": "packages/weak-node-api" }, - "main": "dist/index.js", "type": "module", + "exports": { + ".": "./dist/index.js" + }, "files": [ "dist", "!dist/**/*.test.d.ts", "!dist/**/*.test.d.ts.map", "include", + "generated", "build/Debug", "build/Release", "*.podspec", diff --git a/scripts/run-in-published.ts b/scripts/run-in-published.ts new file mode 100644 index 00000000..cc59253b --- /dev/null +++ b/scripts/run-in-published.ts @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import cp from "node:child_process"; + +console.log("Run command in all non-private packages of the monorepo"); + +function getWorkspaces() { + const workspaces = JSON.parse( + cp.execFileSync("npm", ["query", ".workspace"], { encoding: "utf8" }), + ) as unknown; + assert(Array.isArray(workspaces)); + for (const workspace of workspaces) { + assert(typeof workspace === "object" && workspace !== null); + } + return workspaces as Record[]; +} + +const publishedPackagePaths = getWorkspaces() + .filter((w) => !w.private) + .map((p) => { + assert(typeof p.path === "string"); + return p.path; + }); + +const [, , command, ...argv] = process.argv; + +for (const packagePath of publishedPackagePaths) { + const { status } = cp.spawnSync(command, argv, { + cwd: packagePath, + stdio: "inherit", + }); + assert.equal(status, 0, `Command failed (status = ${status})`); +} From 4afc24e1492974712652f1f84e4d94ad9174e480 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:15:17 +0100 Subject: [PATCH 64/82] Version Packages (#326) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/old-poems-sing.md | 9 --------- .changeset/wild-boats-lay.md | 5 ----- apps/test-app/CHANGELOG.md | 10 ++++++++++ apps/test-app/package.json | 2 +- packages/cli-utils/CHANGELOG.md | 6 ++++++ packages/cli-utils/package.json | 2 +- packages/cmake-file-api/CHANGELOG.md | 7 +++++++ packages/cmake-file-api/package.json | 2 +- packages/cmake-rn/CHANGELOG.md | 11 +++++++++++ packages/cmake-rn/package.json | 10 +++++----- packages/ferric/CHANGELOG.md | 10 ++++++++++ packages/ferric/package.json | 8 ++++---- packages/gyp-to-cmake/CHANGELOG.md | 7 +++++++ packages/gyp-to-cmake/package.json | 4 ++-- packages/host/CHANGELOG.md | 10 ++++++++++ packages/host/package.json | 6 +++--- packages/weak-node-api/CHANGELOG.md | 7 +++++++ packages/weak-node-api/package.json | 2 +- 18 files changed, 86 insertions(+), 32 deletions(-) delete mode 100644 .changeset/old-poems-sing.md delete mode 100644 .changeset/wild-boats-lay.md create mode 100644 packages/cmake-file-api/CHANGELOG.md diff --git a/.changeset/old-poems-sing.md b/.changeset/old-poems-sing.md deleted file mode 100644 index f774682f..00000000 --- a/.changeset/old-poems-sing.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"cmake-file-api": patch -"weak-node-api": patch -"@react-native-node-api/cli-utils": patch -"@react-native-node-api/test-app": patch -"react-native-node-api": patch ---- - -Fix minor package issues. diff --git a/.changeset/wild-boats-lay.md b/.changeset/wild-boats-lay.md deleted file mode 100644 index 302b14b3..00000000 --- a/.changeset/wild-boats-lay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"weak-node-api": patch ---- - -Add missing "generated" directory diff --git a/apps/test-app/CHANGELOG.md b/apps/test-app/CHANGELOG.md index d4c88c46..cf436b94 100644 --- a/apps/test-app/CHANGELOG.md +++ b/apps/test-app/CHANGELOG.md @@ -1,5 +1,15 @@ # react-native-node-api-test-app +## 0.2.1 + +### Patch Changes + +- 7ff2c2b: Fix minor package issues. +- Updated dependencies [7ff2c2b] +- Updated dependencies [7ff2c2b] + - weak-node-api@0.0.3 + - react-native-node-api@0.7.1 + ## 0.2.0 ### Minor Changes diff --git a/apps/test-app/package.json b/apps/test-app/package.json index 39518dd6..559607aa 100644 --- a/apps/test-app/package.json +++ b/apps/test-app/package.json @@ -2,7 +2,7 @@ "name": "@react-native-node-api/test-app", "private": true, "type": "commonjs", - "version": "0.2.0", + "version": "0.2.1", "scripts": { "metro": "react-native start --no-interactive", "android": "react-native run-android --no-packager --active-arch-only", diff --git a/packages/cli-utils/CHANGELOG.md b/packages/cli-utils/CHANGELOG.md index a5290fa7..1f135503 100644 --- a/packages/cli-utils/CHANGELOG.md +++ b/packages/cli-utils/CHANGELOG.md @@ -1,5 +1,11 @@ # @react-native-node-api/cli-utils +## 0.1.2 + +### Patch Changes + +- 7ff2c2b: Fix minor package issues. + ## 0.1.1 ### Patch Changes diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index 05d9be6c..ac95662a 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -1,6 +1,6 @@ { "name": "@react-native-node-api/cli-utils", - "version": "0.1.1", + "version": "0.1.2", "description": "Useful utilities for the CLIs in the React Native Node API mono-repo", "type": "module", "files": [ diff --git a/packages/cmake-file-api/CHANGELOG.md b/packages/cmake-file-api/CHANGELOG.md new file mode 100644 index 00000000..484f35f2 --- /dev/null +++ b/packages/cmake-file-api/CHANGELOG.md @@ -0,0 +1,7 @@ +# cmake-file-api + +## 0.1.1 + +### Patch Changes + +- 7ff2c2b: Fix minor package issues. diff --git a/packages/cmake-file-api/package.json b/packages/cmake-file-api/package.json index 4958f523..f1a3fcb8 100644 --- a/packages/cmake-file-api/package.json +++ b/packages/cmake-file-api/package.json @@ -1,6 +1,6 @@ { "name": "cmake-file-api", - "version": "0.1.0", + "version": "0.1.1", "type": "module", "description": "TypeScript wrapper around the CMake File API", "homepage": "https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html", diff --git a/packages/cmake-rn/CHANGELOG.md b/packages/cmake-rn/CHANGELOG.md index 3c7a5f72..0abb2ded 100644 --- a/packages/cmake-rn/CHANGELOG.md +++ b/packages/cmake-rn/CHANGELOG.md @@ -1,5 +1,16 @@ # cmake-rn +## 0.6.1 + +### Patch Changes + +- Updated dependencies [7ff2c2b] +- Updated dependencies [7ff2c2b] + - cmake-file-api@0.1.1 + - weak-node-api@0.0.3 + - @react-native-node-api/cli-utils@0.1.2 + - react-native-node-api@0.7.1 + ## 0.6.0 ### Minor Changes diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index df59dbb1..8441285d 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -1,6 +1,6 @@ { "name": "cmake-rn", - "version": "0.6.0", + "version": "0.6.1", "description": "Build React Native Node API modules with CMake", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -24,11 +24,11 @@ "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" }, "dependencies": { - "@react-native-node-api/cli-utils": "0.1.1", - "cmake-file-api": "0.1.0", - "react-native-node-api": "0.7.0", + "@react-native-node-api/cli-utils": "0.1.2", + "cmake-file-api": "0.1.1", + "react-native-node-api": "0.7.1", "zod": "^4.1.11", - "weak-node-api": "0.0.2" + "weak-node-api": "0.0.3" }, "peerDependencies": { "node-addon-api": "^8.3.1", diff --git a/packages/ferric/CHANGELOG.md b/packages/ferric/CHANGELOG.md index 7d4a69c3..cd4fc02c 100644 --- a/packages/ferric/CHANGELOG.md +++ b/packages/ferric/CHANGELOG.md @@ -1,5 +1,15 @@ # ferric-cli +## 0.3.9 + +### Patch Changes + +- Updated dependencies [7ff2c2b] +- Updated dependencies [7ff2c2b] + - weak-node-api@0.0.3 + - @react-native-node-api/cli-utils@0.1.2 + - react-native-node-api@0.7.1 + ## 0.3.8 ### Patch Changes diff --git a/packages/ferric/package.json b/packages/ferric/package.json index 274718c5..30dd641f 100644 --- a/packages/ferric/package.json +++ b/packages/ferric/package.json @@ -1,6 +1,6 @@ { "name": "ferric-cli", - "version": "0.3.8", + "version": "0.3.9", "description": "Rust Node-API Modules for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -17,8 +17,8 @@ }, "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" } } diff --git a/packages/gyp-to-cmake/CHANGELOG.md b/packages/gyp-to-cmake/CHANGELOG.md index 86b34c6b..8998b3ce 100644 --- a/packages/gyp-to-cmake/CHANGELOG.md +++ b/packages/gyp-to-cmake/CHANGELOG.md @@ -1,5 +1,12 @@ # gyp-to-cmake +## 0.5.1 + +### Patch Changes + +- Updated dependencies [7ff2c2b] + - @react-native-node-api/cli-utils@0.1.2 + ## 0.5.0 ### Minor Changes diff --git a/packages/gyp-to-cmake/package.json b/packages/gyp-to-cmake/package.json index 9dd09db4..da644783 100644 --- a/packages/gyp-to-cmake/package.json +++ b/packages/gyp-to-cmake/package.json @@ -1,6 +1,6 @@ { "name": "gyp-to-cmake", - "version": "0.5.0", + "version": "0.5.1", "description": "Convert binding.gyp files to CMakeLists.txt", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -22,7 +22,7 @@ "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" }, "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" diff --git a/packages/host/CHANGELOG.md b/packages/host/CHANGELOG.md index c29638f8..9fe8fb94 100644 --- a/packages/host/CHANGELOG.md +++ b/packages/host/CHANGELOG.md @@ -1,5 +1,15 @@ # react-native-node-api +## 0.7.1 + +### Patch Changes + +- 7ff2c2b: Fix minor package issues. +- Updated dependencies [7ff2c2b] +- Updated dependencies [7ff2c2b] + - weak-node-api@0.0.3 + - @react-native-node-api/cli-utils@0.1.2 + ## 0.7.0 ### Minor Changes diff --git a/packages/host/package.json b/packages/host/package.json index a071e6f8..77b51b50 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -1,6 +1,6 @@ { "name": "react-native-node-api", - "version": "0.7.0", + "version": "0.7.1", "description": "Node-API for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -68,7 +68,7 @@ "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" @@ -81,6 +81,6 @@ "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" } } diff --git a/packages/weak-node-api/CHANGELOG.md b/packages/weak-node-api/CHANGELOG.md index 88a11828..182fbff9 100644 --- a/packages/weak-node-api/CHANGELOG.md +++ b/packages/weak-node-api/CHANGELOG.md @@ -1,5 +1,12 @@ # weak-node-api +## 0.0.3 + +### Patch Changes + +- 7ff2c2b: Fix minor package issues. +- 7ff2c2b: Add missing "generated" directory + ## 0.0.2 ### Patch Changes diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json index c5d85c6d..90de2de8 100644 --- a/packages/weak-node-api/package.json +++ b/packages/weak-node-api/package.json @@ -1,6 +1,6 @@ { "name": "weak-node-api", - "version": "0.0.2", + "version": "0.0.3", "description": "A linkable and runtime-injectable Node-API", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { From 14d6d4542d914c7896063a1079df8ce040ffdb20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 9 Nov 2025 07:39:04 +0100 Subject: [PATCH 65/82] Refactor `weak-node-api` generator (#327) * Refactor generation into templated strings and functions * Drive the configure, build and test from package scripts --- packages/weak-node-api/.gitignore | 1 + packages/weak-node-api/package.json | 3 + .../scripts/generate-weak-node-api.ts | 164 ++++++++++-------- 3 files changed, 97 insertions(+), 71 deletions(-) diff --git a/packages/weak-node-api/.gitignore b/packages/weak-node-api/.gitignore index 5cc9e939..652a5f16 100644 --- a/packages/weak-node-api/.gitignore +++ b/packages/weak-node-api/.gitignore @@ -2,6 +2,7 @@ # Everything in weak-node-api is generated, except for the configurations # Generated and built via `npm run bootstrap` /build/ +/build-tests/ /*.xcframework /*.android.node /generated/weak_node_api.cpp diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json index 90de2de8..84a510f3 100644 --- a/packages/weak-node-api/package.json +++ b/packages/weak-node-api/package.json @@ -33,6 +33,9 @@ "build-weak-node-api:apple": "node --run build-weak-node-api -- --apple", "build-weak-node-api:all": "node --run build-weak-node-api -- --android --apple", "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", + "test:configure": "cmake -S . -B build-tests -DBUILD_TESTS=ON", + "test:build": "cmake --build build-tests", + "test:run": "ctest --test-dir build-tests --output-on-failure", "bootstrap": "node --run prepare-weak-node-api && node --run build-weak-node-api", "prerelease": "node --run prepare-weak-node-api && node --run build-weak-node-api:all" }, diff --git a/packages/weak-node-api/scripts/generate-weak-node-api.ts b/packages/weak-node-api/scripts/generate-weak-node-api.ts index b47aed27..50097db6 100644 --- a/packages/weak-node-api/scripts/generate-weak-node-api.ts +++ b/packages/weak-node-api/scripts/generate-weak-node-api.ts @@ -9,92 +9,114 @@ import { export const OUTPUT_PATH = path.join(import.meta.dirname, "../generated"); +type GenerateFileOptions = { + functions: FunctionDecl[]; + fileName: string; + generator: (functions: FunctionDecl[]) => string; +}; + +async function generateFile({ + functions, + fileName, + generator, +}: GenerateFileOptions) { + const output = generator(functions); + const outputPath = path.join(OUTPUT_PATH, fileName); + await fs.promises.writeFile(outputPath, output, "utf-8"); + cp.spawnSync("clang-format", ["-i", outputPath], { stdio: "inherit" }); +} + +async function run() { + await fs.promises.mkdir(OUTPUT_PATH, { recursive: true }); + + const functions = getNodeApiFunctions(); + await generateFile({ + functions, + fileName: "weak_node_api.hpp", + generator: generateHeader, + }); + await generateFile({ + functions, + fileName: "weak_node_api.cpp", + generator: generateSource, + }); +} + +export function generateFunctionDecl({ + returnType, + name, + argumentTypes, +}: FunctionDecl) { + return `${returnType} (*${name})(${argumentTypes.join(", ")});`; +} + /** * Generates source code for a version script for the given Node API version. */ export function generateHeader(functions: FunctionDecl[]) { - return [ - "// This file is generated by react-native-node-api", - "#include ", // Node-API - "#include ", // fprintf() - "#include ", // abort() - "", + return ` + // This file is generated by react-native-node-api + #include // Node-API + #include // fprintf() + #include // abort() + // Ideally we would have just used NAPI_NO_RETURN, but // __declspec(noreturn) (when building with Microsoft Visual C++) cannot be used on members of a struct // TODO: If we targeted C++23 we could use std::unreachable() - "#if defined(__GNUC__)", - "#define WEAK_NODE_API_UNREACHABLE __builtin_unreachable();", - "#else", - "#define WEAK_NODE_API_UNREACHABLE __assume(0);", - "#endif", - "", + + #if defined(__GNUC__) + #define WEAK_NODE_API_UNREACHABLE __builtin_unreachable(); + #else + #define WEAK_NODE_API_UNREACHABLE __assume(0); + #endif + // Generate the struct of function pointers - "struct WeakNodeApiHost {", - ...functions.map(({ returnType, name, argumentTypes }) => - [ - returnType, - // Signature - `(*${name})(${argumentTypes.join(", ")});`, - ].join(" "), - ), - "};", - "typedef void(*InjectHostFunction)(const WeakNodeApiHost&);", - `extern "C" void inject_weak_node_api_host(const WeakNodeApiHost& host);`, - ].join("\n"); + struct WeakNodeApiHost { + ${functions.map(generateFunctionDecl).join("\n")} + }; + typedef void(*InjectHostFunction)(const WeakNodeApiHost&); + extern "C" void inject_weak_node_api_host(const WeakNodeApiHost& host); + `; +} + +function generateFunctionImpl({ + returnType, + name, + argumentTypes, + noReturn, +}: FunctionDecl) { + return ` + extern "C" ${returnType} ${name}( + ${argumentTypes.map((type, index) => `${type} arg${index}`).join(", ")} + ) { + if (g_host.${name} == nullptr) { + fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n"); + abort(); + } + ${returnType === "void" ? "" : "return "} g_host.${name}( + ${argumentTypes.map((_, index) => `arg${index}`).join(", ")} + ); + ${noReturn ? "WEAK_NODE_API_UNREACHABLE" : ""} + }; + `; } /** * Generates source code for a version script for the given Node API version. */ export function generateSource(functions: FunctionDecl[]) { - return [ - "// This file is generated by react-native-node-api", - `#include "weak_node_api.hpp"`, // Generated header - // Generate the struct of function pointers - "WeakNodeApiHost g_host;", - "void inject_weak_node_api_host(const WeakNodeApiHost& host) {", - " g_host = host;", - "};", - ``, - // Generate function calling into the host - ...functions.flatMap(({ returnType, name, argumentTypes, noReturn }) => { - return [ - 'extern "C"', - returnType, - name, - "(", - argumentTypes.map((type, index) => `${type} arg${index}`).join(", "), - ") {", - `if (g_host.${name} == nullptr) {`, - ` fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n");`, - " abort();", - "}", - returnType === "void" ? "" : "return ", - `g_host.${name}`, - "(", - argumentTypes.map((_, index) => `arg${index}`).join(", "), - ");", - noReturn ? "WEAK_NODE_API_UNREACHABLE" : "", - "};", - ].join(" "); - }), - ].join("\n"); -} + return ` + // This file is generated by react-native-node-api + #include "weak_node_api.hpp" // Generated header -async function run() { - await fs.promises.mkdir(OUTPUT_PATH, { recursive: true }); - - const nodeApiFunctions = getNodeApiFunctions(); - - const header = generateHeader(nodeApiFunctions); - const headerPath = path.join(OUTPUT_PATH, "weak_node_api.hpp"); - await fs.promises.writeFile(headerPath, header, "utf-8"); - cp.spawnSync("clang-format", ["-i", headerPath], { stdio: "inherit" }); - - const source = generateSource(nodeApiFunctions); - const sourcePath = path.join(OUTPUT_PATH, "weak_node_api.cpp"); - await fs.promises.writeFile(sourcePath, source, "utf-8"); - cp.spawnSync("clang-format", ["-i", sourcePath], { stdio: "inherit" }); + WeakNodeApiHost g_host; + void inject_weak_node_api_host(const WeakNodeApiHost& host) { + g_host = host; + }; + + // Generate function calling into the host + ${functions.map(generateFunctionImpl).join("\n")} + `; } run().catch((err) => { From a0163f59cc3514496ef0f7bceb1518b58ab08bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 10 Nov 2025 22:42:08 +0100 Subject: [PATCH 66/82] Move existing generators to a separate file (#328) --- .../scripts/generate-weak-node-api.ts | 96 +++---------------- .../scripts/generators/shared.ts | 28 ++++++ .../scripts/generators/weak-node-api.ts | 74 ++++++++++++++ 3 files changed, 117 insertions(+), 81 deletions(-) create mode 100644 packages/weak-node-api/scripts/generators/shared.ts create mode 100644 packages/weak-node-api/scripts/generators/weak-node-api.ts diff --git a/packages/weak-node-api/scripts/generate-weak-node-api.ts b/packages/weak-node-api/scripts/generate-weak-node-api.ts index 50097db6..7b99d472 100644 --- a/packages/weak-node-api/scripts/generate-weak-node-api.ts +++ b/packages/weak-node-api/scripts/generate-weak-node-api.ts @@ -1,3 +1,4 @@ +import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import cp from "node:child_process"; @@ -7,6 +8,8 @@ import { getNodeApiFunctions, } from "../src/node-api-functions.js"; +import * as weakNodeApiGenerator from "./generators/weak-node-api.js"; + export const OUTPUT_PATH = path.join(import.meta.dirname, "../generated"); type GenerateFileOptions = { @@ -20,10 +23,18 @@ async function generateFile({ fileName, generator, }: GenerateFileOptions) { - const output = generator(functions); + const generated = generator(functions); + const output = `// This file is generated - don't edit it directly\n\n${generated}`; const outputPath = path.join(OUTPUT_PATH, fileName); await fs.promises.writeFile(outputPath, output, "utf-8"); - cp.spawnSync("clang-format", ["-i", outputPath], { stdio: "inherit" }); + const { status, stderr = "No error output" } = cp.spawnSync( + "clang-format", + ["-i", outputPath], + { + encoding: "utf8", + }, + ); + assert.equal(status, 0, `Failed to format ${fileName}: ${stderr}`); } async function run() { @@ -33,92 +44,15 @@ async function run() { await generateFile({ functions, fileName: "weak_node_api.hpp", - generator: generateHeader, + generator: weakNodeApiGenerator.generateHeader, }); await generateFile({ functions, fileName: "weak_node_api.cpp", - generator: generateSource, + generator: weakNodeApiGenerator.generateSource, }); } -export function generateFunctionDecl({ - returnType, - name, - argumentTypes, -}: FunctionDecl) { - return `${returnType} (*${name})(${argumentTypes.join(", ")});`; -} - -/** - * Generates source code for a version script for the given Node API version. - */ -export function generateHeader(functions: FunctionDecl[]) { - return ` - // This file is generated by react-native-node-api - #include // Node-API - #include // fprintf() - #include // abort() - - // Ideally we would have just used NAPI_NO_RETURN, but - // __declspec(noreturn) (when building with Microsoft Visual C++) cannot be used on members of a struct - // TODO: If we targeted C++23 we could use std::unreachable() - - #if defined(__GNUC__) - #define WEAK_NODE_API_UNREACHABLE __builtin_unreachable(); - #else - #define WEAK_NODE_API_UNREACHABLE __assume(0); - #endif - - // Generate the struct of function pointers - struct WeakNodeApiHost { - ${functions.map(generateFunctionDecl).join("\n")} - }; - typedef void(*InjectHostFunction)(const WeakNodeApiHost&); - extern "C" void inject_weak_node_api_host(const WeakNodeApiHost& host); - `; -} - -function generateFunctionImpl({ - returnType, - name, - argumentTypes, - noReturn, -}: FunctionDecl) { - return ` - extern "C" ${returnType} ${name}( - ${argumentTypes.map((type, index) => `${type} arg${index}`).join(", ")} - ) { - if (g_host.${name} == nullptr) { - fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n"); - abort(); - } - ${returnType === "void" ? "" : "return "} g_host.${name}( - ${argumentTypes.map((_, index) => `arg${index}`).join(", ")} - ); - ${noReturn ? "WEAK_NODE_API_UNREACHABLE" : ""} - }; - `; -} - -/** - * Generates source code for a version script for the given Node API version. - */ -export function generateSource(functions: FunctionDecl[]) { - return ` - // This file is generated by react-native-node-api - #include "weak_node_api.hpp" // Generated header - - WeakNodeApiHost g_host; - void inject_weak_node_api_host(const WeakNodeApiHost& host) { - g_host = host; - }; - - // Generate function calling into the host - ${functions.map(generateFunctionImpl).join("\n")} - `; -} - run().catch((err) => { console.error(err); process.exitCode = 1; diff --git a/packages/weak-node-api/scripts/generators/shared.ts b/packages/weak-node-api/scripts/generators/shared.ts new file mode 100644 index 00000000..88104e58 --- /dev/null +++ b/packages/weak-node-api/scripts/generators/shared.ts @@ -0,0 +1,28 @@ +import type { FunctionDecl } from "../../src/node-api-functions.js"; + +type FunctionOptions = FunctionDecl & { + extern?: true; + static?: true; + namespace?: string; + body?: string; + argumentNames?: string[]; +}; + +export function generateFunction({ + extern, + static: staticMember, + returnType, + namespace, + name, + argumentTypes, + argumentNames = [], + noReturn, + body, +}: FunctionOptions) { + return ` + ${staticMember ? "static " : ""}${extern ? 'extern "C" ' : ""}${returnType} ${namespace ? namespace + "::" : ""}${name}( + ${argumentTypes.map((type, index) => `${type} ` + (argumentNames[index] ?? `arg${index}`)).join(", ")} + ) ${body ? `{ ${body} ${noReturn ? "WEAK_NODE_API_UNREACHABLE;" : ""}\n}` : ""} + ; + `; +} diff --git a/packages/weak-node-api/scripts/generators/weak-node-api.ts b/packages/weak-node-api/scripts/generators/weak-node-api.ts new file mode 100644 index 00000000..964b3d73 --- /dev/null +++ b/packages/weak-node-api/scripts/generators/weak-node-api.ts @@ -0,0 +1,74 @@ +import type { FunctionDecl } from "../../src/node-api-functions.js"; +import { generateFunction } from "./shared.js"; + +export function generateFunctionDecl({ + returnType, + name, + argumentTypes, +}: FunctionDecl) { + return `${returnType} (*${name})(${argumentTypes.join(", ")});`; +} + +/** + * Generates source code for a version script for the given Node API version. + */ +export function generateHeader(functions: FunctionDecl[]) { + return ` + #pragma once + + #include // Node-API + #include // fprintf() + #include // abort() + + // Ideally we would have just used NAPI_NO_RETURN, but + // __declspec(noreturn) (when building with Microsoft Visual C++) cannot be used on members of a struct + // TODO: If we targeted C++23 we could use std::unreachable() + + #if defined(__GNUC__) + #define WEAK_NODE_API_UNREACHABLE __builtin_unreachable() + #else + #define WEAK_NODE_API_UNREACHABLE __assume(0) + #endif + + // Generate the struct of function pointers + struct WeakNodeApiHost { + ${functions.map(generateFunctionDecl).join("\n")} + }; + typedef void(*InjectHostFunction)(const WeakNodeApiHost&); + extern "C" void inject_weak_node_api_host(const WeakNodeApiHost& host); + `; +} + +function generateFunctionImpl(fn: FunctionDecl) { + const { name, returnType, argumentTypes } = fn; + return generateFunction({ + ...fn, + extern: true, + body: ` + if (g_host.${name} == nullptr) { + fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n"); + abort(); + } + ${returnType === "void" ? "" : "return "} g_host.${name}( + ${argumentTypes.map((_, index) => `arg${index}`).join(", ")} + ); + `, + }); +} + +/** + * Generates source code for a version script for the given Node API version. + */ +export function generateSource(functions: FunctionDecl[]) { + return ` + #include "weak_node_api.hpp" + + WeakNodeApiHost g_host; + void inject_weak_node_api_host(const WeakNodeApiHost& host) { + g_host = host; + }; + + // Generate function calling into the host + ${functions.map(generateFunctionImpl).join("\n")} + `; +} From bf3d1cc4da9e8b08809e938f5a16550dd62a27a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 14 Nov 2025 15:01:03 +0100 Subject: [PATCH 67/82] Fix mocha-and-metro command --- apps/test-app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/test-app/package.json b/apps/test-app/package.json index 559607aa..9eb7048d 100644 --- a/apps/test-app/package.json +++ b/apps/test-app/package.json @@ -8,7 +8,7 @@ "android": "react-native run-android --no-packager --active-arch-only", "ios": "react-native run-ios --no-packager", "pod-install": "cd ios && pod install", - "mocha-and-metro": "mocha-remote --exit-on-error -- node --run metro", + "mocha-and-metro": "mocha-remote --watch -- react-native start", "test:android": "mocha-remote --exit-on-error -- concurrently --kill-others-on-fail --passthrough-arguments npm:metro 'npm:android -- {@}' --", "test:android:allTests": "MOCHA_REMOTE_CONTEXT=allTests node --run test:android -- ", "test:android:nodeAddonExamples": "MOCHA_REMOTE_CONTEXT=nodeAddonExamples node --run test:android -- ", From 22340a2e697f925540707dd479faca232d8e54ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 14 Nov 2025 15:03:56 +0100 Subject: [PATCH 68/82] Strip nothing from host package when debugging --- packages/host/android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/host/android/build.gradle b/packages/host/android/build.gradle index 2f6b6be8..46b7aae9 100644 --- a/packages/host/android/build.gradle +++ b/packages/host/android/build.gradle @@ -122,7 +122,7 @@ android { debug { jniDebuggable true packagingOptions { - doNotStrip "**/libnode-api-host.so" + doNotStrip "**/*.so" } } release { From d1294d5d529d0d55ce9c10e7d3350bc0c530009f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 14 Nov 2025 22:59:09 +0100 Subject: [PATCH 69/82] Use "npm install" instead of "npm ci" to update package lock on release PRs --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f682950b..f071071f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: with: packages: tools platform-tools ndk;${{ env.NDK_VERSION }} - run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android aarch64-apple-ios-sim - - run: npm ci + - run: npm install - name: Create Release Pull Request or Publish to npm id: changesets From 441dcc4ed14e9c804ba0274606dc7c53b902d177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 15 Nov 2025 00:24:28 +0100 Subject: [PATCH 70/82] Ferric `--verbose` `--concurrency` and `--clean` (#332) * Add re-export of "p-limit" to cli-utils * Add --verbose, --concurrency, --clean options * Show notice when --verbose and --concurrency > 1 --- .changeset/big-plums-write.md | 5 ++ .changeset/evil-pens-shop.md | 5 ++ package-lock.json | 54 ++++++++----- packages/cli-utils/package.json | 3 +- packages/cli-utils/src/index.ts | 1 + packages/ferric/src/build.ts | 136 ++++++++++++++++++++++++-------- packages/ferric/src/cargo.ts | 6 +- 7 files changed, 154 insertions(+), 56 deletions(-) create mode 100644 .changeset/big-plums-write.md create mode 100644 .changeset/evil-pens-shop.md 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), From 1269aa0d0d93a5263f3ec42b8611cb6169c3e6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 15 Nov 2025 00:44:42 +0100 Subject: [PATCH 71/82] Fix ferric example (#333) * Prevent weak-node-api from being stripped when building with ferric * Updating napi-rs crates * Pinning napi and napi-sys --- packages/ferric-example/Cargo.toml | 12 ++++++++---- packages/ferric/src/cargo.ts | 5 +++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/ferric-example/Cargo.toml b/packages/ferric-example/Cargo.toml index 2524a0e8..a301bce0 100644 --- a/packages/ferric-example/Cargo.toml +++ b/packages/ferric-example/Cargo.toml @@ -8,17 +8,21 @@ license = "MIT" crate-type = ["cdylib"] [dependencies.napi] -version = "3.1" -default-features = false +version = "=3.4.0" # see https://nodejs.org/api/n-api.html#node-api-version-matrix +default-features = false features = ["napi3"] [dependencies.napi-derive] -version = "3.1" +version = "3.3.0" features = ["type-def"] +# See https://github.com/callstackincubator/react-native-node-api/issues/331 +[dependencies.napi-sys] +version = "=3.0.1" + [build-dependencies] -napi-build = "2" +napi-build = "2.2.4" [profile.release] lto = true diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index ef06c16b..fc4fb2ac 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -221,8 +221,9 @@ export function getTargetEnvironmentVariables({ CARGO_ENCODED_RUSTFLAGS: [ "-L", weakNodeApiPath, - "-l", - "weak-node-api", + "-C", + // Passing --no-as-needed to prevent weak-node-api from being optimized away + "link-arg=-Wl,--push-state,--no-as-needed,-lweak-node-api,--pop-state", ].join(String.fromCharCode(0x1f)), CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: joinPathAndAssertExistence( toolchainBinPath, From 182e22ab85c54be7fcf9abd4dd56919ed3828b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 15 Nov 2025 00:54:51 +0100 Subject: [PATCH 72/82] Use self-hosted Ubuntu runner for Android testing (#322) * Use self-hosted Ubuntu runner for Android testing * Install CMake ourselves --- .github/workflows/check.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 17cfdac4..63da3100 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -3,6 +3,8 @@ name: Check env: # Version here should match the one in React Native template and packages/cmake-rn/src/cli.ts NDK_VERSION: 27.1.12297006 + # Building Hermes from source doesn't support CMake v4 + CMAKE_VERSION: 3.31.6 # Enabling the Gradle test on CI (disabled by default because it downloads a lot) ENABLE_GRADLE_TESTS: true @@ -155,7 +157,7 @@ jobs: - name: Install compatible CMake version uses: jwlawson/actions-setup-cmake@v2 with: - cmake-version: "3.31.2" + cmake-version: ${{ env.CMAKE_VERSION }} - run: rustup target add x86_64-apple-darwin - run: npm ci - run: npm run bootstrap @@ -171,7 +173,7 @@ jobs: test-android: if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Android 🤖') name: Test app (Android) - runs-on: ubuntu-latest + runs-on: ubuntu-self-hosted steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -185,7 +187,7 @@ jobs: - name: Setup Android SDK uses: android-actions/setup-android@v3 with: - packages: tools platform-tools ndk;${{ env.NDK_VERSION }} + packages: tools platform-tools ndk;${{ env.NDK_VERSION }} cmake;${{ env.CMAKE_VERSION }} - run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android aarch64-apple-ios-sim - run: npm ci - run: npm run bootstrap From 3d2e03ea7d1f81260d31037e06d7e83b71db4d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 18 Nov 2025 06:54:34 +0100 Subject: [PATCH 73/82] More refactoring of the `weak-node-api` generator (#330) * Renamed package scripts and generate file * Rename injector related package script * Splitting WeakNodeApiHost out of weak_node_api and renaming to NodeApiHost * Print heading comments in generated files * Add changeset * Remove incorrect comments * Align / simplify namespace and host package functions --- .changeset/slimy-parts-admire.md | 5 ++ .github/workflows/check.yml | 8 +- package.json | 1 - packages/host/android/build.gradle | 2 +- .../host/android/src/main/AndroidManifest.xml | 2 +- packages/host/android/src/main/cpp/OnLoad.cpp | 8 +- .../NodeApiHostPackage.kt} | 4 +- .../host/apple/NodeApiHostModuleProvider.mm | 12 +-- packages/host/cpp/AddonLoaders.hpp | 2 +- packages/host/cpp/CxxNodeApiHostModule.cpp | 6 +- packages/host/cpp/CxxNodeApiHostModule.hpp | 4 +- packages/host/cpp/Logger.cpp | 46 +++++------ packages/host/cpp/Logger.hpp | 10 +-- packages/host/cpp/RuntimeNodeApi.cpp | 63 +++++++-------- packages/host/cpp/RuntimeNodeApi.hpp | 60 +++++++------- packages/host/cpp/RuntimeNodeApiAsync.cpp | 81 +++++++++---------- packages/host/cpp/RuntimeNodeApiAsync.hpp | 28 +++---- packages/host/cpp/WeakNodeApiInjector.hpp | 2 +- packages/host/package.json | 5 +- ...api-injector.mts => generate-injector.mts} | 8 +- packages/weak-node-api/.gitignore | 3 +- packages/weak-node-api/CMakeLists.txt | 1 + packages/weak-node-api/package.json | 15 ++-- ...{generate-weak-node-api.ts => generate.ts} | 40 ++++++++- .../scripts/generators/NodeApiHost.ts | 32 ++++++++ .../scripts/generators/weak-node-api.ts | 49 ++++------- .../weak-node-api/src/node-api-functions.ts | 4 - packages/weak-node-api/tests/test_inject.cpp | 4 +- 28 files changed, 268 insertions(+), 237 deletions(-) create mode 100644 .changeset/slimy-parts-admire.md rename packages/host/android/src/main/java/com/callstack/{node_api_modules/NodeApiModulesPackage.kt => react_native_node_api/NodeApiHostPackage.kt} (89%) rename packages/host/scripts/{generate-weak-node-api-injector.mts => generate-injector.mts} (93%) rename packages/weak-node-api/scripts/{generate-weak-node-api.ts => generate.ts} (54%) create mode 100644 packages/weak-node-api/scripts/generators/NodeApiHost.ts diff --git a/.changeset/slimy-parts-admire.md b/.changeset/slimy-parts-admire.md new file mode 100644 index 00000000..de6b0ed2 --- /dev/null +++ b/.changeset/slimy-parts-admire.md @@ -0,0 +1,5 @@ +--- +"weak-node-api": minor +--- + +Renamed WeakNodeApiHost to NodeApiHost diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 63da3100..3676c622 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -99,7 +99,7 @@ jobs: - run: npm ci - run: npm run build - name: Prepare weak-node-api - run: npm run prepare-weak-node-api --workspace weak-node-api + run: npm run prebuild:prepare --workspace weak-node-api - name: Build and run weak-node-api C++ tests run: | cmake -S . -B build -DBUILD_TESTS=ON @@ -215,7 +215,7 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - name: Build weak-node-api for all Android architectures - run: npm run build-weak-node-api:android --workspace weak-node-api + run: npm run prebuild:build:android --workspace weak-node-api - name: Build ferric-example for all architectures run: npm run build -- --android working-directory: packages/ferric-example @@ -270,8 +270,8 @@ jobs: - run: npm run build - name: Build weak-node-api for all Apple architectures run: | - npm run prepare-weak-node-api --workspace weak-node-api - npm run build-weak-node-api:apple --workspace weak-node-api + npm run prebuild:prepare --workspace weak-node-api + npm run prebuild:build:apple --workspace weak-node-api # Build Ferric example for all Apple architectures - run: npx ferric --apple working-directory: packages/ferric-example diff --git a/package.json b/package.json index 7c4400dc..72dab66e 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "prettier:write": "prettier --experimental-cli --write .", "test": "npm test --workspace react-native-node-api --workspace cmake-rn --workspace gyp-to-cmake --workspace node-addon-examples", "bootstrap": "node --run build && npm run bootstrap --workspaces --if-present", - "prerelease": "node --run build && npm run prerelease --workspaces --if-present", "changeset": "changeset", "release": "changeset publish", "init-macos-test-app": "node scripts/init-macos-test-app.ts" diff --git a/packages/host/android/build.gradle b/packages/host/android/build.gradle index 46b7aae9..39204133 100644 --- a/packages/host/android/build.gradle +++ b/packages/host/android/build.gradle @@ -67,7 +67,7 @@ def supportsNamespace() { android { if (supportsNamespace()) { - namespace "com.callstack.node_api_modules" + namespace "com.callstack.react_native_node_api" sourceSets { main { diff --git a/packages/host/android/src/main/AndroidManifest.xml b/packages/host/android/src/main/AndroidManifest.xml index 99212aac..1b416675 100644 --- a/packages/host/android/src/main/AndroidManifest.xml +++ b/packages/host/android/src/main/AndroidManifest.xml @@ -1,3 +1,3 @@ + package="com.callstack.react_native_node_api"> diff --git a/packages/host/android/src/main/cpp/OnLoad.cpp b/packages/host/android/src/main/cpp/OnLoad.cpp index 35e27128..3cd6f306 100644 --- a/packages/host/android/src/main/cpp/OnLoad.cpp +++ b/packages/host/android/src/main/cpp/OnLoad.cpp @@ -7,13 +7,13 @@ // Called when the library is loaded jint JNI_OnLoad(JavaVM *vm, void *reserved) { - callstack::nodeapihost::injectIntoWeakNodeApi(); + callstack::react_native_node_api::injectIntoWeakNodeApi(); // Register the C++ TurboModule facebook::react::registerCxxModuleToGlobalModuleMap( - callstack::nodeapihost::CxxNodeApiHostModule::kModuleName, + callstack::react_native_node_api::CxxNodeApiHostModule::kModuleName, [](std::shared_ptr jsInvoker) { - return std::make_shared( - jsInvoker); + return std::make_shared< + callstack::react_native_node_api::CxxNodeApiHostModule>(jsInvoker); }); return JNI_VERSION_1_6; } diff --git a/packages/host/android/src/main/java/com/callstack/node_api_modules/NodeApiModulesPackage.kt b/packages/host/android/src/main/java/com/callstack/react_native_node_api/NodeApiHostPackage.kt similarity index 89% rename from packages/host/android/src/main/java/com/callstack/node_api_modules/NodeApiModulesPackage.kt rename to packages/host/android/src/main/java/com/callstack/react_native_node_api/NodeApiHostPackage.kt index 105d347e..38426367 100644 --- a/packages/host/android/src/main/java/com/callstack/node_api_modules/NodeApiModulesPackage.kt +++ b/packages/host/android/src/main/java/com/callstack/react_native_node_api/NodeApiHostPackage.kt @@ -1,4 +1,4 @@ -package com.callstack.node_api_modules +package com.callstack.react_native_node_api import com.facebook.hermes.reactexecutor.HermesExecutor import com.facebook.react.BaseReactPackage @@ -10,7 +10,7 @@ import com.facebook.soloader.SoLoader import java.util.HashMap -class NodeApiModulesPackage : BaseReactPackage() { +class NodeApiHostPackage : BaseReactPackage() { init { SoLoader.loadLibrary("node-api-host") } diff --git a/packages/host/apple/NodeApiHostModuleProvider.mm b/packages/host/apple/NodeApiHostModuleProvider.mm index b01c5306..1ea633fe 100644 --- a/packages/host/apple/NodeApiHostModuleProvider.mm +++ b/packages/host/apple/NodeApiHostModuleProvider.mm @@ -2,19 +2,19 @@ #import "WeakNodeApiInjector.hpp" #import -@interface NodeApiHost : NSObject +@interface NodeApiHostPackage : NSObject @end -@implementation NodeApiHost +@implementation NodeApiHostPackage + (void)load { - callstack::nodeapihost::injectIntoWeakNodeApi(); + callstack::react_native_node_api::injectIntoWeakNodeApi(); facebook::react::registerCxxModuleToGlobalModuleMap( - callstack::nodeapihost::CxxNodeApiHostModule::kModuleName, + callstack::react_native_node_api::CxxNodeApiHostModule::kModuleName, [](std::shared_ptr jsInvoker) { - return std::make_shared( - jsInvoker); + return std::make_shared< + callstack::react_native_node_api::CxxNodeApiHostModule>(jsInvoker); }); } diff --git a/packages/host/cpp/AddonLoaders.hpp b/packages/host/cpp/AddonLoaders.hpp index d0d5d269..2836bdfa 100644 --- a/packages/host/cpp/AddonLoaders.hpp +++ b/packages/host/cpp/AddonLoaders.hpp @@ -7,7 +7,7 @@ #include #include -using callstack::nodeapihost::log_debug; +using callstack::react_native_node_api::log_debug; struct PosixLoader { using Module = void *; diff --git a/packages/host/cpp/CxxNodeApiHostModule.cpp b/packages/host/cpp/CxxNodeApiHostModule.cpp index 950c7af6..0b1961ec 100644 --- a/packages/host/cpp/CxxNodeApiHostModule.cpp +++ b/packages/host/cpp/CxxNodeApiHostModule.cpp @@ -4,7 +4,7 @@ using namespace facebook; -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { CxxNodeApiHostModule::CxxNodeApiHostModule( std::shared_ptr jsInvoker) @@ -127,8 +127,8 @@ bool CxxNodeApiHostModule::initializeNodeModule(jsi::Runtime &rt, napi_set_named_property(env, global, addon.generatedName.data(), exports); assert(status == napi_ok); - callstack::nodeapihost::setCallInvoker(env, callInvoker_); + callstack::react_native_node_api::setCallInvoker(env, callInvoker_); return true; } -} // namespace callstack::nodeapihost +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/CxxNodeApiHostModule.hpp b/packages/host/cpp/CxxNodeApiHostModule.hpp index 9445cdaf..4c753cfe 100644 --- a/packages/host/cpp/CxxNodeApiHostModule.hpp +++ b/packages/host/cpp/CxxNodeApiHostModule.hpp @@ -6,7 +6,7 @@ #include "AddonLoaders.hpp" -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { class JSI_EXPORT CxxNodeApiHostModule : public facebook::react::TurboModule { public: @@ -37,4 +37,4 @@ class JSI_EXPORT CxxNodeApiHostModule : public facebook::react::TurboModule { bool initializeNodeModule(facebook::jsi::Runtime &rt, NodeAddon &addon); }; -} // namespace callstack::nodeapihost +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/Logger.cpp b/packages/host/cpp/Logger.cpp index 0d2a9c9f..b863fcdf 100644 --- a/packages/host/cpp/Logger.cpp +++ b/packages/host/cpp/Logger.cpp @@ -16,33 +16,33 @@ enum class LogLevel { Debug, Warning, Error }; constexpr std::string_view levelToString(LogLevel level) { switch (level) { - case LogLevel::Debug: - return "DEBUG"; - case LogLevel::Warning: - return "WARNING"; - case LogLevel::Error: - return "ERROR"; - default: - return "UNKNOWN"; + case LogLevel::Debug: + return "DEBUG"; + case LogLevel::Warning: + return "WARNING"; + case LogLevel::Error: + return "ERROR"; + default: + return "UNKNOWN"; } } #if defined(__ANDROID__) constexpr int androidLogLevel(LogLevel level) { switch (level) { - case LogLevel::Debug: - return ANDROID_LOG_DEBUG; - case LogLevel::Warning: - return ANDROID_LOG_WARN; - case LogLevel::Error: - return ANDROID_LOG_ERROR; - default: - return ANDROID_LOG_UNKNOWN; + case LogLevel::Debug: + return ANDROID_LOG_DEBUG; + case LogLevel::Warning: + return ANDROID_LOG_WARN; + case LogLevel::Error: + return ANDROID_LOG_ERROR; + default: + return ANDROID_LOG_UNKNOWN; } } #endif -void log_message_internal(LogLevel level, const char* format, va_list args) { +void log_message_internal(LogLevel level, const char *format, va_list args) { #if defined(__ANDROID__) __android_log_vprint(androidLogLevel(level), LOG_TAG, format, args); #elif defined(__APPLE__) @@ -59,27 +59,27 @@ void log_message_internal(LogLevel level, const char* format, va_list args) { fprintf(stdout, "\n"); #endif } -} // anonymous namespace +} // anonymous namespace -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { -void log_debug(const char* format, ...) { +void log_debug(const char *format, ...) { // TODO: Disable logging in release builds va_list args; va_start(args, format); log_message_internal(LogLevel::Debug, format, args); va_end(args); } -void log_warning(const char* format, ...) { +void log_warning(const char *format, ...) { va_list args; va_start(args, format); log_message_internal(LogLevel::Warning, format, args); va_end(args); } -void log_error(const char* format, ...) { +void log_error(const char *format, ...) { va_list args; va_start(args, format); log_message_internal(LogLevel::Error, format, args); va_end(args); } -} // namespace callstack::nodeapihost +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/Logger.hpp b/packages/host/cpp/Logger.hpp index 350e3f21..c064e7da 100644 --- a/packages/host/cpp/Logger.hpp +++ b/packages/host/cpp/Logger.hpp @@ -2,8 +2,8 @@ #include -namespace callstack::nodeapihost { -void log_debug(const char* format, ...); -void log_warning(const char* format, ...); -void log_error(const char* format, ...); -} // namespace callstack::nodeapihost +namespace callstack::react_native_node_api { +void log_debug(const char *format, ...); +void log_warning(const char *format, ...); +void log_error(const char *format, ...); +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/RuntimeNodeApi.cpp b/packages/host/cpp/RuntimeNodeApi.cpp index 3c4d773c..c4e1c44c 100644 --- a/packages/host/cpp/RuntimeNodeApi.cpp +++ b/packages/host/cpp/RuntimeNodeApi.cpp @@ -1,14 +1,14 @@ #include "RuntimeNodeApi.hpp" -#include #include "Logger.hpp" #include "Versions.hpp" +#include auto ArrayType = napi_uint8_array; -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { -napi_status napi_create_buffer( - napi_env env, size_t length, void** data, napi_value* result) { +napi_status napi_create_buffer(napi_env env, size_t length, void **data, + napi_value *result) { napi_value buffer; if (const auto status = napi_create_arraybuffer(env, length, data, &buffer); status != napi_ok) { @@ -22,17 +22,15 @@ napi_status napi_create_buffer( return napi_create_typedarray(env, ArrayType, length, buffer, 0, result); } -napi_status napi_create_buffer_copy(napi_env env, - size_t length, - const void* data, - void** result_data, - napi_value* result) { +napi_status napi_create_buffer_copy(napi_env env, size_t length, + const void *data, void **result_data, + napi_value *result) { if (!length || !data || !result) { return napi_invalid_arg; } void *buffer = nullptr; - if (const auto status = callstack::nodeapihost::napi_create_buffer( + if (const auto status = callstack::react_native_node_api::napi_create_buffer( env, length, &buffer, result); status != napi_ok) { return status; @@ -42,7 +40,7 @@ napi_status napi_create_buffer_copy(napi_env env, return napi_ok; } -napi_status napi_is_buffer(napi_env env, napi_value value, bool* result) { +napi_status napi_is_buffer(napi_env env, napi_value value, bool *result) { if (!result) { return napi_invalid_arg; } @@ -77,8 +75,8 @@ napi_status napi_is_buffer(napi_env env, napi_value value, bool* result) { return napi_ok; } -napi_status napi_get_buffer_info( - napi_env env, napi_value value, void** data, size_t* length) { +napi_status napi_get_buffer_info(napi_env env, napi_value value, void **data, + size_t *length) { if (!data || !length) { return napi_invalid_arg; } @@ -97,19 +95,17 @@ napi_status napi_get_buffer_info( auto isTypedArray{false}; if (const auto status = napi_is_typedarray(env, value, &isTypedArray); status == napi_ok && isTypedArray) { - return napi_get_typedarray_info( - env, value, &ArrayType, length, data, nullptr, nullptr); + return napi_get_typedarray_info(env, value, &ArrayType, length, data, + nullptr, nullptr); } return napi_ok; } -napi_status napi_create_external_buffer(napi_env env, - size_t length, - void* data, - node_api_basic_finalize basic_finalize_cb, - void* finalize_hint, - napi_value* result) { +napi_status +napi_create_external_buffer(napi_env env, size_t length, void *data, + node_api_basic_finalize basic_finalize_cb, + void *finalize_hint, napi_value *result) { napi_value buffer; if (const auto status = napi_create_external_arraybuffer( env, data, length, basic_finalize_cb, finalize_hint, &buffer); @@ -124,25 +120,20 @@ napi_status napi_create_external_buffer(napi_env env, return napi_create_typedarray(env, ArrayType, length, buffer, 0, result); } -void napi_fatal_error(const char* location, - size_t location_len, - const char* message, - size_t message_len) { +void napi_fatal_error(const char *location, size_t location_len, + const char *message, size_t message_len) { if (location && location_len) { - log_error("Fatal Node-API error: %.*s %.*s", - static_cast(location_len), - location, - static_cast(message_len), - message); + log_error("Fatal Node-API error: %.*s %.*s", static_cast(location_len), + location, static_cast(message_len), message); } else { - log_error( - "Fatal Node-API error: %.*s", static_cast(message_len), message); + log_error("Fatal Node-API error: %.*s", static_cast(message_len), + message); } abort(); } -napi_status napi_get_node_version( - node_api_basic_env env, const napi_node_version** result) { +napi_status napi_get_node_version(node_api_basic_env env, + const napi_node_version **result) { if (!result) { return napi_invalid_arg; } @@ -151,7 +142,7 @@ napi_status napi_get_node_version( return napi_generic_failure; } -napi_status napi_get_version(node_api_basic_env env, uint32_t* result) { +napi_status napi_get_version(node_api_basic_env env, uint32_t *result) { if (!result) { return napi_invalid_arg; } @@ -160,4 +151,4 @@ napi_status napi_get_version(node_api_basic_env env, uint32_t* result) { return napi_ok; } -} // namespace callstack::nodeapihost +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/RuntimeNodeApi.hpp b/packages/host/cpp/RuntimeNodeApi.hpp index 67da7856..1a5e62ea 100644 --- a/packages/host/cpp/RuntimeNodeApi.hpp +++ b/packages/host/cpp/RuntimeNodeApi.hpp @@ -2,35 +2,31 @@ #include "node_api.h" -namespace callstack::nodeapihost { -napi_status napi_create_buffer( - napi_env env, size_t length, void** data, napi_value* result); - -napi_status napi_create_buffer_copy(napi_env env, - size_t length, - const void* data, - void** result_data, - napi_value* result); - -napi_status napi_is_buffer(napi_env env, napi_value value, bool* result); - -napi_status napi_get_buffer_info( - napi_env env, napi_value value, void** data, size_t* length); - -napi_status napi_create_external_buffer(napi_env env, - size_t length, - void* data, - node_api_basic_finalize basic_finalize_cb, - void* finalize_hint, - napi_value* result); - -void __attribute__((noreturn)) napi_fatal_error(const char* location, - size_t location_len, - const char* message, - size_t message_len); - -napi_status napi_get_node_version( - node_api_basic_env env, const napi_node_version** result); - -napi_status napi_get_version(node_api_basic_env env, uint32_t* result); -} // namespace callstack::nodeapihost +namespace callstack::react_native_node_api { +napi_status napi_create_buffer(napi_env env, size_t length, void **data, + napi_value *result); + +napi_status napi_create_buffer_copy(napi_env env, size_t length, + const void *data, void **result_data, + napi_value *result); + +napi_status napi_is_buffer(napi_env env, napi_value value, bool *result); + +napi_status napi_get_buffer_info(napi_env env, napi_value value, void **data, + size_t *length); + +napi_status +napi_create_external_buffer(napi_env env, size_t length, void *data, + node_api_basic_finalize basic_finalize_cb, + void *finalize_hint, napi_value *result); + +void __attribute__((noreturn)) napi_fatal_error(const char *location, + size_t location_len, + const char *message, + size_t message_len); + +napi_status napi_get_node_version(node_api_basic_env env, + const napi_node_version **result); + +napi_status napi_get_version(node_api_basic_env env, uint32_t *result); +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/RuntimeNodeApiAsync.cpp b/packages/host/cpp/RuntimeNodeApiAsync.cpp index dd4c87c3..bee380a7 100644 --- a/packages/host/cpp/RuntimeNodeApiAsync.cpp +++ b/packages/host/cpp/RuntimeNodeApiAsync.cpp @@ -1,6 +1,6 @@ #include "RuntimeNodeApiAsync.hpp" -#include #include "Logger.hpp" +#include struct AsyncJob { using IdType = uint64_t; @@ -13,26 +13,25 @@ struct AsyncJob { napi_value async_resource_name; napi_async_execute_callback execute; napi_async_complete_callback complete; - void* data{nullptr}; + void *data{nullptr}; - static AsyncJob* fromWork(napi_async_work work) { - return reinterpret_cast(work); + static AsyncJob *fromWork(napi_async_work work) { + return reinterpret_cast(work); } - static napi_async_work toWork(AsyncJob* job) { + static napi_async_work toWork(AsyncJob *job) { return reinterpret_cast(job); } }; class AsyncWorkRegistry { - public: +public: using IdType = AsyncJob::IdType; - std::shared_ptr create(napi_env env, - napi_value async_resource, - napi_value async_resource_name, - napi_async_execute_callback execute, - napi_async_complete_callback complete, - void* data) { + std::shared_ptr create(napi_env env, napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void *data) { const auto job = std::shared_ptr(new AsyncJob{ .id = next_id(), .state = AsyncJob::State::Created, @@ -68,7 +67,7 @@ class AsyncWorkRegistry { return false; } - private: +private: IdType next_id() { if (current_id_ == std::numeric_limits::max()) [[unlikely]] { current_id_ = 0; @@ -84,10 +83,11 @@ static std::unordered_map> callInvokers; static AsyncWorkRegistry asyncWorkRegistry; -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { -void setCallInvoker(napi_env env, - const std::shared_ptr& invoker) { +void setCallInvoker( + napi_env env, + const std::shared_ptr &invoker) { callInvokers[env] = invoker; } @@ -97,13 +97,11 @@ std::weak_ptr getCallInvoker(napi_env env) { : std::weak_ptr{}; } -napi_status napi_create_async_work(napi_env env, - napi_value async_resource, - napi_value async_resource_name, - napi_async_execute_callback execute, - napi_async_complete_callback complete, - void* data, - napi_async_work* result) { +napi_status napi_create_async_work(napi_env env, napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void *data, napi_async_work *result) { const auto job = asyncWorkRegistry.create( env, async_resource, async_resource_name, execute, complete, data); if (!job) { @@ -115,8 +113,8 @@ napi_status napi_create_async_work(napi_env env, return napi_ok; } -napi_status napi_queue_async_work( - node_api_basic_env env, napi_async_work work) { +napi_status napi_queue_async_work(node_api_basic_env env, + napi_async_work work) { const auto job = asyncWorkRegistry.get(work); if (!job) { log_debug("Error: Received null job in napi_queue_async_work"); @@ -140,8 +138,9 @@ napi_status napi_queue_async_work( } job->complete(env, - job->state == AsyncJob::State::Cancelled ? napi_cancelled : napi_ok, - job->data); + job->state == AsyncJob::State::Cancelled ? napi_cancelled + : napi_ok, + job->data); job->state = AsyncJob::State::Completed; }); @@ -149,8 +148,8 @@ napi_status napi_queue_async_work( return napi_ok; } -napi_status napi_delete_async_work( - node_api_basic_env env, napi_async_work work) { +napi_status napi_delete_async_work(node_api_basic_env env, + napi_async_work work) { const auto job = asyncWorkRegistry.get(work); if (!job) { log_debug("Error: Received non-existent job in napi_delete_async_work"); @@ -165,26 +164,26 @@ napi_status napi_delete_async_work( return napi_ok; } -napi_status napi_cancel_async_work( - node_api_basic_env env, napi_async_work work) { +napi_status napi_cancel_async_work(node_api_basic_env env, + napi_async_work work) { const auto job = asyncWorkRegistry.get(work); if (!job) { log_debug("Error: Received null job in napi_cancel_async_work"); return napi_invalid_arg; } switch (job->state) { - case AsyncJob::State::Completed: - log_debug("Error: Cannot cancel async work that is already completed"); - return napi_generic_failure; - case AsyncJob::State::Deleted: - log_debug("Warning: Async work job is already deleted"); - return napi_generic_failure; - case AsyncJob::State::Cancelled: - log_debug("Warning: Async work job is already cancelled"); - return napi_ok; + case AsyncJob::State::Completed: + log_debug("Error: Cannot cancel async work that is already completed"); + return napi_generic_failure; + case AsyncJob::State::Deleted: + log_debug("Warning: Async work job is already deleted"); + return napi_generic_failure; + case AsyncJob::State::Cancelled: + log_debug("Warning: Async work job is already cancelled"); + return napi_ok; } job->state = AsyncJob::State::Cancelled; return napi_ok; } -} // namespace callstack::nodeapihost +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/RuntimeNodeApiAsync.hpp b/packages/host/cpp/RuntimeNodeApiAsync.hpp index f0108e6d..be20128c 100644 --- a/packages/host/cpp/RuntimeNodeApiAsync.hpp +++ b/packages/host/cpp/RuntimeNodeApiAsync.hpp @@ -1,26 +1,24 @@ #pragma once +#include "node_api.h" #include #include -#include "node_api.h" -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { void setCallInvoker( - napi_env env, const std::shared_ptr& invoker); + napi_env env, const std::shared_ptr &invoker); -napi_status napi_create_async_work(napi_env env, - napi_value async_resource, - napi_value async_resource_name, - napi_async_execute_callback execute, - napi_async_complete_callback complete, - void* data, - napi_async_work* result); +napi_status napi_create_async_work(napi_env env, napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void *data, napi_async_work *result); napi_status napi_queue_async_work(node_api_basic_env env, napi_async_work work); -napi_status napi_delete_async_work( - node_api_basic_env env, napi_async_work work); +napi_status napi_delete_async_work(node_api_basic_env env, + napi_async_work work); -napi_status napi_cancel_async_work( - node_api_basic_env env, napi_async_work work); -} // namespace callstack::nodeapihost +napi_status napi_cancel_async_work(node_api_basic_env env, + napi_async_work work); +} // namespace callstack::react_native_node_api diff --git a/packages/host/cpp/WeakNodeApiInjector.hpp b/packages/host/cpp/WeakNodeApiInjector.hpp index 1b0a718d..52f9fd60 100644 --- a/packages/host/cpp/WeakNodeApiInjector.hpp +++ b/packages/host/cpp/WeakNodeApiInjector.hpp @@ -1,5 +1,5 @@ #include -namespace callstack::nodeapihost { +namespace callstack::react_native_node_api { void injectIntoWeakNodeApi(); } diff --git a/packages/host/package.json b/packages/host/package.json index 77b51b50..8bc1444e 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -42,11 +42,10 @@ ], "scripts": { "build": "tsc --build", - "generate-weak-node-api-injector": "node scripts/generate-weak-node-api-injector.mts", + "injector:generate": "node scripts/generate-injector.mts", "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", "test:gradle": "ENABLE_GRADLE_TESTS=true node --run test", - "bootstrap": "node --run generate-weak-node-api-injector", - "prerelease": "node --run generate-weak-node-api-injector" + "bootstrap": "node --run injector:generate" }, "keywords": [ "node-api", diff --git a/packages/host/scripts/generate-weak-node-api-injector.mts b/packages/host/scripts/generate-injector.mts similarity index 93% rename from packages/host/scripts/generate-weak-node-api-injector.mts rename to packages/host/scripts/generate-injector.mts index 71acfe86..bfd6a150 100644 --- a/packages/host/scripts/generate-weak-node-api-injector.mts +++ b/packages/host/scripts/generate-injector.mts @@ -43,7 +43,7 @@ export function generateSource(functions: FunctionDecl[]) { #error "WEAK_NODE_API_LIBRARY_NAME cannot be defined for this platform" #endif - namespace callstack::nodeapihost { + namespace callstack::react_native_node_api { void injectIntoWeakNodeApi() { void *module = dlopen(WEAK_NODE_API_LIBRARY_NAME, RTLD_NOW | RTLD_LOCAL); @@ -59,8 +59,8 @@ export function generateSource(functions: FunctionDecl[]) { abort(); } - log_debug("Injecting WeakNodeApiHost"); - inject_weak_node_api_host(WeakNodeApiHost { + log_debug("Injecting NodeApiHost"); + inject_weak_node_api_host(NodeApiHost { ${functions .filter( ({ kind, name }) => @@ -70,7 +70,7 @@ export function generateSource(functions: FunctionDecl[]) { .join("\n")} }); } - } // namespace callstack::nodeapihost + } // namespace callstack::react_native_node_api `; } diff --git a/packages/weak-node-api/.gitignore b/packages/weak-node-api/.gitignore index 652a5f16..83e62995 100644 --- a/packages/weak-node-api/.gitignore +++ b/packages/weak-node-api/.gitignore @@ -5,8 +5,7 @@ /build-tests/ /*.xcframework /*.android.node -/generated/weak_node_api.cpp -/generated/weak_node_api.hpp +/generated/ # Copied from node-api-headers by scripts/copy-node-api-headers.ts /include/ diff --git a/packages/weak-node-api/CMakeLists.txt b/packages/weak-node-api/CMakeLists.txt index d61630f2..90525434 100644 --- a/packages/weak-node-api/CMakeLists.txt +++ b/packages/weak-node-api/CMakeLists.txt @@ -16,6 +16,7 @@ target_sources(${PROJECT_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS ${GENERATED_SOURCE_DIR} ${INCLUDE_DIR} FILES ${GENERATED_SOURCE_DIR}/weak_node_api.hpp + ${GENERATED_SOURCE_DIR}/NodeApiHost.hpp ${INCLUDE_DIR}/js_native_api_types.h ${INCLUDE_DIR}/js_native_api.h ${INCLUDE_DIR}/node_api_types.h diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json index 84a510f3..2d2d0f5c 100644 --- a/packages/weak-node-api/package.json +++ b/packages/weak-node-api/package.json @@ -26,18 +26,17 @@ "scripts": { "build": "tsc --build", "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", - "generate-weak-node-api": "tsx scripts/generate-weak-node-api.ts", - "prepare-weak-node-api": "node --run copy-node-api-headers && node --run generate-weak-node-api", - "build-weak-node-api": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension", - "build-weak-node-api:android": "node --run build-weak-node-api -- --android", - "build-weak-node-api:apple": "node --run build-weak-node-api -- --apple", - "build-weak-node-api:all": "node --run build-weak-node-api -- --android --apple", + "generate": "tsx scripts/generate.ts", + "prebuild:prepare": "node --run copy-node-api-headers && node --run generate", + "prebuild:build": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension", + "prebuild:build:android": "node --run prebuild:build -- --android", + "prebuild:build:apple": "node --run prebuild:build -- --apple", + "prebuild:build:all": "node --run prebuild:build -- --android --apple", "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", "test:configure": "cmake -S . -B build-tests -DBUILD_TESTS=ON", "test:build": "cmake --build build-tests", "test:run": "ctest --test-dir build-tests --output-on-failure", - "bootstrap": "node --run prepare-weak-node-api && node --run build-weak-node-api", - "prerelease": "node --run prepare-weak-node-api && node --run build-weak-node-api:all" + "bootstrap": "node --run prebuild:prepare && node --run prebuild:build" }, "keywords": [ "react-native", diff --git a/packages/weak-node-api/scripts/generate-weak-node-api.ts b/packages/weak-node-api/scripts/generate.ts similarity index 54% rename from packages/weak-node-api/scripts/generate-weak-node-api.ts rename to packages/weak-node-api/scripts/generate.ts index 7b99d472..fc21e485 100644 --- a/packages/weak-node-api/scripts/generate-weak-node-api.ts +++ b/packages/weak-node-api/scripts/generate.ts @@ -9,6 +9,7 @@ import { } from "../src/node-api-functions.js"; import * as weakNodeApiGenerator from "./generators/weak-node-api.js"; +import * as hostGenerator from "./generators/NodeApiHost.js"; export const OUTPUT_PATH = path.join(import.meta.dirname, "../generated"); @@ -16,17 +17,32 @@ type GenerateFileOptions = { functions: FunctionDecl[]; fileName: string; generator: (functions: FunctionDecl[]) => string; + headingComment?: string; }; async function generateFile({ functions, fileName, generator, + headingComment = "", }: GenerateFileOptions) { const generated = generator(functions); - const output = `// This file is generated - don't edit it directly\n\n${generated}`; + const output = ` + /** + * @file ${fileName} + * ${headingComment + .trim() + .split("\n") + .map((l) => l.trim()) + .join("\n* ")} + * + * @note This file is generated - don't edit it directly + */ + + ${generated} + `; const outputPath = path.join(OUTPUT_PATH, fileName); - await fs.promises.writeFile(outputPath, output, "utf-8"); + await fs.promises.writeFile(outputPath, output.trim(), "utf-8"); const { status, stderr = "No error output" } = cp.spawnSync( "clang-format", ["-i", outputPath], @@ -41,15 +57,35 @@ async function run() { await fs.promises.mkdir(OUTPUT_PATH, { recursive: true }); const functions = getNodeApiFunctions(); + await generateFile({ + functions, + fileName: "NodeApiHost.hpp", + generator: hostGenerator.generateHeader, + headingComment: ` + @brief NodeApiHost struct. + + This header provides a struct of Node-API functions implemented by a host to inject its implementations. + `, + }); await generateFile({ functions, fileName: "weak_node_api.hpp", generator: weakNodeApiGenerator.generateHeader, + headingComment: ` + @brief Weak Node-API host injection interface. + + This header provides the struct and injection function for deferring Node-API function calls from addons into a Node-API host. + `, }); await generateFile({ functions, fileName: "weak_node_api.cpp", generator: weakNodeApiGenerator.generateSource, + headingComment: ` + @brief Weak Node-API host injection implementation. + + Provides the implementation for deferring Node-API function calls from addons into a Node-API host. + `, }); } diff --git a/packages/weak-node-api/scripts/generators/NodeApiHost.ts b/packages/weak-node-api/scripts/generators/NodeApiHost.ts new file mode 100644 index 00000000..c273cfe7 --- /dev/null +++ b/packages/weak-node-api/scripts/generators/NodeApiHost.ts @@ -0,0 +1,32 @@ +import type { FunctionDecl } from "../../src/node-api-functions.js"; + +export function generateFunctionDecl({ + returnType, + name, + argumentTypes, +}: FunctionDecl) { + return `${returnType} (*${name})(${argumentTypes.join(", ")});`; +} + +export function generateHeader(functions: FunctionDecl[]) { + return ` + #pragma once + + #include + + // Ideally we would have just used NAPI_NO_RETURN, but + // __declspec(noreturn) (when building with Microsoft Visual C++) cannot be used on members of a struct + // TODO: If we targeted C++23 we could use std::unreachable() + + #if defined(__GNUC__) + #define WEAK_NODE_API_UNREACHABLE __builtin_unreachable() + #else + #define WEAK_NODE_API_UNREACHABLE __assume(0) + #endif + + // Generate the struct of function pointers + struct NodeApiHost { + ${functions.map(generateFunctionDecl).join("\n")} + }; + `; +} diff --git a/packages/weak-node-api/scripts/generators/weak-node-api.ts b/packages/weak-node-api/scripts/generators/weak-node-api.ts index 964b3d73..62a1a026 100644 --- a/packages/weak-node-api/scripts/generators/weak-node-api.ts +++ b/packages/weak-node-api/scripts/generators/weak-node-api.ts @@ -1,41 +1,18 @@ import type { FunctionDecl } from "../../src/node-api-functions.js"; import { generateFunction } from "./shared.js"; -export function generateFunctionDecl({ - returnType, - name, - argumentTypes, -}: FunctionDecl) { - return `${returnType} (*${name})(${argumentTypes.join(", ")});`; -} - -/** - * Generates source code for a version script for the given Node API version. - */ -export function generateHeader(functions: FunctionDecl[]) { +export function generateHeader() { return ` #pragma once - #include // Node-API + #include #include // fprintf() #include // abort() - - // Ideally we would have just used NAPI_NO_RETURN, but - // __declspec(noreturn) (when building with Microsoft Visual C++) cannot be used on members of a struct - // TODO: If we targeted C++23 we could use std::unreachable() - - #if defined(__GNUC__) - #define WEAK_NODE_API_UNREACHABLE __builtin_unreachable() - #else - #define WEAK_NODE_API_UNREACHABLE __assume(0) - #endif - // Generate the struct of function pointers - struct WeakNodeApiHost { - ${functions.map(generateFunctionDecl).join("\n")} - }; - typedef void(*InjectHostFunction)(const WeakNodeApiHost&); - extern "C" void inject_weak_node_api_host(const WeakNodeApiHost& host); + #include "NodeApiHost.hpp" + + typedef void(*InjectHostFunction)(const NodeApiHost&); + extern "C" void inject_weak_node_api_host(const NodeApiHost& host); `; } @@ -56,15 +33,19 @@ function generateFunctionImpl(fn: FunctionDecl) { }); } -/** - * Generates source code for a version script for the given Node API version. - */ export function generateSource(functions: FunctionDecl[]) { return ` #include "weak_node_api.hpp" - WeakNodeApiHost g_host; - void inject_weak_node_api_host(const WeakNodeApiHost& host) { + /** + * @brief Global instance of the injected Node-API host. + * + * This variable holds the function table for Node-API calls. + * It is set via inject_weak_node_api_host() before any Node-API function is dispatched. + * All Node-API calls are routed through this host. + */ + NodeApiHost g_host; + void inject_weak_node_api_host(const NodeApiHost& host) { g_host = host; }; diff --git a/packages/weak-node-api/src/node-api-functions.ts b/packages/weak-node-api/src/node-api-functions.ts index 4290a471..f92bd956 100644 --- a/packages/weak-node-api/src/node-api-functions.ts +++ b/packages/weak-node-api/src/node-api-functions.ts @@ -49,10 +49,6 @@ const clangAstDump = z.object({ ), }); -/** - * Generates source code for a version script for the given Node API version. - * @param version - */ export function getNodeApiHeaderAST(version: NodeApiVersion) { const output = cp.execFileSync( "clang", diff --git a/packages/weak-node-api/tests/test_inject.cpp b/packages/weak-node-api/tests/test_inject.cpp index e2101c8c..5b35f15e 100644 --- a/packages/weak-node-api/tests/test_inject.cpp +++ b/packages/weak-node-api/tests/test_inject.cpp @@ -3,7 +3,7 @@ TEST_CASE("inject_weak_node_api_host") { SECTION("is callable") { - WeakNodeApiHost host{}; + NodeApiHost host{}; inject_weak_node_api_host(host); } @@ -14,7 +14,7 @@ TEST_CASE("inject_weak_node_api_host") { called = true; return napi_status::napi_ok; }; - WeakNodeApiHost host{.napi_create_object = my_create_object}; + NodeApiHost host{.napi_create_object = my_create_object}; inject_weak_node_api_host(host); napi_value result; From 323ef0e6e2e598dbdb641efaea2d301de427427f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 17:13:05 +0100 Subject: [PATCH 74/82] Version Packages (#334) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/big-plums-write.md | 5 ----- .changeset/evil-pens-shop.md | 5 ----- .changeset/slimy-parts-admire.md | 5 ----- packages/cli-utils/CHANGELOG.md | 6 ++++++ packages/cli-utils/package.json | 2 +- packages/cmake-rn/CHANGELOG.md | 10 ++++++++++ packages/cmake-rn/package.json | 8 ++++---- packages/ferric/CHANGELOG.md | 11 +++++++++++ packages/ferric/package.json | 8 ++++---- packages/gyp-to-cmake/CHANGELOG.md | 7 +++++++ packages/gyp-to-cmake/package.json | 4 ++-- packages/host/CHANGELOG.md | 9 +++++++++ packages/host/package.json | 6 +++--- packages/node-tests/package.json | 2 +- packages/weak-node-api/CHANGELOG.md | 6 ++++++ packages/weak-node-api/package.json | 2 +- 16 files changed, 65 insertions(+), 31 deletions(-) delete mode 100644 .changeset/big-plums-write.md delete mode 100644 .changeset/evil-pens-shop.md delete mode 100644 .changeset/slimy-parts-admire.md diff --git a/.changeset/big-plums-write.md b/.changeset/big-plums-write.md deleted file mode 100644 index 45bc6c6e..00000000 --- a/.changeset/big-plums-write.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"ferric-cli": patch ---- - -Add --verbose, --concurrency, --clean options diff --git a/.changeset/evil-pens-shop.md b/.changeset/evil-pens-shop.md deleted file mode 100644 index 63ce2e2a..00000000 --- a/.changeset/evil-pens-shop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@react-native-node-api/cli-utils": patch ---- - -Add re-export of "p-limit" diff --git a/.changeset/slimy-parts-admire.md b/.changeset/slimy-parts-admire.md deleted file mode 100644 index de6b0ed2..00000000 --- a/.changeset/slimy-parts-admire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"weak-node-api": minor ---- - -Renamed WeakNodeApiHost to NodeApiHost diff --git a/packages/cli-utils/CHANGELOG.md b/packages/cli-utils/CHANGELOG.md index 1f135503..e0e082be 100644 --- a/packages/cli-utils/CHANGELOG.md +++ b/packages/cli-utils/CHANGELOG.md @@ -1,5 +1,11 @@ # @react-native-node-api/cli-utils +## 0.1.3 + +### Patch Changes + +- 441dcc4: Add re-export of "p-limit" + ## 0.1.2 ### Patch Changes diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index 73da1def..9fc60fd3 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -1,6 +1,6 @@ { "name": "@react-native-node-api/cli-utils", - "version": "0.1.2", + "version": "0.1.3", "description": "Useful utilities for the CLIs in the React Native Node API mono-repo", "type": "module", "files": [ diff --git a/packages/cmake-rn/CHANGELOG.md b/packages/cmake-rn/CHANGELOG.md index 0abb2ded..45bbded4 100644 --- a/packages/cmake-rn/CHANGELOG.md +++ b/packages/cmake-rn/CHANGELOG.md @@ -1,5 +1,15 @@ # cmake-rn +## 0.6.2 + +### Patch Changes + +- Updated dependencies [441dcc4] +- Updated dependencies [3d2e03e] + - @react-native-node-api/cli-utils@0.1.3 + - weak-node-api@0.1.0 + - react-native-node-api@1.0.0 + ## 0.6.1 ### Patch Changes diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index 8441285d..3443d3aa 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -1,6 +1,6 @@ { "name": "cmake-rn", - "version": "0.6.1", + "version": "0.6.2", "description": "Build React Native Node API modules with CMake", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -24,11 +24,11 @@ "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" }, "dependencies": { - "@react-native-node-api/cli-utils": "0.1.2", + "@react-native-node-api/cli-utils": "0.1.3", "cmake-file-api": "0.1.1", - "react-native-node-api": "0.7.1", + "react-native-node-api": "1.0.0", "zod": "^4.1.11", - "weak-node-api": "0.0.3" + "weak-node-api": "0.1.0" }, "peerDependencies": { "node-addon-api": "^8.3.1", diff --git a/packages/ferric/CHANGELOG.md b/packages/ferric/CHANGELOG.md index cd4fc02c..ad9440e4 100644 --- a/packages/ferric/CHANGELOG.md +++ b/packages/ferric/CHANGELOG.md @@ -1,5 +1,16 @@ # ferric-cli +## 0.3.10 + +### Patch Changes + +- 441dcc4: Add --verbose, --concurrency, --clean options +- Updated dependencies [441dcc4] +- Updated dependencies [3d2e03e] + - @react-native-node-api/cli-utils@0.1.3 + - weak-node-api@0.1.0 + - react-native-node-api@1.0.0 + ## 0.3.9 ### Patch Changes diff --git a/packages/ferric/package.json b/packages/ferric/package.json index 30dd641f..dc9e9cfb 100644 --- a/packages/ferric/package.json +++ b/packages/ferric/package.json @@ -1,6 +1,6 @@ { "name": "ferric-cli", - "version": "0.3.9", + "version": "0.3.10", "description": "Rust Node-API Modules for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -17,8 +17,8 @@ }, "dependencies": { "@napi-rs/cli": "~3.0.3", - "@react-native-node-api/cli-utils": "0.1.2", - "react-native-node-api": "0.7.1", - "weak-node-api": "0.0.3" + "@react-native-node-api/cli-utils": "0.1.3", + "react-native-node-api": "1.0.0", + "weak-node-api": "0.1.0" } } diff --git a/packages/gyp-to-cmake/CHANGELOG.md b/packages/gyp-to-cmake/CHANGELOG.md index 8998b3ce..f937cb6c 100644 --- a/packages/gyp-to-cmake/CHANGELOG.md +++ b/packages/gyp-to-cmake/CHANGELOG.md @@ -1,5 +1,12 @@ # gyp-to-cmake +## 0.5.2 + +### Patch Changes + +- Updated dependencies [441dcc4] + - @react-native-node-api/cli-utils@0.1.3 + ## 0.5.1 ### Patch Changes diff --git a/packages/gyp-to-cmake/package.json b/packages/gyp-to-cmake/package.json index da644783..9f2a6911 100644 --- a/packages/gyp-to-cmake/package.json +++ b/packages/gyp-to-cmake/package.json @@ -1,6 +1,6 @@ { "name": "gyp-to-cmake", - "version": "0.5.1", + "version": "0.5.2", "description": "Convert binding.gyp files to CMakeLists.txt", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -22,7 +22,7 @@ "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" }, "dependencies": { - "@react-native-node-api/cli-utils": "0.1.2", + "@react-native-node-api/cli-utils": "0.1.3", "gyp-parser": "^1.0.4", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1" diff --git a/packages/host/CHANGELOG.md b/packages/host/CHANGELOG.md index 9fe8fb94..ca28ce20 100644 --- a/packages/host/CHANGELOG.md +++ b/packages/host/CHANGELOG.md @@ -1,5 +1,14 @@ # react-native-node-api +## 1.0.0 + +### Patch Changes + +- Updated dependencies [441dcc4] +- Updated dependencies [3d2e03e] + - @react-native-node-api/cli-utils@0.1.3 + - weak-node-api@0.1.0 + ## 0.7.1 ### Patch Changes diff --git a/packages/host/package.json b/packages/host/package.json index 8bc1444e..f8b87247 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -1,6 +1,6 @@ { "name": "react-native-node-api", - "version": "0.7.1", + "version": "1.0.0", "description": "Node-API for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -67,7 +67,7 @@ "license": "MIT", "dependencies": { "@expo/plist": "^0.4.7", - "@react-native-node-api/cli-utils": "0.1.2", + "@react-native-node-api/cli-utils": "0.1.3", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1", "zod": "^4.1.11" @@ -80,6 +80,6 @@ "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.3" + "weak-node-api": "0.1.0" } } diff --git a/packages/node-tests/package.json b/packages/node-tests/package.json index 2c0a9c6c..a0541b32 100644 --- a/packages/node-tests/package.json +++ b/packages/node-tests/package.json @@ -28,7 +28,7 @@ "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", - "react-native-node-api": "^0.7.0", + "react-native-node-api": "^1.0.0", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } diff --git a/packages/weak-node-api/CHANGELOG.md b/packages/weak-node-api/CHANGELOG.md index 182fbff9..c9a6843f 100644 --- a/packages/weak-node-api/CHANGELOG.md +++ b/packages/weak-node-api/CHANGELOG.md @@ -1,5 +1,11 @@ # weak-node-api +## 0.1.0 + +### Minor Changes + +- 3d2e03e: Renamed WeakNodeApiHost to NodeApiHost + ## 0.0.3 ### Patch Changes diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json index 2d2d0f5c..e347ce11 100644 --- a/packages/weak-node-api/package.json +++ b/packages/weak-node-api/package.json @@ -1,6 +1,6 @@ { "name": "weak-node-api", - "version": "0.0.3", + "version": "0.1.0", "description": "A linkable and runtime-injectable Node-API", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { From 1dee80fd5629f95cf948ad29dffb043a69e199f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 5 Jan 2026 23:22:28 +0100 Subject: [PATCH 75/82] Add back prerelease script (#339) * Add back the prerelease script * Update lock after release * Run publint and limit files in ferric-cli package * Add changeset --- .changeset/rare-fans-take.md | 15 ++++++++++++++ package-lock.json | 32 ++++++++++++++--------------- package.json | 1 + packages/ferric/package.json | 4 ++++ packages/host/package.json | 3 ++- packages/weak-node-api/package.json | 3 ++- 6 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 .changeset/rare-fans-take.md diff --git a/.changeset/rare-fans-take.md b/.changeset/rare-fans-take.md new file mode 100644 index 00000000..4903dd3b --- /dev/null +++ b/.changeset/rare-fans-take.md @@ -0,0 +1,15 @@ +--- +"ferric-cli": patch +"@react-native-node-api/test-app": patch +"@react-native-node-api/cli-utils": patch +"cmake-file-api": patch +"cmake-rn": patch +"@react-native-node-api/ferric-example": patch +"gyp-to-cmake": patch +"react-native-node-api": patch +"@react-native-node-api/node-addon-examples": patch +"@react-native-node-api/node-tests": patch +"weak-node-api": patch +--- + +Fix missing build artifacts 🙈 diff --git a/package-lock.json b/package-lock.json index 3bd68dec..c7a61523 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15089,7 +15089,7 @@ }, "packages/cli-utils": { "name": "@react-native-node-api/cli-utils", - "version": "0.1.2", + "version": "0.1.3", "dependencies": { "@commander-js/extra-typings": "^14.0.0", "bufout": "^0.3.2", @@ -15322,12 +15322,12 @@ } }, "packages/cmake-rn": { - "version": "0.6.1", + "version": "0.6.2", "dependencies": { - "@react-native-node-api/cli-utils": "0.1.2", + "@react-native-node-api/cli-utils": "0.1.3", "cmake-file-api": "0.1.1", - "react-native-node-api": "0.7.1", - "weak-node-api": "0.0.3", + "react-native-node-api": "1.0.0", + "weak-node-api": "0.1.0", "zod": "^4.1.11" }, "bin": { @@ -15340,12 +15340,12 @@ }, "packages/ferric": { "name": "ferric-cli", - "version": "0.3.9", + "version": "0.3.10", "dependencies": { "@napi-rs/cli": "~3.0.3", - "@react-native-node-api/cli-utils": "0.1.2", - "react-native-node-api": "0.7.1", - "weak-node-api": "0.0.3" + "@react-native-node-api/cli-utils": "0.1.3", + "react-native-node-api": "1.0.0", + "weak-node-api": "0.1.0" }, "bin": { "ferric": "bin/ferric.js" @@ -15359,9 +15359,9 @@ } }, "packages/gyp-to-cmake": { - "version": "0.5.1", + "version": "0.5.2", "dependencies": { - "@react-native-node-api/cli-utils": "0.1.2", + "@react-native-node-api/cli-utils": "0.1.3", "gyp-parser": "^1.0.4", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1" @@ -15372,11 +15372,11 @@ }, "packages/host": { "name": "react-native-node-api", - "version": "0.7.1", + "version": "1.0.0", "license": "MIT", "dependencies": { "@expo/plist": "^0.4.7", - "@react-native-node-api/cli-utils": "0.1.2", + "@react-native-node-api/cli-utils": "0.1.3", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1", "zod": "^4.1.11" @@ -15392,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.3" + "weak-node-api": "0.1.0" } }, "packages/node-addon-examples": { @@ -15414,13 +15414,13 @@ "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", - "react-native-node-api": "^0.7.0", + "react-native-node-api": "^1.0.0", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } }, "packages/weak-node-api": { - "version": "0.0.3", + "version": "0.1.0", "license": "MIT", "dependencies": { "node-api-headers": "^1.5.0" diff --git a/package.json b/package.json index 72dab66e..96d40503 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "bootstrap": "node --run build && npm run bootstrap --workspaces --if-present", "changeset": "changeset", "release": "changeset publish", + "prerelease": "node --run build && npm run prerelease --workspaces --if-present && node --run publint", "init-macos-test-app": "node scripts/init-macos-test-app.ts" }, "author": { diff --git a/packages/ferric/package.json b/packages/ferric/package.json index dc9e9cfb..dcb0e80d 100644 --- a/packages/ferric/package.json +++ b/packages/ferric/package.json @@ -9,6 +9,10 @@ "directory": "packages/ferric" }, "type": "module", + "files": [ + "bin", + "dist" + ], "bin": { "ferric": "./bin/ferric.js" }, diff --git a/packages/host/package.json b/packages/host/package.json index f8b87247..2f72523a 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -45,7 +45,8 @@ "injector:generate": "node scripts/generate-injector.mts", "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", "test:gradle": "ENABLE_GRADLE_TESTS=true node --run test", - "bootstrap": "node --run injector:generate" + "bootstrap": "node --run injector:generate", + "prerelease": "node --run injector:generate" }, "keywords": [ "node-api", diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json index e347ce11..f6271cfb 100644 --- a/packages/weak-node-api/package.json +++ b/packages/weak-node-api/package.json @@ -36,7 +36,8 @@ "test:configure": "cmake -S . -B build-tests -DBUILD_TESTS=ON", "test:build": "cmake --build build-tests", "test:run": "ctest --test-dir build-tests --output-on-failure", - "bootstrap": "node --run prebuild:prepare && node --run prebuild:build" + "bootstrap": "node --run prebuild:prepare && node --run prebuild:build", + "prerelease": "node --run prebuild:prepare && node --run prebuild:build:all" }, "keywords": [ "react-native", From e56a7448770f82e28273ff7838fff9a345607600 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 00:06:29 +0100 Subject: [PATCH 76/82] Version Packages (#340) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/rare-fans-take.md | 15 --------------- apps/test-app/CHANGELOG.md | 12 ++++++++++++ apps/test-app/package.json | 2 +- packages/cli-utils/CHANGELOG.md | 6 ++++++ packages/cli-utils/package.json | 2 +- packages/cmake-file-api/CHANGELOG.md | 6 ++++++ packages/cmake-file-api/package.json | 2 +- packages/cmake-rn/CHANGELOG.md | 11 +++++++++++ packages/cmake-rn/package.json | 10 +++++----- packages/ferric-example/CHANGELOG.md | 6 ++++++ packages/ferric-example/package.json | 2 +- packages/ferric/CHANGELOG.md | 10 ++++++++++ packages/ferric/package.json | 8 ++++---- packages/gyp-to-cmake/CHANGELOG.md | 8 ++++++++ packages/gyp-to-cmake/package.json | 4 ++-- packages/host/CHANGELOG.md | 9 +++++++++ packages/host/package.json | 6 +++--- packages/node-addon-examples/CHANGELOG.md | 7 +++++++ packages/node-addon-examples/package.json | 2 +- packages/node-tests/CHANGELOG.md | 7 +++++++ packages/node-tests/package.json | 4 ++-- packages/weak-node-api/CHANGELOG.md | 6 ++++++ packages/weak-node-api/package.json | 2 +- 23 files changed, 110 insertions(+), 37 deletions(-) delete mode 100644 .changeset/rare-fans-take.md create mode 100644 packages/node-addon-examples/CHANGELOG.md create mode 100644 packages/node-tests/CHANGELOG.md diff --git a/.changeset/rare-fans-take.md b/.changeset/rare-fans-take.md deleted file mode 100644 index 4903dd3b..00000000 --- a/.changeset/rare-fans-take.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"ferric-cli": patch -"@react-native-node-api/test-app": patch -"@react-native-node-api/cli-utils": patch -"cmake-file-api": patch -"cmake-rn": patch -"@react-native-node-api/ferric-example": patch -"gyp-to-cmake": patch -"react-native-node-api": patch -"@react-native-node-api/node-addon-examples": patch -"@react-native-node-api/node-tests": patch -"weak-node-api": patch ---- - -Fix missing build artifacts 🙈 diff --git a/apps/test-app/CHANGELOG.md b/apps/test-app/CHANGELOG.md index cf436b94..8e13a111 100644 --- a/apps/test-app/CHANGELOG.md +++ b/apps/test-app/CHANGELOG.md @@ -1,5 +1,17 @@ # react-native-node-api-test-app +## 0.2.2 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts 🙈 +- Updated dependencies [1dee80f] + - @react-native-node-api/ferric-example@0.1.2 + - react-native-node-api@1.0.1 + - @react-native-node-api/node-addon-examples@0.1.1 + - @react-native-node-api/node-tests@0.1.1 + - weak-node-api@0.1.1 + ## 0.2.1 ### Patch Changes diff --git a/apps/test-app/package.json b/apps/test-app/package.json index 9eb7048d..e2641812 100644 --- a/apps/test-app/package.json +++ b/apps/test-app/package.json @@ -2,7 +2,7 @@ "name": "@react-native-node-api/test-app", "private": true, "type": "commonjs", - "version": "0.2.1", + "version": "0.2.2", "scripts": { "metro": "react-native start --no-interactive", "android": "react-native run-android --no-packager --active-arch-only", diff --git a/packages/cli-utils/CHANGELOG.md b/packages/cli-utils/CHANGELOG.md index e0e082be..078eefd9 100644 --- a/packages/cli-utils/CHANGELOG.md +++ b/packages/cli-utils/CHANGELOG.md @@ -1,5 +1,11 @@ # @react-native-node-api/cli-utils +## 0.1.4 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts 🙈 + ## 0.1.3 ### Patch Changes diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index 9fc60fd3..25cbaeef 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -1,6 +1,6 @@ { "name": "@react-native-node-api/cli-utils", - "version": "0.1.3", + "version": "0.1.4", "description": "Useful utilities for the CLIs in the React Native Node API mono-repo", "type": "module", "files": [ diff --git a/packages/cmake-file-api/CHANGELOG.md b/packages/cmake-file-api/CHANGELOG.md index 484f35f2..ae76827b 100644 --- a/packages/cmake-file-api/CHANGELOG.md +++ b/packages/cmake-file-api/CHANGELOG.md @@ -1,5 +1,11 @@ # cmake-file-api +## 0.1.2 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts 🙈 + ## 0.1.1 ### Patch Changes diff --git a/packages/cmake-file-api/package.json b/packages/cmake-file-api/package.json index f1a3fcb8..7c4245c5 100644 --- a/packages/cmake-file-api/package.json +++ b/packages/cmake-file-api/package.json @@ -1,6 +1,6 @@ { "name": "cmake-file-api", - "version": "0.1.1", + "version": "0.1.2", "type": "module", "description": "TypeScript wrapper around the CMake File API", "homepage": "https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html", diff --git a/packages/cmake-rn/CHANGELOG.md b/packages/cmake-rn/CHANGELOG.md index 45bbded4..8748261e 100644 --- a/packages/cmake-rn/CHANGELOG.md +++ b/packages/cmake-rn/CHANGELOG.md @@ -1,5 +1,16 @@ # cmake-rn +## 0.6.3 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts 🙈 +- Updated dependencies [1dee80f] + - @react-native-node-api/cli-utils@0.1.4 + - cmake-file-api@0.1.2 + - react-native-node-api@1.0.1 + - weak-node-api@0.1.1 + ## 0.6.2 ### Patch Changes diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index 3443d3aa..b766bfc1 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -1,6 +1,6 @@ { "name": "cmake-rn", - "version": "0.6.2", + "version": "0.6.3", "description": "Build React Native Node API modules with CMake", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -24,11 +24,11 @@ "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" }, "dependencies": { - "@react-native-node-api/cli-utils": "0.1.3", - "cmake-file-api": "0.1.1", - "react-native-node-api": "1.0.0", + "@react-native-node-api/cli-utils": "0.1.4", + "cmake-file-api": "0.1.2", + "react-native-node-api": "1.0.1", "zod": "^4.1.11", - "weak-node-api": "0.1.0" + "weak-node-api": "0.1.1" }, "peerDependencies": { "node-addon-api": "^8.3.1", diff --git a/packages/ferric-example/CHANGELOG.md b/packages/ferric-example/CHANGELOG.md index 66f2007e..3818f6fd 100644 --- a/packages/ferric-example/CHANGELOG.md +++ b/packages/ferric-example/CHANGELOG.md @@ -1,5 +1,11 @@ # @react-native-node-api/ferric-example +## 0.1.2 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts 🙈 + ## 0.1.1 ### Patch Changes diff --git a/packages/ferric-example/package.json b/packages/ferric-example/package.json index 0695c049..af2d5ada 100644 --- a/packages/ferric-example/package.json +++ b/packages/ferric-example/package.json @@ -1,6 +1,6 @@ { "name": "@react-native-node-api/ferric-example", - "version": "0.1.1", + "version": "0.1.2", "private": true, "type": "commonjs", "homepage": "https://github.com/callstackincubator/react-native-node-api", diff --git a/packages/ferric/CHANGELOG.md b/packages/ferric/CHANGELOG.md index ad9440e4..1518b83c 100644 --- a/packages/ferric/CHANGELOG.md +++ b/packages/ferric/CHANGELOG.md @@ -1,5 +1,15 @@ # ferric-cli +## 0.3.11 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts 🙈 +- Updated dependencies [1dee80f] + - @react-native-node-api/cli-utils@0.1.4 + - react-native-node-api@1.0.1 + - weak-node-api@0.1.1 + ## 0.3.10 ### Patch Changes diff --git a/packages/ferric/package.json b/packages/ferric/package.json index dcb0e80d..1bf810df 100644 --- a/packages/ferric/package.json +++ b/packages/ferric/package.json @@ -1,6 +1,6 @@ { "name": "ferric-cli", - "version": "0.3.10", + "version": "0.3.11", "description": "Rust Node-API Modules for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -21,8 +21,8 @@ }, "dependencies": { "@napi-rs/cli": "~3.0.3", - "@react-native-node-api/cli-utils": "0.1.3", - "react-native-node-api": "1.0.0", - "weak-node-api": "0.1.0" + "@react-native-node-api/cli-utils": "0.1.4", + "react-native-node-api": "1.0.1", + "weak-node-api": "0.1.1" } } diff --git a/packages/gyp-to-cmake/CHANGELOG.md b/packages/gyp-to-cmake/CHANGELOG.md index f937cb6c..9aec77d2 100644 --- a/packages/gyp-to-cmake/CHANGELOG.md +++ b/packages/gyp-to-cmake/CHANGELOG.md @@ -1,5 +1,13 @@ # gyp-to-cmake +## 0.5.3 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts 🙈 +- Updated dependencies [1dee80f] + - @react-native-node-api/cli-utils@0.1.4 + ## 0.5.2 ### Patch Changes diff --git a/packages/gyp-to-cmake/package.json b/packages/gyp-to-cmake/package.json index 9f2a6911..2a1bde89 100644 --- a/packages/gyp-to-cmake/package.json +++ b/packages/gyp-to-cmake/package.json @@ -1,6 +1,6 @@ { "name": "gyp-to-cmake", - "version": "0.5.2", + "version": "0.5.3", "description": "Convert binding.gyp files to CMakeLists.txt", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -22,7 +22,7 @@ "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" }, "dependencies": { - "@react-native-node-api/cli-utils": "0.1.3", + "@react-native-node-api/cli-utils": "0.1.4", "gyp-parser": "^1.0.4", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1" diff --git a/packages/host/CHANGELOG.md b/packages/host/CHANGELOG.md index ca28ce20..13464f93 100644 --- a/packages/host/CHANGELOG.md +++ b/packages/host/CHANGELOG.md @@ -1,5 +1,14 @@ # react-native-node-api +## 1.0.1 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts 🙈 +- Updated dependencies [1dee80f] + - @react-native-node-api/cli-utils@0.1.4 + - weak-node-api@0.1.1 + ## 1.0.0 ### Patch Changes diff --git a/packages/host/package.json b/packages/host/package.json index 2f72523a..401ba6f1 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -1,6 +1,6 @@ { "name": "react-native-node-api", - "version": "1.0.0", + "version": "1.0.1", "description": "Node-API for React Native", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { @@ -68,7 +68,7 @@ "license": "MIT", "dependencies": { "@expo/plist": "^0.4.7", - "@react-native-node-api/cli-utils": "0.1.3", + "@react-native-node-api/cli-utils": "0.1.4", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1", "zod": "^4.1.11" @@ -81,6 +81,6 @@ "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.1.0" + "weak-node-api": "0.1.1" } } diff --git a/packages/node-addon-examples/CHANGELOG.md b/packages/node-addon-examples/CHANGELOG.md new file mode 100644 index 00000000..25a0bd9d --- /dev/null +++ b/packages/node-addon-examples/CHANGELOG.md @@ -0,0 +1,7 @@ +# @react-native-node-api/node-addon-examples + +## 0.1.1 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts 🙈 diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index 010d2f31..2b855a56 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -1,6 +1,6 @@ { "name": "@react-native-node-api/node-addon-examples", - "version": "0.1.0", + "version": "0.1.1", "type": "commonjs", "main": "dist/index.js", "files": [ diff --git a/packages/node-tests/CHANGELOG.md b/packages/node-tests/CHANGELOG.md new file mode 100644 index 00000000..4c05ad01 --- /dev/null +++ b/packages/node-tests/CHANGELOG.md @@ -0,0 +1,7 @@ +# @react-native-node-api/node-tests + +## 0.1.1 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts 🙈 diff --git a/packages/node-tests/package.json b/packages/node-tests/package.json index a0541b32..642432ee 100644 --- a/packages/node-tests/package.json +++ b/packages/node-tests/package.json @@ -1,6 +1,6 @@ { "name": "@react-native-node-api/node-tests", - "version": "0.1.0", + "version": "0.1.1", "description": "Harness for running the Node.js tests from https://github.com/nodejs/node/tree/main/test", "type": "commonjs", "main": "tests.generated.js", @@ -28,7 +28,7 @@ "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", - "react-native-node-api": "^1.0.0", + "react-native-node-api": "^1.0.1", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } diff --git a/packages/weak-node-api/CHANGELOG.md b/packages/weak-node-api/CHANGELOG.md index c9a6843f..90dec646 100644 --- a/packages/weak-node-api/CHANGELOG.md +++ b/packages/weak-node-api/CHANGELOG.md @@ -1,5 +1,11 @@ # weak-node-api +## 0.1.1 + +### Patch Changes + +- 1dee80f: Fix missing build artifacts 🙈 + ## 0.1.0 ### Minor Changes diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json index f6271cfb..fa557f28 100644 --- a/packages/weak-node-api/package.json +++ b/packages/weak-node-api/package.json @@ -1,6 +1,6 @@ { "name": "weak-node-api", - "version": "0.1.0", + "version": "0.1.1", "description": "A linkable and runtime-injectable Node-API", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { From 3690bd13b577c2af77ca3919a3883b3986b6cbe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 27 Jan 2026 11:21:43 +0100 Subject: [PATCH 77/82] Update release to use the "main" environment --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f071071f..f87aa362 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,7 @@ jobs: release: name: Release runs-on: macos-latest + environment: main steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -32,11 +33,10 @@ jobs: - run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android aarch64-apple-ios-sim - run: npm install - - name: Create Release Pull Request or Publish to npm + - name: Create Release Pull Request or Publish to NPM id: changesets uses: changesets/action@v1 with: publish: npm run release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} From 178f205211a59d038010bdf5e6406223f87d3158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 1 Feb 2026 12:44:24 +0100 Subject: [PATCH 78/82] Fix bundle identifiers (#343) --- .changeset/tall-tips-fail.md | 5 +++++ packages/gyp-to-cmake/src/transformer.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/tall-tips-fail.md diff --git a/.changeset/tall-tips-fail.md b/.changeset/tall-tips-fail.md new file mode 100644 index 00000000..bedcf4c7 --- /dev/null +++ b/.changeset/tall-tips-fail.md @@ -0,0 +1,5 @@ +--- +"gyp-to-cmake": patch +--- + +Fixed escaping bundle ids to no longer contain "\_" diff --git a/packages/gyp-to-cmake/src/transformer.ts b/packages/gyp-to-cmake/src/transformer.ts index a5660eba..4901df4f 100644 --- a/packages/gyp-to-cmake/src/transformer.ts +++ b/packages/gyp-to-cmake/src/transformer.ts @@ -27,8 +27,12 @@ function escapeSpaces(source: string) { return source.replace(/ /g, "\\ "); } -function escapeBundleIdentifier(identifier: string) { - return identifier.replaceAll("__", ".").replace(/[^A-Za-z0-9.-_]/g, "-"); +/** + * Escapes any input to match a CFBundleIdentifier + * See https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleidentifier + */ +export function escapeBundleIdentifier(input: string) { + return input.replaceAll("__", ".").replace(/[^A-Za-z0-9-.]/g, "-"); } /** From d43350ef68bb603d04098b9af8ba23ff7fbe9713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 1 Feb 2026 21:37:09 +0100 Subject: [PATCH 79/82] Pass `headerpad_max_install_names` to linker (#346) * Pass headerpad_max_install_names to linker * Add weak-node-api to macos test app --- .changeset/clear-peas-fly.md | 5 +++++ packages/cmake-rn/src/platforms/apple.ts | 2 ++ scripts/init-macos-test-app.ts | 4 ++++ 3 files changed, 11 insertions(+) create mode 100644 .changeset/clear-peas-fly.md diff --git a/.changeset/clear-peas-fly.md b/.changeset/clear-peas-fly.md new file mode 100644 index 00000000..32da5927 --- /dev/null +++ b/.changeset/clear-peas-fly.md @@ -0,0 +1,5 @@ +--- +"cmake-rn": patch +--- + +Fix auto-linking failures due to lack of padding when renaming install name of libraries, by passing headerpad_max_install_names argument to linker. diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index c61c1e81..cb2e7f1b 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -278,6 +278,8 @@ export const platform: Platform = { CMAKE_SYSTEM_NAME: CMAKE_SYSTEM_NAMES[triplet], CMAKE_OSX_SYSROOT: XCODE_SDK_NAMES[triplet], CMAKE_OSX_ARCHITECTURES: APPLE_ARCHITECTURES[triplet], + // Passing a linker flag to increase the header pad size to allow renaming the install name when linking it into the app. + CMAKE_SHARED_LINKER_FLAGS: "-Wl,-headerpad_max_install_names", }, { // Setting the output directories works around an issue with Xcode generator diff --git a/scripts/init-macos-test-app.ts b/scripts/init-macos-test-app.ts index 7d3abd1e..1f80b1a7 100644 --- a/scripts/init-macos-test-app.ts +++ b/scripts/init-macos-test-app.ts @@ -104,6 +104,10 @@ async function patchPackageJson() { APP_PATH, path.join(ROOT_PATH, "packages", "host"), ), + "weak-node-api": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "weak-node-api"), + ), ...Object.fromEntries( Object.entries(otherDependencies).filter(([name]) => transferredDependencies.has(name), From e11fcab56ffd57a0ba0c81ffcb713de84c2c427f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 1 Feb 2026 22:12:58 +0100 Subject: [PATCH 80/82] Use Node.js LTS Krypton (v24) (#345) * Upgrade setup-node action and node version on CI * Declare dev-engines in root package.json --- .github/workflows/check.yml | 28 +++++++------- .github/workflows/release.yml | 4 +- package-lock.json | 69 ++++++++++++++++++----------------- package.json | 10 +++++ 4 files changed, 62 insertions(+), 49 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 3676c622..e117763e 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -25,9 +25,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: lts/jod + node-version: lts/krypton # Set up JDK and Android SDK only because we need weak-node-api, to build ferric-example and to run the linting # TODO: Remove this once we have a way to run linting without building the native code - name: Set up JDK 17 @@ -64,9 +64,9 @@ jobs: name: Unit tests (${{ matrix.runner }}) steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: lts/jod + node-version: lts/krypton - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -93,9 +93,9 @@ jobs: name: Weak Node-API tests (${{ matrix.runner }}) steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: lts/jod + node-version: lts/krypton - run: npm ci - run: npm run build - name: Prepare weak-node-api @@ -112,9 +112,9 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: lts/jod + node-version: lts/krypton - name: Set up JDK 17 uses: actions/setup-java@v3 with: @@ -145,9 +145,9 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: lts/jod + node-version: lts/krypton - name: Set up JDK 17 uses: actions/setup-java@v3 with: @@ -176,9 +176,9 @@ jobs: runs-on: ubuntu-self-hosted steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: lts/jod + node-version: lts/krypton - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -256,9 +256,9 @@ jobs: runs-on: macos-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: lts/jod + node-version: lts/krypton - name: Set up JDK 17 uses: actions/setup-java@v3 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f87aa362..e096b011 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,9 +18,9 @@ jobs: environment: main steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: - node-version: lts/jod + node-version: lts/krypton - name: Set up JDK 17 uses: actions/setup-java@v3 with: diff --git a/package-lock.json b/package-lock.json index c7a61523..78b36270 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.1", + "version": "0.2.2", "dependencies": { "@babel/core": "^7.26.10", "@babel/preset-env": "^7.26.9", @@ -137,6 +137,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2283,16 +2284,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", @@ -4791,6 +4782,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.4.tgz", "integrity": "sha512-jOT8V1Ba5BdC79sKrRWDdMT5l1R+XNHTPR6CPWzUP2EcfAcvIHZWF0eAbmRcpOOP5gVIwnqNg0C4nvh6Abc3OA==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", @@ -5975,6 +5967,7 @@ "resolved": "https://registry.npmjs.org/@react-native/metro-config/-/metro-config-0.81.4.tgz", "integrity": "sha512-aEXhRMsz6yN5X63Zk+cdKByQ0j3dsKv+ETRP9lLARdZ82fBOCMuK6IfmZMwK3A/3bI7gSvt2MFPn3QHy3WnByw==", "license": "MIT", + "peer": true, "dependencies": { "@react-native/js-polyfills": "0.81.4", "@react-native/metro-babel-transformer": "0.81.4", @@ -6598,6 +6591,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz", "integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -6691,6 +6685,7 @@ "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.44.0", "@typescript-eslint/types": "8.44.0", @@ -7031,6 +7026,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7631,6 +7627,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -9048,6 +9045,7 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -12241,7 +12239,8 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.5.0.tgz", "integrity": "sha512-Yi/FgnN8IU/Cd6KeLxyHkylBUvDTsSScT0Tna2zTrz8klmc8qF2ppj6Q1LHsmOueJWhigQwR4cO2p0XBGW5IaQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/node-int64": { "version": "0.4.0", @@ -13091,6 +13090,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13137,6 +13137,7 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.4.tgz", "integrity": "sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ==", "license": "MIT", + "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.4", @@ -14574,6 +14575,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15089,7 +15091,7 @@ }, "packages/cli-utils": { "name": "@react-native-node-api/cli-utils", - "version": "0.1.3", + "version": "0.1.4", "dependencies": { "@commander-js/extra-typings": "^14.0.0", "bufout": "^0.3.2", @@ -15152,6 +15154,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", "license": "MIT", + "peer": true, "engines": { "node": ">=20" } @@ -15316,18 +15319,18 @@ } }, "packages/cmake-file-api": { - "version": "0.1.1", + "version": "0.1.2", "dependencies": { "zod": "^4.1.11" } }, "packages/cmake-rn": { - "version": "0.6.2", + "version": "0.6.3", "dependencies": { - "@react-native-node-api/cli-utils": "0.1.3", - "cmake-file-api": "0.1.1", - "react-native-node-api": "1.0.0", - "weak-node-api": "0.1.0", + "@react-native-node-api/cli-utils": "0.1.4", + "cmake-file-api": "0.1.2", + "react-native-node-api": "1.0.1", + "weak-node-api": "0.1.1", "zod": "^4.1.11" }, "bin": { @@ -15340,12 +15343,12 @@ }, "packages/ferric": { "name": "ferric-cli", - "version": "0.3.10", + "version": "0.3.11", "dependencies": { "@napi-rs/cli": "~3.0.3", - "@react-native-node-api/cli-utils": "0.1.3", - "react-native-node-api": "1.0.0", - "weak-node-api": "0.1.0" + "@react-native-node-api/cli-utils": "0.1.4", + "react-native-node-api": "1.0.1", + "weak-node-api": "0.1.1" }, "bin": { "ferric": "bin/ferric.js" @@ -15353,15 +15356,15 @@ }, "packages/ferric-example": { "name": "@react-native-node-api/ferric-example", - "version": "0.1.1", + "version": "0.1.2", "devDependencies": { "ferric-cli": "*" } }, "packages/gyp-to-cmake": { - "version": "0.5.2", + "version": "0.5.3", "dependencies": { - "@react-native-node-api/cli-utils": "0.1.3", + "@react-native-node-api/cli-utils": "0.1.4", "gyp-parser": "^1.0.4", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1" @@ -15372,11 +15375,11 @@ }, "packages/host": { "name": "react-native-node-api", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@expo/plist": "^0.4.7", - "@react-native-node-api/cli-utils": "0.1.3", + "@react-native-node-api/cli-utils": "0.1.4", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1", "zod": "^4.1.11" @@ -15392,12 +15395,12 @@ "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.1.0" + "weak-node-api": "0.1.1" } }, "packages/node-addon-examples": { "name": "@react-native-node-api/node-addon-examples", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "assert": "^2.1.0" }, @@ -15410,17 +15413,17 @@ }, "packages/node-tests": { "name": "@react-native-node-api/node-tests", - "version": "0.1.0", + "version": "0.1.1", "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", - "react-native-node-api": "^1.0.0", + "react-native-node-api": "^1.0.1", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } }, "packages/weak-node-api": { - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "dependencies": { "node-api-headers": "^1.5.0" diff --git a/package.json b/package.json index 96d40503..774e27eb 100644 --- a/package.json +++ b/package.json @@ -75,5 +75,15 @@ "tsx": "^4.20.6", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" + }, + "devEngines": { + "runtime": { + "name": "node", + "version": "^24.0.0" + }, + "packageManager": { + "name": "npm", + "version": "^11.0.0" + } } } From 57dfede20879bd1eef99bec3c7252cd3279d426a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 1 Feb 2026 23:46:39 +0100 Subject: [PATCH 81/82] Install `clang-format` via GHA action instead of via NPM (#344) * Install LLVM / clang-format on CI * Prevent NPM from installing "clang-format" --- .github/workflows/check.yml | 28 ++++++++++++++++++++++++++++ .github/workflows/release.yml | 4 ++++ package-lock.json | 26 +++++--------------------- package.json | 3 +++ 4 files changed, 40 insertions(+), 21 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e117763e..99fa9365 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -28,6 +28,10 @@ jobs: - uses: actions/setup-node@v6 with: node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 + with: + clang-format: true # Set up JDK and Android SDK only because we need weak-node-api, to build ferric-example and to run the linting # TODO: Remove this once we have a way to run linting without building the native code - name: Set up JDK 17 @@ -67,6 +71,10 @@ jobs: - uses: actions/setup-node@v6 with: node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 + with: + clang-format: true - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -96,6 +104,10 @@ jobs: - uses: actions/setup-node@v6 with: node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 + with: + clang-format: true - run: npm ci - run: npm run build - name: Prepare weak-node-api @@ -115,6 +127,10 @@ jobs: - uses: actions/setup-node@v6 with: node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 + with: + clang-format: true - name: Set up JDK 17 uses: actions/setup-java@v3 with: @@ -148,6 +164,10 @@ jobs: - uses: actions/setup-node@v6 with: node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 + with: + clang-format: true - name: Set up JDK 17 uses: actions/setup-java@v3 with: @@ -179,6 +199,10 @@ jobs: - uses: actions/setup-node@v6 with: node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 + with: + clang-format: true - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -259,6 +283,10 @@ jobs: - uses: actions/setup-node@v6 with: node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 + with: + clang-format: true - name: Set up JDK 17 uses: actions/setup-java@v3 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e096b011..476f316b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,10 @@ jobs: - uses: actions/setup-node@v6 with: node-version: lts/krypton + - name: Setup clang-format + uses: aminya/setup-cpp@v1 + with: + clang-format: true - name: Set up JDK 17 uses: actions/setup-java@v3 with: diff --git a/package-lock.json b/package-lock.json index 78b36270..1042ceff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7266,13 +7266,6 @@ "node": ">=4" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -7945,21 +7938,12 @@ } }, "node_modules/clang-format": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/clang-format/-/clang-format-1.8.0.tgz", - "integrity": "sha512-pK8gzfu55/lHzIpQ1givIbWfn3eXnU7SfxqIwVgnn5jEM6j4ZJYjpFqFs4iSBPNedzRMmfjYjuQhu657WAXHXw==", + "name": "dry-uninstall", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dry-uninstall/-/dry-uninstall-0.3.0.tgz", + "integrity": "sha512-b8h94RVpETWkVV59x62NsY++79bM7Si6Dxq7a4iVxRcJU3ZJJ4vaiC7wUZwM8WDK0ySRL+i+T/1SMAzbJLejYA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.3", - "glob": "^7.0.0", - "resolve": "^1.1.6" - }, - "bin": { - "check-clang-format": "bin/check-clang-format.js", - "clang-format": "index.js", - "git-clang-format": "bin/git-clang-format" - } + "license": "MIT" }, "node_modules/cli-cursor": { "version": "3.1.0", diff --git a/package.json b/package.json index 774e27eb..1cfefe97 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,9 @@ "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" }, + "overrides": { + "clang-format": "npm:dry-uninstall" + }, "devEngines": { "runtime": { "name": "node", From e87cdf461f3d5e6c9c5a76119b0929962aaab2ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 2 Feb 2026 20:10:47 +0100 Subject: [PATCH 82/82] Bump "node-addon-examples" to remove clang-format entirely (#347) --- package-lock.json | 15 +++------------ package.json | 3 --- packages/node-addon-examples/package.json | 2 +- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1042ceff..2552398b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7937,14 +7937,6 @@ "node": ">=8" } }, - "node_modules/clang-format": { - "name": "dry-uninstall", - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/dry-uninstall/-/dry-uninstall-0.3.0.tgz", - "integrity": "sha512-b8h94RVpETWkVV59x62NsY++79bM7Si6Dxq7a4iVxRcJU3ZJJ4vaiC7wUZwM8WDK0ySRL+i+T/1SMAzbJLejYA==", - "dev": true, - "license": "MIT" - }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -12183,12 +12175,11 @@ }, "node_modules/node-addon-examples": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/nodejs/node-addon-examples.git#4213d4c9d07996ae68629c67926251e117f8e52a", - "integrity": "sha512-4HfGZCsD1IwOx213KjizaXTgEFb7+sXi3dtlay1kERtHRnXMeTsMzruRrSQmm5f1QkVqK3sZPawc1+EW135s4Q==", + "resolved": "git+ssh://git@github.com/nodejs/node-addon-examples.git#4b7dd86a85644610e6de80154df9acac9329b509", + "integrity": "sha512-9bQgZbEIjN7umKgCT4z8781K8h+2EtAaMgXhByll8arccTjSTGyO8ipUngV14ieo58cf89ghM3Jh7quXL1vUwA==", "dev": true, "dependencies": { "chalk": "^5.4.1", - "clang-format": "^1.4.0", "cmake-js": "^7.1.1", "semver": "^7.1.3" } @@ -15391,7 +15382,7 @@ "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", - "node-addon-examples": "github:nodejs/node-addon-examples#4213d4c9d07996ae68629c67926251e117f8e52a", + "node-addon-examples": "github:nodejs/node-addon-examples#4b7dd86a85644610e6de80154df9acac9329b509", "read-pkg": "^9.0.1" } }, diff --git a/package.json b/package.json index 1cfefe97..774e27eb 100644 --- a/package.json +++ b/package.json @@ -76,9 +76,6 @@ "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" }, - "overrides": { - "clang-format": "npm:dry-uninstall" - }, "devEngines": { "runtime": { "name": "node", diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index 2b855a56..6063927b 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "cmake-rn": "*", - "node-addon-examples": "github:nodejs/node-addon-examples#4213d4c9d07996ae68629c67926251e117f8e52a", + "node-addon-examples": "github:nodejs/node-addon-examples#4b7dd86a85644610e6de80154df9acac9329b509", "gyp-to-cmake": "*", "read-pkg": "^9.0.1" },