├── apps └── test-app │ ├── .watchmanconfig │ ├── android │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── build.gradle │ ├── gradle.properties │ └── gradlew.bat │ ├── index.ts │ ├── babel.config.js │ ├── tsconfig.json │ ├── tsconfig.node-scripts.json │ ├── .gitignore │ ├── ios │ └── Podfile │ ├── Gemfile │ ├── app.json │ ├── react-native.config.js │ ├── metro.config.js │ ├── CHANGELOG.md │ ├── package.json │ └── App.tsx ├── packages ├── node-addon-examples │ ├── .gitignore │ ├── tests │ │ ├── async │ │ │ ├── binding.gyp │ │ │ ├── package.json │ │ │ ├── CMakeLists.txt │ │ │ └── addon.js │ │ └── buffers │ │ │ ├── binding.gyp │ │ │ ├── package.json │ │ │ ├── CMakeLists.txt │ │ │ └── addon.js │ ├── tsconfig.json │ ├── tsconfig.tests.json │ ├── tsconfig.node-scripts.json │ ├── scripts │ │ ├── build-examples.mts │ │ ├── cmake-projects.mts │ │ └── verify-prebuilds.mts │ ├── README.md │ ├── package.json │ └── src │ │ └── index.ts ├── node-tests │ ├── common.ts │ ├── .gitignore │ ├── tests.generated.d.ts │ ├── tsconfig.common.json │ ├── tsconfig.node-scripts.json │ ├── tsconfig.json │ ├── scripts │ │ ├── build-tests.mts │ │ ├── utils.mts │ │ ├── generate-entrypoint.mts │ │ └── copy-tests.mts │ ├── package.json │ └── rolldown.config.mts ├── ferric-example │ ├── build.rs │ ├── src │ │ └── lib.rs │ ├── CHANGELOG.md │ ├── .gitignore │ ├── Cargo.toml │ └── package.json ├── ferric │ ├── bin │ │ └── ferric.js │ ├── docs │ │ └── ferric-logo.png │ ├── src │ │ ├── run.ts │ │ ├── program.ts │ │ ├── rustup.ts │ │ ├── banner.ts │ │ └── napi-rs.ts │ ├── tsconfig.json │ ├── tsconfig.tests.json │ ├── README.md │ ├── package.json │ └── CHANGELOG.md ├── host │ ├── cpp │ │ ├── Versions.hpp │ │ ├── WeakNodeApiInjector.hpp │ │ ├── Logger.hpp │ │ ├── RuntimeNodeApiAsync.hpp │ │ ├── RuntimeNodeApi.hpp │ │ ├── CxxNodeApiHostModule.hpp │ │ ├── Logger.cpp │ │ └── AddonLoaders.hpp │ ├── babel-plugin.js │ ├── bin │ │ └── react-native-node-api.mjs │ ├── src │ │ ├── node │ │ │ ├── babel-plugin │ │ │ │ └── index.ts │ │ │ ├── cli │ │ │ │ ├── run.ts │ │ │ │ ├── options.ts │ │ │ │ ├── bin.test.ts │ │ │ │ └── android.ts │ │ │ ├── index.ts │ │ │ ├── prebuilds │ │ │ │ ├── apple.test.ts │ │ │ │ ├── triplets.ts │ │ │ │ └── android.ts │ │ │ ├── podspec.test.ts │ │ │ ├── test-utils.ts │ │ │ └── gradle.test.ts │ │ └── react-native │ │ │ └── index.ts │ ├── android │ │ ├── src │ │ │ └── main │ │ │ │ ├── AndroidManifestNew.xml │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── cpp │ │ │ │ └── OnLoad.cpp │ │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── callstack │ │ │ │ └── react_native_node_api │ │ │ │ └── NodeApiHostPackage.kt │ │ ├── gradle.properties │ │ └── CMakeLists.txt │ ├── react-native.config.js │ ├── tsconfig.json │ ├── tsconfig.node-tests.json │ ├── tsconfig.react-native.json │ ├── .gitignore │ ├── tsconfig.node-scripts.json │ ├── tsconfig.node.json │ ├── README.md │ ├── apple │ │ └── NodeApiHostModuleProvider.mm │ ├── scripts │ │ ├── patch-hermes.rb │ │ └── generate-injector.mts │ ├── react-native-node-api.podspec │ └── package.json ├── cmake-rn │ ├── bin │ │ └── cmake-rn.js │ ├── src │ │ ├── run.ts │ │ ├── helpers.ts │ │ ├── platforms.ts │ │ ├── platforms.test.ts │ │ ├── headers.ts │ │ ├── weak-node-api.ts │ │ └── platforms │ │ │ └── types.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── tsconfig.node-tests.json │ ├── package.json │ └── README.md ├── cli-utils │ ├── tsconfig.json │ ├── src │ │ ├── paths.ts │ │ ├── index.ts │ │ ├── errors.ts │ │ └── actions.ts │ ├── CHANGELOG.md │ └── package.json ├── gyp-to-cmake │ ├── bin │ │ └── gyp-to-cmake.js │ ├── src │ │ ├── gyp-parser.d.ts │ │ ├── run.ts │ │ ├── gyp.test.ts │ │ ├── gyp.ts │ │ └── transformer.test.ts │ ├── README.md │ ├── tsconfig.json │ ├── package.json │ └── CHANGELOG.md ├── cmake-file-api │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── tsconfig.tests.json │ ├── src │ │ ├── schemas.ts │ │ ├── schemas │ │ │ ├── objects │ │ │ │ ├── ConfigureLogV1.ts │ │ │ │ ├── CacheV2.ts │ │ │ │ ├── ToolchainsV1.ts │ │ │ │ ├── CmakeFilesV1.ts │ │ │ │ └── CodemodelV2.ts │ │ │ └── ReplyIndexV1.ts │ │ ├── index.ts │ │ └── query.ts │ ├── README.md │ └── package.json └── weak-node-api │ ├── src │ ├── index.ts │ ├── weak-node-api.ts │ └── restore-xcframework-symlinks.ts │ ├── types │ └── node-api-headers │ │ └── index.d.ts │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── tsconfig.node.json │ ├── .gitignore │ ├── tsconfig.node-scripts.json │ ├── scripts │ ├── copy-node-api-headers.ts │ ├── generators │ │ ├── shared.ts │ │ ├── NodeApiHost.ts │ │ └── weak-node-api.ts │ └── generate.ts │ ├── tests │ ├── test_inject.cpp │ └── CMakeLists.txt │ ├── weak-node-api.podspec │ ├── README.md │ ├── CMakeLists.txt │ ├── weak-node-api-config.cmake │ └── package.json ├── docs ├── logo.png ├── CLI.md ├── WEAK-NODE-API.md ├── AUTO-LINKING.md ├── ANDROID.md ├── PREBUILDS.md └── HOW-IT-WORKS.md ├── .changeset ├── slimy-parts-admire.md ├── big-plums-write.md ├── evil-pens-shop.md ├── config.json └── README.md ├── .gitignore ├── prettier.config.js ├── tsconfig.scripts.json ├── .prettierignore ├── configs ├── tsconfig.node-tests.json └── tsconfig.node.json ├── .vscode └── tasks.json ├── tsconfig.json ├── scripts ├── run-in-published.ts └── depcheck.ts ├── LICENSE.md ├── .github ├── workflows │ └── release.yml └── copilot-instructions.md ├── eslint.config.js └── package.json /apps/test-app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/node-addon-examples/.gitignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | build/ 3 | -------------------------------------------------------------------------------- /packages/node-tests/common.ts: -------------------------------------------------------------------------------- 1 | export const buildType = "Release"; 2 | -------------------------------------------------------------------------------- /packages/ferric-example/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | napi_build::setup(); 3 | } 4 | -------------------------------------------------------------------------------- /packages/ferric/bin/ferric.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "../dist/run.js"; 3 | -------------------------------------------------------------------------------- /packages/host/cpp/Versions.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define NAPI_VERSION 8 4 | -------------------------------------------------------------------------------- /packages/node-tests/.gitignore: -------------------------------------------------------------------------------- 1 | node/ 2 | tests/ 3 | 4 | tests.generated.js 5 | -------------------------------------------------------------------------------- /packages/cmake-rn/bin/cmake-rn.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "../dist/run.js"; 3 | -------------------------------------------------------------------------------- /packages/cli-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.node.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/gyp-to-cmake/bin/gyp-to-cmake.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "../dist/run.js"; 3 | -------------------------------------------------------------------------------- /packages/host/babel-plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./dist/node/babel-plugin/index.js"); 2 | -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callstackincubator/react-native-node-api/HEAD/docs/logo.png -------------------------------------------------------------------------------- /packages/cmake-file-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.node.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/cmake-rn/src/run.ts: -------------------------------------------------------------------------------- 1 | import { program } from "./cli.js"; 2 | program.parse(process.argv); 3 | -------------------------------------------------------------------------------- /packages/host/bin/react-native-node-api.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "../dist/node/cli/run.js"; 3 | -------------------------------------------------------------------------------- /packages/host/src/node/babel-plugin/index.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from "./plugin.js"; 2 | export = plugin; 3 | -------------------------------------------------------------------------------- /.changeset/slimy-parts-admire.md: -------------------------------------------------------------------------------- 1 | --- 2 | "weak-node-api": minor 3 | --- 4 | 5 | Renamed WeakNodeApiHost to NodeApiHost 6 | -------------------------------------------------------------------------------- /packages/weak-node-api/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./weak-node-api.js"; 2 | export * from "./node-api-functions.js"; 3 | -------------------------------------------------------------------------------- /.changeset/big-plums-write.md: -------------------------------------------------------------------------------- 1 | --- 2 | "ferric-cli": patch 3 | --- 4 | 5 | Add --verbose, --concurrency, --clean options 6 | -------------------------------------------------------------------------------- /.changeset/evil-pens-shop.md: -------------------------------------------------------------------------------- 1 | --- 2 | "@react-native-node-api/cli-utils": patch 3 | --- 4 | 5 | Add re-export of "p-limit" 6 | -------------------------------------------------------------------------------- /packages/ferric-example/src/lib.rs: -------------------------------------------------------------------------------- 1 | use napi_derive::napi; 2 | 3 | #[napi] 4 | pub fn sum(a: i32, b: i32) -> i32 { 5 | a + b 6 | } 7 | -------------------------------------------------------------------------------- /packages/gyp-to-cmake/src/gyp-parser.d.ts: -------------------------------------------------------------------------------- 1 | declare module "gyp-parser" { 2 | export function parse(input: string): unknown; 3 | } 4 | -------------------------------------------------------------------------------- /packages/gyp-to-cmake/src/run.ts: -------------------------------------------------------------------------------- 1 | import { program } from "./cli.js"; 2 | 3 | program.parseAsync(process.argv).catch(console.error); 4 | -------------------------------------------------------------------------------- /packages/host/src/node/cli/run.ts: -------------------------------------------------------------------------------- 1 | import { program } from "./program"; 2 | 3 | program.parseAsync(process.argv).catch(console.error); 4 | -------------------------------------------------------------------------------- /packages/host/android/src/main/AndroidManifestNew.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/cmake-file-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # cmake-file-api 2 | 3 | ## 0.1.1 4 | 5 | ### Patch Changes 6 | 7 | - 7ff2c2b: Fix minor package issues. 8 | -------------------------------------------------------------------------------- /packages/ferric/docs/ferric-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callstackincubator/react-native-node-api/HEAD/packages/ferric/docs/ferric-logo.png -------------------------------------------------------------------------------- /packages/host/react-native.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dependency: { 3 | platforms: { 4 | android: {}, 5 | }, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/weak-node-api/types/node-api-headers/index.d.ts: -------------------------------------------------------------------------------- 1 | module "node-api-headers" { 2 | declare const exported: unknown; 3 | export = exported; 4 | } 5 | -------------------------------------------------------------------------------- /docs/CLI.md: -------------------------------------------------------------------------------- 1 | # The `react-native-node-api` command-line interface (CLI) 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/ferric-example/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @react-native-node-api/ferric-example 2 | 3 | ## 0.1.1 4 | 5 | ### Patch Changes 6 | 7 | - a7cc35a: Updated napi packages. 8 | -------------------------------------------------------------------------------- /packages/host/cpp/WeakNodeApiInjector.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace callstack::react_native_node_api { 4 | void injectIntoWeakNodeApi(); 5 | } 6 | -------------------------------------------------------------------------------- /packages/gyp-to-cmake/README.md: -------------------------------------------------------------------------------- 1 | # `gyp-to-cmake` 2 | 3 | A tool to transform `binding.gyp` files into `CMakeLists.txt` files, intended for `cmake-js` or `cmake-rn` to build from. 4 | -------------------------------------------------------------------------------- /packages/node-addon-examples/tests/async/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "addon", 5 | "sources": [ "addon.c" ] 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/node-addon-examples/tests/buffers/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "addon", 5 | "sources": [ "addon.c" ] 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /apps/test-app/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callstackincubator/react-native-node-api/HEAD/apps/test-app/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /packages/cmake-file-api/tsconfig.tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../configs/tsconfig.node-tests.json", 3 | "references": [ 4 | { 5 | "path": "./tsconfig.json" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/host/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /apps/test-app/index.ts: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from "react-native"; 2 | import App from "./App"; 3 | import { name as appName } from "./app.json"; 4 | 5 | AppRegistry.registerComponent(appName, () => App); 6 | -------------------------------------------------------------------------------- /packages/ferric-example/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | 4 | /*.xcframework/ 5 | /*.apple.node/ 6 | /*.android.node/ 7 | 8 | # Generated files 9 | /ferric_example.d.ts 10 | /ferric_example.js 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules/ 4 | dist/ 5 | 6 | *.tsbuildinfo 7 | 8 | # Treading the MacOS app as ephemeral 9 | apps/macos-test-app 10 | 11 | # Cache used by the rust analyzer 12 | target/rust-analyzer/ 13 | -------------------------------------------------------------------------------- /packages/node-tests/tests.generated.d.ts: -------------------------------------------------------------------------------- 1 | // Despite the name, this file isn't generated. 2 | 3 | export interface TestSuite { 4 | [key: string]: TestSuite | (() => void); 5 | } 6 | 7 | export declare const suites: TestSuite; 8 | -------------------------------------------------------------------------------- /packages/host/android/gradle.properties: -------------------------------------------------------------------------------- 1 | NodeApiModules_kotlinVersion=2.0.21 2 | NodeApiModules_minSdkVersion=24 3 | NodeApiModules_targetSdkVersion=34 4 | NodeApiModules_compileSdkVersion=35 5 | NodeApiModules_ndkVersion=27.1.12297006 6 | -------------------------------------------------------------------------------- /packages/gyp-to-cmake/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "dist", 6 | "rootDir": "src" 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/cmake-rn/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true 4 | }, 5 | "files": [], 6 | "references": [ 7 | { "path": "./tsconfig.node.json" }, 8 | { "path": "./tsconfig.node-tests.json" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @satisfies {import("prettier").Config} 3 | * @see https://prettier.io/docs/en/configuration.html 4 | */ 5 | 6 | const config = { 7 | plugins: ["@prettier/plugin-oxc"], 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /packages/cli-utils/src/paths.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import path from "node:path"; 3 | 4 | export function prettyPath(p: string) { 5 | return chalk.dim( 6 | path.relative(process.cwd(), p) || chalk.italic("current directory"), 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /packages/weak-node-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true 4 | }, 5 | "files": [], 6 | "references": [ 7 | { "path": "./tsconfig.node.json" }, 8 | { "path": "./tsconfig.node-scripts.json" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/node-addon-examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true 4 | }, 5 | "files": [], 6 | "references": [ 7 | { "path": "./tsconfig.tests.json" }, 8 | { "path": "./tsconfig.node-scripts.json" } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./configs/tsconfig.node.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "emitDeclarationOnly": true, 6 | "declarationMap": false, 7 | "rootDir": "scripts" 8 | }, 9 | "include": ["scripts"] 10 | } 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts 2 | dist 3 | build 4 | Pods 5 | target 6 | .cxx 7 | 8 | # Ignore hermes 9 | packages/host/hermes 10 | packages/node-addon-examples/examples 11 | packages/node-tests/node 12 | packages/node-tests/tests 13 | packages/node-tests/*.generated.js 14 | -------------------------------------------------------------------------------- /apps/test-app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["module:@react-native/babel-preset"], 3 | // plugins: [['module:react-native-node-api/babel-plugin', { packageName: "strip", pathSuffix: "strip" }]], 4 | plugins: ["module:react-native-node-api/babel-plugin"], 5 | }; 6 | -------------------------------------------------------------------------------- /apps/test-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@react-native/typescript-config/tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["react-native", "mocha"] 5 | }, 6 | "files": ["App.tsx", "index.ts"], 7 | "references": [{ "path": "./tsconfig.node-scripts.json" }] 8 | } 9 | -------------------------------------------------------------------------------- /packages/weak-node-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # weak-node-api 2 | 3 | ## 0.0.3 4 | 5 | ### Patch Changes 6 | 7 | - 7ff2c2b: Fix minor package issues. 8 | - 7ff2c2b: Add missing "generated" directory 9 | 10 | ## 0.0.2 11 | 12 | ### Patch Changes 13 | 14 | - 60fae96: Initial release! 15 | -------------------------------------------------------------------------------- /packages/cli-utils/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @react-native-node-api/cli-utils 2 | 3 | ## 0.1.2 4 | 5 | ### Patch Changes 6 | 7 | - 7ff2c2b: Fix minor package issues. 8 | 9 | ## 0.1.1 10 | 11 | ### Patch Changes 12 | 13 | - 5156d35: Refactored moving prettyPath util to CLI utils package 14 | -------------------------------------------------------------------------------- /configs/tsconfig.node-tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.node.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "emitDeclarationOnly": true, 6 | "declarationMap": false 7 | }, 8 | "include": ["${configDir}/src/**/*.test.ts"], 9 | "exclude": [] 10 | } 11 | -------------------------------------------------------------------------------- /packages/ferric/src/run.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "node:events"; 2 | 3 | import { program } from "./program.js"; 4 | 5 | // We're attaching a lot of listeners when spawning in parallel 6 | EventEmitter.defaultMaxListeners = 100; 7 | 8 | program.parseAsync(process.argv).catch(console.error); 9 | -------------------------------------------------------------------------------- /packages/cmake-rn/src/helpers.ts: -------------------------------------------------------------------------------- 1 | export function toDefineArguments(declarations: Array>) { 2 | return declarations.flatMap((values) => 3 | Object.entries(values).flatMap(([key, definition]) => [ 4 | "-D", 5 | `${key}=${definition}`, 6 | ]), 7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /packages/host/cpp/Logger.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace callstack::react_native_node_api { 6 | void log_debug(const char *format, ...); 7 | void log_warning(const char *format, ...); 8 | void log_error(const char *format, ...); 9 | } // namespace callstack::react_native_node_api 10 | -------------------------------------------------------------------------------- /packages/node-tests/tsconfig.common.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "declarationMap": true, 6 | "emitDeclarationOnly": true, 7 | "outDir": "dist", 8 | "types": [] 9 | }, 10 | "include": ["common.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/test-app/tsconfig.node-scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "dist", 7 | "rootDir": "scripts", 8 | "types": ["node"] 9 | }, 10 | "include": ["scripts/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /apps/test-app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /packages/cli-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@commander-js/extra-typings"; 2 | export { default as chalk } from "chalk"; 3 | export * from "ora"; 4 | export * from "bufout"; 5 | export { default as pLimit } from "p-limit"; 6 | 7 | export * from "./actions.js"; 8 | export * from "./errors.js"; 9 | export * from "./paths.js"; 10 | -------------------------------------------------------------------------------- /packages/ferric/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "declarationMap": true, 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "types": ["node"] 9 | }, 10 | "include": ["src/**/*.ts"], 11 | "exclude": ["**.test.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/ferric/tsconfig.tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.node.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "emitDeclarationOnly": true 6 | }, 7 | "include": ["src/**/*.test.ts"], 8 | "exclude": [], 9 | "references": [ 10 | { 11 | "path": "./tsconfig.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Test cmake-file-api", 6 | "command": "node", 7 | "args": ["--run", "test"], 8 | "options": { 9 | "cwd": "${workspaceFolder}/packages/cmake-file-api" 10 | }, 11 | "group": "test" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /apps/test-app/.gitignore: -------------------------------------------------------------------------------- 1 | *.binlog 2 | *.hprof 3 | *.xcworkspace/ 4 | *.zip 5 | .gradle/ 6 | .kotlin/ 7 | .idea/ 8 | .vs/ 9 | .xcode.env 10 | Pods/ 11 | build/ 12 | local.properties 13 | msbuild.binlog 14 | 15 | # Ignoring the Podfile.lock as the `react-native-node-api` hash updates too frequently 16 | Podfile.lock 17 | 18 | hermes/ 19 | -------------------------------------------------------------------------------- /packages/cmake-rn/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "declarationMap": true, 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "types": ["node"] 9 | }, 10 | "include": ["src/**/*.ts"], 11 | "exclude": ["**/*.test.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/node-addon-examples/tsconfig.tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/react-native", 3 | "compilerOptions": { 4 | "composite": true, 5 | "noEmit": false, 6 | "module": "commonjs", 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | "types": ["react-native"] 10 | }, 11 | "include": ["src/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/cmake-rn/tsconfig.node-tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.node.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "emitDeclarationOnly": true 6 | }, 7 | "include": ["src/**/*.test.ts"], 8 | "exclude": [], 9 | "references": [ 10 | { 11 | "path": "./tsconfig.node.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/host/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true 4 | }, 5 | "files": [], 6 | "references": [ 7 | { "path": "./tsconfig.node.json" }, 8 | { "path": "./tsconfig.node-scripts.json" }, 9 | { "path": "./tsconfig.node-tests.json" }, 10 | { "path": "./tsconfig.react-native.json" } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/host/tsconfig.node-tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.node.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "emitDeclarationOnly": true 6 | }, 7 | "include": ["src/node/**/*.test.ts"], 8 | "exclude": [], 9 | "references": [ 10 | { 11 | "path": "./tsconfig.node.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/ferric/README.md: -------------------------------------------------------------------------------- 1 | # `ferric` 2 | 3 | A wrapper around Cargo 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) as well as [napi.rs](https://napi.rs/) to generate bindings from annotated Rust code. 4 | -------------------------------------------------------------------------------- /packages/host/tsconfig.react-native.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/react-native/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "declarationMap": true, 6 | "noEmit": false, 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | "types": ["react-native"] 10 | }, 11 | "include": ["src/react-native"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/node-tests/tsconfig.node-scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "dist", 7 | "rootDir": "scripts", 8 | "types": ["node", "read-pkg"] 9 | }, 10 | "include": ["scripts/**/*.mts"], 11 | "exclude": [] 12 | } 13 | -------------------------------------------------------------------------------- /packages/weak-node-api/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "declarationMap": true, 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "types": ["node"] 9 | }, 10 | "include": ["src/**/*.ts", "types/**/*.d.ts"], 11 | "exclude": ["**/*.test.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /configs/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "declarationMap": true, 6 | "outDir": "${configDir}/dist", 7 | "rootDir": "${configDir}/src", 8 | "types": ["node"] 9 | }, 10 | "include": ["${configDir}/src/"], 11 | "exclude": ["${configDir}/**/*.test.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/node-addon-examples/tests/async/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-test", 3 | "version": "0.0.0", 4 | "description": "Tests of runtime async functions", 5 | "main": "addon.js", 6 | "private": true, 7 | "dependencies": { 8 | "bindings": "~1.5.0" 9 | }, 10 | "scripts": { 11 | "test": "node addon.js" 12 | }, 13 | "gypfile": true 14 | } 15 | -------------------------------------------------------------------------------- /packages/node-addon-examples/tsconfig.node-scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "dist", 7 | "rootDir": "scripts", 8 | "types": ["node", "read-pkg"] 9 | }, 10 | "include": ["scripts/**/*.mts"], 11 | "exclude": [] 12 | } 13 | -------------------------------------------------------------------------------- /packages/node-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "./dist", 6 | "emitDeclarationOnly": true 7 | }, 8 | "files": ["rolldown.config.mts"], 9 | "references": [ 10 | { "path": "./tsconfig.common.json" }, 11 | { "path": "./tsconfig.node-scripts.json" } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/weak-node-api/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Everything in weak-node-api is generated, except for the configurations 3 | # Generated and built via `npm run bootstrap` 4 | /build/ 5 | /build-tests/ 6 | /*.xcframework 7 | /*.android.node 8 | /generated/ 9 | 10 | # Copied from node-api-headers by scripts/copy-node-api-headers.ts 11 | /include/ 12 | 13 | # Clang cache 14 | /.cache/ 15 | -------------------------------------------------------------------------------- /packages/node-addon-examples/tests/buffers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "buffers-test", 3 | "version": "0.0.0", 4 | "description": "Tests of runtime buffer functions", 5 | "main": "addon.js", 6 | "private": true, 7 | "dependencies": { 8 | "bindings": "~1.5.0" 9 | }, 10 | "scripts": { 11 | "test": "node --expose-gc addon.js" 12 | }, 13 | "gypfile": true 14 | } 15 | -------------------------------------------------------------------------------- /packages/cmake-file-api/src/schemas.ts: -------------------------------------------------------------------------------- 1 | export * from "./schemas/ReplyIndexV1.js"; 2 | export * from "./schemas/objects/CodemodelV2.js"; 3 | export * from "./schemas/objects/TargetV2.js"; 4 | export * from "./schemas/objects/CacheV2.js"; 5 | export * from "./schemas/objects/CmakeFilesV1.js"; 6 | export * from "./schemas/objects/ToolchainsV1.js"; 7 | export * from "./schemas/objects/ConfigureLogV1.js"; 8 | -------------------------------------------------------------------------------- /packages/ferric/src/program.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "@react-native-node-api/cli-utils"; 2 | 3 | import { printBanner } from "./banner.js"; 4 | import { buildCommand } from "./build.js"; 5 | 6 | export const program = new Command("ferric") 7 | .hook("preAction", () => printBanner()) 8 | .description("Rust Node-API Modules for React Native") 9 | .addCommand(buildCommand, { isDefault: true }); 10 | -------------------------------------------------------------------------------- /packages/weak-node-api/tsconfig.node-scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.node.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "dist", 7 | "rootDir": "scripts", 8 | "types": ["node"] 9 | }, 10 | "include": ["scripts/**/*.ts", "types/**/*.d.ts"], 11 | "exclude": [], 12 | "references": [{ "path": "./tsconfig.node.json" }] 13 | } 14 | -------------------------------------------------------------------------------- /packages/host/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Vendored hermes 3 | hermes/ 4 | 5 | # Vendored Node-API header files 6 | include/ 7 | 8 | # Android build artifacts 9 | **/android/.cxx/ 10 | **/android/build/ 11 | 12 | # iOS build artifacts 13 | /auto-linked/ 14 | 15 | # Android build artifacts 16 | android/.cxx/ 17 | android/build/ 18 | 19 | # Generated via `npm run generate-weak-node-api-injector` 20 | /cpp/WeakNodeApiInjector.cpp 21 | -------------------------------------------------------------------------------- /packages/host/tsconfig.node-scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.node.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "dist", 7 | "rootDir": "scripts", 8 | "types": ["node"] 9 | }, 10 | "include": ["scripts/**/*.mts", "types/**/*.d.ts"], 11 | "exclude": [], 12 | "references": [ 13 | { 14 | "path": "../weak-node-api/tsconfig.node.json" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/host/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "declarationMap": true, 6 | "outDir": "dist", 7 | "rootDir": "src", 8 | "types": ["node"] 9 | }, 10 | "include": ["src/node/**/*.ts", "types/**/*.d.ts"], 11 | "exclude": ["**/*.test.ts"], 12 | "references": [ 13 | { 14 | "path": "../weak-node-api/tsconfig.node.json" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /apps/test-app/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Opt into prebuilds of RN dependencies 2 | ENV['RCT_USE_RN_DEP'] = ENV['RCT_USE_RN_DEP'] || '1' 3 | 4 | ws_dir = Pathname.new(__dir__) 5 | ws_dir = ws_dir.parent until 6 | File.exist?("#{ws_dir}/node_modules/react-native-test-app/test_app.rb") || 7 | ws_dir.expand_path.to_s == '/' 8 | require "#{ws_dir}/node_modules/react-native-test-app/test_app.rb" 9 | 10 | use_test_app! :hermes_enabled => true, :fabric_enabled => true, :bridgeless_enabled => true 11 | -------------------------------------------------------------------------------- /packages/node-addon-examples/scripts/build-examples.mts: -------------------------------------------------------------------------------- 1 | import { execSync } from "node:child_process"; 2 | 3 | import { findCMakeProjects } from "./cmake-projects.mjs"; 4 | 5 | const projectDirectories = findCMakeProjects(); 6 | 7 | for (const projectDirectory of projectDirectories) { 8 | console.log(`Running "cmake-rn" in ${projectDirectory}`); 9 | execSync("cmake-rn --configuration RelWithDebInfo", { 10 | cwd: projectDirectory, 11 | stdio: "inherit", 12 | }); 13 | console.log(); 14 | } 15 | -------------------------------------------------------------------------------- /packages/host/src/react-native/index.ts: -------------------------------------------------------------------------------- 1 | import { type TurboModule, TurboModuleRegistry } from "react-native"; 2 | 3 | export interface Spec extends TurboModule { 4 | requireNodeAddon(libraryName: string): T; 5 | } 6 | 7 | const native = TurboModuleRegistry.getEnforcing("NodeApiHost"); 8 | 9 | /** 10 | * Loads a native Node-API addon by filename. 11 | */ 12 | export function requireNodeAddon(libraryName: string): T { 13 | return native.requireNodeAddon(libraryName); 14 | } 15 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/cmake-file-api/src/schemas/objects/ConfigureLogV1.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const ConfigureLogV1_0 = z.object({ 4 | kind: z.literal("configureLog"), 5 | version: z.object({ 6 | major: z.literal(1), 7 | minor: z.literal(0), 8 | }), 9 | path: z.string(), 10 | eventKindNames: z.array(z.string()), 11 | }); 12 | 13 | export const ConfigureLogV1 = z.union([ConfigureLogV1_0]); 14 | 15 | export const configureLogSchemaPerVersion = { 16 | "1.0": ConfigureLogV1_0, 17 | } as const satisfies Record; 18 | -------------------------------------------------------------------------------- /packages/cli-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-node-api/cli-utils", 3 | "version": "0.1.2", 4 | "description": "Useful utilities for the CLIs in the React Native Node API mono-repo", 5 | "type": "module", 6 | "files": [ 7 | "dist" 8 | ], 9 | "exports": { 10 | ".": "./dist/index.js" 11 | }, 12 | "dependencies": { 13 | "@commander-js/extra-typings": "^14.0.0", 14 | "bufout": "^0.3.2", 15 | "chalk": "^5.4.1", 16 | "commander": "^14.0.1", 17 | "ora": "^8.2.0", 18 | "p-limit": "^7.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/test-app/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures. 7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' 8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' 9 | gem 'xcodeproj', '< 1.26.0' 10 | gem 'concurrent-ruby', '< 1.3.4' 11 | 12 | # Ruby 3.4.0 has removed some libraries from the standard library. 13 | gem 'bigdecimal' 14 | gem 'logger' 15 | gem 'benchmark' 16 | gem 'mutex_m' -------------------------------------------------------------------------------- /packages/weak-node-api/scripts/copy-node-api-headers.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import fs from "node:fs"; 3 | import path from "node:path"; 4 | 5 | import { nodeApiHeaders } from "../src/node-api-functions.js"; 6 | const { include_dir: includeSourcePath } = nodeApiHeaders; 7 | 8 | const includeDestinationPath = path.join(import.meta.dirname, "../include"); 9 | assert(fs.existsSync(includeSourcePath), `Expected ${includeSourcePath}`); 10 | console.log(`Copying ${includeSourcePath} to ${includeDestinationPath}`); 11 | fs.cpSync(includeSourcePath, includeDestinationPath, { recursive: true }); 12 | -------------------------------------------------------------------------------- /packages/node-tests/scripts/build-tests.mts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { execSync } from "node:child_process"; 3 | 4 | import { findCMakeProjects } from "./utils.mjs"; 5 | 6 | const rootPath = path.join(import.meta.dirname, ".."); 7 | const projectPaths = findCMakeProjects(); 8 | 9 | for (const projectPath of projectPaths) { 10 | console.log( 11 | `Running "cmake-rn" in ${path.relative( 12 | rootPath, 13 | projectPath, 14 | )} to build for React Native`, 15 | ); 16 | execSync("cmake-rn --cmake-js", { 17 | cwd: projectPath, 18 | stdio: "inherit", 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /packages/ferric/src/rustup.ts: -------------------------------------------------------------------------------- 1 | import cp from "node:child_process"; 2 | 3 | import { UsageError } from "@react-native-node-api/cli-utils"; 4 | 5 | export function getInstalledTargets() { 6 | try { 7 | return new Set( 8 | cp 9 | .execFileSync("rustup", ["target", "list", "--installed"], { 10 | encoding: "utf-8", 11 | }) 12 | .split("\n"), 13 | ); 14 | } catch (error) { 15 | throw new UsageError( 16 | "You need a Rust toolchain: https://doc.rust-lang.org/cargo/getting-started/installation.html#install-rust-and-cargo", 17 | { cause: error }, 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/host/README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | Node-API Modules
for React Native 7 |

