From 3dafec4196cba9885aac9ec916a8d36efa0df6ac Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 22:02:31 +0000 Subject: [PATCH 1/3] Link addons against a generated Hermes stub via --node-api-linkage Generate a link-time stub of libhermesvm from getNodeApiFunctions() and build it alongside weak-node-api. Add `--node-api-linkage ` to cmake-rn (defined once in the host), replacing `--no-weak-node-api-linkage`, and a neutral `node-api-linkage` CMake target that follows the selected mode. The async test addon now links the neutral target and node-addon-examples builds with `--node-api-linkage hermes`, so the iOS and Android test app jobs exercise an addon calling Hermes directly, and verify its linkage and imports against the app's real Hermes. Refs #494 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01U2xSE4PR7T7WFxexRhihBT --- .changeset/node-api-linkage.md | 9 +++ .github/workflows/check.yml | 25 ++++++++ packages/cmake-rn/README.md | 17 +++++ .../cmake/node-api-linkage-config.cmake | 21 ++++++ packages/cmake-rn/package.json | 3 +- packages/cmake-rn/src/cli.ts | 8 +-- packages/cmake-rn/src/platforms/android.ts | 6 +- packages/cmake-rn/src/platforms/apple.ts | 6 +- packages/cmake-rn/src/weak-node-api.ts | 64 ++++++++++++++++--- packages/host/src/node/cli/options.ts | 11 ++++ packages/host/src/node/index.ts | 6 ++ packages/node-addon-examples/package.json | 2 +- .../scripts/verify-hermes-linkage.sh | 64 +++++++++++++++++++ .../tests/async/CMakeLists.txt | 4 +- packages/weak-node-api/.gitignore | 1 + packages/weak-node-api/CMakeLists.txt | 5 ++ packages/weak-node-api/package.json | 5 +- packages/weak-node-api/scripts/generate.ts | 11 ++++ .../weak-node-api/scripts/generators/stub.ts | 15 +++++ packages/weak-node-api/src/weak-node-api.ts | 19 ++++++ packages/weak-node-api/stub/CMakeLists.txt | 22 +++++++ 21 files changed, 299 insertions(+), 25 deletions(-) create mode 100644 .changeset/node-api-linkage.md create mode 100644 packages/cmake-rn/cmake/node-api-linkage-config.cmake create mode 100755 packages/node-addon-examples/scripts/verify-hermes-linkage.sh create mode 100644 packages/weak-node-api/scripts/generators/stub.ts create mode 100644 packages/weak-node-api/stub/CMakeLists.txt diff --git a/.changeset/node-api-linkage.md b/.changeset/node-api-linkage.md new file mode 100644 index 00000000..038e41f3 --- /dev/null +++ b/.changeset/node-api-linkage.md @@ -0,0 +1,9 @@ +--- +"cmake-rn": major +"react-native-node-api": minor +"weak-node-api": minor +--- + +Add `--node-api-linkage ` to `cmake-rn`, replacing `--no-weak-node-api-linkage` (use `--node-api-linkage none`). + +Addons linking the new neutral `node-api-linkage` CMake target (`find_package(node-api-linkage REQUIRED CONFIG)`) link whatever the mode selects. With `hermes` they link a link-time stub of `libhermesvm`, so they record `hermesvm` and call Hermes directly instead of going through `weak-node-api`. The stub is generated from the Node-API headers and built alongside `weak-node-api`; it never ships in an app. Addons naming the `weak-node-api` target directly keep linking it in every mode but `none`. The default stays `weak-node-api`. diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 3bf7be7d..bbcb6927 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -318,6 +318,14 @@ jobs: - name: Build test app run: xcodebuild archive -workspace ReactTestApp.xcworkspace -configuration Release -scheme ReactTestApp -destination "generic/platform=iOS Simulator" -archivePath ./build/test-app.xcarchive CODE_SIGN_IDENTITY="-" CODE_SIGNING_ALLOWED=YES C_COMPILER_LAUNCHER=ccache CLANG_ENABLE_EXPLICIT_MODULES_WITH_COMPILER_LAUNCHER=YES working-directory: apps/test-app/ios + # The async test addon is built with `--node-api-linkage hermes` (#494). + - name: Verify the async addon links Hermes directly + run: | + ./scripts/verify-hermes-linkage.sh \ + tests/async/build/RelWithDebInfo/addon.apple.node/ios-*-simulator/addon.framework/addon \ + ../../apps/test-app/ios/build/test-app.xcarchive/Products/Applications/ReactTestApp.app/Frameworks/hermesvm.framework/hermesvm \ + ../weak-node-api/stub/build/Release/hermesvm.xcframework/ios-*-simulator/hermesvm.framework/hermesvm + working-directory: packages/node-addon-examples - name: Run test app run: | # Install the app @@ -467,6 +475,23 @@ jobs: sleep 5 # Stop logcat kill $LOGCAT_PID || true + # The async test addon is built with `--node-api-linkage hermes` (#494). + - name: Verify the async addon links Hermes directly + run: | + RNTA_DIR=$(node -p "path.dirname(require.resolve('react-native-test-app/package.json', { paths: ['../../apps/test-app'] }))") + APK=$(find ../../apps/test-app/android "$RNTA_DIR/android" -name '*.apk' 2>/dev/null | head -n 1) + if [ -z "$APK" ]; then + echo "::error::Found no APK of the test app" + exit 1 + fi + echo "Extracting Hermes from $APK" + unzip -o -j "$APK" lib/x86_64/libhermesvm.so -d "$RUNNER_TEMP/apk" + export LLVM_BIN="$ANDROID_HOME/ndk/$NDK_VERSION/toolchains/llvm/prebuilt/linux-x86_64/bin" + ./scripts/verify-hermes-linkage.sh \ + tests/async/build/RelWithDebInfo/addon.android.node/x86_64/libaddon.so \ + "$RUNNER_TEMP/apk/libhermesvm.so" \ + ../weak-node-api/stub/build/Release/hermesvm.android.node/x86_64/libhermesvm.so + working-directory: packages/node-addon-examples - name: Upload device logs if: always() uses: actions/upload-artifact@v7 diff --git a/packages/cmake-rn/README.md b/packages/cmake-rn/README.md index d3584a29..159e67f3 100644 --- a/packages/cmake-rn/README.md +++ b/packages/cmake-rn/README.md @@ -86,4 +86,21 @@ else() endif() ``` +### Choosing what to link with `--node-api-linkage` + +An addon can instead link the neutral `node-api-linkage` target, which links whatever `--node-api-linkage ` selects: + +- `weak-node-api` (the default): the `weak-node-api` library described above. +- `hermes`: a link-time stub of Hermes' `hermesvm` library, so the addon records `hermesvm` and calls Hermes' Node-API directly. The stub is never loaded or shipped: the app's own Hermes provides the implementation at runtime. This requires a Hermes exporting Node-API (such as the one vendored by `react-native-node-api`), and the addon gets Hermes' own `napi_fatal_error` instead of the host's. +- `none`: neither target is defined. + +```cmake +# Defines the "node-api-linkage" target +find_package(node-api-linkage REQUIRED CONFIG) + +target_link_libraries(addon PRIVATE node-api-linkage) +``` + +The `weak-node-api` target stays available in every mode but `none`, so addons linking it directly can be built alongside. + This is different from how `cmake-js` "injects" the Node-API for linking (via `${CMAKE_JS_INC}`, `${CMAKE_JS_SRC}` and `${CMAKE_JS_LIB}`). To allow for interoperability between these tools, we inject these when you pass `--cmake-js` to `cmake-rn`. diff --git a/packages/cmake-rn/cmake/node-api-linkage-config.cmake b/packages/cmake-rn/cmake/node-api-linkage-config.cmake new file mode 100644 index 00000000..7a775502 --- /dev/null +++ b/packages/cmake-rn/cmake/node-api-linkage-config.cmake @@ -0,0 +1,21 @@ +# Defines the `node-api-linkage` target, which links whatever cmake-rn's +# --node-api-linkage selected (NODE_API_LINKAGE). + +if(TARGET node-api-linkage) + return() +endif() + +foreach(variable NODE_API_LINKAGE NODE_API_LINKAGE_LIB NODE_API_LINKAGE_INC) + if(NOT DEFINED ${variable}) + message(FATAL_ERROR "${variable} is not set: build with cmake-rn and a --node-api-linkage other than 'none'") + endif() +endforeach() + +message(STATUS "Linking Node-API against ${NODE_API_LINKAGE}: ${NODE_API_LINKAGE_LIB}") + +add_library(node-api-linkage SHARED IMPORTED) + +set_target_properties(node-api-linkage PROPERTIES + IMPORTED_LOCATION "${NODE_API_LINKAGE_LIB}" + INTERFACE_INCLUDE_DIRECTORIES "${NODE_API_LINKAGE_INC}" +) diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index 1b8df35e..2852eb4d 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -17,7 +17,8 @@ "dist", "!dist/**/*.test.d.ts", "!dist/**/*.test.d.ts.map", - "templates" + "templates", + "cmake" ], "scripts": { "build": "tsc", diff --git a/packages/cmake-rn/src/cli.ts b/packages/cmake-rn/src/cli.ts index 41a362a9..d00a38b1 100644 --- a/packages/cmake-rn/src/cli.ts +++ b/packages/cmake-rn/src/cli.ts @@ -14,6 +14,7 @@ import { pLimit, InvalidArgumentError, } from "@react-native-node-api/cli-utils"; +import { nodeApiLinkageOption } from "react-native-node-api"; import { platforms, @@ -115,11 +116,6 @@ const noAutoLinkOption = new Option( "Don't mark the output as auto-linkable by react-native-node-api", ); -const noWeakNodeApiLinkageOption = new Option( - "--no-weak-node-api-linkage", - "Don't pass the path of the weak-node-api library from react-native-node-api", -); - const cmakeJsOption = new Option( "--cmake-js", "Define CMAKE_JS_* variables used for compatibility with cmake-js", @@ -159,7 +155,7 @@ let buildCommand = new Command("build") .addOption(targetOption) .addOption(stripOption) .addOption(noAutoLinkOption) - .addOption(noWeakNodeApiLinkageOption) + .addOption(nodeApiLinkageOption) .addOption(cmakeJsOption) .addOption(ccachePathOption) .addOption(concurrencyOption); diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index 61dc0fc1..08f351df 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -18,7 +18,7 @@ import type { BaseOpts, Platform } from "./types.js"; import { getArtifactName, toDefineArguments } from "../helpers.js"; import { getCmakeJSVariables, - getWeakNodeApiVariables, + getNodeApiLinkageVariables, } from "../weak-node-api.js"; import { isNinjaAvailable } from "../ninja.js"; @@ -183,7 +183,7 @@ export const platform: Platform = { source, define, build, - weakNodeApiLinkage, + nodeApiLinkage, cmakeJs, ccachePath, }, @@ -228,7 +228,7 @@ export const platform: Platform = { "--toolchain", toolchainPath, ...toDefineArguments([ - ...(weakNodeApiLinkage ? [getWeakNodeApiVariables(triplet)] : []), + getNodeApiLinkageVariables(nodeApiLinkage, triplet), ...(cmakeJs ? [getCmakeJSVariables(triplet)] : []), ...commonDefinitions, { diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index 82740793..80dbf12c 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -22,7 +22,7 @@ import * as cmakeFileApi from "cmake-file-api"; import { getArtifactName, toDefineArguments } from "../helpers.js"; import { getCmakeJSVariables, - getWeakNodeApiVariables, + getNodeApiLinkageVariables, } from "../weak-node-api.js"; import * as z from "zod"; @@ -292,7 +292,7 @@ export const platform: Platform = { }, async configure( triplets, - { source, build, define, weakNodeApiLinkage, cmakeJs, ccachePath }, + { source, build, define, nodeApiLinkage, cmakeJs, ccachePath }, ) { // When using ccache, we're creating symlinks for the clang and clang++ binaries to the ccache binary // This is needed for ccache to understand it's being invoked as clang and clang++ respectively. @@ -343,7 +343,7 @@ export const platform: Platform = { "Xcode", ...toDefineArguments([ ...define, - weakNodeApiLinkage ? getWeakNodeApiVariables("apple") : {}, + getNodeApiLinkageVariables(nodeApiLinkage, "apple"), cmakeJs ? getCmakeJSVariables("apple") : {}, compilerDefinitions, { diff --git a/packages/cmake-rn/src/weak-node-api.ts b/packages/cmake-rn/src/weak-node-api.ts index 44b2e79c..e69613e8 100644 --- a/packages/cmake-rn/src/weak-node-api.ts +++ b/packages/cmake-rn/src/weak-node-api.ts @@ -5,12 +5,15 @@ import path from "node:path"; import { isAndroidTriplet, isAppleTriplet, + type NodeApiLinkage, SupportedTriplet, } from "react-native-node-api"; import { applePrebuildPath, androidPrebuildPath, + hermesStubApplePrebuildPath, + hermesStubAndroidPrebuildPath, weakNodeApiCmakePath, } from "weak-node-api"; @@ -21,20 +24,24 @@ export function toCmakePath(input: string) { return input.split(path.win32.sep).join(path.posix.sep); } -export function getWeakNodeApiPath( +const NODE_API_CONFIG_DIR = path.resolve(import.meta.dirname, "../cmake"); + +function getPrebuildPath( triplet: SupportedTriplet | "apple", + { + apple, + android, + libraryName, + }: { apple: string; android: string; libraryName: string }, ): string { if (triplet === "apple" || isAppleTriplet(triplet)) { - assert( - fs.existsSync(applePrebuildPath), - `Expected an XCFramework at ${applePrebuildPath}`, - ); - return applePrebuildPath; + assert(fs.existsSync(apple), `Expected an XCFramework at ${apple}`); + return apple; } else if (isAndroidTriplet(triplet)) { const libraryPath = path.join( - androidPrebuildPath, + android, ANDROID_ARCHITECTURES[triplet], - "libweak-node-api.so", + `lib${libraryName}.so`, ); assert(fs.existsSync(libraryPath), `Expected library at ${libraryPath}`); return libraryPath; @@ -43,6 +50,22 @@ export function getWeakNodeApiPath( } } +export function getWeakNodeApiPath(triplet: SupportedTriplet | "apple") { + return getPrebuildPath(triplet, { + apple: applePrebuildPath, + android: androidPrebuildPath, + libraryName: "weak-node-api", + }); +} + +export function getHermesStubPath(triplet: SupportedTriplet | "apple") { + return getPrebuildPath(triplet, { + apple: hermesStubApplePrebuildPath, + android: hermesStubAndroidPrebuildPath, + libraryName: "hermesvm", + }); +} + function getNodeApiIncludePaths() { const includePaths = [getNodeApiHeadersPath(), getNodeAddonHeadersPath()]; for (const includePath of includePaths) { @@ -67,6 +90,31 @@ export function getWeakNodeApiVariables( }; } +/** + * Variables for the selected --node-api-linkage: the neutral `node-api-linkage` + * target follows the mode, while the `weak-node-api` target stays available so + * addons naming it directly keep building alongside. + */ +export function getNodeApiLinkageVariables( + linkage: NodeApiLinkage, + triplet: SupportedTriplet | "apple", +): Record { + if (linkage === "none") { + return {}; + } + return { + ...getWeakNodeApiVariables(triplet), + // Enable use of `find_package(node-api-linkage REQUIRED CONFIG)` + "node-api-linkage_DIR": NODE_API_CONFIG_DIR, + NODE_API_LINKAGE: linkage, + NODE_API_LINKAGE_INC: getNodeApiIncludePaths().join(";"), + NODE_API_LINKAGE_LIB: + linkage === "hermes" + ? getHermesStubPath(triplet) + : getWeakNodeApiPath(triplet), + }; +} + /** * For compatibility with cmake-js */ diff --git a/packages/host/src/node/cli/options.ts b/packages/host/src/node/cli/options.ts index eb059b42..7680e475 100644 --- a/packages/host/src/node/cli/options.ts +++ b/packages/host/src/node/cli/options.ts @@ -26,3 +26,14 @@ export const pathSuffixOption = new Option( ) .choices(LIBRARY_NAMING_CHOICES) .default(NODE_API_PATH_SUFFIX || "strip"); + +export const NODE_API_LINKAGES = ["weak-node-api", "hermes", "none"] as const; + +export type NodeApiLinkage = (typeof NODE_API_LINKAGES)[number]; + +export const nodeApiLinkageOption = new Option( + "--node-api-linkage ", + "What addons link against for Node-API: weak-node-api, the Hermes library (via a link-time stub) or nothing at all", +) + .choices(NODE_API_LINKAGES) + .default("weak-node-api" satisfies NodeApiLinkage); diff --git a/packages/host/src/node/index.ts b/packages/host/src/node/index.ts index db13383c..59ae16fa 100644 --- a/packages/host/src/node/index.ts +++ b/packages/host/src/node/index.ts @@ -28,3 +28,9 @@ export { } from "./path-utils.js"; export { readAndParsePlist } from "./plist.js"; + +export { + NODE_API_LINKAGES, + type NodeApiLinkage, + nodeApiLinkageOption, +} from "./cli/options.js"; diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index d1961e1f..484e63f0 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -23,7 +23,7 @@ "copy-examples": "tsx scripts/copy-examples.mts", "gyp-to-cmake": "gyp-to-cmake --namespaced-targets --weak-node-api .", "generate-root-project": "tsx scripts/generate-root-project.mts", - "build": "cmake-rn --configuration RelWithDebInfo", + "build": "cmake-rn --configuration RelWithDebInfo --node-api-linkage hermes", "copy-and-build": "node --run copy-examples && node --run gyp-to-cmake && node --run generate-root-project && node --run build", "verify": "tsx scripts/verify-prebuilds.mts", "test": "node --run copy-and-build && node --run verify", diff --git a/packages/node-addon-examples/scripts/verify-hermes-linkage.sh b/packages/node-addon-examples/scripts/verify-hermes-linkage.sh new file mode 100755 index 00000000..f1a64ee6 --- /dev/null +++ b/packages/node-addon-examples/scripts/verify-hermes-linkage.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Verifies an addon built with `--node-api-linkage hermes` links Hermes +# directly and that every Node-API function it imports is exported by the real +# Hermes library. Optionally reports stub exports the real library lacks. +# +# Usage: verify-hermes-linkage.sh [] +# +# On Linux, set LLVM_BIN to a directory with llvm-nm and llvm-readelf (e.g. the NDK's). +set -euo pipefail + +addon=$1 +hermes=$2 +stub=${3:-} + +if [[ "$(uname)" == "Darwin" ]]; then + nm_cmd=(nm) + dependencies() { otool -L "$1" | awk 'NR > 1 && !/:$/ { print $1 }'; } +else + nm_cmd=("${LLVM_BIN:+$LLVM_BIN/}llvm-nm" -D) + dependencies() { + "${LLVM_BIN:+$LLVM_BIN/}llvm-readelf" -d "$1" | sed -n 's/.*Shared library: \[\(.*\)\]/\1/p' + } +fi + +# Mach-O prefixes C symbols with an underscore. +node_api_symbols() { + "${nm_cmd[@]}" "$@" | awk '{ print $NF }' | sed 's/^_//' | { grep -E '^(napi|node_api)_' || true; } | sort -u +} +imported() { node_api_symbols --undefined-only "$1"; } +exported() { node_api_symbols --defined-only --extern-only "$1"; } + +addon_dependencies=$(dependencies "$addon") +echo "Dependencies of $addon:" +echo "$addon_dependencies" | sed 's/^/ /' + +if ! grep -q 'hermesvm' <<<"$addon_dependencies"; then + echo "::error::Expected $addon to link hermesvm" + exit 1 +fi +if grep -q 'weak-node-api' <<<"$addon_dependencies"; then + echo "::error::Expected $addon not to link weak-node-api" + exit 1 +fi + +imports=$(imported "$addon") +if [[ -z "$imports" ]]; then + echo "::error::Found no Node-API imports in $addon" + exit 1 +fi +missing=$(comm -23 <(echo "$imports") <(exported "$hermes")) +if [[ -n "$missing" ]]; then + echo "::error::Hermes doesn't export Node-API functions imported by $addon: $(echo $missing)" + exit 1 +fi +echo "All $(echo "$imports" | wc -l | tr -d ' ') Node-API functions imported by the addon are exported by $hermes" + +if [[ -n "$stub" ]]; then + unexported=$(comm -23 <(exported "$stub") <(exported "$hermes")) + if [[ -n "$unexported" ]]; then + echo "::warning::The stub exports Node-API functions Hermes doesn't: $(echo $unexported)" + else + echo "Every Node-API function exported by the stub is exported by $hermes" + fi +fi diff --git a/packages/node-addon-examples/tests/async/CMakeLists.txt b/packages/node-addon-examples/tests/async/CMakeLists.txt index ca9532d7..f60727e4 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) -find_package(weak-node-api REQUIRED CONFIG) +find_package(node-api-linkage REQUIRED CONFIG) add_library(async-test-addon SHARED addon.c) @@ -24,5 +24,5 @@ else() ) endif() -target_link_libraries(async-test-addon PRIVATE weak-node-api) +target_link_libraries(async-test-addon PRIVATE node-api-linkage) target_compile_features(async-test-addon PRIVATE cxx_std_17) \ No newline at end of file diff --git a/packages/weak-node-api/.gitignore b/packages/weak-node-api/.gitignore index 83e62995..b2058cd6 100644 --- a/packages/weak-node-api/.gitignore +++ b/packages/weak-node-api/.gitignore @@ -12,3 +12,4 @@ # Clang cache /.cache/ +/stub/build/ diff --git a/packages/weak-node-api/CMakeLists.txt b/packages/weak-node-api/CMakeLists.txt index d23d1551..f108c445 100644 --- a/packages/weak-node-api/CMakeLists.txt +++ b/packages/weak-node-api/CMakeLists.txt @@ -68,3 +68,8 @@ if(BUILD_TESTS) enable_testing() add_subdirectory(tests) endif() + +option(BUILD_NODE_API_STUB "Build a link-time stub of a library exporting Node-API" OFF) +if(BUILD_NODE_API_STUB) + add_subdirectory(stub) +endif() diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json index 163192a6..1d6b27ea 100644 --- a/packages/weak-node-api/package.json +++ b/packages/weak-node-api/package.json @@ -20,6 +20,9 @@ "generated", "build/Debug", "build/Release", + "stub/CMakeLists.txt", + "stub/build/Debug", + "stub/build/Release", "*.podspec", "*.cmake" ], @@ -28,7 +31,7 @@ "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", "generate": "tsx scripts/generate.ts", "prebuild:prepare": "node --run copy-node-api-headers && node --run generate", - "prebuild:build": "cmake-rn --no-auto-link --no-weak-node-api-linkage --xcframework-extension", + "prebuild:build": "cmake-rn --no-auto-link --node-api-linkage none --xcframework-extension -D BUILD_NODE_API_STUB=ON", "prebuild:build:android": "node --run prebuild:build -- --android", "prebuild:build:apple": "node --run prebuild:build -- --apple", "prebuild:build:all": "node --run prebuild:build -- --android --apple", diff --git a/packages/weak-node-api/scripts/generate.ts b/packages/weak-node-api/scripts/generate.ts index fc21e485..f6e8a703 100644 --- a/packages/weak-node-api/scripts/generate.ts +++ b/packages/weak-node-api/scripts/generate.ts @@ -10,6 +10,7 @@ import { import * as weakNodeApiGenerator from "./generators/weak-node-api.js"; import * as hostGenerator from "./generators/NodeApiHost.js"; +import * as stubGenerator from "./generators/stub.js"; export const OUTPUT_PATH = path.join(import.meta.dirname, "../generated"); @@ -87,6 +88,16 @@ async function run() { Provides the implementation for deferring Node-API function calls from addons into a Node-API host. `, }); + await generateFile({ + functions, + fileName: "node_api_stub.cpp", + generator: stubGenerator.generateSource, + headingComment: ` + @brief Link-time stub of a library exporting Node-API. + + Lets an addon link directly against a Node-API provider (such as Hermes) without having it installed. + `, + }); } run().catch((err) => { diff --git a/packages/weak-node-api/scripts/generators/stub.ts b/packages/weak-node-api/scripts/generators/stub.ts new file mode 100644 index 00000000..3b3ac46a --- /dev/null +++ b/packages/weak-node-api/scripts/generators/stub.ts @@ -0,0 +1,15 @@ +import type { FunctionDecl } from "../../src/node-api-functions.js"; +import { generateFunction } from "./shared.js"; + +export function generateSource(functions: FunctionDecl[]) { + return ` + #include + #include // abort() + + #define WEAK_NODE_API_UNREACHABLE __builtin_unreachable() + + ${functions + .map((fn) => generateFunction({ ...fn, extern: true, body: "abort();" })) + .join("\n")} + `; +} diff --git a/packages/weak-node-api/src/weak-node-api.ts b/packages/weak-node-api/src/weak-node-api.ts index 87802461..0f2302a5 100644 --- a/packages/weak-node-api/src/weak-node-api.ts +++ b/packages/weak-node-api/src/weak-node-api.ts @@ -24,3 +24,22 @@ export const weakNodeApiCmakePath = path.resolve( weakNodeApiPath, "weak-node-api-config.cmake", ); + +const stubPath = path.resolve(weakNodeApiPath, "stub", "build"); + +const stubOutputPath = fs.existsSync(path.resolve(stubPath, "Debug")) + ? path.resolve(stubPath, "Debug") + : path.resolve(stubPath, "Release"); + +/** + * Link-time stub of the Hermes library, letting addons link Node-API directly against Hermes. + */ +export const hermesStubApplePrebuildPath = path.resolve( + stubOutputPath, + "hermesvm.xcframework", +); + +export const hermesStubAndroidPrebuildPath = path.resolve( + stubOutputPath, + "hermesvm.android.node", +); diff --git a/packages/weak-node-api/stub/CMakeLists.txt b/packages/weak-node-api/stub/CMakeLists.txt new file mode 100644 index 00000000..3951dd79 --- /dev/null +++ b/packages/weak-node-api/stub/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.19) +project(node-api-stub) + +set(NODE_API_STUB_LIBRARY_NAME "hermesvm" CACHE STRING "Name of the library the stub stands in for") + +add_library(node-api-stub SHARED ${CMAKE_CURRENT_SOURCE_DIR}/../generated/node_api_stub.cpp) + +target_include_directories(node-api-stub PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include) +target_compile_definitions(node-api-stub PRIVATE NAPI_VERSION=10) + +if(APPLE) + set_target_properties(node-api-stub PROPERTIES + FRAMEWORK TRUE + MACOSX_FRAMEWORK_IDENTIFIER com.callstack.node-api-stub.${NODE_API_STUB_LIBRARY_NAME} + XCODE_ATTRIBUTE_SKIP_INSTALL NO + OUTPUT_NAME ${NODE_API_STUB_LIBRARY_NAME} + ) +else() + set_target_properties(node-api-stub PROPERTIES + OUTPUT_NAME ${NODE_API_STUB_LIBRARY_NAME} + ) +endif() From 27f63dc5bb38e06b47b0e458edd00165b157b7f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 22:03:09 +0000 Subject: [PATCH 2/3] Trigger CI with labels attached Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01U2xSE4PR7T7WFxexRhihBT From e75c50abb214ea218f3b4e6773114be38c4bd0c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 22:47:02 +0000 Subject: [PATCH 3/3] Move the stub into a node-api-stub package and link it as `node-api` - Add a `node-api-stub` package holding `getNodeApiFunctions()`, `generateFunction` and the stub generator with its CMake project. weak-node-api's generator and the host's injector generator now read the function list from it, and weak-node-api no longer builds a stub. - In `hermes` mode, cmake-rn adds the stub to the addon's own build (and leaves it out of the prebuilds), so the addon doesn't need weak-node-api to be built. - Rename the CMake target to `node-api` (`node-api-config.cmake`). - Remove `tests/async/binding.gyp`, which made gyp-to-cmake overwrite the hand-written CMakeLists.txt with one linking weak-node-api. - Port verify-hermes-linkage to TypeScript, comparing the real Hermes against node-api-headers' symbol list instead of a stub binary. Refs #494 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01U2xSE4PR7T7WFxexRhihBT --- .changeset/node-api-linkage.md | 5 +- .github/workflows/check.yml | 10 +- packages/cmake-rn/README.md | 12 +- packages/cmake-rn/cmake/node-api-config.cmake | 32 +++++ .../cmake/node-api-linkage-config.cmake | 21 ---- packages/cmake-rn/package.json | 3 +- packages/cmake-rn/src/platforms/android.ts | 2 + packages/cmake-rn/src/platforms/apple.ts | 2 + packages/cmake-rn/src/weak-node-api.ts | 73 +++++------- packages/cmake-rn/tsconfig.node.json | 7 +- packages/host/package.json | 1 + packages/host/scripts/generate-injector.mts | 2 +- packages/host/tsconfig.node-scripts.json | 2 +- packages/node-addon-examples/package.json | 4 +- .../scripts/verify-hermes-linkage.mts | 110 ++++++++++++++++++ .../scripts/verify-hermes-linkage.sh | 64 ---------- .../tests/async/CMakeLists.txt | 4 +- .../tests/async/binding.gyp | 8 -- .../tests/async/package.json | 3 +- .../tsconfig.node-scripts.json | 7 +- packages/node-api-stub/.gitignore | 2 + .../stub => node-api-stub}/CMakeLists.txt | 9 +- packages/node-api-stub/README.md | 11 ++ packages/node-api-stub/package.json | 50 ++++++++ .../node-api-stub/scripts/generate-stub.ts | 34 ++++++ .../src/generate-function.ts} | 8 +- packages/node-api-stub/src/index.ts | 3 + .../src/node-api-functions.ts | 0 packages/node-api-stub/src/node-api-stub.ts | 8 ++ packages/node-api-stub/tsconfig.json | 10 ++ .../node-api-stub/tsconfig.node-scripts.json | 13 +++ packages/node-api-stub/tsconfig.node.json | 12 ++ .../types/node-api-headers/index.d.ts | 0 packages/weak-node-api/.gitignore | 1 - packages/weak-node-api/CMakeLists.txt | 5 - packages/weak-node-api/package.json | 10 +- .../scripts/copy-node-api-headers.ts | 2 +- packages/weak-node-api/scripts/generate.ts | 16 +-- .../scripts/generators/NodeApiHost.ts | 2 +- .../weak-node-api/scripts/generators/stub.ts | 15 --- .../scripts/generators/weak-node-api.ts | 4 +- packages/weak-node-api/src/index.ts | 1 - packages/weak-node-api/src/weak-node-api.ts | 19 --- .../weak-node-api/tsconfig.node-scripts.json | 5 +- pnpm-lock.yaml | 28 +++-- tsconfig.json | 1 + 46 files changed, 401 insertions(+), 240 deletions(-) create mode 100644 packages/cmake-rn/cmake/node-api-config.cmake delete mode 100644 packages/cmake-rn/cmake/node-api-linkage-config.cmake create mode 100644 packages/node-addon-examples/scripts/verify-hermes-linkage.mts delete mode 100755 packages/node-addon-examples/scripts/verify-hermes-linkage.sh delete mode 100644 packages/node-addon-examples/tests/async/binding.gyp create mode 100644 packages/node-api-stub/.gitignore rename packages/{weak-node-api/stub => node-api-stub}/CMakeLists.txt (66%) create mode 100644 packages/node-api-stub/README.md create mode 100644 packages/node-api-stub/package.json create mode 100644 packages/node-api-stub/scripts/generate-stub.ts rename packages/{weak-node-api/scripts/generators/shared.ts => node-api-stub/src/generate-function.ts} (74%) create mode 100644 packages/node-api-stub/src/index.ts rename packages/{weak-node-api => node-api-stub}/src/node-api-functions.ts (100%) create mode 100644 packages/node-api-stub/src/node-api-stub.ts create mode 100644 packages/node-api-stub/tsconfig.json create mode 100644 packages/node-api-stub/tsconfig.node-scripts.json create mode 100644 packages/node-api-stub/tsconfig.node.json rename packages/{weak-node-api => node-api-stub}/types/node-api-headers/index.d.ts (100%) delete mode 100644 packages/weak-node-api/scripts/generators/stub.ts diff --git a/.changeset/node-api-linkage.md b/.changeset/node-api-linkage.md index 038e41f3..3e5f4028 100644 --- a/.changeset/node-api-linkage.md +++ b/.changeset/node-api-linkage.md @@ -2,8 +2,11 @@ "cmake-rn": major "react-native-node-api": minor "weak-node-api": minor +"node-api-stub": minor --- Add `--node-api-linkage ` to `cmake-rn`, replacing `--no-weak-node-api-linkage` (use `--node-api-linkage none`). -Addons linking the new neutral `node-api-linkage` CMake target (`find_package(node-api-linkage REQUIRED CONFIG)`) link whatever the mode selects. With `hermes` they link a link-time stub of `libhermesvm`, so they record `hermesvm` and call Hermes directly instead of going through `weak-node-api`. The stub is generated from the Node-API headers and built alongside `weak-node-api`; it never ships in an app. Addons naming the `weak-node-api` target directly keep linking it in every mode but `none`. The default stays `weak-node-api`. +Addons linking the new `node-api` CMake target (`find_package(node-api REQUIRED CONFIG)`) link whatever the mode selects. With `hermes` they link a link-time stub of `libhermesvm`, built along with the addon, so they record `hermesvm` and call Hermes directly instead of going through `weak-node-api`, which doesn't need to be built. The stub never ships in an app. Addons naming the `weak-node-api` target directly keep building alongside. The default stays `weak-node-api`. + +The new `node-api-stub` package holds the stub and `getNodeApiFunctions()`, which moved there from `weak-node-api`. diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index bbcb6927..8ffd4b68 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -321,10 +321,9 @@ jobs: # The async test addon is built with `--node-api-linkage hermes` (#494). - name: Verify the async addon links Hermes directly run: | - ./scripts/verify-hermes-linkage.sh \ + node --run verify:hermes-linkage -- \ tests/async/build/RelWithDebInfo/addon.apple.node/ios-*-simulator/addon.framework/addon \ - ../../apps/test-app/ios/build/test-app.xcarchive/Products/Applications/ReactTestApp.app/Frameworks/hermesvm.framework/hermesvm \ - ../weak-node-api/stub/build/Release/hermesvm.xcframework/ios-*-simulator/hermesvm.framework/hermesvm + ../../apps/test-app/ios/build/test-app.xcarchive/Products/Applications/ReactTestApp.app/Frameworks/hermesvm.framework/hermesvm working-directory: packages/node-addon-examples - name: Run test app run: | @@ -487,10 +486,9 @@ jobs: echo "Extracting Hermes from $APK" unzip -o -j "$APK" lib/x86_64/libhermesvm.so -d "$RUNNER_TEMP/apk" export LLVM_BIN="$ANDROID_HOME/ndk/$NDK_VERSION/toolchains/llvm/prebuilt/linux-x86_64/bin" - ./scripts/verify-hermes-linkage.sh \ + node --run verify:hermes-linkage -- \ tests/async/build/RelWithDebInfo/addon.android.node/x86_64/libaddon.so \ - "$RUNNER_TEMP/apk/libhermesvm.so" \ - ../weak-node-api/stub/build/Release/hermesvm.android.node/x86_64/libhermesvm.so + "$RUNNER_TEMP/apk/libhermesvm.so" working-directory: packages/node-addon-examples - name: Upload device logs if: always() diff --git a/packages/cmake-rn/README.md b/packages/cmake-rn/README.md index 159e67f3..58be78ef 100644 --- a/packages/cmake-rn/README.md +++ b/packages/cmake-rn/README.md @@ -88,19 +88,19 @@ endif() ### Choosing what to link with `--node-api-linkage` -An addon can instead link the neutral `node-api-linkage` target, which links whatever `--node-api-linkage ` selects: +An addon can instead link the `node-api` target, which links whatever `--node-api-linkage ` selects: - `weak-node-api` (the default): the `weak-node-api` library described above. -- `hermes`: a link-time stub of Hermes' `hermesvm` library, so the addon records `hermesvm` and calls Hermes' Node-API directly. The stub is never loaded or shipped: the app's own Hermes provides the implementation at runtime. This requires a Hermes exporting Node-API (such as the one vendored by `react-native-node-api`), and the addon gets Hermes' own `napi_fatal_error` instead of the host's. +- `hermes`: a link-time stub of Hermes' `hermesvm` library from [`node-api-stub`](../node-api-stub), built along with the addon, so the addon records `hermesvm` and calls Hermes' Node-API directly. The stub is never shipped: the app's own Hermes provides the implementation at runtime. This doesn't need `weak-node-api` to be built, but requires a Hermes exporting Node-API (such as the one vendored by `react-native-node-api`), and the addon gets Hermes' own `napi_fatal_error` instead of the host's. - `none`: neither target is defined. ```cmake -# Defines the "node-api-linkage" target -find_package(node-api-linkage REQUIRED CONFIG) +# Defines the "node-api" target +find_package(node-api REQUIRED CONFIG) -target_link_libraries(addon PRIVATE node-api-linkage) +target_link_libraries(addon PRIVATE node-api) ``` -The `weak-node-api` target stays available in every mode but `none`, so addons linking it directly can be built alongside. +Addons linking the `weak-node-api` target directly can be built alongside: it's defined in the `weak-node-api` mode, and in the `hermes` mode whenever `weak-node-api` is built. This is different from how `cmake-js` "injects" the Node-API for linking (via `${CMAKE_JS_INC}`, `${CMAKE_JS_SRC}` and `${CMAKE_JS_LIB}`). To allow for interoperability between these tools, we inject these when you pass `--cmake-js` to `cmake-rn`. diff --git a/packages/cmake-rn/cmake/node-api-config.cmake b/packages/cmake-rn/cmake/node-api-config.cmake new file mode 100644 index 00000000..dcce56d4 --- /dev/null +++ b/packages/cmake-rn/cmake/node-api-config.cmake @@ -0,0 +1,32 @@ +# Defines the `node-api` target, linking what cmake-rn's --node-api-linkage +# selected (NODE_API_LINKAGE). + +if(TARGET node-api) + return() +endif() + +foreach(variable NODE_API_LINKAGE NODE_API_INC) + if(NOT DEFINED ${variable}) + message(FATAL_ERROR "${variable} is not set: build with cmake-rn and a --node-api-linkage other than 'none'") + endif() +endforeach() + +if(NODE_API_LINKAGE STREQUAL "hermes") + if(NOT TARGET node-api-stub) + add_subdirectory("${NODE_API_STUB_DIR}" "${CMAKE_BINARY_DIR}/node-api-stub") + endif() + add_library(node-api INTERFACE) + target_link_libraries(node-api INTERFACE node-api-stub) + target_include_directories(node-api INTERFACE ${NODE_API_INC}) + message(STATUS "Linking Node-API against a stub of Hermes") +else() + if(NOT DEFINED NODE_API_LIB) + message(FATAL_ERROR "NODE_API_LIB is not set") + endif() + add_library(node-api SHARED IMPORTED) + set_target_properties(node-api PROPERTIES + IMPORTED_LOCATION "${NODE_API_LIB}" + INTERFACE_INCLUDE_DIRECTORIES "${NODE_API_INC}" + ) + message(STATUS "Linking Node-API against ${NODE_API_LINKAGE}: ${NODE_API_LIB}") +endif() diff --git a/packages/cmake-rn/cmake/node-api-linkage-config.cmake b/packages/cmake-rn/cmake/node-api-linkage-config.cmake deleted file mode 100644 index 7a775502..00000000 --- a/packages/cmake-rn/cmake/node-api-linkage-config.cmake +++ /dev/null @@ -1,21 +0,0 @@ -# Defines the `node-api-linkage` target, which links whatever cmake-rn's -# --node-api-linkage selected (NODE_API_LINKAGE). - -if(TARGET node-api-linkage) - return() -endif() - -foreach(variable NODE_API_LINKAGE NODE_API_LINKAGE_LIB NODE_API_LINKAGE_INC) - if(NOT DEFINED ${variable}) - message(FATAL_ERROR "${variable} is not set: build with cmake-rn and a --node-api-linkage other than 'none'") - endif() -endforeach() - -message(STATUS "Linking Node-API against ${NODE_API_LINKAGE}: ${NODE_API_LINKAGE_LIB}") - -add_library(node-api-linkage SHARED IMPORTED) - -set_target_properties(node-api-linkage PROPERTIES - IMPORTED_LOCATION "${NODE_API_LINKAGE_LIB}" - INTERFACE_INCLUDE_DIRECTORIES "${NODE_API_LINKAGE_INC}" -) diff --git a/packages/cmake-rn/package.json b/packages/cmake-rn/package.json index 2852eb4d..ac88854a 100644 --- a/packages/cmake-rn/package.json +++ b/packages/cmake-rn/package.json @@ -30,7 +30,8 @@ "cmake-file-api": "workspace:*", "react-native-node-api": "workspace:*", "zod": "^4.1.11", - "weak-node-api": "workspace:*" + "weak-node-api": "workspace:*", + "node-api-stub": "workspace:*" }, "peerDependencies": { "node-addon-api": "^8.3.1", diff --git a/packages/cmake-rn/src/platforms/android.ts b/packages/cmake-rn/src/platforms/android.ts index 08f351df..788b9dbf 100644 --- a/packages/cmake-rn/src/platforms/android.ts +++ b/packages/cmake-rn/src/platforms/android.ts @@ -13,6 +13,7 @@ import { AndroidTriplet as Triplet, } from "react-native-node-api"; import * as cmakeFileApi from "cmake-file-api"; +import { NODE_API_STUB_TARGET } from "node-api-stub"; import type { BaseOpts, Platform } from "./types.js"; import { getArtifactName, toDefineArguments } from "../helpers.js"; @@ -280,6 +281,7 @@ export const platform: Platform = { const sharedLibraries = targets.filter( ({ type, name }) => type === "SHARED_LIBRARY" && + name !== NODE_API_STUB_TARGET && (target.length === 0 || target.includes(name)), ); await Promise.all( diff --git a/packages/cmake-rn/src/platforms/apple.ts b/packages/cmake-rn/src/platforms/apple.ts index 80dbf12c..90ca9481 100644 --- a/packages/cmake-rn/src/platforms/apple.ts +++ b/packages/cmake-rn/src/platforms/apple.ts @@ -19,6 +19,7 @@ import { import type { Platform } from "./types.js"; import * as cmakeFileApi from "cmake-file-api"; +import { NODE_API_STUB_TARGET } from "node-api-stub"; import { getArtifactName, toDefineArguments } from "../helpers.js"; import { getCmakeJSVariables, @@ -189,6 +190,7 @@ async function readCmakeSharedLibraryTargets( return targets.filter( ({ type, name }) => type === "SHARED_LIBRARY" && + name !== NODE_API_STUB_TARGET && (target.length === 0 || target.includes(name)), ); } diff --git a/packages/cmake-rn/src/weak-node-api.ts b/packages/cmake-rn/src/weak-node-api.ts index e69613e8..d13f3a2b 100644 --- a/packages/cmake-rn/src/weak-node-api.ts +++ b/packages/cmake-rn/src/weak-node-api.ts @@ -12,10 +12,9 @@ import { import { applePrebuildPath, androidPrebuildPath, - hermesStubApplePrebuildPath, - hermesStubAndroidPrebuildPath, weakNodeApiCmakePath, } from "weak-node-api"; +import { nodeApiStubPath } from "node-api-stub"; import { ANDROID_ARCHITECTURES } from "./platforms/android.js"; import { getNodeAddonHeadersPath, getNodeApiHeadersPath } from "./headers.js"; @@ -26,44 +25,26 @@ export function toCmakePath(input: string) { const NODE_API_CONFIG_DIR = path.resolve(import.meta.dirname, "../cmake"); -function getPrebuildPath( - triplet: SupportedTriplet | "apple", - { - apple, - android, - libraryName, - }: { apple: string; android: string; libraryName: string }, -): string { +function findWeakNodeApiPath(triplet: SupportedTriplet | "apple") { if (triplet === "apple" || isAppleTriplet(triplet)) { - assert(fs.existsSync(apple), `Expected an XCFramework at ${apple}`); - return apple; + return applePrebuildPath; } else if (isAndroidTriplet(triplet)) { - const libraryPath = path.join( - android, + return path.join( + androidPrebuildPath, ANDROID_ARCHITECTURES[triplet], - `lib${libraryName}.so`, + "libweak-node-api.so", ); - assert(fs.existsSync(libraryPath), `Expected library at ${libraryPath}`); - return libraryPath; } else { throw new Error(`Unexpected triplet: ${triplet as string}`); } } -export function getWeakNodeApiPath(triplet: SupportedTriplet | "apple") { - return getPrebuildPath(triplet, { - apple: applePrebuildPath, - android: androidPrebuildPath, - libraryName: "weak-node-api", - }); -} - -export function getHermesStubPath(triplet: SupportedTriplet | "apple") { - return getPrebuildPath(triplet, { - apple: hermesStubApplePrebuildPath, - android: hermesStubAndroidPrebuildPath, - libraryName: "hermesvm", - }); +export function getWeakNodeApiPath( + triplet: SupportedTriplet | "apple", +): string { + const result = findWeakNodeApiPath(triplet); + assert(fs.existsSync(result), `Expected weak-node-api at ${result}`); + return result; } function getNodeApiIncludePaths() { @@ -91,9 +72,7 @@ export function getWeakNodeApiVariables( } /** - * Variables for the selected --node-api-linkage: the neutral `node-api-linkage` - * target follows the mode, while the `weak-node-api` target stays available so - * addons naming it directly keep building alongside. + * Variables for the selected --node-api-linkage, which the `node-api` target follows. */ export function getNodeApiLinkageVariables( linkage: NodeApiLinkage, @@ -102,16 +81,26 @@ export function getNodeApiLinkageVariables( if (linkage === "none") { return {}; } + const nodeApiVariables = { + // Enable use of `find_package(node-api REQUIRED CONFIG)` + "node-api_DIR": NODE_API_CONFIG_DIR, + NODE_API_LINKAGE: linkage, + NODE_API_INC: getNodeApiIncludePaths().join(";"), + }; + if (linkage === "hermes") { + return { + // Keeps the `weak-node-api` target available to addons naming it, without requiring it + ...(fs.existsSync(findWeakNodeApiPath(triplet)) + ? getWeakNodeApiVariables(triplet) + : {}), + ...nodeApiVariables, + NODE_API_STUB_DIR: nodeApiStubPath, + }; + } return { ...getWeakNodeApiVariables(triplet), - // Enable use of `find_package(node-api-linkage REQUIRED CONFIG)` - "node-api-linkage_DIR": NODE_API_CONFIG_DIR, - NODE_API_LINKAGE: linkage, - NODE_API_LINKAGE_INC: getNodeApiIncludePaths().join(";"), - NODE_API_LINKAGE_LIB: - linkage === "hermes" - ? getHermesStubPath(triplet) - : getWeakNodeApiPath(triplet), + ...nodeApiVariables, + NODE_API_LIB: getWeakNodeApiPath(triplet), }; } diff --git a/packages/cmake-rn/tsconfig.node.json b/packages/cmake-rn/tsconfig.node.json index d6472ce0..11edc397 100644 --- a/packages/cmake-rn/tsconfig.node.json +++ b/packages/cmake-rn/tsconfig.node.json @@ -8,5 +8,10 @@ "types": ["node"] }, "include": ["src/**/*.ts"], - "exclude": ["**/*.test.ts"] + "exclude": ["**/*.test.ts"], + "references": [ + { + "path": "../node-api-stub/tsconfig.node.json" + } + ] } diff --git a/packages/host/package.json b/packages/host/package.json index e31b98c1..13784d3e 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -88,6 +88,7 @@ "@babel/types": "^7.27.0", "@types/babel__core": "^7.20.5", "fswin": "^3.24.829", + "node-api-stub": "workspace:*", "weak-node-api": "workspace:*" }, "peerDependencies": { diff --git a/packages/host/scripts/generate-injector.mts b/packages/host/scripts/generate-injector.mts index c58bb2f5..a045c3e3 100644 --- a/packages/host/scripts/generate-injector.mts +++ b/packages/host/scripts/generate-injector.mts @@ -2,7 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import cp from "node:child_process"; -import { type FunctionDecl, getNodeApiFunctions } from "weak-node-api"; +import { type FunctionDecl, getNodeApiFunctions } from "node-api-stub"; export const CPP_SOURCE_PATH = path.join(import.meta.dirname, "../cpp"); diff --git a/packages/host/tsconfig.node-scripts.json b/packages/host/tsconfig.node-scripts.json index b3771a38..5addfb19 100644 --- a/packages/host/tsconfig.node-scripts.json +++ b/packages/host/tsconfig.node-scripts.json @@ -11,7 +11,7 @@ "exclude": [], "references": [ { - "path": "../weak-node-api/tsconfig.node.json" + "path": "../node-api-stub/tsconfig.node.json" } ] } diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index 484e63f0..c03d5bdd 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -27,12 +27,14 @@ "copy-and-build": "node --run copy-examples && node --run gyp-to-cmake && node --run generate-root-project && node --run build", "verify": "tsx scripts/verify-prebuilds.mts", "test": "node --run copy-and-build && node --run verify", - "bootstrap": "node --run copy-and-build" + "bootstrap": "node --run copy-and-build", + "verify:hermes-linkage": "tsx scripts/verify-hermes-linkage.mts" }, "devDependencies": { "cmake-rn": "workspace:*", "gyp-to-cmake": "workspace:*", "node-addon-examples": "github:nodejs/node-addon-examples#4b7dd86a85644610e6de80154df9acac9329b509", + "node-api-stub": "workspace:*", "read-pkg": "^9.0.1", "weak-node-api": "workspace:*" }, diff --git a/packages/node-addon-examples/scripts/verify-hermes-linkage.mts b/packages/node-addon-examples/scripts/verify-hermes-linkage.mts new file mode 100644 index 00000000..5f5003a3 --- /dev/null +++ b/packages/node-addon-examples/scripts/verify-hermes-linkage.mts @@ -0,0 +1,110 @@ +/** + * Verifies an addon built with `--node-api-linkage hermes` links Hermes + * directly and that every Node-API function it imports is exported by the real + * Hermes library, reporting Node-API functions that library doesn't export. + * + * Usage: verify-hermes-linkage.mts + * + * On Linux, set LLVM_BIN to a directory with llvm-nm and llvm-readelf (e.g. the NDK's). + */ +import assert from "node:assert/strict"; +import cp from "node:child_process"; +import path from "node:path"; + +import { nodeApiHeaders } from "node-api-stub"; + +const [addonPath, hermesPath] = process.argv.slice(2); +assert(addonPath && hermesPath, "Usage: "); + +const isDarwin = process.platform === "darwin"; + +function llvmTool(name: string) { + const { LLVM_BIN } = process.env; + return LLVM_BIN ? path.join(LLVM_BIN, name) : name; +} + +function run(command: string, args: string[]) { + return cp.execFileSync(command, args, { + encoding: "utf8", + maxBuffer: 64 * 1024 * 1024, + }); +} + +function getDependencies(binaryPath: string) { + if (isDarwin) { + return run("otool", ["-L", binaryPath]) + .split("\n") + .filter((line) => /^\s/.test(line)) + .map((line) => line.trim().split(" ")[0]); + } else { + return [ + ...run(llvmTool("llvm-readelf"), ["-d", binaryPath]).matchAll( + /Shared library: \[(.+)\]/g, + ), + ].map(([, name]) => name); + } +} + +function getNodeApiSymbols(binaryPath: string, filter: string[]) { + const args = isDarwin ? filter : ["-D", ...filter]; + const output = run(isDarwin ? "nm" : llvmTool("llvm-nm"), [ + ...args, + binaryPath, + ]); + return new Set( + output + .split("\n") + .map((line) => line.trim().split(/\s+/).at(-1) ?? "") + // Mach-O prefixes C symbols with an underscore + .map((name) => (isDarwin ? name.replace(/^_/, "") : name)) + .filter((name) => /^(napi|node_api)_/.test(name)), + ); +} + +function fail(message: string): never { + console.log(`::error::${message}`); + process.exit(1); +} + +const dependencies = getDependencies(addonPath); +console.log(`Dependencies of ${addonPath}:`); +for (const dependency of dependencies) { + console.log(` ${dependency}`); +} +if (!dependencies.some((name) => name.includes("hermesvm"))) { + fail(`Expected ${addonPath} to link hermesvm`); +} +if (dependencies.some((name) => name.includes("weak-node-api"))) { + fail(`Expected ${addonPath} not to link weak-node-api`); +} + +const imported = getNodeApiSymbols(addonPath, ["--undefined-only"]); +const exported = getNodeApiSymbols(hermesPath, [ + "--defined-only", + "--extern-only", +]); + +if (imported.size === 0) { + fail(`Found no Node-API imports in ${addonPath}`); +} +const missing = [...imported].filter((name) => !exported.has(name)); +if (missing.length > 0) { + fail( + `${hermesPath} doesn't export Node-API functions imported by ${addonPath}: ${missing.join(", ")}`, + ); +} +console.log( + `All ${imported.size} Node-API functions imported by the addon are exported by ${hermesPath}`, +); + +const { js_native_api_symbols, node_api_symbols } = nodeApiHeaders.symbols.v10; +const unexported = [...js_native_api_symbols, ...node_api_symbols].filter( + (name) => !exported.has(name), +); +if (unexported.length > 0) { + console.log( + `::warning::${hermesPath} doesn't export Node-API functions the stub does: ${unexported.join(", ")}`, + ); +} else { + console.log(`${hermesPath} exports every Node-API function the stub does`); +} diff --git a/packages/node-addon-examples/scripts/verify-hermes-linkage.sh b/packages/node-addon-examples/scripts/verify-hermes-linkage.sh deleted file mode 100755 index f1a64ee6..00000000 --- a/packages/node-addon-examples/scripts/verify-hermes-linkage.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -# Verifies an addon built with `--node-api-linkage hermes` links Hermes -# directly and that every Node-API function it imports is exported by the real -# Hermes library. Optionally reports stub exports the real library lacks. -# -# Usage: verify-hermes-linkage.sh [] -# -# On Linux, set LLVM_BIN to a directory with llvm-nm and llvm-readelf (e.g. the NDK's). -set -euo pipefail - -addon=$1 -hermes=$2 -stub=${3:-} - -if [[ "$(uname)" == "Darwin" ]]; then - nm_cmd=(nm) - dependencies() { otool -L "$1" | awk 'NR > 1 && !/:$/ { print $1 }'; } -else - nm_cmd=("${LLVM_BIN:+$LLVM_BIN/}llvm-nm" -D) - dependencies() { - "${LLVM_BIN:+$LLVM_BIN/}llvm-readelf" -d "$1" | sed -n 's/.*Shared library: \[\(.*\)\]/\1/p' - } -fi - -# Mach-O prefixes C symbols with an underscore. -node_api_symbols() { - "${nm_cmd[@]}" "$@" | awk '{ print $NF }' | sed 's/^_//' | { grep -E '^(napi|node_api)_' || true; } | sort -u -} -imported() { node_api_symbols --undefined-only "$1"; } -exported() { node_api_symbols --defined-only --extern-only "$1"; } - -addon_dependencies=$(dependencies "$addon") -echo "Dependencies of $addon:" -echo "$addon_dependencies" | sed 's/^/ /' - -if ! grep -q 'hermesvm' <<<"$addon_dependencies"; then - echo "::error::Expected $addon to link hermesvm" - exit 1 -fi -if grep -q 'weak-node-api' <<<"$addon_dependencies"; then - echo "::error::Expected $addon not to link weak-node-api" - exit 1 -fi - -imports=$(imported "$addon") -if [[ -z "$imports" ]]; then - echo "::error::Found no Node-API imports in $addon" - exit 1 -fi -missing=$(comm -23 <(echo "$imports") <(exported "$hermes")) -if [[ -n "$missing" ]]; then - echo "::error::Hermes doesn't export Node-API functions imported by $addon: $(echo $missing)" - exit 1 -fi -echo "All $(echo "$imports" | wc -l | tr -d ' ') Node-API functions imported by the addon are exported by $hermes" - -if [[ -n "$stub" ]]; then - unexported=$(comm -23 <(exported "$stub") <(exported "$hermes")) - if [[ -n "$unexported" ]]; then - echo "::warning::The stub exports Node-API functions Hermes doesn't: $(echo $unexported)" - else - echo "Every Node-API function exported by the stub is exported by $hermes" - fi -fi diff --git a/packages/node-addon-examples/tests/async/CMakeLists.txt b/packages/node-addon-examples/tests/async/CMakeLists.txt index f60727e4..db6fcee6 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) -find_package(node-api-linkage REQUIRED CONFIG) +find_package(node-api REQUIRED CONFIG) add_library(async-test-addon SHARED addon.c) @@ -24,5 +24,5 @@ else() ) endif() -target_link_libraries(async-test-addon PRIVATE node-api-linkage) +target_link_libraries(async-test-addon PRIVATE node-api) target_compile_features(async-test-addon PRIVATE cxx_std_17) \ No newline at end of file diff --git a/packages/node-addon-examples/tests/async/binding.gyp b/packages/node-addon-examples/tests/async/binding.gyp deleted file mode 100644 index 80f9fa87..00000000 --- a/packages/node-addon-examples/tests/async/binding.gyp +++ /dev/null @@ -1,8 +0,0 @@ -{ - "targets": [ - { - "target_name": "addon", - "sources": [ "addon.c" ] - } - ] -} diff --git a/packages/node-addon-examples/tests/async/package.json b/packages/node-addon-examples/tests/async/package.json index c8016da0..c153ed21 100644 --- a/packages/node-addon-examples/tests/async/package.json +++ b/packages/node-addon-examples/tests/async/package.json @@ -9,6 +9,5 @@ }, "scripts": { "test": "node addon.js" - }, - "gypfile": true + } } diff --git a/packages/node-addon-examples/tsconfig.node-scripts.json b/packages/node-addon-examples/tsconfig.node-scripts.json index e4ca4a5b..d8f6d3d2 100644 --- a/packages/node-addon-examples/tsconfig.node-scripts.json +++ b/packages/node-addon-examples/tsconfig.node-scripts.json @@ -8,5 +8,10 @@ "types": ["node", "read-pkg"] }, "include": ["scripts/**/*.mts"], - "exclude": [] + "exclude": [], + "references": [ + { + "path": "../node-api-stub/tsconfig.node.json" + } + ] } diff --git a/packages/node-api-stub/.gitignore b/packages/node-api-stub/.gitignore new file mode 100644 index 00000000..41a53b37 --- /dev/null +++ b/packages/node-api-stub/.gitignore @@ -0,0 +1,2 @@ +# Generated by `node --run generate` +/generated/ diff --git a/packages/weak-node-api/stub/CMakeLists.txt b/packages/node-api-stub/CMakeLists.txt similarity index 66% rename from packages/weak-node-api/stub/CMakeLists.txt rename to packages/node-api-stub/CMakeLists.txt index 3951dd79..2c2cb2cc 100644 --- a/packages/weak-node-api/stub/CMakeLists.txt +++ b/packages/node-api-stub/CMakeLists.txt @@ -3,16 +3,19 @@ project(node-api-stub) set(NODE_API_STUB_LIBRARY_NAME "hermesvm" CACHE STRING "Name of the library the stub stands in for") -add_library(node-api-stub SHARED ${CMAKE_CURRENT_SOURCE_DIR}/../generated/node_api_stub.cpp) +if(NOT DEFINED NODE_API_INC) + message(FATAL_ERROR "NODE_API_INC must point at the Node-API headers") +endif() + +add_library(node-api-stub SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_LIST_DIR}/generated/node_api_stub.cpp) -target_include_directories(node-api-stub PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include) +target_include_directories(node-api-stub PRIVATE ${NODE_API_INC}) target_compile_definitions(node-api-stub PRIVATE NAPI_VERSION=10) if(APPLE) set_target_properties(node-api-stub PROPERTIES FRAMEWORK TRUE MACOSX_FRAMEWORK_IDENTIFIER com.callstack.node-api-stub.${NODE_API_STUB_LIBRARY_NAME} - XCODE_ATTRIBUTE_SKIP_INSTALL NO OUTPUT_NAME ${NODE_API_STUB_LIBRARY_NAME} ) else() diff --git a/packages/node-api-stub/README.md b/packages/node-api-stub/README.md new file mode 100644 index 00000000..cb1acc2e --- /dev/null +++ b/packages/node-api-stub/README.md @@ -0,0 +1,11 @@ +# Node-API stub + +Describes the Node-API functions declared by [`node-api-headers`](https://www.npmjs.com/package/node-api-headers) and generates a link-time stub of a library exporting them. + +This package is part of the [Node-API for React Native](https://github.com/callstackincubator/react-native-node-api) project. + +## The stub + +Linking an addon against the stub makes it record the stub's library name (`hermesvm` by default) as a dependency, so it calls that library's Node-API directly at runtime. The stub is only ever linked against: every function aborts, and it's never shipped in an app. + +`CMakeLists.txt` defines the `node-api-stub` target, excluded from `all`. It expects `NODE_API_INC` to point at the Node-API headers, and `NODE_API_STUB_LIBRARY_NAME` picks the library it stands in for. [`cmake-rn`](../cmake-rn) adds it to an addon's build when passed `--node-api-linkage hermes`. diff --git a/packages/node-api-stub/package.json b/packages/node-api-stub/package.json new file mode 100644 index 00000000..485dcb3c --- /dev/null +++ b/packages/node-api-stub/package.json @@ -0,0 +1,50 @@ +{ + "name": "node-api-stub", + "version": "0.0.0", + "description": "Node-API function declarations and a link-time stub of a library exporting 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/node-api-stub" + }, + "type": "module", + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist", + "!dist/**/*.test.d.ts", + "!dist/**/*.test.d.ts.map", + "generated", + "CMakeLists.txt" + ], + "scripts": { + "build": "tsc --build", + "generate": "tsx scripts/generate-stub.ts", + "bootstrap": "node --run generate", + "prerelease": "node --run generate" + }, + "keywords": [ + "node-api", + "napi", + "stub", + "hermes", + "cmake" + ], + "author": { + "name": "Callstack", + "url": "https://github.com/callstackincubator" + }, + "contributors": [ + { + "name": "Kræn Hansen", + "url": "https://github.com/kraenhansen" + } + ], + "license": "MIT", + "dependencies": { + "node-api-headers": "^1.5.0", + "zod": "^4.1.11" + } +} diff --git a/packages/node-api-stub/scripts/generate-stub.ts b/packages/node-api-stub/scripts/generate-stub.ts new file mode 100644 index 00000000..91d73b72 --- /dev/null +++ b/packages/node-api-stub/scripts/generate-stub.ts @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import cp from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; + +import { generateFunction, getNodeApiFunctions } from "../src/index.js"; + +const OUTPUT_PATH = path.join(import.meta.dirname, "../generated"); + +const source = ` + /** + * @file node_api_stub.cpp + * @brief Link-time stub of a library exporting Node-API. + * + * Lets an addon link directly against a Node-API provider (such as Hermes) without having it installed. + * + * @note This file is generated - don't edit it directly + */ + + #include + #include // abort() + + ${getNodeApiFunctions() + .map((fn) => generateFunction({ ...fn, extern: true, body: "abort();" })) + .join("\n")} +`; + +await fs.promises.mkdir(OUTPUT_PATH, { recursive: true }); +const outputPath = path.join(OUTPUT_PATH, "node_api_stub.cpp"); +await fs.promises.writeFile(outputPath, source.trim(), "utf-8"); +const { status, stderr } = cp.spawnSync("clang-format", ["-i", outputPath], { + encoding: "utf8", +}); +assert.equal(status, 0, `Failed to format ${outputPath}: ${stderr}`); diff --git a/packages/weak-node-api/scripts/generators/shared.ts b/packages/node-api-stub/src/generate-function.ts similarity index 74% rename from packages/weak-node-api/scripts/generators/shared.ts rename to packages/node-api-stub/src/generate-function.ts index 88104e58..b149d7ad 100644 --- a/packages/weak-node-api/scripts/generators/shared.ts +++ b/packages/node-api-stub/src/generate-function.ts @@ -1,4 +1,4 @@ -import type { FunctionDecl } from "../../src/node-api-functions.js"; +import type { FunctionDecl } from "./node-api-functions.js"; type FunctionOptions = FunctionDecl & { extern?: true; @@ -8,6 +8,9 @@ type FunctionOptions = FunctionDecl & { argumentNames?: string[]; }; +/** + * Generates the C++ declaration of a Node-API function, or its definition when a body is given. + */ export function generateFunction({ extern, static: staticMember, @@ -16,13 +19,12 @@ export function generateFunction({ name, argumentTypes, argumentNames = [], - noReturn, body, }: FunctionOptions) { return ` ${staticMember ? "static " : ""}${extern ? 'extern "C" ' : ""}${returnType} ${namespace ? namespace + "::" : ""}${name}( ${argumentTypes.map((type, index) => `${type} ` + (argumentNames[index] ?? `arg${index}`)).join(", ")} - ) ${body ? `{ ${body} ${noReturn ? "WEAK_NODE_API_UNREACHABLE;" : ""}\n}` : ""} + ) ${body ? `{ ${body}\n}` : ""} ; `; } diff --git a/packages/node-api-stub/src/index.ts b/packages/node-api-stub/src/index.ts new file mode 100644 index 00000000..e887b0af --- /dev/null +++ b/packages/node-api-stub/src/index.ts @@ -0,0 +1,3 @@ +export * from "./node-api-functions.js"; +export * from "./generate-function.js"; +export * from "./node-api-stub.js"; diff --git a/packages/weak-node-api/src/node-api-functions.ts b/packages/node-api-stub/src/node-api-functions.ts similarity index 100% rename from packages/weak-node-api/src/node-api-functions.ts rename to packages/node-api-stub/src/node-api-functions.ts diff --git a/packages/node-api-stub/src/node-api-stub.ts b/packages/node-api-stub/src/node-api-stub.ts new file mode 100644 index 00000000..ea13a136 --- /dev/null +++ b/packages/node-api-stub/src/node-api-stub.ts @@ -0,0 +1,8 @@ +import path from "node:path"; + +export const nodeApiStubPath = path.resolve(import.meta.dirname, ".."); + +/** + * Name of the CMake target building the stub, which is only linked against and never shipped. + */ +export const NODE_API_STUB_TARGET = "node-api-stub"; diff --git a/packages/node-api-stub/tsconfig.json b/packages/node-api-stub/tsconfig.json new file mode 100644 index 00000000..f08015f8 --- /dev/null +++ b/packages/node-api-stub/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true + }, + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.node-scripts.json" } + ] +} diff --git a/packages/node-api-stub/tsconfig.node-scripts.json b/packages/node-api-stub/tsconfig.node-scripts.json new file mode 100644 index 00000000..4bc586fa --- /dev/null +++ b/packages/node-api-stub/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/node-api-stub/tsconfig.node.json b/packages/node-api-stub/tsconfig.node.json new file mode 100644 index 00000000..0028899f --- /dev/null +++ b/packages/node-api-stub/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/weak-node-api/types/node-api-headers/index.d.ts b/packages/node-api-stub/types/node-api-headers/index.d.ts similarity index 100% rename from packages/weak-node-api/types/node-api-headers/index.d.ts rename to packages/node-api-stub/types/node-api-headers/index.d.ts diff --git a/packages/weak-node-api/.gitignore b/packages/weak-node-api/.gitignore index b2058cd6..83e62995 100644 --- a/packages/weak-node-api/.gitignore +++ b/packages/weak-node-api/.gitignore @@ -12,4 +12,3 @@ # Clang cache /.cache/ -/stub/build/ diff --git a/packages/weak-node-api/CMakeLists.txt b/packages/weak-node-api/CMakeLists.txt index f108c445..d23d1551 100644 --- a/packages/weak-node-api/CMakeLists.txt +++ b/packages/weak-node-api/CMakeLists.txt @@ -68,8 +68,3 @@ if(BUILD_TESTS) enable_testing() add_subdirectory(tests) endif() - -option(BUILD_NODE_API_STUB "Build a link-time stub of a library exporting Node-API" OFF) -if(BUILD_NODE_API_STUB) - add_subdirectory(stub) -endif() diff --git a/packages/weak-node-api/package.json b/packages/weak-node-api/package.json index 1d6b27ea..ff551521 100644 --- a/packages/weak-node-api/package.json +++ b/packages/weak-node-api/package.json @@ -20,9 +20,6 @@ "generated", "build/Debug", "build/Release", - "stub/CMakeLists.txt", - "stub/build/Debug", - "stub/build/Release", "*.podspec", "*.cmake" ], @@ -31,7 +28,7 @@ "copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts", "generate": "tsx scripts/generate.ts", "prebuild:prepare": "node --run copy-node-api-headers && node --run generate", - "prebuild:build": "cmake-rn --no-auto-link --node-api-linkage none --xcframework-extension -D BUILD_NODE_API_STUB=ON", + "prebuild:build": "cmake-rn --no-auto-link --node-api-linkage none --xcframework-extension", "prebuild:build:android": "node --run prebuild:build -- --android", "prebuild:build:apple": "node --run prebuild:build -- --apple", "prebuild:build:all": "node --run prebuild:build -- --android --apple", @@ -67,11 +64,8 @@ } ], "license": "MIT", - "dependencies": { - "node-api-headers": "^1.5.0" - }, "devDependencies": { "cmake-rn": "workspace:*", - "zod": "^4.1.11" + "node-api-stub": "workspace:*" } } diff --git a/packages/weak-node-api/scripts/copy-node-api-headers.ts b/packages/weak-node-api/scripts/copy-node-api-headers.ts index 8d90174b..e14ac226 100644 --- a/packages/weak-node-api/scripts/copy-node-api-headers.ts +++ b/packages/weak-node-api/scripts/copy-node-api-headers.ts @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; -import { nodeApiHeaders } from "../src/node-api-functions.js"; +import { nodeApiHeaders } from "node-api-stub"; const { include_dir: includeSourcePath } = nodeApiHeaders; const includeDestinationPath = path.join(import.meta.dirname, "../include"); diff --git a/packages/weak-node-api/scripts/generate.ts b/packages/weak-node-api/scripts/generate.ts index f6e8a703..e3ba8852 100644 --- a/packages/weak-node-api/scripts/generate.ts +++ b/packages/weak-node-api/scripts/generate.ts @@ -3,14 +3,10 @@ import fs from "node:fs"; import path from "node:path"; import cp from "node:child_process"; -import { - FunctionDecl, - getNodeApiFunctions, -} from "../src/node-api-functions.js"; +import { type FunctionDecl, getNodeApiFunctions } from "node-api-stub"; import * as weakNodeApiGenerator from "./generators/weak-node-api.js"; import * as hostGenerator from "./generators/NodeApiHost.js"; -import * as stubGenerator from "./generators/stub.js"; export const OUTPUT_PATH = path.join(import.meta.dirname, "../generated"); @@ -88,16 +84,6 @@ async function run() { Provides the implementation for deferring Node-API function calls from addons into a Node-API host. `, }); - await generateFile({ - functions, - fileName: "node_api_stub.cpp", - generator: stubGenerator.generateSource, - headingComment: ` - @brief Link-time stub of a library exporting Node-API. - - Lets an addon link directly against a Node-API provider (such as Hermes) without having it installed. - `, - }); } run().catch((err) => { diff --git a/packages/weak-node-api/scripts/generators/NodeApiHost.ts b/packages/weak-node-api/scripts/generators/NodeApiHost.ts index c273cfe7..1ac547c6 100644 --- a/packages/weak-node-api/scripts/generators/NodeApiHost.ts +++ b/packages/weak-node-api/scripts/generators/NodeApiHost.ts @@ -1,4 +1,4 @@ -import type { FunctionDecl } from "../../src/node-api-functions.js"; +import type { FunctionDecl } from "node-api-stub"; export function generateFunctionDecl({ returnType, diff --git a/packages/weak-node-api/scripts/generators/stub.ts b/packages/weak-node-api/scripts/generators/stub.ts deleted file mode 100644 index 3b3ac46a..00000000 --- a/packages/weak-node-api/scripts/generators/stub.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { FunctionDecl } from "../../src/node-api-functions.js"; -import { generateFunction } from "./shared.js"; - -export function generateSource(functions: FunctionDecl[]) { - return ` - #include - #include // abort() - - #define WEAK_NODE_API_UNREACHABLE __builtin_unreachable() - - ${functions - .map((fn) => generateFunction({ ...fn, extern: true, body: "abort();" })) - .join("\n")} - `; -} diff --git a/packages/weak-node-api/scripts/generators/weak-node-api.ts b/packages/weak-node-api/scripts/generators/weak-node-api.ts index 62a1a026..a72306a3 100644 --- a/packages/weak-node-api/scripts/generators/weak-node-api.ts +++ b/packages/weak-node-api/scripts/generators/weak-node-api.ts @@ -1,5 +1,4 @@ -import type { FunctionDecl } from "../../src/node-api-functions.js"; -import { generateFunction } from "./shared.js"; +import { type FunctionDecl, generateFunction } from "node-api-stub"; export function generateHeader() { return ` @@ -29,6 +28,7 @@ function generateFunctionImpl(fn: FunctionDecl) { ${returnType === "void" ? "" : "return "} g_host.${name}( ${argumentTypes.map((_, index) => `arg${index}`).join(", ")} ); + ${fn.noReturn ? "WEAK_NODE_API_UNREACHABLE;" : ""} `, }); } diff --git a/packages/weak-node-api/src/index.ts b/packages/weak-node-api/src/index.ts index fbd5337a..62e00b28 100644 --- a/packages/weak-node-api/src/index.ts +++ b/packages/weak-node-api/src/index.ts @@ -1,2 +1 @@ export * from "./weak-node-api.js"; -export * from "./node-api-functions.js"; diff --git a/packages/weak-node-api/src/weak-node-api.ts b/packages/weak-node-api/src/weak-node-api.ts index 0f2302a5..87802461 100644 --- a/packages/weak-node-api/src/weak-node-api.ts +++ b/packages/weak-node-api/src/weak-node-api.ts @@ -24,22 +24,3 @@ export const weakNodeApiCmakePath = path.resolve( weakNodeApiPath, "weak-node-api-config.cmake", ); - -const stubPath = path.resolve(weakNodeApiPath, "stub", "build"); - -const stubOutputPath = fs.existsSync(path.resolve(stubPath, "Debug")) - ? path.resolve(stubPath, "Debug") - : path.resolve(stubPath, "Release"); - -/** - * Link-time stub of the Hermes library, letting addons link Node-API directly against Hermes. - */ -export const hermesStubApplePrebuildPath = path.resolve( - stubOutputPath, - "hermesvm.xcframework", -); - -export const hermesStubAndroidPrebuildPath = path.resolve( - stubOutputPath, - "hermesvm.android.node", -); diff --git a/packages/weak-node-api/tsconfig.node-scripts.json b/packages/weak-node-api/tsconfig.node-scripts.json index 4bc586fa..c6bc24ff 100644 --- a/packages/weak-node-api/tsconfig.node-scripts.json +++ b/packages/weak-node-api/tsconfig.node-scripts.json @@ -9,5 +9,8 @@ }, "include": ["scripts/**/*.ts", "types/**/*.d.ts"], "exclude": [], - "references": [{ "path": "./tsconfig.node.json" }] + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "../node-api-stub/tsconfig.node.json" } + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc4d7aeb..258b3295 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -198,6 +198,9 @@ importers: node-api-headers: specifier: ^1.5.0 version: 1.9.0 + node-api-stub: + specifier: workspace:* + version: link:../node-api-stub react-native-node-api: specifier: workspace:* version: link:../host @@ -317,6 +320,9 @@ importers: fswin: specifier: ^3.24.829 version: 3.25.1108 + node-api-stub: + specifier: workspace:* + version: link:../node-api-stub weak-node-api: specifier: workspace:* version: link:../weak-node-api @@ -342,6 +348,9 @@ importers: node-addon-examples: specifier: github:nodejs/node-addon-examples#4b7dd86a85644610e6de80154df9acac9329b509 version: https://codeload.github.com/nodejs/node-addon-examples/tar.gz/4b7dd86a85644610e6de80154df9acac9329b509 + node-api-stub: + specifier: workspace:* + version: link:../node-api-stub read-pkg: specifier: ^9.0.1 version: 9.0.1 @@ -349,6 +358,15 @@ importers: specifier: workspace:* version: link:../weak-node-api + packages/node-api-stub: + dependencies: + node-api-headers: + specifier: ^1.5.0 + version: 1.9.0 + zod: + specifier: ^4.1.11 + version: 4.4.3 + packages/node-tests: devDependencies: assert: @@ -374,17 +392,13 @@ importers: version: link:../weak-node-api packages/weak-node-api: - dependencies: - node-api-headers: - specifier: ^1.5.0 - version: 1.9.0 devDependencies: cmake-rn: specifier: workspace:* version: link:../cmake-rn - zod: - specifier: ^4.1.11 - version: 4.4.3 + node-api-stub: + specifier: workspace:* + version: link:../node-api-stub packages: diff --git a/tsconfig.json b/tsconfig.json index 2d49bdb1..f05034b3 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-api-stub/tsconfig.json" }, { "path": "./packages/node-tests/tsconfig.json" }, { "path": "./packages/weak-node-api/tsconfig.json" } ]