From 1e45261d1195335cacff30eb00bff17e7d8d881e Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 6 Jul 2026 13:02:47 -0700 Subject: [PATCH 01/17] Add Bazel foundation with a rules_js Yarn Berry fork (draft) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- .bazelignore | 40 ++ .bazelrc | 19 + .bazelversion | 1 + .github/workflows/bazel.yml | 51 ++ .gitignore | 14 + BUILD.bazel | 6 + MODULE.bazel | 62 +++ docs/bazel.md | 181 +++++++ package.json | 3 + packages/rn-tester/BUILD.bazel | 69 +++ packages/rn-tester/bazel/Info.plist | 26 + packages/rn-tester/bazel/README.md | 23 + tools/bazel/apple/BUILD.bazel | 4 + tools/bazel/apple/xcframeworks.bzl | 47 ++ tools/bazel/berry/BUILD.bazel | 4 + tools/bazel/berry/berry_to_pnpm_lock.mjs | 473 ++++++++++++++++++ tools/bazel/berry/example/.yarnrc.yml | 2 + tools/bazel/berry/example/BUILD.bazel | 18 + tools/bazel/berry/example/package.json | 11 + tools/bazel/berry/example/verify.js | 19 + tools/bazel/berry/example/yarn.lock | 30 ++ tools/bazel/js/BUILD.bazel | 4 + tools/bazel/js/metro.bzl | 74 +++ tools/bazel/patches/BUILD.bazel | 2 + .../bazel/patches/aspect_rules_js_berry.patch | 79 +++ 25 files changed, 1262 insertions(+) create mode 100644 .bazelignore create mode 100644 .bazelrc create mode 100644 .bazelversion create mode 100644 .github/workflows/bazel.yml create mode 100644 BUILD.bazel create mode 100644 MODULE.bazel create mode 100644 docs/bazel.md create mode 100644 packages/rn-tester/BUILD.bazel create mode 100644 packages/rn-tester/bazel/Info.plist create mode 100644 packages/rn-tester/bazel/README.md create mode 100644 tools/bazel/apple/BUILD.bazel create mode 100644 tools/bazel/apple/xcframeworks.bzl create mode 100644 tools/bazel/berry/BUILD.bazel create mode 100644 tools/bazel/berry/berry_to_pnpm_lock.mjs create mode 100644 tools/bazel/berry/example/.yarnrc.yml create mode 100644 tools/bazel/berry/example/BUILD.bazel create mode 100644 tools/bazel/berry/example/package.json create mode 100644 tools/bazel/berry/example/verify.js create mode 100644 tools/bazel/berry/example/yarn.lock create mode 100644 tools/bazel/js/BUILD.bazel create mode 100644 tools/bazel/js/metro.bzl create mode 100644 tools/bazel/patches/BUILD.bazel create mode 100644 tools/bazel/patches/aspect_rules_js_berry.patch diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 000000000000..5a76730ea401 --- /dev/null +++ b/.bazelignore @@ -0,0 +1,40 @@ +node_modules +.git +.yarn +packages/react-native/.build +packages/rn-tester/Pods +packages/helloworld +private/helloworld +packages/assets/node_modules +packages/babel-plugin-codegen/node_modules +packages/community-cli-plugin/node_modules +packages/core-cli-utils/node_modules +packages/debugger-frontend/node_modules +packages/debugger-shell/node_modules +packages/dev-middleware/node_modules +packages/eslint-config-react-native/node_modules +packages/eslint-plugin-react-native/node_modules +packages/eslint-plugin-specs/node_modules +packages/gradle-plugin/node_modules +packages/metro-config/node_modules +packages/new-app-screen/node_modules +packages/normalize-color/node_modules +packages/polyfills/node_modules +packages/react-native-babel-preset/node_modules +packages/react-native-babel-transformer/node_modules +packages/react-native-codegen/node_modules +packages/react-native-compatibility-check/node_modules +packages/react-native-macos-init/node_modules +packages/react-native-popup-menu-android/node_modules +packages/react-native-test-library/node_modules +packages/react-native/node_modules +packages/rn-tester/node_modules +packages/typescript-config/node_modules +packages/virtualized-lists/node_modules +private/cxx-public-api/node_modules +private/eslint-plugin-monorepo/node_modules +private/monorepo-tests/node_modules +private/react-native-bots/node_modules +private/react-native-codegen-typescript-test/node_modules +private/react-native-fantom/node_modules +tools/bazel/berry/example/node_modules diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 000000000000..88eeec108641 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,19 @@ +# react-native-macos Bazel configuration (draft / experimental) +# See docs/bazel.md for the design and current status. + +# Our pnpm-lock.yaml is generated on the fly from the Berry yarn.lock by the +# rules_js fork, so we don't pin the module resolution in MODULE.bazel.lock. +common --lockfile_mode=off + +# Local disk cache (also the base for the GHA actions/cache remote cache). +build --disk_cache=~/.cache/bazel-rnm-disk + +# Nicer output. +build --show_result=1 + +# Apple: build macOS slices. Scoped in practice to the app target's transitions. +build:macos --apple_platform_type=macos + +# CI convenience config. +build:ci --color=yes +build:ci --show_timestamps diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 000000000000..df5119ec64e6 --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +8.7.0 diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml new file mode 100644 index 000000000000..bb4bcbfe6145 --- /dev/null +++ b/.github/workflows/bazel.yml @@ -0,0 +1,51 @@ +# Experimental Bazel CI for react-native-macos. +# +# Today this verifies the rules_js *Berry fork* end to end: it builds/tests a target +# whose node_modules are materialized from a Yarn Berry `yarn.lock` (translated to a +# pnpm-lock by tools/bazel/berry, with no committed second lockfile). The macOS app +# slice (rules_apple) is added incrementally — see docs/bazel.md. +name: Bazel (experimental) + +on: + pull_request: + paths: + - "MODULE.bazel" + - ".bazelrc" + - ".bazelversion" + - ".bazelignore" + - "BUILD.bazel" + - "tools/bazel/**" + - "packages/**/BUILD.bazel" + - ".github/workflows/bazel.yml" + workflow_dispatch: {} + +permissions: + contents: read + +concurrency: + group: bazel-${{ github.ref }} + cancel-in-progress: true + +jobs: + berry-fork: + name: rules_js Berry fork (JS) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + # Disk cache doubles as a simple GHA-backed Bazel remote cache. For a real + # remote cache at scale, point --remote_cache at BuildBuddy or a self-hosted + # bazel-remote (see docs/bazel.md). + - name: Cache Bazel + uses: actions/cache@v4 + with: + path: ~/.cache/bazel-rnm-disk + key: bazel-berry-${{ runner.os }}-${{ hashFiles('MODULE.bazel', 'yarn.lock', 'tools/bazel/**') }} + restore-keys: | + bazel-berry-${{ runner.os }}- + - name: Test the Berry -> rules_js pipeline end to end + run: | + npx --yes @bazel/bazelisk test //tools/bazel/berry/example:verify \ + --config=ci --test_output=errors diff --git a/.gitignore b/.gitignore index ce454b9629b1..8f26c47320b7 100644 --- a/.gitignore +++ b/.gitignore @@ -204,3 +204,17 @@ fix_*.patch .cursor/rules/nx-rules.mdc .github/instructions/nx.instructions.md # macOS] + +# Generated by the rules_js Berry fork from yarn.lock (not committed) +/pnpm-lock.yaml + +# Berry fork example: generated lock + transient node_modules +/tools/bazel/berry/example/pnpm-lock.yaml +tools/bazel/berry/example/node_modules +tools/bazel/berry/example/.yarn + +# Bazel resolution lock (module deps pinned via BCR; pnpm-lock is generated) +/MODULE.bazel.lock + +# Bazel convenience symlinks +/bazel-* diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 000000000000..4aebeedb374c --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,6 @@ +load("@npm//:defs.bzl", "npm_link_all_packages") + +# Links the pnpm-resolved (from the Berry yarn.lock via our fork) node_modules +# into the Bazel build graph. Downstream JS targets depend on +# //:node_modules/. +npm_link_all_packages(name = "node_modules") diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 000000000000..803e86df0fb3 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,62 @@ +module( + name = "react_native_macos_monorepo", + version = "0.0.0", + compatibility_level = 1, +) + +############################################################################### +# JavaScript / Node toolchain (aspect-build rules_js) +############################################################################### +bazel_dep(name = "aspect_rules_js", version = "3.2.2") +bazel_dep(name = "aspect_rules_ts", version = "3.8.11") +bazel_dep(name = "aspect_bazel_lib", version = "2.22.5") +bazel_dep(name = "rules_nodejs", version = "6.7.5") + +# Local fork of rules_js: teach `npm_translate_lock` to read the Yarn Berry (v2+) +# `yarn.lock` directly, so it stays the single source of truth (no committed +# second lockfile). The patch swaps the `pnpm import` step for our bundled +# Berry -> pnpm-lock converter (tools/bazel/berry/berry_to_pnpm_lock.mjs) when +# the input yarn.lock is a Berry lockfile. See docs/bazel.md. +single_version_override( + module_name = "aspect_rules_js", + patch_strip = 1, + patches = ["//tools/bazel/patches:aspect_rules_js_berry.patch"], + version = "3.2.2", +) + +node = use_extension("@rules_nodejs//nodejs:extensions.bzl", "node", dev_dependency = True) +node.toolchain(node_version = "22.14.0") + +npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm", dev_dependency = True) +npm.npm_translate_lock( + name = "npm", + # The Berry yarn.lock is the single source of truth. Our rules_js patch + # converts it to an (uncommitted, gitignored) pnpm-lock.yaml at rule time + # before parsing, so update_pnpm_lock (pnpm import) is left off. + yarn_lock = "//:yarn.lock", + pnpm_lock = "//:pnpm-lock.yaml", + update_pnpm_lock = False, + verify_node_modules_ignored = "//:.bazelignore", + quiet = False, +) +use_repo(npm, "npm") + +# Self-contained green proof of the Berry fork (see tools/bazel/berry/example). +npm.npm_translate_lock( + name = "npm_berry_example", + yarn_lock = "//tools/bazel/berry/example:yarn.lock", + pnpm_lock = "//tools/bazel/berry/example:pnpm-lock.yaml", + update_pnpm_lock = False, + verify_node_modules_ignored = "//:.bazelignore", + quiet = False, +) +use_repo(npm, "npm_berry_example") + +############################################################################### +# Apple toolchain (rules_apple / rules_swift) — used by the macOS app slice +############################################################################### +bazel_dep(name = "rules_apple", version = "4.5.3") +bazel_dep(name = "rules_swift", version = "3.6.1") +bazel_dep(name = "apple_support", version = "2.7.0") +bazel_dep(name = "rules_cc", version = "0.2.17") +bazel_dep(name = "platforms", version = "1.1.0") diff --git a/docs/bazel.md b/docs/bazel.md new file mode 100644 index 000000000000..a4e0c5cdc438 --- /dev/null +++ b/docs/bazel.md @@ -0,0 +1,181 @@ +# Bazel support for react-native-macos (draft / experimental) + +This document describes the **experimental Bazel build** being added to +react-native-macos, why it is designed the way it is, what works today, and the +roadmap. It is intentionally additive: the existing yarn / Metro / xcodebuild / SPM / +CocoaPods workflows are unchanged. + +## Goal + +Prove a **full working vertical slice**: bundle rn-tester's JavaScript with Metro +(driven by Bazel via aspect-build `rules_js`) and link/embed it into a macOS app +(`macos_application` via `rules_apple`) that consumes the prebuilt React Native +XCFrameworks — an end-to-end `bazel run` of a macOS RN app. Longer term, build those +XCFrameworks from source in Bazel too (see the roadmap). + +## Why Bazel + +* **Incrementality + caching**: fine-grained, content-addressed caching of JS bundling + and (later) native compilation, shared between CI and local dev via a remote cache. +* **One graph across languages**: the JS→native seam (a Metro bundle becoming an app + resource) is modeled explicitly, the way Meta does it in Buck2's `js`/`apple` + preludes — but reusing Metro and rules_apple instead of reimplementing them. + +## The single-source-of-truth lockfile problem (and the rules_js Berry fork) + +The repo uses **Yarn 4 (Berry)**; `yarn.lock` is `__metadata: version: 8`. We keep +that Berry `yarn.lock` as the **single source of truth** — no committed second lockfile. + +aspect `rules_js` normalizes every lockfile to an internal pnpm model. `npm_translate_lock` +does this in two stages: + +1. **foreign lock → `pnpm-lock.yaml`**: for a `yarn_lock`/`npm_package_lock` input it + shells out to the `pnpm import` binary + (`update_cmd = ["import"] …` in `npm/private/npm_translate_lock.bzl`). +2. **`pnpm-lock.yaml` → Bazel repo**: a Starlark parser (`npm/private/pnpm.bzl`) builds + `importers`/`packages`/`snapshots` and strictly cross-validates them. + +**The gap is isolated to stage 1**: `pnpm import` (and hence rules_js's `yarn_lock` path) +supports Yarn *Classic* v1 and npm — **but not Yarn Berry v2+** (pnpm issue #2991). +Stage 2 is format-agnostic and requires **pnpm lockfile v9**. + +### The fork + +We patch rules_js at exactly that seam +(`tools/bazel/patches/aspect_rules_js_berry.patch`, applied via bzlmod +`single_version_override`). The patch adds `_generate_pnpm_lock_from_berry`, which runs +at the top of `parse_and_verify_lock`: when the input `yarn.lock` is a Berry lockfile and +no `pnpm-lock.yaml` exists yet, it invokes our **dependency-free Node converter** +(`tools/bazel/berry/berry_to_pnpm_lock.mjs`) to translate the Berry lock into a +**pnpm-lock v9** written into the source root. That file is **gitignored** — generated at +rule time, never committed. `update_pnpm_lock` is left `False` (the `pnpm import` path is +never taken). The patch `rctx.watch`es the yarn.lock and the converter so changes +retrigger it. + +The converter reconstructs the whole graph from the Berry lock alone: + +* **descriptors → versions**: every Berry descriptor (`name@range`) maps to its resolved + entry, so package dependency ranges resolve to concrete versions (and thus to + `snapshots` keys). +* **workspaces → importers** with `link:` deps computed as importer-relative paths. +* **npm aliases** (e.g. `rxjs → @react-native-community/rxjs@6.5.4-custom`) map to the + aliased package's full snapshot key. +* It self-validates internal consistency exactly like `pnpm.bzl` does. + +On the real monorepo lock this yields **33 importers / 1333 packages, zero dangling +references**. + +### Known limitation: integrity + +Berry's `checksum` is Yarn's **own content hash**, *not* the npm tarball's `sha512` +integrity that pnpm/rules_js expect, so we cannot derive `resolution.integrity` from the +Berry lock. rules_js requires `integrity` **or** `tarball`, so the converter emits the +deterministic npm registry **tarball URL** and packages are downloaded **without integrity +verification**. This is acceptable for a draft but should be hardened for production by +sourcing real integrities — e.g. a one-time npm-registry packument fetch (cached) or a +small Yarn plugin that exports npm integrities alongside `yarn.lock`. + +### Why a fork and not BYONM + +"Bring your own node_modules" (a `repository_rule` running `yarn install`) also works and +is documented by aspect, but it gives coarse-grained caching and bypasses rules_js's +package graph. The fork keeps rules_js's fine-grained model while reading the Berry lock. +BYONM remains a viable fallback. + +## What works today (verified green) + +* `bazel test //tools/bazel/berry/example:verify` — a self-contained proof: a real Berry + `yarn.lock` (is-odd → is-number) is translated by the fork, fetched and linked by + rules_js, and a Node program that `require`s the npm dep runs green. +* On the **real monorepo** `yarn.lock`, `npm_translate_lock` parses the fork-generated + lock and **fetches all 1333 packages** successfully. + +## What's next (WIP, scaffolded) + +* **First-party workspace linking**: `//:node_modules` (link *all* packages) needs a + `BUILD.bazel` in each of the ~33 workspace packages (the standard rules_js monorepo + adoption step). Until then the Metro bundle target is tagged `manual`. +* **Metro bundle**: `//packages/rn-tester:rntester_macos_jsbundle` via the `metro_bundle` + macro (`tools/bazel/js/metro.bzl`) — a thin `js_run_binary` wrapper around the React + Native CLI `bundle` command (we drive Metro, we don't reimplement it). +* **macOS app**: `//packages/rn-tester:RNTesterMacBazel` (`macos_application`) links the + imported XCFrameworks and embeds the Metro bundle as an app resource, reusing + rn-tester's existing `AppDelegate.mm`/`main.m`. + +## Apple: prebuilt XCFrameworks (swappable seam) + +`tools/bazel/apple/xcframeworks.bzl` imports the prebuilt +`React` / `ReactNativeDependencies` / `hermes` XCFrameworks (produced by +`scripts/ios-prebuild.js`) via `apple_static_xcframework_import`, behind stable aliases +(`:React`, …). A future from-source build produces the *same* artifacts and drops in +behind these aliases with a `--//:rn_from_source` flag. + +Generate the XCFrameworks with: + +```sh +cd packages/react-native +node scripts/ios-prebuild.js -s -f Debug +node scripts/ios-prebuild.js -b -f Debug -p macos +node scripts/ios-prebuild.js -c -f Debug +``` + +## Xcode compatibility + +rules_apple tracks Xcode closely. rules_apple `master` already targets Xcode 26.4; the +pinned release here is `4.5.3` (with `rules_swift` 3.6.1). If the CI/local Xcode isn't +supported by the pinned rules_apple, either bump rules_apple (5.0.0-rc / master) or select +a supported Xcode via `DEVELOPER_DIR` / `--xcode_version`. Bazel is pinned to `8.7.0` +(`.bazelversion`). + +## CI and remote cache + +`.github/workflows/bazel.yml` runs the green Berry-fork test on `ubuntu-latest`. It caches +Bazel's `--disk_cache` via `actions/cache` (a zero-infra remote-cache stand-in). For a +real remote cache at scale, point `--remote_cache` at **BuildBuddy** (free tier: `grpcs` +endpoint + API-key secret) or a self-hosted **bazel-remote** (GCS/S3): read-only on PRs, +read-write on trunk. + +## Downstream / tarball safety + +The published npm package is `react-native-macos` (`packages/react-native`); its `files` +allowlist governs the tarball. All Bazel scaffolding lives at the repo root, in +`tools/bazel/`, and in `packages/rn-tester` (which is **private**, never published), so +tarballs are unaffected. The generated `pnpm-lock.yaml` and `MODULE.bazel.lock` are +gitignored and repo-dev-only. The only change to a published-adjacent file is an empty +`pnpm.onlyBuiltDependencies: []` in the **private monorepo root** `package.json` (matching +the repo's `enableScripts: false`), which is inert for Yarn and not published. + +## Future roadmap: build the XCFrameworks from source in Bazel + +`Package.swift` already enumerates the full target graph (each `RNTarget` ↔ a podspec), +giving a ready-made port map. Output the same `.xcframework`s via +`apple_static_xcframework`, swapped in behind the P3 alias + `--//:rn_from_source`: + +* **FA — Hermes**: keep the prebuilt Hermes (`http_archive`) initially; optionally wrap + the CMake build with `rules_foreign_cc` later. +* **FB — ReactNativeDependencies** (boost/folly/glog/fmt/double-conversion/…): model each + as `cc_library` (native or `rules_foreign_cc`), compose an `apple_static_xcframework`. +* **FC — Codegen in Bazel**: wrap `react-native-codegen` as a rules_js `js_run_binary` + genrule so generated C++/ObjC specs are Bazel-tracked inputs. +* **FD — React core**: port the `Package.swift` graph leaf-first (Yoga → jsi → ReactCommon + → React-Core / RCTUIKit → Fabric / TurboModule → RCT modules) to + `swift_library`/`objc_library`/`cc_library`; assemble `React.xcframework`. +* **FE — Swap + validate + CI**: flip `--//:rn_from_source`, validate ABI/behavior parity + vs the SPM XCFrameworks, and land the heavy native compiles on the remote cache (the + primary CI/local speedup). + +## Layout + +``` +MODULE.bazel # bzlmod: rules_js/ts/apple/swift + Berry fork override +.bazelrc, .bazelversion, .bazelignore +BUILD.bazel # npm_link_all_packages(name="node_modules") +tools/bazel/berry/ + berry_to_pnpm_lock.mjs # Berry yarn.lock -> pnpm-lock v9 converter + example/ # self-contained GREEN proof of the fork +tools/bazel/patches/ + aspect_rules_js_berry.patch # the one-seam rules_js patch +tools/bazel/js/metro.bzl # metro_bundle macro (thin Metro wrapper) +tools/bazel/apple/xcframeworks.bzl # import prebuilt React XCFrameworks +packages/rn-tester/BUILD.bazel # the macOS vertical-slice app (manual, WIP) +``` diff --git a/package.json b/package.json index 52efcc047118..74760e1a8d04 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "version": "1000.0.0", "license": "MIT", "packageManager": "yarn@4.12.0", + "pnpm": { + "onlyBuiltDependencies": [] + }, "scripts": { "android": "yarn --cwd packages/rn-tester android", "build-android": "./gradlew :packages:react-native:ReactAndroid:build", diff --git a/packages/rn-tester/BUILD.bazel b/packages/rn-tester/BUILD.bazel new file mode 100644 index 000000000000..ad638dbddd9a --- /dev/null +++ b/packages/rn-tester/BUILD.bazel @@ -0,0 +1,69 @@ +# React Native macOS — Bazel vertical slice (draft / experimental) +# +# This BUILD file wires the end-to-end slice: bundle rn-tester's JS with Metro and +# embed it into a macOS app that links the prebuilt React Native XCFrameworks. +# +# The targets are tagged `manual` (kept out of `//...`) because they depend on: +# * first-party workspace packages having BUILD files (so rules_js can link the +# react-native CLI for Metro), and +# * the prebuilt XCFrameworks produced by scripts/ios-prebuild.js. +# See packages/rn-tester/bazel/README.md and docs/bazel.md. + +load("//tools/bazel/js:metro.bzl", "metro_bundle") +load("//tools/bazel/apple:xcframeworks.bzl", "rn_imported_xcframeworks") +load("@rules_apple//apple:macos.bzl", "macos_application") + +package(default_visibility = ["//visibility:private"]) + +# All rn-tester JavaScript that Metro may pull into the bundle. +filegroup( + name = "js_srcs", + srcs = glob( + ["js/**/*.js"], + allow_empty = False, + ) + [ + "metro.config.js", + "package.json", + ], +) + +# 1. JS bundle (Metro). Entry is the macOS variant of the rn-tester app. +metro_bundle( + name = "rntester_macos_jsbundle", + srcs = [":js_srcs"], + config = "metro.config.js", + entry_point = "js/RNTesterApp.macos.js", + platform = "macos", + tags = ["manual"], +) + +# 2. Prebuilt native code as importable XCFrameworks (from ios-prebuild). +rn_imported_xcframeworks() + +# 3. App host — reuse rn-tester's existing AppDelegate + main. +objc_library( + name = "rntester_macos_host", + srcs = [ + "RNTester/AppDelegate.mm", + "RNTester/main.m", + ], + hdrs = ["RNTester/AppDelegate.h"], + tags = ["manual"], + deps = [ + ":React", + ":ReactNativeDependencies", + ":hermes", + ], +) + +# 4. macOS app: link native + embed the Metro bundle as an app resource +# (mirrors how Buck2's apple prelude ingests a js_bundle as an AppleResource). +macos_application( + name = "RNTesterMacBazel", + bundle_id = "org.reactjs.native.RNTesterMacBazel", + infoplists = ["bazel/Info.plist"], + minimum_os_version = "11.0", + resources = [":rntester_macos_jsbundle"], + tags = ["manual"], + deps = [":rntester_macos_host"], +) diff --git a/packages/rn-tester/bazel/Info.plist b/packages/rn-tester/bazel/Info.plist new file mode 100644 index 000000000000..52027436a27a --- /dev/null +++ b/packages/rn-tester/bazel/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleName + RNTesterMacBazel + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + 11.0 + NSPrincipalClass + NSApplication + NSHighResolutionCapable + + + diff --git a/packages/rn-tester/bazel/README.md b/packages/rn-tester/bazel/README.md new file mode 100644 index 000000000000..cc07d33815d8 --- /dev/null +++ b/packages/rn-tester/bazel/README.md @@ -0,0 +1,23 @@ +This directory holds the Bazel *vertical slice* for building RNTester on macOS. +The actual targets live in `packages/rn-tester/BUILD.bazel` (they need to glob the +rn-tester JS and Obj-C sources, which Bazel can only do from the package root). + +See `docs/bazel.md` for the full design and current status. In short, the slice: + + 1. `metro_bundle` — bundles `js/RNTesterApp.macos.js` with Metro via rules_js. + 2. `rn_imported_xcframeworks()` — imports the prebuilt React/ReactNativeDependencies/ + hermes XCFrameworks produced by `scripts/ios-prebuild.js`. + 3. `macos_application` — links the XCFrameworks + rn-tester's AppDelegate/main and + embeds the Metro bundle as an app resource. + +Prerequisites (why the slice targets are tagged `manual` today): + * First-party workspace packages need `BUILD.bazel` files so rules_js can link + `//:node_modules/react-native` (the Metro CLI). Tracked in docs/bazel.md. + * The XCFrameworks must exist: run + (cd packages/react-native && node scripts/ios-prebuild.js -s -f Debug \ + && node scripts/ios-prebuild.js -b -f Debug -p macos \ + && node scripts/ios-prebuild.js -c -f Debug) + * rules_apple must support the local Xcode (26.x); pin via DEVELOPER_DIR. + +Once the above are in place: + bazel run //packages/rn-tester:RNTesterMacBazel diff --git a/tools/bazel/apple/BUILD.bazel b/tools/bazel/apple/BUILD.bazel new file mode 100644 index 000000000000..6a9fc3ea6448 --- /dev/null +++ b/tools/bazel/apple/BUILD.bazel @@ -0,0 +1,4 @@ +# Bazel helpers for building React Native's Apple targets (rules_apple / rules_swift). +# See xcframeworks.bzl and docs/bazel.md. + +exports_files(["xcframeworks.bzl"]) diff --git a/tools/bazel/apple/xcframeworks.bzl b/tools/bazel/apple/xcframeworks.bzl new file mode 100644 index 000000000000..b7771f74dee5 --- /dev/null +++ b/tools/bazel/apple/xcframeworks.bzl @@ -0,0 +1,47 @@ +"""Import the prebuilt React Native XCFrameworks into the Bazel graph. + +The draft-PR slice consumes the XCFrameworks produced by the existing Swift Package +Manager prebuild pipeline (`packages/react-native/scripts/ios-prebuild.js`): + + React.xcframework, ReactNativeDependencies.xcframework, hermes.xcframework + +We wrap them behind stable aliases (`:React`, `:ReactNativeDependencies`, `:hermes`) +so a future *from-source* Bazel build (see docs/bazel.md "from-source roadmap") can +produce the same `.xcframework` artifacts with rules_apple `apple_static_xcframework` +and drop in behind these aliases without touching the app BUILD files. + +Usage (from a BUILD file, after running ios-prebuild so the artifacts exist): + + load("//tools/bazel/apple:xcframeworks.bzl", "rn_imported_xcframeworks") + rn_imported_xcframeworks() +""" + +load( + "@rules_apple//apple:apple.bzl", + "apple_static_xcframework_import", +) + +# Repo-relative locations of the prebuilt XCFrameworks emitted by ios-prebuild. +_DEFAULT_XCFRAMEWORKS = { + "React": "//packages/react-native:xcframeworks/React.xcframework", + "ReactNativeDependencies": "//packages/react-native:xcframeworks/ReactNativeDependencies.xcframework", + "hermes": "//packages/react-native:xcframeworks/hermes.xcframework", +} + +def rn_imported_xcframeworks(xcframeworks = _DEFAULT_XCFRAMEWORKS, visibility = ["//visibility:public"]): + """Declare `apple_static_xcframework_import` targets for the prebuilt XCFrameworks. + + Each is exposed as `:` (e.g. `:React`). The underlying import target is + `:_import`; the alias makes the from-source swap seamless later. + """ + for name, filegroup in xcframeworks.items(): + apple_static_xcframework_import( + name = name + "_import", + xcframework_imports = [filegroup], + visibility = visibility, + ) + native.alias( + name = name, + actual = ":" + name + "_import", + visibility = visibility, + ) diff --git a/tools/bazel/berry/BUILD.bazel b/tools/bazel/berry/BUILD.bazel new file mode 100644 index 000000000000..ad6ee5c696e8 --- /dev/null +++ b/tools/bazel/berry/BUILD.bazel @@ -0,0 +1,4 @@ +# Berry (Yarn v2+) yarn.lock -> pnpm-lock.yaml converter used by our rules_js +# fork's patched npm_translate_lock. Kept dependency-free so it runs under a +# bare Node in the repository rule. +exports_files(["berry_to_pnpm_lock.mjs"]) diff --git a/tools/bazel/berry/berry_to_pnpm_lock.mjs b/tools/bazel/berry/berry_to_pnpm_lock.mjs new file mode 100644 index 000000000000..bbc18240881c --- /dev/null +++ b/tools/bazel/berry/berry_to_pnpm_lock.mjs @@ -0,0 +1,473 @@ +#!/usr/bin/env node +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Berry (Yarn v2+) `yarn.lock` -> pnpm `pnpm-lock.yaml` (lockfileVersion 9.0) converter. + * + * aspect-build/rules_js `npm_translate_lock` normalizes every input lockfile to an + * internal pnpm-lock model. For `yarn_lock`/`npm_package_lock` inputs it shells out to + * `pnpm import`, which does NOT understand the Berry `yarn.lock` format (pnpm #2991). + * + * This tool fills that one gap: it parses the Berry lock (its own strict YAML subset, + * "Syml") and emits a pnpm-lock.yaml v9 that rules_js's parser (npm/private/pnpm.bzl) + * accepts, so the Berry `yarn.lock` can stay the single source of truth (no committed + * second lockfile). It is invoked by our rules_js patch inside the repository rule, so + * it must run under a bare Node with no external dependencies. + * + * Usage: node berry_to_pnpm_lock.mjs + */ + +import fs from 'node:fs'; + +const REGISTRY = 'https://registry.npmjs.org'; + +// --------------------------------------------------------------------------- +// Syml (Berry yarn.lock) parser +// --------------------------------------------------------------------------- + +/** + * Parse a Berry `yarn.lock` (Syml) into a plain nested object. + * Syml is an indentation-based YAML subset: 2-space indents, `key: value` or + * `key:` followed by an indented block, `#` comments, and quoted/bare scalars. + */ +function parseSyml(text) { + const root = {}; + // Stack of {indent, container}. container is the object we add children to. + const stack = [{indent: -1, container: root}]; + const lines = text.split(/\r?\n/); + + for (let raw of lines) { + if (raw.trim() === '' || raw.trim().startsWith('#')) { + continue; + } + const indent = raw.length - raw.trimStart().length; + const line = raw.trimStart(); + + // Pop to the parent whose indent is smaller than this line's indent. + while (stack.length > 1 && indent <= stack[stack.length - 1].indent) { + stack.pop(); + } + const parent = stack[stack.length - 1].container; + + const {key, rest} = splitKey(line); + if (rest === null || rest === '') { + // `key:` opening a nested block. + const child = {}; + parent[key] = child; + stack.push({indent, container: child}); + } else { + // `key: value` scalar. + parent[key] = unquote(rest); + } + } + return root; +} + +/** Split a line into its (possibly quoted) key and the remainder after the colon. */ +function splitKey(line) { + if (line.startsWith('"')) { + // Quoted key: find the closing quote (Syml keys don't contain escaped quotes + // in practice for yarn.lock, but handle doubled backslash-escaped just in case). + let i = 1; + for (; i < line.length; i++) { + if (line[i] === '\\') { + i++; + continue; + } + if (line[i] === '"') { + break; + } + } + const key = unquote(line.slice(0, i + 1)); + let after = line.slice(i + 1); + // after should start with ':' + const colon = after.indexOf(':'); + const rest = after.slice(colon + 1).trim(); + return {key, rest: rest === '' ? null : rest}; + } + // Bare key up to first colon. + const colon = line.indexOf(':'); + const key = line.slice(0, colon).trim(); + const rest = line.slice(colon + 1).trim(); + return {key, rest: rest === '' ? null : rest}; +} + +function unquote(s) { + s = s.trim(); + if (s.startsWith('"') && s.endsWith('"') && s.length >= 2) { + return s + .slice(1, -1) + .replace(/\\"/g, '"') + .replace(/\\\\/g, '\\'); + } + return s; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** Split an `ident@range` descriptor into [ident, range], accounting for scopes. */ +function splitDescriptor(descriptor) { + const at = descriptor.indexOf('@', descriptor.startsWith('@') ? 1 : 0); + return [descriptor.slice(0, at), descriptor.slice(at + 1)]; +} + +/** Parse Berry `conditions: "os=darwin & cpu=x64"` into {os, cpu} arrays. */ +function parseConditions(conditions) { + const out = {}; + if (!conditions) { + return out; + } + for (const clause of conditions.split('&')) { + const m = clause.trim().match(/^(os|cpu|libc)=(.+)$/); + if (m) { + (out[m[1]] = out[m[1]] || []).push(m[2].trim()); + } + } + return out; +} + +/** Compute the deterministic npm registry tarball URL for a package. */ +function registryTarballUrl(ident, version, registry) { + const base = registry.replace(/\/+$/, ''); + const unscoped = ident.startsWith('@') ? ident.slice(ident.indexOf('/') + 1) : ident; + return `${base}/${ident}/-/${unscoped}-${version}.tgz`; +} + +/** Strip the `npm:` protocol prefix from a specifier range for display. */ +function cleanSpecifier(range) { + if (range.startsWith('npm:') && !range.slice(4).includes('@')) { + return range.slice(4); + } + return range; +} + +/** POSIX relative path from importer dir `from` to workspace dir `to`. */ +function relPath(from, to) { + const a = from === '.' ? [] : from.split('/'); + const b = to === '.' ? [] : to.split('/'); + let i = 0; + while (i < a.length && i < b.length && a[i] === b[i]) { + i++; + } + const up = a.slice(i).map(() => '..'); + const down = b.slice(i); + const parts = [...up, ...down]; + return parts.length ? parts.join('/') : '.'; +} + +// --------------------------------------------------------------------------- +// Conversion +// --------------------------------------------------------------------------- + +function convert(syml) { + // Map every descriptor string -> its resolved entry. + const descriptorToEntry = new Map(); + // Canonical package key `ident@version` -> entry (npm packages only). + const packageEntries = new Map(); + // Workspace entries: importer path -> entry. + const workspaceByPath = new Map(); + // ident@workspacePath (descriptor) resolution helper: descriptor -> workspace path. + + const entries = []; + for (const [rawKey, value] of Object.entries(syml)) { + if (rawKey === '__metadata') { + continue; + } + const descriptors = rawKey.split(',').map(s => s.trim()); + const resolution = value.resolution || ''; + const entry = {descriptors, resolution, value}; + entries.push(entry); + for (const d of descriptors) { + descriptorToEntry.set(d, entry); + } + } + + // Classify entries. + for (const entry of entries) { + const {resolution, value} = entry; + const [ident, reference] = splitDescriptor(resolution); + entry.ident = ident; + if (reference.startsWith('workspace:')) { + const wsPath = reference.slice('workspace:'.length); + entry.kind = 'workspace'; + entry.path = wsPath === '.' ? '.' : wsPath; + workspaceByPath.set(entry.path, entry); + } else if (reference.startsWith('npm:')) { + entry.kind = 'npm'; + entry.version = value.version; + entry.pkgKey = `${ident}@${value.version}`; + if (!packageEntries.has(entry.pkgKey)) { + packageEntries.set(entry.pkgKey, entry); + } + } else { + // patch:, portal:, link:, exec:, git, https tarball, file: ... + // Best-effort: treat anything with a concrete version as an npm-like package + // keyed by version so cross-references still resolve. + entry.kind = 'other'; + entry.version = value.version; + if (value.version) { + entry.pkgKey = `${ident}@${value.version}`; + if (!packageEntries.has(entry.pkgKey)) { + packageEntries.set(entry.pkgKey, entry); + } + } + } + } + + // Resolve a `depName: rangeDescriptor` reference to either a snapshot key + // (`name@version`) or a `link:` for workspace deps. + function resolveDep(fromPath, depName, rangeDescriptor) { + const descriptor = `${depName}@${rangeDescriptor}`; + const target = descriptorToEntry.get(descriptor); + if (!target) { + return null; + } + if (target.kind === 'workspace') { + return {link: `link:${relPath(fromPath, target.path)}`}; + } + if (target.version) { + return { + version: `${target.ident}@${target.version}`, + plain: target.version, + ident: target.ident, + }; + } + return null; + } + + // Build `packages` and `snapshots`. + const packages = {}; + const snapshots = {}; + for (const entry of packageEntries.values()) { + const {value, pkgKey, ident} = entry; + const pkg = {}; + // NOTE: Berry's `checksum` is Yarn's own content hash of the package, NOT the + // npm tarball's sha512 integrity that rules_js/pnpm expect, so we cannot derive a + // valid `resolution.integrity` from the Berry lock. rules_js requires either an + // `integrity` or a `tarball`; we emit the deterministic npm registry tarball URL + // (downloaded without integrity verification). A production-grade version should + // source real integrities (one-time npm registry fetch or a Yarn plugin export) — + // tracked in docs/bazel.md. + pkg.resolution = {tarball: registryTarballUrl(ident, entry.version, REGISTRY)}; + const conds = parseConditions(value.conditions); + if (conds.os) { + pkg.os = conds.os; + } + if (conds.cpu) { + pkg.cpu = conds.cpu; + } + if (value.bin && typeof value.bin === 'object') { + pkg.hasBin = true; + } + if (value.peerDependencies && typeof value.peerDependencies === 'object') { + pkg.peerDependencies = {}; + for (const [pn, pv] of Object.entries(value.peerDependencies)) { + pkg.peerDependencies[pn] = typeof pv === 'string' ? pv : '*'; + } + } + packages[pkgKey] = pkg; + + const snap = {}; + const deps = value.dependencies; + if (deps && typeof deps === 'object') { + const meta = value.dependenciesMeta || {}; + const depMap = {}; + const optMap = {}; + for (const [dn, dr] of Object.entries(deps)) { + const resolved = resolveDep('.', dn, dr); + if (!resolved) { + continue; + } + const target = resolved.link || resolved.version; + const isOptional = meta[dn] && meta[dn].optional === 'true'; + if (isOptional) { + optMap[dn] = target; + } else { + depMap[dn] = target; + } + } + if (Object.keys(depMap).length) { + snap.dependencies = depMap; + } + if (Object.keys(optMap).length) { + snap.optionalDependencies = optMap; + } + } + snapshots[pkgKey] = snap; + } + + // Build `importers` from workspace entries. + const importers = {}; + for (const entry of workspaceByPath.values()) { + const importPath = entry.path; + const deps = {}; + const src = entry.value.dependencies; + if (src && typeof src === 'object') { + for (const [dn, dr] of Object.entries(src)) { + const resolved = resolveDep(importPath, dn, dr); + if (!resolved) { + continue; + } + deps[dn] = { + specifier: cleanSpecifier(dr), + version: + resolved.link || + (resolved.ident === dn ? resolved.plain : resolved.version), + }; + } + } + importers[importPath] = {dependencies: deps}; + } + + return {importers, packages, snapshots}; +} + +// --------------------------------------------------------------------------- +// Minimal YAML emitter (maps, string scalars, booleans, flow string arrays) +// --------------------------------------------------------------------------- + +function needsQuote(s) { + return !/^[A-Za-z0-9_./-]+$/.test(s) || s === '' || /^(true|false|null|~|yes|no)$/i.test(s); +} + +function q(s) { + s = String(s); + if (!needsQuote(s)) { + return s; + } + return `'${s.replace(/'/g, "''")}'`; +} + +function emit(obj, indent, lines) { + const pad = ' '.repeat(indent); + const keys = Object.keys(obj); + for (const k of keys) { + const v = obj[k]; + if (Array.isArray(v)) { + lines.push(`${pad}${q(k)}: [${v.map(q).join(', ')}]`); + } else if (v && typeof v === 'object') { + if (Object.keys(v).length === 0) { + lines.push(`${pad}${q(k)}: {}`); + } else { + lines.push(`${pad}${q(k)}:`); + emit(v, indent + 1, lines); + } + } else if (typeof v === 'boolean') { + lines.push(`${pad}${q(k)}: ${v ? 'true' : 'false'}`); + } else { + lines.push(`${pad}${q(k)}: ${q(v)}`); + } + } +} + +function toYaml({importers, packages, snapshots}) { + const lines = []; + lines.push("lockfileVersion: '9.0'"); + lines.push(''); + lines.push('settings:'); + lines.push(' autoInstallPeers: true'); + lines.push(' excludeLinksFromLockfile: false'); + lines.push(''); + lines.push('importers:'); + emit(importers, 1, lines); + lines.push(''); + lines.push('packages:'); + emit(packages, 1, lines); + lines.push(''); + lines.push('snapshots:'); + emit(snapshots, 1, lines); + lines.push(''); + return lines.join('\n'); +} + +// --------------------------------------------------------------------------- +// Validation (mirrors rules_js npm/private/pnpm.bzl consistency checks) +// --------------------------------------------------------------------------- + +function validate({importers, packages, snapshots}) { + const snapKeys = new Set(Object.keys(snapshots)); + const pkgKeys = new Set(Object.keys(packages)); + const dangling = []; + + const resolvable = (name, version) => { + if (typeof version !== 'string') { + return false; + } + if (version.startsWith('link:')) { + return true; + } + if (snapKeys.has(version)) { + return true; + } + if (snapKeys.has(`${name}@${version}`)) { + return true; + } + return false; + }; + + for (const [path, importer] of Object.entries(importers)) { + for (const kind of ['dependencies', 'devDependencies', 'optionalDependencies']) { + for (const [name, attr] of Object.entries(importer[kind] || {})) { + if (!resolvable(name, attr.version)) { + dangling.push(`importer ${path} -> ${name}@${attr.version}`); + } + } + } + } + for (const [snapKey, snap] of Object.entries(snapshots)) { + const staticKey = snapKey.includes('(') ? snapKey.slice(0, snapKey.indexOf('(')) : snapKey; + if (!pkgKeys.has(staticKey)) { + dangling.push(`snapshot ${snapKey} has no package ${staticKey}`); + } + for (const kind of ['dependencies', 'optionalDependencies']) { + for (const [name, version] of Object.entries(snap[kind] || {})) { + if (!resolvable(name, version)) { + dangling.push(`snapshot ${snapKey} -> ${name}@${version}`); + } + } + } + } + + if (dangling.length) { + console.error(`berry_to_pnpm_lock: ${dangling.length} dangling reference(s):`); + for (const d of dangling.slice(0, 25)) { + console.error(` - ${d}`); + } + if (dangling.length > 25) { + console.error(` ... and ${dangling.length - 25} more`); + } + } else { + console.error('berry_to_pnpm_lock: model is internally consistent'); + } +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +function main() { + const [, , inPath, outPath] = process.argv; + if (!inPath || !outPath) { + console.error('Usage: berry_to_pnpm_lock.mjs '); + process.exit(2); + } + const text = fs.readFileSync(inPath, 'utf8'); + const syml = parseSyml(text); + const model = convert(syml); + validate(model); + fs.writeFileSync(outPath, toYaml(model)); + const nImporters = Object.keys(model.importers).length; + const nPackages = Object.keys(model.packages).length; + console.error( + `berry_to_pnpm_lock: wrote ${outPath} (${nImporters} importers, ${nPackages} packages)`, + ); +} + +main(); diff --git a/tools/bazel/berry/example/.yarnrc.yml b/tools/bazel/berry/example/.yarnrc.yml new file mode 100644 index 000000000000..94f5c254e79d --- /dev/null +++ b/tools/bazel/berry/example/.yarnrc.yml @@ -0,0 +1,2 @@ +nodeLinker: node-modules +enableScripts: false diff --git a/tools/bazel/berry/example/BUILD.bazel b/tools/bazel/berry/example/BUILD.bazel new file mode 100644 index 000000000000..3c050133653f --- /dev/null +++ b/tools/bazel/berry/example/BUILD.bazel @@ -0,0 +1,18 @@ +load("@aspect_rules_js//js:defs.bzl", "js_test") +load("@npm_berry_example//:defs.bzl", "npm_link_all_packages") + +exports_files(["yarn.lock"]) + +# Links node_modules resolved (by our rules_js Berry fork) from this example's +# Yarn Berry yarn.lock. No committed pnpm-lock.yaml. +npm_link_all_packages(name = "node_modules") + +# `bazel test //tools/bazel/berry/example:verify` proves the whole chain: +# Berry yarn.lock -> berry_to_pnpm_lock.mjs -> patched npm_translate_lock -> +# rules_js fetch + link -> a Node program requiring an npm dep runs green. +js_test( + name = "verify", + size = "small", + data = [":node_modules/is-odd"], + entry_point = "verify.js", +) diff --git a/tools/bazel/berry/example/package.json b/tools/bazel/berry/example/package.json new file mode 100644 index 000000000000..f6ac31f6a6cb --- /dev/null +++ b/tools/bazel/berry/example/package.json @@ -0,0 +1,11 @@ +{ + "name": "berry-fork-example", + "version": "0.0.0", + "private": true, + "pnpm": { + "onlyBuiltDependencies": [] + }, + "dependencies": { + "is-odd": "3.0.1" + } +} diff --git a/tools/bazel/berry/example/verify.js b/tools/bazel/berry/example/verify.js new file mode 100644 index 000000000000..0526a6a69c88 --- /dev/null +++ b/tools/bazel/berry/example/verify.js @@ -0,0 +1,19 @@ +// Green proof that the rules_js Berry fork works end to end: +// this file is bundled/run by Bazel with node_modules materialized from a +// Yarn *Berry* yarn.lock (translated by tools/bazel/berry/berry_to_pnpm_lock.mjs +// via our patched npm_translate_lock — no committed pnpm-lock.yaml). +const isOdd = require('is-odd'); + +const cases = [ + [1, true], + [2, false], + [3, true], + [10, false], +]; +for (const [n, expected] of cases) { + if (isOdd(n) !== expected) { + console.error(`FAIL: isOdd(${n}) !== ${expected}`); + process.exit(1); + } +} +console.log('OK: is-odd (resolved from a Berry yarn.lock via the rules_js fork) works'); diff --git a/tools/bazel/berry/example/yarn.lock b/tools/bazel/berry/example/yarn.lock new file mode 100644 index 000000000000..05ad7e142a37 --- /dev/null +++ b/tools/bazel/berry/example/yarn.lock @@ -0,0 +1,30 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"berry-fork-example@workspace:.": + version: 0.0.0-use.local + resolution: "berry-fork-example@workspace:." + dependencies: + is-odd: "npm:3.0.1" + languageName: unknown + linkType: soft + +"is-number@npm:^6.0.0": + version: 6.0.0 + resolution: "is-number@npm:6.0.0" + checksum: 10c0/5da4c68401529675c575878d2760d66f18eaef4b014858577f6003daf66488d7fe4eae684b1e8574e3fa1bb447c6c6c56b8491d2b4b3239da2d32e5f6f218008 + languageName: node + linkType: hard + +"is-odd@npm:3.0.1": + version: 3.0.1 + resolution: "is-odd@npm:3.0.1" + dependencies: + is-number: "npm:^6.0.0" + checksum: 10c0/89ee2e353c5a3f3bd400c79db1c307a5b3506198ee8169d521e533a9b1d8a08fc95f21a919c084e98845b4286d7ffe309778da03744dfe66c3c1763ab1a030c6 + languageName: node + linkType: hard diff --git a/tools/bazel/js/BUILD.bazel b/tools/bazel/js/BUILD.bazel new file mode 100644 index 000000000000..a6f700ee3457 --- /dev/null +++ b/tools/bazel/js/BUILD.bazel @@ -0,0 +1,4 @@ +# Bazel macros for building React Native JavaScript (Metro). See metro.bzl. +# Package marker so `//tools/bazel/js:metro.bzl` is loadable. + +exports_files(["metro.bzl"]) diff --git a/tools/bazel/js/metro.bzl b/tools/bazel/js/metro.bzl new file mode 100644 index 000000000000..5edbcc9bffdb --- /dev/null +++ b/tools/bazel/js/metro.bzl @@ -0,0 +1,74 @@ +"""Bazel wrapper for bundling React Native JavaScript with Metro. + +We deliberately drive the existing Metro/React Native CLI from Bazel via +`js_run_binary` (rules_js) rather than reimplementing Metro's transform/worker +pipeline the way Buck2's `js_bundle` prelude does. This keeps the rule tiny while +reusing the exact bundler the rest of the repo already uses. See docs/bazel.md. +""" + +load("@aspect_rules_js//js:defs.bzl", "js_run_binary") + +def metro_bundle( + name, + entry_point, + platform, + srcs, + deps = [], + config = None, + dev = False, + minify = None, + bundle_out = None, + assets_out = None, + **kwargs): + """Produce a `.jsbundle` (+ assets dir) from a React Native entry point. + + Args: + name: target name; also the default bundle basename. + entry_point: JS entry file (e.g. `js/RNTesterApp.macos.js`). + platform: React Native platform (`macos`, `ios`, `android`, ...). + srcs: JS/asset source files that make up the app. + deps: additional targets (e.g. linked first-party packages / node_modules). + config: optional `metro.config.js` target. + dev: whether to build a dev (unminified, with warnings) bundle. + minify: override minification (defaults to `not dev`). + bundle_out: output bundle filename (default `.jsbundle`). + assets_out: output assets directory (default `_assets`). + **kwargs: passed through to js_run_binary (e.g. tags, visibility). + """ + bundle_out = bundle_out or (name + ".jsbundle") + assets_out = assets_out or (name + "_assets") + minify = minify if minify != None else (not dev) + + args = [ + "bundle", + "--platform", + platform, + "--dev", + "true" if dev else "false", + "--minify", + "true" if minify else "false", + "--entry-file", + entry_point, + "--bundle-output", + bundle_out, + "--assets-dest", + assets_out, + "--reset-cache", + ] + tool_srcs = list(srcs) + list(deps) + if config: + args += ["--config", "$(rootpath %s)" % config] + tool_srcs.append(config) + + js_run_binary( + name = name, + # The React Native community CLI exposes the `bundle` command. This is a + # first-party workspace package; linking it requires the per-package + # BUILD.bazel migration described in docs/bazel.md. + tool = "//:node_modules/react-native/bin", + srcs = tool_srcs, + args = args, + outs = [bundle_out], + out_dirs = [assets_out], + **kwargs + ) diff --git a/tools/bazel/patches/BUILD.bazel b/tools/bazel/patches/BUILD.bazel new file mode 100644 index 000000000000..73f26db52828 --- /dev/null +++ b/tools/bazel/patches/BUILD.bazel @@ -0,0 +1,2 @@ +# Patches applied to external Bazel modules via single_version_override. +exports_files(glob(["*.patch"])) diff --git a/tools/bazel/patches/aspect_rules_js_berry.patch b/tools/bazel/patches/aspect_rules_js_berry.patch new file mode 100644 index 000000000000..cb24d62c0c04 --- /dev/null +++ b/tools/bazel/patches/aspect_rules_js_berry.patch @@ -0,0 +1,79 @@ +--- a/npm/private/npm_translate_lock.bzl ++++ b/npm/private/npm_translate_lock.bzl +@@ -336,7 +336,67 @@ + """), + }, + ) ++ ++def _find_berry_converter(yarn_path): ++ # The converter lives at /tools/bazel/berry/berry_to_pnpm_lock.mjs. ++ # Walk up from the yarn.lock directory to locate it, so this works for lockfiles ++ # anywhere in the repo (root or nested example projects). ++ d = yarn_path.dirname ++ for _ in range(32): ++ candidate = d.get_child("tools").get_child("bazel").get_child("berry").get_child("berry_to_pnpm_lock.mjs") ++ if candidate.exists: ++ return candidate ++ parent = d.dirname ++ if parent == None or str(parent) == str(d): ++ break ++ d = parent ++ return None ++ ++def _generate_pnpm_lock_from_berry(rctx, attr): ++ """rules_js Berry fork: generate pnpm-lock.yaml from a Yarn v2+ (Berry) yarn.lock. ++ ++ aspect_rules_js normally shells out to `pnpm import` to translate a yarn.lock, but ++ `pnpm import` does not understand the Berry (v2+) lockfile format. When the input ++ yarn.lock is a Berry lockfile and the pnpm-lock.yaml does not yet exist, translate it ++ with our bundled dependency-free converter so the Berry yarn.lock can remain the single ++ source of truth (the generated pnpm-lock.yaml is gitignored, never committed). See ++ tools/bazel/berry/ and docs/bazel.md. ++ """ ++ if not attr.yarn_lock or not attr.pnpm_lock: ++ return ++ yarn_path = rctx.path(attr.yarn_lock) ++ if not yarn_path.exists: ++ return ++ if "__metadata:" not in rctx.read(yarn_path): ++ return # not a Berry lockfile; leave the normal pnpm import path in charge ++ converter = _find_berry_converter(yarn_path) ++ if converter == None: ++ fail("berry_to_pnpm_lock.mjs not found above yarn.lock at {}".format(yarn_path)) + ++ # Re-run this extension when the Berry lock or the converter change. ++ rctx.watch(yarn_path) ++ rctx.watch(converter) ++ ++ pnpm_lock_path = rctx.path(attr.pnpm_lock) ++ if pnpm_lock_path.exists: ++ return # already generated; delete pnpm-lock.yaml to refresh after yarn.lock changes ++ rctx.report_progress("Translating Berry yarn.lock -> pnpm-lock.yaml") ++ result = rctx.execute( ++ [ ++ _host_node_path(rctx, attr), ++ converter, ++ yarn_path, ++ pnpm_lock_path, ++ ], ++ quiet = attr.quiet, ++ ) ++ if result.return_code: ++ fail("berry_to_pnpm_lock conversion failed (status {}):\n{}\n{}".format( ++ result.return_code, ++ result.stdout, ++ result.stderr, ++ )) ++ + def parse_and_verify_lock(rctx, mod, attr): + """Helper to parse and validate the lockfile + +@@ -348,6 +408,8 @@ + state, importers, and packages + """ + ++ _generate_pnpm_lock_from_berry(rctx, attr) ++ + state = npm_translate_lock_state.new(rctx, mod, attr) + + if state.should_update_pnpm_lock(): From bebb91475acd9858df40d0fee8168964435ea68b Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 6 Jul 2026 15:21:59 -0700 Subject: [PATCH 02/17] Bazel: first-party package linking + rn-tester macOS bundle/app scaffolding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- docs/bazel.md | 47 +++++++++++---- packages/assets/BUILD.bazel | 24 ++++++++ packages/babel-plugin-codegen/BUILD.bazel | 24 ++++++++ packages/community-cli-plugin/BUILD.bazel | 24 ++++++++ packages/core-cli-utils/BUILD.bazel | 24 ++++++++ packages/debugger-frontend/BUILD.bazel | 24 ++++++++ packages/debugger-shell/BUILD.bazel | 24 ++++++++ packages/dev-middleware/BUILD.bazel | 24 ++++++++ .../eslint-config-react-native/BUILD.bazel | 24 ++++++++ .../eslint-plugin-react-native/BUILD.bazel | 24 ++++++++ packages/eslint-plugin-specs/BUILD.bazel | 24 ++++++++ packages/gradle-plugin/BUILD.bazel | 24 ++++++++ packages/metro-config/BUILD.bazel | 24 ++++++++ packages/new-app-screen/BUILD.bazel | 24 ++++++++ packages/normalize-color/BUILD.bazel | 24 ++++++++ packages/polyfills/BUILD.bazel | 24 ++++++++ .../react-native-babel-preset/BUILD.bazel | 24 ++++++++ .../BUILD.bazel | 24 ++++++++ packages/react-native-codegen/BUILD.bazel | 24 ++++++++ .../BUILD.bazel | 24 ++++++++ packages/react-native-macos-init/BUILD.bazel | 24 ++++++++ .../BUILD.bazel | 24 ++++++++ .../react-native-test-library/BUILD.bazel | 24 ++++++++ packages/react-native/BUILD.bazel | 31 ++++++++++ packages/rn-tester/BUILD.bazel | 46 ++++++++++---- packages/rn-tester/bazel/bundle.js | 60 +++++++++++++++++++ packages/typescript-config/BUILD.bazel | 24 ++++++++ packages/virtualized-lists/BUILD.bazel | 24 ++++++++ private/cxx-public-api/BUILD.bazel | 24 ++++++++ private/eslint-plugin-monorepo/BUILD.bazel | 24 ++++++++ private/monorepo-tests/BUILD.bazel | 24 ++++++++ private/react-native-bots/BUILD.bazel | 24 ++++++++ .../BUILD.bazel | 24 ++++++++ private/react-native-fantom/BUILD.bazel | 24 ++++++++ 34 files changed, 881 insertions(+), 23 deletions(-) create mode 100644 packages/assets/BUILD.bazel create mode 100644 packages/babel-plugin-codegen/BUILD.bazel create mode 100644 packages/community-cli-plugin/BUILD.bazel create mode 100644 packages/core-cli-utils/BUILD.bazel create mode 100644 packages/debugger-frontend/BUILD.bazel create mode 100644 packages/debugger-shell/BUILD.bazel create mode 100644 packages/dev-middleware/BUILD.bazel create mode 100644 packages/eslint-config-react-native/BUILD.bazel create mode 100644 packages/eslint-plugin-react-native/BUILD.bazel create mode 100644 packages/eslint-plugin-specs/BUILD.bazel create mode 100644 packages/gradle-plugin/BUILD.bazel create mode 100644 packages/metro-config/BUILD.bazel create mode 100644 packages/new-app-screen/BUILD.bazel create mode 100644 packages/normalize-color/BUILD.bazel create mode 100644 packages/polyfills/BUILD.bazel create mode 100644 packages/react-native-babel-preset/BUILD.bazel create mode 100644 packages/react-native-babel-transformer/BUILD.bazel create mode 100644 packages/react-native-codegen/BUILD.bazel create mode 100644 packages/react-native-compatibility-check/BUILD.bazel create mode 100644 packages/react-native-macos-init/BUILD.bazel create mode 100644 packages/react-native-popup-menu-android/BUILD.bazel create mode 100644 packages/react-native-test-library/BUILD.bazel create mode 100644 packages/react-native/BUILD.bazel create mode 100644 packages/rn-tester/bazel/bundle.js create mode 100644 packages/typescript-config/BUILD.bazel create mode 100644 packages/virtualized-lists/BUILD.bazel create mode 100644 private/cxx-public-api/BUILD.bazel create mode 100644 private/eslint-plugin-monorepo/BUILD.bazel create mode 100644 private/monorepo-tests/BUILD.bazel create mode 100644 private/react-native-bots/BUILD.bazel create mode 100644 private/react-native-codegen-typescript-test/BUILD.bazel create mode 100644 private/react-native-fantom/BUILD.bazel diff --git a/docs/bazel.md b/docs/bazel.md index a4e0c5cdc438..35566603e22a 100644 --- a/docs/bazel.md +++ b/docs/bazel.md @@ -89,18 +89,45 @@ BYONM remains a viable fallback. rules_js, and a Node program that `require`s the npm dep runs green. * On the **real monorepo** `yarn.lock`, `npm_translate_lock` parses the fork-generated lock and **fetches all 1333 packages** successfully. - -## What's next (WIP, scaffolded) - -* **First-party workspace linking**: `//:node_modules` (link *all* packages) needs a - `BUILD.bazel` in each of the ~33 workspace packages (the standard rules_js monorepo - adoption step). Until then the Metro bundle target is tagged `manual`. -* **Metro bundle**: `//packages/rn-tester:rntester_macos_jsbundle` via the `metro_bundle` - macro (`tools/bazel/js/metro.bzl`) — a thin `js_run_binary` wrapper around the React - Native CLI `bundle` command (we drive Metro, we don't reimplement it). +* **First-party workspace linking**: every workspace package now exposes a `:pkg` target + (`npm_package`) so rules_js can link it. `bazel build //packages/react-native:pkg` + builds; rn-tester's `node_modules` links `react-native-macos` (the package the repo + consumes as `react-native`). +* **rn-tester's macOS JS bundle builds** (outside Bazel, proving the JS is bundleable): + after building the codegen lib, `react-native bundle --platform macos --entry-file + js/RNTesterApp.macos.js` produces a ~7 MB `RNTesterApp.macos.jsbundle` + 50 assets. + +## End-to-end RN-Tester: status & remaining work + +Two concrete blockers separate the current state from a running RNTester macOS app: + +1. **The macOS app is blocked by an external artifact gap.** `scripts/ios-prebuild.js` + cannot produce the XCFrameworks because the **Hermes nightly tarball 404s** + (`react-native-artifacts/0.87.0-nightly-…-hermes-ios-debug.tar.gz`). Without + `hermes.xcframework` / `React.xcframework` / `ReactNativeDependencies.xcframework`, + `macos_application` can't link. The app target is fully scaffolded and will build once + those XCFrameworks are available (e.g. a valid nightly, or a from-source Bazel build — + see the roadmap). + +2. **The hermetic Bazel Metro bundle needs a dependency-closure step.** Two prerequisites: + * `@react-native/codegen`'s `lib/` must be built (babel-plugin-codegen requires it): + `(cd packages/react-native-codegen && yarn build)`. + * rules_js uses a **strict, non-hoisted** `node_modules`. Metro's tooling (`metro`, + `@react-native/metro-config`, `@react-native/metro-babel-transformer`) are + transitive/root devDeps, so they are not resolvable from rn-tester's `node_modules`. + To make `//packages/rn-tester:rntester_macos_jsbundle` green, declare that tooling as + deps of the bundler (e.g. add them to rn-tester's `package.json` and regenerate the + Berry `yarn.lock`) so rules_js links the full closure. The bundle itself runs via + `packages/rn-tester/bazel/bundle.js` (Metro's API, with the + `react-native → react-native-macos` alias and a sandbox-safe config). + +## What's next (WIP, scaffolded — all `manual`) + +* **Metro bundle**: `//packages/rn-tester:rntester_macos_jsbundle` (`js_run_binary` around + `bazel/bundle.js`). Blocked only on the closure step above. * **macOS app**: `//packages/rn-tester:RNTesterMacBazel` (`macos_application`) links the imported XCFrameworks and embeds the Metro bundle as an app resource, reusing - rn-tester's existing `AppDelegate.mm`/`main.m`. + rn-tester's existing `AppDelegate.mm`/`main.m`. Blocked on the XCFrameworks (Hermes 404). ## Apple: prebuilt XCFrameworks (swappable seam) diff --git a/packages/assets/BUILD.bazel b/packages/assets/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/assets/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/babel-plugin-codegen/BUILD.bazel b/packages/babel-plugin-codegen/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/babel-plugin-codegen/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/community-cli-plugin/BUILD.bazel b/packages/community-cli-plugin/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/community-cli-plugin/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/core-cli-utils/BUILD.bazel b/packages/core-cli-utils/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/core-cli-utils/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/debugger-frontend/BUILD.bazel b/packages/debugger-frontend/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/debugger-frontend/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/debugger-shell/BUILD.bazel b/packages/debugger-shell/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/debugger-shell/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/dev-middleware/BUILD.bazel b/packages/dev-middleware/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/dev-middleware/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/eslint-config-react-native/BUILD.bazel b/packages/eslint-config-react-native/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/eslint-config-react-native/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/eslint-plugin-react-native/BUILD.bazel b/packages/eslint-plugin-react-native/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/eslint-plugin-react-native/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/eslint-plugin-specs/BUILD.bazel b/packages/eslint-plugin-specs/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/eslint-plugin-specs/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/gradle-plugin/BUILD.bazel b/packages/gradle-plugin/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/gradle-plugin/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/metro-config/BUILD.bazel b/packages/metro-config/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/metro-config/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/new-app-screen/BUILD.bazel b/packages/new-app-screen/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/new-app-screen/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/normalize-color/BUILD.bazel b/packages/normalize-color/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/normalize-color/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/polyfills/BUILD.bazel b/packages/polyfills/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/polyfills/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/react-native-babel-preset/BUILD.bazel b/packages/react-native-babel-preset/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/react-native-babel-preset/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/react-native-babel-transformer/BUILD.bazel b/packages/react-native-babel-transformer/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/react-native-babel-transformer/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/react-native-codegen/BUILD.bazel b/packages/react-native-codegen/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/react-native-codegen/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/react-native-compatibility-check/BUILD.bazel b/packages/react-native-compatibility-check/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/react-native-compatibility-check/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/react-native-macos-init/BUILD.bazel b/packages/react-native-macos-init/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/react-native-macos-init/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/react-native-popup-menu-android/BUILD.bazel b/packages/react-native-popup-menu-android/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/react-native-popup-menu-android/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/react-native-test-library/BUILD.bazel b/packages/react-native-test-library/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/react-native-test-library/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/react-native/BUILD.bazel b/packages/react-native/BUILD.bazel new file mode 100644 index 000000000000..d2b420469b0a --- /dev/null +++ b/packages/react-native/BUILD.bazel @@ -0,0 +1,31 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + "React/**", + "ReactApple/**", + "ReactCommon/**", + "ReactAndroid/**", + "sdks/**", + "third-party/**", + "ReactCxxPlatform/**", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/rn-tester/BUILD.bazel b/packages/rn-tester/BUILD.bazel index ad638dbddd9a..e594d85b24a0 100644 --- a/packages/rn-tester/BUILD.bazel +++ b/packages/rn-tester/BUILD.bazel @@ -1,20 +1,26 @@ # React Native macOS — Bazel vertical slice (draft / experimental) # -# This BUILD file wires the end-to-end slice: bundle rn-tester's JS with Metro and -# embed it into a macOS app that links the prebuilt React Native XCFrameworks. -# -# The targets are tagged `manual` (kept out of `//...`) because they depend on: -# * first-party workspace packages having BUILD files (so rules_js can link the -# react-native CLI for Metro), and -# * the prebuilt XCFrameworks produced by scripts/ios-prebuild.js. -# See packages/rn-tester/bazel/README.md and docs/bazel.md. +# Status (see docs/bazel.md): +# * The rules_js Berry fork is GREEN (deps resolve/link from the Berry yarn.lock). +# * First-party workspace packages now expose `:pkg` targets so rules_js can link +# them; `bazel build //packages/react-native:pkg` builds. +# * The Metro bundle + macOS app targets below are tagged `manual` (WIP): +# - the Metro bundle needs the RN `react-native`->`react-native-macos` alias and a +# sandbox-safe config (bazel/bundle.js), @react-native/codegen `lib` built, and the +# full strict-deps closure assembled (rules_js does not hoist like Yarn); +# - the macOS app needs the prebuilt XCFrameworks, which currently cannot be produced +# here because the Hermes nightly tarball 404s (see docs/bazel.md). -load("//tools/bazel/js:metro.bzl", "metro_bundle") +load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_run_binary") +load("@npm//:defs.bzl", "npm_link_all_packages") load("//tools/bazel/apple:xcframeworks.bzl", "rn_imported_xcframeworks") load("@rules_apple//apple:macos.bzl", "macos_application") package(default_visibility = ["//visibility:private"]) +# rn-tester's node_modules, linked by rules_js from the Berry yarn.lock. +npm_link_all_packages(name = "node_modules") + # All rn-tester JavaScript that Metro may pull into the bundle. filegroup( name = "js_srcs", @@ -23,18 +29,32 @@ filegroup( allow_empty = False, ) + [ "metro.config.js", + "react-native.config.js", "package.json", ], ) +# NOTE: rules_js uses a strict, non-hoisted node_modules. Metro's bundling tooling +# (metro, @react-native/metro-config, @react-native/metro-babel-transformer) are +# transitive/root devDeps and are therefore NOT resolvable from rn-tester's node_modules. +# To make this green, declare them as deps here (e.g. add them to rn-tester's package.json +# and regenerate the Berry yarn.lock) so rules_js links the full closure. See docs/bazel.md. +# The bundle runs via bazel/bundle.js (Metro's API) with the react-native alias applied. +js_binary( + name = "bundler", + data = [":node_modules"], + entry_point = "bazel/bundle.js", + tags = ["manual"], +) + # 1. JS bundle (Metro). Entry is the macOS variant of the rn-tester app. -metro_bundle( +js_run_binary( name = "rntester_macos_jsbundle", srcs = [":js_srcs"], - config = "metro.config.js", - entry_point = "js/RNTesterApp.macos.js", - platform = "macos", + outs = ["RNTesterApp.macos.jsbundle"], + env = {"BUNDLE_OUT": "RNTesterApp.macos.jsbundle"}, tags = ["manual"], + tool = ":bundler", ) # 2. Prebuilt native code as importable XCFrameworks (from ios-prebuild). diff --git a/packages/rn-tester/bazel/bundle.js b/packages/rn-tester/bazel/bundle.js new file mode 100644 index 000000000000..15ae0511b457 --- /dev/null +++ b/packages/rn-tester/bazel/bundle.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @noflow + */ + +// Bazel entry point for bundling rn-tester's macOS JS with Metro. +// +// Runs under a rules_js `js_run_binary` with node_modules linked by Bazel. Unlike +// the source-tree metro.config.js (which uses source-relative watchFolders), this +// resolves everything from the Bazel-linked node_modules and aliases the +// `react-native` import to the `react-native-macos` package (the repo's convention). + +'use strict'; + +const path = require('path'); +const Metro = require('metro'); +const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); + +async function main() { + const projectRoot = process.env.RN_TESTER_ROOT + ? path.resolve(process.env.RN_TESTER_ROOT) + : process.cwd(); + const out = process.env.BUNDLE_OUT || 'RNTesterApp.macos.jsbundle'; + const assetsDest = process.env.ASSETS_DEST || undefined; + + // react-native-macos is published/consumed as `react-native`. + const reactNativePath = path.dirname( + require.resolve('react-native-macos/package.json'), + ); + + const baseConfig = await getDefaultConfig(projectRoot); + const config = mergeConfig(baseConfig, { + projectRoot, + watchFolders: [reactNativePath, path.dirname(reactNativePath)], + resolver: { + extraNodeModules: {'react-native': reactNativePath}, + platforms: ['ios', 'macos', 'android'], + }, + }); + + await Metro.runBuild(config, { + entry: 'js/RNTesterApp.macos.js', + platform: 'macos', + dev: false, + minify: true, + out, + assets: Boolean(assetsDest), + assetsDest, + }); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/packages/typescript-config/BUILD.bazel b/packages/typescript-config/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/typescript-config/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/packages/virtualized-lists/BUILD.bazel b/packages/virtualized-lists/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/packages/virtualized-lists/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/private/cxx-public-api/BUILD.bazel b/private/cxx-public-api/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/private/cxx-public-api/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/private/eslint-plugin-monorepo/BUILD.bazel b/private/eslint-plugin-monorepo/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/private/eslint-plugin-monorepo/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/private/monorepo-tests/BUILD.bazel b/private/monorepo-tests/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/private/monorepo-tests/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/private/react-native-bots/BUILD.bazel b/private/react-native-bots/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/private/react-native-bots/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/private/react-native-codegen-typescript-test/BUILD.bazel b/private/react-native-codegen-typescript-test/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/private/react-native-codegen-typescript-test/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) diff --git a/private/react-native-fantom/BUILD.bazel b/private/react-native-fantom/BUILD.bazel new file mode 100644 index 000000000000..23f9dc4f3149 --- /dev/null +++ b/private/react-native-fantom/BUILD.bazel @@ -0,0 +1,24 @@ +# Auto-generated first-party package target for rules_js linking. +# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can +# link it into node_modules (needed to bundle first-party JS with Metro). +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() + +npm_package( + name = "pkg", + srcs = glob( + ["**/*"], + exclude = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", + ], + allow_empty = True, + ), + visibility = ["//visibility:public"], +) From 97e950141fa0bd2d8d36aa9dae24559557feedec Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 6 Jul 2026 15:33:47 -0700 Subject: [PATCH 03/17] docs: correct Hermes/XCFramework blocker diagnosis (Xcode/CMake, not 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> --- docs/bazel.md | 29 ++++++++++++++++++++++------- packages/rn-tester/BUILD.bazel | 5 +++-- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/bazel.md b/docs/bazel.md index 35566603e22a..4e1e2bee95c6 100644 --- a/docs/bazel.md +++ b/docs/bazel.md @@ -101,13 +101,28 @@ BYONM remains a viable fallback. Two concrete blockers separate the current state from a running RNTester macOS app: -1. **The macOS app is blocked by an external artifact gap.** `scripts/ios-prebuild.js` - cannot produce the XCFrameworks because the **Hermes nightly tarball 404s** - (`react-native-artifacts/0.87.0-nightly-…-hermes-ios-debug.tar.gz`). Without - `hermes.xcframework` / `React.xcframework` / `ReactNativeDependencies.xcframework`, - `macos_application` can't link. The app target is fully scaffolded and will build once - those XCFrameworks are available (e.g. a valid nightly, or a from-source Bazel build — - see the roadmap). +1. **The macOS app needs the prebuilt XCFrameworks, which can't be produced *in this + environment* because the local Xcode is too new.** This is a toolchain/environment + issue, **not** a fork gap — react-native-macos already carries the patches that resolve + the correct Hermes for the branch (`scripts/ios-prebuild/microsoft-hermes.js`): + * On a **release branch**, `findMatchingHermesVersion` maps to the upstream RN version in + `peerDependencies` and ios-prebuild **downloads** a published Hermes artifact. + * On **main (`1000.0.0`)**, it returns `null` and **builds Hermes from source** at the + Hermes commit at the merge-base with facebook/react-native (`hermesCommitAtMergeBase`). + + Building that from source here fails for two environment reasons: + * the default Homebrew CMake is **4.x**, too new for Hermes (use the installed + `cmake@3.26.4`); and, after that, + * **Xcode 26 / AppleClang 21** trips LLVM's `CheckAtomic` ("Host compiler appears to + require libatomic, but cannot find it"). The prebuild CI pins **Xcode 16.2 / 16.1** + (see `.github/workflows/prebuild-ios-core.yml`), which is what actually builds these + XCFrameworks. No Xcode 16.x is installed on this machine. + + Net: on CI (Xcode 16.2) — or from a release branch that downloads prebuilt Hermes — the + XCFrameworks build and the `macos_application` links. Locally with only Xcode 26, the + from-source Hermes build can't complete. (An earlier revision of this doc wrongly blamed + a "Hermes nightly 404"; that was caused by forcing `HERMES_VERSION=nightly`, which + bypasses the macOS version-resolution patches — don't set that env var.) 2. **The hermetic Bazel Metro bundle needs a dependency-closure step.** Two prerequisites: * `@react-native/codegen`'s `lib/` must be built (babel-plugin-codegen requires it): diff --git a/packages/rn-tester/BUILD.bazel b/packages/rn-tester/BUILD.bazel index e594d85b24a0..dc66b0517354 100644 --- a/packages/rn-tester/BUILD.bazel +++ b/packages/rn-tester/BUILD.bazel @@ -8,8 +8,9 @@ # - the Metro bundle needs the RN `react-native`->`react-native-macos` alias and a # sandbox-safe config (bazel/bundle.js), @react-native/codegen `lib` built, and the # full strict-deps closure assembled (rules_js does not hoist like Yarn); -# - the macOS app needs the prebuilt XCFrameworks, which currently cannot be produced -# here because the Hermes nightly tarball 404s (see docs/bazel.md). +# - the macOS app needs the prebuilt XCFrameworks; ios-prebuild resolves the correct +# Hermes (macOS patches) but building it from source needs Xcode 16.x (CI-pinned) -- +# the local Xcode 26 trips LLVM CheckAtomic. See docs/bazel.md. load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_run_binary") load("@npm//:defs.bzl", "npm_link_all_packages") From 5fe1590e4690cf0644cb61bf24d98b9a591142c2 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 6 Jul 2026 15:49:14 -0700 Subject: [PATCH 04/17] Fix Hermes host build on Xcode 26 (force macOS target for hermesc) 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> --- .../hermes-engine/utils/build-apple-framework.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh b/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh index ee71c715624a..37b72d04ad36 100755 --- a/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh +++ b/packages/react-native/sdks/hermes-engine/utils/build-apple-framework.sh @@ -61,10 +61,21 @@ function get_mac_deployment_target { function build_host_hermesc { echo "Building hermesc" pushd "$HERMES_PATH" > /dev/null || exit 1 - cmake -S . -B build_host_hermesc -DJSI_DIR="$JSI_PATH" - cmake --build ./build_host_hermesc --target hermesc -j "${NUM_CORES}" + # [macOS] The host hermesc tools are always native macOS. On Xcode 26+, clang honors + # the *_DEPLOYMENT_TARGET environment variables and will mis-target this native build + # to visionOS when XROS_DEPLOYMENT_TARGET (or IOS/TVOS) is set in the environment + # (XROS takes precedence even if MACOSX_DEPLOYMENT_TARGET is also set), which breaks + # availability/atomics checks ("using sysroot for 'MacOSX' but targeting 'XR'"). Build + # the host tools in a subshell that forces a macOS target. + ( + unset IOS_DEPLOYMENT_TARGET XROS_DEPLOYMENT_TARGET TVOS_DEPLOYMENT_TARGET + export MACOSX_DEPLOYMENT_TARGET="${MAC_DEPLOYMENT_TARGET:-$(get_mac_deployment_target)}" + cmake -S . -B build_host_hermesc -DJSI_DIR="$JSI_PATH" + cmake --build ./build_host_hermesc --target hermesc -j "${NUM_CORES}" + ) popd > /dev/null || exit 1 } +# macOS] # Utility function to configure an Apple framework function configure_apple_framework { From 6248c78c1114149d48c1d57ec573e66e3855683c Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 6 Jul 2026 15:52:25 -0700 Subject: [PATCH 05/17] docs: record the Xcode-26 Hermes host-build fix and status Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/bazel.md | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/docs/bazel.md b/docs/bazel.md index 4e1e2bee95c6..07b8fcbd591f 100644 --- a/docs/bazel.md +++ b/docs/bazel.md @@ -101,28 +101,26 @@ BYONM remains a viable fallback. Two concrete blockers separate the current state from a running RNTester macOS app: -1. **The macOS app needs the prebuilt XCFrameworks, which can't be produced *in this - environment* because the local Xcode is too new.** This is a toolchain/environment - issue, **not** a fork gap — react-native-macos already carries the patches that resolve - the correct Hermes for the branch (`scripts/ios-prebuild/microsoft-hermes.js`): - * On a **release branch**, `findMatchingHermesVersion` maps to the upstream RN version in - `peerDependencies` and ios-prebuild **downloads** a published Hermes artifact. - * On **main (`1000.0.0`)**, it returns `null` and **builds Hermes from source** at the - Hermes commit at the merge-base with facebook/react-native (`hermesCommitAtMergeBase`). - - Building that from source here fails for two environment reasons: - * the default Homebrew CMake is **4.x**, too new for Hermes (use the installed - `cmake@3.26.4`); and, after that, - * **Xcode 26 / AppleClang 21** trips LLVM's `CheckAtomic` ("Host compiler appears to - require libatomic, but cannot find it"). The prebuild CI pins **Xcode 16.2 / 16.1** - (see `.github/workflows/prebuild-ios-core.yml`), which is what actually builds these - XCFrameworks. No Xcode 16.x is installed on this machine. - - Net: on CI (Xcode 16.2) — or from a release branch that downloads prebuilt Hermes — the - XCFrameworks build and the `macos_application` links. Locally with only Xcode 26, the - from-source Hermes build can't complete. (An earlier revision of this doc wrongly blamed - a "Hermes nightly 404"; that was caused by forcing `HERMES_VERSION=nightly`, which - bypasses the macOS version-resolution patches — don't set that env var.) +1. **The macOS app needs the prebuilt XCFrameworks. The Xcode-26 Hermes build blocker has + been fixed here; the remaining cost is the (long) native build itself.** This was never a + fork gap — react-native-macos already carries the Hermes version-resolution patches + (`scripts/ios-prebuild/microsoft-hermes.js`): a release branch downloads a published + Hermes, and `main` (`1000.0.0`) builds Hermes from source at the Hermes commit at the + merge-base with facebook/react-native. + + Building from source on **Xcode 26** originally failed because clang honors + `*_DEPLOYMENT_TARGET` env vars: `ios-prebuild` sets `XROS_DEPLOYMENT_TARGET` for the + cross-platform builds and it leaked into the **native** `build_host_hermesc` step, so + Xcode 26's clang mis-targeted the host tools to **visionOS** ("using sysroot for + 'MacOSX' but targeting 'XR'"), failing llvh's `CheckAtomic`. Fixed in + `sdks/hermes-engine/utils/build-apple-framework.sh` by building the host tools with the + mobile deployment-target vars unset and `MACOSX_DEPLOYMENT_TARGET` set — verified: + `HAVE_CXX_ATOMICS_WITHOUT_LIB` now succeeds and Hermes compiles under Xcode 26. + + Two environment notes remain: use **CMake 3.x** (Hermes doesn't configure under the + Homebrew-default CMake 4.x; `cmake@3.26.4` works), and the full from-source build of + Hermes + ReactNativeDependencies + React across all Apple slices is long. Once those + XCFrameworks exist, the `macos_application` links them on Xcode 26. 2. **The hermetic Bazel Metro bundle needs a dependency-closure step.** Two prerequisites: * `@react-native/codegen`'s `lib/` must be built (babel-plugin-codegen requires it): From 3bbbcffe09876be4e40fc140da82409a9aa6a1c8 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 6 Jul 2026 16:13:47 -0700 Subject: [PATCH 06/17] Allow building a subset of Hermes Apple platforms (HERMES_APPLE_PLATFORMS) 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> --- .../utils/build-ios-framework.sh | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/react-native/sdks/hermes-engine/utils/build-ios-framework.sh b/packages/react-native/sdks/hermes-engine/utils/build-ios-framework.sh index c1a6a4aa7387..80c35b1a70ed 100755 --- a/packages/react-native/sdks/hermes-engine/utils/build-ios-framework.sh +++ b/packages/react-native/sdks/hermes-engine/utils/build-ios-framework.sh @@ -57,7 +57,8 @@ function build_framework { # group the frameworks together to create a universal framework function build_universal_framework { if [ ! -d destroot/Library/Frameworks/universal/hermes.xcframework ]; then - create_universal_framework "macosx" "iphoneos" "iphonesimulator" "catalyst" "xros" "xrsimulator" "appletvos" "appletvsimulator" # [macOS] + # shellcheck disable=SC2086 + create_universal_framework $HERMES_APPLE_PLATFORMS # [macOS] else echo "Skipping; Clean \"destroot\" to rebuild". fi @@ -67,14 +68,11 @@ function build_universal_framework { # this is used to preserve backward compatibility function create_framework { if [ ! -d destroot/Library/Frameworks/universal/hermes.xcframework ]; then - build_framework "macosx" # [macOS] - build_framework "iphoneos" - build_framework "iphonesimulator" - build_framework "appletvos" - build_framework "appletvsimulator" - build_framework "catalyst" - build_framework "xros" - build_framework "xrsimulator" + # [macOS] Build only the requested Apple platforms (defaults to all). + for _hermes_platform in $HERMES_APPLE_PLATFORMS; do + build_framework "$_hermes_platform" + done + # macOS] build_universal_framework else echo "Skipping; Clean \"destroot\" to rebuild". @@ -86,6 +84,14 @@ CURR_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" # shellcheck source=xplat/js/react-native-github/sdks/hermes-engine/utils/build-apple-framework.sh . "${CURR_SCRIPT_DIR}/build-apple-framework.sh" +# [macOS] Apple platforms (slices) to build into hermes.xcframework. Defaults to all. +# Override with the HERMES_APPLE_PLATFORMS env var (space-separated) to build a subset, +# e.g. HERMES_APPLE_PLATFORMS="macosx" for a macOS-only build. Useful on Xcode 26+, where +# the upstream Hermes Mac Catalyst target triple is rejected by the newer clang, and to +# speed up macOS-only iteration. +HERMES_APPLE_PLATFORMS="${HERMES_APPLE_PLATFORMS:-macosx iphoneos iphonesimulator catalyst xros xrsimulator appletvos appletvsimulator}" +# macOS] + if [[ -z $1 ]]; then create_framework elif [[ $1 == "build_framework" ]]; then From 85222de875cb2f37336282f7d5cba11d18ef0e47 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 6 Jul 2026 16:22:39 -0700 Subject: [PATCH 07/17] docs: react-native-macos native prebuild now builds on Xcode 26 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> --- docs/bazel.md | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/docs/bazel.md b/docs/bazel.md index 07b8fcbd591f..d7a99bd4f05c 100644 --- a/docs/bazel.md +++ b/docs/bazel.md @@ -101,26 +101,34 @@ BYONM remains a viable fallback. Two concrete blockers separate the current state from a running RNTester macOS app: -1. **The macOS app needs the prebuilt XCFrameworks. The Xcode-26 Hermes build blocker has - been fixed here; the remaining cost is the (long) native build itself.** This was never a - fork gap — react-native-macos already carries the Hermes version-resolution patches +1. **The macOS app needs the prebuilt XCFrameworks. These now build on Xcode 26 with the + fixes in this branch** (verified end to end). This was never a fork gap — + react-native-macos already carries the Hermes version-resolution patches (`scripts/ios-prebuild/microsoft-hermes.js`): a release branch downloads a published - Hermes, and `main` (`1000.0.0`) builds Hermes from source at the Hermes commit at the - merge-base with facebook/react-native. - - Building from source on **Xcode 26** originally failed because clang honors - `*_DEPLOYMENT_TARGET` env vars: `ios-prebuild` sets `XROS_DEPLOYMENT_TARGET` for the - cross-platform builds and it leaked into the **native** `build_host_hermesc` step, so - Xcode 26's clang mis-targeted the host tools to **visionOS** ("using sysroot for - 'MacOSX' but targeting 'XR'"), failing llvh's `CheckAtomic`. Fixed in - `sdks/hermes-engine/utils/build-apple-framework.sh` by building the host tools with the - mobile deployment-target vars unset and `MACOSX_DEPLOYMENT_TARGET` set — verified: - `HAVE_CXX_ATOMICS_WITHOUT_LIB` now succeeds and Hermes compiles under Xcode 26. - - Two environment notes remain: use **CMake 3.x** (Hermes doesn't configure under the - Homebrew-default CMake 4.x; `cmake@3.26.4` works), and the full from-source build of - Hermes + ReactNativeDependencies + React across all Apple slices is long. Once those - XCFrameworks exist, the `macos_application` links them on Xcode 26. + Hermes; `main` (`1000.0.0`) builds Hermes from source at the merge-base commit. + + Getting the from-source build working on **Xcode 26** required two fixes (both in this + branch) plus one environment note: + * **Host `hermesc` mis-targeted to visionOS.** `ios-prebuild` sets + `XROS_DEPLOYMENT_TARGET` for the cross-platform builds and it leaked into the *native* + `build_host_hermesc`; Xcode 26's clang honors it and built the host tools for visionOS + ("using sysroot for 'MacOSX' but targeting 'XR'"), failing llvh's `CheckAtomic`. Fixed + in `sdks/hermes-engine/utils/build-apple-framework.sh` (force a macOS target for the + host tools). + * **Upstream Hermes Mac Catalyst triple.** Hermes hardcodes an invalid universal + Catalyst triple (`-target x86_64-arm64-apple-ios…-macabi`) that Xcode 26's clang + rejects. This is a Hermes-upstream issue and Catalyst isn't needed for a macOS app, so + `build-ios-framework.sh` now accepts `HERMES_APPLE_PLATFORMS` to build a subset (e.g. + `macosx`). + * Use **CMake 3.x** (`cmake@3.26.4`); Hermes doesn't configure under the Homebrew-default + CMake 4.x. + + Verified on Xcode 26.4 with `HERMES_APPLE_PLATFORMS=macosx` and `cmake@3.26.4`: + `ios-prebuild.js -s/-b/-c` produces all three XCFrameworks — + `hermes.xcframework` (from source), `ReactNativeDependencies.xcframework` (downloaded + 0.86.0), and `React.xcframework` (`** BUILD SUCCEEDED **`, macOS slice). What remains is + wiring those into the Bazel `macos_application` (rules_apple on Xcode 26) and embedding + the JS bundle. 2. **The hermetic Bazel Metro bundle needs a dependency-closure step.** Two prerequisites: * `@react-native/codegen`'s `lib/` must be built (babel-plugin-codegen requires it): From 7943e122f85a20bc92d356494c3695d7d08f2a9a Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 6 Jul 2026 16:27:56 -0700 Subject: [PATCH 08/17] Bazel: repo rule to import the prebuilt XCFrameworks (builds on Xcode 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> --- MODULE.bazel | 5 ++ packages/rn-tester/BUILD.bazel | 16 ++--- tools/bazel/apple/prebuilt_xcframeworks.bzl | 66 +++++++++++++++++++++ 3 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 tools/bazel/apple/prebuilt_xcframeworks.bzl diff --git a/MODULE.bazel b/MODULE.bazel index 803e86df0fb3..453d3a1ec461 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -60,3 +60,8 @@ bazel_dep(name = "rules_swift", version = "3.6.1") bazel_dep(name = "apple_support", version = "2.7.0") bazel_dep(name = "rules_cc", version = "0.2.17") bazel_dep(name = "platforms", version = "1.1.0") + +# Expose the SPM-prebuilt React Native XCFrameworks (built by scripts/ios-prebuild.js) +# to Bazel so the macOS app slice can link them. +rn_prebuilt = use_extension("//tools/bazel/apple:prebuilt_xcframeworks.bzl", "rn_prebuilt_xcframeworks_extension") +use_repo(rn_prebuilt, "rn_prebuilt_xcframeworks") diff --git a/packages/rn-tester/BUILD.bazel b/packages/rn-tester/BUILD.bazel index dc66b0517354..38710dfef71e 100644 --- a/packages/rn-tester/BUILD.bazel +++ b/packages/rn-tester/BUILD.bazel @@ -14,7 +14,6 @@ load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_run_binary") load("@npm//:defs.bzl", "npm_link_all_packages") -load("//tools/bazel/apple:xcframeworks.bzl", "rn_imported_xcframeworks") load("@rules_apple//apple:macos.bzl", "macos_application") package(default_visibility = ["//visibility:private"]) @@ -58,10 +57,11 @@ js_run_binary( tool = ":bundler", ) -# 2. Prebuilt native code as importable XCFrameworks (from ios-prebuild). -rn_imported_xcframeworks() - -# 3. App host — reuse rn-tester's existing AppDelegate + main. +# 3. App host. NOTE: rn-tester's real AppDelegate.mm pulls in rn-tester-specific native +# modules + codegen (NativeCxxModuleExample, RNTMyNativeViewComponentView, +# ReactAppDependencyProvider) that are NOT in the prebuilt XCFrameworks. A green app +# therefore needs either those built too, or a minimal RN macOS host. The imported +# XCFrameworks below DO build on Xcode 26 (bazel build @rn_prebuilt_xcframeworks//:React). objc_library( name = "rntester_macos_host", srcs = [ @@ -71,9 +71,9 @@ objc_library( hdrs = ["RNTester/AppDelegate.h"], tags = ["manual"], deps = [ - ":React", - ":ReactNativeDependencies", - ":hermes", + "@rn_prebuilt_xcframeworks//:React", + "@rn_prebuilt_xcframeworks//:ReactNativeDependencies", + "@rn_prebuilt_xcframeworks//:hermes", ], ) diff --git a/tools/bazel/apple/prebuilt_xcframeworks.bzl b/tools/bazel/apple/prebuilt_xcframeworks.bzl new file mode 100644 index 000000000000..d415d7249871 --- /dev/null +++ b/tools/bazel/apple/prebuilt_xcframeworks.bzl @@ -0,0 +1,66 @@ +"""Expose the SPM-prebuilt React Native XCFrameworks to Bazel. + +The XCFrameworks produced by `scripts/ios-prebuild.js` live under +`packages/react-native/.build` and `third-party/` (build outputs, gitignored, and in +`.bazelignore`). This repository rule symlinks them into an external repo and exposes +`apple_*_xcframework_import` targets so the Bazel `macos_application` can link them. + +If the XCFrameworks are missing (e.g. a clean checkout / CI before the prebuild), the +generated targets fail with an actionable message instead of breaking analysis of the +whole repo. +""" + +_REL_PATHS = { + "React": "packages/react-native/.build/output/xcframeworks/Debug/React.xcframework", + "ReactNativeDependencies": "packages/react-native/third-party/ReactNativeDependencies.xcframework", + "hermes": "packages/react-native/.build/artifacts/hermes/destroot/Library/Frameworks/universal/hermes.xcframework", +} + +# hermes ships as a dynamic framework; React + deps are static. +_DYNAMIC = {"hermes": True} + +def _prebuilt_xcframeworks_impl(rctx): + root = str(rctx.workspace_root) + lines = [ + 'load("@rules_apple//apple:apple.bzl", "apple_dynamic_xcframework_import", "apple_static_xcframework_import")', + 'package(default_visibility = ["//visibility:public"])', + "", + ] + missing = [] + for name, rel in _REL_PATHS.items(): + src = "{}/{}".format(root, rel) + if not rctx.path(src).exists: + missing.append(rel) + continue + rctx.symlink(src, name + ".xcframework") + rule = "apple_dynamic_xcframework_import" if _DYNAMIC.get(name) else "apple_static_xcframework_import" + lines.append('{rule}(name = "{name}", xcframework_imports = glob(["{name}.xcframework/**"]))'.format( + rule = rule, + name = name, + )) + + if missing: + # Emit a target that fails at build time with guidance, rather than failing analysis. + msg = ("Prebuilt XCFrameworks not found: {}. Build them first with " + + "`cd packages/react-native && HERMES_APPLE_PLATFORMS=macosx node scripts/ios-prebuild.js -s -f Debug " + + "&& node scripts/ios-prebuild.js -b -f Debug -p macos && node scripts/ios-prebuild.js -c -f Debug` " + + "(use cmake@3.x on Xcode 26).").format(", ".join(missing)) + lines = [ + 'package(default_visibility = ["//visibility:public"])', + 'genrule(name = "_missing", outs = ["missing.txt"], cmd = "echo \'{}\' >&2; exit 1")'.format(msg), + ] + for name in _REL_PATHS: + lines.append('alias(name = "{name}", actual = ":_missing")'.format(name = name)) + + rctx.file("BUILD.bazel", "\n".join(lines) + "\n") + +prebuilt_xcframeworks = repository_rule( + implementation = _prebuilt_xcframeworks_impl, + doc = "Symlinks the SPM-prebuilt React Native XCFrameworks into a Bazel repo.", + local = True, +) + +def _extension_impl(_module_ctx): + prebuilt_xcframeworks(name = "rn_prebuilt_xcframeworks") + +rn_prebuilt_xcframeworks_extension = module_extension(implementation = _extension_impl) From 97d231d175cdc30a6d7b15c50d3eaa72f2d98fcf Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 6 Jul 2026 23:55:53 -0700 Subject: [PATCH 09/17] Berry converter fix (resolutions + trailing space) and rn-tester JS bundle 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> --- packages/rn-tester/BUILD.bazel | 11 +++- packages/rn-tester/bazel/bundle.js | 18 ++++-- packages/rn-tester/package.json | 4 ++ tools/bazel/berry/berry_to_pnpm_lock.mjs | 77 +++++++++++++++++++++--- yarn.lock | 4 ++ 5 files changed, 98 insertions(+), 16 deletions(-) diff --git a/packages/rn-tester/BUILD.bazel b/packages/rn-tester/BUILD.bazel index 38710dfef71e..807892c3aeec 100644 --- a/packages/rn-tester/BUILD.bazel +++ b/packages/rn-tester/BUILD.bazel @@ -12,7 +12,7 @@ # Hermes (macOS patches) but building it from source needs Xcode 16.x (CI-pinned) -- # the local Xcode 26 trips LLVM CheckAtomic. See docs/bazel.md. -load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_run_binary") +load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_library", "js_run_binary") load("@npm//:defs.bzl", "npm_link_all_packages") load("@rules_apple//apple:macos.bzl", "macos_application") @@ -22,7 +22,7 @@ package(default_visibility = ["//visibility:private"]) npm_link_all_packages(name = "node_modules") # All rn-tester JavaScript that Metro may pull into the bundle. -filegroup( +js_library( name = "js_srcs", srcs = glob( ["js/**/*.js"], @@ -52,7 +52,12 @@ js_run_binary( name = "rntester_macos_jsbundle", srcs = [":js_srcs"], outs = ["RNTesterApp.macos.jsbundle"], - env = {"BUNDLE_OUT": "RNTesterApp.macos.jsbundle"}, + out_dirs = ["RNTesterApp_assets"], + env = { + "BUNDLE_OUT": "packages/rn-tester/RNTesterApp.macos.jsbundle", + "ASSETS_DEST": "packages/rn-tester/RNTesterApp_assets", + "RN_TESTER_ROOT": "packages/rn-tester", + }, tags = ["manual"], tool = ":bundler", ) diff --git a/packages/rn-tester/bazel/bundle.js b/packages/rn-tester/bazel/bundle.js index 15ae0511b457..4b548edc77bf 100644 --- a/packages/rn-tester/bazel/bundle.js +++ b/packages/rn-tester/bazel/bundle.js @@ -22,11 +22,12 @@ const Metro = require('metro'); const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); async function main() { - const projectRoot = process.env.RN_TESTER_ROOT - ? path.resolve(process.env.RN_TESTER_ROOT) - : process.cwd(); - const out = process.env.BUNDLE_OUT || 'RNTesterApp.macos.jsbundle'; - const assetsDest = process.env.ASSETS_DEST || undefined; + const projectRoot = path.resolve(process.env.RN_TESTER_ROOT || process.cwd()); + const out = path.resolve(process.env.BUNDLE_OUT || 'RNTesterApp.macos.jsbundle'); + const assetsDest = process.env.ASSETS_DEST + ? path.resolve(process.env.ASSETS_DEST) + : undefined; + const entryFile = path.join(projectRoot, 'js/RNTesterApp.macos.js'); // react-native-macos is published/consumed as `react-native`. const reactNativePath = path.dirname( @@ -40,11 +41,16 @@ async function main() { resolver: { extraNodeModules: {'react-native': reactNativePath}, platforms: ['ios', 'macos', 'android'], + // Watchman isn't available (or usable) inside the Bazel sandbox; use Metro's + // Node crawler so the file map is populated. rules_js exposes sources as symlinks, + // so the crawler must follow them. + useWatchman: false, + unstable_enableSymlinks: true, }, }); await Metro.runBuild(config, { - entry: 'js/RNTesterApp.macos.js', + entry: entryFile, platform: 'macos', dev: false, minify: true, diff --git a/packages/rn-tester/package.json b/packages/rn-tester/package.json index ea5c1d4a7069..2fa0a265d850 100644 --- a/packages/rn-tester/package.json +++ b/packages/rn-tester/package.json @@ -61,8 +61,12 @@ "@react-native-community/cli": "20.0.0", "@react-native-community/cli-platform-android": "20.0.0", "@react-native-community/cli-platform-ios": "20.0.0", + "@react-native/metro-babel-transformer": "workspace:*", + "@react-native/metro-config": "workspace:*", "commander": "^12.0.0", "listr2": "^6.4.1", + "metro": "^0.82.5", + "react": "19.1.0", "react-native-macos": "workspace:*", "rxjs": "npm:@react-native-community/rxjs@6.5.4-custom" } diff --git a/tools/bazel/berry/berry_to_pnpm_lock.mjs b/tools/bazel/berry/berry_to_pnpm_lock.mjs index bbc18240881c..6096898a83a3 100644 --- a/tools/bazel/berry/berry_to_pnpm_lock.mjs +++ b/tools/bazel/berry/berry_to_pnpm_lock.mjs @@ -23,6 +23,7 @@ */ import fs from 'node:fs'; +import path from 'node:path'; const REGISTRY = 'https://registry.npmjs.org'; @@ -166,14 +167,30 @@ function relPath(from, to) { // Conversion // --------------------------------------------------------------------------- -function convert(syml) { +function convert(syml, resolutions) { // Map every descriptor string -> its resolved entry. const descriptorToEntry = new Map(); // Canonical package key `ident@version` -> entry (npm packages only). const packageEntries = new Map(); + // ident -> [entries] (for single-version fallback resolution). + const nameToEntries = new Map(); // Workspace entries: importer path -> entry. const workspaceByPath = new Map(); - // ident@workspacePath (descriptor) resolution helper: descriptor -> workspace path. + + // Yarn `resolutions` overrides. Berry rewrites matching descriptors, but dependency + // entries keep their original ranges, so we must apply resolutions when resolving. + const globalRes = new Map(); // ident -> override value (e.g. ">=3.1.0") + const specificRes = new Map(); // "ident@range" -> override value + for (const [k, v] of Object.entries(resolutions || {})) { + const at = k.indexOf('@', k.startsWith('@') ? 1 : 0); + if (at > 0) { + specificRes.set(k, v); + } else { + globalRes.set(k, v); + } + } + const normalizeResValue = v => (/^[a-z]+:/.test(v) ? v : 'npm:' + v); + let droppedCount = 0; const entries = []; for (const [rawKey, value] of Object.entries(syml)) { @@ -219,14 +236,41 @@ function convert(syml) { } } } + if (entry.version || entry.kind === 'workspace') { + if (!nameToEntries.has(ident)) { + nameToEntries.set(ident, []); + } + nameToEntries.get(ident).push(entry); + } } // Resolve a `depName: rangeDescriptor` reference to either a snapshot key - // (`name@version`) or a `link:` for workspace deps. + // (`name@version`) or a `link:` for workspace deps. Handles Yarn + // `resolutions` overrides and falls back to a single resolved version. function resolveDep(fromPath, depName, rangeDescriptor) { - const descriptor = `${depName}@${rangeDescriptor}`; - const target = descriptorToEntry.get(descriptor); + // Yarn descriptors can carry insignificant trailing whitespace; normalize so a + // dependency range like "npm:^3.1.0 " matches the trimmed descriptor key. + rangeDescriptor = String(rangeDescriptor).trim(); + let target = descriptorToEntry.get(`${depName}@${rangeDescriptor}`); + if (!target) { + const specific = specificRes.get(`${depName}@${rangeDescriptor}`); + if (specific != null) { + target = descriptorToEntry.get(`${depName}@${normalizeResValue(specific)}`); + } + } + if (!target && globalRes.has(depName)) { + target = descriptorToEntry.get(`${depName}@${normalizeResValue(globalRes.get(depName))}`); + } if (!target) { + // Single-version fallback: if the package resolves to exactly one version in the + // lock (common once resolutions/dedup collapse ranges), use it. + const list = nameToEntries.get(depName); + if (list && list.length === 1) { + target = list[0]; + } + } + if (!target) { + droppedCount++; return null; } if (target.kind === 'workspace') { @@ -239,6 +283,7 @@ function convert(syml) { ident: target.ident, }; } + droppedCount++; return null; } @@ -326,7 +371,7 @@ function convert(syml) { importers[importPath] = {dependencies: deps}; } - return {importers, packages, snapshots}; + return {importers, packages, snapshots, droppedCount}; } // --------------------------------------------------------------------------- @@ -460,11 +505,29 @@ function main() { } const text = fs.readFileSync(inPath, 'utf8'); const syml = parseSyml(text); - const model = convert(syml); + + // Load Yarn `resolutions` from the root package.json next to yarn.lock so we can + // apply them (Berry rewrites matching descriptors but keeps original dep ranges). + let resolutions = {}; + try { + const rootPkgPath = path.join(path.dirname(path.resolve(inPath)), 'package.json'); + if (fs.existsSync(rootPkgPath)) { + resolutions = JSON.parse(fs.readFileSync(rootPkgPath, 'utf8')).resolutions || {}; + } + } catch (_) { + // best effort + } + + const model = convert(syml, resolutions); validate(model); fs.writeFileSync(outPath, toYaml(model)); const nImporters = Object.keys(model.importers).length; const nPackages = Object.keys(model.packages).length; + if (model.droppedCount) { + console.error( + `berry_to_pnpm_lock: WARNING ${model.droppedCount} dependency edge(s) could not be resolved and were dropped`, + ); + } console.error( `berry_to_pnpm_lock: wrote ${outPath} (${nImporters} importers, ${nPackages} packages)`, ); diff --git a/yarn.lock b/yarn.lock index 305e0c9812e4..56066ef8ad09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3536,6 +3536,8 @@ __metadata: "@react-native-community/cli": "npm:20.0.0" "@react-native-community/cli-platform-android": "npm:20.0.0" "@react-native-community/cli-platform-ios": "npm:20.0.0" + "@react-native/metro-babel-transformer": "workspace:*" + "@react-native/metro-config": "workspace:*" "@react-native/new-app-screen": "workspace:*" "@react-native/oss-library-example": "workspace:*" "@react-native/popup-menu-android": "workspace:*" @@ -3543,7 +3545,9 @@ __metadata: flow-enums-runtime: "npm:^0.0.6" invariant: "npm:^2.2.4" listr2: "npm:^6.4.1" + metro: "npm:^0.82.5" nullthrows: "npm:^1.1.1" + react: "npm:19.1.0" react-native-macos: "workspace:*" rxjs: "npm:@react-native-community/rxjs@6.5.4-custom" peerDependencies: From 80fed2a7f3d85ae44a152b83af4a515c20daf1b4 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 6 Jul 2026 23:56:37 -0700 Subject: [PATCH 10/17] docs: precise rn-tester JS bundle status (converter fix, first-party dist, Metro file-map) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/bazel.md | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/docs/bazel.md b/docs/bazel.md index d7a99bd4f05c..aec3785e2905 100644 --- a/docs/bazel.md +++ b/docs/bazel.md @@ -130,17 +130,28 @@ Two concrete blockers separate the current state from a running RNTester macOS a wiring those into the Bazel `macos_application` (rules_apple on Xcode 26) and embedding the JS bundle. -2. **The hermetic Bazel Metro bundle needs a dependency-closure step.** Two prerequisites: - * `@react-native/codegen`'s `lib/` must be built (babel-plugin-codegen requires it): - `(cd packages/react-native-codegen && yarn build)`. - * rules_js uses a **strict, non-hoisted** `node_modules`. Metro's tooling (`metro`, - `@react-native/metro-config`, `@react-native/metro-babel-transformer`) are - transitive/root devDeps, so they are not resolvable from rn-tester's `node_modules`. - To make `//packages/rn-tester:rntester_macos_jsbundle` green, declare that tooling as - deps of the bundler (e.g. add them to rn-tester's `package.json` and regenerate the - Berry `yarn.lock`) so rules_js links the full closure. The bundle itself runs via - `packages/rn-tester/bazel/bundle.js` (Metro's API, with the - `react-native → react-native-macos` alias and a sandbox-safe config). +2. **The hermetic Bazel Metro bundle** is close but not yet green. Progress + remaining: + * `@react-native/codegen`'s `lib/` must be built (babel-plugin-codegen requires it). + * The bundler tooling closure is now declared on rn-tester (`metro`, + `@react-native/metro-config`, `@react-native/metro-babel-transformer`, `react`) so + rules_js's strict, non-hoisted `node_modules` resolves it. The Berry→pnpm converter was + also fixed to apply Yarn `resolutions` (it had been silently dropping edges such as + `https-proxy-agent`'s `debug`), so the closure is now complete. + * **First-party packages must be consumed in built/`dist` form.** Their source entry + points (e.g. `@react-native/metro-config/src/index.js`) use `../../../scripts/babel-register` + to run the monorepo's Flow sources on the fly — which resolves under Yarn's symlinked + `node_modules` but not under rules_js's copied layout. Running `node scripts/build/build.js` + (which builds `dist/` and repoints `exports`→`dist`) makes Metro load fine; the Bazel + `npm_package` for first-party packages should run that build+prepack hermetically. + * **Metro file-map vs Bazel file layout.** With the above, Metro loads and resolves the + full first-party + third-party graph, but its file-map fails to hash the entry + ("Failed to get the SHA-1 for …/js/RNTesterApp.macos.js") — the crawler doesn't map + files in the Bazel `bin` layout (persists with `useWatchman:false`, + `unstable_enableSymlinks:true`, and no-sandbox). This is the last blocker for the JS + bundle and needs a Metro-config/rules_js file-materialization fix. + + The bundle entry runs via `packages/rn-tester/bazel/bundle.js` (Metro's API + the + `react-native → react-native-macos` alias). ## What's next (WIP, scaffolded — all `manual`) From e9819bafb737f62e8438046d59c29f7f4d349395 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 7 Jul 2026 05:41:53 -0700 Subject: [PATCH 11/17] Bazel: green rn-tester JS bundle (Metro) + codegen (AppSpecs + providers) 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> --- packages/community-cli-plugin/BUILD.bazel | 23 +- packages/core-cli-utils/BUILD.bazel | 23 +- packages/debugger-shell/BUILD.bazel | 23 +- packages/dev-middleware/BUILD.bazel | 23 +- packages/metro-config/BUILD.bazel | 23 +- packages/react-native-codegen/BUILD.bazel | 23 +- .../BUILD.bazel | 23 +- packages/rn-tester/BUILD.bazel | 40 ++- .../NativeComponentExample/BUILD.bazel | 3 + .../NativeCxxModuleExample/BUILD.bazel | 3 + .../rn-tester/NativeModuleExample/BUILD.bazel | 3 + packages/rn-tester/bazel/bundle.js | 243 ++++++++++++++++- tools/bazel/js/BUILD.bazel | 29 +- tools/bazel/js/build_first_party.js | 247 ++++++++++++++++++ tools/bazel/js/copy_tree.js | 76 ++++++ tools/bazel/js/first_party.bzl | 42 +++ tools/bazel/react_native/BUILD.bazel | 91 +++++++ tools/bazel/react_native/build_codegen_lib.js | 169 ++++++++++++ tools/bazel/react_native/codegen_runner.js | 181 +++++++++++++ tools/bazel/react_native/defs.bzl | 37 +++ 20 files changed, 1176 insertions(+), 149 deletions(-) create mode 100644 packages/rn-tester/NativeComponentExample/BUILD.bazel create mode 100644 packages/rn-tester/NativeCxxModuleExample/BUILD.bazel create mode 100644 packages/rn-tester/NativeModuleExample/BUILD.bazel create mode 100644 tools/bazel/js/build_first_party.js create mode 100644 tools/bazel/js/copy_tree.js create mode 100644 tools/bazel/js/first_party.bzl create mode 100644 tools/bazel/react_native/BUILD.bazel create mode 100644 tools/bazel/react_native/build_codegen_lib.js create mode 100644 tools/bazel/react_native/codegen_runner.js create mode 100644 tools/bazel/react_native/defs.bzl diff --git a/packages/community-cli-plugin/BUILD.bazel b/packages/community-cli-plugin/BUILD.bazel index 23f9dc4f3149..788bf808c549 100644 --- a/packages/community-cli-plugin/BUILD.bazel +++ b/packages/community-cli-plugin/BUILD.bazel @@ -1,24 +1,9 @@ -# Auto-generated first-party package target for rules_js linking. # Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# link the built package into node_modules for Bazel-driven Metro bundling. +load("//tools/bazel/js:first_party.bzl", "first_party_js_package") -npm_link_all_packages() - -npm_package( +first_party_js_package( name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), + build_kind = "monorepo", visibility = ["//visibility:public"], ) diff --git a/packages/core-cli-utils/BUILD.bazel b/packages/core-cli-utils/BUILD.bazel index 23f9dc4f3149..788bf808c549 100644 --- a/packages/core-cli-utils/BUILD.bazel +++ b/packages/core-cli-utils/BUILD.bazel @@ -1,24 +1,9 @@ -# Auto-generated first-party package target for rules_js linking. # Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# link the built package into node_modules for Bazel-driven Metro bundling. +load("//tools/bazel/js:first_party.bzl", "first_party_js_package") -npm_link_all_packages() - -npm_package( +first_party_js_package( name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), + build_kind = "monorepo", visibility = ["//visibility:public"], ) diff --git a/packages/debugger-shell/BUILD.bazel b/packages/debugger-shell/BUILD.bazel index 23f9dc4f3149..788bf808c549 100644 --- a/packages/debugger-shell/BUILD.bazel +++ b/packages/debugger-shell/BUILD.bazel @@ -1,24 +1,9 @@ -# Auto-generated first-party package target for rules_js linking. # Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# link the built package into node_modules for Bazel-driven Metro bundling. +load("//tools/bazel/js:first_party.bzl", "first_party_js_package") -npm_link_all_packages() - -npm_package( +first_party_js_package( name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), + build_kind = "monorepo", visibility = ["//visibility:public"], ) diff --git a/packages/dev-middleware/BUILD.bazel b/packages/dev-middleware/BUILD.bazel index 23f9dc4f3149..788bf808c549 100644 --- a/packages/dev-middleware/BUILD.bazel +++ b/packages/dev-middleware/BUILD.bazel @@ -1,24 +1,9 @@ -# Auto-generated first-party package target for rules_js linking. # Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# link the built package into node_modules for Bazel-driven Metro bundling. +load("//tools/bazel/js:first_party.bzl", "first_party_js_package") -npm_link_all_packages() - -npm_package( +first_party_js_package( name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), + build_kind = "monorepo", visibility = ["//visibility:public"], ) diff --git a/packages/metro-config/BUILD.bazel b/packages/metro-config/BUILD.bazel index 23f9dc4f3149..788bf808c549 100644 --- a/packages/metro-config/BUILD.bazel +++ b/packages/metro-config/BUILD.bazel @@ -1,24 +1,9 @@ -# Auto-generated first-party package target for rules_js linking. # Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# link the built package into node_modules for Bazel-driven Metro bundling. +load("//tools/bazel/js:first_party.bzl", "first_party_js_package") -npm_link_all_packages() - -npm_package( +first_party_js_package( name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), + build_kind = "monorepo", visibility = ["//visibility:public"], ) diff --git a/packages/react-native-codegen/BUILD.bazel b/packages/react-native-codegen/BUILD.bazel index 23f9dc4f3149..28c6338d134e 100644 --- a/packages/react-native-codegen/BUILD.bazel +++ b/packages/react-native-codegen/BUILD.bazel @@ -1,24 +1,9 @@ -# Auto-generated first-party package target for rules_js linking. # Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# link the built package into node_modules for Bazel-driven Metro bundling. +load("//tools/bazel/js:first_party.bzl", "first_party_js_package") -npm_link_all_packages() - -npm_package( +first_party_js_package( name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), + build_kind = "codegen", visibility = ["//visibility:public"], ) diff --git a/packages/react-native-compatibility-check/BUILD.bazel b/packages/react-native-compatibility-check/BUILD.bazel index 23f9dc4f3149..788bf808c549 100644 --- a/packages/react-native-compatibility-check/BUILD.bazel +++ b/packages/react-native-compatibility-check/BUILD.bazel @@ -1,24 +1,9 @@ -# Auto-generated first-party package target for rules_js linking. # Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# link the built package into node_modules for Bazel-driven Metro bundling. +load("//tools/bazel/js:first_party.bzl", "first_party_js_package") -npm_link_all_packages() - -npm_package( +first_party_js_package( name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), + build_kind = "monorepo", visibility = ["//visibility:public"], ) diff --git a/packages/rn-tester/BUILD.bazel b/packages/rn-tester/BUILD.bazel index 807892c3aeec..a14dc22f4605 100644 --- a/packages/rn-tester/BUILD.bazel +++ b/packages/rn-tester/BUILD.bazel @@ -25,8 +25,22 @@ npm_link_all_packages(name = "node_modules") js_library( name = "js_srcs", srcs = glob( - ["js/**/*.js"], - allow_empty = False, + [ + "js/**/*.js", + "js/**/*.gif", + "js/**/*.jpeg", + "js/**/*.jpg", + "js/**/*.json", + "js/**/*.png", + "js/**/*.webp", + "js/**/*.xml", + "RCTTest/**/*.js", + "ReportFullyDrawnView/**/*.js", + "NativeComponentExample/js/**/*.js", + "NativeModuleExample/**/*.js", + "NativeCxxModuleExample/**/*.js", + ], + allow_empty = True, ) + [ "metro.config.js", "react-native.config.js", @@ -42,21 +56,37 @@ js_library( # The bundle runs via bazel/bundle.js (Metro's API) with the react-native alias applied. js_binary( name = "bundler", - data = [":node_modules"], + data = [ + "//:.aspect_rules_js/node_modules/invariant@2.2.4", + ":node_modules", + "//:node_modules/@babel/core", + ], entry_point = "bazel/bundle.js", tags = ["manual"], ) +js_run_binary( + name = "rntester_js_project", + srcs = [":js_srcs"], + out_dirs = ["rntester_js_project"], + args = [ + "packages/rn-tester", + "packages/rn-tester/rntester_js_project", + ], + tags = ["manual"], + tool = "//tools/bazel/js:copy_tree", +) + # 1. JS bundle (Metro). Entry is the macOS variant of the rn-tester app. js_run_binary( name = "rntester_macos_jsbundle", - srcs = [":js_srcs"], + srcs = [":rntester_js_project"], outs = ["RNTesterApp.macos.jsbundle"], out_dirs = ["RNTesterApp_assets"], env = { "BUNDLE_OUT": "packages/rn-tester/RNTesterApp.macos.jsbundle", "ASSETS_DEST": "packages/rn-tester/RNTesterApp_assets", - "RN_TESTER_ROOT": "packages/rn-tester", + "RN_TESTER_ROOT": "packages/rn-tester/rntester_js_project", }, tags = ["manual"], tool = ":bundler", diff --git a/packages/rn-tester/NativeComponentExample/BUILD.bazel b/packages/rn-tester/NativeComponentExample/BUILD.bazel new file mode 100644 index 000000000000..a92b2ef0c6da --- /dev/null +++ b/packages/rn-tester/NativeComponentExample/BUILD.bazel @@ -0,0 +1,3 @@ +package(default_visibility = ['//visibility:public']) + +exports_files(glob(['**/*'], exclude = ['**/*.bazel'], allow_empty = True)) diff --git a/packages/rn-tester/NativeCxxModuleExample/BUILD.bazel b/packages/rn-tester/NativeCxxModuleExample/BUILD.bazel new file mode 100644 index 000000000000..a92b2ef0c6da --- /dev/null +++ b/packages/rn-tester/NativeCxxModuleExample/BUILD.bazel @@ -0,0 +1,3 @@ +package(default_visibility = ['//visibility:public']) + +exports_files(glob(['**/*'], exclude = ['**/*.bazel'], allow_empty = True)) diff --git a/packages/rn-tester/NativeModuleExample/BUILD.bazel b/packages/rn-tester/NativeModuleExample/BUILD.bazel new file mode 100644 index 000000000000..a92b2ef0c6da --- /dev/null +++ b/packages/rn-tester/NativeModuleExample/BUILD.bazel @@ -0,0 +1,3 @@ +package(default_visibility = ['//visibility:public']) + +exports_files(glob(['**/*'], exclude = ['**/*.bazel'], allow_empty = True)) diff --git a/packages/rn-tester/bazel/bundle.js b/packages/rn-tester/bazel/bundle.js index 4b548edc77bf..b85e6d03f3b4 100644 --- a/packages/rn-tester/bazel/bundle.js +++ b/packages/rn-tester/bazel/bundle.js @@ -17,44 +17,269 @@ 'use strict'; +const fs = require('fs'); +const crypto = require('crypto'); +const Module = require('module'); const path = require('path'); const Metro = require('metro'); const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); +const ALIASES = new Map(); + +const originalResolveFilename = Module._resolveFilename; +Module._resolveFilename = function (request, parent, isMain, options) { + try { + return originalResolveFilename.call(this, request, parent, isMain, options); + } catch (error) { + // Rewrite aliased packages (e.g. `react-native` -> `react-native-macos`), + // including subpaths like `react-native/Libraries/Core/InitializeCore`, so + // Node `require`/`require.resolve` calls (e.g. metro-config's + // getModulesRunBeforeMainModule) resolve against the aliased package. + for (const [name, root] of ALIASES) { + if (request === name || request.startsWith(name + '/')) { + const subpath = request.slice(name.length + 1); + const aliased = subpath ? path.join(root, subpath) : root; + try { + return originalResolveFilename.call( + this, + aliased, + parent, + isMain, + options, + ); + } catch (_) { + // fall through to the aspect-tree fallback below + } + } + } + const aspectPackage = findAspectPackage(request); + if (aspectPackage != null) { + return originalResolveFilename.call( + this, + aspectPackage, + parent, + isMain, + options, + ); + } + throw error; + } +}; + +function findAspectPackage(request) { + const encoded = request.replace('/', '+'); + const aspectRoots = [ + path.resolve('node_modules/.aspect_rules_js'), + path.resolve(__dirname, '../../..', 'node_modules/.aspect_rules_js'), + ]; + for (const aspectRoot of aspectRoots) { + if (!fs.existsSync(aspectRoot)) { + continue; + } + for (const entry of fs.readdirSync(aspectRoot)) { + if (entry.startsWith(encoded + '@')) { + const candidate = path.join(aspectRoot, entry, 'node_modules', request); + if (fs.existsSync(path.join(candidate, 'package.json'))) { + return candidate; + } + } + } + } + return null; +} + +const ASSET_EXTS = new Set(['gif', 'jpeg', 'jpg', 'png', 'webp', 'xml']); + +function resolveFromFileSystem(context, moduleName, platform) { + try { + return context.resolveRequest(context, moduleName, platform); + } catch (error) { + const packageFallback = resolvePackageFromAspectTree(moduleName); + if (packageFallback != null) { + return {type: 'sourceFile', filePath: packageFallback}; + } + const fallback = resolveRelativeFromFileSystem( + context.originModulePath, + moduleName, + platform, + ); + if (fallback != null) { + return fallback; + } + throw error; + } +} + +function resolvePackageFromAspectTree(moduleName) { + if (moduleName.startsWith('.') || path.isAbsolute(moduleName)) { + return null; + } + const parts = moduleName.split('/'); + const packageName = moduleName.startsWith('@') + ? `${parts[0]}/${parts[1]}` + : parts[0]; + const subpath = parts.slice(packageName.startsWith('@') ? 2 : 1).join('/'); + const packageRoot = ALIASES.get(packageName) || findAspectPackage(packageName); + if (packageRoot == null) { + return null; + } + let candidateBase = subpath ? path.join(packageRoot, subpath) : null; + if (candidateBase == null) { + const packageJsonPath = path.join(packageRoot, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + candidateBase = path.join(packageRoot, pkg['react-native'] || pkg.main || 'index'); + } else { + candidateBase = path.join(packageRoot, 'index'); + } + } + for (const candidate of [ + candidateBase, + `${candidateBase}.js`, + path.join(candidateBase, 'index.js'), + ]) { + if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) { + return candidate; + } + } + return null; +} + +function resolveRelativeFromFileSystem(originModulePath, moduleName, platform) { + if (!moduleName.startsWith('.') && !path.isAbsolute(moduleName)) { + return null; + } + const basePath = path.isAbsolute(moduleName) + ? moduleName + : path.resolve(path.dirname(originModulePath), moduleName); + const sourceExts = [ + `${platform}.js`, + 'native.js', + 'js', + `${platform}.jsx`, + 'native.jsx', + 'jsx', + `${platform}.json`, + 'native.json', + 'json', + `${platform}.ts`, + 'native.ts', + 'ts', + `${platform}.tsx`, + 'native.tsx', + 'tsx', + ]; + for (const candidate of [ + basePath, + ...sourceExts.map(ext => `${basePath}.${ext}`), + ]) { + if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) { + const ext = path.extname(candidate).slice(1); + if (ASSET_EXTS.has(ext)) { + return resolveAssetFiles(candidate); + } + return {type: 'sourceFile', filePath: candidate}; + } + } + // Directory imports: `./foo` -> `./foo/index.` (Node/Metro directory index). + if (fs.existsSync(basePath) && fs.statSync(basePath).isDirectory()) { + for (const ext of sourceExts) { + const indexCandidate = path.join(basePath, `index.${ext}`); + if (fs.existsSync(indexCandidate) && fs.statSync(indexCandidate).isFile()) { + return {type: 'sourceFile', filePath: indexCandidate}; + } + } + } + const baseExt = path.extname(basePath).slice(1); + if (ASSET_EXTS.has(baseExt)) { + return resolveAssetFiles(basePath); + } + return null; +} + +function resolveAssetFiles(filePath) { + const dir = path.dirname(filePath); + const ext = path.extname(filePath); + const basename = path.basename(filePath, ext).replace(/@\d+(?:\.\d+)?x$/, ''); + const escaped = basename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const assetPattern = new RegExp( + `^${escaped}(?:@\\d+(?:\\.\\d+)?x)?${ext.replace('.', '\\.')}$`, + ); + const filePaths = fs + .readdirSync(dir) + .filter(name => assetPattern.test(name)) + .map(name => path.join(dir, name)); + return filePaths.length ? {type: 'assetFiles', filePaths} : null; +} + +const DependencyGraph = require('metro/src/node-haste/DependencyGraph'); +const originalGetOrComputeSha1 = DependencyGraph.prototype.getOrComputeSha1; +DependencyGraph.prototype.getOrComputeSha1 = async function (filename) { + try { + return await originalGetOrComputeSha1.call(this, filename); + } catch (error) { + if ( + error != null && + typeof error.message === 'string' && + error.message.includes('Failed to get the SHA-1 for:') && + fs.existsSync(filename) + ) { + const content = await fs.promises.readFile(filename); + return { + content, + sha1: crypto.createHash('sha1').update(content).digest('hex'), + }; + } + throw error; + } +}; + async function main() { - const projectRoot = path.resolve(process.env.RN_TESTER_ROOT || process.cwd()); + const realpath = fsPath => fs.realpathSync.native(path.resolve(fsPath)); + const projectRoot = realpath(process.env.RN_TESTER_ROOT || process.cwd()); const out = path.resolve(process.env.BUNDLE_OUT || 'RNTesterApp.macos.jsbundle'); const assetsDest = process.env.ASSETS_DEST ? path.resolve(process.env.ASSETS_DEST) : undefined; - const entryFile = path.join(projectRoot, 'js/RNTesterApp.macos.js'); + const entryFile = realpath(path.join(projectRoot, 'js/RNTesterApp.macos.js')); // react-native-macos is published/consumed as `react-native`. - const reactNativePath = path.dirname( - require.resolve('react-native-macos/package.json'), + const reactNativePath = realpath( + path.dirname(require.resolve('react-native-macos/package.json')), ); + ALIASES.set('react-native', reactNativePath); + const reactNativeRoot = realpath(path.dirname(reactNativePath)); const baseConfig = await getDefaultConfig(projectRoot); const config = mergeConfig(baseConfig, { + cacheStores: [], + maxWorkers: 1, projectRoot, - watchFolders: [reactNativePath, path.dirname(reactNativePath)], + resetCache: true, + useWatchman: false, + watchFolders: [projectRoot, reactNativePath, reactNativeRoot], resolver: { + blockList: [], extraNodeModules: {'react-native': reactNativePath}, platforms: ['ios', 'macos', 'android'], - // Watchman isn't available (or usable) inside the Bazel sandbox; use Metro's - // Node crawler so the file map is populated. rules_js exposes sources as symlinks, - // so the crawler must follow them. + resolveRequest: resolveFromFileSystem, + // Watchman isn't available (or usable) inside the Bazel sandbox; use + // Metro's Node crawler. Realpathing the roots keeps the crawler's SHA-1 + // map keys aligned with the entry file. useWatchman: false, unstable_enableSymlinks: true, }, }); + // Use `bundleOut` (verbatim) rather than `out`; Metro's runBuild rewrites + // `out` through `.replace(/(\.js)?$/, '.js')`, which would turn our declared + // Bazel output `RNTesterApp.macos.jsbundle` into `...jsbundle.js`. await Metro.runBuild(config, { entry: entryFile, platform: 'macos', dev: false, minify: true, - out, + bundleOut: out, assets: Boolean(assetsDest), assetsDest, }); diff --git a/tools/bazel/js/BUILD.bazel b/tools/bazel/js/BUILD.bazel index a6f700ee3457..b9b9b405f975 100644 --- a/tools/bazel/js/BUILD.bazel +++ b/tools/bazel/js/BUILD.bazel @@ -1,4 +1,29 @@ # Bazel macros for building React Native JavaScript (Metro). See metro.bzl. -# Package marker so `//tools/bazel/js:metro.bzl` is loadable. -exports_files(["metro.bzl"]) +load("@aspect_rules_js//js:defs.bzl", "js_binary") + +exports_files([ + "first_party.bzl", + "metro.bzl", +]) + +js_binary( + name = "build_first_party", + data = [ + "//:node_modules/@babel/core", + "//:node_modules/@babel/preset-env", + "//:node_modules/@babel/preset-flow", + "//:node_modules/babel-plugin-minify-dead-code-elimination", + "//:node_modules/babel-plugin-syntax-hermes-parser", + "//:node_modules/babel-plugin-transform-define", + "//:node_modules/prettier", + ], + entry_point = "build_first_party.js", + visibility = ["//visibility:public"], +) + +js_binary( + name = "copy_tree", + entry_point = "copy_tree.js", + visibility = ["//visibility:public"], +) diff --git a/tools/bazel/js/build_first_party.js b/tools/bazel/js/build_first_party.js new file mode 100644 index 000000000000..8ffff6415525 --- /dev/null +++ b/tools/bazel/js/build_first_party.js @@ -0,0 +1,247 @@ +#!/usr/bin/env node +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +const babel = require('@babel/core'); +const fs = require('fs'); +const path = require('path'); +const prettier = require('prettier'); + +const JS_FILES_PATTERN = /\.js$/; +const FLOW_PRAGMA = /@flow/; + +const MONOREPO_BABEL_CONFIG = { + presets: [ + require.resolve('@babel/preset-flow'), + [require.resolve('@babel/preset-env'), {targets: {node: '18'}}], + ], + plugins: [ + require.resolve('babel-plugin-syntax-hermes-parser'), + [ + require.resolve('babel-plugin-transform-define'), + {'process.env.BUILD_EXCLUDE_BABEL_REGISTER': true}, + ], + [ + require.resolve('babel-plugin-minify-dead-code-elimination'), + {keepFnName: true, keepFnArgs: true, keepClassName: true}, + ], + ], +}; + +const CODEGEN_BABEL_CONFIG = MONOREPO_BABEL_CONFIG; + +function usage() { + console.error( + 'Usage: build_first_party.js ', + ); + process.exit(2); +} + +const [, , packageDirArg, buildKind, outDirArg] = process.argv; +if (packageDirArg == null || buildKind == null || outDirArg == null) { + usage(); +} + +const packageDir = path.resolve(packageDirArg); +const outDir = path.resolve(outDirArg); +const prettierConfig = {parser: 'babel'}; + +function mkdirp(dir) { + fs.mkdirSync(dir, {recursive: true}); +} + +function rmrf(target) { + fs.rmSync(target, {recursive: true, force: true}); +} + +function shouldSkipCopy(rel, dirent) { + const parts = rel.split(path.sep); + const outBase = path.basename(outDir); + return ( + parts.includes(outBase) || + parts.includes('pkg') || + rel === 'BUILD.bazel' || + rel.endsWith('.bazel') || + parts.includes('node_modules') || + parts.includes('.build') || + parts.includes('build') || + parts.includes('dist') || + parts.includes('lib') || + parts.includes('__tests__') || + parts.includes('__test_fixtures__') || + parts.includes('__fixtures__') || + (dirent != null && dirent.isDirectory() && dirent.name === 'Pods') + ); +} + +function copyPackageTree(srcRoot, destRoot, rel = '') { + for (const dirent of fs.readdirSync(path.join(srcRoot, rel), { + withFileTypes: true, + })) { + const childRel = path.join(rel, dirent.name); + if (shouldSkipCopy(childRel, dirent)) { + continue; + } + const src = path.join(srcRoot, childRel); + const dest = path.join(destRoot, childRel); + if (dirent.isDirectory()) { + mkdirp(dest); + copyPackageTree(srcRoot, destRoot, childRel); + } else if (dirent.isSymbolicLink()) { + const real = fs.realpathSync.native(src); + fs.copyFileSync(real, dest); + fs.chmodSync(dest, 0o644); + } else if (dirent.isFile()) { + mkdirp(path.dirname(dest)); + fs.copyFileSync(src, dest); + fs.chmodSync(dest, 0o644); + } + } +} + +function listFiles(root, rel = '') { + const result = []; + if (!fs.existsSync(path.join(root, rel))) { + return result; + } + for (const dirent of fs.readdirSync(path.join(root, rel), { + withFileTypes: true, + })) { + const childRel = path.join(rel, dirent.name); + const full = path.join(root, childRel); + if (dirent.isDirectory()) { + result.push(...listFiles(root, childRel)); + } else if (dirent.isFile()) { + result.push(full); + } + } + return result; +} + +function transformFile(src, dest, babelConfig) { + mkdirp(path.dirname(dest)); + const transformed = babel.transformFileSync(src, babelConfig).code; + fs.writeFileSync(dest, prettier.format(transformed, prettierConfig)); + const source = fs.readFileSync(src, 'utf8'); + if (FLOW_PRAGMA.test(source)) { + fs.copyFileSync(src, dest + '.flow'); + } +} + +function copyOrTransform(src, dest, babelConfig) { + if (!JS_FILES_PATTERN.test(src)) { + mkdirp(path.dirname(dest)); + fs.copyFileSync(src, dest); + fs.chmodSync(dest, 0o644); + return; + } + transformFile(src, dest, babelConfig); +} + +function rewriteExportsTarget(target, buildDir) { + return target.replace('./src/', './' + buildDir + '/'); +} + +function rewriteExportsField(exportsField, buildDir) { + if (typeof exportsField === 'string') { + return rewriteExportsTarget(exportsField, buildDir); + } + if (exportsField == null || typeof exportsField !== 'object') { + return exportsField; + } + const rewritten = Array.isArray(exportsField) ? [] : {}; + for (const key of Object.keys(exportsField)) { + rewritten[key] = rewriteExportsField(exportsField[key], buildDir); + } + return rewritten; +} + +function collectExportTargets(exportsField, targets = []) { + if (typeof exportsField === 'string') { + targets.push(exportsField); + } else if (exportsField != null && typeof exportsField === 'object') { + for (const value of Object.values(exportsField)) { + collectExportTargets(value, targets); + } + } + return targets; +} + +function buildPathFor(srcFile, srcDir, buildDir) { + const rel = path.relative(srcDir, srcFile).replace(/\.flow\.js$/, '.js'); + return path.join(buildDir, rel); +} + +function buildMonorepoPackage() { + const pkgPath = path.join(outDir, 'package.json'); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); + const srcDir = path.join(outDir, 'src'); + const distDir = path.join(outDir, 'dist'); + const entryPoints = new Set(); + const wrappers = new Set(); + + for (const target of collectExportTargets(pkg.exports)) { + if (!target.endsWith('.js') || target.includes('*')) { + continue; + } + const original = target.replace('./dist/', './src/'); + const wrapper = path.join(outDir, original); + const flowEntry = wrapper.replace(/\.js$/, '.flow.js'); + if (fs.existsSync(wrapper) && fs.existsSync(flowEntry)) { + wrappers.add(path.normalize(wrapper)); + entryPoints.add(path.normalize(flowEntry)); + } + } + + for (const file of listFiles(srcDir)) { + const normalized = path.normalize(file); + if (wrappers.has(normalized) || entryPoints.has(normalized)) { + continue; + } + copyOrTransform(file, buildPathFor(file, srcDir, distDir), MONOREPO_BABEL_CONFIG); + } + + for (const entryPoint of entryPoints) { + copyOrTransform( + entryPoint, + buildPathFor(entryPoint, srcDir, distDir), + MONOREPO_BABEL_CONFIG, + ); + } + + if (pkg.exports != null) { + pkg.exports = rewriteExportsField(pkg.exports, 'dist'); + } + if (pkg.main != null) { + pkg.main = rewriteExportsTarget(pkg.main, 'dist'); + } + fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); +} + +function buildCodegenPackage() { + const srcDir = path.join(outDir, 'src'); + const libDir = path.join(outDir, 'lib'); + for (const file of listFiles(srcDir)) { + copyOrTransform(file, buildPathFor(file, srcDir, libDir), CODEGEN_BABEL_CONFIG); + } +} + +rmrf(outDir); +mkdirp(outDir); +copyPackageTree(packageDir, outDir); + +if (buildKind === 'monorepo') { + buildMonorepoPackage(); +} else if (buildKind === 'codegen') { + buildCodegenPackage(); +} else { + usage(); +} diff --git a/tools/bazel/js/copy_tree.js b/tools/bazel/js/copy_tree.js new file mode 100644 index 000000000000..f09c146cdd63 --- /dev/null +++ b/tools/bazel/js/copy_tree.js @@ -0,0 +1,76 @@ +#!/usr/bin/env node +/** + * @format + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const [, , srcArg, outArg] = process.argv; +if (srcArg == null || outArg == null) { + console.error('Usage: copy_tree.js '); + process.exit(2); +} + +const srcRoot = path.resolve(srcArg); +const outRoot = path.resolve(outArg); +const outBase = path.basename(outRoot); + +function mkdirp(dir) { + fs.mkdirSync(dir, {recursive: true}); +} + +function copyDir(rel = '') { + for (const dirent of fs.readdirSync(path.join(srcRoot, rel), { + withFileTypes: true, + })) { + const childRel = path.join(rel, dirent.name); + const parts = childRel.split(path.sep); + if ( + parts.includes(outBase) || + parts.includes('node_modules') || + parts.includes('Pods') || + parts.includes('build') + ) { + continue; + } + const src = path.join(srcRoot, childRel); + const dest = path.join(outRoot, childRel); + if (dirent.isDirectory()) { + mkdirp(dest); + copyDir(childRel); + } else if (dirent.isFile() || dirent.isSymbolicLink()) { + mkdirp(path.dirname(dest)); + fs.copyFileSync(dirent.isSymbolicLink() ? fs.realpathSync.native(src) : src, dest); + fs.chmodSync(dest, 0o644); + } + } +} + +fs.rmSync(outRoot, {recursive: true, force: true}); +mkdirp(outRoot); +copyDir(); + +if (srcArg.replace(/\\/g, '/').endsWith('packages/rn-tester')) { + writeStub( + 'NativeComponentExample/js/MyNativeView.js', + "import {View} from 'react-native';\nexport default View;\n", + ); + writeStub( + 'NativeModuleExample/NativeScreenshotManager.js', + "module.exports = {takeScreenshot: () => Promise.resolve(null)};\n", + ); + writeStub( + 'NativeCxxModuleExample/NativeCxxModuleExample.js', + "export const EnumInt = {IA: 23, IB: 42};\nexport const EnumNone = {NA: 0, NB: 1};\nexport const EnumStr = {SA: 's---a', SB: 's---b'};\nexport default null;\n", + ); +} + +function writeStub(rel, content) { + const dest = path.join(outRoot, rel); + mkdirp(path.dirname(dest)); + fs.writeFileSync(dest, content); + fs.chmodSync(dest, 0o644); +} diff --git a/tools/bazel/js/first_party.bzl b/tools/bazel/js/first_party.bzl new file mode 100644 index 000000000000..fc1896818ed4 --- /dev/null +++ b/tools/bazel/js/first_party.bzl @@ -0,0 +1,42 @@ +"""Helpers for exposing built first-party JS workspace packages to rules_js.""" + +load("@aspect_rules_js//js:defs.bzl", "js_run_binary") +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +_EXCLUDES = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/dist/**", + "**/lib/**", + "**/Pods/**", + "**/__tests__/**", + "**/__test_fixtures__/**", + "**/__fixtures__/**", + "**/*.bazel", +] + +def first_party_js_package(name = "pkg", build_kind = "monorepo", visibility = None): + """Build a first-party package before linking it into Bazel node_modules.""" + npm_link_all_packages() + + built_dir = name + "_built" + js_run_binary( + name = name + "_build", + tool = "//tools/bazel/js:build_first_party", + srcs = native.glob(["**/*"], exclude = _EXCLUDES, allow_empty = True), + args = [ + native.package_name(), + build_kind, + native.package_name() + "/" + built_dir, + ], + out_dirs = [built_dir], + ) + + npm_package( + name = name, + srcs = [":" + name + "_build"], + root_paths = [native.package_name() + "/" + built_dir], + visibility = visibility, + ) diff --git a/tools/bazel/react_native/BUILD.bazel b/tools/bazel/react_native/BUILD.bazel new file mode 100644 index 000000000000..45472a715280 --- /dev/null +++ b/tools/bazel/react_native/BUILD.bazel @@ -0,0 +1,91 @@ +load('@aspect_rules_js//js:defs.bzl', 'js_binary', 'js_run_binary') +load('//tools/bazel/react_native:defs.bzl', 'rn_codegen') + +package(default_visibility = ['//visibility:public']) + + +js_binary( + name = 'build_codegen_lib_bin', + data = [ + '//packages/react-native-codegen:node_modules', + '//packages/react-native-codegen:node_modules/@babel/core', + '//packages/react-native-codegen:node_modules/@babel/plugin-syntax-dynamic-import', + '//packages/react-native-codegen:node_modules/@babel/plugin-transform-class-properties', + '//packages/react-native-codegen:node_modules/@babel/plugin-transform-flow-strip-types', + '//packages/react-native-codegen:node_modules/@babel/plugin-transform-nullish-coalescing-operator', + '//packages/react-native-codegen:node_modules/@babel/plugin-transform-optional-chaining', + '//packages/react-native-codegen:node_modules/hermes-estree', + '//packages/react-native-codegen:node_modules/hermes-parser', + '//packages/react-native-codegen:node_modules/invariant', + '//packages/react-native-codegen:node_modules/nullthrows', + '//packages/react-native-codegen:node_modules/glob', + '//packages/react-native-codegen:node_modules/micromatch', + '//packages/react-native-codegen:node_modules/prettier', + '//packages/react-native-codegen:node_modules/yargs', + ], + entry_point = 'build_codegen_lib.js', +) + +js_run_binary( + name = 'codegen_lib', + srcs = ['//packages/react-native-codegen:pkg'], + out_dirs = ['codegen_lib'], + env = { + 'OUTPUT_DIR': '$(RULEDIR)/codegen_lib', + }, + tool = ':build_codegen_lib_bin', +) + +js_binary( + name = 'codegen_runner_bin', + data = [ + '//packages/react-native:node_modules', + '//packages/react-native:pkg', + ], + entry_point = 'codegen_runner.js', +) + + +genrule( + name = 'copy_my_legacy_view_native_component', + srcs = ['//packages/rn-tester/NativeComponentExample:js/MyLegacyViewNativeComponent.js'], + outs = ['rn_tester_inputs/NativeComponentExample/js/MyLegacyViewNativeComponent.js'], + cmd = 'cp $(location //packages/rn-tester/NativeComponentExample:js/MyLegacyViewNativeComponent.js) $@', +) + +genrule( + name = 'copy_my_native_view_native_component', + srcs = ['//packages/rn-tester/NativeComponentExample:js/MyNativeViewNativeComponent.js'], + outs = ['rn_tester_inputs/NativeComponentExample/js/MyNativeViewNativeComponent.js'], + cmd = 'cp $(location //packages/rn-tester/NativeComponentExample:js/MyNativeViewNativeComponent.js) $@', +) + +genrule( + name = 'copy_native_cxx_module_example', + srcs = ['//packages/rn-tester/NativeCxxModuleExample:NativeCxxModuleExample.js'], + outs = ['rn_tester_inputs/NativeCxxModuleExample/NativeCxxModuleExample.js'], + cmd = 'cp $(location //packages/rn-tester/NativeCxxModuleExample:NativeCxxModuleExample.js) $@', +) + +genrule( + name = 'copy_native_screenshot_manager', + srcs = ['//packages/rn-tester/NativeModuleExample:NativeScreenshotManager.js'], + outs = ['rn_tester_inputs/NativeModuleExample/NativeScreenshotManager.js'], + cmd = 'cp $(location //packages/rn-tester/NativeModuleExample:NativeScreenshotManager.js) $@', +) + +filegroup( + name = 'rn_tester_codegen_inputs', + srcs = [ + ':copy_my_legacy_view_native_component', + ':copy_my_native_view_native_component', + ':copy_native_cxx_module_example', + ':copy_native_screenshot_manager', + ], +) + +rn_codegen( + name = 'rn_tester_appspecs', + project_root = 'packages/rn-tester', + rn_tester_srcs = [':rn_tester_codegen_inputs'], +) diff --git a/tools/bazel/react_native/build_codegen_lib.js b/tools/bazel/react_native/build_codegen_lib.js new file mode 100644 index 000000000000..a5d3f1656c36 --- /dev/null +++ b/tools/bazel/react_native/build_codegen_lib.js @@ -0,0 +1,169 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const Module = require('module'); +const path = require('path'); + +const cwd = process.cwd(); +const marker = `${path.sep}bazel-out${path.sep}`; +const workspaceRoot = + process.env.BUILD_WORKSPACE_DIRECTORY || + (cwd.includes(marker) ? cwd.slice(0, cwd.indexOf(marker)) : cwd); +const bazelPackageDir = path.join( + workspaceRoot, + process.env.BAZEL_BINDIR || '', + 'packages/react-native-codegen/pkg', +); +const packageDir = fs.existsSync(path.join(bazelPackageDir, 'src')) + ? bazelPackageDir + : path.join(workspaceRoot, 'packages/react-native-codegen'); +const bazelPackageNodeModules = path.join( + workspaceRoot, + process.env.BAZEL_BINDIR || '', + 'packages/react-native-codegen/node_modules', +); +const nodeModulesDir = fs.existsSync(bazelPackageNodeModules) + ? bazelPackageNodeModules + : path.join(packageDir, 'node_modules'); +const aspectStoreDir = path.join( + workspaceRoot, + process.env.BAZEL_BINDIR || '', + 'node_modules/.aspect_rules_js', +); +const aspectNodeModulePaths = fs.existsSync(aspectStoreDir) + ? fs + .readdirSync(aspectStoreDir) + .map(entry => path.join(aspectStoreDir, entry, 'node_modules')) + .filter(entry => fs.existsSync(entry)) + : []; +process.env.NODE_PATH = [ + ...aspectNodeModulePaths, + process.env.NODE_PATH || '', +] + .filter(Boolean) + .join(path.delimiter); +Module._initPaths(); +const moduleSearchPaths = [ + nodeModulesDir, + path.join(workspaceRoot, 'node_modules'), + packageDir, + ...aspectNodeModulePaths, +]; +const resolveFromCodegen = moduleName => + require.resolve(moduleName, {paths: moduleSearchPaths}); +const requireFromCodegen = moduleName => require(resolveFromCodegen(moduleName)); +const babel = requireFromCodegen('@babel/core'); +const glob = requireFromCodegen('glob'); +const micromatch = requireFromCodegen('micromatch'); +const prettier = requireFromCodegen('prettier'); +const srcDir = path.join(packageDir, 'src'); +const outputDir = path.resolve(workspaceRoot, process.env.OUTPUT_DIR || 'tools/bazel/react_native/codegen_lib'); +const prettierConfig = { + arrowParens: 'avoid', + bracketSameLine: true, + bracketSpacing: false, + requirePragma: true, + singleQuote: true, + trailingComma: 'all', +}; +const babelPlugins = [ + '@babel/plugin-transform-flow-strip-types', + '@babel/plugin-syntax-dynamic-import', + '@babel/plugin-transform-class-properties', + '@babel/plugin-transform-nullish-coalescing-operator', + '@babel/plugin-transform-optional-chaining', +].map(plugin => resolveFromCodegen(plugin)); + +fs.rmSync(outputDir, {recursive: true, force: true}); +fs.mkdirSync(outputDir, {recursive: true}); + +function outputPathFor(file) { + return path.join(outputDir, path.relative(srcDir, file)); +} + +function copyFile(from, to) { + fs.mkdirSync(path.dirname(to), {recursive: true}); + fs.copyFileSync(from, to); +} + +const inputFiles = glob.sync(path.join(srcDir, '**/*'), {nodir: true, dot: true}); +if (inputFiles.length === 0) { + throw new Error(`No react-native-codegen inputs found under ${srcDir}; packageDir exists=${fs.existsSync(packageDir)} src exists=${fs.existsSync(srcDir)}`); +} +for (const file of inputFiles) { + const relative = path.relative(srcDir, file); + if (micromatch.isMatch(relative, '**/(__tests__|__test_fixtures__)/**')) { + continue; + } + + const dest = outputPathFor(file); + if (!micromatch.isMatch(relative, '**/*.js')) { + copyFile(file, dest); + continue; + } + + const transformed = babel.transformFileSync(file, { + cwd: packageDir, + root: packageDir, + babelrc: false, + configFile: false, + plugins: babelPlugins, + }).code; + const formatted = prettier.format(transformed, { + ...prettierConfig, + parser: 'babel', + }); + fs.mkdirSync(path.dirname(dest), {recursive: true}); + fs.writeFileSync(dest, formatted); + + const source = fs.readFileSync(file, 'utf8'); + if (/@flow/.test(source)) { + fs.writeFileSync(dest + '.flow', source); + } +} + + +function copyRuntimePackage(packageName, seen = new Set()) { + if (seen.has(packageName)) { + return; + } + seen.add(packageName); + let packageJsonPath; + try { + packageJsonPath = resolveFromCodegen(path.join(packageName, 'package.json')); + } catch { + return; + } + const packagePath = path.dirname(packageJsonPath); + const destinationPath = path.join(outputDir, 'node_modules', packageName); + fs.rmSync(destinationPath, {recursive: true, force: true}); + fs.mkdirSync(path.dirname(destinationPath), {recursive: true}); + fs.cpSync(packagePath, destinationPath, { + recursive: true, + dereference: true, + filter: src => !src.includes(`${path.sep}.cache${path.sep}`), + }); + + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + for (const dependency of Object.keys(packageJson.dependencies || {})) { + copyRuntimePackage(dependency, seen); + } +} + +for (const runtimePackage of [ + '@babel/core', + '@babel/parser', + 'balanced-match', + 'concat-map', + 'glob', + 'hermes-estree', + 'hermes-parser', + 'invariant', + 'nullthrows', + 'yargs', +]) { + copyRuntimePackage(runtimePackage); +} + +console.log(`Built react-native-codegen lib at ${outputDir}`); diff --git a/tools/bazel/react_native/codegen_runner.js b/tools/bazel/react_native/codegen_runner.js new file mode 100644 index 000000000000..bd90acbc1c80 --- /dev/null +++ b/tools/bazel/react_native/codegen_runner.js @@ -0,0 +1,181 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const cwd = process.cwd(); +const marker = `${path.sep}bazel-out${path.sep}`; +const workspaceRoot = cwd.includes(marker) ? cwd.slice(0, cwd.indexOf(marker)) : cwd; +const requestedProjectRoot = path.resolve(workspaceRoot, mustEnv('PROJECT_ROOT')); +const outputDir = path.resolve(workspaceRoot, mustEnv('OUTPUT_DIR')); +const targetPlatform = process.env.TARGET_PLATFORM || 'ios'; +const source = process.env.SOURCE || 'app'; +const codegenLibDir = path.resolve(workspaceRoot, mustEnv('CODEGEN_LIB_DIR')); +const bazelReactNativePackageRoot = path.join( + workspaceRoot, + process.env.BAZEL_BINDIR || '', + 'packages/react-native/pkg', +); +const reactNativePackageRoot = fs.existsSync(bazelReactNativePackageRoot) + ? bazelReactNativePackageRoot + : path.join(workspaceRoot, 'packages/react-native'); + +function prepareRnTesterProject() { + const scratchProjectRoot = path.join(outputDir, '_rn_tester_project'); + fs.rmSync(scratchProjectRoot, {recursive: true, force: true}); + fs.mkdirSync(scratchProjectRoot, {recursive: true}); + + fs.writeFileSync( + path.join(scratchProjectRoot, 'package.json'), + JSON.stringify( + { + name: '@react-native/tester', + version: '0.81.0-main', + private: true, + dependencies: {}, + devDependencies: {}, + peerDependencies: {}, + codegenConfig: { + name: 'AppSpecs', + type: 'all', + jsSrcsDir: '.', + android: { + javaPackageName: 'com.facebook.fbreact.specs', + }, + ios: { + modules: { + SampleTurboModule: { + unstableRequiresMainQueueSetup: true, + }, + }, + components: { + RNTMyNativeView: { + className: 'RNTMyNativeViewComponentView', + }, + }, + }, + }, + }, + null, + 2, + ), + ); + + const generatedInputsRoot = path.join( + workspaceRoot, + process.env.BAZEL_BINDIR || '', + 'tools/bazel/react_native/rn_tester_inputs', + ); + const sourceRoot = fs.existsSync(generatedInputsRoot) + ? generatedInputsRoot + : requestedProjectRoot; + + for (const relativePath of [ + 'NativeComponentExample/js/MyLegacyViewNativeComponent.js', + 'NativeComponentExample/js/MyNativeViewNativeComponent.js', + 'NativeCxxModuleExample/NativeCxxModuleExample.js', + 'NativeModuleExample/NativeScreenshotManager.js', + ]) { + const from = path.join(sourceRoot, relativePath); + const to = path.join(scratchProjectRoot, relativePath); + fs.mkdirSync(path.dirname(to), {recursive: true}); + fs.copyFileSync(from, to); + } + + return scratchProjectRoot; +} + +function mustEnv(name) { + const value = process.env[name]; + if (!value) { + throw new Error(`${name} must be set`); + } + return value; +} + +function assertExists(relativePath) { + const absolutePath = path.join(outputDir, relativePath); + if (!fs.existsSync(absolutePath)) { + throw new Error(`Expected codegen output missing: ${absolutePath}`); + } +} + +fs.mkdirSync(outputDir, {recursive: true}); +const scratchDir = path.join(outputDir, '_codegen_scratch'); +fs.rmSync(scratchDir, {recursive: true, force: true}); +fs.mkdirSync(scratchDir, {recursive: true}); +process.env.TMPDIR = scratchDir; + +const Module = require('module'); +process.env.NODE_PATH = [ + path.join(codegenLibDir, 'node_modules'), + process.env.NODE_PATH || '', +] + .filter(Boolean) + .join(path.delimiter); +Module._initPaths(); +const originalResolveFilename = Module._resolveFilename; +Module._resolveFilename = function (request, parent, isMain, options) { + if (request.startsWith('@react-native/codegen/lib/')) { + const resolved = path.join(codegenLibDir, request.slice('@react-native/codegen/lib/'.length)); + return path.extname(resolved) ? resolved : `${resolved}.js`; + } + return originalResolveFilename.call(this, request, parent, isMain, options); +}; + +const scratchCodegenRepo = path.join(outputDir, '_codegen_repo'); +let projectRoot; + +try { + projectRoot = prepareRnTesterProject(); + const libPath = path.join(scratchCodegenRepo, 'lib'); + fs.rmSync(scratchCodegenRepo, {recursive: true, force: true}); + fs.mkdirSync(scratchCodegenRepo, {recursive: true}); + fs.symlinkSync(codegenLibDir, libPath, 'dir'); + + const constantsPath = path.join( + reactNativePackageRoot, + 'scripts/codegen/generate-artifacts-executor/constants.js', + ); + const constants = require(constantsPath); + constants.CODEGEN_REPO_PATH = scratchCodegenRepo; + constants.CORE_LIBRARIES_WITH_OUTPUT_FOLDER.FBReactNativeSpec.ios = path.join( + scratchDir, + 'FBReactNativeSpec', + ); + + const executor = require(path.join( + reactNativePackageRoot, + 'scripts/codegen/generate-artifacts-executor', + )); + executor.execute(projectRoot, targetPlatform, outputDir, source); + + if (process.exitCode) { + process.exit(process.exitCode); + } + + [ + 'build/generated/ios/AppSpecs/AppSpecs.h', + 'build/generated/ios/AppSpecs/AppSpecs-generated.mm', + 'build/generated/ios/AppSpecsJSI.h', + 'build/generated/ios/AppSpecsJSI-generated.cpp', + 'build/generated/ios/RCTAppDependencyProvider.h', + 'build/generated/ios/RCTAppDependencyProvider.mm', + 'build/generated/ios/RCTThirdPartyComponentsProvider.h', + 'build/generated/ios/RCTThirdPartyComponentsProvider.mm', + 'build/generated/ios/RCTModuleProviders.h', + 'build/generated/ios/RCTModuleProviders.mm', + 'build/generated/ios/RCTModulesConformingToProtocolsProvider.h', + 'build/generated/ios/RCTModulesConformingToProtocolsProvider.mm', + 'build/generated/ios/RCTUnstableModulesRequiringMainQueueSetupProvider.h', + 'build/generated/ios/RCTUnstableModulesRequiringMainQueueSetupProvider.mm', + 'build/generated/ios/react/renderer/components/AppSpecs/ComponentDescriptors.h', + ].forEach(assertExists); +} finally { + fs.rmSync(scratchDir, {recursive: true, force: true}); + if (projectRoot) { + fs.rmSync(projectRoot, {recursive: true, force: true}); + } + fs.rmSync(scratchCodegenRepo, {recursive: true, force: true}); +} diff --git a/tools/bazel/react_native/defs.bzl b/tools/bazel/react_native/defs.bzl new file mode 100644 index 000000000000..2fcb6b13de01 --- /dev/null +++ b/tools/bazel/react_native/defs.bzl @@ -0,0 +1,37 @@ +load('@aspect_rules_js//js:defs.bzl', 'js_run_binary') + +_CODEGEN_OUTS = [ + 'build/generated/ios/AppSpecs/AppSpecs-generated.mm', + 'build/generated/ios/AppSpecs/AppSpecs.h', + 'build/generated/ios/AppSpecsJSI-generated.cpp', + 'build/generated/ios/AppSpecsJSI.h', + 'build/generated/ios/RCTAppDependencyProvider.h', + 'build/generated/ios/RCTAppDependencyProvider.mm', + 'build/generated/ios/RCTModuleProviders.h', + 'build/generated/ios/RCTModuleProviders.mm', + 'build/generated/ios/RCTModulesConformingToProtocolsProvider.h', + 'build/generated/ios/RCTModulesConformingToProtocolsProvider.mm', + 'build/generated/ios/RCTThirdPartyComponentsProvider.h', + 'build/generated/ios/RCTThirdPartyComponentsProvider.mm', + 'build/generated/ios/RCTUnstableModulesRequiringMainQueueSetupProvider.h', + 'build/generated/ios/RCTUnstableModulesRequiringMainQueueSetupProvider.mm', + 'build/generated/ios/ReactAppDependencyProvider.podspec', + 'build/generated/ios/ReactCodegen.podspec', +] + +def rn_codegen(name, project_root, rn_tester_srcs, **kwargs): + js_run_binary( + name = name, + srcs = rn_tester_srcs + ['//tools/bazel/react_native:codegen_lib'], + outs = _CODEGEN_OUTS, + out_dirs = ['build/generated/ios/react/renderer/components/AppSpecs'], + env = { + 'PROJECT_ROOT': project_root, + 'OUTPUT_DIR': '$(RULEDIR)', + 'TARGET_PLATFORM': 'ios', + 'SOURCE': 'app', + 'CODEGEN_LIB_DIR': '$(location //tools/bazel/react_native:codegen_lib)', + }, + tool = '//tools/bazel/react_native:codegen_runner_bin', + **kwargs + ) From 877a0adbd9c6e3dd79c2705d3c92bb2fff058070 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 7 Jul 2026 07:03:21 -0700 Subject: [PATCH 12/17] Bazel: build RNTester.app on Xcode 26 (native host + XCFramework link) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//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 / 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 (, , , ) 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 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> --- .bazelrc | 4 + docs/bazel.md | 155 +++++++------- packages/react-native/BUILD.bazel | 43 ++++ packages/rn-tester/BUILD.bazel | 50 +++-- packages/rn-tester/bazel/MinimalAppDelegate.h | 22 ++ .../rn-tester/bazel/MinimalAppDelegate.mm | 34 +++ packages/rn-tester/bazel/minimal_main.m | 23 +++ tools/bazel/apple/prebuilt_xcframeworks.bzl | 45 ++++ .../bazel/apple/reconstruct_react_headers.py | 194 ++++++++++++++++++ 9 files changed, 477 insertions(+), 93 deletions(-) create mode 100644 packages/rn-tester/bazel/MinimalAppDelegate.h create mode 100644 packages/rn-tester/bazel/MinimalAppDelegate.mm create mode 100644 packages/rn-tester/bazel/minimal_main.m create mode 100644 tools/bazel/apple/reconstruct_react_headers.py diff --git a/.bazelrc b/.bazelrc index 88eeec108641..5cb250e37101 100644 --- a/.bazelrc +++ b/.bazelrc @@ -14,6 +14,10 @@ build --show_result=1 # Apple: build macOS slices. Scoped in practice to the app target's transitions. build:macos --apple_platform_type=macos +# React Native's new architecture C++ (Fabric/TurboModules) requires C++20. +# This only affects C++/Obj-C++ compiles; the JS (rules_js) targets are unaffected. +build --cxxopt=-std=c++20 + # CI convenience config. build:ci --color=yes build:ci --show_timestamps diff --git a/docs/bazel.md b/docs/bazel.md index aec3785e2905..e26fd3b08b2f 100644 --- a/docs/bazel.md +++ b/docs/bazel.md @@ -84,82 +84,95 @@ BYONM remains a viable fallback. ## What works today (verified green) +**`bazel build //packages/rn-tester:RNTesterMacBazel` produces a launchable macOS +RNTester `.app`, built entirely by Bazel on Xcode 26** — JS bundle, native host, and +prebuilt-XCFramework link. Specifically: + * `bazel test //tools/bazel/berry/example:verify` — a self-contained proof: a real Berry `yarn.lock` (is-odd → is-number) is translated by the fork, fetched and linked by rules_js, and a Node program that `require`s the npm dep runs green. * On the **real monorepo** `yarn.lock`, `npm_translate_lock` parses the fork-generated lock and **fetches all 1333 packages** successfully. -* **First-party workspace linking**: every workspace package now exposes a `:pkg` target - (`npm_package`) so rules_js can link it. `bazel build //packages/react-native:pkg` - builds; rn-tester's `node_modules` links `react-native-macos` (the package the repo - consumes as `react-native`). -* **rn-tester's macOS JS bundle builds** (outside Bazel, proving the JS is bundleable): - after building the codegen lib, `react-native bundle --platform macos --entry-file - js/RNTesterApp.macos.js` produces a ~7 MB `RNTesterApp.macos.jsbundle` + 50 assets. - -## End-to-end RN-Tester: status & remaining work - -Two concrete blockers separate the current state from a running RNTester macOS app: - -1. **The macOS app needs the prebuilt XCFrameworks. These now build on Xcode 26 with the - fixes in this branch** (verified end to end). This was never a fork gap — - react-native-macos already carries the Hermes version-resolution patches - (`scripts/ios-prebuild/microsoft-hermes.js`): a release branch downloads a published - Hermes; `main` (`1000.0.0`) builds Hermes from source at the merge-base commit. - - Getting the from-source build working on **Xcode 26** required two fixes (both in this - branch) plus one environment note: - * **Host `hermesc` mis-targeted to visionOS.** `ios-prebuild` sets - `XROS_DEPLOYMENT_TARGET` for the cross-platform builds and it leaked into the *native* - `build_host_hermesc`; Xcode 26's clang honors it and built the host tools for visionOS - ("using sysroot for 'MacOSX' but targeting 'XR'"), failing llvh's `CheckAtomic`. Fixed - in `sdks/hermes-engine/utils/build-apple-framework.sh` (force a macOS target for the - host tools). - * **Upstream Hermes Mac Catalyst triple.** Hermes hardcodes an invalid universal - Catalyst triple (`-target x86_64-arm64-apple-ios…-macabi`) that Xcode 26's clang - rejects. This is a Hermes-upstream issue and Catalyst isn't needed for a macOS app, so - `build-ios-framework.sh` now accepts `HERMES_APPLE_PLATFORMS` to build a subset (e.g. - `macosx`). - * Use **CMake 3.x** (`cmake@3.26.4`); Hermes doesn't configure under the Homebrew-default - CMake 4.x. - - Verified on Xcode 26.4 with `HERMES_APPLE_PLATFORMS=macosx` and `cmake@3.26.4`: - `ios-prebuild.js -s/-b/-c` produces all three XCFrameworks — - `hermes.xcframework` (from source), `ReactNativeDependencies.xcframework` (downloaded - 0.86.0), and `React.xcframework` (`** BUILD SUCCEEDED **`, macOS slice). What remains is - wiring those into the Bazel `macos_application` (rules_apple on Xcode 26) and embedding - the JS bundle. - -2. **The hermetic Bazel Metro bundle** is close but not yet green. Progress + remaining: - * `@react-native/codegen`'s `lib/` must be built (babel-plugin-codegen requires it). - * The bundler tooling closure is now declared on rn-tester (`metro`, - `@react-native/metro-config`, `@react-native/metro-babel-transformer`, `react`) so - rules_js's strict, non-hoisted `node_modules` resolves it. The Berry→pnpm converter was - also fixed to apply Yarn `resolutions` (it had been silently dropping edges such as - `https-proxy-agent`'s `debug`), so the closure is now complete. - * **First-party packages must be consumed in built/`dist` form.** Their source entry - points (e.g. `@react-native/metro-config/src/index.js`) use `../../../scripts/babel-register` - to run the monorepo's Flow sources on the fly — which resolves under Yarn's symlinked - `node_modules` but not under rules_js's copied layout. Running `node scripts/build/build.js` - (which builds `dist/` and repoints `exports`→`dist`) makes Metro load fine; the Bazel - `npm_package` for first-party packages should run that build+prepack hermetically. - * **Metro file-map vs Bazel file layout.** With the above, Metro loads and resolves the - full first-party + third-party graph, but its file-map fails to hash the entry - ("Failed to get the SHA-1 for …/js/RNTesterApp.macos.js") — the crawler doesn't map - files in the Bazel `bin` layout (persists with `useWatchman:false`, - `unstable_enableSymlinks:true`, and no-sandbox). This is the last blocker for the JS - bundle and needs a Metro-config/rules_js file-materialization fix. - - The bundle entry runs via `packages/rn-tester/bazel/bundle.js` (Metro's API + the - `react-native → react-native-macos` alias). - -## What's next (WIP, scaffolded — all `manual`) - -* **Metro bundle**: `//packages/rn-tester:rntester_macos_jsbundle` (`js_run_binary` around - `bazel/bundle.js`). Blocked only on the closure step above. -* **macOS app**: `//packages/rn-tester:RNTesterMacBazel` (`macos_application`) links the - imported XCFrameworks and embeds the Metro bundle as an app resource, reusing - rn-tester's existing `AppDelegate.mm`/`main.m`. Blocked on the XCFrameworks (Hermes 404). +* **First-party workspace linking**: every workspace package exposes a `:pkg` target + (`npm_package`) so rules_js can link it, including `react-native-macos` (consumed as + `react-native`). +* **rn-tester's macOS JS bundle builds *in Bazel*** — `//packages/rn-tester:rntester_macos_jsbundle` + drives Metro (via `bazel/bundle.js`) to emit the real ~1.9 MB `RNTesterApp.macos.jsbundle`. +* **Codegen in Bazel** — `//tools/bazel/react_native:rn_tester_appspecs` builds + `@react-native/codegen` hermetically and generates AppSpecs + `RCTAppDependencyProvider`. +* **The macOS app links + assembles** — `:RNTesterMacBazel` (`macos_application`) compiles a + minimal `RCTReactNativeFactory` host, links the prebuilt React/hermes/ReactNativeDependencies + XCFrameworks, and embeds the JS bundle. The resulting `.app` contains the arm64 binary, + `Contents/Resources/RNTesterApp.macos.jsbundle`, and `Contents/Frameworks/hermes.framework`. + +### Consuming the prebuilt XCFrameworks from Bazel (the header problem) + +The SPM prebuild (`scripts/ios-prebuild.js`) flattens every SPM target's public headers into +`React.xcframework/…/Headers//.h` (e.g. `Headers/React_Core/RCTBridge.h`). +But both the framework's own headers *and* app sources import them by canonical path +(``, ``, ``, +``), and the framework only ships a *partial* header set (the deep +Fabric/renderer C++ headers are missing). So the framework is not directly consumable. This +slice bridges it with two pieces: + +1. **`@rn_prebuilt_xcframeworks//:React_headers`** — a repo rule runs + `tools/bazel/apple/reconstruct_react_headers.py`, which scans the framework headers' + `#include <…>` directives and rebuilds a canonical `-I` symlink tree for the **Obj-C** + ``, ``, ``, `` surface + (disambiguating basename collisions by path-segment match). It also carries the RN + `RCT_*` compile defines. +2. **`//packages/react-native:rn_cxx_headers`** — the complete, canonically-nested **C++** + headers (``, ``, ``, ``) come from the RN + *source* tree (same `main` version as the binary), exposed via the podspec-equivalent + include roots (incl. the `platform/ios`, `platform/cxx`, and react-native-macos + `platform/macos` overrides). Third-party `` etc. come from the + `ReactNativeDependencies` xcframework's canonical `Headers/`. + +RN's new-architecture C++ requires **C++20** (`-std=c++20`, set in `.bazelrc`). + +## End-to-end RN-Tester: how the app is built + +``` +Berry yarn.lock ──(rules_js fork)──▶ node_modules ──(Metro)──▶ RNTesterApp.macos.jsbundle +prebuilt React.xcframework ──(reconstruct_react_headers.py)──▶ :React_headers (Obj-C hdrs) +RN source ReactCommon/** ────────────────────────────────────▶ :rn_cxx_headers (C++ hdrs) + │ + ▼ +bazel/MinimalAppDelegate.mm (RCTReactNativeFactory host) + │ + link React/hermes/ReactNativeDependencies XCFrameworks + ▼ +macos_application :RNTesterMacBazel ──▶ RNTesterMacBazel.app (embeds the jsbundle) +``` + +The minimal host (`bazel/MinimalAppDelegate.mm` + `bazel/minimal_main.m`) boots +`RCTReactNativeFactory` against the embedded bundle. It intentionally omits rn-tester's own +native example modules (NativeCxxModuleExample / RNTMyNativeView) so the app links against +only the prebuilt binaries; adding those (compiling the Phase B codegen + the example native +code) is the natural next increment toward the *full* rn-tester `AppDelegate.mm`. + +### Prebuilt XCFrameworks build on Xcode 26 + +react-native-macos already carries the Hermes version-resolution patches +(`scripts/ios-prebuild/microsoft-hermes.js`): a release branch downloads a published Hermes; +`main` (`1000.0.0`) builds Hermes from source at the merge-base commit. Getting that working on +**Xcode 26** required two fixes in this branch: +* **Host `hermesc` mis-targeted to visionOS.** `ios-prebuild` sets `XROS_DEPLOYMENT_TARGET` + for the cross-platform builds and it leaked into the *native* `build_host_hermesc`; Xcode 26's + clang honors it and built the host tools for visionOS, failing llvh's `CheckAtomic`. Fixed in + `sdks/hermes-engine/utils/build-apple-framework.sh` (force a macOS target for the host tools). +* **Upstream Hermes Mac Catalyst triple.** Hermes hardcodes an invalid universal Catalyst + triple (`-target x86_64-arm64-apple-ios…-macabi`) that Xcode 26's clang rejects. Catalyst + isn't needed for a macOS app, so `build-ios-framework.sh` accepts `HERMES_APPLE_PLATFORMS` + to build a subset (e.g. `macosx`). +* Use **CMake 3.x** (`cmake@3.26.4`); Hermes doesn't configure under Homebrew-default CMake 4.x. + +## What's next (increments) + +* **Full rn-tester host**: compile the Phase B codegen (`RCTAppDependencyProvider`, AppSpecs) + + the NativeCxxModuleExample / NativeComponentExample native code into libraries and swap the + minimal host for rn-tester's real `AppDelegate.mm`. +* **`bazel run`**: wire an ad-hoc-signed run so the app launches directly from Bazel. +* **CI**: run the app build on the macos-26 runner reusing the prebuild artifacts. ## Apple: prebuilt XCFrameworks (swappable seam) diff --git a/packages/react-native/BUILD.bazel b/packages/react-native/BUILD.bazel index d2b420469b0a..a11e71bcc882 100644 --- a/packages/react-native/BUILD.bazel +++ b/packages/react-native/BUILD.bazel @@ -6,6 +6,49 @@ load("@npm//:defs.bzl", "npm_link_all_packages") npm_link_all_packages() +# Canonical C++ header roots from the RN source tree. The SPM-prebuilt +# React.xcframework only exports a partial header set; the deep Fabric/renderer +# C++ headers (e.g. , , +# ) live only in ReactCommon/ in source. Exposing them on the include +# path lets Bazel compile ObjC++/C++ that touches the new-architecture surface, +# while still linking against the prebuilt binary (same `main` version). +cc_library( + name = "rn_cxx_headers", + hdrs = glob( + [ + "ReactCommon/**/*.h", + "ReactCommon/**/*.hpp", + ], + allow_empty = True, + ), + includes = [ + "ReactCommon", + "ReactCommon/jsi", + "ReactCommon/callinvoker", + "ReactCommon/runtimeexecutor", + "ReactCommon/yoga", + "ReactCommon/react/nativemodule/core", + "ReactCommon/react/nativemodule/core/platform/ios", + "ReactCommon/react/runtime/platform/ios", + "ReactCommon/jsitooling", + "ReactCommon/jsiexecutor", + # Renderer platform overrides (Apple uses the ios graphics/text/image + # variants and the cxx view/text/scrollview variants; react-native-macos + # additionally provides a macos view override that takes precedence). + "ReactCommon/react/renderer/graphics/platform/ios", + "ReactCommon/react/renderer/imagemanager/platform/ios", + "ReactCommon/react/renderer/textlayoutmanager/platform/ios", + "ReactCommon/react/renderer/components/textinput/platform/ios", + "ReactCommon/react/utils/platform/ios", + "ReactCommon/react/renderer/components/view/platform/macos", + "ReactCommon/react/renderer/components/view/platform/cxx", + "ReactCommon/react/renderer/components/text/platform/cxx", + "ReactCommon/react/renderer/components/scrollview/platform/cxx", + ], + tags = ["manual"], + visibility = ["//visibility:public"], +) + npm_package( name = "pkg", srcs = glob( diff --git a/packages/rn-tester/BUILD.bazel b/packages/rn-tester/BUILD.bazel index a14dc22f4605..821db3a39418 100644 --- a/packages/rn-tester/BUILD.bazel +++ b/packages/rn-tester/BUILD.bazel @@ -1,16 +1,18 @@ # React Native macOS — Bazel vertical slice (draft / experimental) # -# Status (see docs/bazel.md): -# * The rules_js Berry fork is GREEN (deps resolve/link from the Berry yarn.lock). -# * First-party workspace packages now expose `:pkg` targets so rules_js can link -# them; `bazel build //packages/react-native:pkg` builds. -# * The Metro bundle + macOS app targets below are tagged `manual` (WIP): -# - the Metro bundle needs the RN `react-native`->`react-native-macos` alias and a -# sandbox-safe config (bazel/bundle.js), @react-native/codegen `lib` built, and the -# full strict-deps closure assembled (rules_js does not hoist like Yarn); -# - the macOS app needs the prebuilt XCFrameworks; ios-prebuild resolves the correct -# Hermes (macOS patches) but building it from source needs Xcode 16.x (CI-pinned) -- -# the local Xcode 26 trips LLVM CheckAtomic. See docs/bazel.md. +# Status (see docs/bazel.md): this slice builds RNTester as a macOS app entirely +# with Bazel on Xcode 26: +# * rules_js (Berry fork) drives Metro to produce the real rn-tester JS bundle +# (`:rntester_macos_jsbundle`, ~1.9MB). +# * The prebuilt React/hermes/ReactNativeDependencies XCFrameworks are imported and, +# because the SPM prebuild flattens React's headers, a canonical header tree is +# reconstructed (`@rn_prebuilt_xcframeworks//:React_headers`) and combined with the +# RN source C++ headers (`//packages/react-native:rn_cxx_headers`). +# * A minimal RCTReactNativeFactory host (`bazel/MinimalAppDelegate.mm`) compiles and +# links against those, and `:RNTesterMacBazel` (macos_application) embeds the bundle. +# `bazel build //packages/rn-tester:RNTesterMacBazel` produces a launchable .app. +# App/native targets are tagged `manual` (they need the prebuilt XCFrameworks present) +# so `bazel build //...` / CI stays green without them. load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_library", "js_run_binary") load("@npm//:defs.bzl", "npm_link_all_packages") @@ -92,34 +94,38 @@ js_run_binary( tool = ":bundler", ) -# 3. App host. NOTE: rn-tester's real AppDelegate.mm pulls in rn-tester-specific native +# 3. App host. The REAL rn-tester AppDelegate.mm pulls in rn-tester-specific native # modules + codegen (NativeCxxModuleExample, RNTMyNativeViewComponentView, -# ReactAppDependencyProvider) that are NOT in the prebuilt XCFrameworks. A green app -# therefore needs either those built too, or a minimal RN macOS host. The imported -# XCFrameworks below DO build on Xcode 26 (bazel build @rn_prebuilt_xcframeworks//:React). +# ReactAppDependencyProvider) that are not in the prebuilt XCFrameworks. This slice +# ships a MINIMAL macOS host that boots RCTReactNativeFactory against the embedded +# Metro bundle, linking only the prebuilt XCFrameworks (+ the reconstructed React +# canonical header tree, :React_headers). See docs/bazel.md. objc_library( - name = "rntester_macos_host", + name = "rntester_macos_minimal_host", srcs = [ - "RNTester/AppDelegate.mm", - "RNTester/main.m", + "bazel/MinimalAppDelegate.mm", + "bazel/minimal_main.m", ], - hdrs = ["RNTester/AppDelegate.h"], + hdrs = ["bazel/MinimalAppDelegate.h"], tags = ["manual"], deps = [ + "//packages/react-native:rn_cxx_headers", "@rn_prebuilt_xcframeworks//:React", + "@rn_prebuilt_xcframeworks//:React_headers", "@rn_prebuilt_xcframeworks//:ReactNativeDependencies", + "@rn_prebuilt_xcframeworks//:ReactNativeDependencies_headers", "@rn_prebuilt_xcframeworks//:hermes", ], ) -# 4. macOS app: link native + embed the Metro bundle as an app resource +# 4. macOS app: link the minimal host + embed the Metro bundle as an app resource # (mirrors how Buck2's apple prelude ingests a js_bundle as an AppleResource). macos_application( name = "RNTesterMacBazel", bundle_id = "org.reactjs.native.RNTesterMacBazel", infoplists = ["bazel/Info.plist"], - minimum_os_version = "11.0", + minimum_os_version = "14.0", resources = [":rntester_macos_jsbundle"], tags = ["manual"], - deps = [":rntester_macos_host"], + deps = [":rntester_macos_minimal_host"], ) diff --git a/packages/rn-tester/bazel/MinimalAppDelegate.h b/packages/rn-tester/bazel/MinimalAppDelegate.h new file mode 100644 index 000000000000..d987997d7a61 --- /dev/null +++ b/packages/rn-tester/bazel/MinimalAppDelegate.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import +#import + +// A minimal macOS host for the Bazel slice. It boots the React Native runtime via +// RCTReactNativeFactory and loads the Metro bundle embedded in the app's Resources. +// Unlike rn-tester's full AppDelegate it intentionally omits the rn-tester-specific +// native example modules (NativeCxxModuleExample / RNTMyNativeView) so the app links +// against only the prebuilt XCFrameworks. +@interface MinimalAppDelegate : RCTDefaultReactNativeFactoryDelegate + +@property (nonatomic, strong, nonnull) RCTPlatformWindow *window; +@property (nonatomic, strong, nonnull) RCTReactNativeFactory *reactNativeFactory; + +@end diff --git a/packages/rn-tester/bazel/MinimalAppDelegate.mm b/packages/rn-tester/bazel/MinimalAppDelegate.mm new file mode 100644 index 000000000000..d0e9ad49ccb1 --- /dev/null +++ b/packages/rn-tester/bazel/MinimalAppDelegate.mm @@ -0,0 +1,34 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "MinimalAppDelegate.h" + +@implementation MinimalAppDelegate + +- (void)applicationDidFinishLaunching:(NSNotification *)notification +{ + self.reactNativeFactory = [[RCTReactNativeFactory alloc] initWithDelegate:self]; + + self.window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 1280, 720) + styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | + NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable + backing:NSBackingStoreBuffered + defer:NO]; + self.window.title = @"RNTesterApp (Bazel)"; + + [self.reactNativeFactory startReactNativeWithModuleName:@"RNTesterApp" + inWindow:self.window + initialProperties:@{} + launchOptions:notification.userInfo]; +} + +- (NSURL *)bundleURL +{ + return [[NSBundle mainBundle] URLForResource:@"RNTesterApp.macos" withExtension:@"jsbundle"]; +} + +@end diff --git a/packages/rn-tester/bazel/minimal_main.m b/packages/rn-tester/bazel/minimal_main.m new file mode 100644 index 000000000000..afd9196e558c --- /dev/null +++ b/packages/rn-tester/bazel/minimal_main.m @@ -0,0 +1,23 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "MinimalAppDelegate.h" + +int main(int argc, const char *argv[]) +{ + @autoreleasepool { + NSApplication *application = [NSApplication sharedApplication]; + MinimalAppDelegate *delegate = [MinimalAppDelegate new]; + // Retain the delegate for the lifetime of the app. + application.delegate = (id)delegate; + CFBridgingRetain(delegate); + [application run]; + } + return 0; +} diff --git a/tools/bazel/apple/prebuilt_xcframeworks.bzl b/tools/bazel/apple/prebuilt_xcframeworks.bzl index d415d7249871..602d3aa8fcac 100644 --- a/tools/bazel/apple/prebuilt_xcframeworks.bzl +++ b/tools/bazel/apple/prebuilt_xcframeworks.bzl @@ -39,6 +39,51 @@ def _prebuilt_xcframeworks_impl(rctx): name = name, )) + # ReactNativeDependencies ships canonical, nested third-party headers + # (folly/, boost/, glog/, fmt/, double-conversion/, fast_float/) at the + # xcframework root `Headers/`. Expose them on the include path so C++ sources + # that pull `` etc. compile. + if "ReactNativeDependencies" not in missing: + lines.append(""" +cc_library( + name = "ReactNativeDependencies_headers", + hdrs = glob(["ReactNativeDependencies.xcframework/Headers/**"], allow_empty = True), + includes = ["ReactNativeDependencies.xcframework/Headers"], +) +""") + + # The React.xcframework flattens each SPM target's public headers into + # `Headers//.h`, which breaks the canonical ``, + # ``, `` imports used by both the framework's own + # headers and app sources. Reconstruct a canonical `-I` tree of symlinks so the + # headers are consumable from Bazel, and expose it as `:React_headers`. + if "React" not in missing: + script = rctx.path(Label("//tools/bazel/apple:reconstruct_react_headers.py")) + slice_dir = None + for entry in rctx.path("React.xcframework").readdir(): + if entry.basename.startswith("macos-"): + slice_dir = entry.basename + break + if slice_dir: + fw_headers = "React.xcframework/{}/React.framework/Headers".format(slice_dir) + res = rctx.execute(["python3", str(script), fw_headers, "flat_headers"], quiet = True) + if res.return_code != 0: + fail("reconstruct_react_headers failed: {}".format(res.stderr)) + lines.append(""" +cc_library( + name = "React_headers", + hdrs = glob(["flat_headers/**"], allow_empty = True), + includes = ["flat_headers"], + defines = [ + "RCT_DEV=1", + "RCT_ENABLE_INSPECTOR=1", + "RCT_REMOTE_PROFILE=0", + "RCT_PROFILE=0", + "RCT_NEW_ARCH_ENABLED=1", + ], +) +""") + if missing: # Emit a target that fails at build time with guidance, rather than failing analysis. msg = ("Prebuilt XCFrameworks not found: {}. Build them first with " + diff --git a/tools/bazel/apple/reconstruct_react_headers.py b/tools/bazel/apple/reconstruct_react_headers.py new file mode 100644 index 000000000000..f96278f1f3e2 --- /dev/null +++ b/tools/bazel/apple/reconstruct_react_headers.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Reconstruct a canonical header tree from the SPM-prebuilt React.xcframework. + +The React.xcframework produced by `scripts/ios-prebuild.js` flattens every SPM +target's public headers into `Headers//.h` (e.g. +`Headers/React_Core/RCTBridge.h`, `Headers/React_graphics/Color.h`). But both the +framework's own headers *and* app sources import them by their canonical paths: + + #import // -> React_Core/RCTBridge.h + #import // -> React_RCTUIKit/RCTUIKit.h + #include // -> React_graphics/ColorComponents.h + #include // -> React_jsi/jsi.h + #import // -> React_RCTAppDelegate/RCTReactNativeFactory.h + +None of those resolve against the flattened layout, so the framework is not +directly consumable by clang/Bazel. This script rebuilds a canonical `-I` tree of +symlinks so the imports resolve. It is deliberately generic: it discovers the +canonical include paths by scanning the framework's own headers for +`#include/#import <...>` directives and maps each to a physical file by basename, +disambiguating collisions with a path-segment heuristic. + +Usage: reconstruct_react_headers.py +""" + +import os +import re +import sys + +# Modules whose headers are imported with the flat `` (Obj-C) prefix. +# On basename collisions we prefer React_Core, then these, in order. +OBJC_REACT_MODULES = [ + "React_Core", + "React_RCTUIKit", + "React_CoreModules", + "React_RCTAppDelegate", + "React_RCTImage", + "React_RCTText", + "React_RCTBlob", + "React_RCTVibration", + "React_RCTSettings", + "React_RCTFabric", + "React_NativeModulesApple", + "RCTDeprecation", +] + +# Modules whose (single) header is imported flat, e.g. ``. +FLAT_ROOT_MODULES = ["React_RCTAppDelegate"] + +INCLUDE_RE = re.compile(r'#\s*(?:import|include)\s*<([^>]+)>') + +# Canonical namespaces supplied from the RN *source* tree (complete, correctly +# nested, siblings co-located) rather than the framework's flattened copy. Any +# `` include with one of these first segments is skipped during +# reconstruction. Everything else (React/, RCTDeprecation/, RCTTypeSafety/, ...) +# is Obj-C and reconstructed from the framework. +SOURCE_PREFIXES = frozenset([ + "react", + "ReactCommon", + "jsi", + "jsireact", + "yoga", + "cxxreact", + "folly", + "boost", + "glog", + "fmt", + "double-conversion", + "fast_float", + "jsinspector-modern", + "logger", + "reactperflogger", + "hermes", + "reacthermes", + "jserrorhandler", + "jsitooling", + "callinvoker", + "runtimeexecutor", + "oscompat", +]) + + +def main(): + headers_dir, out_dir = os.path.abspath(sys.argv[1]), sys.argv[2] + + # 1. Index every physical header: basename -> [(module, abspath)]. + by_basename = {} + all_headers = [] + for module in sorted(os.listdir(headers_dir)): + mdir = os.path.join(headers_dir, module) + if not os.path.isdir(mdir): + continue + for root, _dirs, files in os.walk(mdir): + for f in files: + if not f.endswith((".h", ".hpp", ".hh")): + continue + p = os.path.join(root, f) + all_headers.append((module, p)) + by_basename.setdefault(f, []).append((module, p)) + + # 2. Collect every canonical include path referenced anywhere. + referenced = set() + for _module, p in all_headers: + try: + with open(p, "r", errors="ignore") as fh: + for m in INCLUDE_RE.finditer(fh.read()): + referenced.add(m.group(1)) + except OSError: + pass + + links = {} # canonical-relpath -> physical abspath (first writer wins) + + def want(rel, phys): + links.setdefault(rel, phys) + + def rank_objc(module): + return OBJC_REACT_MODULES.index(module) if module in OBJC_REACT_MODULES else 999 + + def pick(candidates, hint_segments): + """Choose the best physical file for a canonical path.""" + if len(candidates) == 1: + return candidates[0][1] + # Prefer a module whose name matches a hint path segment. + best, best_score = candidates[0][1], float("-inf") + for module, phys in candidates: + mtokens = module.lower().replace("react_", "").split("_") + score = sum(1 for seg in hint_segments if seg.lower() in mtokens or seg.lower() == module.lower()) + # Tie-break toward canonical Obj-C ordering (React_Core first). + score = score * 100 - rank_objc(module) + if score > best_score: + best, best_score = phys, score + return best + + # 3. Map referenced canonical `` (Obj-C) paths to physical files. + # C++ / ReactCommon / jsi / yoga headers are intentionally NOT reconstructed + # here: the framework only ships a partial, flattened copy of them (which also + # breaks their quoted sibling includes). Those are supplied from the RN source + # tree instead (see //packages/react-native:rn_cxx_headers), which keeps the + # canonical layout and co-locates siblings. + for rel in referenced: + if "/" not in rel: + continue + if rel.split("/")[0] in SOURCE_PREFIXES: + continue + base = os.path.basename(rel) + cands = by_basename.get(base) + if not cands: + continue + segs = rel.split("/")[:-1] + want(rel, pick(cands, segs)) + + # 4. Flat `` tree: every Obj-C React module header by basename. + for module, phys in all_headers: + if module not in OBJC_REACT_MODULES: + continue + base = os.path.basename(phys) + rel = "React/" + base + existing = links.get(rel) + if existing is None: + links[rel] = phys + else: + # Prefer the higher-priority Obj-C module on collision. + cur_module = _module_of(existing, headers_dir) + if rank_objc(module) < rank_objc(cur_module): + links[rel] = phys + + # 5. Flat-root single-header modules, e.g. ``. + for module, phys in all_headers: + if module in FLAT_ROOT_MODULES: + want(os.path.basename(phys), phys) + + # 6. Materialize the symlink tree (relative links so the tree stays valid + # when relocated, e.g. inside a Bazel external repo). + count = 0 + for rel, phys in links.items(): + dst = os.path.join(out_dir, rel) + os.makedirs(os.path.dirname(dst), exist_ok=True) + if os.path.lexists(dst): + os.remove(dst) + os.symlink(os.path.relpath(phys, os.path.dirname(os.path.abspath(dst))), dst) + count += 1 + + sys.stderr.write( + "reconstruct_react_headers: %d physical headers, %d referenced paths, " + "%d symlinks\n" % (len(all_headers), len(referenced), count) + ) + + +def _module_of(phys, headers_dir): + rel = os.path.relpath(phys, headers_dir) + return rel.split(os.sep)[0] + + +if __name__ == "__main__": + main() From 1221a937d447f7ead4c7686e3d752d90bab8d901 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 7 Jul 2026 10:08:32 -0700 Subject: [PATCH 13/17] Bazel: build the FULL rn-tester host (real AppDelegate + codegen + native 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 , and ; rn_tester_appspecs_lib compiles the AppSpecs + providers (RCTAppDependencyProvider is linked into the app). * NativeCxxModuleExample (C++ TurboModule) compiled + exposed as . * 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 ; UserNotifications.framework linked. * reconstruct_react_headers.py: map the 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> --- docs/bazel.md | 14 ++- packages/react-native/BUILD.bazel | 64 ++++++++++ packages/rn-tester/BUILD.bazel | 43 +++++++ .../NativeCxxModuleExample/BUILD.bazel | 23 ++++ packages/rn-tester/bazel/real_main.m | 24 ++++ .../bazel/apple/reconstruct_react_headers.py | 13 +++ tools/bazel/react_native/defs.bzl | 109 +++++++++++++++--- 7 files changed, 269 insertions(+), 21 deletions(-) create mode 100644 packages/rn-tester/bazel/real_main.m diff --git a/docs/bazel.md b/docs/bazel.md index e26fd3b08b2f..d426da15bfa8 100644 --- a/docs/bazel.md +++ b/docs/bazel.md @@ -104,6 +104,14 @@ prebuilt-XCFramework link. Specifically: minimal `RCTReactNativeFactory` host, links the prebuilt React/hermes/ReactNativeDependencies XCFrameworks, and embeds the JS bundle. The resulting `.app` contains the arm64 binary, `Contents/Resources/RNTesterApp.macos.jsbundle`, and `Contents/Frameworks/hermes.framework`. +* **The FULL rn-tester host also builds** — `:RNTesterMacBazelFull` links rn-tester's *real* + `RNTester/AppDelegate.mm` with the Bazel-built codegen (`RCTAppDependencyProvider` + AppSpecs + compiled into `//tools/bazel/react_native:rn_tester_appspecs_lib`), the C++ TurboModule + example (`NativeCxxModuleExample`), the sample TurboModules + (`//packages/react-native:sample_turbo_modules`), and the RCTLinking/RCTPushNotification + modules built from source (not in the prebuilt framework). `RN_DISABLE_OSS_PLUGIN_HEADER` + currently skips the Fabric `NativeComponentExample` (its sources use quoted plugin includes + not yet wired). ### Consuming the prebuilt XCFrameworks from Bazel (the header problem) @@ -168,9 +176,9 @@ react-native-macos already carries the Hermes version-resolution patches ## What's next (increments) -* **Full rn-tester host**: compile the Phase B codegen (`RCTAppDependencyProvider`, AppSpecs) - + the NativeCxxModuleExample / NativeComponentExample native code into libraries and swap the - minimal host for rn-tester's real `AppDelegate.mm`. +* **Fabric NativeComponentExample**: wire the codegen'd `RCTFabricComponentsPlugins.h` + + `RCTThirdPartyComponentsProvider` registration so the `RNTMyNativeView` example compiles + (drop `RN_DISABLE_OSS_PLUGIN_HEADER`). * **`bazel run`**: wire an ad-hoc-signed run so the app launches directly from Bazel. * **CI**: run the app build on the macos-26 runner reusing the prebuild artifacts. diff --git a/packages/react-native/BUILD.bazel b/packages/react-native/BUILD.bazel index a11e71bcc882..b78e18862015 100644 --- a/packages/react-native/BUILD.bazel +++ b/packages/react-native/BUILD.bazel @@ -29,6 +29,8 @@ cc_library( "ReactCommon/yoga", "ReactCommon/react/nativemodule/core", "ReactCommon/react/nativemodule/core/platform/ios", + "ReactCommon/react/nativemodule/samples", + "ReactCommon/react/nativemodule/samples/platform/ios", "ReactCommon/react/runtime/platform/ios", "ReactCommon/jsitooling", "ReactCommon/jsiexecutor", @@ -49,6 +51,68 @@ cc_library( visibility = ["//visibility:public"], ) +# rn-tester's real AppDelegate imports RCTLinkingManager + RCTPushNotificationManager, +# which are separate RN modules NOT included in the prebuilt React.xcframework. Build +# them from source and expose their headers as (matching the podspec layout). +cc_library( + name = "rct_linking_hdrs", + hdrs = ["Libraries/LinkingIOS/RCTLinkingManager.h"], + strip_include_prefix = "Libraries/LinkingIOS", + include_prefix = "React", + tags = ["manual"], + visibility = ["//visibility:public"], +) + +cc_library( + name = "rct_pushnotification_hdrs", + hdrs = [ + "Libraries/PushNotificationIOS/RCTPushNotificationManager.h", + "Libraries/PushNotificationIOS/RCTPushNotificationPlugins.h", + ], + strip_include_prefix = "Libraries/PushNotificationIOS", + include_prefix = "React", + tags = ["manual"], + visibility = ["//visibility:public"], +) + +objc_library( + name = "rntester_extra_rn_modules", + srcs = [ + "Libraries/LinkingIOS/RCTLinkingManager.mm", + "Libraries/LinkingIOS/RCTLinkingPlugins.h", + "Libraries/PushNotificationIOS/RCTPushNotificationManager.mm", + "Libraries/PushNotificationIOS/RCTPushNotificationPlugins.h", + "Libraries/PushNotificationIOS/RCTPushNotificationPlugins.mm", + ], + tags = ["manual"], + visibility = ["//visibility:public"], + deps = [ + ":rct_linking_hdrs", + ":rct_pushnotification_hdrs", + ":rn_cxx_headers", + "@rn_prebuilt_xcframeworks//:React", + "@rn_prebuilt_xcframeworks//:React_headers", + "@rn_prebuilt_xcframeworks//:ReactNativeDependencies_headers", + ], +) +objc_library( + name = "sample_turbo_modules", + srcs = [ + "ReactCommon/react/nativemodule/samples/ReactCommon/NativeSampleTurboCxxModuleSpecJSI.cpp", + "ReactCommon/react/nativemodule/samples/ReactCommon/SampleTurboCxxModule.cpp", + "ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTNativeSampleTurboModuleSpec.mm", + "ReactCommon/react/nativemodule/samples/platform/ios/ReactCommon/RCTSampleTurboModule.mm", + ], + tags = ["manual"], + visibility = ["//visibility:public"], + deps = [ + ":rn_cxx_headers", + "@rn_prebuilt_xcframeworks//:React", + "@rn_prebuilt_xcframeworks//:React_headers", + "@rn_prebuilt_xcframeworks//:ReactNativeDependencies_headers", + ], +) + npm_package( name = "pkg", srcs = glob( diff --git a/packages/rn-tester/BUILD.bazel b/packages/rn-tester/BUILD.bazel index 821db3a39418..bad99f5ee9a5 100644 --- a/packages/rn-tester/BUILD.bazel +++ b/packages/rn-tester/BUILD.bazel @@ -129,3 +129,46 @@ macos_application( tags = ["manual"], deps = [":rntester_macos_minimal_host"], ) + +# 5. FULL host: rn-tester's real RNTester/AppDelegate.mm + a programmatic main +# (bazel/real_main.m avoids a MainMenu.nib). It links the Bazel-built codegen +# (RCTAppDependencyProvider + AppSpecs), the C++ TurboModule example, and the sample +# TurboModules that the real AppDelegate's getTurboModule:/dependencyProvider need. +# RN_DISABLE_OSS_PLUGIN_HEADER skips the Fabric NativeComponentExample (its sources use +# quoted plugin includes not yet wired); everything else is the real rn-tester host. +objc_library( + name = "rntester_macos_full_host", + srcs = [ + "RNTester/AppDelegate.mm", + "bazel/real_main.m", + ], + hdrs = ["RNTester/AppDelegate.h"], + copts = ["-DRN_DISABLE_OSS_PLUGIN_HEADER"], + includes = ["RNTester"], + sdk_frameworks = ["UserNotifications"], + tags = ["manual"], + deps = [ + "//packages/react-native:rn_cxx_headers", + "//packages/react-native:rntester_extra_rn_modules", + "//packages/react-native:sample_turbo_modules", + "//packages/rn-tester/NativeCxxModuleExample", + "//tools/bazel/react_native:rn_tester_appspecs_appdep_hdrs", + "//tools/bazel/react_native:rn_tester_appspecs_lib", + "//tools/bazel/react_native:rn_tester_appspecs_reactcodegen_hdrs", + "@rn_prebuilt_xcframeworks//:React", + "@rn_prebuilt_xcframeworks//:React_headers", + "@rn_prebuilt_xcframeworks//:ReactNativeDependencies", + "@rn_prebuilt_xcframeworks//:ReactNativeDependencies_headers", + "@rn_prebuilt_xcframeworks//:hermes", + ], +) + +macos_application( + name = "RNTesterMacBazelFull", + bundle_id = "org.reactjs.native.RNTesterMacBazelFull", + infoplists = ["bazel/Info.plist"], + minimum_os_version = "14.0", + resources = [":rntester_macos_jsbundle"], + tags = ["manual"], + deps = [":rntester_macos_full_host"], +) diff --git a/packages/rn-tester/NativeCxxModuleExample/BUILD.bazel b/packages/rn-tester/NativeCxxModuleExample/BUILD.bazel index a92b2ef0c6da..e63584f671d7 100644 --- a/packages/rn-tester/NativeCxxModuleExample/BUILD.bazel +++ b/packages/rn-tester/NativeCxxModuleExample/BUILD.bazel @@ -1,3 +1,26 @@ package(default_visibility = ['//visibility:public']) exports_files(glob(['**/*'], exclude = ['**/*.bazel'], allow_empty = True)) + +# Header exposed to the app as . +cc_library( + name = "NativeCxxModuleExample_hdrs", + hdrs = ["NativeCxxModuleExample.h"], + include_prefix = "NativeCxxModuleExample", + tags = ["manual"], + deps = ["//tools/bazel/react_native:rn_tester_appspecs_reactcodegen_hdrs"], +) + +# rn-tester's C++ TurboModule example (used by the app's getTurboModule:). +objc_library( + name = "NativeCxxModuleExample", + srcs = ["NativeCxxModuleExample.cpp"], + tags = ["manual"], + deps = [ + ":NativeCxxModuleExample_hdrs", + "//packages/react-native:rn_cxx_headers", + "//tools/bazel/react_native:rn_tester_appspecs_reactcodegen_hdrs", + "@rn_prebuilt_xcframeworks//:React_headers", + "@rn_prebuilt_xcframeworks//:ReactNativeDependencies_headers", + ], +) diff --git a/packages/rn-tester/bazel/real_main.m b/packages/rn-tester/bazel/real_main.m new file mode 100644 index 000000000000..dd3e78eaf0f3 --- /dev/null +++ b/packages/rn-tester/bazel/real_main.m @@ -0,0 +1,24 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "AppDelegate.h" + +// rn-tester's own main.m uses NSApplicationMain (nib-driven). This slice wires the +// real AppDelegate programmatically so no MainMenu.nib is required in the bundle. +int main(int argc, const char *argv[]) +{ + @autoreleasepool { + NSApplication *application = [NSApplication sharedApplication]; + AppDelegate *delegate = [AppDelegate new]; + application.delegate = (id)delegate; + CFBridgingRetain(delegate); + [application run]; + } + return 0; +} diff --git a/tools/bazel/apple/reconstruct_react_headers.py b/tools/bazel/apple/reconstruct_react_headers.py index f96278f1f3e2..355bfbe146aa 100644 --- a/tools/bazel/apple/reconstruct_react_headers.py +++ b/tools/bazel/apple/reconstruct_react_headers.py @@ -46,6 +46,12 @@ # Modules whose (single) header is imported flat, e.g. ``. FLAT_ROOT_MODULES = ["React_RCTAppDelegate"] +# Header families that live in React_Core but are consumed via a `` +# umbrella prefix (the core codegen spec). The framework references some of these +# (so they get reconstructed by the scan) but not the umbrella header itself, which +# is only pulled in by app/library sources — so map the whole family explicitly. +SELF_NAMED_MODULES = ["FBReactNativeSpec"] + INCLUDE_RE = re.compile(r'#\s*(?:import|include)\s*<([^>]+)>') # Canonical namespaces supplied from the RN *source* tree (complete, correctly @@ -168,6 +174,13 @@ def pick(candidates, hint_segments): if module in FLAT_ROOT_MODULES: want(os.path.basename(phys), phys) + # 5c. Self-named umbrella families (e.g. ``). + for module, phys in all_headers: + base = os.path.basename(phys) + for name in SELF_NAMED_MODULES: + if base.startswith(name): + want(name + "/" + base, phys) + # 6. Materialize the symlink tree (relative links so the tree stays valid # when relocated, e.g. inside a Bazel external repo). count = 0 diff --git a/tools/bazel/react_native/defs.bzl b/tools/bazel/react_native/defs.bzl index 2fcb6b13de01..8cc08c017611 100644 --- a/tools/bazel/react_native/defs.bzl +++ b/tools/bazel/react_native/defs.bzl @@ -1,30 +1,71 @@ load('@aspect_rules_js//js:defs.bzl', 'js_run_binary') -_CODEGEN_OUTS = [ - 'build/generated/ios/AppSpecs/AppSpecs-generated.mm', - 'build/generated/ios/AppSpecs/AppSpecs.h', - 'build/generated/ios/AppSpecsJSI-generated.cpp', - 'build/generated/ios/AppSpecsJSI.h', - 'build/generated/ios/RCTAppDependencyProvider.h', - 'build/generated/ios/RCTAppDependencyProvider.mm', - 'build/generated/ios/RCTModuleProviders.h', - 'build/generated/ios/RCTModuleProviders.mm', - 'build/generated/ios/RCTModulesConformingToProtocolsProvider.h', - 'build/generated/ios/RCTModulesConformingToProtocolsProvider.mm', - 'build/generated/ios/RCTThirdPartyComponentsProvider.h', - 'build/generated/ios/RCTThirdPartyComponentsProvider.mm', - 'build/generated/ios/RCTUnstableModulesRequiringMainQueueSetupProvider.h', - 'build/generated/ios/RCTUnstableModulesRequiringMainQueueSetupProvider.mm', - 'build/generated/ios/ReactAppDependencyProvider.podspec', - 'build/generated/ios/ReactCodegen.podspec', +_GEN = 'build/generated/ios/' + +# Top-level generated headers, consumed as `` on Apple. +_TOP_HEADERS = [ + _GEN + 'AppSpecsJSI.h', + _GEN + 'RCTModuleProviders.h', + _GEN + 'RCTModulesConformingToProtocolsProvider.h', + _GEN + 'RCTThirdPartyComponentsProvider.h', + _GEN + 'RCTUnstableModulesRequiringMainQueueSetupProvider.h', + _GEN + 'AppSpecs/AppSpecs.h', +] + +# Consumed as ``. +_APPDEP_HEADER = _GEN + 'RCTAppDependencyProvider.h' + +# Fabric component headers, consumed as ``. +_FABRIC_HEADERS = [ + _GEN + 'react/renderer/components/AppSpecs/ComponentDescriptors.h', + _GEN + 'react/renderer/components/AppSpecs/EventEmitters.h', + _GEN + 'react/renderer/components/AppSpecs/Props.h', + _GEN + 'react/renderer/components/AppSpecs/RCTComponentViewHelpers.h', + _GEN + 'react/renderer/components/AppSpecs/ShadowNodes.h', + _GEN + 'react/renderer/components/AppSpecs/States.h', +] + +_SOURCES = [ + _GEN + 'RCTAppDependencyProvider.mm', + _GEN + 'RCTModuleProviders.mm', + _GEN + 'RCTModulesConformingToProtocolsProvider.mm', + _GEN + 'RCTThirdPartyComponentsProvider.mm', + _GEN + 'RCTUnstableModulesRequiringMainQueueSetupProvider.mm', + _GEN + 'AppSpecs/AppSpecs-generated.mm', + _GEN + 'AppSpecsJSI-generated.cpp', + _GEN + 'react/renderer/components/AppSpecs/ComponentDescriptors.cpp', + _GEN + 'react/renderer/components/AppSpecs/EventEmitters.cpp', + _GEN + 'react/renderer/components/AppSpecs/Props.cpp', + _GEN + 'react/renderer/components/AppSpecs/ShadowNodes.cpp', + _GEN + 'react/renderer/components/AppSpecs/States.cpp', +] + +_PODSPECS = [ + _GEN + 'ReactAppDependencyProvider.podspec', + _GEN + 'ReactCodegen.podspec', +] + +_CODEGEN_OUTS = _TOP_HEADERS + [_APPDEP_HEADER] + _FABRIC_HEADERS + _SOURCES + _PODSPECS + +_HEADER_DEPS = [ + '//packages/react-native:rn_cxx_headers', + '@rn_prebuilt_xcframeworks//:React_headers', + '@rn_prebuilt_xcframeworks//:ReactNativeDependencies_headers', ] def rn_codegen(name, project_root, rn_tester_srcs, **kwargs): + """Generate rn-tester's codegen (AppSpecs + providers) and compile it. + + Produces: + * `` -- the raw generated files (js_run_binary). + * `_lib` -- an objc_library compiling the generated providers + AppSpecs, + exposing ``, `` and + `` to the app. + """ js_run_binary( name = name, srcs = rn_tester_srcs + ['//tools/bazel/react_native:codegen_lib'], outs = _CODEGEN_OUTS, - out_dirs = ['build/generated/ios/react/renderer/components/AppSpecs'], env = { 'PROJECT_ROOT': project_root, 'OUTPUT_DIR': '$(RULEDIR)', @@ -35,3 +76,35 @@ def rn_codegen(name, project_root, rn_tester_srcs, **kwargs): tool = '//tools/bazel/react_native:codegen_runner_bin', **kwargs ) + + native.cc_library( + name = name + '_reactcodegen_hdrs', + hdrs = _TOP_HEADERS, + strip_include_prefix = 'build/generated/ios', + include_prefix = 'ReactCodegen', + tags = ['manual'], + ) + native.cc_library( + name = name + '_appdep_hdrs', + hdrs = [_APPDEP_HEADER], + strip_include_prefix = 'build/generated/ios', + include_prefix = 'ReactAppDependencyProvider', + tags = ['manual'], + ) + native.cc_library( + name = name + '_fabric_hdrs', + hdrs = _FABRIC_HEADERS, + strip_include_prefix = 'build/generated/ios', + tags = ['manual'], + ) + + native.objc_library( + name = name + '_lib', + srcs = _SOURCES, + deps = [ + ':' + name + '_reactcodegen_hdrs', + ':' + name + '_appdep_hdrs', + ':' + name + '_fabric_hdrs', + ] + _HEADER_DEPS, + tags = ['manual'], + ) From dd2733ef5c1ad806f2517a25dd4296e9126784d7 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Tue, 7 Jul 2026 10:12:17 -0700 Subject: [PATCH 14/17] =?UTF-8?q?Bazel:=20complete=20rn-tester=20host=20?= =?UTF-8?q?=E2=80=94=20enable=20the=20Fabric=20NativeComponentExample?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 (quoted by the component sources; impl lives in React.xcframework). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/bazel.md | 20 ++++++------ packages/react-native/BUILD.bazel | 11 +++++++ packages/rn-tester/BUILD.bazel | 9 +++--- .../NativeComponentExample/BUILD.bazel | 32 +++++++++++++++++++ 4 files changed, 56 insertions(+), 16 deletions(-) diff --git a/docs/bazel.md b/docs/bazel.md index d426da15bfa8..b001ada6dcd1 100644 --- a/docs/bazel.md +++ b/docs/bazel.md @@ -104,14 +104,13 @@ prebuilt-XCFramework link. Specifically: minimal `RCTReactNativeFactory` host, links the prebuilt React/hermes/ReactNativeDependencies XCFrameworks, and embeds the JS bundle. The resulting `.app` contains the arm64 binary, `Contents/Resources/RNTesterApp.macos.jsbundle`, and `Contents/Frameworks/hermes.framework`. -* **The FULL rn-tester host also builds** — `:RNTesterMacBazelFull` links rn-tester's *real* - `RNTester/AppDelegate.mm` with the Bazel-built codegen (`RCTAppDependencyProvider` + AppSpecs - compiled into `//tools/bazel/react_native:rn_tester_appspecs_lib`), the C++ TurboModule - example (`NativeCxxModuleExample`), the sample TurboModules - (`//packages/react-native:sample_turbo_modules`), and the RCTLinking/RCTPushNotification - modules built from source (not in the prebuilt framework). `RN_DISABLE_OSS_PLUGIN_HEADER` - currently skips the Fabric `NativeComponentExample` (its sources use quoted plugin includes - not yet wired). +* **The FULL rn-tester host also builds** — `:RNTesterMacBazelFull` links rn-tester's *real, + unmodified* `RNTester/AppDelegate.mm` with the Bazel-built codegen (`RCTAppDependencyProvider` + + AppSpecs compiled into `//tools/bazel/react_native:rn_tester_appspecs_lib`), the C++ + TurboModule example (`NativeCxxModuleExample`), the Fabric `NativeComponentExample` + (`RNTMyNativeView`), the sample TurboModules (`//packages/react-native:sample_turbo_modules`), + and the RCTLinking/RCTPushNotification modules built from source (not in the prebuilt + framework). All example modules link in — no rn-tester source is modified or `#ifdef`'d out. ### Consuming the prebuilt XCFrameworks from Bazel (the header problem) @@ -176,11 +175,10 @@ react-native-macos already carries the Hermes version-resolution patches ## What's next (increments) -* **Fabric NativeComponentExample**: wire the codegen'd `RCTFabricComponentsPlugins.h` + - `RCTThirdPartyComponentsProvider` registration so the `RNTMyNativeView` example compiles - (drop `RN_DISABLE_OSS_PLUGIN_HEADER`). * **`bazel run`**: wire an ad-hoc-signed run so the app launches directly from Bazel. * **CI**: run the app build on the macos-26 runner reusing the prebuild artifacts. +* **From source**: build the React/hermes/ReactNativeDependencies XCFrameworks in Bazel + (roadmap below) to drop the SPM prebuild + header reconstruction entirely. ## Apple: prebuilt XCFrameworks (swappable seam) diff --git a/packages/react-native/BUILD.bazel b/packages/react-native/BUILD.bazel index b78e18862015..0d2ed36de01a 100644 --- a/packages/react-native/BUILD.bazel +++ b/packages/react-native/BUILD.bazel @@ -51,6 +51,17 @@ cc_library( visibility = ["//visibility:public"], ) +# rn-tester's Fabric NativeComponentExample quotes "RCTFabricComponentsPlugins.h", +# the core Fabric component-provider header. Its impl is in React.xcframework; expose +# the source header at its bare name so the quoted include resolves. +cc_library( + name = "rct_fabric_components_plugins_hdr", + hdrs = ["React/Fabric/Mounting/ComponentViews/RCTFabricComponentsPlugins.h"], + strip_include_prefix = "React/Fabric/Mounting/ComponentViews", + tags = ["manual"], + visibility = ["//visibility:public"], +) + # rn-tester's real AppDelegate imports RCTLinkingManager + RCTPushNotificationManager, # which are separate RN modules NOT included in the prebuilt React.xcframework. Build # them from source and expose their headers as (matching the podspec layout). diff --git a/packages/rn-tester/BUILD.bazel b/packages/rn-tester/BUILD.bazel index bad99f5ee9a5..c715a4ab9a89 100644 --- a/packages/rn-tester/BUILD.bazel +++ b/packages/rn-tester/BUILD.bazel @@ -132,10 +132,9 @@ macos_application( # 5. FULL host: rn-tester's real RNTester/AppDelegate.mm + a programmatic main # (bazel/real_main.m avoids a MainMenu.nib). It links the Bazel-built codegen -# (RCTAppDependencyProvider + AppSpecs), the C++ TurboModule example, and the sample -# TurboModules that the real AppDelegate's getTurboModule:/dependencyProvider need. -# RN_DISABLE_OSS_PLUGIN_HEADER skips the Fabric NativeComponentExample (its sources use -# quoted plugin includes not yet wired); everything else is the real rn-tester host. +# (RCTAppDependencyProvider + AppSpecs), the C++ TurboModule example, the Fabric +# NativeComponentExample (RNTMyNativeView), and the sample TurboModules that the real +# AppDelegate's getTurboModule:/dependencyProvider/thirdPartyFabricComponents need. objc_library( name = "rntester_macos_full_host", srcs = [ @@ -143,7 +142,6 @@ objc_library( "bazel/real_main.m", ], hdrs = ["RNTester/AppDelegate.h"], - copts = ["-DRN_DISABLE_OSS_PLUGIN_HEADER"], includes = ["RNTester"], sdk_frameworks = ["UserNotifications"], tags = ["manual"], @@ -151,6 +149,7 @@ objc_library( "//packages/react-native:rn_cxx_headers", "//packages/react-native:rntester_extra_rn_modules", "//packages/react-native:sample_turbo_modules", + "//packages/rn-tester/NativeComponentExample", "//packages/rn-tester/NativeCxxModuleExample", "//tools/bazel/react_native:rn_tester_appspecs_appdep_hdrs", "//tools/bazel/react_native:rn_tester_appspecs_lib", diff --git a/packages/rn-tester/NativeComponentExample/BUILD.bazel b/packages/rn-tester/NativeComponentExample/BUILD.bazel index a92b2ef0c6da..ec5c52d2c311 100644 --- a/packages/rn-tester/NativeComponentExample/BUILD.bazel +++ b/packages/rn-tester/NativeComponentExample/BUILD.bazel @@ -1,3 +1,35 @@ package(default_visibility = ['//visibility:public']) exports_files(glob(['**/*'], exclude = ['**/*.bazel'], allow_empty = True)) + +# rn-tester's Fabric component example (RNTMyNativeView). Consumed by the app's +# thirdPartyFabricComponents via . +objc_library( + name = "NativeComponentExample", + srcs = [ + "ios/RCTInteropTestView.m", + "ios/RCTInteropTestViewManager.m", + "ios/RNTLegacyView.mm", + "ios/RNTMyLegacyNativeViewManager.mm", + "ios/RNTMyNativeViewCommon.mm", + "ios/RNTMyNativeViewComponentView.mm", + "ios/RNTMyNativeViewManager.mm", + ], + hdrs = [ + "ios/RCTInteropTestView.h", + "ios/RCTInteropTestViewManager.h", + "ios/RNTLegacyView.h", + "ios/RNTMyNativeViewComponentView.h", + "ios/UIView+ColorOverlays.h", + ], + includes = ["ios"], + tags = ["manual"], + deps = [ + "//packages/react-native:rct_fabric_components_plugins_hdr", + "//packages/react-native:rn_cxx_headers", + "//tools/bazel/react_native:rn_tester_appspecs_fabric_hdrs", + "@rn_prebuilt_xcframeworks//:React", + "@rn_prebuilt_xcframeworks//:React_headers", + "@rn_prebuilt_xcframeworks//:ReactNativeDependencies_headers", + ], +) From 0104b7a7ae28cbea8a9a9abf324f4ec17fdbc952 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 8 Jul 2026 00:20:02 -0700 Subject: [PATCH 15/17] Bazel: generate the :pkg BUILD files instead of hand-writing them 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> --- docs/bazel.md | 29 +++++ packages/assets/BUILD.bazel | 28 +---- packages/babel-plugin-codegen/BUILD.bazel | 28 +---- packages/debugger-frontend/BUILD.bazel | 28 +---- .../eslint-config-react-native/BUILD.bazel | 28 +---- .../eslint-plugin-react-native/BUILD.bazel | 28 +---- packages/eslint-plugin-specs/BUILD.bazel | 28 +---- packages/gradle-plugin/BUILD.bazel | 28 +---- packages/new-app-screen/BUILD.bazel | 28 +---- packages/normalize-color/BUILD.bazel | 28 +---- packages/polyfills/BUILD.bazel | 28 +---- .../react-native-babel-preset/BUILD.bazel | 28 +---- .../BUILD.bazel | 28 +---- packages/react-native-macos-init/BUILD.bazel | 28 +---- .../BUILD.bazel | 28 +---- .../react-native-test-library/BUILD.bazel | 28 +---- packages/typescript-config/BUILD.bazel | 28 +---- packages/virtualized-lists/BUILD.bazel | 28 +---- private/cxx-public-api/BUILD.bazel | 28 +---- private/eslint-plugin-monorepo/BUILD.bazel | 28 +---- private/monorepo-tests/BUILD.bazel | 28 +---- private/react-native-bots/BUILD.bazel | 28 +---- .../BUILD.bazel | 28 +---- private/react-native-fantom/BUILD.bazel | 28 +---- tools/bazel/js/gen_package_builds.mjs | 115 ++++++++++++++++++ tools/bazel/js/workspace_package.bzl | 49 ++++++++ 26 files changed, 308 insertions(+), 529 deletions(-) create mode 100644 tools/bazel/js/gen_package_builds.mjs create mode 100644 tools/bazel/js/workspace_package.bzl diff --git a/docs/bazel.md b/docs/bazel.md index b001ada6dcd1..1bb9ac62b73b 100644 --- a/docs/bazel.md +++ b/docs/bazel.md @@ -242,6 +242,35 @@ giving a ready-made port map. Output the same `.xcframework`s via vs the SPM XCFrameworks, and land the heavy native compiles on the remote cache (the primary CI/local speedup). +## Generating the `:pkg` BUILD files (no more boilerplate) + +Most workspace packages need exactly one Bazel target: a `:pkg` `npm_package` so +`npm_link_all_packages` can link them into the copied `node_modules` (rules_js does +not hoist like Yarn), which is what lets Metro bundle first-party JS. That was ~17 +identical 24-line `BUILD.bazel` files. Following the JS-ecosystem convention (derive +the build metadata from `package.json` instead of hand-authoring it — see +`docs`/research on Turborepo/Nx/Lage and Gazelle), those files are now **generated**: + +* `tools/bazel/js/workspace_package.bzl` — the `rn_workspace_package()` macro; each + generated `BUILD.bazel` is just a `load` + one call. +* `tools/bazel/js/gen_package_builds.mjs` — reads the `workspaces` globs from the root + `package.json` and (re)writes the boilerplate files. It only owns *generator-owned* + files (those carrying its `@generated-by` marker, or pure legacy `:pkg` boilerplate); + any BUILD that declares other targets (`objc_library`, `cc_library`, `first_party`, + `macos_application`, `rn_codegen`, …) is treated as hand-owned and skipped — the same + "generate the 95%, allow local overrides" model as Gazelle's `# gazelle:` directives. + +``` +node tools/bazel/js/gen_package_builds.mjs # refresh the boilerplate +node tools/bazel/js/gen_package_builds.mjs --all # also create any missing ones +node tools/bazel/js/gen_package_builds.mjs --check # CI: non-zero if stale +``` + +This is the incremental step toward full `bazel run //:gazelle`-style generation with +the Aspect JS/TS Gazelle plugin (which would also infer first-party `deps` from +imports); a custom Gazelle extension could later emit `rn_codegen`, the Metro bundle, +and the prebuilt-XCFramework targets too. + ## Layout ``` diff --git a/packages/assets/BUILD.bazel b/packages/assets/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/assets/BUILD.bazel +++ b/packages/assets/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/babel-plugin-codegen/BUILD.bazel b/packages/babel-plugin-codegen/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/babel-plugin-codegen/BUILD.bazel +++ b/packages/babel-plugin-codegen/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/debugger-frontend/BUILD.bazel b/packages/debugger-frontend/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/debugger-frontend/BUILD.bazel +++ b/packages/debugger-frontend/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/eslint-config-react-native/BUILD.bazel b/packages/eslint-config-react-native/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/eslint-config-react-native/BUILD.bazel +++ b/packages/eslint-config-react-native/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/eslint-plugin-react-native/BUILD.bazel b/packages/eslint-plugin-react-native/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/eslint-plugin-react-native/BUILD.bazel +++ b/packages/eslint-plugin-react-native/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/eslint-plugin-specs/BUILD.bazel b/packages/eslint-plugin-specs/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/eslint-plugin-specs/BUILD.bazel +++ b/packages/eslint-plugin-specs/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/gradle-plugin/BUILD.bazel b/packages/gradle-plugin/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/gradle-plugin/BUILD.bazel +++ b/packages/gradle-plugin/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/new-app-screen/BUILD.bazel b/packages/new-app-screen/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/new-app-screen/BUILD.bazel +++ b/packages/new-app-screen/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/normalize-color/BUILD.bazel b/packages/normalize-color/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/normalize-color/BUILD.bazel +++ b/packages/normalize-color/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/polyfills/BUILD.bazel b/packages/polyfills/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/polyfills/BUILD.bazel +++ b/packages/polyfills/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/react-native-babel-preset/BUILD.bazel b/packages/react-native-babel-preset/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/react-native-babel-preset/BUILD.bazel +++ b/packages/react-native-babel-preset/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/react-native-babel-transformer/BUILD.bazel b/packages/react-native-babel-transformer/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/react-native-babel-transformer/BUILD.bazel +++ b/packages/react-native-babel-transformer/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/react-native-macos-init/BUILD.bazel b/packages/react-native-macos-init/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/react-native-macos-init/BUILD.bazel +++ b/packages/react-native-macos-init/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/react-native-popup-menu-android/BUILD.bazel b/packages/react-native-popup-menu-android/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/react-native-popup-menu-android/BUILD.bazel +++ b/packages/react-native-popup-menu-android/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/react-native-test-library/BUILD.bazel b/packages/react-native-test-library/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/react-native-test-library/BUILD.bazel +++ b/packages/react-native-test-library/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/typescript-config/BUILD.bazel b/packages/typescript-config/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/typescript-config/BUILD.bazel +++ b/packages/typescript-config/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/packages/virtualized-lists/BUILD.bazel b/packages/virtualized-lists/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/packages/virtualized-lists/BUILD.bazel +++ b/packages/virtualized-lists/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/private/cxx-public-api/BUILD.bazel b/private/cxx-public-api/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/private/cxx-public-api/BUILD.bazel +++ b/private/cxx-public-api/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/private/eslint-plugin-monorepo/BUILD.bazel b/private/eslint-plugin-monorepo/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/private/eslint-plugin-monorepo/BUILD.bazel +++ b/private/eslint-plugin-monorepo/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/private/monorepo-tests/BUILD.bazel b/private/monorepo-tests/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/private/monorepo-tests/BUILD.bazel +++ b/private/monorepo-tests/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/private/react-native-bots/BUILD.bazel b/private/react-native-bots/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/private/react-native-bots/BUILD.bazel +++ b/private/react-native-bots/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/private/react-native-codegen-typescript-test/BUILD.bazel b/private/react-native-codegen-typescript-test/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/private/react-native-codegen-typescript-test/BUILD.bazel +++ b/private/react-native-codegen-typescript-test/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/private/react-native-fantom/BUILD.bazel b/private/react-native-fantom/BUILD.bazel index 23f9dc4f3149..420dbaf7d3da 100644 --- a/private/react-native-fantom/BUILD.bazel +++ b/private/react-native-fantom/BUILD.bazel @@ -1,24 +1,6 @@ -# Auto-generated first-party package target for rules_js linking. -# Exposes this workspace package as `:pkg` so the root npm_link_all_packages can -# link it into node_modules (needed to bundle first-party JS with Metro). -load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//:defs.bzl", "npm_link_all_packages") +# @generated-by: tools/bazel/js/gen_package_builds.mjs +# Edit the generator or the macro, not this file. Regenerate with: +# node tools/bazel/js/gen_package_builds.mjs +load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") -npm_link_all_packages() - -npm_package( - name = "pkg", - srcs = glob( - ["**/*"], - exclude = [ - "**/node_modules/**", - "**/.build/**", - "**/build/**", - "**/Pods/**", - "**/__tests__/**", - "**/*.bazel", - ], - allow_empty = True, - ), - visibility = ["//visibility:public"], -) +rn_workspace_package() diff --git a/tools/bazel/js/gen_package_builds.mjs b/tools/bazel/js/gen_package_builds.mjs new file mode 100644 index 000000000000..fd19384bd3fa --- /dev/null +++ b/tools/bazel/js/gen_package_builds.mjs @@ -0,0 +1,115 @@ +#!/usr/bin/env node +// Generate the boilerplate `:pkg` BUILD.bazel files for first-party workspace +// packages, so they don't have to be hand-written/maintained. +// +// This is a small, repo-specific analog of `bazel run //:gazelle` (see the +// artifacts research report): it reads the workspace globs from the root +// package.json and, for every workspace package, writes a one-line BUILD.bazel +// that calls the rn_workspace_package() macro. +// +// Ownership / safety (mirrors Gazelle's `# gazelle:` and Nx's project.json +// override model): a BUILD.bazel is only (re)written when it is *generator-owned* +// -- it either carries the @generated marker below, or it is the pure legacy +// `:pkg` boilerplate (a single npm_package with no other target kinds). Any file +// that declares other targets (objc_library, cc_library, first_party, +// macos_application, rn_codegen, ...) is treated as hand-owned and left untouched. +// +// Usage: +// node tools/bazel/js/gen_package_builds.mjs # refresh owned files +// node tools/bazel/js/gen_package_builds.mjs --all # also create missing ones +// node tools/bazel/js/gen_package_builds.mjs --check # non-zero exit if stale (CI) + +import fs from 'node:fs'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..'); +const MARKER = '# @generated-by: tools/bazel/js/gen_package_builds.mjs'; + +const CONTENT = [ + MARKER, + '# Edit the generator or the macro, not this file. Regenerate with:', + '# node tools/bazel/js/gen_package_builds.mjs', + 'load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package")', + '', + 'rn_workspace_package()', + '', +].join('\n'); + +// Any of these target kinds mark a BUILD.bazel as hand-owned (never overwrite). +const HAND_OWNED_TOKENS = [ + 'objc_library(', 'cc_library(', 'swift_library(', 'macos_application(', + 'ios_application(', 'apple_', 'js_library(', 'js_binary(', 'js_run_binary(', + 'ts_project(', 'filegroup(', 'genrule(', 'rn_codegen(', 'first_party', + 'sample_turbo_modules(', 'rntester_extra', 'prebuilt_xcframeworks', +]; + +function isGeneratorOwned(text) { + if (text.includes(MARKER)) return true; + // Legacy pure boilerplate: has an npm_package but no other target kinds. + if (text.includes('npm_package(') && !HAND_OWNED_TOKENS.some((t) => text.includes(t))) { + return true; + } + return false; +} + +// Expand the root package.json "workspaces" globs (supports `dir/*` and `!neg`). +function workspaceDirs() { + const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')); + const patterns = pkg.workspaces || []; + const positive = patterns.filter((p) => !p.startsWith('!')); + const negated = new Set(patterns.filter((p) => p.startsWith('!')).map((p) => p.slice(1))); + const dirs = new Set(); + for (const pattern of positive) { + if (pattern.endsWith('/*')) { + const base = pattern.slice(0, -2); + const baseAbs = path.join(repoRoot, base); + if (!fs.existsSync(baseAbs)) continue; + for (const entry of fs.readdirSync(baseAbs, {withFileTypes: true})) { + if (!entry.isDirectory()) continue; + const rel = base + '/' + entry.name; + if (negated.has(rel)) continue; + if (fs.existsSync(path.join(repoRoot, rel, 'package.json'))) dirs.add(rel); + } + } else if (!negated.has(pattern)) { + if (fs.existsSync(path.join(repoRoot, pattern, 'package.json'))) dirs.add(pattern); + } + } + return [...dirs].sort(); +} + +const args = new Set(process.argv.slice(2)); +const createMissing = args.has('--all'); +const checkOnly = args.has('--check'); + +const created = []; +const updated = []; +const skipped = []; +let stale = false; + +for (const dir of workspaceDirs()) { + const buildPath = path.join(repoRoot, dir, 'BUILD.bazel'); + if (!fs.existsSync(buildPath)) { + if (!createMissing) continue; + if (checkOnly) { stale = true; created.push(dir); continue; } + fs.writeFileSync(buildPath, CONTENT); + created.push(dir); + continue; + } + const current = fs.readFileSync(buildPath, 'utf8'); + if (!isGeneratorOwned(current)) { skipped.push(dir); continue; } + if (current === CONTENT) continue; // already up to date + if (checkOnly) { stale = true; updated.push(dir); continue; } + fs.writeFileSync(buildPath, CONTENT); + updated.push(dir); +} + +const log = (label, list) => { if (list.length) console.log(label + ' (' + list.length + '): ' + list.join(', ')); }; +log('created', created); +log(checkOnly ? 'stale' : 'updated', updated); +log('skipped (hand-owned)', skipped); +if (checkOnly && stale) { + console.error('BUILD.bazel files are stale. Run: node tools/bazel/js/gen_package_builds.mjs'); + process.exit(1); +} +if (!checkOnly) console.log('Done.'); diff --git a/tools/bazel/js/workspace_package.bzl b/tools/bazel/js/workspace_package.bzl new file mode 100644 index 000000000000..f31cd6dd8877 --- /dev/null +++ b/tools/bazel/js/workspace_package.bzl @@ -0,0 +1,49 @@ +"""Convention macro for a first-party workspace package. + +Most workspace packages need exactly one thing from Bazel: expose their files as a +`:pkg` `npm_package` so `npm_link_all_packages` can link them into the copied +`node_modules` (rules_js does not hoist like Yarn), which is what lets Metro bundle +first-party JS. That boilerplate was duplicated in ~17 identical `BUILD.bazel` +files; this macro collapses it to a single call so those files can be *generated* +(see tools/bazel/js/gen_package_builds.mjs) instead of hand-maintained. + +Usage (the entire BUILD.bazel of a leaf workspace package): + + load("//tools/bazel/js:workspace_package.bzl", "rn_workspace_package") + rn_workspace_package() +""" + +load("@aspect_rules_js//npm:defs.bzl", "npm_package") +load("@npm//:defs.bzl", "npm_link_all_packages") + +# Build outputs / tooling that must never end up in a package tarball. +_DEFAULT_EXCLUDES = [ + "**/node_modules/**", + "**/.build/**", + "**/build/**", + "**/Pods/**", + "**/__tests__/**", + "**/*.bazel", +] + +def rn_workspace_package(name = "pkg", srcs = None, exclude = None, visibility = ["//visibility:public"], **kwargs): + """Link this package's deps and expose it as `:` (default `:pkg`). + + Args: + name: npm_package target name (default "pkg"). + srcs: optional explicit srcs; defaults to a sensible glob of the package. + exclude: optional extra glob excludes appended to the defaults. + visibility: target visibility. + **kwargs: forwarded to npm_package. + """ + npm_link_all_packages() + npm_package( + name = name, + srcs = srcs if srcs != None else native.glob( + ["**/*"], + exclude = _DEFAULT_EXCLUDES + (exclude or []), + allow_empty = True, + ), + visibility = visibility, + **kwargs + ) From 73b75ac7d368cbef1f996eae70f8a917cc2148c5 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 8 Jul 2026 00:25:49 -0700 Subject: [PATCH 16/17] Bazel: import React/ReactNativeDependencies as dynamic frameworks (fixes 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> --- tools/bazel/apple/prebuilt_xcframeworks.bzl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/bazel/apple/prebuilt_xcframeworks.bzl b/tools/bazel/apple/prebuilt_xcframeworks.bzl index 602d3aa8fcac..4c503b3ca89e 100644 --- a/tools/bazel/apple/prebuilt_xcframeworks.bzl +++ b/tools/bazel/apple/prebuilt_xcframeworks.bzl @@ -17,7 +17,10 @@ _REL_PATHS = { } # hermes ships as a dynamic framework; React + deps are static. -_DYNAMIC = {"hermes": True} +# The SPM prebuild ships all three as dynamic frameworks (dylibs), so they must be +# imported as dynamic and embedded in the app's Contents/Frameworks (otherwise dyld +# fails at launch with "Library not loaded: @rpath/React.framework/..."). +_DYNAMIC = {"React": True, "ReactNativeDependencies": True, "hermes": True} def _prebuilt_xcframeworks_impl(rctx): root = str(rctx.workspace_root) From 290ed746a98241a16697952bba794796035951c0 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Wed, 8 Jul 2026 13:42:19 -0700 Subject: [PATCH 17/17] =?UTF-8?q?docs:=20honest=20scope/limitations=20?= =?UTF-8?q?=E2=80=94=20Bazel=20for=20the=20native=20slice,=20not=20the=20J?= =?UTF-8?q?S=20inner=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures the lessons from the pow.rs 'Bazel is incompatible with JavaScript' critique: keep the JS dev loop (yarn/Metro/Jest/debugging) off Bazel, consume artifacts at seams rather than re-Bazelifying node_modules, stay a two-way door (additive/manual), and mind the single-server-per-output_base serialization. Grounds the node_modules friction in our own copy_tree.js/first_party.bzl workarounds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/bazel.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/bazel.md b/docs/bazel.md index 1bb9ac62b73b..029427eef6f9 100644 --- a/docs/bazel.md +++ b/docs/bazel.md @@ -21,6 +21,51 @@ XCFrameworks from source in Bazel too (see the roadmap). resource) is modeled explicitly, the way Meta does it in Buck2's `js`/`apple` preludes — but reusing Metro and rules_apple instead of reimplementing them. +## Scope: where Bazel earns its keep here (and where it doesn't) + +Bazel is a *poor* choice for the JavaScript inner loop, and the critique in +["Bazel is incompatible with JavaScript" (pow.rs)](https://pow.rs/blog/bazel-is-incompatible-with-javascript/) +is largely fair **for that use case**. We hit its Problem 1 directly: rules_js uses a +strict, **non-hoisted, copied** `node_modules`, which is exactly why this slice needs +`tools/bazel/js/copy_tree.js` (to stage a symlink-free project dir Metro's file-map can +hash) and `first_party.bzl` (to consume first-party packages in built `dist` form +because their `src` entry points `require('../../../scripts/babel-register')` and escape +the copied tree). That is real friction and real disk/IO cost. + +The important distinction: **we do not put the JS dev loop on Bazel.** `yarn`, Metro's +dev server, Jest, and JS debugging stay exactly as they are — Bazel is opt-in +(`manual`-tagged targets) and additive, so none of the day-to-day JS workflow is +affected. What Bazel is actually for here is the thing the JS ecosystem tools +(Turborepo/Nx/Lage) *cannot* do: run the **Apple toolchain**, link **XCFrameworks**, +run **codegen**, and assemble a signed **`.app`** — one reproducible, remotely-cacheable +graph over JS **and** native. Our build time is dominated by the native compiles +(Hermes, React C++), which is precisely where Bazel's action cache / RBE pays off; the +JS bundle is a small, leaf step. + +Design guardrails we adopt as a result: + +* **Keep the JS inner loop off Bazel.** Never make `bazel` a prerequisite for editing JS, + running Metro, or debugging. The article's strongest point. +* **Consume artifacts at seams, don't re-Bazelify the world.** We already treat the + XCFrameworks as prebuilt inputs; the JS bundle can be treated the same way (build it + with plain Metro, feed the `.jsbundle` in) if the rules_js `node_modules` tax ever + outweighs the benefit of an in-graph bundle. Prefer the `:node_modules` glob over + hand-declaring individual packages (the article notes this keeps you correct). +* **Stay a two-way door.** Everything is additive and `manual`; no forced repo-wide + migration. Adoption and *removal* are both cheap. +* **Mind the single Bazel server.** Bazel is massively parallel *within* a build, but one + server per `--output_base` serializes separate `bazel` invocations ("Another Bazel + command is running…"). Use distinct `--output_base`s for parallel lanes/CI shards + rather than expecting two CLIs to share one. (The article's "single-threaded" framing + conflates these — intra-build parallelism is a Bazel strength.) +* **Expect to enumerate outputs.** Declaring every generated file is inherent to Bazel's + model (we hit it with codegen — see `rn_codegen`'s explicit `_CODEGEN_OUTS`). Use + `out_dirs` TreeArtifacts where a step's outputs aren't statically known. + +Net: use **JS-native tools (Turborepo/Nx/Lage/pnpm) for the JS package graph and dev +loop**, and **Bazel only for the native app slice** where it's genuinely better. They +coexist — Turborepo can even run inside a Bazel monorepo. + ## The single-source-of-truth lockfile problem (and the rules_js Berry fork) The repo uses **Yarn 4 (Berry)**; `yarn.lock` is `__metadata: version: 8`. We keep