8 | 9 |

10 | Write once, run anywhere:
11 | Build native modules for React Native with Node-API. 12 |

13 | 14 | ## Getting started 15 | 16 | > [!WARNING] 17 | > This library is still under active development. Feel free to hack around, but use at your own risk. 18 | 19 | ``` 20 | npm install react-native-node-api 21 | ``` 22 | -------------------------------------------------------------------------------- /apps/test-app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-node-api-test-app", 3 | "displayName": "React Native Node-API Test App", 4 | "components": [ 5 | { 6 | "appKey": "react-native-node-api-test-app", 7 | "displayName": "React Native Node-API Test App" 8 | } 9 | ], 10 | "resources": { 11 | "android": ["dist/res", "dist/main.android.jsbundle"], 12 | "ios": ["dist/assets", "dist/main.ios.jsbundle"], 13 | "macos": ["dist/assets", "dist/main.macos.jsbundle"], 14 | "visionos": ["dist/assets", "dist/main.visionos.jsbundle"], 15 | "windows": ["dist/assets", "dist/main.windows.bundle"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/cmake-file-api/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | createSharedStatelessQuery, 3 | createClientStatelessQuery, 4 | createClientStatefulQuery, 5 | type VersionSpec, 6 | type QueryRequest, 7 | type StatefulQuery, 8 | } from "./query.js"; 9 | 10 | export { 11 | readReplyIndex, 12 | isReplyErrorIndexPath, 13 | readReplyErrorIndex, 14 | readCodemodel, 15 | readTarget, 16 | readCache, 17 | readCmakeFiles, 18 | readToolchains, 19 | readConfigureLog, 20 | findCurrentReplyIndexPath, 21 | readCurrentSharedCodemodel, 22 | readCurrentTargets, 23 | readCurrentTargetsDeep, 24 | } from "./reply.js"; 25 | 26 | export * from "./schemas.js"; 27 | -------------------------------------------------------------------------------- /packages/node-tests/scripts/utils.mts: -------------------------------------------------------------------------------- 1 | import { readdirSync, statSync } from "node:fs"; 2 | import path from "node:path"; 3 | 4 | export const TESTS_DIR = path.resolve(import.meta.dirname, "../tests"); 5 | 6 | export function findCMakeProjects(dir = TESTS_DIR): string[] { 7 | let results: string[] = []; 8 | const files = readdirSync(dir); 9 | 10 | for (const file of files) { 11 | const fullPath = path.join(dir, file); 12 | if (statSync(fullPath).isDirectory()) { 13 | results = results.concat(findCMakeProjects(fullPath)); 14 | } else if (file === "CMakeLists.txt") { 15 | results.push(dir); 16 | } 17 | } 18 | 19 | return results; 20 | } 21 | -------------------------------------------------------------------------------- /apps/test-app/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | google() 6 | } 7 | } 8 | 9 | rootProject.name = "react-native-node-api-test-app" 10 | 11 | apply(from: { 12 | def searchDir = rootDir.toPath() 13 | do { 14 | def p = searchDir.resolve("node_modules/react-native-test-app/test-app.gradle") 15 | if (p.toFile().exists()) { 16 | return p.toRealPath().toString() 17 | } 18 | } while (searchDir = searchDir.getParent()) 19 | throw new GradleException("Could not find `react-native-test-app`"); 20 | }()) 21 | applyTestAppSettings(settings) 22 | -------------------------------------------------------------------------------- /packages/ferric-example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ferric-example" 3 | version = "1.0.0" 4 | edition = "2021" 5 | license = "MIT" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies.napi] 11 | version = "=3.4.0" 12 | # see https://nodejs.org/api/n-api.html#node-api-version-matrix 13 | default-features = false 14 | features = ["napi3"] 15 | 16 | [dependencies.napi-derive] 17 | version = "3.3.0" 18 | features = ["type-def"] 19 | 20 | # See https://github.com/callstackincubator/react-native-node-api/issues/331 21 | [dependencies.napi-sys] 22 | version = "=3.0.1" 23 | 24 | [build-dependencies] 25 | napi-build = "2.2.4" 26 | 27 | [profile.release] 28 | lto = true 29 | codegen-units = 1 30 | -------------------------------------------------------------------------------- /packages/cmake-file-api/README.md: -------------------------------------------------------------------------------- 1 | # CMake File API (unofficial) 2 | 3 | The CMake File API provides an interface for querying CMake's configuration and project information. 4 | 5 | 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. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /packages/host/src/node/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | SUPPORTED_TRIPLETS, 3 | ANDROID_TRIPLETS, 4 | APPLE_TRIPLETS, 5 | type SupportedTriplet, 6 | type AndroidTriplet, 7 | type AppleTriplet, 8 | isSupportedTriplet, 9 | isAppleTriplet, 10 | isAndroidTriplet, 11 | } from "./prebuilds/triplets.js"; 12 | 13 | export { 14 | determineAndroidLibsFilename, 15 | createAndroidLibsDirectory, 16 | } from "./prebuilds/android.js"; 17 | 18 | export { 19 | createAppleFramework, 20 | createXCframework, 21 | createUniversalAppleLibrary, 22 | determineXCFrameworkFilename, 23 | } from "./prebuilds/apple.js"; 24 | 25 | export { 26 | determineLibraryBasename, 27 | dereferenceDirectory, 28 | } from "./path-utils.js"; 29 | -------------------------------------------------------------------------------- /apps/test-app/react-native.config.js: -------------------------------------------------------------------------------- 1 | const project = (() => { 2 | try { 3 | const { configureProjects } = require("react-native-test-app"); 4 | const project = configureProjects({ 5 | android: { 6 | sourceDir: "android", 7 | }, 8 | ios: { 9 | sourceDir: "ios", 10 | automaticPodsInstallation: false, 11 | }, 12 | // windows: { 13 | // sourceDir: "windows", 14 | // solutionFile: "windows/react-native-node-api-example.sln", 15 | // }, 16 | }); 17 | return { 18 | ...project, 19 | }; 20 | } catch { 21 | return undefined; 22 | } 23 | })(); 24 | 25 | module.exports = { 26 | ...(project ? { project } : undefined), 27 | }; 28 | -------------------------------------------------------------------------------- /packages/host/apple/NodeApiHostModuleProvider.mm: -------------------------------------------------------------------------------- 1 | #import "CxxNodeApiHostModule.hpp" 2 | #import "WeakNodeApiInjector.hpp" 3 | 4 | #import 5 | @interface NodeApiHostPackage : NSObject 6 | 7 | @end 8 | 9 | @implementation NodeApiHostPackage 10 | + (void)load { 11 | callstack::react_native_node_api::injectIntoWeakNodeApi(); 12 | 13 | facebook::react::registerCxxModuleToGlobalModuleMap( 14 | callstack::react_native_node_api::CxxNodeApiHostModule::kModuleName, 15 | [](std::shared_ptr jsInvoker) { 16 | return std::make_shared< 17 | callstack::react_native_node_api::CxxNodeApiHostModule>(jsInvoker); 18 | }); 19 | } 20 | 21 | @end -------------------------------------------------------------------------------- /packages/host/src/node/prebuilds/apple.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { describe, it } from "node:test"; 3 | 4 | import { escapeBundleIdentifier } from "./apple"; 5 | 6 | describe("escapeBundleIdentifier", () => { 7 | it("escapes and passes through values as expected", () => { 8 | assert.equal( 9 | escapeBundleIdentifier("abc-def-123-789.-"), 10 | "abc-def-123-789.-", 11 | ); 12 | assert.equal(escapeBundleIdentifier("abc_def"), "abc-def"); 13 | assert.equal(escapeBundleIdentifier("abc\ndef"), "abc-def"); 14 | assert.equal(escapeBundleIdentifier("\0abc"), "-abc"); 15 | assert.equal(escapeBundleIdentifier("🤷"), "--"); // An emoji takes up two chars 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/ferric/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ferric-cli", 3 | "version": "0.3.9", 4 | "description": "Rust Node-API Modules for React Native", 5 | "homepage": "https://github.com/callstackincubator/react-native-node-api", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/callstackincubator/react-native-node-api.git", 9 | "directory": "packages/ferric" 10 | }, 11 | "type": "module", 12 | "bin": { 13 | "ferric": "./bin/ferric.js" 14 | }, 15 | "scripts": { 16 | "start": "tsx src/run.ts" 17 | }, 18 | "dependencies": { 19 | "@napi-rs/cli": "~3.0.3", 20 | "@react-native-node-api/cli-utils": "0.1.2", 21 | "react-native-node-api": "0.7.1", 22 | "weak-node-api": "0.0.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/cmake-file-api/src/schemas/objects/CacheV2.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | const CacheEntryProperty = z.object({ 4 | name: z.string(), 5 | value: z.string(), 6 | }); 7 | 8 | const CacheEntry = z.object({ 9 | name: z.string(), 10 | value: z.string(), 11 | type: z.string(), 12 | properties: z.array(CacheEntryProperty), 13 | }); 14 | 15 | export const CacheV2_0 = z.object({ 16 | kind: z.literal("cache"), 17 | version: z.object({ 18 | major: z.literal(2), 19 | minor: z.number().int().nonnegative(), 20 | }), 21 | entries: z.array(CacheEntry), 22 | }); 23 | 24 | export const CacheV2 = z.union([CacheV2_0]); 25 | 26 | export const cacheSchemaPerVersion = { 27 | "2.0": CacheV2_0, 28 | } as const satisfies Record; 29 | -------------------------------------------------------------------------------- /packages/host/android/src/main/cpp/OnLoad.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | // Called when the library is loaded 9 | jint JNI_OnLoad(JavaVM *vm, void *reserved) { 10 | callstack::react_native_node_api::injectIntoWeakNodeApi(); 11 | // Register the C++ TurboModule 12 | facebook::react::registerCxxModuleToGlobalModuleMap( 13 | callstack::react_native_node_api::CxxNodeApiHostModule::kModuleName, 14 | [](std::shared_ptr jsInvoker) { 15 | return std::make_shared< 16 | callstack::react_native_node_api::CxxNodeApiHostModule>(jsInvoker); 17 | }); 18 | return JNI_VERSION_1_6; 19 | } 20 | -------------------------------------------------------------------------------- /apps/test-app/metro.config.js: -------------------------------------------------------------------------------- 1 | const { makeMetroConfig } = require("@rnx-kit/metro-config"); 2 | 3 | const config = makeMetroConfig({ 4 | transformer: { 5 | getTransformOptions: async () => ({ 6 | transform: { 7 | experimentalImportSupport: false, 8 | inlineRequires: false, 9 | }, 10 | }), 11 | }, 12 | }); 13 | 14 | if (config.watchFolders.length === 0) { 15 | // This patch is needed to locate packages in the monorepo from the MacOS app 16 | // which is intentionally kept outside of the workspaces configuration to prevent 17 | // duplicate react-native version and pollution of the package lock. 18 | const path = require("node:path"); 19 | config.watchFolders.push(path.resolve(__dirname, "../..")); 20 | } 21 | 22 | module.exports = config; 23 | -------------------------------------------------------------------------------- /packages/weak-node-api/tests/test_inject.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | TEST_CASE("inject_weak_node_api_host") { 5 | SECTION("is callable") { 6 | NodeApiHost host{}; 7 | inject_weak_node_api_host(host); 8 | } 9 | 10 | SECTION("propagates calls to napi_create_object") { 11 | static bool called = false; 12 | auto my_create_object = [](napi_env env, 13 | napi_value *result) -> napi_status { 14 | called = true; 15 | return napi_status::napi_ok; 16 | }; 17 | NodeApiHost host{.napi_create_object = my_create_object}; 18 | inject_weak_node_api_host(host); 19 | 20 | napi_value result; 21 | napi_create_object({}, &result); 22 | 23 | REQUIRE(called); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/weak-node-api/src/weak-node-api.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fs from "node:fs"; 3 | 4 | export const weakNodeApiPath = path.resolve(import.meta.dirname, ".."); 5 | 6 | const debugOutputPath = path.resolve(weakNodeApiPath, "build", "Debug"); 7 | const releaseOutputPath = path.resolve(weakNodeApiPath, "build", "Release"); 8 | 9 | export const outputPath = fs.existsSync(debugOutputPath) 10 | ? debugOutputPath 11 | : releaseOutputPath; 12 | 13 | export const applePrebuildPath = path.resolve( 14 | outputPath, 15 | "weak-node-api.xcframework", 16 | ); 17 | 18 | export const androidPrebuildPath = path.resolve( 19 | outputPath, 20 | "weak-node-api.android.node", 21 | ); 22 | 23 | export const weakNodeApiCmakePath = path.resolve( 24 | weakNodeApiPath, 25 | "weak-node-api-config.cmake", 26 | ); 27 | -------------------------------------------------------------------------------- /packages/node-addon-examples/tests/async/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15...3.31) 2 | project(async-test) 3 | 4 | find_package(weak-node-api REQUIRED CONFIG) 5 | 6 | add_library(addon SHARED addon.c) 7 | 8 | option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON) 9 | 10 | if(APPLE AND BUILD_APPLE_FRAMEWORK) 11 | set_target_properties(addon PROPERTIES 12 | FRAMEWORK TRUE 13 | MACOSX_FRAMEWORK_IDENTIFIER async-test.addon 14 | MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 15 | MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 16 | XCODE_ATTRIBUTE_SKIP_INSTALL NO 17 | ) 18 | else() 19 | set_target_properties(addon PROPERTIES 20 | PREFIX "" 21 | SUFFIX .node 22 | ) 23 | endif() 24 | 25 | target_link_libraries(addon PRIVATE weak-node-api) 26 | target_compile_features(addon PRIVATE cxx_std_17) -------------------------------------------------------------------------------- /packages/node-addon-examples/tests/buffers/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15...3.31) 2 | project(buffers-test) 3 | 4 | find_package(weak-node-api REQUIRED CONFIG) 5 | 6 | add_library(addon SHARED addon.c) 7 | 8 | option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON) 9 | 10 | if(APPLE AND BUILD_APPLE_FRAMEWORK) 11 | set_target_properties(addon PROPERTIES 12 | FRAMEWORK TRUE 13 | MACOSX_FRAMEWORK_IDENTIFIER buffers-test.addon 14 | MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 15 | MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 16 | XCODE_ATTRIBUTE_SKIP_INSTALL NO 17 | ) 18 | else() 19 | set_target_properties(addon PROPERTIES 20 | PREFIX "" 21 | SUFFIX .node 22 | ) 23 | endif() 24 | 25 | target_link_libraries(addon PRIVATE weak-node-api) 26 | target_compile_features(addon PRIVATE cxx_std_17) -------------------------------------------------------------------------------- /packages/cli-utils/src/errors.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | 3 | export type Fix = 4 | | { 5 | instructions: string; 6 | command?: never; 7 | } 8 | | { 9 | instructions?: never; 10 | command: string; 11 | }; 12 | 13 | export class UsageError extends Error { 14 | public readonly fix?: Fix; 15 | 16 | constructor( 17 | message: string, 18 | { fix, cause }: { cause?: unknown; fix?: Fix } = {}, 19 | ) { 20 | super(message, { cause }); 21 | this.fix = fix; 22 | } 23 | } 24 | 25 | export function assertFixable( 26 | value: unknown, 27 | message: string, 28 | fix: Fix, 29 | ): asserts value { 30 | try { 31 | assert(value, message); 32 | } catch (error) { 33 | assert(error instanceof Error); 34 | throw new UsageError(message, { fix }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/ferric-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-node-api/ferric-example", 3 | "version": "0.1.1", 4 | "private": true, 5 | "type": "commonjs", 6 | "homepage": "https://github.com/callstackincubator/react-native-node-api", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/callstackincubator/react-native-node-api.git", 10 | "directory": "packages/ferric-example" 11 | }, 12 | "main": "ferric_example.js", 13 | "types": "ferric_example.d.ts", 14 | "files": [ 15 | "ferric_example.js", 16 | "ferric_example.d.ts", 17 | "ferric_example.apple.node", 18 | "ferric_example.android.node" 19 | ], 20 | "scripts": { 21 | "build": "ferric build", 22 | "bootstrap": "node --run build" 23 | }, 24 | "devDependencies": { 25 | "ferric-cli": "*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/weak-node-api/tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | Include(FetchContent) 2 | 3 | FetchContent_Declare( 4 | Catch2 5 | GIT_REPOSITORY https://github.com/catchorg/Catch2.git 6 | GIT_TAG v3.11.0 7 | ) 8 | 9 | FetchContent_MakeAvailable(Catch2) 10 | 11 | add_executable(weak-node-api-tests 12 | test_inject.cpp 13 | ) 14 | target_link_libraries(weak-node-api-tests 15 | PRIVATE 16 | weak-node-api 17 | Catch2::Catch2WithMain 18 | ) 19 | 20 | target_compile_features(weak-node-api-tests PRIVATE cxx_std_20) 21 | target_compile_definitions(weak-node-api-tests PRIVATE NAPI_VERSION=8) 22 | 23 | # As per https://github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md#catchcmake-and-catchaddtestscmake 24 | list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) 25 | include(CTest) 26 | include(Catch) 27 | catch_discover_tests(weak-node-api-tests) 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "allowJs": true 6 | }, 7 | "files": ["prettier.config.js", "eslint.config.js"], 8 | "references": [ 9 | { "path": "./tsconfig.scripts.json" }, 10 | { "path": "./packages/cli-utils/tsconfig.json" }, 11 | { "path": "./packages/cmake-file-api/tsconfig.json" }, 12 | { "path": "./packages/cmake-file-api/tsconfig.tests.json" }, 13 | { "path": "./packages/host/tsconfig.json" }, 14 | { "path": "./packages/gyp-to-cmake/tsconfig.json" }, 15 | { "path": "./packages/cmake-rn/tsconfig.json" }, 16 | { "path": "./packages/ferric/tsconfig.json" }, 17 | { "path": "./packages/node-addon-examples/tsconfig.json" }, 18 | { "path": "./packages/node-tests/tsconfig.json" }, 19 | { "path": "./packages/weak-node-api/tsconfig.json" } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /docs/WEAK-NODE-API.md: -------------------------------------------------------------------------------- 1 | # The `weak-node-api` library 2 | 3 | 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`. 4 | 5 | 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. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /packages/host/src/node/podspec.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { describe, it } from "node:test"; 3 | import cp from "node:child_process"; 4 | 5 | describe("Podspec", () => { 6 | // We cannot support prebuilds of React Native Core since we're patching JSI 7 | it( 8 | "should error when RCT_USE_PREBUILT_RNCORE is set", 9 | // We cannot call `pod` on non-macOS systems 10 | { skip: process.platform !== "darwin" }, 11 | () => { 12 | const { status, stdout } = cp.spawnSync("pod", ["spec", "lint"], { 13 | env: { ...process.env, RCT_USE_PREBUILT_RNCORE: "1" }, 14 | encoding: "utf-8", 15 | }); 16 | 17 | assert.notEqual(status, 0); 18 | assert.match( 19 | stdout, 20 | /React Native Node-API cannot reliably patch JSI when React Native Core is prebuilt/, 21 | ); 22 | }, 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/weak-node-api/scripts/generators/shared.ts: -------------------------------------------------------------------------------- 1 | import type { FunctionDecl } from "../../src/node-api-functions.js"; 2 | 3 | type FunctionOptions = FunctionDecl & { 4 | extern?: true; 5 | static?: true; 6 | namespace?: string; 7 | body?: string; 8 | argumentNames?: string[]; 9 | }; 10 | 11 | export function generateFunction({ 12 | extern, 13 | static: staticMember, 14 | returnType, 15 | namespace, 16 | name, 17 | argumentTypes, 18 | argumentNames = [], 19 | noReturn, 20 | body, 21 | }: FunctionOptions) { 22 | return ` 23 | ${staticMember ? "static " : ""}${extern ? 'extern "C" ' : ""}${returnType} ${namespace ? namespace + "::" : ""}${name}( 24 | ${argumentTypes.map((type, index) => `${type} ` + (argumentNames[index] ?? `arg${index}`)).join(", ")} 25 | ) ${body ? `{ ${body} ${noReturn ? "WEAK_NODE_API_UNREACHABLE;" : ""}\n}` : ""} 26 | ; 27 | `; 28 | } 29 | -------------------------------------------------------------------------------- /packages/node-addon-examples/tests/buffers/addon.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const addon = require("bindings")("addon.node"); 3 | 4 | const toLocale = (text) => { 5 | return text 6 | .toString() 7 | .split(",") 8 | .map((code) => String.fromCharCode(parseInt(code, 10))) 9 | .join(""); 10 | }; 11 | 12 | module.exports = () => { 13 | assert.strictEqual(toLocale(addon.newBuffer()), addon.theText); 14 | assert.strictEqual(toLocale(addon.newExternalBuffer()), addon.theText); 15 | assert.strictEqual(toLocale(addon.copyBuffer()), addon.theText); 16 | let buffer = addon.staticBuffer(); 17 | assert.strictEqual(addon.bufferHasInstance(buffer), true); 18 | assert.strictEqual(addon.bufferInfo(buffer), true); 19 | addon.invalidObjectAsBuffer({}); 20 | 21 | // TODO: Add gc tests 22 | // @see 23 | // https://github.com/callstackincubator/react-native-node-api/issues/182 24 | }; 25 | -------------------------------------------------------------------------------- /packages/host/android/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | project(react-native-node-api) 4 | set(CMAKE_CXX_STANDARD 20) 5 | 6 | find_package(ReactAndroid REQUIRED CONFIG) 7 | find_package(hermes-engine REQUIRED CONFIG) 8 | find_package(weak-node-api REQUIRED CONFIG) 9 | 10 | add_library(node-api-host SHARED 11 | src/main/cpp/OnLoad.cpp 12 | ../cpp/Logger.cpp 13 | ../cpp/CxxNodeApiHostModule.cpp 14 | ../cpp/WeakNodeApiInjector.cpp 15 | ../cpp/RuntimeNodeApi.cpp 16 | ../cpp/RuntimeNodeApi.hpp 17 | ../cpp/RuntimeNodeApiAsync.cpp 18 | ../cpp/RuntimeNodeApiAsync.hpp 19 | ) 20 | 21 | target_include_directories(node-api-host PRIVATE 22 | ../cpp 23 | ) 24 | 25 | target_link_libraries(node-api-host 26 | PRIVATE 27 | # android 28 | log 29 | ReactAndroid::reactnative 30 | ReactAndroid::jsi 31 | hermes-engine::libhermes 32 | weak-node-api 33 | # react_codegen_NodeApiHostSpec 34 | ) 35 | -------------------------------------------------------------------------------- /packages/ferric/src/banner.ts: -------------------------------------------------------------------------------- 1 | import { chalk } from "@react-native-node-api/cli-utils"; 2 | 3 | const LINES = [ 4 | // Pagga on https://www.asciiart.eu/text-to-ascii-art 5 | // Box elements from https://www.compart.com/en/unicode/block/U+2500 6 | "╭─────────────────────────╮", 7 | "│░█▀▀░█▀▀░█▀▄░█▀▄░▀█▀░█▀▀░│", 8 | "│░█▀▀░█▀▀░█▀▄░█▀▄░░█░░█░░░│", 9 | "│░▀░░░▀▀▀░▀░▀░▀░▀░▀▀▀░▀▀▀░│", 10 | "╰─────────────────────────╯", 11 | ]; 12 | 13 | export function getBlockComment() { 14 | return ( 15 | "/**\n" + 16 | ["This file was generated by", ...LINES, "Powered by napi.rs"] 17 | .map((line) => ` * ${line}`) 18 | .join("\n") + 19 | "\n */" 20 | ); 21 | } 22 | 23 | export function printBanner() { 24 | console.log( 25 | LINES.map((line, lineNumber, lines) => { 26 | const ratio = lineNumber / lines.length; 27 | return chalk.rgb(Math.round(250 - 100 * ratio), 0, 0)(line); 28 | }).join("\n"), 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/node-addon-examples/scripts/cmake-projects.mts: -------------------------------------------------------------------------------- 1 | import { readdirSync, statSync } from "node:fs"; 2 | import path from "node:path"; 3 | 4 | export const EXAMPLES_DIR = path.resolve(import.meta.dirname, "../examples"); 5 | export const TESTS_DIR = path.resolve(import.meta.dirname, "../tests"); 6 | export const DIRS = [EXAMPLES_DIR, TESTS_DIR]; 7 | 8 | export function findCMakeProjectsRecursively(dir: string): string[] { 9 | let results: string[] = []; 10 | const files = readdirSync(dir); 11 | 12 | for (const file of files) { 13 | const fullPath = path.join(dir, file); 14 | if (statSync(fullPath).isDirectory()) { 15 | results = results.concat(findCMakeProjectsRecursively(fullPath)); 16 | } else if (file === "CMakeLists.txt") { 17 | results.push(dir); 18 | } 19 | } 20 | 21 | return results; 22 | } 23 | 24 | export function findCMakeProjects(): string[] { 25 | return DIRS.flatMap(findCMakeProjectsRecursively); 26 | } 27 | -------------------------------------------------------------------------------- /packages/gyp-to-cmake/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gyp-to-cmake", 3 | "version": "0.5.1", 4 | "description": "Convert binding.gyp files to CMakeLists.txt", 5 | "homepage": "https://github.com/callstackincubator/react-native-node-api", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/callstackincubator/react-native-node-api.git", 9 | "directory": "packages/gyp-to-cmake" 10 | }, 11 | "type": "module", 12 | "files": [ 13 | "bin", 14 | "dist" 15 | ], 16 | "bin": { 17 | "gyp-to-cmake": "./bin/gyp-to-cmake.js" 18 | }, 19 | "scripts": { 20 | "build": "tsc", 21 | "start": "tsx src/run.ts", 22 | "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" 23 | }, 24 | "dependencies": { 25 | "@react-native-node-api/cli-utils": "0.1.2", 26 | "gyp-parser": "^1.0.4", 27 | "pkg-dir": "^8.0.0", 28 | "read-pkg": "^9.0.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/gyp-to-cmake/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # gyp-to-cmake 2 | 3 | ## 0.5.1 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [7ff2c2b] 8 | - @react-native-node-api/cli-utils@0.1.2 9 | 10 | ## 0.5.0 11 | 12 | ### Minor Changes 13 | 14 | - 60fae96: Use `find_package` instead of `include` to locate "weak-node-api" 15 | 16 | ## 0.4.0 17 | 18 | ### Minor Changes 19 | 20 | - 5156d35: Use of CMake targets producing Apple frameworks instead of free dylibs is now supported 21 | 22 | ### Patch Changes 23 | 24 | - 5156d35: Refactored moving prettyPath util to CLI utils package 25 | - Updated dependencies [5156d35] 26 | - @react-native-node-api/cli-utils@0.1.1 27 | 28 | ## 0.3.0 29 | 30 | ### Minor Changes 31 | 32 | - ff34c45: Add --weak-node-api option to emit CMake configuration for use with cmake-rn's default way of Node-API linkage. 33 | 34 | ### Patch Changes 35 | 36 | - 2a30d8d: Refactored CLIs to use a shared utility package 37 | 38 | ## 0.2.0 39 | 40 | ### Minor Changes 41 | 42 | - 4379d8c: Initial release 43 | -------------------------------------------------------------------------------- /packages/cmake-rn/src/platforms.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | 3 | import { platform as android } from "./platforms/android.js"; 4 | import { platform as apple } from "./platforms/apple.js"; 5 | import { Platform } from "./platforms/types.js"; 6 | 7 | export const platforms: Platform[] = [android, apple] as const; 8 | export const allTriplets = [...android.triplets, ...apple.triplets] as const; 9 | 10 | export function platformHasTriplet

