diff --git a/.changeset/eight-walls-push.md b/.changeset/eight-walls-push.md new file mode 100644 index 00000000..58793ae4 --- /dev/null +++ b/.changeset/eight-walls-push.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Moved weak-node-api into a separate "weak-node-api" package. diff --git a/.changeset/quick-poets-greet.md b/.changeset/quick-poets-greet.md new file mode 100644 index 00000000..4b53b523 --- /dev/null +++ b/.changeset/quick-poets-greet.md @@ -0,0 +1,7 @@ +--- +"gyp-to-cmake": minor +"cmake-rn": minor +"react-native-node-api": minor +--- + +Use `find_package` instead of `include` to locate "weak-node-api" diff --git a/.changeset/spotty-beers-repeat.md b/.changeset/spotty-beers-repeat.md new file mode 100644 index 00000000..cfa21deb --- /dev/null +++ b/.changeset/spotty-beers-repeat.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": minor +--- + +No longer exporting weakNodeApiPath, import from "weak-node-api" instead diff --git a/.changeset/tired-words-relate.md b/.changeset/tired-words-relate.md new file mode 100644 index 00000000..e1a3ec02 --- /dev/null +++ b/.changeset/tired-words-relate.md @@ -0,0 +1,5 @@ +--- +"weak-node-api": patch +--- + +Initial release! diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a221cfc4..f24d9c04 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -40,9 +40,9 @@ jobs: - run: rustup target add x86_64-linux-android - run: npm ci - run: npm run build - # Bootstrap host package to get weak-node-api and ferric-example to get types + # Bootstrap weak-node-api and ferric-example to get types # TODO: Solve this by adding an option to ferric to build only types or by committing the types into the repo as a fixture for an "init" command - - run: npm run bootstrap --workspace react-native-node-api + - run: npm run bootstrap --workspace weak-node-api - run: npm run bootstrap --workspace @react-native-node-api/ferric-example - run: npm run lint env: @@ -184,9 +184,8 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Build weak-node-api for all architectures - run: npm run build-weak-node-api:android - working-directory: packages/host + - name: Build weak-node-api for all Android architectures + run: npm run build-weak-node-api:android --workspace weak-node-api - name: Build ferric-example for all architectures run: npm run build -- --android working-directory: packages/ferric-example @@ -239,11 +238,10 @@ jobs: - run: rustup toolchain install nightly --component rust-src - run: npm ci - run: npm run build - # Build weak-node-api for all Apple architectures - - run: | - npm run prepare-weak-node-api - npm run build-weak-node-api:apple - working-directory: packages/host + - name: Build weak-node-api for all Apple architectures + run: | + npm run prepare-weak-node-api --workspace weak-node-api + npm run build-weak-node-api:apple --workspace weak-node-api # Build Ferric example for all Apple architectures - run: npx ferric --apple working-directory: packages/ferric-example diff --git a/.gitignore b/.gitignore index 6c7c7bf3..6a69674c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ dist/ # Treading the MacOS app as ephemeral apps/macos-test-app + +# Cache used by the rust analyzer +target/rust-analyzer/ diff --git a/apps/test-app/package.json b/apps/test-app/package.json index 08f55a35..2f41e8c4 100644 --- a/apps/test-app/package.json +++ b/apps/test-app/package.json @@ -42,6 +42,7 @@ "react": "19.1.0", "react-native": "0.81.4", "react-native-node-api": "*", - "react-native-test-app": "^4.4.7" + "react-native-test-app": "^4.4.7", + "weak-node-api": "*" } } diff --git a/package-lock.json b/package-lock.json index 9604cb48..845f156c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "workspaces": [ "packages/cli-utils", "packages/cmake-file-api", + "packages/weak-node-api", "packages/cmake-rn", "packages/ferric", "packages/gyp-to-cmake", @@ -32,7 +33,7 @@ "prettier": "^3.6.2", "react-native": "0.81.4", "read-pkg": "^9.0.1", - "tsx": "^4.20.5", + "tsx": "^4.20.6", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" } @@ -63,7 +64,8 @@ "react": "19.1.0", "react-native": "0.81.4", "react-native-node-api": "*", - "react-native-test-app": "^4.4.7" + "react-native-test-app": "^4.4.7", + "weak-node-api": "*" } }, "node_modules/@actions/core": { @@ -14006,9 +14008,9 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.20.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", - "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", "dependencies": { @@ -14355,6 +14357,10 @@ "defaults": "^1.0.3" } }, + "node_modules/weak-node-api": { + "resolved": "packages/weak-node-api", + "link": true + }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", @@ -14825,11 +14831,11 @@ } }, "packages/cmake-rn": { - "version": "0.5.1", + "version": "0.5.2", "dependencies": { "@react-native-node-api/cli-utils": "0.1.1", "cmake-file-api": "0.1.0", - "react-native-node-api": "0.6.1", + "react-native-node-api": "0.6.2", "zod": "^4.1.11" }, "bin": { @@ -14842,11 +14848,11 @@ }, "packages/ferric": { "name": "ferric-cli", - "version": "0.3.6", + "version": "0.3.7", "dependencies": { "@napi-rs/cli": "~3.0.3", "@react-native-node-api/cli-utils": "0.1.1", - "react-native-node-api": "0.6.1" + "react-native-node-api": "0.6.2" }, "bin": { "ferric": "bin/ferric.js" @@ -14873,7 +14879,7 @@ }, "packages/host": { "name": "react-native-node-api", - "version": "0.6.1", + "version": "0.6.2", "license": "MIT", "dependencies": { "@expo/plist": "^0.4.7", @@ -14888,12 +14894,12 @@ "devDependencies": { "@babel/core": "^7.26.10", "@babel/types": "^7.27.0", - "fswin": "^3.24.829", - "node-api-headers": "^1.5.0" + "fswin": "^3.24.829" }, "peerDependencies": { "@babel/core": "^7.26.10", - "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.79.7 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4" + "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.79.7 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4 || 0.81.5", + "weak-node-api": "0.0.1" } }, "packages/node-addon-examples": { @@ -14920,6 +14926,14 @@ "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } + }, + "packages/weak-node-api": { + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "node-api-headers": "^1.5.0", + "zod": "^4.1.11" + } } } } diff --git a/package.json b/package.json index be2dbf31..668f6c31 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "workspaces": [ "packages/cli-utils", "packages/cmake-file-api", + "packages/weak-node-api", "packages/cmake-rn", "packages/ferric", "packages/gyp-to-cmake", @@ -66,7 +67,7 @@ "prettier": "^3.6.2", "react-native": "0.81.4", "read-pkg": "^9.0.1", - "tsx": "^4.20.5", + "tsx": "^4.20.6", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" } diff --git a/packages/cmake-rn/README.md b/packages/cmake-rn/README.md index ba41aecb..61772ce2 100644 --- a/packages/cmake-rn/README.md +++ b/packages/cmake-rn/README.md @@ -10,14 +10,14 @@ Android's dynamic linker imposes restrictions on the access to global symbols (s The implementation of Node-API is split between Hermes and our host package and to avoid addons having to explicitly link against either, we've introduced a `weak-node-api` library (published in `react-native-node-api` package). This library exposes only Node-API and will have its implementation injected by the host. -To link against `weak-node-api` just include the CMake config exposed through `WEAK_NODE_API_CONFIG` and add `weak-node-api` to the `target_link_libraries` of the addon's library target. +To link against `weak-node-api` just use `find_package` to import the `weak-node-api` target and add it to the `target_link_libraries` of the addon's library target. ```cmake cmake_minimum_required(VERSION 3.15...3.31) project(tests-buffers) # Defines the "weak-node-api" target -include(${WEAK_NODE_API_CONFIG}) +find_package(weak-node-api REQUIRED CONFIG) add_library(addon SHARED addon.c) target_link_libraries(addon PRIVATE weak-node-api) diff --git a/packages/cmake-rn/src/weak-node-api.ts b/packages/cmake-rn/src/weak-node-api.ts index ee6cb154..44b2e79c 100644 --- a/packages/cmake-rn/src/weak-node-api.ts +++ b/packages/cmake-rn/src/weak-node-api.ts @@ -6,9 +6,14 @@ import { isAndroidTriplet, isAppleTriplet, SupportedTriplet, - weakNodeApiPath, } from "react-native-node-api"; +import { + applePrebuildPath, + androidPrebuildPath, + weakNodeApiCmakePath, +} from "weak-node-api"; + import { ANDROID_ARCHITECTURES } from "./platforms/android.js"; import { getNodeAddonHeadersPath, getNodeApiHeadersPath } from "./headers.js"; @@ -20,19 +25,14 @@ export function getWeakNodeApiPath( triplet: SupportedTriplet | "apple", ): string { if (triplet === "apple" || isAppleTriplet(triplet)) { - const xcframeworkPath = path.join( - weakNodeApiPath, - "weak-node-api.xcframework", - ); assert( - fs.existsSync(xcframeworkPath), - `Expected an XCFramework at ${xcframeworkPath}`, + fs.existsSync(applePrebuildPath), + `Expected an XCFramework at ${applePrebuildPath}`, ); - return xcframeworkPath; + return applePrebuildPath; } else if (isAndroidTriplet(triplet)) { const libraryPath = path.join( - weakNodeApiPath, - "weak-node-api.android.node", + androidPrebuildPath, ANDROID_ARCHITECTURES[triplet], "libweak-node-api.so", ); @@ -58,8 +58,10 @@ export function getWeakNodeApiVariables( triplet: SupportedTriplet | "apple", ): Record { return { - // Expose an includable CMake config file declaring the weak-node-api target - WEAK_NODE_API_CONFIG: path.join(weakNodeApiPath, "weak-node-api.cmake"), + // Enable use of `find_package(weak-node-api REQUIRED CONFIG)` + "weak-node-api_DIR": path.dirname(weakNodeApiCmakePath), + // Enable use of `include(${WEAK_NODE_API_CONFIG})` + WEAK_NODE_API_CONFIG: weakNodeApiCmakePath, WEAK_NODE_API_INC: getNodeApiIncludePaths().join(";"), WEAK_NODE_API_LIB: getWeakNodeApiPath(triplet), }; diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index 4671a47c..4eee45b2 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -9,7 +9,7 @@ import { UsageError, spawn, } from "@react-native-node-api/cli-utils"; -import { weakNodeApiPath } from "react-native-node-api"; +import { applePrebuildPath, androidPrebuildPath } from "weak-node-api"; import { AndroidTargetName, @@ -169,25 +169,20 @@ export function getTargetAndroidPlatform(target: AndroidTargetName) { } export function getWeakNodeApiFrameworkPath(target: AppleTargetName) { - const xcframeworkPath = joinPathAndAssertExistence( - weakNodeApiPath, - "weak-node-api.xcframework", - ); const result = APPLE_XCFRAMEWORK_SLICES_PER_TARGET[target].find((slice) => { - const candidatePath = path.join(xcframeworkPath, slice); + const candidatePath = path.join(applePrebuildPath, slice); return fs.existsSync(candidatePath); }); assert( result, `No matching slice found in weak-node-api.xcframework for target ${target}`, ); - return joinPathAndAssertExistence(xcframeworkPath, result); + return joinPathAndAssertExistence(applePrebuildPath, result); } export function getWeakNodeApiAndroidLibraryPath(target: AndroidTargetName) { return joinPathAndAssertExistence( - weakNodeApiPath, - "weak-node-api.android.node", + androidPrebuildPath, ANDROID_ARCH_PR_TARGET[target], ); } diff --git a/packages/gyp-to-cmake/src/transformer.ts b/packages/gyp-to-cmake/src/transformer.ts index 3c9b65d8..a5660eba 100644 --- a/packages/gyp-to-cmake/src/transformer.ts +++ b/packages/gyp-to-cmake/src/transformer.ts @@ -85,7 +85,7 @@ export function bindingGypToCmakeLists({ ]; if (weakNodeApi) { - lines.push(`include(\${WEAK_NODE_API_CONFIG})`, ""); + lines.push(`find_package(weak-node-api REQUIRED CONFIG)`, ""); } for (const target of gyp.targets) { diff --git a/packages/host/.gitignore b/packages/host/.gitignore index b3f12e5b..5ba3e2fe 100644 --- a/packages/host/.gitignore +++ b/packages/host/.gitignore @@ -16,12 +16,5 @@ include/ android/.cxx/ android/build/ -# Everything in weak-node-api is generated, except for the configurations -# Generated and built bia `npm run build-weak-node-api-injector` -/weak-node-api/build/ -/weak-node-api/*.xcframework -/weak-node-api/*.android.node -/weak-node-api/weak_node_api.cpp -/weak-node-api/weak_node_api.hpp # Generated via `npm run generate-weak-node-api-injector` /cpp/WeakNodeApiInjector.cpp diff --git a/packages/host/android/CMakeLists.txt b/packages/host/android/CMakeLists.txt index 3e9fd392..e4c183f7 100644 --- a/packages/host/android/CMakeLists.txt +++ b/packages/host/android/CMakeLists.txt @@ -5,12 +5,7 @@ set(CMAKE_CXX_STANDARD 20) find_package(ReactAndroid REQUIRED CONFIG) find_package(hermes-engine REQUIRED CONFIG) - -add_library(weak-node-api INTERFACE) -target_include_directories(weak-node-api INTERFACE - ../weak-node-api - ../weak-node-api/include -) +find_package(weak-node-api REQUIRED CONFIG) add_library(node-api-host SHARED src/main/cpp/OnLoad.cpp diff --git a/packages/host/android/build.gradle b/packages/host/android/build.gradle index 41c7b7bd..2f6b6be8 100644 --- a/packages/host/android/build.gradle +++ b/packages/host/android/build.gradle @@ -14,6 +14,17 @@ if (!System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR")) { ].join('\n')) } +def findWeakNodeApiDir() { + def searchDir = rootDir.toPath() + do { + def p = searchDir.resolve("node_modules/weak-node-api") + if (p.toFile().exists()) { + return p.toRealPath().toString() + } + } while (searchDir = searchDir.getParent()) + throw new GradleException("Could not find `weak-node-api`"); +} + buildscript { ext.getExtOrDefault = {name -> return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['NodeApiModules_' + name] @@ -61,8 +72,11 @@ android { sourceSets { main { manifest.srcFile "src/main/AndroidManifestNew.xml" - // Include the weak-node-api to enable a dynamic load - jniLibs.srcDirs += ["../weak-node-api/weak-node-api.android.node"] + // Include the weak-node-api native libraries directly + jniLibs.srcDirs += [ + "../../weak-node-api/build/Debug/weak-node-api.android.node", + "../../weak-node-api/build/Release/weak-node-api.android.node" + ] } } } @@ -71,7 +85,8 @@ android { compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") buildFeatures { - prefab = true + buildConfig true + prefab true } defaultConfig { @@ -82,7 +97,7 @@ android { cmake { targets "node-api-host" cppFlags "-frtti -fexceptions -Wall -fstack-protector-all" - arguments "-DANDROID_STL=c++_shared" + arguments "-DANDROID_STL=c++_shared", "-Dweak-node-api_DIR=${findWeakNodeApiDir()}" abiFilters (*reactNativeArchitectures()) buildTypes { @@ -103,10 +118,6 @@ android { } } - buildFeatures { - buildConfig true - } - buildTypes { debug { jniDebuggable true diff --git a/packages/host/package.json b/packages/host/package.json index 1edcc6ba..2f1199bf 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -43,33 +43,18 @@ ], "scripts": { "build": "tsc --build", - "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", - "generate-weak-node-api": "tsx scripts/generate-weak-node-api.ts", - "generate-weak-node-api-injector": "tsx scripts/generate-weak-node-api-injector.ts", - "prepare-weak-node-api": "node --run copy-node-api-headers && node --run generate-weak-node-api-injector && node --run generate-weak-node-api", - "build-weak-node-api": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension --source ./weak-node-api --out ./weak-node-api", - "build-weak-node-api:android": "node --run build-weak-node-api -- --android", - "build-weak-node-api:apple": "node --run build-weak-node-api -- --apple", - "build-weak-node-api:all": "node --run build-weak-node-api -- --android --apple", + "generate-weak-node-api-injector": "node scripts/generate-weak-node-api-injector.mts", "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", "test:gradle": "ENABLE_GRADLE_TESTS=true node --run test", - "bootstrap": "node --run prepare-weak-node-api && node --run build-weak-node-api", - "prerelease": "node --run prepare-weak-node-api && node --run build-weak-node-api:all" + "bootstrap": "node --run generate-weak-node-api-injector", + "prerelease": "node --run generate-weak-node-api-injector" }, "keywords": [ - "react-native", "node-api", "napi", - "node-api", "node-addon-api", "native", - "addon", - "module", - "c", - "c++", - "bindings", - "buildtools", - "cmake" + "addon" ], "author": { "name": "Callstack", @@ -92,11 +77,11 @@ "devDependencies": { "@babel/core": "^7.26.10", "@babel/types": "^7.27.0", - "fswin": "^3.24.829", - "node-api-headers": "^1.5.0" + "fswin": "^3.24.829" }, "peerDependencies": { "@babel/core": "^7.26.10", - "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.79.7 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4 || 0.81.5" + "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.79.7 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4 || 0.81.5", + "weak-node-api": "0.0.1" } } diff --git a/packages/host/react-native-node-api.podspec b/packages/host/react-native-node-api.podspec index 5ed4072a..ab4b0e75 100644 --- a/packages/host/react-native-node-api.podspec +++ b/packages/host/react-native-node-api.podspec @@ -31,10 +31,11 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/callstackincubator/react-native-node-api.git", :tag => "#{s.version}" } - s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "weak-node-api/include/*.h", "weak-node-api/*.hpp" - s.public_header_files = "weak-node-api/include/*.h" + s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}" - s.vendored_frameworks = "auto-linked/apple/*.xcframework", "weak-node-api/weak-node-api.xcframework" + s.dependency "weak-node-api" + + s.vendored_frameworks = "auto-linked/apple/*.xcframework" s.script_phase = { :name => 'Copy Node-API xcframeworks', :execution_position => :before_compile, diff --git a/packages/host/scripts/generate-weak-node-api-injector.ts b/packages/host/scripts/generate-weak-node-api-injector.mts similarity index 94% rename from packages/host/scripts/generate-weak-node-api-injector.ts rename to packages/host/scripts/generate-weak-node-api-injector.mts index d5adfd83..71acfe86 100644 --- a/packages/host/scripts/generate-weak-node-api-injector.ts +++ b/packages/host/scripts/generate-weak-node-api-injector.mts @@ -2,9 +2,9 @@ import fs from "node:fs"; import path from "node:path"; import cp from "node:child_process"; -import { FunctionDecl, getNodeApiFunctions } from "./node-api-functions"; +import { type FunctionDecl, getNodeApiFunctions } from "weak-node-api"; -export const CPP_SOURCE_PATH = path.join(__dirname, "../cpp"); +export const CPP_SOURCE_PATH = path.join(import.meta.dirname, "../cpp"); // TODO: Remove when all runtime Node API functions are implemented const IMPLEMENTED_RUNTIME_FUNCTIONS = [ @@ -28,9 +28,10 @@ const IMPLEMENTED_RUNTIME_FUNCTIONS = [ export function generateSource(functions: FunctionDecl[]) { return ` // This file is generated by react-native-node-api - #include #include #include + + #include #include #include diff --git a/packages/host/src/node/cli/program.ts b/packages/host/src/node/cli/program.ts index 85b9016f..169ca85b 100644 --- a/packages/host/src/node/cli/program.ts +++ b/packages/host/src/node/cli/program.ts @@ -1,7 +1,6 @@ import assert from "node:assert/strict"; import path from "node:path"; import { EventEmitter } from "node:stream"; -import fs from "node:fs"; import { Command, @@ -27,9 +26,8 @@ import { import { command as vendorHermes } from "./hermes"; import { packageNameOption, pathSuffixOption } from "./options"; import { linkModules, pruneLinkedModules, ModuleLinker } from "./link-modules"; -import { linkXcframework, restoreFrameworkLinks } from "./apple"; +import { linkXcframework } from "./apple"; import { linkAndroidDir } from "./android"; -import { weakNodeApiPath } from "../weak-node-api"; // We're attaching a lot of listeners when spawning in parallel EventEmitter.defaultMaxListeners = 100; @@ -171,38 +169,6 @@ program await pruneLinkedModules(platform, modules); } } - - if (apple) { - await oraPromise( - async () => { - const xcframeworkPath = path.join( - weakNodeApiPath, - "weak-node-api.xcframework", - ); - await Promise.all( - [ - path.join(xcframeworkPath, "macos-x86_64"), - path.join(xcframeworkPath, "macos-arm64"), - path.join(xcframeworkPath, "macos-arm64_x86_64"), - ].map(async (slicePath) => { - const frameworkPath = path.join( - slicePath, - "weak-node-api.framework", - ); - if (fs.existsSync(frameworkPath)) { - await restoreFrameworkLinks(frameworkPath); - } - }), - ); - }, - { - text: "Restoring weak-node-api symlinks", - successText: "Restored weak-node-api symlinks", - failText: (error) => - `Failed to restore weak-node-api symlinks: ${error.message}`, - }, - ); - } }, ), ); diff --git a/packages/host/src/node/index.ts b/packages/host/src/node/index.ts index 3071f233..1c4c69a9 100644 --- a/packages/host/src/node/index.ts +++ b/packages/host/src/node/index.ts @@ -26,5 +26,3 @@ export { determineLibraryBasename, dereferenceDirectory, } from "./path-utils.js"; - -export { weakNodeApiPath } from "./weak-node-api.js"; diff --git a/packages/host/src/node/weak-node-api.ts b/packages/host/src/node/weak-node-api.ts deleted file mode 100644 index 02e3befe..00000000 --- a/packages/host/src/node/weak-node-api.ts +++ /dev/null @@ -1,10 +0,0 @@ -import assert from "node:assert/strict"; -import fs from "node:fs"; -import path from "node:path"; - -export const weakNodeApiPath = path.resolve(__dirname, "../../weak-node-api"); - -assert( - fs.existsSync(weakNodeApiPath), - `Expected Weak Node API path to exist: ${weakNodeApiPath}`, -); diff --git a/packages/host/tsconfig.node-scripts.json b/packages/host/tsconfig.node-scripts.json index 4e11d816..b3771a38 100644 --- a/packages/host/tsconfig.node-scripts.json +++ b/packages/host/tsconfig.node-scripts.json @@ -7,6 +7,11 @@ "rootDir": "scripts", "types": ["node"] }, - "include": ["scripts/**/*.ts", "types/**/*.d.ts"], - "exclude": [] + "include": ["scripts/**/*.mts", "types/**/*.d.ts"], + "exclude": [], + "references": [ + { + "path": "../weak-node-api/tsconfig.node.json" + } + ] } diff --git a/packages/host/tsconfig.node.json b/packages/host/tsconfig.node.json index bf847c8c..e0982db2 100644 --- a/packages/host/tsconfig.node.json +++ b/packages/host/tsconfig.node.json @@ -8,5 +8,10 @@ "types": ["node"] }, "include": ["src/node/**/*.ts", "types/**/*.d.ts"], - "exclude": ["**/*.test.ts"] + "exclude": ["**/*.test.ts"], + "references": [ + { + "path": "../weak-node-api/tsconfig.node.json" + } + ] } diff --git a/packages/host/weak-node-api/weak-node-api.cmake b/packages/host/weak-node-api/weak-node-api.cmake deleted file mode 100644 index 2fda647e..00000000 --- a/packages/host/weak-node-api/weak-node-api.cmake +++ /dev/null @@ -1,6 +0,0 @@ -add_library(weak-node-api SHARED IMPORTED) - -set_target_properties(weak-node-api PROPERTIES - IMPORTED_LOCATION "${WEAK_NODE_API_LIB}" - INTERFACE_INCLUDE_DIRECTORIES "${WEAK_NODE_API_INC}" -) diff --git a/packages/node-addon-examples/tests/async/CMakeLists.txt b/packages/node-addon-examples/tests/async/CMakeLists.txt index 2b6b2b81..67e5448b 100644 --- a/packages/node-addon-examples/tests/async/CMakeLists.txt +++ b/packages/node-addon-examples/tests/async/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.15...3.31) project(async-test) -include(${WEAK_NODE_API_CONFIG}) +find_package(weak-node-api REQUIRED CONFIG) add_library(addon SHARED addon.c) diff --git a/packages/node-addon-examples/tests/buffers/CMakeLists.txt b/packages/node-addon-examples/tests/buffers/CMakeLists.txt index 8d7ac2d2..da615db2 100644 --- a/packages/node-addon-examples/tests/buffers/CMakeLists.txt +++ b/packages/node-addon-examples/tests/buffers/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.15...3.31) project(buffers-test) -include(${WEAK_NODE_API_CONFIG}) +find_package(weak-node-api REQUIRED CONFIG) add_library(addon SHARED addon.c) diff --git a/packages/weak-node-api/.gitignore b/packages/weak-node-api/.gitignore new file mode 100644 index 00000000..30ea0716 --- /dev/null +++ b/packages/weak-node-api/.gitignore @@ -0,0 +1,11 @@ + +# Everything in weak-node-api is generated, except for the configurations +# Generated and built via `npm run bootstrap` +/build/ +/*.xcframework +/*.android.node +/generated/weak_node_api.cpp +/generated/weak_node_api.hpp + +# Copied from node-api-headers by scripts/copy-node-api-headers.ts +/include/ diff --git a/packages/host/weak-node-api/CMakeLists.txt b/packages/weak-node-api/CMakeLists.txt similarity index 96% rename from packages/host/weak-node-api/CMakeLists.txt rename to packages/weak-node-api/CMakeLists.txt index 08422d62..de2784e0 100644 --- a/packages/host/weak-node-api/CMakeLists.txt +++ b/packages/weak-node-api/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.15) project(weak-node-api) add_library(${PROJECT_NAME} SHARED - weak_node_api.cpp + generated/weak_node_api.cpp ) # Stripping the prefix from the library name diff --git a/packages/weak-node-api/README.md b/packages/weak-node-api/README.md new file mode 100644 index 00000000..073c9121 --- /dev/null +++ b/packages/weak-node-api/README.md @@ -0,0 +1,19 @@ +# Weak Node-API + +A clean linkable interface for Node-API and with runtime-injectable implementation. + +This package is part of the [Node-API for React Native](https://github.com/callstackincubator/react-native-node-api) project, which brings Node-API support to React Native applications. However, it can be used independently in any context where an indirect / weak Node-API implementation is needed. + +## Why is this needed? + +Android's dynamic linker restricts access to global symbols—dynamic libraries must explicitly declare dependencies as `DT_NEEDED` to access symbols. In the context of React Native, the Node-API implementation is split between Hermes and a host runtime, native addons built for Android would otherwise need to explicitly link against both - which is not ideal for multiple reasons. + +This library provides a solution by: + +- Exposing only Node-API functions without implementation +- Allowing runtime injection of the actual implementation by the host +- Eliminating the need for addons to suppress undefined symbol errors + +## Is this usable in the context of Node.js? + +While originally designed for React Native's split Node-API implementation, this approach could potentially be adapted for Node.js scenarios where addons need to link with undefined symbols allowed. Usage patterns and examples for Node.js contexts are being explored and this pattern could eventually be upstreamed to Node.js itself, benefiting the broader Node-API ecosystem. diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json new file mode 100644 index 00000000..63cfb410 --- /dev/null +++ b/packages/weak-node-api/package.json @@ -0,0 +1,65 @@ +{ + "name": "weak-node-api", + "version": "0.0.1", + "description": "A linkable and runtime-injectable Node-API", + "homepage": "https://github.com/callstackincubator/react-native-node-api", + "repository": { + "type": "git", + "url": "git+https://github.com/callstackincubator/react-native-node-api.git", + "directory": "packages/weak-node-api" + }, + "main": "dist/index.js", + "type": "module", + "files": [ + "dist", + "!dist/**/*.test.d.ts", + "!dist/**/*.test.d.ts.map", + "include", + "build/Debug", + "build/Release", + "*.podspec", + "*.cmake" + ], + "scripts": { + "build": "tsc --build", + "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", + "generate-weak-node-api": "tsx scripts/generate-weak-node-api.ts", + "prepare-weak-node-api": "node --run copy-node-api-headers && node --run generate-weak-node-api", + "build-weak-node-api": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension", + "build-weak-node-api:android": "node --run build-weak-node-api -- --android", + "build-weak-node-api:apple": "node --run build-weak-node-api -- --apple", + "build-weak-node-api:all": "node --run build-weak-node-api -- --android --apple", + "test": "tsx --test --test-reporter=@reporters/github --test-reporter-destination=stdout --test-reporter=spec --test-reporter-destination=stdout src/node/**/*.test.ts src/node/*.test.ts", + "bootstrap": "node --run prepare-weak-node-api && node --run build-weak-node-api", + "prerelease": "node --run prepare-weak-node-api && node --run build-weak-node-api:all" + }, + "keywords": [ + "react-native", + "napi", + "node-api", + "node-addon-api", + "native", + "addon", + "module", + "c", + "c++", + "bindings", + "buildtools", + "cmake" + ], + "author": { + "name": "Callstack", + "url": "https://github.com/callstackincubator" + }, + "contributors": [ + { + "name": "Kræn Hansen", + "url": "https://github.com/kraenhansen" + } + ], + "license": "MIT", + "devDependencies": { + "node-api-headers": "^1.5.0", + "zod": "^4.1.11" + } +} diff --git a/packages/host/scripts/copy-node-api-headers.ts b/packages/weak-node-api/scripts/copy-node-api-headers.ts similarity index 82% rename from packages/host/scripts/copy-node-api-headers.ts rename to packages/weak-node-api/scripts/copy-node-api-headers.ts index 9632627e..2bfd43dc 100644 --- a/packages/host/scripts/copy-node-api-headers.ts +++ b/packages/weak-node-api/scripts/copy-node-api-headers.ts @@ -3,7 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { include_dir as includeSourcePath } from "node-api-headers"; -const includeDestinationPath = path.join(__dirname, "../weak-node-api/include"); +const includeDestinationPath = path.join(import.meta.dirname, "../include"); assert(fs.existsSync(includeSourcePath), `Expected ${includeSourcePath}`); console.log(`Copying ${includeSourcePath} to ${includeDestinationPath}`); fs.cpSync(includeSourcePath, includeDestinationPath, { recursive: true }); diff --git a/packages/host/scripts/generate-weak-node-api.ts b/packages/weak-node-api/scripts/generate-weak-node-api.ts similarity index 87% rename from packages/host/scripts/generate-weak-node-api.ts rename to packages/weak-node-api/scripts/generate-weak-node-api.ts index 1090de41..b9a8736c 100644 --- a/packages/host/scripts/generate-weak-node-api.ts +++ b/packages/weak-node-api/scripts/generate-weak-node-api.ts @@ -2,9 +2,12 @@ import fs from "node:fs"; import path from "node:path"; import cp from "node:child_process"; -import { FunctionDecl, getNodeApiFunctions } from "./node-api-functions"; +import { + FunctionDecl, + getNodeApiFunctions, +} from "../src/node-api-functions.js"; -export const WEAK_NODE_API_PATH = path.join(__dirname, "../weak-node-api"); +export const OUTPUT_PATH = path.join(import.meta.dirname, "../generated"); /** * Generates source code for a version script for the given Node API version. @@ -67,17 +70,17 @@ export function generateSource(functions: FunctionDecl[]) { } async function run() { - await fs.promises.mkdir(WEAK_NODE_API_PATH, { recursive: true }); + await fs.promises.mkdir(OUTPUT_PATH, { recursive: true }); const nodeApiFunctions = getNodeApiFunctions(); const header = generateHeader(nodeApiFunctions); - const headerPath = path.join(WEAK_NODE_API_PATH, "weak_node_api.hpp"); + const headerPath = path.join(OUTPUT_PATH, "weak_node_api.hpp"); await fs.promises.writeFile(headerPath, header, "utf-8"); cp.spawnSync("clang-format", ["-i", headerPath], { stdio: "inherit" }); const source = generateSource(nodeApiFunctions); - const sourcePath = path.join(WEAK_NODE_API_PATH, "weak_node_api.cpp"); + const sourcePath = path.join(OUTPUT_PATH, "weak_node_api.cpp"); await fs.promises.writeFile(sourcePath, source, "utf-8"); cp.spawnSync("clang-format", ["-i", sourcePath], { stdio: "inherit" }); } diff --git a/packages/weak-node-api/src/index.ts b/packages/weak-node-api/src/index.ts new file mode 100644 index 00000000..fbd5337a --- /dev/null +++ b/packages/weak-node-api/src/index.ts @@ -0,0 +1,2 @@ +export * from "./weak-node-api.js"; +export * from "./node-api-functions.js"; diff --git a/packages/host/scripts/node-api-functions.ts b/packages/weak-node-api/src/node-api-functions.ts similarity index 100% rename from packages/host/scripts/node-api-functions.ts rename to packages/weak-node-api/src/node-api-functions.ts diff --git a/packages/weak-node-api/src/restore-xcframework-symlinks.ts b/packages/weak-node-api/src/restore-xcframework-symlinks.ts new file mode 100644 index 00000000..41373809 --- /dev/null +++ b/packages/weak-node-api/src/restore-xcframework-symlinks.ts @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; + +import { applePrebuildPath } from "./weak-node-api.js"; + +async function restoreVersionedFrameworkSymlinks(frameworkPath: string) { + const currentLinkPath = path.join(frameworkPath, "Versions", "Current"); + + if (!fs.existsSync(currentLinkPath)) { + await fs.promises.symlink("A", currentLinkPath); + } + + const binaryLinkPath = path.join(frameworkPath, "weak-node-api"); + + if (!fs.existsSync(binaryLinkPath)) { + await fs.promises.symlink("Versions/Current/weak-node-api", binaryLinkPath); + } + + const resourcesLinkPath = path.join(frameworkPath, "Resources"); + + if (!fs.existsSync(resourcesLinkPath)) { + await fs.promises.symlink("Versions/Current/Resources", resourcesLinkPath); + } +} + +if (process.platform === "darwin") { + assert( + fs.existsSync(applePrebuildPath), + `Expected an Xcframework at ${applePrebuildPath}`, + ); + + const macosFrameworkPath = path.join( + applePrebuildPath, + "macos-arm64_x86_64", + "weak-node-api.framework", + ); + + if (fs.existsSync(macosFrameworkPath)) { + await restoreVersionedFrameworkSymlinks(macosFrameworkPath); + } +} diff --git a/packages/weak-node-api/src/weak-node-api.ts b/packages/weak-node-api/src/weak-node-api.ts new file mode 100644 index 00000000..87802461 --- /dev/null +++ b/packages/weak-node-api/src/weak-node-api.ts @@ -0,0 +1,26 @@ +import path from "node:path"; +import fs from "node:fs"; + +export const weakNodeApiPath = path.resolve(import.meta.dirname, ".."); + +const debugOutputPath = path.resolve(weakNodeApiPath, "build", "Debug"); +const releaseOutputPath = path.resolve(weakNodeApiPath, "build", "Release"); + +export const outputPath = fs.existsSync(debugOutputPath) + ? debugOutputPath + : releaseOutputPath; + +export const applePrebuildPath = path.resolve( + outputPath, + "weak-node-api.xcframework", +); + +export const androidPrebuildPath = path.resolve( + outputPath, + "weak-node-api.android.node", +); + +export const weakNodeApiCmakePath = path.resolve( + weakNodeApiPath, + "weak-node-api-config.cmake", +); diff --git a/packages/weak-node-api/tsconfig.json b/packages/weak-node-api/tsconfig.json new file mode 100644 index 00000000..f08015f8 --- /dev/null +++ b/packages/weak-node-api/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true + }, + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.node-scripts.json" } + ] +} diff --git a/packages/weak-node-api/tsconfig.node-scripts.json b/packages/weak-node-api/tsconfig.node-scripts.json new file mode 100644 index 00000000..4bc586fa --- /dev/null +++ b/packages/weak-node-api/tsconfig.node-scripts.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.node.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "rootDir": "scripts", + "types": ["node"] + }, + "include": ["scripts/**/*.ts", "types/**/*.d.ts"], + "exclude": [], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/weak-node-api/tsconfig.node.json b/packages/weak-node-api/tsconfig.node.json new file mode 100644 index 00000000..0028899f --- /dev/null +++ b/packages/weak-node-api/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "compilerOptions": { + "composite": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*.ts", "types/**/*.d.ts"], + "exclude": ["**/*.test.ts"] +} diff --git a/packages/host/types/node-api-headers/index.d.ts b/packages/weak-node-api/types/node-api-headers/index.d.ts similarity index 100% rename from packages/host/types/node-api-headers/index.d.ts rename to packages/weak-node-api/types/node-api-headers/index.d.ts diff --git a/packages/weak-node-api/weak-node-api-config.cmake b/packages/weak-node-api/weak-node-api-config.cmake new file mode 100644 index 00000000..7d4d4a05 --- /dev/null +++ b/packages/weak-node-api/weak-node-api-config.cmake @@ -0,0 +1,39 @@ + +# Get the current file directory +get_filename_component(WEAK_NODE_API_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) + +if(NOT DEFINED WEAK_NODE_API_LIB) + # Auto-detect library path for Android NDK builds + if(ANDROID) + # Define the library path pattern for Android + set(WEAK_NODE_API_LIB_PATH "weak-node-api.android.node/${ANDROID_ABI}/libweak-node-api.so") + + # Try Debug first, then Release using the packaged Android node structure + set(WEAK_NODE_API_LIB_DEBUG "${WEAK_NODE_API_CMAKE_DIR}/build/Debug/${WEAK_NODE_API_LIB_PATH}") + set(WEAK_NODE_API_LIB_RELEASE "${WEAK_NODE_API_CMAKE_DIR}/build/Release/${WEAK_NODE_API_LIB_PATH}") + + if(EXISTS "${WEAK_NODE_API_LIB_DEBUG}") + set(WEAK_NODE_API_LIB "${WEAK_NODE_API_LIB_DEBUG}") + message(STATUS "Using Debug weak-node-api library: ${WEAK_NODE_API_LIB}") + elseif(EXISTS "${WEAK_NODE_API_LIB_RELEASE}") + set(WEAK_NODE_API_LIB "${WEAK_NODE_API_LIB_RELEASE}") + message(STATUS "Using Release weak-node-api library: ${WEAK_NODE_API_LIB}") + else() + message(FATAL_ERROR "Could not find weak-node-api library for Android ABI ${ANDROID_ABI}. Expected at:\n ${WEAK_NODE_API_LIB_DEBUG}\n ${WEAK_NODE_API_LIB_RELEASE}") + endif() + else() + message(FATAL_ERROR "WEAK_NODE_API_LIB is not set") + endif() +endif() + +if(NOT DEFINED WEAK_NODE_API_INC) + set(WEAK_NODE_API_INC "${WEAK_NODE_API_CMAKE_DIR}/include;${WEAK_NODE_API_CMAKE_DIR}/generated") + message(STATUS "Using weak-node-api include directories: ${WEAK_NODE_API_INC}") +endif() + +add_library(weak-node-api SHARED IMPORTED) + +set_target_properties(weak-node-api PROPERTIES + IMPORTED_LOCATION "${WEAK_NODE_API_LIB}" + INTERFACE_INCLUDE_DIRECTORIES "${WEAK_NODE_API_INC}" +) diff --git a/packages/weak-node-api/weak-node-api.podspec b/packages/weak-node-api/weak-node-api.podspec new file mode 100644 index 00000000..236c6ec2 --- /dev/null +++ b/packages/weak-node-api/weak-node-api.podspec @@ -0,0 +1,34 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +# We need to restore symlinks in the versioned framework directories, +# as these are not preserved when in the archive uploaded to NPM +unless defined?(@restored) + RESTORE_COMMAND = "node '#{File.join(__dir__, "dist/restore-xcframework-symlinks.js")}'" + Pod::UI.info("[weak-node-api] ".green + "Restoring symbolic links in Xcframework") + system(RESTORE_COMMAND) or raise "Failed to restore symlinks in Xcframework" + # Setting a flag to avoid running this command on every require + @restored = true +end + +Pod::Spec.new do |s| + s.name = package["name"] + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.source = { :git => "https://github.com/callstackincubator/react-native-node-api.git", :tag => "#{s.version}" } + + # TODO: These headers could be included in the Xcframework? + # (tracked by https://github.com/callstackincubator/react-native-node-api/issues/315) + s.source_files = "generated/*.hpp", "include/*.h" + s.public_header_files = "generated/*.hpp", "include/*.h" + + s.vendored_frameworks = "build/*/weak-node-api.xcframework" + + # Avoiding the header dir to allow for idiomatic Node-API includes + s.header_dir = nil +end \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 4ca25b74..2d49bdb1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ { "path": "./packages/cmake-rn/tsconfig.json" }, { "path": "./packages/ferric/tsconfig.json" }, { "path": "./packages/node-addon-examples/tsconfig.json" }, - { "path": "./packages/node-tests/tsconfig.json" } + { "path": "./packages/node-tests/tsconfig.json" }, + { "path": "./packages/weak-node-api/tsconfig.json" } ] }