Skip to content

refactor(iOS): include resources like bundles into the precompiled XCFrameworks#57305

Draft
chrfalch wants to merge 11 commits into
mainfrom
chrfalch/prebuilt-resources
Draft

refactor(iOS): include resources like bundles into the precompiled XCFrameworks#57305
chrfalch wants to merge 11 commits into
mainfrom
chrfalch/prebuilt-resources

Conversation

@chrfalch

@chrfalch chrfalch commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Depends on #57285

In source builds, React-Core ships non-header resources — its privacy manifest (PrivacyInfo.xcprivacy) and its localized strings (RCTI18nStrings) — via the podspec's resource_bundles. In the prebuilt path those source pods aren't installed (CocoaPods facades) or aren't present at all (SwiftPM), so these resources were silently dropped: prebuilt/SwiftPM apps shipped no React Native privacy manifest, and localized strings were unavailable.

This embeds them in React.framework at prebuild time so they ship uniformly across CocoaPods-prebuilt and SwiftPM, with source builds unchanged:

  • Privacy manifest — the PrivacyInfo.xcprivacy of the pods baked into React.framework are merged into one manifest at the framework root. React.framework is a dynamic framework, so Xcode's privacy-report aggregation picks it up automatically (no runtime involvement).
  • RCTI18nStrings — rebuilt as RCTI18nStrings.bundle inside React.framework. RCTLocalizedString.mm now resolves the bundle from its own framework first (bundleForClass:), falling back to the app's main bundle for static source builds.

It also fixes a latent bug in React-Core.podspec: a later resource_bundles = was overwriting the earlier resource_bundle =, so source builds had stopped shipping RCTI18nStrings. Both bundles are now declared together. With the artifact owning these resources, the prebuilt RNCore facade no longer carries them.

Changelog:

[IOS][FIXED] - Ship React-Core's privacy manifest and localized strings (RCTI18nStrings) inside the prebuilt React.xcframework, so CocoaPods-prebuilt and SwiftPM apps include them

Test Plan

  • ✅ yarn jest packages/react-native/scripts/ios-prebuild — unit + integration tests for the merge/discovery/bundle-build logic (red/green).
  • ✅ Built React.xcframework from this branch and confirmed each slice's React.framework carries the merged PrivacyInfo.xcprivacy and RCTI18nStrings.bundle (37 locales).
  • ✅ Cold-built rn-tester in prebuilt mode and verified the app bundle contains Frameworks/React.framework/{PrivacyInfo.xcprivacy, RCTI18nStrings.bundle}.
  • ✅ Confirmed React-Core.podspec now reports both resource bundles (RCTI18nStrings, React-Core_privacy) for source builds.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 22, 2026
chrfalch and others added 5 commits June 22, 2026 18:40
The minimal machinery to build the packaged header structures:

- headers-spec.js: the executable layout contract (rules R1-R8) — which
  namespaces are hoisted into the React framework, which carry module maps,
  and how collisions are rejected.
- headers-inventory.js: scans the source tree and classifies every shipped
  header (language surface + modularizability bucket) — the input to the spec.
  computeInventory() feeds the build in-memory; the CLI writes a JSON manifest.