( 11 | platform: P, 12 | triplet: unknown, 13 | ): triplet is P["triplets"][number] { 14 | return (platform.triplets as unknown[]).includes(triplet); 15 | } 16 | 17 | export function findPlatformForTriplet(triplet: unknown) { 18 | const platform = Object.values(platforms).find((platform) => 19 | platformHasTriplet(platform, triplet), 20 | ); 21 | assert( 22 | platform, 23 | `Unable to determine platform from triplet: ${ 24 | typeof triplet === "string" ? triplet : JSON.stringify(triplet) 25 | }`, 26 | ); 27 | return platform; 28 | } 29 | -------------------------------------------------------------------------------- /packages/cmake-file-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cmake-file-api", 3 | "version": "0.1.1", 4 | "type": "module", 5 | "description": "TypeScript wrapper around the CMake File API", 6 | "homepage": "https://cmake.org/cmake/help/latest/manual/cmake-file-api.7.html", 7 | "scripts": { 8 | "build": "tsc --build", 9 | "lint": "eslint 'src/**/*.ts'", 10 | "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" 11 | }, 12 | "files": [ 13 | "dist/", 14 | "!*.test.d.ts", 15 | "!*.test.d.ts.map" 16 | ], 17 | "exports": { 18 | ".": "./dist/index.js" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/callstackincubator/react-native-node-api.git", 23 | "directory": "packages/cmake-file-api" 24 | }, 25 | "author": { 26 | "name": "Kræn Hansen", 27 | "url": "https://github.com/kraenhansen" 28 | }, 29 | "dependencies": { 30 | "zod": "^4.1.11" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/host/src/node/cli/options.ts: -------------------------------------------------------------------------------- 1 | import { Option } from "@react-native-node-api/cli-utils"; 2 | 3 | import { 4 | assertLibraryNamingChoice, 5 | LIBRARY_NAMING_CHOICES, 6 | } from "../path-utils"; 7 | 8 | const { NODE_API_PACKAGE_NAME, NODE_API_PATH_SUFFIX } = process.env; 9 | if (typeof NODE_API_PACKAGE_NAME === "string") { 10 | assertLibraryNamingChoice(NODE_API_PACKAGE_NAME); 11 | } 12 | if (typeof NODE_API_PATH_SUFFIX === "string") { 13 | assertLibraryNamingChoice(NODE_API_PATH_SUFFIX); 14 | } 15 | 16 | export const packageNameOption = new Option( 17 | "--package-name ", 18 | "Controls how the package name is transformed into a library name", 19 | ) 20 | .choices(LIBRARY_NAMING_CHOICES) 21 | .default(NODE_API_PACKAGE_NAME || "strip"); 22 | 23 | export const pathSuffixOption = new Option( 24 | "--path-suffix ", 25 | "Controls how the path of the addon inside a package is transformed into a library name", 26 | ) 27 | .choices(LIBRARY_NAMING_CHOICES) 28 | .default(NODE_API_PATH_SUFFIX || "strip"); 29 | -------------------------------------------------------------------------------- /packages/host/android/src/main/java/com/callstack/react_native_node_api/NodeApiHostPackage.kt: -------------------------------------------------------------------------------- 1 | package com.callstack.react_native_node_api 2 | 3 | import com.facebook.hermes.reactexecutor.HermesExecutor 4 | import com.facebook.react.BaseReactPackage 5 | import com.facebook.react.bridge.NativeModule 6 | import com.facebook.react.bridge.ReactApplicationContext 7 | import com.facebook.react.module.model.ReactModuleInfo 8 | import com.facebook.react.module.model.ReactModuleInfoProvider 9 | import com.facebook.soloader.SoLoader 10 | 11 | import java.util.HashMap 12 | 13 | class NodeApiHostPackage : BaseReactPackage() { 14 | init { 15 | SoLoader.loadLibrary("node-api-host") 16 | } 17 | 18 | override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { 19 | return null 20 | } 21 | 22 | override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { 23 | return ReactModuleInfoProvider { 24 | val moduleInfos: MutableMap = HashMap() 25 | moduleInfos 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scripts/run-in-published.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import cp from "node:child_process"; 3 | 4 | console.log("Run command in all non-private packages of the monorepo"); 5 | 6 | function getWorkspaces() { 7 | const workspaces = JSON.parse( 8 | cp.execFileSync("npm", ["query", ".workspace"], { encoding: "utf8" }), 9 | ) as unknown; 10 | assert(Array.isArray(workspaces)); 11 | for (const workspace of workspaces) { 12 | assert(typeof workspace === "object" && workspace !== null); 13 | } 14 | return workspaces as Record[]; 15 | } 16 | 17 | const publishedPackagePaths = getWorkspaces() 18 | .filter((w) => !w.private) 19 | .map((p) => { 20 | assert(typeof p.path === "string"); 21 | return p.path; 22 | }); 23 | 24 | const [, , command, ...argv] = process.argv; 25 | 26 | for (const packagePath of publishedPackagePaths) { 27 | const { status } = cp.spawnSync(command, argv, { 28 | cwd: packagePath, 29 | stdio: "inherit", 30 | }); 31 | assert.equal(status, 0, `Command failed (status = ${status})`); 32 | } 33 | -------------------------------------------------------------------------------- /packages/weak-node-api/scripts/generators/NodeApiHost.ts: -------------------------------------------------------------------------------- 1 | import type { FunctionDecl } from "../../src/node-api-functions.js"; 2 | 3 | export function generateFunctionDecl({ 4 | returnType, 5 | name, 6 | argumentTypes, 7 | }: FunctionDecl) { 8 | return `${returnType} (*${name})(${argumentTypes.join(", ")});`; 9 | } 10 | 11 | export function generateHeader(functions: FunctionDecl[]) { 12 | return ` 13 | #pragma once 14 | 15 | #include 16 | 17 | // Ideally we would have just used NAPI_NO_RETURN, but 18 | // __declspec(noreturn) (when building with Microsoft Visual C++) cannot be used on members of a struct 19 | // TODO: If we targeted C++23 we could use std::unreachable() 20 | 21 | #if defined(__GNUC__) 22 | #define WEAK_NODE_API_UNREACHABLE __builtin_unreachable() 23 | #else 24 | #define WEAK_NODE_API_UNREACHABLE __assume(0) 25 | #endif 26 | 27 | // Generate the struct of function pointers 28 | struct NodeApiHost { 29 | ${functions.map(generateFunctionDecl).join("\n")} 30 | }; 31 | `; 32 | } 33 | -------------------------------------------------------------------------------- /packages/host/cpp/RuntimeNodeApiAsync.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "node_api.h" 4 | #include 5 | #include 6 | 7 | namespace callstack::react_native_node_api { 8 | void setCallInvoker( 9 | napi_env env, const std::shared_ptr &invoker); 10 | 11 | napi_status napi_create_async_work(napi_env env, napi_value async_resource, 12 | napi_value async_resource_name, 13 | napi_async_execute_callback execute, 14 | napi_async_complete_callback complete, 15 | void *data, napi_async_work *result); 16 | 17 | napi_status napi_queue_async_work(node_api_basic_env env, napi_async_work work); 18 | 19 | napi_status napi_delete_async_work(node_api_basic_env env, 20 | napi_async_work work); 21 | 22 | napi_status napi_cancel_async_work(node_api_basic_env env, 23 | napi_async_work work); 24 | } // namespace callstack::react_native_node_api 25 | -------------------------------------------------------------------------------- /packages/node-addon-examples/README.md: -------------------------------------------------------------------------------- 1 | # Node Addon Examples (`@react-native-node-api/node-addon-examples`) 2 | 3 | We're using the [nodejs/node-addon-examples](https://github.com/nodejs/node-addon-examples) repository from the Node.js project as tests for our Node-API implementation and this package is a wrapper around those, using `gyp-to-cmake` and `cmake-rn` to prepare prebuilds and scaffolding for loading the addons. 4 | 5 | The main purpose is to use these as tests to verify the implementation: We choose to use this as our first signal for compliance, over the [js-native-api](https://github.com/nodejs/node/tree/main/test/js-native-api) tests in the Node.js project, because the examples depends much less on Node.js built-in runtime APIs. A drawback is that these examples were not built as tests with assertions, but examples using console logging to signal functionality and we work around this limitation by wrapping the loading of the example JS code with a console.log stub implementation which buffer and asserts messages printed by the addon. 6 | 7 | This package is imported by our [test app](../../apps/test-app). 8 | -------------------------------------------------------------------------------- /packages/cmake-rn/src/platforms.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { describe, it } from "node:test"; 3 | 4 | import { 5 | platforms, 6 | platformHasTriplet, 7 | findPlatformForTriplet, 8 | } from "./platforms.js"; 9 | import { Platform } from "./platforms/types.js"; 10 | 11 | const mockPlatform = { 12 | triplets: ["triplet1", "triplet2"], 13 | } as unknown as Platform; 14 | 15 | describe("platformHasTriplet", () => { 16 | it("returns true when platform has triplet", () => { 17 | assert.equal(platformHasTriplet(mockPlatform, "triplet1"), true); 18 | }); 19 | 20 | it("returns false when platform doesn't have triplet", () => { 21 | assert.equal(platformHasTriplet(mockPlatform, "triplet3"), false); 22 | }); 23 | }); 24 | 25 | describe("findPlatformForTriplet", () => { 26 | it("returns platform when triplet is found", () => { 27 | assert(platforms.length >= 2, "Expects at least two platforms"); 28 | const [platform1, platform2] = platforms; 29 | const platform = findPlatformForTriplet(platform1.triplets[0]); 30 | assert.equal(platform, platform1); 31 | assert.notEqual(platform, platform2); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/host/src/node/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { TestContext } from "node:test"; 2 | import os from "node:os"; 3 | import fs from "node:fs"; 4 | import path from "node:path"; 5 | 6 | export interface FileMap { 7 | [key: string]: string | FileMap; 8 | } 9 | 10 | function writeFiles(fromPath: string, files: FileMap) { 11 | for (const [filePath, content] of Object.entries(files)) { 12 | const fullPath = path.join(fromPath, filePath); 13 | fs.mkdirSync(path.dirname(fullPath), { recursive: true }); 14 | if (typeof content === "string") { 15 | fs.writeFileSync(fullPath, content, "utf8"); 16 | } else { 17 | writeFiles(fullPath, content); 18 | } 19 | } 20 | } 21 | 22 | export function setupTempDirectory(context: TestContext, files: FileMap) { 23 | const tempDirectoryPath = fs.realpathSync( 24 | fs.mkdtempSync(path.join(os.tmpdir(), "react-native-node-api-test-")), 25 | ); 26 | 27 | context.after(() => { 28 | if (!process.env.KEEP_TEMP_DIRS) { 29 | fs.rmSync(tempDirectoryPath, { recursive: true, force: true }); 30 | } 31 | }); 32 | 33 | writeFiles(tempDirectoryPath, files); 34 | 35 | return tempDirectoryPath; 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025-present, Callstack and React Native Node API contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/cmake-rn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cmake-rn", 3 | "version": "0.6.1", 4 | "description": "Build React Native Node API modules with CMake", 5 | "homepage": "https://github.com/callstackincubator/react-native-node-api", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/callstackincubator/react-native-node-api.git", 9 | "directory": "packages/cmake-rn" 10 | }, 11 | "type": "module", 12 | "bin": { 13 | "cmake-rn": "./bin/cmake-rn.js" 14 | }, 15 | "files": [ 16 | "bin", 17 | "dist", 18 | "!dist/**/*.test.d.ts", 19 | "!dist/**/*.test.d.ts.map" 20 | ], 21 | "scripts": { 22 | "build": "tsc", 23 | "start": "tsx src/run.ts", 24 | "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout" 25 | }, 26 | "dependencies": { 27 | "@react-native-node-api/cli-utils": "0.1.2", 28 | "cmake-file-api": "0.1.1", 29 | "react-native-node-api": "0.7.1", 30 | "zod": "^4.1.11", 31 | "weak-node-api": "0.0.3" 32 | }, 33 | "peerDependencies": { 34 | "node-addon-api": "^8.3.1", 35 | "node-api-headers": "^1.5.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/cmake-file-api/src/schemas/objects/ToolchainsV1.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | const ToolchainCompilerImplicit = z.object({ 4 | includeDirectories: z.array(z.string()).optional(), 5 | linkDirectories: z.array(z.string()).optional(), 6 | linkFrameworkDirectories: z.array(z.string()).optional(), 7 | linkLibraries: z.array(z.string()).optional(), 8 | }); 9 | 10 | const ToolchainCompiler = z.object({ 11 | path: z.string().optional(), 12 | id: z.string().optional(), 13 | version: z.string().optional(), 14 | target: z.string().optional(), 15 | implicit: ToolchainCompilerImplicit, 16 | }); 17 | 18 | const Toolchain = z.object({ 19 | language: z.string(), 20 | compiler: ToolchainCompiler, 21 | sourceFileExtensions: z.array(z.string()).optional(), 22 | }); 23 | 24 | export const ToolchainsV1_0 = z.object({ 25 | kind: z.literal("toolchains"), 26 | version: z.object({ 27 | major: z.literal(1), 28 | minor: z.number().int().nonnegative(), 29 | }), 30 | toolchains: z.array(Toolchain), 31 | }); 32 | 33 | export const ToolchainsV1 = z.union([ToolchainsV1_0]); 34 | 35 | export const toolchainsSchemaPerVersion = { 36 | "1.0": ToolchainsV1_0, 37 | } as const satisfies Record; 38 | -------------------------------------------------------------------------------- /packages/node-addon-examples/tests/async/addon.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const test_async = require("bindings")("addon.node"); 3 | 4 | const test = () => 5 | new Promise((resolve, reject) => { 6 | test_async.Test(5, {}, (err, val) => { 7 | if (err) { 8 | reject(err); 9 | return; 10 | } 11 | try { 12 | assert.strictEqual(err, null); 13 | assert.strictEqual(val, 10); 14 | } catch (e) { 15 | reject(e); 16 | } 17 | resolve(); 18 | }); 19 | }); 20 | 21 | const testCancel = () => 22 | new Promise((resolve) => { 23 | test_async.TestCancel(() => resolve()); 24 | }); 25 | 26 | const doRepeatedWork = (count = 0) => 27 | new Promise((resolve, reject) => { 28 | const iterations = 100; 29 | const workDone = (status) => { 30 | try { 31 | assert.strictEqual(status, 0); 32 | } catch (e) { 33 | reject(e); 34 | } 35 | if (++count < iterations) { 36 | test_async.DoRepeatedWork(workDone); 37 | } else { 38 | resolve(); 39 | } 40 | }; 41 | test_async.DoRepeatedWork(workDone); 42 | }); 43 | 44 | module.exports = () => { 45 | return Promise.all([test(), testCancel(), doRepeatedWork()]); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/gyp-to-cmake/src/gyp.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "node:test"; 2 | import assert from "node:assert/strict"; 3 | 4 | import { assertBinding } from "./gyp.js"; 5 | 6 | describe("gyp.assertRoot", () => { 7 | it("should throw if input is malformed", () => { 8 | assert.throws(() => { 9 | assertBinding("not an object"); 10 | }, /Expected an object/); 11 | 12 | assert.throws(() => { 13 | assertBinding({}); 14 | }, /Expected a 'targets' property/); 15 | 16 | assert.throws(() => { 17 | assertBinding({ targets: "not an array" }); 18 | }, /Expected a 'targets' array/); 19 | }); 20 | 21 | it("should throw if input has extra properties", () => { 22 | assert.throws(() => { 23 | assertBinding({ targets: [], extra: "not allowed" }, true); 24 | }, /Unexpected property: extra/); 25 | 26 | assert.throws(() => { 27 | assertBinding( 28 | { 29 | targets: [{ target_name: "", sources: [], extra: "not allowed" }], 30 | }, 31 | true, 32 | ); 33 | }, /Unexpected property: extra/); 34 | }); 35 | 36 | it("should parse a file with no targets", () => { 37 | const input: unknown = { targets: [] }; 38 | assertBinding(input); 39 | assert(Array.isArray(input.targets)); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/node-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-node-api/node-tests", 3 | "version": "0.1.0", 4 | "description": "Harness for running the Node.js tests from https://github.com/nodejs/node/tree/main/test", 5 | "type": "commonjs", 6 | "main": "tests.generated.js", 7 | "files": [ 8 | "dist", 9 | "tests/**/*.js", 10 | "**/*.apple.node/**", 11 | "**/*.android.node/**" 12 | ], 13 | "private": true, 14 | "homepage": "https://github.com/callstackincubator/react-native-node-api", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/callstackincubator/react-native-node-api.git", 18 | "directory": "packages/node-tests" 19 | }, 20 | "scripts": { 21 | "copy-tests": "tsx scripts/copy-tests.mts", 22 | "gyp-to-cmake": "gyp-to-cmake ./tests", 23 | "build-tests": "tsx scripts/build-tests.mts", 24 | "bundle": "rolldown -c rolldown.config.mts", 25 | "generate-entrypoint": "tsx scripts/generate-entrypoint.mts", 26 | "bootstrap": "node --run copy-tests && node --run gyp-to-cmake && node --run build-tests && node --run bundle && node --run generate-entrypoint" 27 | }, 28 | "devDependencies": { 29 | "cmake-rn": "*", 30 | "gyp-to-cmake": "*", 31 | "react-native-node-api": "^0.7.0", 32 | "read-pkg": "^9.0.1", 33 | "rolldown": "1.0.0-beta.29" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | env: 4 | # Version here should match the one in React Native template and packages/cmake-rn/src/cli.ts 5 | NDK_VERSION: 27.1.12297006 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | concurrency: ${{ github.workflow }}-${{ github.ref }} 13 | 14 | jobs: 15 | release: 16 | name: Release 17 | runs-on: macos-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/jod 23 | - name: Set up JDK 17 24 | uses: actions/setup-java@v3 25 | with: 26 | java-version: "17" 27 | distribution: "temurin" 28 | - name: Setup Android SDK 29 | uses: android-actions/setup-android@v3 30 | with: 31 | packages: tools platform-tools ndk;${{ env.NDK_VERSION }} 32 | - run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi i686-linux-android aarch64-apple-ios-sim 33 | - run: npm install 34 | 35 | - name: Create Release Pull Request or Publish to npm 36 | id: changesets 37 | uses: changesets/action@v1 38 | with: 39 | publish: npm run release 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | -------------------------------------------------------------------------------- /packages/cmake-file-api/src/schemas/objects/CmakeFilesV1.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | const CmakeFilesInput = z.object({ 4 | path: z.string(), 5 | isGenerated: z.boolean().optional(), 6 | isExternal: z.boolean().optional(), 7 | isCMake: z.boolean().optional(), 8 | }); 9 | 10 | const CmakeFilesGlobDependent = z.object({ 11 | expression: z.string(), 12 | recurse: z.boolean().optional(), 13 | listDirectories: z.boolean().optional(), 14 | followSymlinks: z.boolean().optional(), 15 | relative: z.string().optional(), 16 | paths: z.array(z.string()), 17 | }); 18 | 19 | export const CmakeFilesV1_0 = z.object({ 20 | kind: z.literal("cmakeFiles"), 21 | version: z.object({ 22 | major: z.literal(1), 23 | minor: z.number().max(0), 24 | }), 25 | paths: z.object({ 26 | source: z.string(), 27 | build: z.string(), 28 | }), 29 | inputs: z.array(CmakeFilesInput), 30 | }); 31 | 32 | export const CmakeFilesV1_1 = CmakeFilesV1_0.extend({ 33 | version: z.object({ 34 | major: z.literal(1), 35 | minor: z.number().min(1), 36 | }), 37 | globsDependent: z.array(CmakeFilesGlobDependent).optional(), 38 | }); 39 | 40 | export const CmakeFilesV1 = z.union([CmakeFilesV1_0, CmakeFilesV1_1]); 41 | 42 | export const cmakeFilesSchemaPerVersion = { 43 | "1.0": CmakeFilesV1_0, 44 | "1.1": CmakeFilesV1_1, 45 | } as const satisfies Record; 46 | -------------------------------------------------------------------------------- /packages/weak-node-api/weak-node-api.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | 5 | # We need to restore symlinks in the versioned framework directories, 6 | # as these are not preserved when in the archive uploaded to NPM 7 | unless defined?(@restored) 8 | RESTORE_COMMAND = "node '#{File.join(__dir__, "dist/restore-xcframework-symlinks.js")}'" 9 | Pod::UI.info("[weak-node-api] ".green + "Restoring symbolic links in Xcframework") 10 | system(RESTORE_COMMAND) or raise "Failed to restore symlinks in Xcframework" 11 | # Setting a flag to avoid running this command on every require 12 | @restored = true 13 | end 14 | 15 | Pod::Spec.new do |s| 16 | s.name = package["name"] 17 | s.version = package["version"] 18 | s.summary = package["description"] 19 | s.homepage = package["homepage"] 20 | s.license = package["license"] 21 | s.authors = package["author"] 22 | 23 | s.source = { :git => "https://github.com/callstackincubator/react-native-node-api.git", :tag => "#{s.version}" } 24 | 25 | s.source_files = "generated/*.hpp", "include/*.h" 26 | s.public_header_files = "generated/*.hpp", "include/*.h" 27 | s.vendored_frameworks = "build/*/weak-node-api.xcframework" 28 | 29 | # Avoiding the header dir to allow for idiomatic Node-API includes 30 | s.header_dir = nil 31 | end -------------------------------------------------------------------------------- /apps/test-app/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # react-native-node-api-test-app 2 | 3 | ## 0.2.1 4 | 5 | ### Patch Changes 6 | 7 | - 7ff2c2b: Fix minor package issues. 8 | - Updated dependencies [7ff2c2b] 9 | - Updated dependencies [7ff2c2b] 10 | - weak-node-api@0.0.3 11 | - react-native-node-api@0.7.1 12 | 13 | ## 0.2.0 14 | 15 | ### Minor Changes 16 | 17 | - a0212c8: Add explicit support for React Native 0.81.1 (0.79.6, 0.80.0, 0.80.1, 0.80.2 & 0.81.0) 18 | 19 | ### Patch Changes 20 | 21 | - a0212c8: Renamed ferric-example in test app to match mono-repo packages 22 | - Updated dependencies [a0212c8] 23 | - Updated dependencies [a0212c8] 24 | - react-native-node-api@0.4.0 25 | - @react-native-node-api/node-tests@undefined 26 | - @react-native-node-api/node-addon-examples@undefined 27 | 28 | ## 0.1.2 29 | 30 | ### Patch Changes 31 | 32 | - dc33f3c: Added implementation of async work runtime functions 33 | - Updated dependencies [a477b84] 34 | - Updated dependencies [dc33f3c] 35 | - Updated dependencies [4924f66] 36 | - Updated dependencies [acf1a7c] 37 | - react-native-node-api@0.3.3 38 | - @react-native-node-api/node-tests@undefined 39 | - @react-native-node-api/node-addon-examples@undefined 40 | 41 | ## 0.1.1 42 | 43 | ### Patch Changes 44 | 45 | - 7ad62f7: Adding support for React Native 0.79.3, 0.79.4 & 0.79.5 46 | - Updated dependencies [7ad62f7] 47 | - react-native-node-api@0.3.1 48 | -------------------------------------------------------------------------------- /apps/test-app/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | apply(from: { 3 | def searchDir = rootDir.toPath() 4 | do { 5 | def p = searchDir.resolve("node_modules/react-native-test-app/android/dependencies.gradle") 6 | if (p.toFile().exists()) { 7 | return p.toRealPath().toString() 8 | } 9 | } while (searchDir = searchDir.getParent()) 10 | throw new GradleException("Could not find `react-native-test-app`"); 11 | }()) 12 | 13 | repositories { 14 | mavenCentral() 15 | google() 16 | } 17 | 18 | dependencies { 19 | getReactNativeDependencies().each { dependency -> 20 | classpath(dependency) 21 | } 22 | } 23 | } 24 | 25 | allprojects { 26 | repositories { 27 | { 28 | def searchDir = rootDir.toPath() 29 | do { 30 | def p = searchDir.resolve("node_modules/react-native/android") 31 | if (p.toFile().exists()) { 32 | maven { 33 | url(p.toRealPath().toString()) 34 | } 35 | break 36 | } 37 | } while (searchDir = searchDir.getParent()) 38 | // As of 0.80, React Native is no longer installed from npm 39 | }() 40 | mavenCentral() 41 | google() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/node-addon-examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-node-api/node-addon-examples", 3 | "version": "0.1.0", 4 | "type": "commonjs", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist", 8 | "examples/**/package.json", 9 | "examples/**/*.js", 10 | "tests/**/package.json", 11 | "tests/**/*.js", 12 | "**/*.apple.node/**", 13 | "**/*.android.node/**" 14 | ], 15 | "private": true, 16 | "homepage": "https://github.com/callstackincubator/react-native-node-api", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/callstackincubator/react-native-node-api.git", 20 | "directory": "packages/node-addon-examples" 21 | }, 22 | "scripts": { 23 | "copy-examples": "tsx scripts/copy-examples.mts", 24 | "gyp-to-cmake": "gyp-to-cmake --weak-node-api .", 25 | "build": "tsx scripts/build-examples.mts", 26 | "copy-and-build": "node --run copy-examples && node --run gyp-to-cmake && node --run build", 27 | "verify": "tsx scripts/verify-prebuilds.mts", 28 | "test": "node --run copy-and-build && node --run verify", 29 | "bootstrap": "node --run copy-and-build" 30 | }, 31 | "devDependencies": { 32 | "cmake-rn": "*", 33 | "node-addon-examples": "github:nodejs/node-addon-examples#4213d4c9d07996ae68629c67926251e117f8e52a", 34 | "gyp-to-cmake": "*", 35 | "read-pkg": "^9.0.1" 36 | }, 37 | "dependencies": { 38 | "assert": "^2.1.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/host/cpp/RuntimeNodeApi.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "node_api.h" 4 | 5 | namespace callstack::react_native_node_api { 6 | napi_status napi_create_buffer(napi_env env, size_t length, void **data, 7 | napi_value *result); 8 | 9 | napi_status napi_create_buffer_copy(napi_env env, size_t length, 10 | const void *data, void **result_data, 11 | napi_value *result); 12 | 13 | napi_status napi_is_buffer(napi_env env, napi_value value, bool *result); 14 | 15 | napi_status napi_get_buffer_info(napi_env env, napi_value value, void **data, 16 | size_t *length); 17 | 18 | napi_status 19 | napi_create_external_buffer(napi_env env, size_t length, void *data, 20 | node_api_basic_finalize basic_finalize_cb, 21 | void *finalize_hint, napi_value *result); 22 | 23 | void __attribute__((noreturn)) napi_fatal_error(const char *location, 24 | size_t location_len, 25 | const char *message, 26 | size_t message_len); 27 | 28 | napi_status napi_get_node_version(node_api_basic_env env, 29 | const napi_node_version **result); 30 | 31 | napi_status napi_get_version(node_api_basic_env env, uint32_t *result); 32 | } // namespace callstack::react_native_node_api 33 | -------------------------------------------------------------------------------- /packages/weak-node-api/README.md: -------------------------------------------------------------------------------- 1 | # Weak Node-API 2 | 3 | A clean linkable interface for Node-API and with runtime-injectable implementation. 4 | 5 | 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. 6 | 7 | ## Why is this needed? 8 | 9 | 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. 10 | 11 | This library provides a solution by: 12 | 13 | - Exposing only Node-API functions without implementation 14 | - Allowing runtime injection of the actual implementation by the host 15 | - Eliminating the need for addons to suppress undefined symbol errors 16 | 17 | ## Is this usable in the context of Node.js? 18 | 19 | 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. 20 | -------------------------------------------------------------------------------- /packages/host/cpp/CxxNodeApiHostModule.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "AddonLoaders.hpp" 8 | 9 | namespace callstack::react_native_node_api { 10 | 11 | class JSI_EXPORT CxxNodeApiHostModule : public facebook::react::TurboModule { 12 | public: 13 | static constexpr const char *kModuleName = "NodeApiHost"; 14 | 15 | CxxNodeApiHostModule(std::shared_ptr jsInvoker); 16 | 17 | static facebook::jsi::Value 18 | requireNodeAddon(facebook::jsi::Runtime &rt, 19 | facebook::react::TurboModule &turboModule, 20 | const facebook::jsi::Value args[], size_t count); 21 | facebook::jsi::Value requireNodeAddon(facebook::jsi::Runtime &rt, 22 | const facebook::jsi::String path); 23 | 24 | protected: 25 | struct NodeAddon { 26 | void *moduleHandle; 27 | napi_addon_register_func init; 28 | std::string generatedName; 29 | }; 30 | std::unordered_map nodeAddons_; 31 | std::shared_ptr callInvoker_; 32 | 33 | using LoaderPolicy = PosixLoader; // FIXME: HACK: This is temporary workaround 34 | // for my lazyness (work on iOS and Android) 35 | 36 | bool loadNodeAddon(NodeAddon &addon, const std::string &path) const; 37 | bool initializeNodeModule(facebook::jsi::Runtime &rt, NodeAddon &addon); 38 | }; 39 | 40 | } // namespace callstack::react_native_node_api 41 | -------------------------------------------------------------------------------- /packages/cmake-rn/src/headers.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from "node:module"; 2 | import path from "node:path"; 3 | import fs from "node:fs"; 4 | import assert from "node:assert/strict"; 5 | 6 | const require = createRequire(import.meta.url); 7 | 8 | /** 9 | * @returns path of the directory containing the headers which provide the Node-API C API (node_api.h and js_native_api.h) 10 | */ 11 | export function getNodeApiHeadersPath(): string { 12 | try { 13 | const packagePath = path.dirname( 14 | require.resolve("node-api-headers/package.json"), 15 | ); 16 | const result = path.join(packagePath, "include"); 17 | const stat = fs.statSync(packagePath); 18 | assert(stat.isDirectory(), `Expected ${packagePath} to be a directory`); 19 | return result; 20 | } catch (error) { 21 | throw new Error( 22 | `Failed resolve Node-API headers: Did you install the 'node-api-headers' package?`, 23 | { 24 | cause: error, 25 | }, 26 | ); 27 | } 28 | } 29 | 30 | /** 31 | * @returns path of the directory containing the headers which provide the Node-API C++ wrapper (napi.h) 32 | */ 33 | export function getNodeAddonHeadersPath(): string { 34 | try { 35 | const packagePath = path.dirname( 36 | require.resolve("node-addon-api/package.json"), 37 | ); 38 | return packagePath; 39 | } catch (error) { 40 | throw new Error( 41 | `Failed resolve Node-API addon headers: Did you install the 'node-addon-api' package?`, 42 | { 43 | cause: error, 44 | }, 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/host/scripts/patch-hermes.rb: -------------------------------------------------------------------------------- 1 | Pod::UI.warn "!!! PATCHING HERMES WITH NODE-API SUPPORT !!!" 2 | 3 | if ENV['RCT_USE_PREBUILT_RNCORE'] == '1' 4 | raise "React Native Node-API cannot reliably patch JSI when React Native Core is prebuilt." 5 | end 6 | 7 | def get_react_native_package 8 | if caller.any? { |frame| frame.include?("node_modules/react-native-macos/") } 9 | return "react-native-macos" 10 | elsif caller.any? { |frame| frame.include?("node_modules/react-native/") } 11 | return "react-native" 12 | else 13 | raise "Unable to determine React Native package from call stack." 14 | end 15 | end 16 | 17 | if ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].nil? 18 | VENDORED_HERMES_DIR ||= `npx react-native-node-api vendor-hermes --react-native-package '#{get_react_native_package()}' --silent '#{Pod::Config.instance.installation_root}'`.strip 19 | # Signal the patched Hermes to React Native 20 | ENV['BUILD_FROM_SOURCE'] = 'true' 21 | ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'] = VENDORED_HERMES_DIR 22 | elsif Dir.exist?(ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR']) 23 | # Setting an override path implies building from source 24 | ENV['BUILD_FROM_SOURCE'] = 'true' 25 | end 26 | 27 | if !ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].empty? 28 | if Dir.exist?(ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR']) 29 | Pod::UI.info "[Node-API] Using overridden Hermes in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}" 30 | else 31 | raise "Hermes patching failed: Expected override to exist in #{ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].inspect}" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /docs/AUTO-LINKING.md: -------------------------------------------------------------------------------- 1 | # Auto-linking 2 | 3 | The `react-native-node-api` package (sometimes referred to as "the host package") has mechanisms to automatically find and link prebuilt binaries with Node-API modules. 4 | 5 | When auto-linking, prebuilt binaries are copied (sometimes referred to as vendored) from dependencies of the app into the host package. As they're copied, they get renamed to avoid conflicts in naming as the library files across multiple dependency packages will be sharing a namespace when building the app. 6 | 7 | ## Naming scheme of libraries when linked into the host 8 | 9 | The name of the library when linked / copied into the host is based on two things: 10 | 11 | - The package name of the encapsulating package: The directory tree is walked from the original library path to the nearest `package.json` (this is the Node-API module's package root). 12 | - The relative path of the library to the package root: 13 | - Normalized (any "lib" prefix or file extension is stripped from the filename). 14 | - Escaped (any non-alphanumeric character is replaced with "-"). 15 | 16 | ## How do I link Node-API module libraries into my app? 17 | 18 | Linking will run when you `pod install` and as part of building your app with Gradle as long as your app has a dependency on the `react-native-node-api` package. 19 | 20 | You can also manually link by running the following in your app directory: 21 | 22 | ```bash 23 | npx react-native-node-api link --android --apple 24 | ``` 25 | 26 | > [!NOTE] 27 | > Because vendored frameworks must be present when running `pod install`, you have to run `pod install` if you add or remove a dependency with a Node-API module (or after creation if you're doing active development on it). 28 | -------------------------------------------------------------------------------- /packages/cli-utils/src/actions.ts: -------------------------------------------------------------------------------- 1 | import { SpawnFailure } from "bufout"; 2 | import chalk from "chalk"; 3 | import * as commander from "@commander-js/extra-typings"; 4 | 5 | import { UsageError } from "./errors.js"; 6 | 7 | export function wrapAction< 8 | Args extends unknown[], 9 | Opts extends commander.OptionValues, 10 | GlobalOpts extends commander.OptionValues, 11 | Command extends commander.Command, 12 | ActionArgs extends unknown[], 13 | >(fn: (this: Command, ...args: ActionArgs) => void | Promise) { 14 | return async function (this: Command, ...args: ActionArgs) { 15 | try { 16 | await fn.call(this, ...args); 17 | } catch (error) { 18 | process.exitCode = 1; 19 | if (error instanceof SpawnFailure) { 20 | error.flushOutput("both"); 21 | } else if ( 22 | error instanceof Error && 23 | error.cause instanceof SpawnFailure 24 | ) { 25 | error.cause.flushOutput("both"); 26 | } 27 | // Ensure some visual distance to the previous output 28 | console.error(); 29 | if (error instanceof UsageError || error instanceof SpawnFailure) { 30 | console.error(chalk.red("ERROR"), error.message); 31 | if (error.cause instanceof Error) { 32 | console.error(chalk.blue("CAUSE"), error.cause.message); 33 | } 34 | if (error instanceof UsageError && error.fix) { 35 | console.error( 36 | chalk.green("FIX"), 37 | error.fix.command 38 | ? chalk.dim("Run: ") + error.fix.command 39 | : error.fix.instructions, 40 | ); 41 | } 42 | } else { 43 | throw error; 44 | } 45 | } 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/host/src/node/prebuilds/triplets.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://developer.android.com/ndk/guides/other_build_systems 3 | */ 4 | export const ANDROID_TRIPLETS = [ 5 | "aarch64-linux-android", 6 | "armv7a-linux-androideabi", 7 | "i686-linux-android", 8 | "x86_64-linux-android", 9 | ] as const; 10 | 11 | export type AndroidTriplet = (typeof ANDROID_TRIPLETS)[number]; 12 | 13 | export const APPLE_TRIPLETS = [ 14 | "x86_64-apple-darwin", 15 | "arm64-apple-darwin", 16 | "arm64;x86_64-apple-darwin", 17 | 18 | "arm64-apple-ios", 19 | "x86_64-apple-ios-sim", 20 | "arm64-apple-ios-sim", 21 | "arm64;x86_64-apple-ios-sim", 22 | 23 | "arm64-apple-tvos", 24 | // "x86_64-apple-tvos", 25 | "x86_64-apple-tvos-sim", 26 | "arm64-apple-tvos-sim", 27 | "arm64;x86_64-apple-tvos-sim", 28 | 29 | "arm64-apple-visionos", 30 | "x86_64-apple-visionos-sim", 31 | "arm64-apple-visionos-sim", 32 | "arm64;x86_64-apple-visionos-sim", 33 | ] as const; 34 | 35 | export type AppleTriplet = (typeof APPLE_TRIPLETS)[number]; 36 | 37 | export const SUPPORTED_TRIPLETS = [ 38 | ...APPLE_TRIPLETS, 39 | ...ANDROID_TRIPLETS, 40 | ] as const; 41 | 42 | export type SupportedTriplet = (typeof SUPPORTED_TRIPLETS)[number]; 43 | 44 | export function isSupportedTriplet( 45 | triplet: unknown, 46 | ): triplet is SupportedTriplet { 47 | return (SUPPORTED_TRIPLETS as readonly unknown[]).includes(triplet); 48 | } 49 | 50 | export function isAndroidTriplet( 51 | triplet: SupportedTriplet, 52 | ): triplet is AndroidTriplet { 53 | return (ANDROID_TRIPLETS as readonly unknown[]).includes(triplet); 54 | } 55 | 56 | export function isAppleTriplet( 57 | triplet: SupportedTriplet, 58 | ): triplet is AppleTriplet { 59 | return (APPLE_TRIPLETS as readonly unknown[]).includes(triplet); 60 | } 61 | -------------------------------------------------------------------------------- /packages/host/src/node/cli/bin.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { describe, it } from "node:test"; 3 | import cp from "node:child_process"; 4 | import path from "node:path"; 5 | 6 | const PACKAGE_ROOT = path.join(__dirname, "../../.."); 7 | const BIN_PATH = path.join(PACKAGE_ROOT, "bin/react-native-node-api.mjs"); 8 | 9 | describe("bin", () => { 10 | describe("help command", () => { 11 | it("should succeed with a mention of usage", () => { 12 | const { status, stdout, stderr } = cp.spawnSync( 13 | process.execPath, 14 | [BIN_PATH, "help"], 15 | { 16 | cwd: PACKAGE_ROOT, 17 | encoding: "utf8", 18 | }, 19 | ); 20 | 21 | assert.equal( 22 | status, 23 | 0, 24 | `Expected success (got ${status}): ${stdout} ${stderr}`, 25 | ); 26 | assert.match( 27 | stdout, 28 | /Usage: react-native-node-api/, 29 | `Failed to find expected output (stdout: ${stdout} stderr: ${stderr})`, 30 | ); 31 | }); 32 | }); 33 | 34 | describe("link command", () => { 35 | it("should succeed with a mention of Node-API modules", () => { 36 | const { status, stdout, stderr } = cp.spawnSync( 37 | process.execPath, 38 | [BIN_PATH, "link", "--android", "--apple"], 39 | { 40 | cwd: PACKAGE_ROOT, 41 | encoding: "utf8", 42 | }, 43 | ); 44 | 45 | assert.equal( 46 | status, 47 | 0, 48 | `Expected success (got ${status}): ${stdout} ${stderr}`, 49 | ); 50 | assert.match( 51 | stdout + stderr, 52 | /Auto-linking Node-API modules/, 53 | `Failed to find expected output (stdout: ${stdout} stderr: ${stderr})`, 54 | ); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/weak-node-api/scripts/generators/weak-node-api.ts: -------------------------------------------------------------------------------- 1 | import type { FunctionDecl } from "../../src/node-api-functions.js"; 2 | import { generateFunction } from "./shared.js"; 3 | 4 | export function generateHeader() { 5 | return ` 6 | #pragma once 7 | 8 | #include 9 | #include // fprintf() 10 | #include // abort() 11 | 12 | #include "NodeApiHost.hpp" 13 | 14 | typedef void(*InjectHostFunction)(const NodeApiHost&); 15 | extern "C" void inject_weak_node_api_host(const NodeApiHost& host); 16 | `; 17 | } 18 | 19 | function generateFunctionImpl(fn: FunctionDecl) { 20 | const { name, returnType, argumentTypes } = fn; 21 | return generateFunction({ 22 | ...fn, 23 | extern: true, 24 | body: ` 25 | if (g_host.${name} == nullptr) { 26 | fprintf(stderr, "Node-API function '${name}' called before it was injected!\\n"); 27 | abort(); 28 | } 29 | ${returnType === "void" ? "" : "return "} g_host.${name}( 30 | ${argumentTypes.map((_, index) => `arg${index}`).join(", ")} 31 | ); 32 | `, 33 | }); 34 | } 35 | 36 | export function generateSource(functions: FunctionDecl[]) { 37 | return ` 38 | #include "weak_node_api.hpp" 39 | 40 | /** 41 | * @brief Global instance of the injected Node-API host. 42 | * 43 | * This variable holds the function table for Node-API calls. 44 | * It is set via inject_weak_node_api_host() before any Node-API function is dispatched. 45 | * All Node-API calls are routed through this host. 46 | */ 47 | NodeApiHost g_host; 48 | void inject_weak_node_api_host(const NodeApiHost& host) { 49 | g_host = host; 50 | }; 51 | 52 | // Generate function calling into the host 53 | ${functions.map(generateFunctionImpl).join("\n")} 54 | `; 55 | } 56 | -------------------------------------------------------------------------------- /packages/node-tests/scripts/generate-entrypoint.mts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import fs from "node:fs"; 3 | import path from "node:path"; 4 | 5 | const packageRoot = path.join(import.meta.dirname, ".."); 6 | const entrypointPath = path.join(packageRoot, "tests.generated.js"); 7 | 8 | const testPaths = fs.globSync("**/*.bundle.js", { 9 | cwd: path.join(packageRoot, "tests"), 10 | }); 11 | 12 | interface TestSuite { 13 | [key: string]: string | TestSuite; 14 | } 15 | 16 | const suites: TestSuite = {}; 17 | 18 | for (const testPath of testPaths) { 19 | const paths = testPath.split(path.sep); 20 | const testName = paths.pop(); 21 | assert(typeof testName === "string"); 22 | let parent: TestSuite = suites; 23 | for (const part of paths) { 24 | if (!parent[part]) { 25 | // Init if missing 26 | parent[part] = {}; 27 | } 28 | assert(typeof parent[part] === "object"); 29 | parent = parent[part]; 30 | } 31 | parent[path.basename(testName, ".bundle.js")] = path.join("tests", testPath); 32 | } 33 | 34 | function suiteToString(suite: TestSuite, indent = 1): string { 35 | const padding = " ".repeat(indent); 36 | return Object.entries(suite) 37 | .map(([key, value]) => { 38 | if (typeof value === "string") { 39 | return `${padding}"${key}": require("./${value}")`; 40 | } else { 41 | return `${padding}"${key}": {\n${suiteToString( 42 | value, 43 | indent + 1, 44 | )}\n${padding}}`; 45 | } 46 | }) 47 | .join(", "); 48 | } 49 | 50 | const comment = "Generated by ./scripts/generate-entrypoint.mts"; 51 | 52 | console.log( 53 | `Writing entrypoint to ${path.relative( 54 | import.meta.dirname, 55 | entrypointPath, 56 | )} for ${testPaths.length} tests ...`, 57 | ); 58 | 59 | fs.writeFileSync( 60 | entrypointPath, 61 | `/* ${comment} */\nmodule.exports.suites = {\n${suiteToString(suites)}\n};`, 62 | ); 63 | -------------------------------------------------------------------------------- /docs/ANDROID.md: -------------------------------------------------------------------------------- 1 | # Android support 2 | 3 | ## Building Hermes from source 4 | 5 | Because we're using a version of Hermes patched with Node-API support, we need to build React Native from source. 6 | 7 | Follow [the React Native documentation on how to build from source](https://reactnative.dev/contributing/how-to-build-from-source#update-your-project-to-build-from-source). 8 | 9 | In particular, you will have to edit the `android/settings.gradle` file as follows: 10 | 11 | > ```diff 12 | > // ... 13 | > include ':app' 14 | > includeBuild('../node_modules/@react-native/gradle-plugin') 15 | > 16 | > + includeBuild('../node_modules/react-native') { 17 | > + dependencySubstitution { 18 | > + substitute(module("com.facebook.react:react-android")).using(project(":packages:react-native:ReactAndroid")) 19 | > + substitute(module("com.facebook.react:react-native")).using(project(":packages:react-native:ReactAndroid")) 20 | > + substitute(module("com.facebook.react:hermes-android")).using(project(":packages:react-native:ReactAndroid:hermes-engine")) 21 | > + substitute(module("com.facebook.react:hermes-engine")).using(project(":packages:react-native:ReactAndroid:hermes-engine")) 22 | > + } 23 | > + } 24 | > ``` 25 | 26 | To download our custom version of Hermes, you need to run from your app package: 27 | 28 | ``` 29 | npx react-native-node-api vendor-hermes 30 | ``` 31 | 32 | This will print a path which needs to be stored in `REACT_NATIVE_OVERRIDE_HERMES_DIR` to instruct the React Native Gradle scripts to use it. 33 | 34 | This can be combined into a single line: 35 | 36 | ``` 37 | export REACT_NATIVE_OVERRIDE_HERMES_DIR=$(npx react-native-node-api vendor-hermes --silent) 38 | ``` 39 | 40 | ## Cleaning your React Native build folders 41 | 42 | If you've accidentally built your app without Hermes patched, you can clean things up by deleting the `ReactAndroid` build folder. 43 | 44 | ``` 45 | rm -rf node_modules/react-native/ReactAndroid/build 46 | ``` 47 | -------------------------------------------------------------------------------- /packages/weak-node-api/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.19) 2 | project(weak-node-api) 3 | 4 | # Read version from package.json 5 | file(READ "${CMAKE_CURRENT_SOURCE_DIR}/package.json" PACKAGE_JSON) 6 | string(JSON PACKAGE_VERSION GET ${PACKAGE_JSON} version) 7 | 8 | add_library(${PROJECT_NAME} SHARED) 9 | 10 | set(INCLUDE_DIR "include") 11 | set(GENERATED_SOURCE_DIR "generated") 12 | 13 | target_sources(${PROJECT_NAME} 14 | PUBLIC 15 | ${GENERATED_SOURCE_DIR}/weak_node_api.cpp 16 | PUBLIC FILE_SET HEADERS 17 | BASE_DIRS ${GENERATED_SOURCE_DIR} ${INCLUDE_DIR} FILES 18 | ${GENERATED_SOURCE_DIR}/weak_node_api.hpp 19 | ${GENERATED_SOURCE_DIR}/NodeApiHost.hpp 20 | ${INCLUDE_DIR}/js_native_api_types.h 21 | ${INCLUDE_DIR}/js_native_api.h 22 | ${INCLUDE_DIR}/node_api_types.h 23 | ${INCLUDE_DIR}/node_api.h 24 | ) 25 | 26 | get_target_property(PUBLIC_HEADER_FILES ${PROJECT_NAME} HEADER_SET) 27 | 28 | # Stripping the prefix from the library name 29 | # to make sure the name of the XCFramework will match the name of the library 30 | if(APPLE) 31 | set_target_properties(${PROJECT_NAME} PROPERTIES 32 | FRAMEWORK TRUE 33 | MACOSX_FRAMEWORK_IDENTIFIER com.callstack.${PROJECT_NAME} 34 | MACOSX_FRAMEWORK_SHORT_VERSION_STRING ${PACKAGE_VERSION} 35 | MACOSX_FRAMEWORK_BUNDLE_VERSION ${PACKAGE_VERSION} 36 | VERSION ${PACKAGE_VERSION} 37 | XCODE_ATTRIBUTE_SKIP_INSTALL NO 38 | PUBLIC_HEADER "${PUBLIC_HEADER_FILES}" 39 | ) 40 | endif() 41 | 42 | # C++20 is needed to use designated initializers 43 | target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_20) 44 | target_compile_definitions(${PROJECT_NAME} PRIVATE NAPI_VERSION=8) 45 | 46 | target_compile_options(${PROJECT_NAME} PRIVATE 47 | $<$:/W4 /WX> 48 | $<$>:-Wall -Wextra -Werror> 49 | ) 50 | 51 | option(BUILD_TESTS "Build the tests" OFF) 52 | if(BUILD_TESTS) 53 | enable_testing() 54 | add_subdirectory(tests) 55 | endif() 56 | -------------------------------------------------------------------------------- /packages/weak-node-api/weak-node-api-config.cmake: -------------------------------------------------------------------------------- 1 | 2 | # Get the current file directory 3 | get_filename_component(WEAK_NODE_API_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) 4 | 5 | if(NOT DEFINED WEAK_NODE_API_LIB) 6 | # Auto-detect library path for Android NDK builds 7 | if(ANDROID) 8 | # Define the library path pattern for Android 9 | set(WEAK_NODE_API_LIB_PATH "weak-node-api.android.node/${ANDROID_ABI}/libweak-node-api.so") 10 | 11 | # Try Debug first, then Release using the packaged Android node structure 12 | set(WEAK_NODE_API_LIB_DEBUG "${WEAK_NODE_API_CMAKE_DIR}/build/Debug/${WEAK_NODE_API_LIB_PATH}") 13 | set(WEAK_NODE_API_LIB_RELEASE "${WEAK_NODE_API_CMAKE_DIR}/build/Release/${WEAK_NODE_API_LIB_PATH}") 14 | 15 | if(EXISTS "${WEAK_NODE_API_LIB_DEBUG}") 16 | set(WEAK_NODE_API_LIB "${WEAK_NODE_API_LIB_DEBUG}") 17 | message(STATUS "Using Debug weak-node-api library: ${WEAK_NODE_API_LIB}") 18 | elseif(EXISTS "${WEAK_NODE_API_LIB_RELEASE}") 19 | set(WEAK_NODE_API_LIB "${WEAK_NODE_API_LIB_RELEASE}") 20 | message(STATUS "Using Release weak-node-api library: ${WEAK_NODE_API_LIB}") 21 | else() 22 | 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}") 23 | endif() 24 | else() 25 | message(FATAL_ERROR "WEAK_NODE_API_LIB is not set") 26 | endif() 27 | endif() 28 | 29 | if(NOT DEFINED WEAK_NODE_API_INC) 30 | set(WEAK_NODE_API_INC "${WEAK_NODE_API_CMAKE_DIR}/include;${WEAK_NODE_API_CMAKE_DIR}/generated") 31 | message(STATUS "Using weak-node-api include directories: ${WEAK_NODE_API_INC}") 32 | endif() 33 | 34 | add_library(weak-node-api SHARED IMPORTED) 35 | 36 | set_target_properties(weak-node-api PROPERTIES 37 | IMPORTED_LOCATION "${WEAK_NODE_API_LIB}" 38 | INTERFACE_INCLUDE_DIRECTORIES "${WEAK_NODE_API_INC}" 39 | ) 40 | -------------------------------------------------------------------------------- /packages/weak-node-api/src/restore-xcframework-symlinks.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import fs from "node:fs"; 3 | import path from "node:path"; 4 | 5 | import { applePrebuildPath } from "./weak-node-api.js"; 6 | 7 | async function restoreSymlink(target: string, path: string) { 8 | if (!fs.existsSync(path)) { 9 | await fs.promises.symlink(target, path); 10 | } 11 | } 12 | 13 | async function guessCurrentFrameworkVersion(frameworkPath: string) { 14 | const versionsPath = path.join(frameworkPath, "Versions"); 15 | assert(fs.existsSync(versionsPath)); 16 | 17 | const versionDirectoryEntries = await fs.promises.readdir(versionsPath, { 18 | withFileTypes: true, 19 | }); 20 | const versions = versionDirectoryEntries 21 | .filter((dirent) => dirent.isDirectory()) 22 | .map((dirent) => dirent.name); 23 | assert.equal( 24 | versions.length, 25 | 1, 26 | `Expected exactly one directory in ${versionsPath}, found ${JSON.stringify(versions)}`, 27 | ); 28 | const [version] = versions; 29 | return version; 30 | } 31 | 32 | async function restoreVersionedFrameworkSymlinks(frameworkPath: string) { 33 | const currentVersionName = await guessCurrentFrameworkVersion(frameworkPath); 34 | await restoreSymlink( 35 | currentVersionName, 36 | path.join(frameworkPath, "Versions", "Current"), 37 | ); 38 | await restoreSymlink( 39 | "Versions/Current/weak-node-api", 40 | path.join(frameworkPath, "weak-node-api"), 41 | ); 42 | await restoreSymlink( 43 | "Versions/Current/Resources", 44 | path.join(frameworkPath, "Resources"), 45 | ); 46 | await restoreSymlink( 47 | "Versions/Current/Headers", 48 | path.join(frameworkPath, "Headers"), 49 | ); 50 | } 51 | 52 | if (process.platform === "darwin") { 53 | assert( 54 | fs.existsSync(applePrebuildPath), 55 | `Expected an Xcframework at ${applePrebuildPath}`, 56 | ); 57 | 58 | const macosFrameworkPath = path.join( 59 | applePrebuildPath, 60 | "macos-arm64_x86_64", 61 | "weak-node-api.framework", 62 | ); 63 | 64 | if (fs.existsSync(macosFrameworkPath)) { 65 | await restoreVersionedFrameworkSymlinks(macosFrameworkPath); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/cmake-file-api/src/schemas/objects/CodemodelV2.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | const index = z.number().int().nonnegative(); 4 | 5 | const MinimumCMakeVersion = z.object({ 6 | string: z.string(), 7 | }); 8 | 9 | const DirectoryV2_0 = z.object({ 10 | source: z.string(), 11 | build: z.string(), 12 | parentIndex: index.optional(), 13 | childIndexes: z.array(index).optional(), 14 | projectIndex: index, 15 | targetIndexes: z.array(index).optional(), 16 | minimumCMakeVersion: MinimumCMakeVersion.optional(), 17 | hasInstallRule: z.boolean().optional(), 18 | }); 19 | 20 | const DirectoryV2_3 = DirectoryV2_0.extend({ 21 | jsonFile: z.string(), 22 | }); 23 | 24 | const Project = z.object({ 25 | name: z.string(), 26 | parentIndex: index.optional(), 27 | childIndexes: z.array(index).optional(), 28 | directoryIndexes: z.array(index), 29 | targetIndexes: z.array(index).optional(), 30 | }); 31 | 32 | const Target = z.object({ 33 | name: z.string(), 34 | id: z.string(), 35 | directoryIndex: index, 36 | projectIndex: index, 37 | jsonFile: z.string(), 38 | }); 39 | 40 | const ConfigurationV2_0 = z.object({ 41 | name: z.string(), 42 | directories: z.array(DirectoryV2_0), 43 | projects: z.array(Project), 44 | targets: z.array(Target), 45 | }); 46 | 47 | const ConfigurationV2_3 = ConfigurationV2_0.extend({ 48 | directories: z.array(DirectoryV2_3), 49 | }); 50 | 51 | export const CodemodelV2_0 = z.object({ 52 | kind: z.literal("codemodel"), 53 | version: z.object({ 54 | major: z.literal(2), 55 | minor: z.number().max(2), 56 | }), 57 | paths: z.object({ 58 | source: z.string(), 59 | build: z.string(), 60 | }), 61 | configurations: z.array(ConfigurationV2_0), 62 | }); 63 | 64 | export const CodemodelV2_3 = CodemodelV2_0.extend({ 65 | version: z.object({ 66 | major: z.literal(2), 67 | minor: z.number().min(3), 68 | }), 69 | configurations: z.array(ConfigurationV2_3), 70 | }); 71 | 72 | export const CodemodelV2 = z.union([CodemodelV2_0, CodemodelV2_3]); 73 | 74 | export const codemodelFilesSchemaPerVersion = { 75 | "2.0": CodemodelV2_0, 76 | "2.3": CodemodelV2_3, 77 | } as const satisfies Record; 78 | -------------------------------------------------------------------------------- /packages/cmake-rn/README.md: -------------------------------------------------------------------------------- 1 | # `cmake-rn` 2 | 3 | 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). 4 | 5 | Serves the same purpose as `cmake-js` does for the Node.js community and could potentially be upstreamed into `cmake-js` eventually. 6 | 7 | ## Linking against Node-API 8 | 9 | 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`. 10 | 11 | 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. 12 | 13 | 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. 14 | 15 | ```cmake 16 | cmake_minimum_required(VERSION 3.15...3.31) 17 | project(tests-buffers) 18 | 19 | # Defines the "weak-node-api" target 20 | find_package(weak-node-api REQUIRED CONFIG) 21 | 22 | add_library(addon SHARED addon.c) 23 | target_link_libraries(addon PRIVATE weak-node-api) 24 | target_compile_features(addon PRIVATE cxx_std_20) 25 | 26 | if(APPLE) 27 | # Build frameworks when building for Apple (optional) 28 | set_target_properties(addon PROPERTIES 29 | FRAMEWORK TRUE 30 | MACOSX_FRAMEWORK_IDENTIFIER async_test.addon 31 | MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 32 | MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 33 | XCODE_ATTRIBUTE_SKIP_INSTALL NO 34 | ) 35 | else() 36 | set_target_properties(addon PROPERTIES 37 | PREFIX "" 38 | SUFFIX .node 39 | ) 40 | endif() 41 | ``` 42 | 43 | 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`. 44 | -------------------------------------------------------------------------------- /packages/node-tests/scripts/copy-tests.mts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import cp from "node:child_process"; 4 | 5 | import { TESTS_DIR } from "./utils.mjs"; 6 | 7 | const NODE_REPO_URL = "https://github.com/nodejs/node.git"; 8 | const NODE_REPO_DIR = path.resolve(import.meta.dirname, "../node"); 9 | 10 | const ALLOW_LIST = [ 11 | "js-native-api/common.h", 12 | "js-native-api/common-inl.h", 13 | "js-native-api/entry_point.h", 14 | "js-native-api/2_function_arguments", 15 | // "node-api/test_async", 16 | // "node-api/test_buffer", 17 | ]; 18 | 19 | console.log("Copying files to", TESTS_DIR); 20 | 21 | // Clean up the destination directory before copying 22 | // fs.rmSync(EXAMPLES_DIR, { recursive: true, force: true }); 23 | 24 | if (!fs.existsSync(NODE_REPO_DIR)) { 25 | console.log( 26 | "Sparse and shallow cloning Node.js repository to", 27 | NODE_REPO_DIR, 28 | ); 29 | 30 | // Init a new git repository 31 | cp.execFileSync("git", ["init", NODE_REPO_DIR], { 32 | stdio: "inherit", 33 | }); 34 | // Set the remote origin to the Node.js repository 35 | cp.execFileSync("git", ["remote", "add", "origin", NODE_REPO_URL], { 36 | stdio: "inherit", 37 | cwd: NODE_REPO_DIR, 38 | }); 39 | // Enable sparse checkout 40 | cp.execFileSync( 41 | "git", 42 | ["sparse-checkout", "set", "test/js-native-api", "test/node-api"], 43 | { 44 | stdio: "inherit", 45 | cwd: NODE_REPO_DIR, 46 | }, 47 | ); 48 | // Pull the latest changes from the master branch 49 | console.log("Pulling latest changes from Node.js repository..."); 50 | cp.execFileSync("git", ["pull", "--depth=1", "origin", "main"], { 51 | stdio: "inherit", 52 | cwd: NODE_REPO_DIR, 53 | }); 54 | } 55 | const SRC_DIR = path.join(NODE_REPO_DIR, "test"); 56 | console.log("Copying files from", SRC_DIR); 57 | 58 | for (const src of ALLOW_LIST) { 59 | const srcPath = path.join(SRC_DIR, src); 60 | const destPath = path.join(TESTS_DIR, src); 61 | 62 | if (fs.existsSync(destPath)) { 63 | console.warn( 64 | `Destination path ${destPath} already exists - skipping copy of ${src}.`, 65 | ); 66 | continue; 67 | } 68 | 69 | console.log("Copying from", srcPath, "to", destPath); 70 | fs.cpSync(srcPath, destPath, { recursive: true }); 71 | } 72 | -------------------------------------------------------------------------------- /packages/host/react-native-node-api.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | 5 | require_relative "./scripts/patch-hermes" 6 | 7 | NODE_PATH ||= `which node`.strip 8 | CLI_COMMAND ||= "'#{NODE_PATH}' '#{File.join(__dir__, "dist/node/cli/run.js")}'" 9 | COPY_FRAMEWORKS_COMMAND ||= "#{CLI_COMMAND} link --apple '#{Pod::Config.instance.installation_root}'" 10 | 11 | # We need to run this now to ensure the xcframeworks are copied vendored_frameworks are considered 12 | XCFRAMEWORKS_DIR ||= File.join(__dir__, "xcframeworks") 13 | unless defined?(@xcframeworks_copied) 14 | puts "Executing #{COPY_FRAMEWORKS_COMMAND}" 15 | system(COPY_FRAMEWORKS_COMMAND) or raise "Failed to copy xcframeworks" 16 | # Setting a flag to avoid running this command on every require 17 | @xcframeworks_copied = true 18 | end 19 | 20 | if ENV['RCT_NEW_ARCH_ENABLED'] == '0' 21 | Pod::UI.warn "React Native Node-API doesn't support the legacy architecture (but RCT_NEW_ARCH_ENABLED == '0')" 22 | end 23 | 24 | Pod::Spec.new do |s| 25 | s.name = package["name"] 26 | s.version = package["version"] 27 | s.summary = package["description"] 28 | s.homepage = package["homepage"] 29 | s.license = package["license"] 30 | s.authors = package["author"] 31 | 32 | s.source = { :git => "https://github.com/callstackincubator/react-native-node-api.git", :tag => "#{s.version}" } 33 | 34 | s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}" 35 | 36 | s.dependency "weak-node-api" 37 | 38 | s.vendored_frameworks = "auto-linked/apple/*.xcframework" 39 | s.script_phase = { 40 | :name => 'Copy Node-API xcframeworks', 41 | :execution_position => :before_compile, 42 | :script => <<-CMD 43 | set -e 44 | #{COPY_FRAMEWORKS_COMMAND} 45 | CMD 46 | } 47 | 48 | # Use install_modules_dependencies helper to install the dependencies (requires React Native version >=0.71.0). 49 | # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. 50 | if respond_to?(:install_modules_dependencies, true) 51 | install_modules_dependencies(s) 52 | else 53 | raise "This version of React Native is too old for React Native Node-API." 54 | end 55 | end -------------------------------------------------------------------------------- /packages/host/src/node/cli/android.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import fs from "node:fs"; 3 | import path from "node:path"; 4 | 5 | import { getLatestMtime, getLibraryName, MAGIC_FILENAME } from "../path-utils"; 6 | import { 7 | getLinkedModuleOutputPath, 8 | LinkModuleResult, 9 | type LinkModuleOptions, 10 | } from "./link-modules"; 11 | 12 | const ANDROID_ARCHITECTURES = [ 13 | "arm64-v8a", 14 | "armeabi-v7a", 15 | "x86_64", 16 | "x86", 17 | ] as const; 18 | 19 | export async function linkAndroidDir({ 20 | incremental, 21 | modulePath, 22 | naming, 23 | platform, 24 | }: LinkModuleOptions): Promise { 25 | const libraryName = getLibraryName(modulePath, naming); 26 | const outputPath = getLinkedModuleOutputPath(platform, modulePath, naming); 27 | 28 | if (incremental && fs.existsSync(outputPath)) { 29 | const moduleModified = getLatestMtime(modulePath); 30 | const outputModified = getLatestMtime(outputPath); 31 | if (moduleModified < outputModified) { 32 | return { 33 | originalPath: modulePath, 34 | libraryName, 35 | outputPath, 36 | skipped: true, 37 | }; 38 | } 39 | } 40 | 41 | await fs.promises.rm(outputPath, { recursive: true, force: true }); 42 | await fs.promises.cp(modulePath, outputPath, { recursive: true }); 43 | for (const arch of ANDROID_ARCHITECTURES) { 44 | const archPath = path.join(outputPath, arch); 45 | if (!fs.existsSync(archPath)) { 46 | // Skip missing architectures 47 | continue; 48 | } 49 | const libraryDirents = await fs.promises.readdir(archPath, { 50 | withFileTypes: true, 51 | }); 52 | assert(libraryDirents.length === 1, "Expected exactly one library file"); 53 | const [libraryDirent] = libraryDirents; 54 | assert(libraryDirent.isFile(), "Expected a library file"); 55 | const libraryPath = path.join(libraryDirent.parentPath, libraryDirent.name); 56 | await fs.promises.rename( 57 | libraryPath, 58 | path.join(archPath, `lib${libraryName}.so`), 59 | ); 60 | } 61 | await fs.promises.rm(path.join(outputPath, MAGIC_FILENAME), { 62 | recursive: true, 63 | }); 64 | 65 | // TODO: Update the DT_NEEDED entry in the .so files 66 | 67 | return { 68 | originalPath: modulePath, 69 | outputPath, 70 | libraryName, 71 | skipped: false, 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /packages/host/cpp/Logger.cpp: -------------------------------------------------------------------------------- 1 | #include "Logger.hpp" 2 | #include 3 | #include 4 | 5 | #if defined(__ANDROID__) 6 | #include 7 | #define LOG_TAG "NodeApiHost" 8 | #elif defined(__APPLE__) 9 | #include 10 | #endif 11 | 12 | namespace { 13 | constexpr auto LineFormat = "[%s] [NodeApiHost] "; 14 | 15 | enum class LogLevel { Debug, Warning, Error }; 16 | 17 | constexpr std::string_view levelToString(LogLevel level) { 18 | switch (level) { 19 | case LogLevel::Debug: 20 | return "DEBUG"; 21 | case LogLevel::Warning: 22 | return "WARNING"; 23 | case LogLevel::Error: 24 | return "ERROR"; 25 | default: 26 | return "UNKNOWN"; 27 | } 28 | } 29 | 30 | #if defined(__ANDROID__) 31 | constexpr int androidLogLevel(LogLevel level) { 32 | switch (level) { 33 | case LogLevel::Debug: 34 | return ANDROID_LOG_DEBUG; 35 | case LogLevel::Warning: 36 | return ANDROID_LOG_WARN; 37 | case LogLevel::Error: 38 | return ANDROID_LOG_ERROR; 39 | default: 40 | return ANDROID_LOG_UNKNOWN; 41 | } 42 | } 43 | #endif 44 | 45 | void log_message_internal(LogLevel level, const char *format, va_list args) { 46 | #if defined(__ANDROID__) 47 | __android_log_vprint(androidLogLevel(level), LOG_TAG, format, args); 48 | #elif defined(__APPLE__) 49 | // iOS or macOS 50 | const auto level_str = levelToString(level); 51 | fprintf(stderr, LineFormat, level_str.data()); 52 | vfprintf(stderr, format, args); 53 | fprintf(stderr, "\n"); 54 | #else 55 | // Fallback for other platforms 56 | const auto level_str = levelToString(level); 57 | fprintf(stdout, LineFormat, level_str.data()); 58 | vfprintf(stdout, format, args); 59 | fprintf(stdout, "\n"); 60 | #endif 61 | } 62 | } // anonymous namespace 63 | 64 | namespace callstack::react_native_node_api { 65 | 66 | void log_debug(const char *format, ...) { 67 | // TODO: Disable logging in release builds 68 | va_list args; 69 | va_start(args, format); 70 | log_message_internal(LogLevel::Debug, format, args); 71 | va_end(args); 72 | } 73 | void log_warning(const char *format, ...) { 74 | va_list args; 75 | va_start(args, format); 76 | log_message_internal(LogLevel::Warning, format, args); 77 | va_end(args); 78 | } 79 | void log_error(const char *format, ...) { 80 | va_list args; 81 | va_start(args, format); 82 | log_message_internal(LogLevel::Error, format, args); 83 | va_end(args); 84 | } 85 | } // namespace callstack::react_native_node_api 86 | -------------------------------------------------------------------------------- /packages/weak-node-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weak-node-api", 3 | "version": "0.0.3", 4 | "description": "A linkable and runtime-injectable Node-API", 5 | "homepage": "https://github.com/callstackincubator/react-native-node-api", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/callstackincubator/react-native-node-api.git", 9 | "directory": "packages/weak-node-api" 10 | }, 11 | "type": "module", 12 | "exports": { 13 | ".": "./dist/index.js" 14 | }, 15 | "files": [ 16 | "dist", 17 | "!dist/**/*.test.d.ts", 18 | "!dist/**/*.test.d.ts.map", 19 | "include", 20 | "generated", 21 | "build/Debug", 22 | "build/Release", 23 | "*.podspec", 24 | "*.cmake" 25 | ], 26 | "scripts": { 27 | "build": "tsc --build", 28 | "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", 29 | "generate": "tsx scripts/generate.ts", 30 | "prebuild:prepare": "node --run copy-node-api-headers && node --run generate", 31 | "prebuild:build": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension", 32 | "prebuild:build:android": "node --run prebuild:build -- --android", 33 | "prebuild:build:apple": "node --run prebuild:build -- --apple", 34 | "prebuild:build:all": "node --run prebuild:build -- --android --apple", 35 | "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", 36 | "test:configure": "cmake -S . -B build-tests -DBUILD_TESTS=ON", 37 | "test:build": "cmake --build build-tests", 38 | "test:run": "ctest --test-dir build-tests --output-on-failure", 39 | "bootstrap": "node --run prebuild:prepare && node --run prebuild:build" 40 | }, 41 | "keywords": [ 42 | "react-native", 43 | "napi", 44 | "node-api", 45 | "node-addon-api", 46 | "native", 47 | "addon", 48 | "module", 49 | "c", 50 | "c++", 51 | "bindings", 52 | "buildtools", 53 | "cmake" 54 | ], 55 | "author": { 56 | "name": "Callstack", 57 | "url": "https://github.com/callstackincubator" 58 | }, 59 | "contributors": [ 60 | { 61 | "name": "Kræn Hansen", 62 | "url": "https://github.com/kraenhansen" 63 | } 64 | ], 65 | "license": "MIT", 66 | "dependencies": { 67 | "node-api-headers": "^1.5.0" 68 | }, 69 | "devDependencies": { 70 | "zod": "^4.1.11" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/ferric/src/napi-rs.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import fs from "node:fs"; 3 | import path from "node:path"; 4 | 5 | import { NapiCli } from "@napi-rs/cli"; 6 | 7 | const napiCli = new NapiCli(); 8 | 9 | import { getBlockComment } from "./banner.js"; 10 | 11 | const PACKAGE_ROOT = path.join(import.meta.dirname, ".."); 12 | 13 | type TypeScriptDeclarationsOptions = { 14 | /** 15 | * Path to the directory containing the Cargo.toml file. 16 | */ 17 | createPath: string; 18 | /** 19 | * Path to the output directory where the TypeScript declarations will be copied into. 20 | */ 21 | outputPath: string; 22 | /** 23 | * File name of the generated TypeScript declarations (including .d.ts). 24 | */ 25 | outputFilename: string; 26 | }; 27 | 28 | export async function generateTypeScriptDeclarations({ 29 | createPath, 30 | outputPath, 31 | outputFilename, 32 | }: TypeScriptDeclarationsOptions) { 33 | // Using a temporary directory to avoid polluting crate with any other side-effects for generating TypeScript declarations 34 | const tempPath = fs.realpathSync( 35 | fs.mkdtempSync(path.join(PACKAGE_ROOT, "dts-tmp-")), 36 | ); 37 | const finalOutputPath = path.join(outputPath, outputFilename); 38 | try { 39 | // Write a dummy package.json file to avoid errors from napi-rs 40 | await fs.promises.writeFile( 41 | path.join(tempPath, "package.json"), 42 | "{}", 43 | "utf8", 44 | ); 45 | const tempOutputPath = path.join(tempPath, outputFilename); 46 | // Call into napi.rs to generate TypeScript declarations 47 | const { task } = await napiCli.build({ 48 | verbose: false, 49 | dts: outputFilename, 50 | outputDir: tempPath, 51 | cwd: createPath, 52 | cargoOptions: ["--quiet"], 53 | }); 54 | await task; 55 | // Override the banner 56 | assert( 57 | fs.existsSync(tempOutputPath), 58 | `Expected napi.rs to emit ${tempOutputPath}`, 59 | ); 60 | const contents = await fs.promises.readFile(tempOutputPath, "utf8"); 61 | const patchedContents = contents.replace( 62 | "/* auto-generated by NAPI-RS */", 63 | getBlockComment(), 64 | ); 65 | // Copy out the generated TypeScript declarations 66 | await fs.promises.writeFile(finalOutputPath, patchedContents, { 67 | encoding: "utf8", 68 | }); 69 | return finalOutputPath; 70 | } finally { 71 | await fs.promises.rm(tempPath, { recursive: true, force: true }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /docs/PREBUILDS.md: -------------------------------------------------------------------------------- 1 | # Prebuilds 2 | 3 | This document codifies the naming and directory structure of prebuilt binaries, expected by the auto-linking mechanism. 4 | 5 | At the time of writing, our auto-linking host package (`react-native-node-api`) support two kinds of prebuilds: 6 | 7 | ## `*.android.node` (for Android) 8 | 9 | A jniLibs-like directory structure of CPU-architecture specific directories containing a single `.so` library file. 10 | 11 | The name of all the `.so` library files: 12 | 13 | - must be the same across all CPU-architectures 14 | - can have a "lib" prefix, but doesn't have to 15 | - must have an `.so` or `.node` file extension 16 | 17 | > [!NOTE] 18 | > The `SONAME` doesn't have to match and is not updated as the .so is copied into the host package. 19 | > This might cause trouble if you're trying to link with the library from other native code. 20 | > We're tracking [#14](https://github.com/callstackincubator/react-native-node-api/issues/14) to fix this 🤞 21 | 22 | The directory must have a `react-native-node-api-module` file (the content doesn't matter), to signal that the directory is intended for auto-linking by the `react-native-node-api-module` package. 23 | 24 | ## `*.apple.node` (for Apple) 25 | 26 | An XCFramework of dynamic libraries wrapped in `.framework` bundles, renamed from `.xcframework` to `.apple.node` to ease discoverability. 27 | 28 | The Apple Developer documentation on ["Creating a multiplatform binary framework bundle"](https://developer.apple.com/documentation/xcode/creating-a-multi-platform-binary-framework-bundle#Avoid-issues-when-using-alternate-build-systems) mentions: 29 | 30 | > An XCFramework can include dynamic library files, but only macOS supports these libraries for dynamic linking. Dynamic linking on iOS, watchOS, and tvOS requires the XCFramework to contain .framework bundles. 31 | 32 | The directory must have a `react-native-node-api-module` file (the content doesn't matter), to signal that the directory is intended for auto-linking by the `react-native-node-api-module` package. 33 | 34 | ## Why did we choose this naming scheme? 35 | 36 | To align with prior art and established patterns around the distribution of Node-API modules for Node.js, we've chosen to use the ".node" filename extension for prebuilds of Node-API modules, targeting React Native. 37 | 38 | To enable distribution of packages with multiple co-existing platform-specific prebuilts, we've chosen to lean into the pattern of platform-specific filename extensions, used by the Metro bundler. 39 | -------------------------------------------------------------------------------- /apps/test-app/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the Gradle Daemon. The setting is 11 | # particularly useful for configuring JVM memory settings for build performance. 12 | # This does not affect the JVM settings for the Gradle client VM. 13 | # The default is `-Xmx512m -XX:MaxMetaspaceSize=256m`. 14 | org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 15 | 16 | # When configured, Gradle will fork up to org.gradle.workers.max JVMs to execute 17 | # projects in parallel. To learn more about parallel task execution, see the 18 | # section on Gradle build performance: 19 | # https://docs.gradle.org/current/userguide/performance.html#parallel_execution. 20 | # Default is `false`. 21 | #org.gradle.parallel=true 22 | 23 | # AndroidX package structure to make it clearer which packages are bundled with the 24 | # Android operating system, and which are packaged with your app's APK 25 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 26 | android.useAndroidX=true 27 | # Automatically convert third-party libraries to use AndroidX 28 | #android.enableJetifier=true 29 | # Jetifier randomly fails on these libraries 30 | #android.jetifier.ignorelist=hermes-android,react-android 31 | 32 | # Use this property to specify which architecture you want to build. 33 | # You can also override it from the CLI using 34 | # ./gradlew -PreactNativeArchitectures=x86_64 35 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 36 | 37 | # Use this property to enable support to the new architecture. 38 | # This will allow you to use TurboModules and the Fabric render in 39 | # your application. You should enable this flag either if you want 40 | # to write custom TurboModules/Fabric components OR use libraries that 41 | # are providing them. 42 | # Note that this is incompatible with web debugging. 43 | newArchEnabled=true 44 | #bridgelessEnabled=true 45 | 46 | # Uncomment the line below to build React Native from source. 47 | react.buildFromSource=true 48 | 49 | # Version of Android NDK to build against. 50 | #ANDROID_NDK_VERSION=26.1.10909125 51 | 52 | # Version of Kotlin to build against. 53 | #KOTLIN_VERSION=1.8.22 -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { globalIgnores } from "eslint/config"; 4 | import globals from "globals"; 5 | import eslint from "@eslint/js"; 6 | import tseslint from "typescript-eslint"; 7 | import eslintConfigPrettier from "eslint-config-prettier/flat"; 8 | 9 | export default tseslint.config( 10 | globalIgnores([ 11 | "**/dist/**", 12 | "**/build/**", 13 | "apps/test-app/ios/**", 14 | "packages/host/hermes/**", 15 | "packages/node-addon-examples/examples/**", 16 | "packages/ferric-example/ferric_example.js", 17 | "packages/ferric-example/ferric_example.d.ts", 18 | "packages/ferric-example/target/**", 19 | "packages/node-tests/node/**", 20 | "packages/node-tests/tests/**", 21 | "packages/node-tests/*.generated.js", 22 | "packages/node-tests/*.generated.d.ts", 23 | ]), 24 | eslint.configs.recommended, 25 | tseslint.configs.recommendedTypeChecked, 26 | { 27 | rules: { 28 | "@typescript-eslint/no-floating-promises": [ 29 | "error", 30 | { 31 | allowForKnownSafeCalls: [ 32 | { from: "package", name: ["suite", "test"], package: "node:test" }, 33 | ], 34 | }, 35 | ], 36 | }, 37 | }, 38 | { 39 | languageOptions: { 40 | parserOptions: { 41 | projectService: true, 42 | tsconfigRootDir: import.meta.dirname, 43 | }, 44 | }, 45 | }, 46 | eslintConfigPrettier, 47 | { 48 | files: [ 49 | "apps/test-app/*.js", 50 | "apps/macos-test-app/*.js", 51 | "packages/node-addon-examples/**/*.js", 52 | "packages/host/babel-plugin.js", 53 | "packages/host/react-native.config.js", 54 | "packages/node-tests/tests.generated.js", 55 | ], 56 | extends: [tseslint.configs.disableTypeChecked], 57 | languageOptions: { 58 | parserOptions: { 59 | sourceType: "commonjs", 60 | }, 61 | globals: { 62 | ...globals.commonjs, 63 | }, 64 | }, 65 | rules: { 66 | // We're using CommonJS here for Node.js backwards compatibility 67 | "@typescript-eslint/no-require-imports": "off", 68 | }, 69 | }, 70 | { 71 | files: [ 72 | "**/metro.config.js", 73 | "packages/gyp-to-cmake/bin/*.js", 74 | "packages/host/bin/*.mjs", 75 | "packages/host/scripts/*.mjs", 76 | "packages/ferric/bin/*.js", 77 | "packages/cmake-rn/bin/*.js", 78 | ], 79 | extends: [tseslint.configs.disableTypeChecked], 80 | languageOptions: { 81 | globals: { 82 | ...globals.node, 83 | }, 84 | }, 85 | }, 86 | ); 87 | -------------------------------------------------------------------------------- /packages/cmake-rn/src/weak-node-api.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import assert from "node:assert/strict"; 3 | import path from "node:path"; 4 | 5 | import { 6 | isAndroidTriplet, 7 | isAppleTriplet, 8 | SupportedTriplet, 9 | } from "react-native-node-api"; 10 | 11 | import { 12 | applePrebuildPath, 13 | androidPrebuildPath, 14 | weakNodeApiCmakePath, 15 | } from "weak-node-api"; 16 | 17 | import { ANDROID_ARCHITECTURES } from "./platforms/android.js"; 18 | import { getNodeAddonHeadersPath, getNodeApiHeadersPath } from "./headers.js"; 19 | 20 | export function toCmakePath(input: string) { 21 | return input.split(path.win32.sep).join(path.posix.sep); 22 | } 23 | 24 | export function getWeakNodeApiPath( 25 | triplet: SupportedTriplet | "apple", 26 | ): string { 27 | if (triplet === "apple" || isAppleTriplet(triplet)) { 28 | assert( 29 | fs.existsSync(applePrebuildPath), 30 | `Expected an XCFramework at ${applePrebuildPath}`, 31 | ); 32 | return applePrebuildPath; 33 | } else if (isAndroidTriplet(triplet)) { 34 | const libraryPath = path.join( 35 | androidPrebuildPath, 36 | ANDROID_ARCHITECTURES[triplet], 37 | "libweak-node-api.so", 38 | ); 39 | assert(fs.existsSync(libraryPath), `Expected library at ${libraryPath}`); 40 | return libraryPath; 41 | } else { 42 | throw new Error(`Unexpected triplet: ${triplet as string}`); 43 | } 44 | } 45 | 46 | function getNodeApiIncludePaths() { 47 | const includePaths = [getNodeApiHeadersPath(), getNodeAddonHeadersPath()]; 48 | for (const includePath of includePaths) { 49 | assert( 50 | !includePath.includes(";"), 51 | `Include path with a ';' is not supported: ${includePath}`, 52 | ); 53 | } 54 | return includePaths; 55 | } 56 | 57 | export function getWeakNodeApiVariables( 58 | triplet: SupportedTriplet | "apple", 59 | ): Record { 60 | return { 61 | // Enable use of `find_package(weak-node-api REQUIRED CONFIG)` 62 | "weak-node-api_DIR": path.dirname(weakNodeApiCmakePath), 63 | // Enable use of `include(${WEAK_NODE_API_CONFIG})` 64 | WEAK_NODE_API_CONFIG: weakNodeApiCmakePath, 65 | WEAK_NODE_API_INC: getNodeApiIncludePaths().join(";"), 66 | WEAK_NODE_API_LIB: getWeakNodeApiPath(triplet), 67 | }; 68 | } 69 | 70 | /** 71 | * For compatibility with cmake-js 72 | */ 73 | export function getCmakeJSVariables( 74 | triplet: SupportedTriplet | "apple", 75 | ): Record { 76 | return { 77 | CMAKE_JS_INC: getNodeApiIncludePaths().join(";"), 78 | CMAKE_JS_LIB: getWeakNodeApiPath(triplet), 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /apps/test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-node-api/test-app", 3 | "private": true, 4 | "type": "commonjs", 5 | "version": "0.2.1", 6 | "scripts": { 7 | "metro": "react-native start --no-interactive", 8 | "android": "react-native run-android --no-packager --active-arch-only", 9 | "ios": "react-native run-ios --no-packager", 10 | "pod-install": "cd ios && pod install", 11 | "mocha-and-metro": "mocha-remote --watch -- react-native start", 12 | "test:android": "mocha-remote --exit-on-error -- concurrently --kill-others-on-fail --passthrough-arguments npm:metro 'npm:android -- {@}' --", 13 | "test:android:allTests": "MOCHA_REMOTE_CONTEXT=allTests node --run test:android -- ", 14 | "test:android:nodeAddonExamples": "MOCHA_REMOTE_CONTEXT=nodeAddonExamples node --run test:android -- ", 15 | "test:android:nodeTests": "MOCHA_REMOTE_CONTEXT=nodeTests node --run test:android -- ", 16 | "test:android:ferricExample": "MOCHA_REMOTE_CONTEXT=ferricExample node --run test:android -- ", 17 | "test:ios": "mocha-remote --exit-on-error -- concurrently --passthrough-arguments --kill-others-on-fail npm:metro 'npm:ios -- {@}' --", 18 | "test:ios:allTests": "MOCHA_REMOTE_CONTEXT=allTests node --run test:ios -- ", 19 | "test:ios:nodeAddonExamples": "MOCHA_REMOTE_CONTEXT=nodeAddonExamples node --run test:ios -- ", 20 | "test:ios:nodeTests": "MOCHA_REMOTE_CONTEXT=nodeTests node --run test:ios -- ", 21 | "test:ios:ferricExample": "MOCHA_REMOTE_CONTEXT=ferricExample node --run test:ios -- " 22 | }, 23 | "dependencies": { 24 | "@babel/core": "^7.26.10", 25 | "@babel/preset-env": "^7.26.9", 26 | "@babel/runtime": "^7.27.0", 27 | "@react-native-community/cli": "^20.0.2", 28 | "@react-native-community/cli-platform-android": "^20.0.2", 29 | "@react-native-community/cli-platform-ios": "^20.0.2", 30 | "@react-native-node-api/ferric-example": "*", 31 | "@react-native-node-api/node-addon-examples": "*", 32 | "@react-native-node-api/node-tests": "*", 33 | "@react-native/babel-preset": "0.81.4", 34 | "@react-native/metro-config": "0.81.4", 35 | "@react-native/typescript-config": "0.81.4", 36 | "@rnx-kit/metro-config": "^2.1.1", 37 | "@types/mocha": "^10.0.10", 38 | "@types/react": "^19.1.0", 39 | "concurrently": "^9.1.2", 40 | "mocha": "^11.6.0", 41 | "mocha-remote-cli": "^1.13.2", 42 | "mocha-remote-react-native": "^1.13.2", 43 | "react": "19.1.0", 44 | "react-native": "0.81.4", 45 | "react-native-node-api": "*", 46 | "react-native-test-app": "^4.4.7", 47 | "weak-node-api": "*" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/host/src/node/gradle.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import { describe, it } from "node:test"; 3 | import cp from "node:child_process"; 4 | import path from "node:path"; 5 | 6 | const PACKAGE_ROOT = path.join(__dirname, "../.."); 7 | const MONOREPO_ROOT = path.join(PACKAGE_ROOT, "../.."); 8 | const TEST_APP_ANDROID_PATH = path.join(MONOREPO_ROOT, "apps/test-app/android"); 9 | 10 | describe( 11 | "Gradle tasks", 12 | // Skipping these tests by default, as they download a lot and takes a long time 13 | { skip: process.env.ENABLE_GRADLE_TESTS !== "true" }, 14 | () => { 15 | describe("linkNodeApiModules task", () => { 16 | it("should fail if REACT_NATIVE_OVERRIDE_HERMES_DIR is not set", () => { 17 | const { status, stdout, stderr } = cp.spawnSync( 18 | "sh", 19 | ["gradlew", "react-native-node-api:linkNodeApiModules"], 20 | { 21 | cwd: TEST_APP_ANDROID_PATH, 22 | env: { 23 | ...process.env, 24 | REACT_NATIVE_OVERRIDE_HERMES_DIR: undefined, 25 | }, 26 | encoding: "utf-8", 27 | }, 28 | ); 29 | 30 | assert.notEqual(status, 0, `Expected failure: ${stdout} ${stderr}`); 31 | assert.match( 32 | stderr, 33 | /React Native Node-API needs a custom version of Hermes with Node-API enabled/, 34 | ); 35 | assert.match( 36 | stderr, 37 | /Run the following in your Bash- or Zsh-compatible terminal, to clone Hermes and instruct React Native to use it/, 38 | ); 39 | assert.match( 40 | stderr, 41 | /export REACT_NATIVE_OVERRIDE_HERMES_DIR=\$\(npx react-native-node-api vendor-hermes --silent\)/, 42 | ); 43 | assert.match( 44 | stderr, 45 | /And follow this guide to build React Native from source/, 46 | ); 47 | }); 48 | 49 | it("should call the CLI to autolink", () => { 50 | const { status, stdout, stderr } = cp.spawnSync( 51 | "sh", 52 | ["gradlew", "react-native-node-api:linkNodeApiModules"], 53 | { 54 | cwd: TEST_APP_ANDROID_PATH, 55 | env: { 56 | ...process.env, 57 | // We're passing some directory which exists 58 | REACT_NATIVE_OVERRIDE_HERMES_DIR: __dirname, 59 | }, 60 | encoding: "utf-8", 61 | }, 62 | ); 63 | 64 | assert.equal(status, 0, `Expected success: ${stdout} ${stderr}`); 65 | assert.match(stdout, /Auto-linking Node-API modules/); 66 | }); 67 | }); 68 | }, 69 | ); 70 | -------------------------------------------------------------------------------- /packages/cmake-rn/src/platforms/types.ts: -------------------------------------------------------------------------------- 1 | import * as cli from "@react-native-node-api/cli-utils"; 2 | 3 | import type { program } from "../cli.js"; 4 | 5 | type InferOptionValues = ReturnType< 6 | Command["opts"] 7 | >; 8 | 9 | type BaseCommand = typeof program; 10 | type ExtendedCommand = cli.Command< 11 | [], 12 | Opts & InferOptionValues, 13 | Record // Global opts are not supported 14 | >; 15 | 16 | export type BaseOpts = Omit, "triplet">; 17 | 18 | export type TripletContext = { 19 | triplet: Triplet; 20 | /** 21 | * Spawn a command in the context of this triplet 22 | */ 23 | spawn: Spawn; 24 | }; 25 | 26 | export type Spawn = ( 27 | command: string, 28 | args: string[], 29 | cwd?: string, 30 | ) => Promise; 31 | 32 | export type Platform< 33 | Triplets extends string[] = string[], 34 | Opts extends cli.OptionValues = Record, 35 | Command = ExtendedCommand, 36 | Triplet extends string = Triplets[number], 37 | > = { 38 | /** 39 | * Used to identify the platform in the CLI. 40 | */ 41 | id: string; 42 | /** 43 | * Name of the platform, used for display purposes. 44 | */ 45 | name: string; 46 | /** 47 | * All the triplets supported by this platform. 48 | */ 49 | triplets: Readonly; 50 | /** 51 | * Get the limited subset of triplets that should be built by default for this platform. 52 | */ 53 | defaultTriplets( 54 | mode: "current-development" | "all", 55 | ): Triplet[] | Promise; 56 | /** 57 | * Implement this to add any platform specific options to the command. 58 | */ 59 | amendCommand(command: BaseCommand): Command; 60 | /** 61 | * Check if the platform is supported by the host system, running the build. 62 | */ 63 | isSupportedByHost(): boolean | Promise; 64 | /** 65 | * Configure all projects for this platform. 66 | */ 67 | configure( 68 | triplets: TripletContext[], 69 | options: BaseOpts & Opts, 70 | spawn: Spawn, 71 | ): Promise; 72 | /** 73 | * Platform specific command to build a triplet project. 74 | */ 75 | build( 76 | context: TripletContext, 77 | options: BaseOpts & Opts, 78 | ): Promise; 79 | /** 80 | * Called to combine multiple triplets into a single prebuilt artefact. 81 | */ 82 | postBuild( 83 | /** 84 | * Location of the final prebuilt artefact. 85 | */ 86 | outputPath: string, 87 | triplets: TripletContext[], 88 | options: BaseOpts & Opts, 89 | ): Promise; 90 | }; 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-native-node-api/root", 3 | "description": "Node-API Modules for React Native", 4 | "type": "module", 5 | "private": true, 6 | "workspaces": [ 7 | "packages/cli-utils", 8 | "packages/cmake-file-api", 9 | "packages/weak-node-api", 10 | "packages/cmake-rn", 11 | "packages/ferric", 12 | "packages/gyp-to-cmake", 13 | "packages/host", 14 | "packages/node-addon-examples", 15 | "packages/node-tests", 16 | "packages/ferric-example", 17 | "apps/test-app" 18 | ], 19 | "homepage": "https://github.com/callstackincubator/react-native-node-api#readme", 20 | "scripts": { 21 | "build": "tsc --build", 22 | "clean": "tsc --build --clean && git clean -fdx -e node_modules", 23 | "dev": "tsc --build --watch", 24 | "lint": "eslint .", 25 | "depcheck": "node scripts/depcheck.ts", 26 | "publint": "node scripts/run-in-published.ts npx publint --strict", 27 | "prettier:check": "prettier --experimental-cli --check .", 28 | "prettier:write": "prettier --experimental-cli --write .", 29 | "test": "npm test --workspace react-native-node-api --workspace cmake-rn --workspace gyp-to-cmake --workspace node-addon-examples", 30 | "bootstrap": "node --run build && npm run bootstrap --workspaces --if-present", 31 | "changeset": "changeset", 32 | "release": "changeset publish", 33 | "init-macos-test-app": "node scripts/init-macos-test-app.ts" 34 | }, 35 | "author": { 36 | "name": "Callstack", 37 | "url": "https://github.com/callstackincubator" 38 | }, 39 | "contributors": [ 40 | { 41 | "name": "Kræn Hansen", 42 | "url": "https://github.com/kraenhansen" 43 | }, 44 | { 45 | "name": "Jamie Birch", 46 | "url": "https://github.com/shirakaba" 47 | }, 48 | { 49 | "name": "Mariusz Pasiński", 50 | "url": "https://github.com/mani3xis" 51 | }, 52 | { 53 | "name": "Kamil Paradowski", 54 | "url": "https://github.com/paradowstack" 55 | } 56 | ], 57 | "license": "MIT", 58 | "devDependencies": { 59 | "@changesets/cli": "^2.29.5", 60 | "@eslint/js": "^9.32.0", 61 | "@prettier/plugin-oxc": "^0.0.4", 62 | "@reporters/github": "^1.7.2", 63 | "@tsconfig/node22": "^22.0.0", 64 | "@tsconfig/react-native": "3.0.6", 65 | "@types/node": "^22", 66 | "depcheck": "^1.4.7", 67 | "eslint": "^9.32.0", 68 | "eslint-config-prettier": "^10.1.8", 69 | "globals": "^16.0.0", 70 | "prettier": "^3.6.2", 71 | "publint": "^0.3.15", 72 | "react-native": "0.81.4", 73 | "read-pkg": "^9.0.1", 74 | "tsx": "^4.20.6", 75 | "typescript": "^5.8.0", 76 | "typescript-eslint": "^8.38.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/host/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-node-api", 3 | "version": "0.7.1", 4 | "description": "Node-API for React Native", 5 | "homepage": "https://github.com/callstackincubator/react-native-node-api", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/callstackincubator/react-native-node-api.git", 9 | "directory": "packages/host" 10 | }, 11 | "main": "dist/react-native/index.js", 12 | "type": "commonjs", 13 | "bin": { 14 | "react-native-node-api": "./bin/react-native-node-api.mjs" 15 | }, 16 | "exports": { 17 | ".": { 18 | "node": "./dist/node/index.js", 19 | "react-native": "./dist/react-native/index.js" 20 | }, 21 | "./babel-plugin": "./dist/node/babel-plugin/index.js", 22 | "./cli": "./dist/node/cli/run.js" 23 | }, 24 | "files": [ 25 | "logo.svg", 26 | "bin", 27 | "dist", 28 | "!dist/**/*.test.d.ts", 29 | "!dist/**/*.test.d.ts.map", 30 | "cpp", 31 | "android", 32 | "!android/.cxx", 33 | "!android/build", 34 | "apple", 35 | "include", 36 | "babel-plugin.js", 37 | "scripts/patch-hermes.rb", 38 | "weak-node-api/**", 39 | "!weak-node-api/build/", 40 | "*.js", 41 | "*.podspec" 42 | ], 43 | "scripts": { 44 | "build": "tsc --build", 45 | "injector:generate": "node scripts/generate-injector.mts", 46 | "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", 47 | "test:gradle": "ENABLE_GRADLE_TESTS=true node --run test", 48 | "bootstrap": "node --run injector:generate" 49 | }, 50 | "keywords": [ 51 | "node-api", 52 | "napi", 53 | "node-addon-api", 54 | "native", 55 | "addon" 56 | ], 57 | "author": { 58 | "name": "Callstack", 59 | "url": "https://github.com/callstackincubator" 60 | }, 61 | "contributors": [ 62 | { 63 | "name": "Kræn Hansen", 64 | "url": "https://github.com/kraenhansen" 65 | } 66 | ], 67 | "license": "MIT", 68 | "dependencies": { 69 | "@expo/plist": "^0.4.7", 70 | "@react-native-node-api/cli-utils": "0.1.2", 71 | "pkg-dir": "^8.0.0", 72 | "read-pkg": "^9.0.1", 73 | "zod": "^4.1.11" 74 | }, 75 | "devDependencies": { 76 | "@babel/core": "^7.26.10", 77 | "@babel/types": "^7.27.0", 78 | "fswin": "^3.24.829" 79 | }, 80 | "peerDependencies": { 81 | "@babel/core": "^7.26.10", 82 | "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", 83 | "weak-node-api": "0.0.3" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/gyp-to-cmake/src/gyp.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import fs from "node:fs"; 3 | 4 | import { parse } from "gyp-parser"; 5 | 6 | export type GypTarget = { 7 | target_name: string; 8 | sources: string[]; 9 | include_dirs?: string[]; 10 | defines?: string[]; 11 | }; 12 | 13 | export type GypBinding = { 14 | targets: GypTarget[]; 15 | }; 16 | 17 | function assertNoExtraProperties( 18 | input: T, 19 | expectedKeys: string[], 20 | ) { 21 | for (const key of Object.keys(input)) { 22 | if (!expectedKeys.includes(key)) { 23 | throw new Error(`Unexpected property: ${key}`); 24 | } 25 | } 26 | } 27 | 28 | export function assertTarget( 29 | target: unknown, 30 | disallowUnknownProperties = false, 31 | ): asserts target is GypTarget { 32 | assert(typeof target === "object" && target !== null, "Expected an object"); 33 | assert("target_name" in target, "Expected a 'target_name' property"); 34 | assert("sources" in target, "Expected a 'sources' property"); 35 | const { sources } = target; 36 | assert(Array.isArray(sources), "Expected a 'sources' array"); 37 | assert( 38 | sources.every((source) => typeof source === "string"), 39 | "Expected all sources to be strings", 40 | ); 41 | if ("include_dirs" in target) { 42 | const { include_dirs } = target; 43 | assert( 44 | Array.isArray(include_dirs), 45 | "Expected 'include_dirs' to be an array", 46 | ); 47 | assert( 48 | include_dirs.every((dir) => typeof dir === "string"), 49 | "Expected all include_dirs to be strings", 50 | ); 51 | } 52 | if (disallowUnknownProperties) { 53 | assertNoExtraProperties(target, ["target_name", "sources", "include_dirs"]); 54 | } 55 | } 56 | 57 | export function assertBinding( 58 | json: unknown, 59 | disallowUnknownProperties = false, 60 | ): asserts json is GypBinding { 61 | assert(typeof json === "object" && json !== null, "Expected an object"); 62 | assert("targets" in json, "Expected a 'targets' property"); 63 | const { targets } = json; 64 | assert(Array.isArray(targets), "Expected a 'targets' array"); 65 | for (const target of targets) { 66 | assertTarget(target, disallowUnknownProperties); 67 | } 68 | if (disallowUnknownProperties) { 69 | assertNoExtraProperties(json, ["targets"]); 70 | } 71 | } 72 | 73 | export function readBindingFile( 74 | path: string, 75 | disallowUnknownProperties = false, 76 | ): GypBinding { 77 | try { 78 | const contents = fs.readFileSync(path, "utf-8"); 79 | const json = parse(contents); 80 | assertBinding(json, disallowUnknownProperties); 81 | return json; 82 | } catch (err) { 83 | throw new Error("Failed to parse binding.gyp file", { cause: err }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /scripts/depcheck.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import assert from "node:assert/strict"; 3 | import cp from "node:child_process"; 4 | import fs from "node:fs"; 5 | 6 | import depcheck from "depcheck"; 7 | 8 | function getWorkspaces() { 9 | const workspaces = JSON.parse( 10 | cp.execFileSync("npm", ["query", ".workspace"], { encoding: "utf8" }), 11 | ) as unknown; 12 | assert(Array.isArray(workspaces)); 13 | for (const workspace of workspaces) { 14 | assert(typeof workspace === "object" && workspace !== null); 15 | } 16 | return workspaces as Record[]; 17 | } 18 | 19 | const rootDir = path.resolve(import.meta.dirname, ".."); 20 | const root = await depcheck(rootDir, {}); 21 | 22 | const rootPackage = JSON.parse( 23 | await fs.promises.readFile(path.join(rootDir, "package.json"), { 24 | encoding: "utf8", 25 | }), 26 | ) as unknown; 27 | 28 | assert( 29 | typeof rootPackage === "object" && 30 | rootPackage !== null && 31 | "devDependencies" in rootPackage && 32 | typeof rootPackage.devDependencies === "object" && 33 | rootPackage.devDependencies !== null, 34 | ); 35 | 36 | const rootDevDependencies = new Set(Object.keys(rootPackage.devDependencies)); 37 | for (const packageName of [...rootDevDependencies.values()]) { 38 | rootDevDependencies.add(`@types/${packageName}`); 39 | } 40 | 41 | for (const { 42 | name: workspaceName, 43 | path: workspacePath, 44 | private: workspacePrivate, 45 | } of getWorkspaces()) { 46 | assert(typeof workspaceName === "string"); 47 | assert(typeof workspacePath === "string"); 48 | assert( 49 | typeof workspacePrivate === "boolean" || 50 | typeof workspacePrivate === "undefined", 51 | ); 52 | if (workspacePrivate) { 53 | console.warn(`Skipping private package '${workspaceName}'`); 54 | continue; 55 | } 56 | const result = await depcheck(workspacePath, { 57 | ignoreMatches: [...rootDevDependencies], 58 | }); 59 | for (const [name, filePaths] of Object.entries(result.missing)) { 60 | if (!rootDevDependencies.has(name)) { 61 | console.error(`Missing '${name}' in '${workspaceName}':`); 62 | for (const filePath of filePaths) { 63 | console.error("↳", path.relative(workspacePath, filePath)); 64 | } 65 | console.error(); 66 | process.exitCode = 1; 67 | } 68 | } 69 | for (const name of result.dependencies) { 70 | console.error(`Unused dependency '${name}' in '${workspaceName}'`); 71 | console.error(); 72 | process.exitCode = 1; 73 | } 74 | for (const name of result.devDependencies) { 75 | console.error(`Unused dev-dependency '${name}' in '${workspaceName}'`); 76 | console.error(); 77 | process.exitCode = 1; 78 | } 79 | } 80 | 81 | assert.deepEqual(root.dependencies, [], "Found unused dependencies"); 82 | -------------------------------------------------------------------------------- /packages/host/src/node/prebuilds/android.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import fs from "node:fs"; 3 | import path from "node:path"; 4 | 5 | import { AndroidTriplet } from "./triplets.js"; 6 | import { determineLibraryBasename } from "../path-utils.js"; 7 | 8 | export const DEFAULT_ANDROID_TRIPLETS = [ 9 | "aarch64-linux-android", 10 | "armv7a-linux-androideabi", 11 | "i686-linux-android", 12 | "x86_64-linux-android", 13 | ] as const satisfies AndroidTriplet[]; 14 | 15 | type AndroidArchitecture = "armeabi-v7a" | "arm64-v8a" | "x86" | "x86_64"; 16 | 17 | export const ANDROID_ARCHITECTURES = { 18 | "armv7a-linux-androideabi": "armeabi-v7a", 19 | "aarch64-linux-android": "arm64-v8a", 20 | "i686-linux-android": "x86", 21 | "x86_64-linux-android": "x86_64", 22 | } satisfies Record; 23 | 24 | /** 25 | * Determine the filename of the Android libs directory based on the framework paths. 26 | * Ensuring that all framework paths have the same base name. 27 | */ 28 | export function determineAndroidLibsFilename(libraryPaths: string[]) { 29 | const libraryName = determineLibraryBasename(libraryPaths); 30 | return `${libraryName}.android.node`; 31 | } 32 | 33 | type AndroidLibsDirectoryOptions = { 34 | outputPath: string; 35 | libraries: { triplet: AndroidTriplet; libraryPath: string }[]; 36 | autoLink: boolean; 37 | }; 38 | 39 | export async function createAndroidLibsDirectory({ 40 | outputPath, 41 | libraries, 42 | autoLink, 43 | }: AndroidLibsDirectoryOptions) { 44 | // Delete and recreate any existing output directory 45 | await fs.promises.rm(outputPath, { recursive: true, force: true }); 46 | await fs.promises.mkdir(outputPath, { recursive: true }); 47 | for (const { triplet, libraryPath } of libraries) { 48 | assert( 49 | fs.existsSync(libraryPath), 50 | `Library not found: ${libraryPath} for triplet ${triplet}`, 51 | ); 52 | const arch = ANDROID_ARCHITECTURES[triplet]; 53 | const archOutputPath = path.join(outputPath, arch); 54 | await fs.promises.mkdir(archOutputPath, { recursive: true }); 55 | // Strip the ".node" extension from the library name 56 | const libraryName = path.basename(libraryPath, ".node"); 57 | const soSuffixedName = 58 | path.extname(libraryName) === ".so" ? libraryName : `${libraryName}.so`; 59 | const finalLibraryName = libraryName.startsWith("lib") 60 | ? soSuffixedName 61 | : `lib${soSuffixedName}`; 62 | const libraryOutputPath = path.join(archOutputPath, finalLibraryName); 63 | await fs.promises.copyFile(libraryPath, libraryOutputPath); 64 | // TODO: Update the install path in the library file 65 | } 66 | if (autoLink) { 67 | // Write a file to mark the Android libs directory is a Node-API module 68 | await fs.promises.writeFile( 69 | path.join(outputPath, "react-native-node-api-module"), 70 | "", 71 | "utf8", 72 | ); 73 | } 74 | return outputPath; 75 | } 76 | -------------------------------------------------------------------------------- /packages/weak-node-api/scripts/generate.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import fs from "node:fs"; 3 | import path from "node:path"; 4 | import cp from "node:child_process"; 5 | 6 | import { 7 | FunctionDecl, 8 | getNodeApiFunctions, 9 | } from "../src/node-api-functions.js"; 10 | 11 | import * as weakNodeApiGenerator from "./generators/weak-node-api.js"; 12 | import * as hostGenerator from "./generators/NodeApiHost.js"; 13 | 14 | export const OUTPUT_PATH = path.join(import.meta.dirname, "../generated"); 15 | 16 | type GenerateFileOptions = { 17 | functions: FunctionDecl[]; 18 | fileName: string; 19 | generator: (functions: FunctionDecl[]) => string; 20 | headingComment?: string; 21 | }; 22 | 23 | async function generateFile({ 24 | functions, 25 | fileName, 26 | generator, 27 | headingComment = "", 28 | }: GenerateFileOptions) { 29 | const generated = generator(functions); 30 | const output = ` 31 | /** 32 | * @file ${fileName} 33 | * ${headingComment 34 | .trim() 35 | .split("\n") 36 | .map((l) => l.trim()) 37 | .join("\n* ")} 38 | * 39 | * @note This file is generated - don't edit it directly 40 | */ 41 | 42 | ${generated} 43 | `; 44 | const outputPath = path.join(OUTPUT_PATH, fileName); 45 | await fs.promises.writeFile(outputPath, output.trim(), "utf-8"); 46 | const { status, stderr = "No error output" } = cp.spawnSync( 47 | "clang-format", 48 | ["-i", outputPath], 49 | { 50 | encoding: "utf8", 51 | }, 52 | ); 53 | assert.equal(status, 0, `Failed to format ${fileName}: ${stderr}`); 54 | } 55 | 56 | async function run() { 57 | await fs.promises.mkdir(OUTPUT_PATH, { recursive: true }); 58 | 59 | const functions = getNodeApiFunctions(); 60 | await generateFile({ 61 | functions, 62 | fileName: "NodeApiHost.hpp", 63 | generator: hostGenerator.generateHeader, 64 | headingComment: ` 65 | @brief NodeApiHost struct. 66 | 67 | This header provides a struct of Node-API functions implemented by a host to inject its implementations. 68 | `, 69 | }); 70 | await generateFile({ 71 | functions, 72 | fileName: "weak_node_api.hpp", 73 | generator: weakNodeApiGenerator.generateHeader, 74 | headingComment: ` 75 | @brief Weak Node-API host injection interface. 76 | 77 | This header provides the struct and injection function for deferring Node-API function calls from addons into a Node-API host. 78 | `, 79 | }); 80 | await generateFile({ 81 | functions, 82 | fileName: "weak_node_api.cpp", 83 | generator: weakNodeApiGenerator.generateSource, 84 | headingComment: ` 85 | @brief Weak Node-API host injection implementation. 86 | 87 | Provides the implementation for deferring Node-API function calls from addons into a Node-API host. 88 | `, 89 | }); 90 | } 91 | 92 | run().catch((err) => { 93 | console.error(err); 94 | process.exitCode = 1; 95 | }); 96 | -------------------------------------------------------------------------------- /packages/host/scripts/generate-injector.mts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import cp from "node:child_process"; 4 | 5 | import { type FunctionDecl, getNodeApiFunctions } from "weak-node-api"; 6 | 7 | export const CPP_SOURCE_PATH = path.join(import.meta.dirname, "../cpp"); 8 | 9 | // TODO: Remove when all runtime Node API functions are implemented 10 | const IMPLEMENTED_RUNTIME_FUNCTIONS = [ 11 | "napi_create_buffer", 12 | "napi_create_buffer_copy", 13 | "napi_is_buffer", 14 | "napi_get_buffer_info", 15 | "napi_create_external_buffer", 16 | "napi_create_async_work", 17 | "napi_queue_async_work", 18 | "napi_delete_async_work", 19 | "napi_cancel_async_work", 20 | "napi_fatal_error", 21 | "napi_get_node_version", 22 | "napi_get_version", 23 | ]; 24 | 25 | /** 26 | * Generates source code which injects the Node API functions from the host. 27 | */ 28 | export function generateSource(functions: FunctionDecl[]) { 29 | return ` 30 | // This file is generated by react-native-node-api 31 | #include 32 | #include 33 | 34 | #include 35 | #include 36 | #include 37 | 38 | #if defined(__APPLE__) 39 | #define WEAK_NODE_API_LIBRARY_NAME "@rpath/weak-node-api.framework/weak-node-api" 40 | #elif defined(__ANDROID__) 41 | #define WEAK_NODE_API_LIBRARY_NAME "libweak-node-api.so" 42 | #else 43 | #error "WEAK_NODE_API_LIBRARY_NAME cannot be defined for this platform" 44 | #endif 45 | 46 | namespace callstack::react_native_node_api { 47 | 48 | void injectIntoWeakNodeApi() { 49 | void *module = dlopen(WEAK_NODE_API_LIBRARY_NAME, RTLD_NOW | RTLD_LOCAL); 50 | if (nullptr == module) { 51 | log_debug("NapiHost: Failed to load weak-node-api: %s", dlerror()); 52 | abort(); 53 | } 54 | 55 | auto inject_weak_node_api_host = (InjectHostFunction)dlsym( 56 | module, "inject_weak_node_api_host"); 57 | if (nullptr == inject_weak_node_api_host) { 58 | log_debug("NapiHost: Failed to find 'inject_weak_node_api_host' function: %s", dlerror()); 59 | abort(); 60 | } 61 | 62 | log_debug("Injecting NodeApiHost"); 63 | inject_weak_node_api_host(NodeApiHost { 64 | ${functions 65 | .filter( 66 | ({ kind, name }) => 67 | kind === "engine" || IMPLEMENTED_RUNTIME_FUNCTIONS.includes(name), 68 | ) 69 | .flatMap(({ name }) => `.${name} = ${name},`) 70 | .join("\n")} 71 | }); 72 | } 73 | } // namespace callstack::react_native_node_api 74 | `; 75 | } 76 | 77 | async function run() { 78 | const nodeApiFunctions = getNodeApiFunctions(); 79 | 80 | const source = generateSource(nodeApiFunctions); 81 | const sourcePath = path.join(CPP_SOURCE_PATH, "WeakNodeApiInjector.cpp"); 82 | await fs.promises.writeFile(sourcePath, source, "utf-8"); 83 | cp.spawnSync("clang-format", ["-i", sourcePath], { stdio: "inherit" }); 84 | } 85 | 86 | run().catch((err) => { 87 | console.error(err); 88 | process.exitCode = 1; 89 | }); 90 | -------------------------------------------------------------------------------- /packages/cmake-file-api/src/schemas/ReplyIndexV1.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | 3 | export const ReplyFileReferenceV1 = z.object({ 4 | kind: z.enum([ 5 | "codemodel", 6 | "configureLog", 7 | "cache", 8 | "cmakeFiles", 9 | "toolchains", 10 | ]), 11 | version: z.object({ 12 | major: z.number(), 13 | minor: z.number(), 14 | }), 15 | jsonFile: z.string(), 16 | }); 17 | 18 | const ReplyErrorObject = z.object({ 19 | error: z.string(), 20 | }); 21 | 22 | const VersionNumber = z.number(); 23 | 24 | const VersionObject = z.object({ 25 | major: z.number(), 26 | minor: z.number().optional(), 27 | }); 28 | 29 | const VersionSpec = z.union([ 30 | VersionNumber, 31 | VersionObject, 32 | z.array(z.union([VersionNumber, VersionObject])), 33 | ]); 34 | 35 | const QueryRequest = z.object({ 36 | kind: z.string(), 37 | version: VersionSpec.optional(), 38 | client: z.unknown().optional(), 39 | }); 40 | 41 | const ClientStatefulQueryReply = z.object({ 42 | client: z.unknown().optional(), 43 | requests: z.array(QueryRequest).optional(), 44 | responses: z.array(ReplyFileReferenceV1).optional(), 45 | }); 46 | 47 | export const IndexReplyV1 = z.object({ 48 | cmake: z.object({ 49 | version: z.object({ 50 | major: z.number(), 51 | minor: z.number(), 52 | patch: z.number(), 53 | suffix: z.string(), 54 | string: z.string(), 55 | isDirty: z.boolean(), 56 | }), 57 | paths: z.object({ 58 | cmake: z.string(), 59 | ctest: z.string(), 60 | cpack: z.string(), 61 | root: z.string(), 62 | }), 63 | generator: z.object({ 64 | multiConfig: z.boolean(), 65 | name: z.string(), 66 | platform: z.string().optional(), 67 | }), 68 | }), 69 | objects: z.array(ReplyFileReferenceV1), 70 | reply: z.record( 71 | z.string(), 72 | z 73 | .union([ 74 | ReplyFileReferenceV1, 75 | ReplyErrorObject, 76 | z.record( 77 | z.string(), 78 | z.union([ 79 | ReplyFileReferenceV1, 80 | ReplyErrorObject, 81 | ClientStatefulQueryReply, 82 | ]), 83 | ), 84 | ]) 85 | .optional(), 86 | ), 87 | }); 88 | 89 | const ReplyErrorIndexFileReference = ReplyFileReferenceV1.extend({ 90 | kind: z.enum(["configureLog"]), 91 | }); 92 | 93 | const ClientStatefulQueryReplyForErrorIndex = ClientStatefulQueryReply.extend({ 94 | responses: z.array(ReplyErrorIndexFileReference).optional(), 95 | }); 96 | 97 | export const ReplyErrorIndex = IndexReplyV1.extend({ 98 | objects: z.array(ReplyErrorIndexFileReference), 99 | reply: z.record( 100 | z.string(), 101 | z 102 | .union([ 103 | ReplyErrorIndexFileReference, 104 | ReplyErrorObject, 105 | z.record( 106 | z.string(), 107 | z.union([ 108 | ReplyErrorIndexFileReference, 109 | ReplyErrorObject, 110 | ClientStatefulQueryReplyForErrorIndex, 111 | ]), 112 | ), 113 | ]) 114 | .optional(), 115 | ), 116 | }); 117 | -------------------------------------------------------------------------------- /packages/host/cpp/AddonLoaders.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Logger.hpp" 3 | 4 | #include 5 | 6 | #if defined(__APPLE__) || defined(__ANDROID__) 7 | #include 8 | #include 9 | 10 | using callstack::react_native_node_api::log_debug; 11 | 12 | struct PosixLoader { 13 | using Module = void *; 14 | using Symbol = void *; 15 | 16 | static Module loadLibrary(const char *filePath) { 17 | assert(NULL != filePath); 18 | 19 | Module result = dlopen(filePath, RTLD_NOW | RTLD_LOCAL); 20 | if (NULL == result) { 21 | log_debug("NapiHost: Failed to load library '%s': %s", filePath, 22 | dlerror()); 23 | } 24 | return result; 25 | } 26 | 27 | static Symbol getSymbol(Module library, const char *name) { 28 | assert(NULL != library); 29 | assert(NULL != name); 30 | Symbol result = dlsym(library, name); 31 | // if (NULL == result) { 32 | // NSLog(@"NapiHost: Cannot find '%s' symbol!", name); 33 | // } 34 | return result; 35 | } 36 | 37 | static void unloadLibrary(Module library) { 38 | if (NULL != library) { 39 | dlclose(library); 40 | } 41 | } 42 | }; 43 | #endif 44 | 45 | #if defined(_WIN32) 46 | struct Win32Loader { 47 | using Module = HMODULE; 48 | using Symbol = void *; 49 | 50 | static Module loadLibrary(const char *filePath) { 51 | assert(NULL != filePath); 52 | Module result = LoadLibrary(filePath); 53 | if (NULL == result) { 54 | // TODO: Handle the error case... call GetLastError() that gives us error 55 | // code as DWORD 56 | } 57 | return result; 58 | } 59 | 60 | static Symbol getSymbol(Module library, const char *name) { 61 | assert(NULL != library); 62 | assert(NULL != name); 63 | Symbol result = GetProcAddress(library, name); 64 | if (NULL == result) { 65 | // TODO: Handle the error case... call GetLastError() that gives us error 66 | // code as DWORD 67 | } 68 | return result; 69 | } 70 | 71 | static void unloadLibrary(Module library) { 72 | if (NULL != library) { 73 | FreeLibrary(library); 74 | } 75 | } 76 | }; 77 | 78 | struct WinRTLoader { 79 | using Module = HMODULE; 80 | using Symbol = void *; 81 | 82 | static Module loadLibrary(const char *filePath) { 83 | assert(NULL != filePath); 84 | Module result = LoadPackagedLibrary(filePath); 85 | if (NULL == result) { 86 | // TODO: Handle the error case... call GetLastError() that gives us error 87 | // code as DWORD 88 | } 89 | return result; 90 | } 91 | 92 | static Symbol getSymbol(Module library, const char *name) { 93 | assert(NULL != library); 94 | assert(NULL != name); 95 | Symbol result = GetProcAddress(library, name); 96 | if (NULL == result) { 97 | // TODO: Handle the error case... call GetLastError() that gives us error 98 | // code as DWORD 99 | } 100 | return result; 101 | } 102 | 103 | static void unloadLibrary(Module library) { 104 | if (NULL != library) { 105 | FreeLibrary(library); 106 | } 107 | } 108 | }; 109 | #endif -------------------------------------------------------------------------------- /packages/node-addon-examples/scripts/verify-prebuilds.mts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import assert from "node:assert/strict"; 3 | import path from "node:path"; 4 | 5 | import { EXAMPLES_DIR } from "./cmake-projects.mjs"; 6 | 7 | const EXPECTED_ANDROID_ARCHS = ["armeabi-v7a", "arm64-v8a", "x86_64", "x86"]; 8 | 9 | const EXPECTED_XCFRAMEWORK_PLATFORMS = [ 10 | "ios-arm64", 11 | "ios-arm64-simulator", 12 | "macos-arm64_x86_64", 13 | "tvos-arm64", 14 | "tvos-arm64-simulator", 15 | "xros-arm64", 16 | "xros-arm64-simulator", 17 | ]; 18 | 19 | async function verifyAndroidPrebuild(dirent: fs.Dirent) { 20 | console.log( 21 | "Verifying Android prebuild", 22 | dirent.name, 23 | "in", 24 | dirent.parentPath, 25 | ); 26 | for (const arch of EXPECTED_ANDROID_ARCHS) { 27 | const archDir = path.join(dirent.parentPath, dirent.name, arch); 28 | for (const file of await fs.promises.readdir(archDir, { 29 | withFileTypes: true, 30 | })) { 31 | assert(file.isFile()); 32 | assert( 33 | !file.name.endsWith(".node"), 34 | `Unexpected .node file: ${path.join(file.parentPath, file.name)}`, 35 | ); 36 | } 37 | } 38 | } 39 | 40 | async function verifyApplePrebuild(dirent: fs.Dirent) { 41 | console.log("Verifying Apple prebuild", dirent.name, "in", dirent.parentPath); 42 | for (const arch of EXPECTED_XCFRAMEWORK_PLATFORMS) { 43 | const archDir = path.join(dirent.parentPath, dirent.name, arch); 44 | for (const file of await fs.promises.readdir(archDir, { 45 | withFileTypes: true, 46 | })) { 47 | assert( 48 | file.isDirectory(), 49 | "Expected only directories in xcframework arch directory", 50 | ); 51 | assert(file.name.endsWith(".framework"), "Expected framework directory"); 52 | const frameworkDir = path.join(file.parentPath, file.name); 53 | for (const file of await fs.promises.readdir(frameworkDir, { 54 | withFileTypes: true, 55 | })) { 56 | if (file.isDirectory()) { 57 | assert.equal( 58 | file.name, 59 | "Headers", 60 | "Unexpected directory in xcframework", 61 | ); 62 | } else { 63 | assert( 64 | file.isFile(), 65 | "Expected only directory and files in framework", 66 | ); 67 | if (file.name === "Info.plist") { 68 | // TODO: Verify the contents of the Info.plist file 69 | continue; 70 | } else { 71 | assert( 72 | !file.name.endsWith(".node"), 73 | `Didn't expected a .node file in xcframework: ${path.join( 74 | frameworkDir, 75 | file.name, 76 | )}`, 77 | ); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | for await (const dirent of fs.promises.glob("**/*.*.node", { 86 | cwd: EXAMPLES_DIR, 87 | withFileTypes: true, 88 | })) { 89 | if (dirent.name.endsWith(".android.node")) { 90 | await verifyAndroidPrebuild(dirent); 91 | } else if (dirent.name.endsWith(".apple.node")) { 92 | await verifyApplePrebuild(dirent); 93 | } else { 94 | throw new Error( 95 | `Unexpected prebuild file: ${dirent.name} in ${dirent.parentPath}`, 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /apps/test-app/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /packages/cmake-file-api/src/query.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | /** 5 | * Creates a shared stateless query file for the specified object kind and major version. 6 | * These are stateless shared queries not owned by any specific client. 7 | * 8 | * @param buildPath Path to the build directory 9 | * @param kind Object kind to query for 10 | * @param majorVersion Major version number as string 11 | */ 12 | export async function createSharedStatelessQuery( 13 | buildPath: string, 14 | kind: "codemodel" | "configureLog" | "cache" | "cmakeFiles" | "toolchains", 15 | majorVersion: string, 16 | ) { 17 | const queryPath = path.join( 18 | buildPath, 19 | `.cmake/api/v1/query/${kind}-v${majorVersion}`, 20 | ); 21 | await fs.promises.mkdir(path.dirname(queryPath), { recursive: true }); 22 | await fs.promises.writeFile(queryPath, ""); 23 | } 24 | 25 | /** 26 | * Creates a client stateless query file for the specified client, object kind and major version. 27 | * These are stateless queries owned by the specified client. 28 | * 29 | * @param buildPath Path to the build directory 30 | * @param clientName Unique identifier for the client 31 | * @param kind Object kind to query for 32 | * @param majorVersion Major version number as string 33 | */ 34 | export async function createClientStatelessQuery( 35 | buildPath: string, 36 | clientName: string, 37 | kind: "codemodel" | "configureLog" | "cache" | "cmakeFiles" | "toolchains", 38 | majorVersion: string, 39 | ) { 40 | const queryPath = path.join( 41 | buildPath, 42 | `.cmake/api/v1/query/client-${clientName}/${kind}-v${majorVersion}`, 43 | ); 44 | await fs.promises.mkdir(path.dirname(queryPath), { recursive: true }); 45 | await fs.promises.writeFile(queryPath, ""); 46 | } 47 | 48 | /** 49 | * Version specification for stateful queries 50 | */ 51 | export type VersionSpec = 52 | | number // major version only 53 | | { major: number; minor?: number } // major with optional minor 54 | | (number | { major: number; minor?: number })[]; // array of version specs 55 | 56 | /** 57 | * Request specification for stateful queries 58 | */ 59 | export interface QueryRequest { 60 | kind: "codemodel" | "configureLog" | "cache" | "cmakeFiles" | "toolchains"; 61 | version?: VersionSpec; 62 | client?: unknown; // Reserved for client use 63 | } 64 | 65 | /** 66 | * Stateful query specification 67 | */ 68 | export interface StatefulQuery { 69 | requests: QueryRequest[]; 70 | client?: unknown; // Reserved for client use 71 | } 72 | 73 | /** 74 | * Creates a client stateful query file (query.json) for the specified client. 75 | * These are stateful queries owned by the specified client that can request 76 | * specific versions and get only the most recent version recognized by CMake. 77 | * 78 | * @param buildPath Path to the build directory 79 | * @param clientName Unique identifier for the client 80 | * @param query Stateful query specification 81 | */ 82 | export async function createClientStatefulQuery( 83 | buildPath: string, 84 | clientName: string, 85 | query: StatefulQuery, 86 | ) { 87 | const queryPath = path.join( 88 | buildPath, 89 | `.cmake/api/v1/query/client-${clientName}/query.json`, 90 | ); 91 | await fs.promises.mkdir(path.dirname(queryPath), { recursive: true }); 92 | await fs.promises.writeFile(queryPath, JSON.stringify(query, null, 2)); 93 | } 94 | -------------------------------------------------------------------------------- /packages/node-addon-examples/src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | 3 | function assertLogs(cb: () => void, expectedMessages: string[]) { 4 | const errors: Error[] = []; 5 | // Spying on the console.log function, as the examples don't assert anything themselves 6 | const originalLog = console.log; 7 | console.log = (message: string, ...args: unknown[]) => { 8 | const nextMessage = expectedMessages.shift(); 9 | const combinedMessage = [message, ...args].map(String).join(" "); 10 | if (nextMessage !== combinedMessage) { 11 | errors.push(new Error(`Unexpected log message '${combinedMessage}'`)); 12 | } 13 | }; 14 | try { 15 | cb(); 16 | if (expectedMessages.length > 0) { 17 | errors.push( 18 | new Error( 19 | `Missing expected message(s): ${expectedMessages.join(", ")}`, 20 | ), 21 | ); 22 | } 23 | } finally { 24 | console.log = originalLog; 25 | } 26 | // Throw and first error 27 | const [firstError] = errors; 28 | if (firstError) { 29 | throw firstError; 30 | } 31 | } 32 | 33 | export const suites: Record< 34 | string, 35 | Record void | (() => void | Promise)> 36 | > = { 37 | "1-getting-started": { 38 | "1_hello_world/napi": () => 39 | assertLogs(() => { 40 | require("../examples/1-getting-started/1_hello_world/napi/hello.js"); 41 | }, ["world"]), 42 | "1_hello_world/node-addon-api": () => 43 | assertLogs(() => { 44 | require("../examples/1-getting-started/1_hello_world/node-addon-api/hello.js"); 45 | }, ["world"]), 46 | "1_hello_world/node-addon-api-addon-class": () => 47 | assertLogs(() => { 48 | require("../examples/1-getting-started/1_hello_world/node-addon-api-addon-class/hello.js"); 49 | }, ["world"]), 50 | "2_function_arguments/napi": () => 51 | assertLogs(() => { 52 | require("../examples/1-getting-started/2_function_arguments/napi/addon.js"); 53 | }, ["This should be eight: 8"]), 54 | "2_function_arguments/node-addon-api": () => 55 | assertLogs(() => { 56 | require("../examples/1-getting-started/2_function_arguments/node-addon-api/addon.js"); 57 | }, ["This should be eight: 8"]), 58 | "3_callbacks/napi": () => 59 | assertLogs(() => { 60 | require("../examples/1-getting-started/3_callbacks/napi/addon.js"); 61 | }, ["hello world"]), 62 | "3_callbacks/node-addon-api": () => 63 | assertLogs(() => { 64 | require("../examples/1-getting-started/3_callbacks/node-addon-api/addon.js"); 65 | }, ["hello world"]), 66 | "4_object_factory/napi": () => 67 | assertLogs(() => { 68 | require("../examples/1-getting-started/4_object_factory/napi/addon.js"); 69 | }, ["hello world"]), 70 | "4_object_factory/node-addon-api": () => 71 | assertLogs(() => { 72 | require("../examples/1-getting-started/4_object_factory/node-addon-api/addon.js"); 73 | }, ["hello world"]), 74 | "5_function_factory": () => 75 | assertLogs(() => { 76 | require("../examples/1-getting-started/5_function_factory/napi/addon.js"); 77 | }, ["hello world"]), 78 | }, 79 | "5-async-work": { 80 | // TODO: This crashes (SIGABRT) 81 | // "async_work_thread_safe_function": () => require("../examples/5-async-work/async_work_thread_safe_function/napi/index.js"), 82 | }, 83 | tests: { 84 | buffers: () => { 85 | require("../tests/buffers/addon.js"); 86 | }, 87 | async: () => require("../tests/async/addon.js") as () => Promise, 88 | }, 89 | }; 90 | -------------------------------------------------------------------------------- /docs/HOW-IT-WORKS.md: -------------------------------------------------------------------------------- 1 | # How it works 2 | 3 | This document will outline what happens throughout the various parts of the system, when the app calls the `add` method on the library introduced in the ["usage" document](./USAGE.md). 4 | 5 | 6 | 7 | 8 | 9 | ## `my-app` makes an `import` 10 | 11 | Everything starts from the consuming app importing the `calculator-lib`. 12 | Metro handles the resolution and the `calculator-lib`'s entrypoint is added to the JavaScript-bundle when bundling. 13 | 14 | ## `calculator-lib` does `require("./prebuild.node")` which is transformed into a call into the host TurboModule 15 | 16 | The library has a require call to a `.node` file, which would normally not have any special meaning: 17 | 18 | ```javascript 19 | module.exports = require("./prebuild.node"); 20 | ``` 21 | 22 | Since the app developer has added the `react-native-node-api/babel-plugin` to their Babel configuration, the require statement gets transformed when the app is being bundled by Metro, into a `requireNodeAddon` call on our TurboModule. 23 | 24 | The generated code looks something like this: 25 | 26 | ```javascript 27 | module.exports = require("react-native-node-api").requireNodeAddon( 28 | "calculator-lib--prebuild", 29 | ); 30 | ``` 31 | 32 | > [!NOTE] 33 | > In the time of writing, this code only supports iOS as passes the path to the library with its .framework. 34 | > We plan on generalizing this soon 🤞 35 | 36 | ## Transformed code calls into `react-native-node-api`, loading the platform specific dynamic library 37 | 38 | The native implementation of `requireNodeAddon` is responsible for loading the dynamic library and allow the Node-API module to register its initialization function, either by exporting a `napi_register_module_v1` function or by calling the (deprecated) `napi_module_register` function. 39 | 40 | In any case the native code stores the initialization function in a data-structure. 41 | 42 | ## `react-native-node-api` creates a `node_env` and initialize the Node-API module 43 | 44 | The initialization function of a Node-API module expects a `node_env`, which we create by calling `createNodeApiEnv` on the `jsi::Runtime`. 45 | 46 | ## The library's C++ code initialize the `exports` object 47 | 48 | An `exports` object is created for the Node-API module and both the `napi_env` and `exports` object is passed to the Node-API module's initialization function and the third party code is able to call the Node-API free functions: 49 | 50 | - The engine-specific functions (see [js_native_api.h](https://github.com/nodejs/node/blob/main/src/js_native_api.h)) are implemented by the `jsi::Runtime` (currently only Hermes supports this). 51 | - The runtime-specific functions (see [node_api.h](https://github.com/nodejs/node/blob/main/src/node_api.h)) are implemented by `react-native-node-api`. 52 | 53 | ## `my-app` regain control and call `add` 54 | 55 | When the `exports` object is populated by `calculator-lib`'s Node-API module, control is returned to `react-native-node-api` which returns the `exports` object to JavaScript, with the `add` function defined on it. 56 | 57 | ```javascript 58 | import { add } from "calculator-lib"; 59 | console.log("1 + 2 =", add(1, 2)); 60 | ``` 61 | 62 | ## The library's C++ code execute the native function 63 | 64 | Now that the app's JavaScript call the `add` function, the JavaScript engine will know to call the associated native function, which was setup during the initialization of the Node-API module and the native `Add` function is executed and control returned to JavaScript again. 65 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # Copilot Instructions for React Native Node-API 2 | 3 | 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. 4 | 5 | ## Package-Specific Instructions 6 | 7 | **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. 8 | 9 | ## Architecture Overview 10 | 11 | **Core Flow**: JS `require("./addon.node")` → Babel transform → `requireNodeAddon()` TurboModule call → native library loading → Node-API module initialization 12 | 13 | ### Package Architecture 14 | 15 | See the [README.md](../README.md#packages) for detailed descriptions of each package and their roles in the system. Key packages include: 16 | 17 | - `packages/host` - Core Node-API runtime and Babel plugin 18 | - `packages/cmake-rn` - CMake wrapper for native builds 19 | - `packages/cmake-file-api` - TypeScript wrapper for CMake File API with Zod validation 20 | - `packages/ferric` - Rust/Cargo wrapper with napi-rs integration 21 | - `packages/gyp-to-cmake` - Legacy binding.gyp compatibility 22 | - `apps/test-app` - Integration testing harness 23 | 24 | ## Critical Build Dependencies 25 | 26 | - **Custom Hermes**: Currently depends on a patched Hermes with Node-API support (see [facebook/hermes#1377](https://github.com/facebook/hermes/pull/1377)) 27 | - **Prebuilt Binary Spec**: All tools must output to the exact naming scheme: 28 | - Android: `*.android.node/` with jniLibs structure + `react-native-node-api-module` marker file 29 | - iOS: `*.apple.node` (XCFramework renamed) + marker file 30 | 31 | ## Essential Workflows 32 | 33 | ### Development Setup 34 | 35 | ```bash 36 | npm ci && npm run build # Install deps and build all packages 37 | npm run bootstrap # Build native components (weak-node-api, examples) 38 | ``` 39 | 40 | ### Package Development 41 | 42 | - **TypeScript project references**: Use `tsc --build` for incremental compilation 43 | - **Workspace scripts**: Most build/test commands use npm workspaces (`--workspace` flag) 44 | - **Focus on Node.js packages**: AI development primarily targets the Node.js tooling packages rather than native mobile code 45 | - **No TypeScript type asserts**: You have to ask explicitly and justify if you want to add `as` type assertions. 46 | 47 | ## Key Patterns 48 | 49 | ### Babel Transformation 50 | 51 | The core magic happens in `packages/host/src/node/babel-plugin/plugin.ts`: 52 | 53 | ```js 54 | // Input: require("./addon.node") 55 | // Output: require("react-native-node-api").requireNodeAddon("pkg-name--addon") 56 | ``` 57 | 58 | ### CMake Integration 59 | 60 | For linking against Node-API in CMakeLists.txt: 61 | 62 | ```cmake 63 | include(${WEAK_NODE_API_CONFIG}) 64 | target_link_libraries(addon PRIVATE weak-node-api) 65 | ``` 66 | 67 | ### Cross-Platform Naming 68 | 69 | Library names use double-dash separation: `package-name--path-component--addon-name` 70 | 71 | ### Testing 72 | 73 | - **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. 74 | - **Cross-package**: Use root-level `npm test` for cross-package testing once individual package tests pass 75 | - **Mobile integration**: Available but not the primary AI development focus - ask the developer to run those tests as needed 76 | 77 | **Documentation**: Integration details, platform setup, and toolchain configuration are covered in existing repo documentation files. 78 | -------------------------------------------------------------------------------- /packages/ferric/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ferric-cli 2 | 3 | ## 0.3.9 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [7ff2c2b] 8 | - Updated dependencies [7ff2c2b] 9 | - weak-node-api@0.0.3 10 | - @react-native-node-api/cli-utils@0.1.2 11 | - react-native-node-api@0.7.1 12 | 13 | ## 0.3.8 14 | 15 | ### Patch Changes 16 | 17 | - 61fff3f: Allow passing --apple-bundle-identifier to specify the bundle identifiers used when creating Apple frameworks. 18 | - Updated dependencies [60fae96] 19 | - Updated dependencies [61fff3f] 20 | - Updated dependencies [61fff3f] 21 | - Updated dependencies [5dea205] 22 | - Updated dependencies [60fae96] 23 | - Updated dependencies [60fae96] 24 | - Updated dependencies [eca721e] 25 | - Updated dependencies [60fae96] 26 | - react-native-node-api@0.7.0 27 | - weak-node-api@0.0.2 28 | 29 | ## 0.3.7 30 | 31 | ### Patch Changes 32 | 33 | - 9411a8c: Add x86_64 ios simulator target and output universal libraries for iOS simulators. 34 | - 9411a8c: It's no longer required to pass "build" to ferric, as this is default now 35 | - b661176: Add support for visionOS and tvOS targets 36 | - Updated dependencies [07ea9dc] 37 | - Updated dependencies [7536c6c] 38 | - Updated dependencies [c698698] 39 | - Updated dependencies [a2fd422] 40 | - Updated dependencies [bdc172e] 41 | - Updated dependencies [4672e01] 42 | - react-native-node-api@0.6.2 43 | 44 | ## 0.3.6 45 | 46 | ### Patch Changes 47 | 48 | - Updated dependencies [5c3de89] 49 | - Updated dependencies [bb9a78c] 50 | - react-native-node-api@0.6.1 51 | 52 | ## 0.3.5 53 | 54 | ### Patch Changes 55 | 56 | - 5156d35: Refactored moving prettyPath util to CLI utils package 57 | - Updated dependencies [acd06f2] 58 | - Updated dependencies [5156d35] 59 | - Updated dependencies [9f1a301] 60 | - Updated dependencies [5016ed2] 61 | - Updated dependencies [5156d35] 62 | - react-native-node-api@0.6.0 63 | - @react-native-node-api/cli-utils@0.1.1 64 | 65 | ## 0.3.4 66 | 67 | ### Patch Changes 68 | 69 | - Updated dependencies [2b9a538] 70 | - react-native-node-api@0.5.2 71 | 72 | ## 0.3.3 73 | 74 | ### Patch Changes 75 | 76 | - 2a30d8d: Refactored CLIs to use a shared utility package 77 | - Updated dependencies [2a30d8d] 78 | - Updated dependencies [c72970f] 79 | - react-native-node-api@0.5.1 80 | 81 | ## 0.3.2 82 | 83 | ### Patch Changes 84 | 85 | - Updated dependencies [90a1471] 86 | - Updated dependencies [75aaed1] 87 | - Updated dependencies [90a1471] 88 | - react-native-node-api@0.5.0 89 | 90 | ## 0.3.1 91 | 92 | ### Patch Changes 93 | 94 | - Updated dependencies [a0212c8] 95 | - Updated dependencies [a0212c8] 96 | - react-native-node-api@0.4.0 97 | 98 | ## 0.3.0 99 | 100 | ### Minor Changes 101 | 102 | - 8557768: Derive default targets from the FERRIC_TARGETS environment variable 103 | 104 | ### Patch Changes 105 | 106 | - e613efe: Fixed cargo build release flag 107 | - a7cc35a: Updated napi packages. 108 | - Updated dependencies [a477b84] 109 | - Updated dependencies [dc33f3c] 110 | - Updated dependencies [4924f66] 111 | - Updated dependencies [acf1a7c] 112 | - react-native-node-api@0.3.3 113 | 114 | ## 0.2.3 115 | 116 | ### Patch Changes 117 | 118 | - Updated dependencies [045e9e5] 119 | - react-native-node-api@0.3.2 120 | 121 | ## 0.2.2 122 | 123 | ### Patch Changes 124 | 125 | - Updated dependencies [7ad62f7] 126 | - react-native-node-api@0.3.1 127 | 128 | ## 0.2.1 129 | 130 | ### Patch Changes 131 | 132 | - Updated dependencies [bd733b8] 133 | - Updated dependencies [b771a27] 134 | - react-native-node-api@0.3.0 135 | 136 | ## 0.2.0 137 | 138 | ### Minor Changes 139 | 140 | - 4379d8c: Initial release 141 | 142 | ### Patch Changes 143 | 144 | - Updated dependencies [4379d8c] 145 | - react-native-node-api@0.2.0 146 | -------------------------------------------------------------------------------- /apps/test-app/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StyleSheet, View, SafeAreaView } from "react-native"; 3 | 4 | import { 5 | MochaRemoteProvider, 6 | ConnectionText, 7 | StatusEmoji, 8 | StatusText, 9 | } from "mocha-remote-react-native"; 10 | 11 | import { suites as nodeAddonExamplesSuites } from "@react-native-node-api/node-addon-examples"; 12 | import { suites as nodeTestsSuites } from "@react-native-node-api/node-tests"; 13 | 14 | function describeIf( 15 | condition: boolean, 16 | title: string, 17 | fn: (this: Mocha.Suite) => void, 18 | ) { 19 | return condition ? describe(title, fn) : describe.skip(title, fn); 20 | } 21 | 22 | type Context = { 23 | allTests?: boolean; 24 | nodeAddonExamples?: boolean; 25 | nodeTests?: boolean; 26 | ferricExample?: boolean; 27 | }; 28 | 29 | function loadTests({ 30 | allTests = false, 31 | nodeAddonExamples = allTests, 32 | nodeTests = allTests, 33 | ferricExample = allTests, 34 | }: Context) { 35 | describeIf(nodeAddonExamples, "Node Addon Examples", () => { 36 | for (const [suiteName, examples] of Object.entries( 37 | nodeAddonExamplesSuites, 38 | )) { 39 | describe(suiteName, () => { 40 | for (const [exampleName, requireExample] of Object.entries(examples)) { 41 | it(exampleName, async () => { 42 | const test = requireExample(); 43 | if (test instanceof Function) { 44 | const result = test(); 45 | if (result instanceof Promise) { 46 | await result; 47 | } 48 | } 49 | }); 50 | } 51 | }); 52 | } 53 | }); 54 | 55 | describeIf(nodeTests, "Node Tests", () => { 56 | function registerTestSuite(suite: typeof nodeTestsSuites) { 57 | for (const [name, suiteOrTest] of Object.entries(suite)) { 58 | if (typeof suiteOrTest === "function") { 59 | it(name, suiteOrTest); 60 | } else { 61 | describe(name, () => { 62 | registerTestSuite(suiteOrTest); 63 | }); 64 | } 65 | } 66 | } 67 | 68 | registerTestSuite(nodeTestsSuites); 69 | }); 70 | 71 | describeIf(ferricExample, "ferric-example", () => { 72 | it("exports a callable sum function", () => { 73 | const exampleAddon = 74 | /* eslint-disable-next-line @typescript-eslint/no-require-imports -- TODO: Determine why a dynamic import doesn't work on Android */ 75 | require("@react-native-node-api/ferric-example") as typeof import("@react-native-node-api/ferric-example"); 76 | const result = exampleAddon.sum(1, 3); 77 | if (result !== 4) { 78 | throw new Error(`Expected 1 + 3 to equal 4, but got ${result}`); 79 | } 80 | }); 81 | }); 82 | } 83 | 84 | export default function App() { 85 | return ( 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | ); 96 | } 97 | 98 | const styles = StyleSheet.create({ 99 | container: { 100 | flex: 1, 101 | backgroundColor: "#fff", 102 | }, 103 | statusContainer: { 104 | flex: 1, 105 | alignItems: "center", 106 | justifyContent: "center", 107 | }, 108 | statusEmoji: { 109 | fontSize: 30, 110 | margin: 30, 111 | textAlign: "center", 112 | }, 113 | statusText: { 114 | fontSize: 20, 115 | margin: 20, 116 | textAlign: "center", 117 | }, 118 | connectionText: { 119 | textAlign: "center", 120 | }, 121 | }); 122 | -------------------------------------------------------------------------------- /packages/gyp-to-cmake/src/transformer.test.ts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert"; 2 | import { describe, it } from "node:test"; 3 | 4 | import { bindingGypToCmakeLists } from "./transformer.js"; 5 | 6 | describe("bindingGypToCmakeLists", () => { 7 | it("should declare a project name", () => { 8 | const output = bindingGypToCmakeLists({ 9 | projectName: "some-project", 10 | gyp: { targets: [] }, 11 | }); 12 | assert(output.includes("project(some-project)")); 13 | }); 14 | 15 | it("should declare target libraries", () => { 16 | const output = bindingGypToCmakeLists({ 17 | projectName: "some-project", 18 | gyp: { 19 | targets: [ 20 | { 21 | target_name: "foo", 22 | sources: ["foo.cc"], 23 | }, 24 | { 25 | target_name: "bar", 26 | sources: ["bar.cc"], 27 | }, 28 | ], 29 | }, 30 | }); 31 | 32 | assert(output.includes("add_library(foo SHARED foo.cc")); 33 | assert(output.includes("add_library(bar SHARED bar.cc")); 34 | }); 35 | 36 | it("transform \\ to / in source filenames", () => { 37 | const output = bindingGypToCmakeLists({ 38 | projectName: "some-project", 39 | gyp: { 40 | targets: [ 41 | { 42 | target_name: "foo", 43 | sources: ["file\\with\\win32\\separator.cc"], 44 | }, 45 | ], 46 | }, 47 | }); 48 | 49 | assert( 50 | output.includes("add_library(foo SHARED file/with/win32/separator.cc"), 51 | ); 52 | }); 53 | 54 | it("escapes spaces in source filenames", () => { 55 | const output = bindingGypToCmakeLists({ 56 | projectName: "some-project", 57 | gyp: { 58 | targets: [ 59 | { 60 | target_name: "foo", 61 | sources: ["file with spaces.cc"], 62 | }, 63 | ], 64 | }, 65 | }); 66 | 67 | assert(output.includes("add_library(foo SHARED file\\ with\\ spaces.cc")); 68 | }); 69 | 70 | describe("command expansions", () => { 71 | it("should expand", () => { 72 | const output = bindingGypToCmakeLists({ 73 | projectName: "some-project", 74 | gyp: { 75 | targets: [ 76 | { 77 | target_name: "foo", 78 | sources: [" { 89 | const output = bindingGypToCmakeLists({ 90 | projectName: "some-project", 91 | gyp: { 92 | targets: [ 93 | { 94 | target_name: "foo", 95 | sources: [" { 106 | it("should add defines as target-specific compile definitions", () => { 107 | const output = bindingGypToCmakeLists({ 108 | projectName: "some-project", 109 | gyp: { 110 | targets: [ 111 | { 112 | target_name: "foo", 113 | sources: ["foo.cc"], 114 | defines: ["FOO", "BAR=value"], 115 | }, 116 | ], 117 | }, 118 | }); 119 | 120 | assert( 121 | output.includes( 122 | "target_compile_definitions(foo PRIVATE FOO BAR=value)", 123 | ), 124 | `Expected output to include target_compile_definitions:\n${output}`, 125 | ); 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /packages/node-tests/rolldown.config.mts: -------------------------------------------------------------------------------- 1 | import assert from "node:assert/strict"; 2 | import fs from "node:fs"; 3 | import path from "node:path"; 4 | 5 | import { defineConfig, type RolldownOptions } from "rolldown"; 6 | import { aliasPlugin, replacePlugin } from "rolldown/experimental"; 7 | 8 | function readGypTargetNames(gypFilePath: string): string[] { 9 | const contents = JSON.parse(fs.readFileSync(gypFilePath, "utf-8")) as unknown; 10 | assert( 11 | typeof contents === "object" && contents !== null, 12 | "Expected gyp file to contain a valid JSON object", 13 | ); 14 | assert("targets" in contents, "Expected targets in gyp file"); 15 | const { targets } = contents; 16 | assert(Array.isArray(targets), "Expected targets to be an array"); 17 | return targets.map(({ target_name }) => { 18 | assert( 19 | typeof target_name === "string", 20 | "Expected target_name to be a string", 21 | ); 22 | return target_name; 23 | }); 24 | } 25 | 26 | function testSuiteConfig(suitePath: string): RolldownOptions[] { 27 | const testFiles = fs.globSync("*.js", { 28 | cwd: suitePath, 29 | exclude: ["*.bundle.js"], 30 | }); 31 | const gypFilePath = path.join(suitePath, "binding.gyp"); 32 | const targetNames = readGypTargetNames(gypFilePath); 33 | return testFiles.map((testFile) => ({ 34 | input: path.join(suitePath, testFile), 35 | output: { 36 | file: path.join(suitePath, path.basename(testFile, ".js") + ".bundle.js"), 37 | }, 38 | resolve: { 39 | conditionNames: ["react-native"], 40 | }, 41 | polyfillRequire: false, 42 | plugins: [ 43 | // Replace dynamic require statements for addon targets to allow the babel plugin to handle them correctly 44 | replacePlugin( 45 | Object.fromEntries( 46 | targetNames.map((targetName) => [ 47 | `require(\`./build/\${common.buildType}/${targetName}\`)`, 48 | `require("./build/Release/${targetName}")`, 49 | ]), 50 | ), 51 | { 52 | delimiters: ["", ""], 53 | }, 54 | ), 55 | replacePlugin( 56 | Object.fromEntries( 57 | targetNames.map((targetName) => [ 58 | // Replace "__require" statement with a regular "require" to allow Metro to resolve it 59 | `__require("./build/Release/${targetName}")`, 60 | `require("./build/Release/${targetName}")`, 61 | ]), 62 | ), 63 | { 64 | delimiters: ["", ""], 65 | }, 66 | ), 67 | replacePlugin( 68 | { 69 | // Replace the default export to return a function instead of initializing the addon immediately 70 | // This allows the test runner to intercept any errors which would normally be thrown when importing 71 | // to work around Metro's `guardedLoadModule` swallowing errors during module initialization 72 | // See https://github.com/facebook/metro/blob/34bb8913ec4b5b02690b39d2246599faf094f721/packages/metro-runtime/src/polyfills/require.js#L348-L353 73 | "export default require_test();": "export default require_test;", 74 | }, 75 | { 76 | delimiters: ["", ""], 77 | }, 78 | ), 79 | aliasPlugin({ 80 | entries: [ 81 | { 82 | find: "../../common", 83 | replacement: "./common.ts", 84 | }, 85 | ], 86 | }), 87 | ], 88 | external: targetNames.map((targetName) => `./build/Release/${targetName}`), 89 | })); 90 | } 91 | 92 | const suitePaths = fs 93 | .globSync("tests/*/*", { 94 | cwd: import.meta.dirname, 95 | withFileTypes: true, 96 | }) 97 | .filter((dirent) => dirent.isDirectory()) 98 | .map((dirent) => 99 | path.join( 100 | path.relative(import.meta.dirname, dirent.parentPath), 101 | dirent.name, 102 | ), 103 | ); 104 | 105 | export default defineConfig(suitePaths.flatMap(testSuiteConfig)); 106 | --------------------------------------------------------------------------------