diff --git a/.changeset/silly-mice-warn.md b/.changeset/silly-mice-warn.md new file mode 100644 index 00000000..7ebdc578 --- /dev/null +++ b/.changeset/silly-mice-warn.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Detects "pod install" from React Native MacOS apps and vendors Hermes accordingly diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ec9c7cee..812af311 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -107,6 +107,40 @@ jobs: # TODO: Enable release mode when it works # run: npm run test:ios -- --mode Release working-directory: apps/test-app + test-macos: + # Disabling this on main for now, as initializing the template takes a long time and + # we don't have macOS-specific code yet + if: contains(github.event.pull_request.labels.*.name, 'MacOS 💻') + name: Test app (macOS) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/jod + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: "17" + distribution: "temurin" + # Install CMake 3 since 4.x may have compatibility issues with Hermes build system + - name: Install compatible CMake version + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: "3.31.2" + - run: rustup target add x86_64-apple-darwin + - run: npm ci + - run: npm run bootstrap + env: + CMAKE_RN_TRIPLETS: arm64;x86_64-apple-darwin + FERRIC_TARGETS: aarch64-apple-darwin,x86_64-apple-darwin + - run: npm run init-macos-test-app + - run: pod install --project-directory=macos + working-directory: apps/macos-test-app + - name: Build MacOS test app + run: npx react-native build-macos --mode Release + working-directory: apps/macos-test-app + # TODO: Run the app eventually test-android: if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Android 🤖') name: Test app (Android) diff --git a/.gitignore b/.gitignore index 42f6c1a8..6c7c7bf3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ node_modules/ dist/ *.tsbuildinfo + +# Treading the MacOS app as ephemeral +apps/macos-test-app diff --git a/package-lock.json b/package-lock.json index ccc7603b..9604cb48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "globals": "^16.0.0", "prettier": "^3.6.2", "react-native": "0.81.4", + "read-pkg": "^9.0.1", "tsx": "^4.20.5", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" @@ -14607,7 +14608,7 @@ }, "packages/cli-utils": { "name": "@react-native-node-api/cli-utils", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "@commander-js/extra-typings": "^14.0.0", "bufout": "^0.3.2", @@ -14824,11 +14825,11 @@ } }, "packages/cmake-rn": { - "version": "0.4.1", + "version": "0.5.1", "dependencies": { - "@react-native-node-api/cli-utils": "0.1.0", + "@react-native-node-api/cli-utils": "0.1.1", "cmake-file-api": "0.1.0", - "react-native-node-api": "0.5.2", + "react-native-node-api": "0.6.1", "zod": "^4.1.11" }, "bin": { @@ -14841,11 +14842,11 @@ }, "packages/ferric": { "name": "ferric-cli", - "version": "0.3.4", + "version": "0.3.6", "dependencies": { "@napi-rs/cli": "~3.0.3", - "@react-native-node-api/cli-utils": "0.1.0", - "react-native-node-api": "0.5.2" + "@react-native-node-api/cli-utils": "0.1.1", + "react-native-node-api": "0.6.1" }, "bin": { "ferric": "bin/ferric.js" @@ -14859,9 +14860,9 @@ } }, "packages/gyp-to-cmake": { - "version": "0.3.0", + "version": "0.4.0", "dependencies": { - "@react-native-node-api/cli-utils": "0.1.0", + "@react-native-node-api/cli-utils": "0.1.1", "gyp-parser": "^1.0.4", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1" @@ -14872,11 +14873,11 @@ }, "packages/host": { "name": "react-native-node-api", - "version": "0.5.2", + "version": "0.6.1", "license": "MIT", "dependencies": { "@expo/plist": "^0.4.7", - "@react-native-node-api/cli-utils": "0.1.0", + "@react-native-node-api/cli-utils": "0.1.1", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1", "zod": "^4.1.11" @@ -14892,11 +14893,12 @@ }, "peerDependencies": { "@babel/core": "^7.26.10", - "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4" + "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.79.7 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4" } }, "packages/node-addon-examples": { "name": "@react-native-node-api/node-addon-examples", + "version": "0.1.0", "dependencies": { "assert": "^2.1.0" }, @@ -14909,11 +14911,12 @@ }, "packages/node-tests": { "name": "@react-native-node-api/node-tests", + "version": "0.1.0", "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", "prebuildify": "^6.0.1", - "react-native-node-api": "^0.5.2", + "react-native-node-api": "^0.6.1", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } diff --git a/package.json b/package.json index eb2f9dff..be2dbf31 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "test": "npm test --workspace react-native-node-api --workspace cmake-rn --workspace gyp-to-cmake --workspace node-addon-examples", "bootstrap": "node --run build && npm run bootstrap --workspaces --if-present", "prerelease": "node --run build && npm run prerelease --workspaces --if-present", - "release": "changeset publish" + "release": "changeset publish", + "init-macos-test-app": "node scripts/init-macos-test-app.ts" }, "author": { "name": "Callstack", @@ -64,6 +65,7 @@ "globals": "^16.0.0", "prettier": "^3.6.2", "react-native": "0.81.4", + "read-pkg": "^9.0.1", "tsx": "^4.20.5", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" diff --git a/packages/ferric-example/package.json b/packages/ferric-example/package.json index 73678bb3..0695c049 100644 --- a/packages/ferric-example/package.json +++ b/packages/ferric-example/package.json @@ -1,8 +1,8 @@ { "name": "@react-native-node-api/ferric-example", + "version": "0.1.1", "private": true, "type": "commonjs", - "version": "0.1.1", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { "type": "git", @@ -11,6 +11,12 @@ }, "main": "ferric_example.js", "types": "ferric_example.d.ts", + "files": [ + "ferric_example.js", + "ferric_example.d.ts", + "ferric_example.apple.node", + "ferric_example.android.node" + ], "scripts": { "build": "ferric build", "bootstrap": "node --run build" diff --git a/packages/ferric/src/cargo.ts b/packages/ferric/src/cargo.ts index 9afa0770..4671a47c 100644 --- a/packages/ferric/src/cargo.ts +++ b/packages/ferric/src/cargo.ts @@ -19,22 +19,46 @@ import { isThirdTierTarget, } from "./targets.js"; -const APPLE_XCFRAMEWORK_CHILDS_PER_TARGET: Record = { - "aarch64-apple-darwin": "macos-arm64_x86_64", // Universal - "x86_64-apple-darwin": "macos-arm64_x86_64", // Universal +/** + * A per apple target mapping to a list of xcframework slices in order of priority + */ +const APPLE_XCFRAMEWORK_SLICES_PER_TARGET: Record = { + "aarch64-apple-darwin": [ + "macos-arm64_x86_64", // Universal + "macos-arm64", + ], + "x86_64-apple-darwin": [ + "macos-arm64_x86_64", // Universal + "macos-x86_64", + ], - "aarch64-apple-ios": "ios-arm64", - "aarch64-apple-ios-sim": "ios-arm64_x86_64-simulator", // Universal - "x86_64-apple-ios": "ios-arm64_x86_64-simulator", // Universal + "aarch64-apple-ios": ["ios-arm64"], + "aarch64-apple-ios-sim": [ + "ios-arm64_x86_64-simulator", // Universal + "ios-arm64-simulator", + ], + "x86_64-apple-ios": [ + "ios-arm64_x86_64-simulator", // Universal + "ios-x86_64-simulator", + ], - "aarch64-apple-visionos": "xros-arm64", - "aarch64-apple-visionos-sim": "xros-arm64_x86_64-simulator", // Universal + "aarch64-apple-visionos": ["xros-arm64"], + "aarch64-apple-visionos-sim": [ + "xros-arm64_x86_64-simulator", // Universal + "xros-arm64-simulator", + ], // The x86_64 target for vision simulator isn't supported // see https://doc.rust-lang.org/rustc/platform-support.html - "aarch64-apple-tvos": "tvos-arm64", - "aarch64-apple-tvos-sim": "tvos-arm64_x86_64-simulator", - "x86_64-apple-tvos": "tvos-arm64_x86_64-simulator", + "aarch64-apple-tvos": ["tvos-arm64"], + "aarch64-apple-tvos-sim": [ + "tvos-arm64_x86_64-simulator", // Universal + "tvos-arm64-simulator", + ], + "x86_64-apple-tvos": [ + "tvos-arm64_x86_64-simulator", // Universal + "tvos-x86_64-simulator", + ], // "aarch64-apple-ios-macabi": "", // Catalyst // "x86_64-apple-ios-macabi": "ios-x86_64-simulator", @@ -145,11 +169,19 @@ export function getTargetAndroidPlatform(target: AndroidTargetName) { } export function getWeakNodeApiFrameworkPath(target: AppleTargetName) { - return joinPathAndAssertExistence( + const xcframeworkPath = joinPathAndAssertExistence( weakNodeApiPath, "weak-node-api.xcframework", - APPLE_XCFRAMEWORK_CHILDS_PER_TARGET[target], ); + const result = APPLE_XCFRAMEWORK_SLICES_PER_TARGET[target].find((slice) => { + const candidatePath = path.join(xcframeworkPath, slice); + return fs.existsSync(candidatePath); + }); + assert( + result, + `No matching slice found in weak-node-api.xcframework for target ${target}`, + ); + return joinPathAndAssertExistence(xcframeworkPath, result); } export function getWeakNodeApiAndroidLibraryPath(target: AndroidTargetName) { diff --git a/packages/host/apple/NodeApiHostModuleProvider.mm b/packages/host/apple/NodeApiHostModuleProvider.mm new file mode 100644 index 00000000..b01c5306 --- /dev/null +++ b/packages/host/apple/NodeApiHostModuleProvider.mm @@ -0,0 +1,21 @@ +#import "CxxNodeApiHostModule.hpp" +#import "WeakNodeApiInjector.hpp" + +#import +@interface NodeApiHost : NSObject + +@end + +@implementation NodeApiHost ++ (void)load { + callstack::nodeapihost::injectIntoWeakNodeApi(); + + facebook::react::registerCxxModuleToGlobalModuleMap( + callstack::nodeapihost::CxxNodeApiHostModule::kModuleName, + [](std::shared_ptr jsInvoker) { + return std::make_shared( + jsInvoker); + }); +} + +@end \ No newline at end of file diff --git a/packages/host/ios/NodeApiHostModuleProvider.mm b/packages/host/ios/NodeApiHostModuleProvider.mm deleted file mode 100644 index d4ecd94f..00000000 --- a/packages/host/ios/NodeApiHostModuleProvider.mm +++ /dev/null @@ -1,44 +0,0 @@ -#import "CxxNodeApiHostModule.hpp" -#import "WeakNodeApiInjector.hpp" - -#define USE_CXX_TURBO_MODULE_UTILS 0 -#if defined(__has_include) -#if __has_include() -#undef USE_CXX_TURBO_MODULE_UTILS -#define USE_CXX_TURBO_MODULE_UTILS 1 -#endif -#endif - -#if USE_CXX_TURBO_MODULE_UTILS -#import -@interface NodeApiHost : NSObject -#else -#import -@interface NodeApiHost : NSObject -#endif // USE_CXX_TURBO_MODULE_UTILS - -@end - -@implementation NodeApiHost -#if USE_CXX_TURBO_MODULE_UTILS -+ (void)load { - callstack::nodeapihost::injectIntoWeakNodeApi(); - - facebook::react::registerCxxModuleToGlobalModuleMap( - callstack::nodeapihost::CxxNodeApiHostModule::kModuleName, - [](std::shared_ptr jsInvoker) { - return std::make_shared( - jsInvoker); - }); -} -#else -RCT_EXPORT_MODULE() - -- (std::shared_ptr)getTurboModule: - (const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared( - params.jsInvoker); -} -#endif // USE_CXX_TURBO_MODULE_UTILS - -@end \ No newline at end of file diff --git a/packages/host/react-native-node-api.podspec b/packages/host/react-native-node-api.podspec index 5066e82d..ff2cbe9d 100644 --- a/packages/host/react-native-node-api.podspec +++ b/packages/host/react-native-node-api.podspec @@ -25,10 +25,9 @@ Pod::Spec.new do |s| s.license = package["license"] s.authors = package["author"] - s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://github.com/callstackincubator/react-native-node-api.git", :tag => "#{s.version}" } - s.source_files = "ios/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "weak-node-api/include/*.h", "weak-node-api/*.hpp" + s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "weak-node-api/include/*.h", "weak-node-api/*.hpp" s.public_header_files = "weak-node-api/include/*.h" s.vendored_frameworks = "auto-linked/apple/*.xcframework", "weak-node-api/weak-node-api.xcframework" diff --git a/packages/host/scripts/patch-hermes.rb b/packages/host/scripts/patch-hermes.rb index a6cb11f7..76252154 100644 --- a/packages/host/scripts/patch-hermes.rb +++ b/packages/host/scripts/patch-hermes.rb @@ -4,8 +4,18 @@ raise "React Native Node-API cannot reliably patch JSI when React Native Core is prebuilt." end +def get_react_native_package + if caller.any? { |frame| frame.include?("node_modules/react-native-macos/") } + return "react-native-macos" + elsif caller.any? { |frame| frame.include?("node_modules/react-native/") } + return "react-native" + else + raise "Unable to determine React Native package from call stack." + end +end + if ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'].nil? - VENDORED_HERMES_DIR ||= `npx react-native-node-api vendor-hermes --silent '#{Pod::Config.instance.installation_root}'`.strip + VENDORED_HERMES_DIR ||= `npx react-native-node-api vendor-hermes --react-native-package '#{get_react_native_package()}' --silent '#{Pod::Config.instance.installation_root}'`.strip # Signal the patched Hermes to React Native ENV['BUILD_FROM_SOURCE'] = 'true' ENV['REACT_NATIVE_OVERRIDE_HERMES_DIR'] = VENDORED_HERMES_DIR diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index c5eec200..b92487db 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -192,6 +192,41 @@ export async function linkFlatFramework({ } } +/** + * NPM packages aren't preserving internal symlinks inside versioned frameworks. + * This function attempts to restore those. + */ +export async function restoreFrameworkLinks(frameworkPath: string) { + // Reconstruct missing symbolic links if needed + const versionAPath = path.join(frameworkPath, "Versions", "A"); + const versionCurrentPath = path.join(frameworkPath, "Versions", "Current"); + if (fs.existsSync(versionAPath) && !fs.existsSync(versionCurrentPath)) { + await fs.promises.symlink( + path.relative(path.dirname(versionCurrentPath), versionAPath), + versionCurrentPath, + ); + } + + const { CFBundleExecutable } = await readFrameworkInfo( + path.join(frameworkPath, "Versions", "Current", "Resources", "Info.plist"), + ); + + const libraryRealPath = path.join( + frameworkPath, + "Versions", + "Current", + CFBundleExecutable, + ); + const libraryLinkPath = path.join(frameworkPath, CFBundleExecutable); + // Reconstruct missing symbolic links if needed + if (fs.existsSync(libraryRealPath) && !fs.existsSync(libraryLinkPath)) { + await fs.promises.symlink( + path.relative(path.dirname(libraryLinkPath), libraryRealPath), + libraryLinkPath, + ); + } +} + export async function linkVersionedFramework({ frameworkPath, newLibraryName, @@ -201,6 +236,9 @@ export async function linkVersionedFramework({ "darwin", "Linking Apple addons are only supported on macOS", ); + + await restoreFrameworkLinks(frameworkPath); + const frameworkInfoPath = path.join( frameworkPath, "Versions", diff --git a/packages/host/src/node/cli/hermes.ts b/packages/host/src/node/cli/hermes.ts index 6bd3daa3..4b41692c 100644 --- a/packages/host/src/node/cli/hermes.ts +++ b/packages/host/src/node/cli/hermes.ts @@ -23,6 +23,32 @@ const platformOption = new Option( "The React Native package to vendor Hermes into", ).default("react-native"); +type PatchJSIHeadersOptions = { + reactNativePath: string; + hermesJsiPath: string; + silent: boolean; +}; + +async function patchJsiHeaders({ + reactNativePath, + hermesJsiPath, + silent, +}: PatchJSIHeadersOptions) { + const reactNativeJsiPath = path.join(reactNativePath, "ReactCommon/jsi/jsi/"); + await oraPromise( + fs.promises.cp(hermesJsiPath, reactNativeJsiPath, { + recursive: true, + }), + { + text: `Copying JSI from patched Hermes to React Native`, + successText: "Copied JSI from patched Hermes to React Native", + failText: (err) => + `Failed to copy JSI from Hermes to React Native: ${err.message}`, + isEnabled: !silent, + }, + ); +} + export const command = new Command("vendor-hermes") .argument("[from]", "Path to a file inside the app package", process.cwd()) .option("--silent", "Don't print anything except the final path", false) @@ -64,11 +90,6 @@ export const command = new Command("vendor-hermes") console.log(`Using Hermes version: ${hermesVersion}`); } - const reactNativeJsiPath = path.join( - reactNativePath, - "ReactCommon/jsi/jsi/", - ); - const hermesPath = path.join(reactNativePath, "sdks", "node-api-hermes"); if (force && fs.existsSync(hermesPath)) { await oraPromise( @@ -125,19 +146,11 @@ export const command = new Command("vendor-hermes") fs.existsSync(hermesJsiPath), `Hermes JSI path does not exist: ${hermesJsiPath}`, ); - - await oraPromise( - fs.promises.cp(hermesJsiPath, reactNativeJsiPath, { - recursive: true, - }), - { - text: `Copying JSI from patched Hermes to React Native`, - successText: "Copied JSI from patched Hermes to React Native", - failText: (err) => - `Failed to copy JSI from Hermes to React Native: ${err.message}`, - isEnabled: !silent, - }, - ); + await patchJsiHeaders({ + reactNativePath, + hermesJsiPath, + silent, + }); console.log(hermesPath); }), ); diff --git a/packages/host/src/node/cli/program.ts b/packages/host/src/node/cli/program.ts index 169ca85b..85b9016f 100644 --- a/packages/host/src/node/cli/program.ts +++ b/packages/host/src/node/cli/program.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import path from "node:path"; import { EventEmitter } from "node:stream"; +import fs from "node:fs"; import { Command, @@ -26,8 +27,9 @@ import { import { command as vendorHermes } from "./hermes"; import { packageNameOption, pathSuffixOption } from "./options"; import { linkModules, pruneLinkedModules, ModuleLinker } from "./link-modules"; -import { linkXcframework } from "./apple"; +import { linkXcframework, restoreFrameworkLinks } from "./apple"; import { linkAndroidDir } from "./android"; +import { weakNodeApiPath } from "../weak-node-api"; // We're attaching a lot of listeners when spawning in parallel EventEmitter.defaultMaxListeners = 100; @@ -169,6 +171,38 @@ program await pruneLinkedModules(platform, modules); } } + + if (apple) { + await oraPromise( + async () => { + const xcframeworkPath = path.join( + weakNodeApiPath, + "weak-node-api.xcframework", + ); + await Promise.all( + [ + path.join(xcframeworkPath, "macos-x86_64"), + path.join(xcframeworkPath, "macos-arm64"), + path.join(xcframeworkPath, "macos-arm64_x86_64"), + ].map(async (slicePath) => { + const frameworkPath = path.join( + slicePath, + "weak-node-api.framework", + ); + if (fs.existsSync(frameworkPath)) { + await restoreFrameworkLinks(frameworkPath); + } + }), + ); + }, + { + text: "Restoring weak-node-api symlinks", + successText: "Restored weak-node-api symlinks", + failText: (error) => + `Failed to restore weak-node-api symlinks: ${error.message}`, + }, + ); + } }, ), ); diff --git a/packages/node-addon-examples/.gitignore b/packages/node-addon-examples/.gitignore index d838da98..7470cb91 100644 --- a/packages/node-addon-examples/.gitignore +++ b/packages/node-addon-examples/.gitignore @@ -1 +1,2 @@ examples/ +build/ diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index db48db3f..010d2f31 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -1,7 +1,17 @@ { "name": "@react-native-node-api/node-addon-examples", + "version": "0.1.0", "type": "commonjs", "main": "dist/index.js", + "files": [ + "dist", + "examples/**/package.json", + "examples/**/*.js", + "tests/**/package.json", + "tests/**/*.js", + "**/*.apple.node/**", + "**/*.android.node/**" + ], "private": true, "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { diff --git a/packages/node-addon-examples/tests/.gitignore b/packages/node-addon-examples/tests/.gitignore deleted file mode 100644 index 378eac25..00000000 --- a/packages/node-addon-examples/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build diff --git a/packages/node-addon-examples/tests/async/CMakeLists.txt b/packages/node-addon-examples/tests/async/CMakeLists.txt index 659e3461..2b6b2b81 100644 --- a/packages/node-addon-examples/tests/async/CMakeLists.txt +++ b/packages/node-addon-examples/tests/async/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15...3.31) -project(async_test) +project(async-test) include(${WEAK_NODE_API_CONFIG}) @@ -10,12 +10,12 @@ option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON) if(APPLE AND BUILD_APPLE_FRAMEWORK) set_target_properties(addon PROPERTIES FRAMEWORK TRUE - MACOSX_FRAMEWORK_IDENTIFIER async_test.addon + MACOSX_FRAMEWORK_IDENTIFIER async-test.addon MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 XCODE_ATTRIBUTE_SKIP_INSTALL NO ) -elseif(APPLE) +else() set_target_properties(addon PROPERTIES PREFIX "" SUFFIX .node diff --git a/packages/node-addon-examples/tests/buffers/CMakeLists.txt b/packages/node-addon-examples/tests/buffers/CMakeLists.txt index bca19bce..8d7ac2d2 100644 --- a/packages/node-addon-examples/tests/buffers/CMakeLists.txt +++ b/packages/node-addon-examples/tests/buffers/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.15...3.31) -project(buffers_test) +project(buffers-test) include(${WEAK_NODE_API_CONFIG}) @@ -10,12 +10,12 @@ option(BUILD_APPLE_FRAMEWORK "Wrap addon in an Apple framework" ON) if(APPLE AND BUILD_APPLE_FRAMEWORK) set_target_properties(addon PROPERTIES FRAMEWORK TRUE - MACOSX_FRAMEWORK_IDENTIFIER buffers_test.addon + MACOSX_FRAMEWORK_IDENTIFIER buffers-test.addon MACOSX_FRAMEWORK_SHORT_VERSION_STRING 1.0 MACOSX_FRAMEWORK_BUNDLE_VERSION 1.0 XCODE_ATTRIBUTE_SKIP_INSTALL NO ) -elseif(APPLE) +else() set_target_properties(addon PROPERTIES PREFIX "" SUFFIX .node diff --git a/packages/node-tests/package.json b/packages/node-tests/package.json index eec9e5ed..fd1d497b 100644 --- a/packages/node-tests/package.json +++ b/packages/node-tests/package.json @@ -1,8 +1,15 @@ { "name": "@react-native-node-api/node-tests", + "version": "0.1.0", "description": "Harness for running the Node.js tests from https://github.com/nodejs/node/tree/main/test", "type": "commonjs", "main": "tests.generated.js", + "files": [ + "dist", + "tests/**/*.js", + "**/*.apple.node/**", + "**/*.android.node/**" + ], "private": true, "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { diff --git a/scripts/init-macos-test-app.ts b/scripts/init-macos-test-app.ts new file mode 100644 index 00000000..a3ac2ab9 --- /dev/null +++ b/scripts/init-macos-test-app.ts @@ -0,0 +1,174 @@ +import assert from "node:assert/strict"; +import cp from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { readPackage } from "read-pkg"; + +const REACT_NATIVE_VERSION = "0.79.6"; +const ROOT_PATH = path.join(import.meta.dirname, ".."); +const APP_PATH = path.join(ROOT_PATH, "apps", "macos-test-app"); +const OTHER_APP_PATH = path.join(ROOT_PATH, "apps", "test-app"); + +function exec(command: string, args: string[], options: cp.SpawnOptions = {}) { + const { status } = cp.spawnSync(command, args, { + stdio: "inherit", + ...options, + }); + assert.equal(status, 0, `Failed to execute '${command}'`); +} + +async function deletePreviousApp() { + if (fs.existsSync(APP_PATH)) { + console.log("Deleting existing app directory"); + await fs.promises.rm(APP_PATH, { recursive: true, force: true }); + } +} + +async function initializeReactNativeTemplate() { + console.log("Initializing community template"); + exec("npx", [ + "@react-native-community/cli", + "init", + "MacOSTestApp", + "--skip-install", + "--skip-git-init", + // "--platform-name", + // "react-native-macos", + "--version", + REACT_NATIVE_VERSION, + "--directory", + APP_PATH, + ]); + + // Clean up + const CLEANUP_PATHS = ["ios", "android", "__tests__"]; + + for (const cleanupPath of CLEANUP_PATHS) { + await fs.promises.rm(path.join(APP_PATH, cleanupPath), { + recursive: true, + force: true, + }); + } +} + +async function patchPackageJson() { + console.log("Patching package.json scripts"); + const packageJson = await readPackage({ cwd: APP_PATH }); + const otherPackageJson = await readPackage({ cwd: OTHER_APP_PATH }); + + packageJson.scripts = { + ...packageJson.scripts, + metro: "react-native start --reset-cache --no-interactive", + "mocha-and-metro": "mocha-remote --exit-on-error -- node --run metro", + premacos: "killall 'MacOSTestApp' || true", + macos: "react-native run-macos --no-packager", + test: "mocha-remote --exit-on-error -- concurrently --passthrough-arguments --kill-others-on-fail npm:metro 'npm:macos -- {@}' --", + "test:allTests": "MOCHA_REMOTE_CONTEXT=allTests node --run test:ios -- ", + "test:nodeAddonExamples": + "MOCHA_REMOTE_CONTEXT=nodeAddonExamples node --run test -- ", + "test:nodeTests": "MOCHA_REMOTE_CONTEXT=nodeTests node --run test -- ", + "test:ferricExample": + "MOCHA_REMOTE_CONTEXT=ferricExample node --run test -- ", + }; + + const { + "mocha-remote-cli": mochaRemoteCliSpec, + "mocha-remote-react-native": mochaRemoteReactNativeSpec, + } = otherPackageJson.dependencies || {}; + + assert(typeof mochaRemoteCliSpec === "string"); + assert(typeof mochaRemoteReactNativeSpec === "string"); + + packageJson.dependencies = { + ["mocha-remote-cli"]: mochaRemoteCliSpec, + ["mocha-remote-react-native"]: mochaRemoteReactNativeSpec, + ...packageJson.dependencies, + }; + + await fs.promises.writeFile( + path.join(APP_PATH, "package.json"), + JSON.stringify(packageJson, null, 2), + "utf8", + ); +} + +function installDependencies() { + console.log("Installing dependencies"); + exec( + "npm", + [ + "install", + "--save", + "--prefer-offline", + "--install-links", + "react-native-macos-init", + path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "node-addon-examples"), + ), + path.relative(APP_PATH, path.join(ROOT_PATH, "packages", "node-tests")), + path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "ferric-example"), + ), + path.relative(APP_PATH, path.join(ROOT_PATH, "packages", "host")), + ], + { + cwd: APP_PATH, + }, + ); +} + +function initializeReactNativeMacOSTemplate() { + console.log("Initializing react-native-macos template"); + exec("npx", ["react-native-macos-init"], { + cwd: APP_PATH, + }); +} + +async function patchPodfile() { + console.log("Patching Podfile"); + const replacements = [ + [ + // As per https://github.com/microsoft/react-native-macos/issues/2723#issuecomment-3392930688 + "require_relative '../node_modules/react-native-macos/scripts/react_native_pods'\nrequire_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'", + "require_relative '../node_modules/react-native-macos/scripts/cocoapods/autolinking'", + ], + [":hermes_enabled => false,", ":hermes_enabled => true,"], + [ + ":fabric_enabled => ENV['RCT_NEW_ARCH_ENABLED'] == '1',", + ":fabric_enabled => true,", + ], + [ + "react_native_post_install(installer)", + "react_native_post_install(installer, '../node_modules/react-native-macos')", + ], + ]; + + const podfilePath = path.join(APP_PATH, "macos", "Podfile"); + let podfileContents = await fs.promises.readFile(podfilePath, "utf8"); + for (const [searchValue, replaceValue] of replacements) { + podfileContents = podfileContents.replace(searchValue, replaceValue); + } + await fs.promises.writeFile(podfilePath, podfileContents, "utf8"); +} + +async function copySourceFiles() { + console.log("Copying source files from test-app into macos-test-app:"); + const FILE_NAMES = ["App.tsx", "babel.config.js"]; + for (const fileName of FILE_NAMES) { + console.log(`↳ ${fileName}`); + await fs.promises.copyFile( + path.join(OTHER_APP_PATH, fileName), + path.join(APP_PATH, fileName), + ); + } +} + +await deletePreviousApp(); +await initializeReactNativeTemplate(); +await patchPackageJson(); +installDependencies(); +initializeReactNativeMacOSTemplate(); +await patchPodfile(); +await copySourceFiles(); diff --git a/tsconfig.json b/tsconfig.json index a733c3ff..4ca25b74 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ }, "files": ["prettier.config.js", "eslint.config.js"], "references": [ + { "path": "./tsconfig.scripts.json" }, { "path": "./packages/cli-utils/tsconfig.json" }, { "path": "./packages/cmake-file-api/tsconfig.json" }, { "path": "./packages/cmake-file-api/tsconfig.tests.json" }, diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json new file mode 100644 index 00000000..88041106 --- /dev/null +++ b/tsconfig.scripts.json @@ -0,0 +1,10 @@ +{ + "extends": "./configs/tsconfig.node.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "declarationMap": false, + "rootDir": "scripts" + }, + "include": ["scripts"] +}