refactor(ios): remove clang VFS overlay, resolve headers via new ReactNativeHeaders framework#57285
refactor(ios): remove clang VFS overlay, resolve headers via new ReactNativeHeaders framework#57285chrfalch wants to merge 20 commits into
Conversation
|
CI is really red @chrfalch |
I know - working on it! Forgot to put it in draft, @cortinico :) |
`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>
`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>
920e88b to
684fcf7
Compare
…S overlay ## Summary In RN 0.87 we'll hopefully release a new version of our precompiled binaries that works without the VFS overlay: - react/react-native#57285 - removal of VFS overlay - react/react-native#57305 - moved resource bundles to xcframework Meaning that we'll get completely self-contained XCFrameworks from RN which is a prerequisite for SwiftPM to not become too complex. ## How This PR addds support for detecting wether we're running with an RN version that uses these new frameworks or not - making sure we fallback to the old version when running with pre 0.87. In addition it updates the pod install pipeline with the same tests. NOTE: When merging 0.87 into Expo (as our current React Native), we can remove a lot of code in a follow-up PR that removes the VFS support all-over. There are a couple of 3rd party packages not compatible with RN 0.87 yet, so I tested with some patches for these packages. In addition a small binary fix was added to fix a missing method in 0.87 in EXpo's code. ## Test-plan - ✅ Built Bare-Expo, minimal-tester and precompiled with the new binaries from React native - ✅ Verified that the same versions are using VFS and precompiling correctly when using RN 0.86
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>
…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>
…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.
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.
…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.
…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.
…est imports Two CI fixes for the prebuild-ios-core workflow: - prebuild-ios-core.yml: the compose-xcframework job downloaded the build slices and React headers but never staged third-party/ReactNativeDependencies.xcframework. buildReactNativeHeadersXcframework folds the third-party deps namespaces (folly/glog/boost/...) into ReactNativeHeaders.xcframework, so it fail-closed with "deps namespace 'folly' missing ... refusing to ship an incomplete ReactNativeHeaders.xcframework". Add the Download + Extract ReactNativeDependencies steps (mirroring build-slices) so the deps headers are present before composing. - headers-spec-test.js: reorder requires so the `../headers-spec` import sorts before `fs`, fixing the @react-native/monorepo/sort-imports warning that failed `eslint --max-warnings 0` in test_js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- copy facade resources into the facade pod dir: CocoaPods file accessors cannot glob past the pod root, so the ..-escaping globs shipped the privacy-manifest / i18n resource bundles empty - quote -fmodule-map-file so a PODS_ROOT containing spaces stays a single clang argument (matches the quoted HEADER_SEARCH_PATHS beside it) - fail closed in React-Core-prebuilt's prepare_command and in replace-rncore-version.js when the tarball lacks ReactNativeHeaders.xcframework, instead of silently leaving an empty or deleted Headers/ with the module-map flag dangling - thread rnRoot through planFromInventory/isUmbrellaSafe instead of the hardcoded hosting-package root (the one spot that didn't take the inventory's root) - drop the dead ios-prebuild templates/ files (their only consumers were removed by the headers-spec compose) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…1, verify, ratchet) Hardens the header layout against new/changed headers so consumer-facing regressions fail the PREBUILD instead of a downstream (rn-tester/Expo/ community) build: - headers-verify.js (new; runs in the prebuild compose CI job): include-health ratchet against a committed baseline (notShipped/unresolved/quoted- unresolvable includes in shipped headers — 27 baselined today); structural byte-compare of the composed module maps/umbrellas against the spec render; compile smokes — an ObjC TU precompiling the React module (every umbrella header) + all 14 R5 namespace modules + the R10 umbrella + __has_include asserts, an Expo-shaped ObjC++ TU (the R9 textual Fabric surface), and a Swift TU (import React + RCTBridge.moduleRegistry). - R11: one source, one content location. 116 sources ship under multiple spellings (React/X.h + CoreModules/RCTImage/RCTAnimation/... forms, bare React_RCTAppDelegate aliases). The flattened layout duplicated their declarations, so any -fmodules consumer touching two spellings (even transitively via a legacy spelling) hit redefinition errors — found by the gate's first run. The module-owned spelling keeps the content; every other spelling is emitted as a one-line redirect shim. - Single source of truth for third-party namespaces: the inventory's include classifier now derives from DEPS_NAMESPACES, and compose enforces set-equality with the deps artifact in BOTH directions (missing OR undeclared namespace fails). The undeclared direction immediately surfaced SocketRocket, which the deps artifact ships but was never relocated. - R5 exemption assert: an invalid-module-identifier namespace gaining a modular-candidate header now fails the plan instead of silently shipping a non-modular header. - Inventory records quoted includes that don't resolve to a shipped header (quotedNotShipped) instead of dropping them. Docs: __docs__/headers-rules.md documents rules R1-R11, the emission pipeline, and the resilience model.
…iveHeaders Declaring SocketRocket in DEPS_NAMESPACES (added to satisfy the deps set-equality guard) relocated textual copies of its headers into ReactNativeHeaders and thus into the flattened React-Core-prebuilt/Headers, which sits on every pod's HEADER_SEARCH_PATHS. The real SocketRocket pod still exists in consumer graphs and vends the same headers, so the copies collide: duplicate @interface definitions when the SocketRocket pod compiles (rn-tester, prebuilt + USE_FRAMEWORKS=dynamic), and a poisoned module graph in use_frameworks/explicit-modules apps that don't compile SocketRocket sources (Expo: ReactCodegen/RCTModulesConformingToProtocolsProvider.h "file not found"). Bisect confirmed removing the SocketRocket dir flips red->green and the R11 shims are not implicated. Add DEPS_NAMESPACES_NOT_RELOCATED: such namespaces count as declared for the set-equality guard (a genuinely new dep still fails closed), the include classifier still recognizes them, and the headers gate now asserts they stay ABSENT from the artifact. Note: no CI lane covers prebuilt + use_frameworks (the dynamic-frameworks lane builds from source), which is why CI stayed green.
62cabbe to
b4b1a94
Compare
| # otherwise the React framework's clang explicit-module precompile trips | ||
| # -Wnon-modular-include-in-framework-module on `<yoga/...>` / `<react/...>`. | ||
| # Quoted so a $(PODS_ROOT) containing spaces stays a single clang argument. | ||
| module_map_flag = " \"-fmodule-map-file=$(PODS_ROOT)/React-Core-prebuilt/Headers/module.modulemap\"" |
There was a problem hiding this comment.
I see that the modulemap template has been deleted. Why are we setting this? From where the modulemap comes from?
| } | ||
| } | ||
|
|
||
| const collisions = []; |
There was a problem hiding this comment.
Major: Inventory natural-path collisions are computed here but never enforced. computeInventory() (~line 581) doesn't return collisions, and the compose gate in headers-compose.js (~line 40) only checks destination collisions from headers-spec.js. Because addIdentity merges two same-naturalPath sources into one entry and planFromInventory only uses identities[0].source, the second source is silently dropped and nothing fails — a gap versus the R8 'can't silently regress' guarantee in the docs. Consider surfacing these collisions and treating a non-empty set as fatal.
| // aggregate context is required, avoiding false positives on file-scope | ||
| // definitions. Unguarded by construction (definitions can't sit under a | ||
| // pure `#ifdef __cplusplus` and still be the ObjC surface). | ||
| const aggregateMemberInitRe = |
There was a problem hiding this comment.
Minor: The default-member-initializer heuristic requires a named aggregate, so an anonymous typedef struct { CGFloat x = NAN; } Foo; is misbucketed as objc/c and could wrongly enter the umbrella. The generator compile gate is the backstop, but widening this regex (or a comment) would help.
| /^\s*(namespace\s+[A-Za-z_]|template\s*<|extern\s+"C\+\+"|enum\s+class\b|constexpr\b|using\s+(namespace\s|[A-Za-z_]\w*\s*=))/; | ||
|
|
||
| for (const rawLine of text.split('\n')) { | ||
| const line = rawLine.replace(/\/\/.*$/, ''); |
There was a problem hiding this comment.
Minor: Line scanners strip only // comments, not /* ... */ block comments, so a block-comment line starting with namespace/template </constexpr can trip the C++ detector and push a header to textual/objc-blocked. Direction is safe (only loses modularity), but it can silently shrink the umbrella.
| const headerMaps = podSpecsWithHeaderFiles[podspecPath]; | ||
| // xcframework.js and vfs.js both use the ROOT spec's name (first map) as | ||
| // the pod folder, with the same first-occurrence '-' -> '_' replacement. | ||
| const podName = headerMaps[0].specName.replace('-', '_'); |
There was a problem hiding this comment.
Minor: .replace('-', '_') replaces only the first hyphen, so a future pod like React-Foo-Bar would namespace as React_Foo-Bar. Intentional parity with the old code and no current pod has two hyphens, but /-/g would be safer.
| // Fold the hermes-headers presence into the marker so a slot that gains | ||
| // staged hermes headers (e.g. after a tooling upgrade re-downloads them) | ||
| // recomposes instead of reusing a hermes-less ReactNativeHeaders. | ||
| const marker = `${sourceXcfw}\n${sourceStat.mtimeMs}\n${hermesHeaders ?? 'no-hermes'}\n`; |
There was a problem hiding this comment.
Major: The recompose freshness marker keys only on the React.xcframework Info.plist mtime. Editing a source header or headers-spec.js/headers-compose.js without changing the cached xcframework will serve stale composed output. Fine for downloaded-artifact consumers, but a footgun for local iteration — consider folding a hash of the plan (or of the script mtimes) into the marker.
| for (const slice of slices) { | ||
| const fwk = path.join(xcfwPath, slice, 'React.framework'); | ||
| fs.rmSync(path.join(fwk, 'Headers'), {recursive: true, force: true}); | ||
| execSync(`/bin/cp -Rc "${stage}" "${path.join(fwk, 'Headers')}"`); |
There was a problem hiding this comment.
Minor: compose uses execSync with template-interpolated double-quoted paths, while headers-verify.js uses execFileSync with arg arrays. A path containing " or $ could break or shell-expand. Low risk (CI/dev-Mac controlled paths), just an inconsistency worth noting.
| execSync(`/bin/cp -Rc "${src}" "${path.join(stage, 'hermes')}"`); | ||
| hermesFolded = true; | ||
| } else { | ||
| console.warn(`headers-compose: hermes headers missing at ${src}`); |
There was a problem hiding this comment.
Minor: Missing hermes namespace warns and continues, whereas missing deps namespaces fail closed. Documented as intentional, but a consumer-side compose can silently produce a ReactNativeHeaders without <hermes/...> resolution and only discover it at a downstream build.
| 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 |
There was a problem hiding this comment.
Major: This hard-fails install when a tarball lacks ReactNativeHeaders.xcframework (mirrored by a throw in replace-rncore-version.js). Deliberate, but any artifact published before this layout becomes uninstallable with no version-gated fallback — notable on a stable branch. Please confirm the prebuild-ios-core.yml cache key was bumped, otherwise a warm pre-PR cache can skip the new download/extract steps and ship a ReactNativeHeaders-less tarball.
| * header manifest). | ||
| * R8. Collisions are ERRORS: two different source files may never project to | ||
| * the same destination path. | ||
| * R11. ONE source file, ONE content location. Some sources ship under several |
There was a problem hiding this comment.
Nit: The 'contract' docblock lists R1–R8 then jumps to R11; R9/R10 exist only as inline comments, and other files say R1–R10 / R1–R11. Align the numbering.
…den compose freshness + shell usage
Review feedback on the header modularization (VFS-overlay removal):
- computeInventory() now returns natural-path collisions; computeSpecPlan()
fails closed on them (R8) so two sources projecting to the same Headers/
path can no longer be silently merged (only identities[0].source was kept).
- ensureHeadersLayout()'s freshness marker folds a sha256 of the compose
tooling (headers-{inventory,spec,compose}.js) so a local script edit forces
a recompose — the source xcframework's Info.plist mtime alone couldn't.
- scanHeader() strips /* ... */ block comments (multi-line aware) so a
documentation line mentioning namespace/template/constexpr can't trip the
C++ detector and shrink the umbrella.
- Anonymous aggregates with C++ member initializers (typedef struct { x = ..; })
are now detected as ObjC++ (tag name made optional).
- Multi-hyphen pod names namespaced correctly (.replace(/-/g,'_')).
- cp/tar shell-outs switched to execFileSync with arg arrays (CodeQL
"command from environment values"; parity with headers-verify.js).
- Clearer hermes-missing warning; headers-spec R9/R10 added to the contract
docblock (was R1–R8 then R11).
- New headers-inventory-test.js covering scanHeader (cxx guards, #else/#elif
flips, anonymous aggregate, block/line comments).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the detailed review! Addressed in Majors
Question — Minors / nits — all done: |
… sort-imports) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/bin/cp -c (APFS clonefile) is macOS-only; Linux CI exercises these paths through the jest integration tests, where GNU cp rejects it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
The prebuilt
React.xcframeworkpreviously relied on a Clang VFS overlay (React-VFS.yaml) to make headers importable, because the headers were laid out in CocoaPods-style namespaced folders rather than standard framework conventions. The overlay had to be generated at build time, re-resolved at pod-install time per slice, and injected as-ivfsoverlayflags into every Obj-C, C++, and Swift compile (including aggregate and third-party pod targets).This is fragile, hard to reason about, and incompatible with SwiftPM consumption.
This PR removes the VFS overlay entirely and resolves headers through standard framework/header-search-path mechanics instead.
Headers are now emitted into the artifact according to an explicit, executable spec:
React.xcframework— each slice'sReact.frameworkcarries every<React/...>header plus a framework module map, so#import <React/...>and@import Reactresolve throughFRAMEWORK_SEARCH_PATHSautomatically.ReactNativeHeaders.xcframework(new, headers-only) — carries every other namespace (<react/...>,<yoga/...>,folly,glog, …), shipped alongside in the prebuilt tarball and exposed via a single header search path.ReactNativeDependenciesbinary-only.No clang VFS overlay, no per-target
-ivfsoverlayflags. The layout is driven by a single source of truth (headers-spec.js, rules R1–R11) that both the prebuild compose step and downstream SwiftPM tooling derive from, so the shipped header set cannot drift from the spec.Source headers are byte-identical to the repo — the only consumer-facing change needed is bare-form angle includes (
#import <RCTAppDelegate.h>→#import <React/RCTAppDelegate.h>).Consumer surfaces the flattened layout initially dropped are restored (validated against Expo and community Fabric modules):
RCTBridge+Private.h+ the FabricRCTComponentView*family) are exposed in theReactmodule map — modular where safe,textualwhere they reach C++ — so frameworks like Expo compile unchanged, incl. Swift access toRCTBridge.moduleRegistry.React_RCTAppDelegate-umbrella.his re-emitted (derived from the live header set) for consumers probing it via__has_include.React/X.h+ legacyCoreModules/…,RCTImage/…, bare aliases — 116 today) keep content at ONE module-owned spelling; other spellings become generated redirect shims, so-fmodulesconsumers cannot hit duplicate declarations.React-RCTFabricfacade re-vendsRCTFabricComponentsPlugins.hatheader_dir "React", keeping community Fabric modules' quoted#import "RCTFabricComponentsPlugins.h"working as with source pods.The layout is verified at generator time (
headers-verify.js, runs in the prebuild compose CI job): unresolvable includes ratchet against a committed baseline, composed module maps/umbrellas must byte-match the spec render, and consumer-shaped compile smokes must pass (theReactmodule, every namespace module, an Expo-shaped ObjC++ fixture, and a SwiftmoduleRegistryfixture). Fail-closed guards cover header collisions, allowlist drift, and missing OR undeclared third-party deps namespaces (the latter surfacedSocketRocket, which is deliberately NOT relocated — the real pod vends it, and textual copies collide underuse_frameworks; the gate asserts its absence).Key changes
headers-spec.js(the executable layout contract, R1–R11),headers-compose.js(emitter for both xcframeworks),headers-inventory.js(podspec-driven header classifier feeding the spec + a diagnostic manifest),headers-verify.js(generator-time gate + CI step),__docs__/headers-rules.md(rules + rationale).vfs.js, VFS types intypes.js, and the VFS processing/flag-injection paths inrncore.rbandxcframework.js.React-Core-prebuilt.podspec(vends both xcframeworks, flattensReactNativeHeadersheaders intoHeaders/viaprepare_command, fails closed on incomplete tarballs),rncore.rb/react_native_pods.rb(header search path instead of overlay flags),prebuild-ios-core.yml(core tarball ships both xcframeworks; compose job verifies the composed headers), README (VFS docs replaced with the new model).RCTFabricComponentsPlugins.hre-vend exception noted above).Changelog:
[IOS] [CHANGED] - Remove the Clang VFS overlay from prebuilt React Native Core; resolve headers via React.xcframework + a new headers-only ReactNativeHeaders.xcframework
Test Plan
React-Core-prebuiltpod (Debug + Release) with no-ivfsoverlayflags present in the generated xcconfigs.#import <React/...>,@import React;, and the relocated namespaces (<react/...>,<yoga/...>,folly/glog) all resolve.React.xcframeworkandReactNativeHeaders.xcframework; pod install flattens headers intoReact-Core-prebuilt/Headers.React.xcframeworkandReactNativeHeaders.xcframeworkare changed between debug and release correctly.headers-verify.jsgate green on Debug and Release composes (-Werror=non-modular-include-in-framework-modulenever trips in consumer builds).private/helloworldbuilds against the prebuilt core (CocoaPods path).React_RCTAppDelegateumbrella probe, Fabric quoted imports).