- headers-compose.js: emits the layout — writes the <React/...> headers +
  umbrella + module map into each React.framework slice (detected by the
  framework's presence), and assembles the headers-only
  ReactNativeHeaders.xcframework (every other namespace + deps + Hermes).
  Called by xcframework.js during compose.

This is the alternative header source that lets consumers resolve React Native
headers without a clang VFS overlay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Emit the headers-spec layout unconditionally and delete the VFS overlay across
JS, CI publish, and Ruby. Consumers resolve headers the way the SwiftPM branch
does: <React/...> from the vendored React.framework, every other namespace from
ReactNativeHeaders. No root Headers/ on the xcframework, no VFS.

JS:
- xcframework.js: always emit the React.framework spec layout and build
  ReactNativeHeaders.xcframework (was gated behind RN_ZERO_I_LAYOUT=1). Remove
  the legacy header path entirely — the podspec->root-Headers enumeration,
  createModuleMapFile, and copyHeaderFilesToSlices — so the published
  React.xcframework is a standard framework (Info.plist + per-slice
  React.framework/{Headers,Modules}), no root Headers/ or Modules/. Ship
  ReactNativeHeaders.xcframework inside the reactnative-core tarball (sibling of
  React.xcframework) so the prebuilt pod can vend both; keep the standalone
  ReactNativeHeaders.xcframework.tar.gz for the SPM path. Drop the
  React-VFS-template.yaml emit and the ./vfs import.
- vfs.js: deleted (its only consumer was xcframework.js).
- types.js: drop the now-unused VFSEntry/VFSOverlay/HeaderMapping types.
- replace-rncore-version.js: drop the React-VFS.yaml preservation rationale.

Ruby/CocoaPods:
- React-Core-prebuilt.podspec: vend React.xcframework (its per-slice
  React.framework + module map serves <React/...> and @import React via
  FRAMEWORK_SEARCH_PATHS); flatten ReactNativeHeaders' headers into a top-level
  Headers/ in prepare_command and expose them via the pod header search path.
  Drop the VFS-era root header_mappings_dir/module_map.
- rncore.rb: remove the -ivfsoverlay injection and process_vfs_overlay;
  add_rncore_dependency and configure_aggregate_xcconfig now add a
  HEADER_SEARCH_PATHS to React-Core-prebuilt/Headers for podspec, aggregate, and
  third-party targets.
- react_native_pods.rb: drop the process_vfs_overlay post-install call.

Docs: replace the "VFS Overlay System" section with the headers-spec layout;
drop the obsolete "Known Issues" (pre-headers-spec) section.

Verified end-to-end: prebuild compose produces a VFS-free, root-Headers-free
React.xcframework; rn-tester pod install + xcodebuild (prebuilt path) BUILD
SUCCEEDED with zero -ivfsoverlay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
React.framework is a clang module; when an SPM consumer precompiles it, a
modular React/ header that #imports <react/...> hit
-Wnon-modular-include-in-framework-module because the lowercase react/
namespace (served from ReactNativeHeaders, per R1's Linux/Windows-safe layout)
was deliberately kept out of any module.

Give react/ a module where it already lives instead of relocating it (relocation
would require case-folding react.framework -> React.framework, which only works
on case-insensitive filesystems):

- headers-spec.js: drop the react/ namespace-module exemption so its
  objc-modular-candidates get a module; emit that module as
  ReactNativeHeaders_react (a module literally named 'react' would alias the
  React framework module on a case-insensitive filesystem). Module names are
  internal; <react/...> still resolves by header path and is now modular.
- headers-inventory.js: classify C++ default member initializers in aggregates
  (e.g. struct { NSString *family = nil; } in RCTFontProperties.h) as ObjC++ so
  these are not misclassified objc-modular-candidate and pulled into a plain
  ObjC module they cannot compile in.

The unguarded ObjC/C react/ headers (e.g. JSRuntimeFactoryCAPI.h) now resolve
modularly; the C++ react/renderer/* includes are #ifdef __cplusplus-guarded and
skipped during the ObjC module emit, so they need no module.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In prebuilt mode the React core pods' code + headers live entirely in
React.xcframework / React-Core-prebuilt. Re-installing their SOURCE podspecs
made them ship duplicate headers that shadow the prebuilt artifact and break
the React framework's clang explicit-module precompile
(-Wnon-modular-include-in-framework-module) under Xcode 26.

Install those core pods as dependency-only FACADES instead: generated podspecs
with no sources/headers, installed via :path (so nothing is fetched), each
depending on React-Core-prebuilt. Version, subspecs, default_subspec and
resources (e.g. the privacy manifest) are DERIVED from the real podspec so the
facade stays graph- and resource-equivalent to the source pod.

With the shadowing gone the React module precompiles cleanly with
SWIFT_ENABLE_EXPLICIT_MODULES on, so the Xcode-26 workaround (#53457) is
removed. The prebuilt header search path + ReactNativeHeaders module-map
activation are consolidated into a single post-install injection site
(configure_aggregate_xcconfig); add_rncore_dependency now only declares the
React-Core-prebuilt dependency.

rn-tester's NativeComponentExample uses the canonical <React/...> include for
RCTFabricComponentsPlugins.h (resolved from the framework) so it builds against
the facaded React-RCTFabric in prebuilt mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`yarn format-check` (prettier) was failing CI on PR #57285. Run prettier on the
ios-prebuild headers scripts (headers-compose.js, headers-inventory.js),
replace-rncore-version.js, and __docs__/README.md so format-check passes. No
logic changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chrfalch chrfalch force-pushed the chrfalch/prebuilt-resources branch from 9b0b73d to 2c1bf1d Compare June 22, 2026 16:45
@chrfalch chrfalch changed the title Chrfalch/prebuilt resources refactor(iOS): include resources like bundles into the precompiled XCFrameworks Jun 22, 2026
…tarball

The prebuilt React core now ships two xcframeworks — React.xcframework and the
headers-only ReactNativeHeaders.xcframework. React-Core-prebuilt's prepare_command
flattens the latter's Headers (including module.modulemap) into the pod. The
compose step only tar'd React.xcframework, so consumers got no
React-Core-prebuilt/Headers/module.modulemap and failed the build with
"module map file ... not found". Tar both xcframeworks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chrfalch chrfalch force-pushed the chrfalch/prebuilt-resources branch 2 times, most recently from 28e4489 to a9e8b06 Compare June 22, 2026 18:36
…le (R9)

The public-umbrella model (which replaced the VFS overlay) excludes `+Private`
and objc-blocked headers from React.framework's module, so privileged framework
consumers (e.g. Expo) that `#import <React/RCTBridge+Private.h>`,
`<React/RCTMountingManager.h>`, etc. fail to compile under explicit modules even
though the headers still ship in React.framework/Headers.

Add R9: a curated allowlist appended to the React module map —
`RCTBridge+Private.h` as a real `header` (objc-modular-candidate, reaches no
C++) and the six Fabric headers as `textual header` (objc-blocked; a real
member would re-trip -Wnon-modular-include, and their <react/...> C++ includes
resolve at the consumer's use site). Backwards-compatible: existing
`#import <React/...>` (and Swift `import React`) sites are unchanged. Fails
closed if an allowlisted header is removed/renamed or drifts bucket.

Note: RCTUIKit.h / RCTRootContentView.h are absent from source entirely and
need restoration, not exposure — out of scope here.
@chrfalch chrfalch force-pushed the chrfalch/prebuilt-resources branch from a9e8b06 to 5751187 Compare June 23, 2026 11:11
The flattened ReactNativeHeaders layout ships the individual
React_RCTAppDelegate/*.h headers but no per-namespace umbrella. Consumers like
Expo probe `<React_RCTAppDelegate/React_RCTAppDelegate-umbrella.h>` via
__has_include (RCTAppDelegateUmbrella.h); with the umbrella gone the probe
fails and RCTReactNativeFactory / RCTRootViewFactory are never declared,
breaking the Expo pod's clang module.

Add R10: emit a per-namespace umbrella (content DERIVED from namespaceModules
so it can't drift — e.g. RCTArchConfiguratorProtocol.h, gone from this branch,
is correctly omitted) and add it to that namespace's module so the import stays
modular under explicit modules. Targeted via UMBRELLA_NAMESPACES (currently just
React_RCTAppDelegate, the only umbrella Expo imports); fails closed if a listed
namespace loses its modular headers.
@chrfalch chrfalch force-pushed the chrfalch/prebuilt-resources branch from 5751187 to e074784 Compare June 23, 2026 12:40
…ic facade

Community Fabric modules quote-import "RCTFabricComponentsPlugins.h" (~47x:
slider, maps, pager-view, keyboard-controller, ...). In source, React-RCTFabric
vended it at header_dir "React", so it landed in dependents' CocoaPods header
maps and the bare quoted name resolved. In prebuilt mode React-RCTFabric is a
dependency-only facade that ships no headers — the only copy is baked angle-only
into React.framework (it's objc-blocked, excluded from the framework module
map), so quoted imports fail to resolve.

Re-vend JUST that one header from the facade (FACADE_REEXPOSED_HEADERS), copied
as a self-contained snapshot at header_dir "React", restoring dependents' header
maps exactly as the source pod did. Header-only (no compiled sources, no
duplicate symbols — the impl stays in React.framework). Re-exposing a single
header does not put <react/...>/<yoga/...> on -I, so it does not reintroduce the
non-modular-include shadowing the modular layout eliminates. Fails closed if the
glob matches nothing.
@chrfalch chrfalch force-pushed the chrfalch/prebuilt-resources branch from e074784 to c2c6397 Compare June 24, 2026 06:30
chrfalch and others added 2 commits June 24, 2026 08:53
…the headers compose

buildReactNativeHeadersXcframework copied each declared deps namespace
(folly/glog/boost/fmt/double-conversion/fast_float) from the staged
ReactNativeDependencies headers, but only console.warn'd on a missing one and
kept going — silently shipping a ReactNativeHeaders.xcframework without
third-party header resolution (consumers then fail on <folly/...> etc.). The
summary log also printed the INTENDED namespace list, masking the gap.

Throw instead: a missing declared deps namespace means deps weren't staged
(third-party/ReactNativeDependencies.xcframework/Headers — from a full prebuild
or the cache slot), so refuse to emit an incomplete artifact.
…prebuilt/SwiftPM

Source builds get React-Core's non-header resources from the podspec
resource_bundles, but in the prebuilt path the source pods aren't installed
(CocoaPods facades) / not present (SwiftPM), so they were lost. Reproduce them in
the artifact at compose time via scripts/ios-prebuild/framework-resources.js:

  - Privacy manifest: merge the PrivacyInfo.xcprivacy of the pods baked into
    React.framework into one manifest at the framework root, where Xcode's
    privacy-report aggregation picks it up (React.framework is dynamic; no runtime).
  - RCTI18nStrings: rebuild RCTI18nStrings.bundle from React/I18n/strings/*.lproj
    inside React.framework, resolved at runtime by the framework-aware loader.

RCTLocalizedString.mm now resolves the strings bundle from the code's own bundle
first (React.framework when prebuilt/SwiftPM) with a main-bundle fallback (static
source builds), keeping the graceful nil -> default behaviour.

React-Core.podspec declares RCTI18nStrings and React-Core_privacy in one
resource_bundles map (a later resource_bundles= had silently overwritten the
first), so source builds ship both again. The RNCore facade no longer carries
React-Core_privacy: the prebuilt artifact owns these resources now.

Red/green unit + integration tests in __tests__/framework-resources-test.js.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chrfalch chrfalch force-pushed the chrfalch/prebuilt-resources branch from c2c6397 to 2540bdc Compare June 24, 2026 06:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Expo Partner: Expo Partner

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant