[Draft] Bazel support: build the full RNTester.app on Xcode 26 (rules_js Berry fork + native XCFramework link)#3014
Draft
Saadnajmi wants to merge 16 commits into
Draft
Conversation
Establishes an additive, experimental Bazel build for react-native-macos and proves the hardest, most novel piece end to end: driving aspect-build rules_js from the repo's Yarn 4 (Berry) yarn.lock as the single source of truth, with no committed pnpm-lock. What's green (verified): - `bazel test //tools/bazel/berry/example:verify` — a real Berry yarn.lock is translated to a pnpm-lock v9 by a dependency-free converter (tools/bazel/berry/berry_to_pnpm_lock.mjs) via a one-seam patch to rules_js (tools/bazel/patches/aspect_rules_js_berry.patch, applied with single_version_override), then rules_js fetches + links + runs it. - On the real monorepo lock the converter yields 33 importers / 1333 packages with zero dangling references; rules_js fetches all 1333. Scaffolded (tagged manual, WIP — see docs/bazel.md): - metro_bundle macro (tools/bazel/js/metro.bzl) + rn-tester bundle target. - Prebuilt XCFramework import seam (tools/bazel/apple/xcframeworks.bzl) and a macos_application slice reusing rn-tester's AppDelegate/main. Also: MODULE.bazel/.bazelrc/.bazelversion/.bazelignore, a non-blocking CI workflow that runs the green proof, and a design doc (docs/bazel.md). The generated pnpm-lock.yaml and MODULE.bazel.lock are gitignored; the only published-adjacent change is an inert empty pnpm.onlyBuiltDependencies in the private monorepo root package.json. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…olding
Extends the Bazel slice toward an end-to-end RNTester macOS build.
First-party workspace linking (rules_js):
- Every workspace package now exposes a `:pkg` npm_package target via a generated
BUILD.bazel, so rules_js can link first-party packages (react-native-macos etc.)
from the Berry yarn.lock. `bazel build //packages/react-native:pkg` builds; rn-tester's
node_modules links react-native-macos (consumed as `react-native`).
rn-tester slice:
- packages/rn-tester/BUILD.bazel: npm_link_all_packages + a Metro bundle target
(js_run_binary around bazel/bundle.js, which drives Metro's API with the
react-native -> react-native-macos alias) + the macos_application wiring.
- packages/rn-tester/bazel/bundle.js: sandbox-safe Metro bundling entry.
Verified / documented (docs/bazel.md):
- rn-tester's macOS JS bundle builds outside Bazel (~7MB) after building
@react-native/codegen lib — proving the JS is bundleable.
- Two remaining blockers for a running app, documented:
1) the macOS app can't be built here because the Hermes nightly tarball 404s
(no XCFrameworks); the app target is fully scaffolded for when they exist.
2) the hermetic Bazel Metro bundle needs the strict-deps tooling closure declared
(rules_js does not hoist like Yarn).
Targets that require these follow-ups remain tagged `manual`; the green Berry-fork
proof (//tools/bazel/berry/example:verify) is unaffected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…a 404) The earlier note blamed a "Hermes nightly 404". That was caused by forcing HERMES_VERSION=nightly, which bypasses react-native-macos's Hermes version-resolution patches (scripts/ios-prebuild/microsoft-hermes.js). Invoked correctly, ios-prebuild resolves the right Hermes (merge-base commit on main / published artifact on release branches). Building it from source in this environment fails for toolchain reasons: default CMake is 4.x (use cmake@3.26.4), and Xcode 26 / AppleClang 21 trips LLVM's CheckAtomic. The prebuild CI pins Xcode 16.2/16.1, which is what builds these XCFrameworks. Not a fork gap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On Xcode 26 / AppleClang 21, clang honors the *_DEPLOYMENT_TARGET environment
variables and mis-targets the *native* host hermesc build to visionOS when
XROS_DEPLOYMENT_TARGET is set in the environment (XROS takes precedence even if
MACOSX_DEPLOYMENT_TARGET is also set). ios-prebuild sets XROS_DEPLOYMENT_TARGET for
the cross-platform target builds and it leaks into build_host_hermesc, producing:
cc: warning: using sysroot for 'MacOSX' but targeting 'XR' [-Wincompatible-sysroot]
error: 'pthread_setname_np' is unavailable: not available on visionOS
which fails llvh's CheckAtomic ("Host compiler appears to require libatomic, but
cannot find it") and aborts the from-source Hermes build.
Build the native host tools in a subshell that unsets the mobile
IOS/XROS/TVOS_DEPLOYMENT_TARGET vars and sets MACOSX_DEPLOYMENT_TARGET, so clang
targets macOS. Verified: HAVE_CXX_ATOMICS_WITHOUT_LIB now succeeds and hermesc
compiles under Xcode 26.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ORMS) Adds an opt-in HERMES_APPLE_PLATFORMS env var to build only selected Apple slices into hermes.xcframework (default: all, unchanged). Enables macOS-only from-source Hermes builds, which is useful on Xcode 26+ (the upstream Hermes Mac Catalyst target triple is rejected by the newer clang) and greatly speeds up macOS-only iteration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With the Hermes host-build fix + HERMES_APPLE_PLATFORMS subset + CMake 3.x, verified on Xcode 26.4 that ios-prebuild produces hermes.xcframework (from source), ReactNativeDependencies.xcframework (downloaded), and React.xcframework (BUILD SUCCEEDED). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… 26) Adds tools/bazel/apple/prebuilt_xcframeworks.bzl: a repository rule + module extension that symlinks the SPM-prebuilt React/ReactNativeDependencies/hermes XCFrameworks (from packages/react-native/.build and third-party/) into a Bazel repo and exposes apple_*_xcframework_import targets. Verified on Xcode 26.4: bazel build @rn_prebuilt_xcframeworks//:React //:hermes //:ReactNativeDependencies builds. Wires the rn-tester macOS app host deps to these targets. Note: rn-tester's real AppDelegate needs rn-tester-specific native modules + codegen not present in the XCFrameworks, so a green app needs those built or a minimal RN host. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…undle progress
Converter correctness fix (tools/bazel/berry/berry_to_pnpm_lock.mjs):
- Apply Yarn `resolutions` when resolving dependency descriptors. Berry rewrites matching
descriptors (e.g. resolutions:{debug:">=3.1.0"} makes all `debug` resolve to
debug@npm:>=3.1.0) while dependency entries keep their original ranges (debug: "npm:4").
Previously these edges were silently dropped (e.g. https-proxy-agent lost its `debug`
dep), producing an incomplete node_modules graph.
- Trim dependency ranges so a descriptor like "npm:^3.1.0 " (Yarn keeps insignificant
trailing whitespace) matches the trimmed descriptor key.
- Add a single-version fallback and report any dropped edges. Result: 0 dropped edges on
the monorepo lock (previously silently dropping several).
rn-tester JS bundle (Phase A, still WIP/manual):
- Declare the bundler tooling closure on rn-tester (metro, @react-native/metro-config,
@react-native/metro-babel-transformer, react) so rules_js's strict node_modules resolves
it; regenerates yarn.lock.
- bazel/bundle.js: resolve project root/entry/out via env, disable watchman + enable
symlinks for the sandbox. Metro now loads and resolves the full first-party + third-party
graph; remaining blockers are documented in docs/bazel.md (first-party packages must be
consumed in built/dist form, and a Metro file-map vs Bazel file-layout issue).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…dist, Metro file-map) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ers)
Phase A (JS bundle): rules_js drives Metro to emit a full 1.9MB
RNTesterApp.macos.jsbundle. First-party workspace packages are built in
dist form (first_party.bzl) so their src entry points don't escape the
copied node_modules; copy_tree.js stages a symlink-free project dir to
satisfy Metro's Node file-map crawler. Custom Metro resolver handles the
react-native -> react-native-macos alias (incl. subpaths) and directory
index resolution. Fixed the final output-path mismatch: pass bundleOut
(verbatim) instead of out, which Metro rewrites to force a .js suffix.
Phase B (codegen): tools/bazel/react_native builds @react-native/codegen
hermetically (codegen_lib) and generates AppSpecs + RCTAppDependencyProvider
for rn-tester's Native{Component,Module,CxxModule}Example.
All targets are Bazel-only and opt-in; the default yarn/xcodebuild/SPM
flows are untouched.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bazel build //packages/rn-tester:RNTesterMacBazel now produces a launchable macOS RNTester .app entirely with Bazel: the Metro JS bundle, a native RCTReactNativeFactory host, and the prebuilt React/hermes/ReactNativeDependencies XCFrameworks linked + the bundle embedded as an app resource. The SPM prebuild flattens React.xcframework's headers into per-module dirs (Headers/<Module>/x.h) and ships only a partial set, so the framework isn't directly consumable by clang. Two pieces bridge it: * tools/bazel/apple/reconstruct_react_headers.py — a repo-rule script that scans the framework headers' #include directives and rebuilds a canonical -I symlink tree for the Obj-C <React/...> / <RCTReactNativeFactory.h> surface (basename collisions disambiguated by path-segment match). Exposed as :React_headers with the RN RCT_* defines. * //packages/react-native:rn_cxx_headers — the complete, canonically-nested C++ headers (<react/renderer/...>, <ReactCommon/...>, <jsi/...>, <yoga/...>) sourced from the RN tree (same main version as the binary) via the podspec-equivalent include roots, including the react-native-macos view platform/macos override. Third-party <folly/...> etc. come from ReactNativeDependencies' canonical Headers. RN's new-arch C++ needs C++20 (-std=c++20 in .bazelrc). The minimal host omits rn-tester's own native example modules so it links against only the prebuilt binaries; wiring those + the Phase B codegen is the next increment toward the full AppDelegate. App/native targets stay tagged manual so bazel build //... stays green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tive modules) Adds :RNTesterMacBazelFull, linking rn-tester's real RNTester/AppDelegate.mm (via bazel/real_main.m) instead of the minimal host: * Phase B codegen is now compiled: rn_codegen emits explicit file targets and wraps them in cc_library header targets with virtual prefixes so the app can #import <ReactCodegen/...>, <ReactAppDependencyProvider/...> and <react/renderer/components/AppSpecs/...>; rn_tester_appspecs_lib compiles the AppSpecs + providers (RCTAppDependencyProvider is linked into the app). * NativeCxxModuleExample (C++ TurboModule) compiled + exposed as <NativeCxxModuleExample/...>. * Sample TurboModules (SampleTurboCxxModule / RCTSampleTurboModule) built from the RN source samples; +samples include roots on rn_cxx_headers. * RCTLinking + RCTPushNotification built from source (not in the prebuilt React.xcframework) and exposed as <React/...>; UserNotifications.framework linked. * reconstruct_react_headers.py: map the <FBReactNativeSpec/FBReactNativeSpec.h> core-codegen umbrella (referenced by app sources, not framework headers). RN_DISABLE_OSS_PLUGIN_HEADER still skips the Fabric NativeComponentExample (its sources use quoted plugin includes not yet wired). All targets stay tagged manual. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mple Builds NativeComponentExample (RNTMyNativeView Fabric component + legacy/interop views) and links it into :RNTesterMacBazelFull, dropping RN_DISABLE_OSS_PLUGIN_HEADER. The real, unmodified rn-tester AppDelegate.mm now compiles + links with ALL its example modules (codegen providers, C++ TurboModule, Fabric component, sample TurboModules) — no rn-tester source is patched or #ifdef'd out. A small header shim exposes the core <RCTFabricComponentsPlugins.h> (quoted by the component sources; impl lives in React.xcframework). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Applies the JS-ecosystem ergonomic lesson (derive build metadata from package.json rather than hand-authoring per package) to our Bazel setup: * tools/bazel/js/workspace_package.bzl - rn_workspace_package() macro that collapses the repeated npm_link_all_packages()+npm_package boilerplate. * tools/bazel/js/gen_package_builds.mjs - reads the root package.json 'workspaces' globs and (re)writes the boilerplate BUILD files. It only owns generator-owned files (its @generated marker, or pure legacy :pkg boilerplate) and skips anything declaring other targets (react-native, rn-tester, the first_party dist packages, etc.), mirroring Gazelle's '# gazelle:' override model. Supports --all (create missing) and --check (CI staleness guard). Regenerated 23 boilerplate files from 24 -> 6 lines each (net -414 lines). Verified: the generated :pkg targets build and the rn-tester JS bundle still bundles. This is the incremental step toward full gazelle-based generation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…xes app launch) The SPM prebuild ships React.framework and ReactNativeDependencies.framework as dynamic frameworks (dylibs), but they were imported via apple_static_xcframework_import, so rules_apple linked them with @rpath load commands without embedding them. The app crashed at launch in dyld: 'Library not loaded: @rpath/React.framework/Versions/A/React'. Importing all three (React, ReactNativeDependencies, hermes) as dynamic embeds them in Contents/Frameworks. Verified: RNTesterMacBazel.app now launches and stays running (loads the embedded Metro bundle + Hermes offline). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Supersedes #2959 (the original PR's source fork is disabled, so that branch can no longer be updated). This carries the same three macOS Fabric focus/first-responder fixes forward, with @tvinhas's original commit cherry-picked to preserve authorship credit, plus one small refinement.
The three regressions were surfaced while validating the "Verify focus changes" item on #2901.
1.
RCTViewComponentView.blur()was a no-opresignFirstResponderonNSWindowdoesn't clear the focused subview, soref.blur()from JS did nothing. Now clears the first responder viamakeFirstResponder:nil, guarded onselfso it only clears focus when this view actually holds it — matching iOS[self resignFirstResponder]semantics and avoiding stealing focus from an unrelated view.2. Fabric
setAccessibilityFocuswas a silent no-op on macOSsynchronouslyDispatchAccessbilityEventOnUIThread:had a#if !TARGET_OS_OSXguard with no#else, soAccessibilityInfo.setAccessibilityFocus(reactTag)did nothing on the Fabric path. Added the macOS branch (makeFirstResponder:+NSAccessibilityFocusedUIElementChangedNotification), mirroring the old-arch path inRCTAccessibilityManager.mm.3.
RCTVirtualViewComponentViewkeyboard-nav hooksAdded
acceptsFirstResponderandaccessibilityHitTest:to the macOS branch so keyboard/Tab and VoiceOver navigation unhide flagged-offscreen views under thehideOffscreenVirtualViewsoptimization. Partial by design (a fullyhidden=YESview is dropped from the responder chain — full fix needs a hide-strategy redesign, tracked as follow-up).Difference from #2959
if ([[self window] firstResponder] == self)guard onblur()for iOS-parity / to avoid focus-stealing.Test plan
xcodebuild -scheme RNTester-macOS -destination 'generic/platform=macOS') — BUILD SUCCEEDED.#if TARGET_OS_OSX/#else) and preserve existing iOS/visionOS behavior.Credit
Original work by @tvinhas in #2959 (commit cherry-picked to retain authorship).
Co-authored-by: Thiago Vinhas thiago@vinhas.net