Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
68f9046
feat(ios): headers-spec layout + ReactNativeHeaders compose
chrfalch Jun 18, 2026
705b310
refactor(ios): remove clang VFS overlay, resolve headers via frameworks
chrfalch Jun 18, 2026
9ee4199
fix(ios): serve the react/ namespace as a module for SPM consumers
chrfalch Jun 21, 2026
18735a0
fix(cocoapods): install prebuilt RNCore pods as header-less facades
chrfalch Jun 22, 2026
02f3500
style: apply prettier to ios-prebuild scripts and docs
chrfalch Jun 22, 2026
977f1ae
ci(ios): include ReactNativeHeaders.xcframework in the prebuilt core …
chrfalch Jun 22, 2026
ec979d3
feat(ios): expose allowlisted private React headers in the React modu…
chrfalch Jun 23, 2026
556abab
feat(ios): emit per-namespace umbrella for React_RCTAppDelegate (R10)
chrfalch Jun 23, 2026
e53e6b2
fix(ios): re-vend RCTFabricComponentsPlugins.h from the React-RCTFabr…
chrfalch Jun 24, 2026
42d8310
fix(ios-prebuild): fail closed when a deps namespace is missing from …
chrfalch Jun 24, 2026
51d4e7e
fix(ios-prebuild): stage deps headers in compose-xcframework + sort t…
chrfalch Jun 25, 2026
7b3be97
fix(ios): harden prebuilt header layout consumption (review findings)
chrfalch Jul 3, 2026
cd67dee
feat(ios-prebuild): generator-time headers gate + drift hardening (R1…
chrfalch Jul 3, 2026
e6ab638
style: prettier-format headers-rules.md (format-check lints md too)
chrfalch Jul 3, 2026
6648f90
style: add language tags to headers-rules.md fenced blocks (markdownl…
chrfalch Jul 3, 2026
dcb5ad8
style: sort requires in headers-inventory.js (eslint sort-imports, ma…
chrfalch Jul 3, 2026
b4b1a94
fix(ios-prebuild): do not relocate SocketRocket headers into ReactNat…
chrfalch Jul 3, 2026
405adbd
feat(ios-prebuild): embed React.framework's non-header resources for …
chrfalch Jun 22, 2026
562985d
fix(cocoapods): define podspec_dir in RNCoreFacades.generate
chrfalch Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion .github/workflows/prebuild-ios-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,22 @@ jobs:
pattern: prebuild-ios-core-headers-${{ matrix.flavor }}-*
path: packages/react-native/.build/headers
merge-multiple: true
- name: Download ReactNativeDependencies
if: steps.restore-ios-xcframework.outputs.cache-hit != 'true'
uses: actions/download-artifact@v7
with:
name: ReactNativeDependencies${{ matrix.flavor }}.xcframework.tar.gz
path: /tmp/third-party/
- name: Extract ReactNativeDependencies
if: steps.restore-ios-xcframework.outputs.cache-hit != 'true'
shell: bash
run: |
# ReactNativeHeaders.xcframework (built by the compose step) folds in
# the third-party deps namespaces (folly/glog/boost/...), so the deps
# headers must be staged here too — not just in build-slices.
tar -xzf /tmp/third-party/ReactNativeDependencies${{ matrix.flavor }}.xcframework.tar.gz -C /tmp/third-party/
mkdir -p packages/react-native/third-party/
mv /tmp/third-party/packages/react-native/third-party/ReactNativeDependencies.xcframework packages/react-native/third-party/ReactNativeDependencies.xcframework
- name: Setup Keychain
if: ${{ steps.restore-ios-xcframework.outputs.cache-hit != 'true' && env.REACT_ORG_CODE_SIGNING_P12_CERT != '' }}
uses: apple-actions/import-codesign-certs@v3 # https://github.com/marketplace/actions/import-code-signing-certificates
Expand All @@ -173,11 +189,24 @@ jobs:
run: |
cd packages/react-native
node scripts/ios-prebuild -c -f "${{ matrix.flavor }}" -i "React Org"
- name: Verify composed headers
if: steps.restore-ios-xcframework.outputs.cache-hit != 'true'
run: |
# Generator-time header gate: include-health ratchet, structural
# byte-compare of module maps/umbrellas against the spec render, and
# compile smokes (React module + every namespace module + the
# privileged-consumer/Expo fixtures). Catches consumer-facing header
# regressions here instead of in downstream builds.
cd packages/react-native
node scripts/ios-prebuild/headers-verify.js --flavor "${{ matrix.flavor }}"
- name: Compress and Rename XCFramework
if: steps.restore-ios-xcframework.outputs.cache-hit != 'true'
run: |
cd packages/react-native/.build/output/xcframeworks/${{matrix.flavor}}
tar -cz -f ../ReactCore${{matrix.flavor}}.xcframework.tar.gz React.xcframework
# Ship BOTH xcframeworks: React-Core-prebuilt's prepare_command flattens
# ReactNativeHeaders.xcframework's Headers (incl. module.modulemap) into the
# pod. Omitting it leaves consumers without React-Core-prebuilt/Headers/module.modulemap.
tar -cz -f ../ReactCore${{matrix.flavor}}.xcframework.tar.gz React.xcframework ReactNativeHeaders.xcframework
- name: Compress and Rename dSYM
if: steps.restore-ios-xcframework.outputs.cache-hit != 'true'
run: |
Expand Down
58 changes: 41 additions & 17 deletions packages/react-native/React-Core-prebuilt.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,60 @@ Pod::Spec.new do |s|
s.author = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.source = source

# We vend two xcframeworks that ship together in the prebuilt tarball:
# - React.xcframework: the compiled core. Its per-slice React.framework carries
# every <React/...> header + the framework module map, so `#import <React/...>`
# and `@import React;` resolve through FRAMEWORK_SEARCH_PATHS automatically.
# - ReactNativeHeaders.xcframework: headers-only. Carries every other namespace
# (<react/...>, <yoga/...>, folly, glog, ...). Its headers are flattened into a
# top-level Headers/ (see prepare_command) and exposed via the standard pod
# header search path. (<hermes/...> is supplied by the hermes-engine pod here;
# it is folded into ReactNativeHeaders only on the SwiftPM consumer side.)
# There is no clang VFS overlay.
s.vendored_frameworks = "React.xcframework"

s.preserve_paths = '**/*.*'
s.header_mappings_dir = 'React.xcframework/Headers'
s.source_files = 'React.xcframework/Headers/**/*.{h,hpp}'

s.module_name = 'React'
s.module_map = 'React.xcframework/Modules/module.modulemap'
s.public_header_files = 'React.xcframework/Headers/**/*.h'
s.header_mappings_dir = 'Headers'
s.source_files = 'Headers/**/*.{h,hpp}'
s.public_header_files = 'Headers/**/*.h'

add_rn_third_party_dependencies(s)

# We need to make sure that the React.xcframework is copied correctly - in the downloaded tarball
# the root directory is the framework, but when using it we need to have it in a subdirectory
# called React.xcframework, so we need to move the contents of the tarball into that directory.
# This is done in the prepare_command.
# We need to make sure that the headers are copied to the right place - local tar.gz has a different structure
# than the one from the maven repo
# The downloaded tarball ships React.xcframework and ReactNativeHeaders.xcframework
# at its root. We make sure React.xcframework is in its own subdirectory (the Maven
# tarball lays the framework contents at the root; the local tar.gz has a different
# structure) and flatten ReactNativeHeaders' headers into a top-level Headers/ dir
# so CocoaPods exposes them on the header search path.
s.prepare_command = <<~'CMD'
CURRENT_PATH=$(pwd)
XCFRAMEWORK_PATH="${CURRENT_PATH}/React.xcframework"

# Check if XCFRAMEWORK_PATH is empty
if [ -z "$XCFRAMEWORK_PATH" ]; then
echo "ERROR: XCFRAMEWORK_PATH is empty."
exit 0
# Flatten ReactNativeHeaders' headers (identical across slices) into Headers/
# BEFORE we sweep stray root entries into React.xcframework. Fail closed:
# a tarball without ReactNativeHeaders.xcframework (an artifact published
# before the headers-spec layout, or a truncated download) would otherwise
# yield a green install with an empty Headers/ and every <react/...> or
# <yoga/...> include failing much later, far from the cause.
mkdir -p Headers
RNH_XCFRAMEWORK_PATH=$(find "$CURRENT_PATH" -type d -name "ReactNativeHeaders.xcframework" | head -n 1)
if [ -z "$RNH_XCFRAMEWORK_PATH" ]; then
echo "[React-Core-prebuilt] ERROR: ReactNativeHeaders.xcframework not found in the prebuilt tarball." >&2
echo "The artifact predates the headers-spec layout or is incomplete; use a matching react-native version." >&2
exit 1
fi
RNH_HEADERS_PATH=$(find "$RNH_XCFRAMEWORK_PATH" -type d -name "Headers" | head -n 1)
if [ -z "$RNH_HEADERS_PATH" ]; then
echo "[React-Core-prebuilt] ERROR: no Headers directory inside $RNH_XCFRAMEWORK_PATH." >&2
exit 1
fi
cp -R "$RNH_HEADERS_PATH/." Headers
rm -rf "$RNH_XCFRAMEWORK_PATH"

mkdir -p "${XCFRAMEWORK_PATH}"
find "$CURRENT_PATH" -mindepth 1 -maxdepth 1 ! -name "$(basename "$XCFRAMEWORK_PATH")" -exec mv {} "$XCFRAMEWORK_PATH" \;
find "$CURRENT_PATH" -mindepth 1 -maxdepth 1 \
! -name "$(basename "$XCFRAMEWORK_PATH")" ! -name "Headers" \
-exec mv {} "$XCFRAMEWORK_PATH" \;
CMD

# If we are passing a local tarball, we don't want to switch between Debug and Release
Expand Down
11 changes: 9 additions & 2 deletions packages/react-native/React-Core.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ Pod::Spec.new do |s|
s.author = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.source = source
s.resource_bundle = { "RCTI18nStrings" => ["React/I18n/strings/*.lproj"]}
s.compiler_flags = js_engine_flags()
s.header_dir = "React"
s.weak_framework = "JavaScriptCore"
Expand Down Expand Up @@ -122,7 +121,15 @@ Pod::Spec.new do |s|
s.dependency "React-hermes"
end

s.resource_bundles = {'React-Core_privacy' => 'React/Resources/PrivacyInfo.xcprivacy'}
# Both bundles in one declaration: a second `resource_bundle(s) =` would replace
# (not merge) the first. RCTI18nStrings holds React-Core's localized strings
# (loaded by RCTLocalizedString); React-Core_privacy is the privacy manifest.
# (Prebuilt/SwiftPM get both from inside React.xcframework instead — see

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking — needs verification before merge.

This design ships the privacy manifest and RCTI18nStrings.bundle only from inside React.xcframework's React.framework; the prebuilt facade carries no pod-level resource_bundles/resources (see rncore_facades.rb: "the facade carries NO resources"). We still install the prebuilt via CocoaPods, and most apps link their pod graph statically (the New Architecture default: static libraries, no use_frameworks!).

CocoaPods copies resources nested inside a .framework only when it embeds that framework. It does embed vendored dynamic frameworks (via the [CP] Embed Pods Frameworks phase), and the prebuilt is currently built type: .dynamic (scripts/releases/ios-prebuild/swift-package.js), so in principle the nested bundle should be copied even in a static-pods app. But this is silent if it breaks — you'd get untranslated strings (RCTLocalizedStringFromKey falling back to the English default) and a missing PrivacyInfo.xcprivacy in the app's privacy report, with no build error.

Could we verify on a real app with the default CocoaPods setup (static libraries, no use_frameworks!) that:

  1. React.framework/RCTI18nStrings.bundle is actually copied into App.app/Frameworks/React.framework/ and RCTLocalizedStringFromKey returns translated strings, and
  2. PrivacyInfo.xcprivacy is aggregated into the generated privacy report?

Also worth checking use_frameworks! :linkage => :static and app-extension / Mac Catalyst targets, which don't always get an embed-frameworks phase.

# scripts/ios-prebuild/{i18n,privacy}.js — but source builds ship them here.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major: This comment points to ios-prebuild/i18n.js and ios-prebuild/privacy.js, but no such files exist — all the logic lives in the single new framework-resources.js. (Same stale pointer in rncore_facades.rb.) These are load-bearing 'see X' references; please update them to ios-prebuild/framework-resources.js.

s.resource_bundles = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice — consolidating these also fixes a latent bug: the old podspec's separate resource_bundle and resource_bundles assignments both singularize to the same attribute, so the second replaced the first and source builds were effectively dropping RCTI18nStrings.bundle.

'RCTI18nStrings' => ['React/I18n/strings/*.lproj'],
'React-Core_privacy' => 'React/Resources/PrivacyInfo.xcprivacy',
}

add_dependency(s, "React-runtimeexecutor", :additional_framework_paths => ["platform/ios"])
add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
Expand Down
26 changes: 24 additions & 2 deletions packages/react-native/React/I18n/RCTLocalizedString.mm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@

#if !defined(WITH_FBI18N) || !(WITH_FBI18N)

// Anchors resource lookups to the bundle that contains this code: React.framework
// when React Native is consumed prebuilt / via SwiftPM, or the app's main bundle
// for static source builds.
@interface RCTI18nStringsAnchor : NSObject
@end
@implementation RCTI18nStringsAnchor
@end

// Resolves RCTI18nStrings.bundle wherever it ships: the code's own bundle first
// (prebuilt/SwiftPM embed it inside React.framework), then the app's main bundle
// (source builds copy it there via the podspec resource_bundles). Returns nil
// when absent, so the caller falls back to the untranslated default value.
static NSBundle *RCTI18nStringsBundle(void)
{
NSBundle *codeBundle = [NSBundle bundleForClass:[RCTI18nStringsAnchor class]];
NSURL *url = [codeBundle URLForResource:@"RCTI18nStrings" withExtension:@"bundle"];
if (url != nil) {
return [NSBundle bundleWithURL:url];
}
NSString *mainPath = [[NSBundle mainBundle] pathForResource:@"RCTI18nStrings" ofType:@"bundle"];
return mainPath != nil ? [NSBundle bundleWithPath:mainPath] : nil;
}

extern "C" {

static NSString *FBTStringByConvertingIntegerToBase64(uint64_t number)
Expand All @@ -33,8 +56,7 @@

NSString *RCTLocalizedStringFromKey(uint64_t key, NSString *defaultValue)
{
static NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"RCTI18nStrings"
ofType:@"bundle"]];
static NSBundle *bundle = RCTI18nStringsBundle();
if (bundle == nil) {
return defaultValue;
} else {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native/scripts/cocoapods/fabric.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def setup_fabric!(react_native_path: "../node_modules/react-native")
pod 'React-Fabric', :path => "#{react_native_path}/ReactCommon"
pod 'React-FabricComponents', :path => "#{react_native_path}/ReactCommon"
pod 'React-graphics', :path => "#{react_native_path}/ReactCommon/react/renderer/graphics"
pod 'React-RCTFabric', :path => "#{react_native_path}/React", :modular_headers => true
rncore_pod 'React-RCTFabric', :path => "#{react_native_path}/React", :modular_headers => true
pod 'React-ImageManager', :path => "#{react_native_path}/ReactCommon/react/renderer/imagemanager/platform/ios"
pod 'React-FabricImage', :path => "#{react_native_path}/ReactCommon"
end
Loading
Loading