From 1adf988868cf21523ee064411f9605b33d8f4ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 28 Oct 2025 14:19:04 +0100 Subject: [PATCH 1/5] Remove platform restriction from host package's podspec --- packages/host/react-native-node-api.podspec | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/host/react-native-node-api.podspec b/packages/host/react-native-node-api.podspec index 13e941b7..5ed4072a 100644 --- a/packages/host/react-native-node-api.podspec +++ b/packages/host/react-native-node-api.podspec @@ -29,7 +29,6 @@ Pod::Spec.new do |s| s.license = package["license"] s.authors = package["author"] - s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://github.com/callstackincubator/react-native-node-api.git", :tag => "#{s.version}" } s.source_files = "apple/**/*.{h,m,mm}", "cpp/**/*.{hpp,cpp,c,h}", "weak-node-api/include/*.h", "weak-node-api/*.hpp" From 97c5c36d366f01fb6002a5ab9250c47134e8ea1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 29 Oct 2025 21:34:25 +0100 Subject: [PATCH 2/5] Add fallback for watchFolders in the metro config --- apps/test-app/metro.config.js | 13 ++++++++++++- eslint.config.js | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/test-app/metro.config.js b/apps/test-app/metro.config.js index 2c321b49..95e22157 100644 --- a/apps/test-app/metro.config.js +++ b/apps/test-app/metro.config.js @@ -1,5 +1,6 @@ const { makeMetroConfig } = require("@rnx-kit/metro-config"); -module.exports = makeMetroConfig({ + +const config = makeMetroConfig({ transformer: { getTransformOptions: async () => ({ transform: { @@ -9,3 +10,13 @@ module.exports = makeMetroConfig({ }), }, }); + +if (config.watchFolders.length === 0) { + // This patch is needed to locate packages in the monorepo from the MacOS app + // which is intentionally kept outside of the workspaces configuration to prevent + // duplicate react-native version and pollution of the package lock. + const path = require("node:path"); + config.watchFolders.push(path.resolve(__dirname, "../..")); +} + +module.exports = config; diff --git a/eslint.config.js b/eslint.config.js index c0af837e..e1bacb32 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -47,6 +47,7 @@ export default tseslint.config( { files: [ "apps/test-app/*.js", + "apps/macos-test-app/*.js", "packages/node-addon-examples/**/*.js", "packages/host/babel-plugin.js", "packages/host/react-native.config.js", @@ -68,6 +69,7 @@ export default tseslint.config( }, { files: [ + "**/metro.config.js", "packages/gyp-to-cmake/bin/*.js", "packages/host/bin/*.mjs", "packages/host/scripts/*.mjs", From f83c5c518d88d1ca09f13116168ce65b964a053a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 27 Oct 2025 22:57:39 +0100 Subject: [PATCH 3/5] Update Podspec to detect React Native package name --- .changeset/silly-mice-warn.md | 5 +++++ packages/host/scripts/patch-hermes.rb | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .changeset/silly-mice-warn.md 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/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 From f509d4ac5edf51100fb596815acd564e22f1f67f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 27 Oct 2025 22:33:14 +0100 Subject: [PATCH 4/5] Add script to init a MacOS test app Enable Hermes and Fabric Patch react_native_post_install to pass react native path Don't pod install when initializing Patch macos app with scripts and source files Re-arm the init script Move linked deps into install command to make --install-links effective Using regular linking for monorepo deps Include original dependencies Enable new arch in Podfile Fix comment from review --- .gitignore | 3 + package-lock.json | 27 ++--- package.json | 4 +- scripts/init-macos-test-app.ts | 190 +++++++++++++++++++++++++++++++++ tsconfig.json | 1 + tsconfig.scripts.json | 10 ++ 6 files changed, 221 insertions(+), 14 deletions(-) create mode 100644 scripts/init-macos-test-app.ts create mode 100644 tsconfig.scripts.json 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 76bde18c..9604cb48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "globals": "^16.0.0", "prettier": "^3.6.2", "react-native": "0.81.4", + "read-pkg": "^9.0.1", "tsx": "^4.20.5", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" @@ -14607,7 +14608,7 @@ }, "packages/cli-utils": { "name": "@react-native-node-api/cli-utils", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "@commander-js/extra-typings": "^14.0.0", "bufout": "^0.3.2", @@ -14824,11 +14825,11 @@ } }, "packages/cmake-rn": { - "version": "0.4.1", + "version": "0.5.1", "dependencies": { - "@react-native-node-api/cli-utils": "0.1.0", + "@react-native-node-api/cli-utils": "0.1.1", "cmake-file-api": "0.1.0", - "react-native-node-api": "0.5.2", + "react-native-node-api": "0.6.1", "zod": "^4.1.11" }, "bin": { @@ -14841,11 +14842,11 @@ }, "packages/ferric": { "name": "ferric-cli", - "version": "0.3.4", + "version": "0.3.6", "dependencies": { "@napi-rs/cli": "~3.0.3", - "@react-native-node-api/cli-utils": "0.1.0", - "react-native-node-api": "0.5.2" + "@react-native-node-api/cli-utils": "0.1.1", + "react-native-node-api": "0.6.1" }, "bin": { "ferric": "bin/ferric.js" @@ -14859,9 +14860,9 @@ } }, "packages/gyp-to-cmake": { - "version": "0.3.0", + "version": "0.4.0", "dependencies": { - "@react-native-node-api/cli-utils": "0.1.0", + "@react-native-node-api/cli-utils": "0.1.1", "gyp-parser": "^1.0.4", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1" @@ -14872,11 +14873,11 @@ }, "packages/host": { "name": "react-native-node-api", - "version": "0.5.2", + "version": "0.6.1", "license": "MIT", "dependencies": { "@expo/plist": "^0.4.7", - "@react-native-node-api/cli-utils": "0.1.0", + "@react-native-node-api/cli-utils": "0.1.1", "pkg-dir": "^8.0.0", "read-pkg": "^9.0.1", "zod": "^4.1.11" @@ -14892,7 +14893,7 @@ }, "peerDependencies": { "@babel/core": "^7.26.10", - "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4" + "react-native": "0.79.1 || 0.79.2 || 0.79.3 || 0.79.4 || 0.79.5 || 0.79.6 || 0.79.7 || 0.80.0 || 0.80.1 || 0.80.2 || 0.81.0 || 0.81.1 || 0.81.2 || 0.81.3 || 0.81.4" } }, "packages/node-addon-examples": { @@ -14915,7 +14916,7 @@ "cmake-rn": "*", "gyp-to-cmake": "*", "prebuildify": "^6.0.1", - "react-native-node-api": "^0.5.2", + "react-native-node-api": "^0.6.1", "read-pkg": "^9.0.1", "rolldown": "1.0.0-beta.29" } diff --git a/package.json b/package.json index eb2f9dff..be2dbf31 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "test": "npm test --workspace react-native-node-api --workspace cmake-rn --workspace gyp-to-cmake --workspace node-addon-examples", "bootstrap": "node --run build && npm run bootstrap --workspaces --if-present", "prerelease": "node --run build && npm run prerelease --workspaces --if-present", - "release": "changeset publish" + "release": "changeset publish", + "init-macos-test-app": "node scripts/init-macos-test-app.ts" }, "author": { "name": "Callstack", @@ -64,6 +65,7 @@ "globals": "^16.0.0", "prettier": "^3.6.2", "react-native": "0.81.4", + "read-pkg": "^9.0.1", "tsx": "^4.20.5", "typescript": "^5.8.0", "typescript-eslint": "^8.38.0" diff --git a/scripts/init-macos-test-app.ts b/scripts/init-macos-test-app.ts new file mode 100644 index 00000000..7d3abd1e --- /dev/null +++ b/scripts/init-macos-test-app.ts @@ -0,0 +1,190 @@ +import assert from "node:assert/strict"; +import cp from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { readPackage } from "read-pkg"; + +const REACT_NATIVE_VERSION = "0.79.6"; +const ROOT_PATH = path.join(import.meta.dirname, ".."); +const APP_PATH = path.join(ROOT_PATH, "apps", "macos-test-app"); +const OTHER_APP_PATH = path.join(ROOT_PATH, "apps", "test-app"); + +function exec(command: string, args: string[], options: cp.SpawnOptions = {}) { + const { status } = cp.spawnSync(command, args, { + stdio: "inherit", + ...options, + }); + assert.equal(status, 0, `Failed to execute '${command}'`); +} + +async function deletePreviousApp() { + if (fs.existsSync(APP_PATH)) { + console.log("Deleting existing app directory"); + await fs.promises.rm(APP_PATH, { recursive: true, force: true }); + } +} + +async function initializeReactNativeTemplate() { + console.log("Initializing community template"); + exec("npx", [ + "@react-native-community/cli", + "init", + "MacOSTestApp", + "--skip-install", + "--skip-git-init", + // "--platform-name", + // "react-native-macos", + "--version", + REACT_NATIVE_VERSION, + "--directory", + APP_PATH, + ]); + + // Clean up + const CLEANUP_PATHS = [ + "ios", + "android", + "__tests__", + ".prettierrc.js", + ".gitignore", + ]; + + for (const cleanupPath of CLEANUP_PATHS) { + await fs.promises.rm(path.join(APP_PATH, cleanupPath), { + recursive: true, + force: true, + }); + } +} + +async function patchPackageJson() { + console.log("Patching package.json scripts"); + const packageJson = await readPackage({ cwd: APP_PATH }); + const otherPackageJson = await readPackage({ cwd: OTHER_APP_PATH }); + + packageJson.scripts = { + ...packageJson.scripts, + metro: "react-native start --reset-cache --no-interactive", + "mocha-and-metro": "mocha-remote --exit-on-error -- node --run metro", + premacos: "killall 'MacOSTestApp' || true", + macos: "react-native run-macos --no-packager", + test: "mocha-remote --exit-on-error -- concurrently --passthrough-arguments --kill-others-on-fail npm:metro 'npm:macos -- {@}' --", + "test:allTests": "MOCHA_REMOTE_CONTEXT=allTests node --run test -- ", + "test:nodeAddonExamples": + "MOCHA_REMOTE_CONTEXT=nodeAddonExamples node --run test -- ", + "test:nodeTests": "MOCHA_REMOTE_CONTEXT=nodeTests node --run test -- ", + "test:ferricExample": + "MOCHA_REMOTE_CONTEXT=ferricExample node --run test -- ", + }; + + const transferredDependencies = new Set([ + "@rnx-kit/metro-config", + "mocha-remote-cli", + "mocha-remote-react-native", + ]); + + const { dependencies: otherDependencies = {} } = otherPackageJson; + + packageJson.dependencies = { + ...packageJson.dependencies, + "react-native-macos-init": "^2.1.3", + "@react-native-node-api/node-addon-examples": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "node-addon-examples"), + ), + "@react-native-node-api/node-tests": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "node-tests"), + ), + "@react-native-node-api/ferric-example": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "ferric-example"), + ), + "react-native-node-api": path.relative( + APP_PATH, + path.join(ROOT_PATH, "packages", "host"), + ), + ...Object.fromEntries( + Object.entries(otherDependencies).filter(([name]) => + transferredDependencies.has(name), + ), + ), + }; + + await fs.promises.writeFile( + path.join(APP_PATH, "package.json"), + JSON.stringify(packageJson, null, 2), + "utf8", + ); +} + +function installDependencies() { + console.log("Installing dependencies"); + exec("npm", ["install", "--prefer-offline"], { + cwd: APP_PATH, + }); +} + +function initializeReactNativeMacOSTemplate() { + console.log("Initializing react-native-macos template"); + exec("npx", ["react-native-macos-init"], { + cwd: APP_PATH, + }); +} + +async function patchPodfile() { + console.log("Patching Podfile"); + const replacements = [ + [ + // As per https://github.com/microsoft/react-native-macos/issues/2723#issuecomment-3392930688 + "require_relative '../node_modules/react-native-macos/scripts/react_native_pods'\nrequire_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'", + "require_relative '../node_modules/react-native-macos/scripts/cocoapods/autolinking'", + ], + [ + ":hermes_enabled => false,", + // Adding the new_arch_enabled here as it's not a part of the template + ":hermes_enabled => true,\n :new_arch_enabled => true,", + ], + [ + ":fabric_enabled => ENV['RCT_NEW_ARCH_ENABLED'] == '1',", + ":fabric_enabled => true,", + ], + [ + "react_native_post_install(installer)", + "react_native_post_install(installer, '../node_modules/react-native-macos')", + ], + ]; + + const podfilePath = path.join(APP_PATH, "macos", "Podfile"); + let podfileContents = await fs.promises.readFile(podfilePath, "utf8"); + for (const [searchValue, replaceValue] of replacements) { + podfileContents = podfileContents.replace(searchValue, replaceValue); + } + await fs.promises.writeFile(podfilePath, podfileContents, "utf8"); +} + +async function copySourceFiles() { + console.log("Copying source files from test-app into macos-test-app:"); + const FILE_NAMES = [ + "App.tsx", + // Adds the babel plugin needed to transform require calls + "babel.config.js", + // Adds the ability to reference symlinked packages + "metro.config.js", + ]; + for (const fileName of FILE_NAMES) { + console.log(`↳ ${fileName}`); + await fs.promises.copyFile( + path.join(OTHER_APP_PATH, fileName), + path.join(APP_PATH, fileName), + ); + } +} + +await deletePreviousApp(); +await initializeReactNativeTemplate(); +await patchPackageJson(); +installDependencies(); +initializeReactNativeMacOSTemplate(); +await patchPodfile(); +await copySourceFiles(); diff --git a/tsconfig.json b/tsconfig.json index a733c3ff..4ca25b74 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ }, "files": ["prettier.config.js", "eslint.config.js"], "references": [ + { "path": "./tsconfig.scripts.json" }, { "path": "./packages/cli-utils/tsconfig.json" }, { "path": "./packages/cmake-file-api/tsconfig.json" }, { "path": "./packages/cmake-file-api/tsconfig.tests.json" }, diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json new file mode 100644 index 00000000..88041106 --- /dev/null +++ b/tsconfig.scripts.json @@ -0,0 +1,10 @@ +{ + "extends": "./configs/tsconfig.node.json", + "compilerOptions": { + "composite": true, + "emitDeclarationOnly": true, + "declarationMap": false, + "rootDir": "scripts" + }, + "include": ["scripts"] +} From 02ae255dd89c96cd3bfa572d5e19c89bc33637d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 27 Oct 2025 23:18:46 +0100 Subject: [PATCH 5/5] Add job in the check workflow to initialize and build the MacOS test app No need to Setup Android SDK Debugging with Copilot Pass --mode when building from CLI Install CMake 3.22 Fix bootstrap issue Trying a higher CMake version Remove debug info from workflow Build universal Darwin libraries Add missing x86_64-apple-darwin Rust target to macOS CI job (#298) * Initial plan * Add missing x86_64-apple-darwin Rust target to macOS job Co-authored-by: kraenhansen <1243959+kraenhansen@users.noreply.github.com> * Add only missing x86_64-apple-darwin target (aarch64 is host) Co-authored-by: kraenhansen <1243959+kraenhansen@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: kraenhansen <1243959+kraenhansen@users.noreply.github.com> Run MacOS test app Use package script to run all tests --- .github/workflows/check.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ec9c7cee..a221cfc4 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -107,6 +107,39 @@ jobs: # TODO: Enable release mode when it works # run: npm run test:ios -- --mode Release working-directory: apps/test-app + test-macos: + # Disabling this on main for now, as initializing the template takes a long time and + # we don't have macOS-specific code yet + if: contains(github.event.pull_request.labels.*.name, 'MacOS 💻') + name: Test app (macOS) + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/jod + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: "17" + distribution: "temurin" + # Install CMake 3 since 4.x may have compatibility issues with Hermes build system + - name: Install compatible CMake version + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: "3.31.2" + - run: rustup target add x86_64-apple-darwin + - run: npm ci + - run: npm run bootstrap + env: + CMAKE_RN_TRIPLETS: arm64;x86_64-apple-darwin + FERRIC_TARGETS: aarch64-apple-darwin,x86_64-apple-darwin + - run: npm run init-macos-test-app + - run: pod install --project-directory=macos + working-directory: apps/macos-test-app + - name: Run MacOS test app + run: npm run test:allTests -- --mode Release + working-directory: apps/macos-test-app test-android: if: github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'Android 🤖') name: Test app (Android)