Skip to content

feat(iOS) swiftpm support in iOS#57332

Open
chrfalch wants to merge 37 commits into
mainfrom
chrfalch/swift-package-manager
Open

feat(iOS) swiftpm support in iOS#57332
chrfalch wants to merge 37 commits into
mainfrom
chrfalch/swift-package-manager

Conversation

@chrfalch

@chrfalch chrfalch commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

⚠️ This is a stacked PR — review/merge the base PRs first

Builds on, and should land after (bottom-up):

  1. refactor(ios): remove clang VFS overlay, resolve headers via new ReactNativeHeaders framework #57285 — Remove the Clang VFS overlay / modularize React headers (base of the stack). <React/…> via the framework module map; lowercase namespaces (react/, yoga/, jsi/, …) via the ReactNativeHeaders module map.
  2. refactor(iOS): include resources like bundles into the precompiled XCFrameworks #57305 — Prebuilt artifact resources + ReactNativeHeaders.xcframework. Ships the prebuilt core artifacts (incl. ReactNativeHeaders in the tarball + embedded React.framework resources) and the CocoaPods React-Core-prebuilt facades.
  3. feat(iOS): serve third-party deps from the prebuilt ReactNativeDependencies pod via dependency-only facades #57440 — Self-serving prebuilt ReactNativeDependencies + dependency-only facades. The deps pod becomes the single header authority for the third-party namespaces (RCT-Folly/glog/boost/…); community s.dependency "RCT-Folly" resolves to local facades instead of trunk source pods.
  4. feat(iOS): ReactNativeDependenciesHeaders sidecar + pure-RN ReactNativeHeaders, published to Maven #57442 — ReactNativeDependenciesHeaders sidecar + pure-RN ReactNativeHeaders (immediate base). The deps prebuild emits a headers-only LIBRARY-type sidecar (the binary deps xcframework is framework-type — invisible to SwiftPM binaryTargets), ReactNativeHeaders drops the third-party namespaces, and both headers artifacts publish standalone to Maven.

The whole stack is rebased onto current main (2026-07-06).

This PR consumes the final five-artifact set: React, ReactNativeHeaders (pure-RN), ReactNativeDependencies, ReactNativeDependenciesHeaders, hermes-engine.

Summary

Adds npx react-native spm and the package-generation tooling that turns an app into a SwiftPM-integrated RN app using the base stack's prebuilt XCFrameworks. CocoaPods stays supported — additive, opt-in, no Ruby toolchain. Integration is injected into the existing .xcodeproj in place (nothing generated/renamed/replaced), recorded in .spm-injected.json so it reverses exactly.

What this PR adds

  • The spm CLI (add/update/deinit/scaffold + hidden sync/codegen/download), zero-arg auto-resolution, --deintegrate for CocoaPods→SwiftPM.
  • SwiftPM package generation (scripts/spm/): autolinking→Package.swift, Codegen→React-GeneratedCode, core XCFramework binary targets, artifact download/cache, surgical pbxproj inject/remove.
  • ReactNativeDependenciesHeaders wired as the 5th binaryTarget: the headers-only companions (ReactNativeHeaders from the core tarball, the deps sidecar from the deps tarball) are staged automatically out of their parent tarballs — no --headers-tarball priming; REQUIRED_ARTIFACTS covers all five.
  • Community-library scaffolding from podspecs (incl. root-level-source podspecs via generated include// shims, sibling wiring for transitive spm.dependencies, self-ingestion guards).
  • In-place Xcode integration (XCLocalSwiftPackageReference + Sync build phase + scheme pre-action + auto-sync; ${PODS_ROOT}-anchored REACT_NATIVE_PATH replaced on --deintegrate with exact deinit rollback).
  • rn-tester + helloworld SwiftPM consumption + test library; npm-packaging hygiene; a small RNCoreFacades.podspec_dir fix.

Documentation

Changelog:

[IOS] [ADDED] - npx react-native spm command + SwiftPM package-generation tooling (opt-in; CocoaPods stays supported)

Test Plan

350 scripts/spm unit tests (incl. byte-identical add→deinit round-trip); manual E2E against the five-artifact set: fresh app via cli initspm add --deintegrate→build (artifact resources verified in the app), rn-tester in-place migration (RNTesterPods.xcodeproj, test libraries + spmModules), helloworld in-place migration — all BUILD SUCCEEDED. Verified npx react-native spm <cmd> from the command line in each journey.

Scope & limitations

iOS, prebuilt-only (no build-from-source yet); full spm.xcframework/spm.source library metadata not yet (app-local spm.modules + scaffolding are); Expo not yet.

Follow-ups

Remote-mode ReactNative Package.swift must vend the ReactNativeDependenciesHeaders product (spm-distribution repo); library self-containment for repo-portable manifests; build-from-source.

@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 25, 2026
Comment thread packages/react-native/scripts/spm/download-spm-artifacts.js Fixed
Comment thread packages/react-native/scripts/spm/download-spm-artifacts.js Fixed
Comment thread packages/react-native/scripts/spm/download-spm-artifacts.js Fixed
Comment thread packages/react-native/scripts/spm/spm-pbxproj.js Fixed
@chrfalch chrfalch force-pushed the chrfalch/swift-package-manager branch from 64fcaab to 68bbbba Compare June 25, 2026 12:28
Comment thread packages/react-native/scripts/spm/spm-pbxproj.js Fixed
Comment thread packages/react-native/scripts/spm/download-spm-artifacts.js Fixed
@chrfalch chrfalch force-pushed the chrfalch/swift-package-manager branch 2 times, most recently from 8177e87 to c47b761 Compare July 6, 2026 15:24
chrfalch and others added 11 commits July 6, 2026 18:35
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>
chrfalch and others added 26 commits July 6, 2026 18:35
- 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.
…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>
The FACADE_REEXPOSED_HEADERS loop passed an undefined `podspec_dir` to
copy_reexposed_headers, crashing `pod install` at `use_react_native!`
("undefined local variable or method 'podspec_dir'"). Derive it from the
real podspec path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… handling

- rndependencies.rb: the previous "||= [] << path" only added the deps
  header search path when HEADER_SEARCH_PATHS was unset — with an existing
  value the path was silently dropped. Normalize string values and always
  append. Also point the search path at the pod-local flattened Headers/
  (single header home) instead of the artifact root.
- ReactNativeDependencies.podspec: prepare_command exited 0 when
  XCFRAMEWORK_PATH/HEADERS_PATH resolved empty, producing a silent
  no-link pod; fail closed with exit 1.
- reactNativeDependencies.js: an artifact without a version marker logged
  "we are going to use it anyway" but then fell through to rmSync +
  re-download, destroying locally staged deps builds; honor the message
  and use it as-is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uilt-deps mode

In prebuilt-deps mode the real source pods (RCT-Folly, glog, boost,
DoubleConversion, fmt, fast_float, SocketRocket) are not declared in the
Podfile, so a community podspec's hardcoded 's.dependency "RCT-Folly"'
would resolve from the CocoaPods trunk and compile from source next to
the prebuilt binary — the dual-copy bug class behind the 2026-07-03
SocketRocket regression.

RNDepsFacades generates dependency-only local facade podspecs
(build/rndeps-facades/<Name>/): no sources, no headers, a single
dependency on ReactNativeDependencies. Versions and subspecs are derived
from the real podspecs in third-party-podspecs/ (RCT-Folly keeps
/Default + /Fabric with default_subspecs = ["Default"]; SocketRocket is
synthesized from socket_rocket_config, fail-closed if absent). Declared
as :path pods so Podfile-local resolution beats trunk and nothing is
fetched. ReactNativeDependencies remains the single header authority.

Docs: scripts/cocoapods/__docs__/prebuilt-deps.md describes the full
prebuilt-deps header/facade contract and the mode-by-mode supplier table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ayout

- Embed an RN-authored, accurate-empty PrivacyInfo.xcprivacy for
  SocketRocket in the deps prebuild (upstream ships no privacy manifest;
  the deps artifact historically carried resource bundles only for
  boost/folly/glog). SocketRocket uses no Required Reason APIs.
- Xcode 26's SwiftPM rejects a public-headers directory where the
  umbrella header has sibling directories ("target 'SocketRocket' has
  invalid header layout"): stage the flat public headers into include/
  via prepareScript and point publicHeaderFiles there. Xcode 16 accepts
  both layouts; the artifact's Headers/SocketRocket namespace is
  unaffected.
- Rename the dependency 'socket-rocket' -> 'SocketRocket' so the bundle
  and header-namespace naming line up
  (ReactNativeDependencies_SocketRocket.bundle).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pure formatting — no content change. Fixes the format-check lint lane.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…idecar

The binary ReactNativeDependencies.xcframework is FRAMEWORK-type: it
cannot carry a HeadersPath key, so its root Headers/ dir is invisible to
SwiftPM binaryTargets (verified empirically 2026-07-04 - xcodebuild
rejects -headers without -library, and SwiftPM hard-rejects HeadersPath
on framework entries). This adds a headers-only LIBRARY-type sidecar,
ReactNativeDependenciesHeaders.xcframework, that SwiftPM auto-serves
with zero flags: stub static archives paired with per-slice Headers/
holding the seven third-party namespaces (folly, glog, boost, fmt,
double-conversion, fast_float, SocketRocket).

- headers-xcframework.js (new): dependency-light shared emitter -
  composeHeadersOnlyXcframework (the stub-archive recipe factored out of
  buildReactNativeHeadersXcframework), stubSlicesFromXcframework (slice
  parity derived from the binary artifact's Info.plist, so the sidecar
  resolves for every platform the binary does), and
  buildDepsHeadersXcframework with a set-equality gate that fails closed
  in both directions (declared-but-missing and undeclared namespaces).
- compose-framework.js: the deps compose emits and signs the sidecar
  right after assembling the root Headers/ (slice-uniform content).
- prebuild-ios-dependencies.yml: the sidecar ships INSIDE the deps
  tarball (self-contained for CocoaPods) and as a standalone tarball;
  cache key bumped and now also hashes compose-framework.js.
- reactNativeDependencies.js: consumers extract the sidecar alongside
  the binary; pre-sidecar tarballs (pinned RN_DEP_VERSION) warn and
  continue - CocoaPods is unaffected, SwiftPM recomposes it locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every namespace now has exactly ONE physical home: RN namespaces in
ReactNativeHeaders, third-party deps namespaces in the
ReactNativeDependenciesHeaders sidecar / the ReactNativeDependencies
pod. Relocated deps copies are what made the 2026-07-03 SocketRocket
regression possible (duplicate @interface + poisoned module graph under
use_frameworks when a real pod coexisted with relocated copies) - that
bug class is now structurally impossible, and the SocketRocket
exclusion list is deleted rather than maintained.

- headers-spec.js: R2 rewritten; DEPS_NAMESPACES now includes
  SocketRocket (= the sidecar contents = the artifact's Headers/ dirs,
  set-equality enforced); DEPS_NAMESPACES_NOT_RELOCATED deleted.
- headers-compose.js: RNH stages RN headers only; ensureHeadersLayout
  also builds the sidecar from the cache slot's deps headers and
  returns it, so SwiftPM consumers need no published sidecar.
- headers-verify.js: structural gate flipped - deps namespaces must be
  ABSENT from RNH; compile gates take the deps artifact's Headers on
  the include path (exactly how consumers resolve them).
- CocoaPods: NEW ReactNativeDependenciesUtils.configure_aggregate_xcconfig
  injects $(PODS_ROOT)/ReactNativeDependencies/Headers into the
  aggregate and every pod target in prebuilt-deps mode, mirroring the
  rncore injection. Pods previously resolved folly/glog through the
  globally-injected React-Core-prebuilt/Headers flatten of RNH; after
  the flip the deps pod is the single global home, so the mirror
  injection is what keeps arbitrary pods compiling.

Verified: headers gate green (include-health, structural, compile
smokes vs the deps include path); rn-tester prebuilt static + dynamic
(the regression config), helloworld static, source-core+prebuilt-deps
(React compiled from source resolves folly via the deps pod), and a
source-mode control with an unchanged dependency graph.

BREAKING (prebuilt artifacts only): ReactNativeHeaders.xcframework no
longer contains folly/glog/boost/fmt/double-conversion/fast_float;
consumers reaching them through React-Core-prebuilt/Headers must use
the ReactNativeDependencies pod / ReactNativeDependenciesHeaders
sidecar paths instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
prebuilt + use_frameworks is the exact configuration of the 2026-07-03
SocketRocket dual-copy regression, and no CI lane covered it: the
test-ios-rntester action hard-coupled use-frameworks:true to building
from source. Add a use-prebuilds input ('auto' keeps the historical
coupling for existing callers) and pass use-prebuilds:true from
test_ios_rntester, so its dynamic-frameworks matrix cells now consume
the prebuilt ReactCore + ReactNativeDependencies artifacts built in the
same workflow run. Source-built dynamic frameworks stay covered by
test_ios_rntester_dynamic_frameworks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eaders to Maven

Publish the two headers-only xcframeworks as standalone artifacts on
the react-native-artifacts coordinate so SwiftPM consumers can wire
them as separate binaryTargets (they also keep shipping inside the
ReactCore / ReactNativeDependencies tarballs for CocoaPods):

- external-artifacts/build.gradle.kts: four new PublishArtifacts with
  classifiers reactnative-headers-{debug,release} and
  reactnative-dependencies-headers-{debug,release}.
- prebuild-ios-core.yml: upload the standalone
  ReactNativeHeaders<flavor>.xcframework.tar.gz the compose step
  already produces (previously only bundled inside the ReactCore
  tarball); include it in the workflow cache.
- build-npm-package action: download ReactNativeHeaders* artifacts
  (ReactNativeDependenciesHeaders* already rides the existing
  ReactNativeDependencies* pattern).
- verifyArtifactsAreOnMaven.js: HEAD-check every classifier tarball,
  not just the POM - a release with a missing classifier artifact now
  fails verification instead of passing silently.

Validated locally end-to-end with publishReleasePublicationToMavenLocal:
all 12 files + POM land on com.facebook.react:react-native-artifacts
with the expected classifier names.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The verifier now HEAD-checks the POM plus all eight classifier tarballs
instead of GETting only the POM; the mocks/assertions follow, and a new
case covers a missing classifier behind an existing POM (the exact gap
the classifier checks were added to catch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scripts/spm toolchain that generates a SwiftPM Package.swift + xcodeproj
for React Native and community libraries, on top of the headers-spec header
layout (React.framework + ReactNativeHeaders, no VFS). Consumes
headers-compose.ensureHeadersLayout to compose the header artifacts; ships the
compose runtime + scripts/spm via the npm files allowlist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SPM-mode consumption for the example apps: rn-tester and helloworld each get a
Package.swift + *-SPM xcodeproj that consume React via SwiftPM product deps
(zero header search paths). Adds react-native-test-library (apple + common) and
its rn-tester example, plus the react-native.config.js spmModules wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…scripts

generate-spm-package previously composed the headers-spec layout on the consumer
side when the prebuilt artifacts lacked ReactNativeHeaders.xcframework. That
pulled the ios-prebuild build scripts (headers-compose and its deps) into the
published npm package, which in turn required the `plist` dependency at consumer
runtime.

The prebuilt core artifact ships React.xcframework AND ReactNativeHeaders.xcframework
together, so a consumer never needs to compose the layout. Remove the
consumer-side compose: fail with a clear error if ReactNativeHeaders is absent,
and stop listing scripts/ios-prebuild/* in the package files allowlist — those
are full-repo prebuild scripts, not consumer runtime code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…m-scratch generator

Replace the ambiguous `init`/`--from-scratch` (with its silent
in-place→from-scratch fall-through that could rename a user's project to
.legacy) with four clear verbs:

  add      inject SPM into the existing .xcodeproj, in place (idempotent;
           fail-loud on a CocoaPods-integrated pbxproj). --deintegrate runs
           `pod deintegrate` + strips RN from the Podfile + removes the
           leftover empty Pods group.
  update   re-run the pipeline + refresh the injection.
  deinit   surgical inverse of add (reverse exactly what .spm-injected.json
           records → byte-identical revert; no prompt).
  scaffold unchanged.
  sync/codegen/download stay but are hidden from primary help.

Zero-arg `npx react-native spm` auto-resolves: injected→update; a fresh
CocoaPods project (safe-gate: stock Podfile + git-clean pbxproj/Podfile)→
add --deintegrate; else strict add.

- Delete the from-scratch xcodeproj generator + legacy-migration/Podfile-patch
  machinery and the now-dead whole-file-generation helpers in spm-pbxproj.js.
- Add surgical pbxproj removal primitives (removeObjectByUuid,
  removeArrayMembersByUuid, removeField, removeArrayStringValues,
  removeEmptyPodsGroup); injection now records its exact edits in
  .spm-injected.json so deinit can reverse them.
- Merge flags: --skip/force-download → --download <auto|skip|force>;
  --local-xcframework + --artifacts-dir → --artifacts <path>. Remove
  --skip-xcodeproj/--force-xcodeproj/--bundle-identifier/--entry-file and the
  --platform-name CLI shim.
- generate-spm-package now throws on artifact-slot failures (incl. missing
  ReactNativeHeaders) instead of a silent exitCode+return.
- Update CLI registration, __doc__/spm-scripts.md, and the spm test suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The newer toolchain on the rebased base flags deprecated Flow utility
types and a few lint nits in the SPM scripts + test library:

- Flow: `$ReadOnly`/`$ReadOnlyArray`/`$ReadOnlySet` -> `Readonly*`,
  `$Shape` -> `Partial`, `mixed` -> `unknown`, `+` variance sigil ->
  `readonly`; narrow the nullable remote-package config before reading
  `.url`/`.version` (the `plan.isRemote` branch didn't refine it).
- ESLint: sort requires in expand-spm-dependencies.js and the two
  react-native-test-library entrypoints; drop unnecessary `\"` escapes
  in generate-spm-autolinking-test.js.
- Prettier formatting on spm-pbxproj.js.

No behavior change; 349 scripts/spm tests still green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…orld

The `*-SPM.xcodeproj` projects (+ their app-local Package.swift) added to
the example apps were untested demo scaffolding — no CI job builds them.
In helloworld they also broke `test_ios_helloworld`: two `.xcodeproj` in
`ios/` made CocoaPods unable to auto-select the project ("Could not
automatically select an Xcode project") during `pod install`.

Remove Helloworld-SPM.xcodeproj and RNTester-SPM.xcodeproj plus their
Package.swift, and revert the SPM-artifact lines from the .gitignore
files. The rn-tester TestLibrary example and SPM tooling are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ReactNativeHeaders is pure-RN after the deps-headers split: the
third-party namespaces (folly/glog/boost/fmt/double-conversion/
fast_float/SocketRocket) ship in the ReactNativeDependenciesHeaders
sidecar the deps prebuild emits (the binary deps xcframework is
framework-type — invisible to SwiftPM binaryTargets).

- download-spm-artifacts: headers-only companions are staged out of
  their parent tarball (ReactNativeHeaders from the ReactCore tarball,
  the sidecar from the deps tarball) on fresh extracts, with fast-path
  backfill — generalizing the hermes-headers pattern. This retires the
  --headers-tarball priming workaround for ReactNativeHeaders; the
  override remains for testing, joined by --deps-headers-tarball /
  RN_DEPS_HEADERS_TARBALL_PATH. Both companions register in
  artifacts.json fail-closed (pre-companion tarballs die with
  actionable advice). REQUIRED_ARTIFACTS now lists all five artifacts,
  fixing the inconsistency where generate-spm-package hard-required
  ReactNativeHeaders but cache validation never checked it.
- generate-spm-package needs no code change: it renders one product +
  binaryTarget per artifacts.json entry.
- Product consumers add the ReactNativeDependenciesHeaders product
  wherever deps namespaces must resolve: reactProductDeps()
  (autolinking), the codegen Package.swift template, and scaffolded
  community-library manifests (SCAFFOLDER_VERSION 17 -> 18 so existing
  scaffolds regenerate).

Verified: fresh-app E2E (verdaccio -> cli init -> spm add -> build),
rn-tester and helloworld in-place migrations, 350 spm jest tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Migrating rn-tester (RNTesterPods.xcodeproj, in-place) and helloworld to
SwiftPM surfaced five latent bugs:

- scaffold: podspecs with root-level sources (s.source_files =
  "*.{h,m,mm}") got no publicHeadersPath, so SPM rejected the target
  over the nonexistent default include/. The scaffolder now mirrors
  root headers into a generated include/<SwiftName>/ shim dir
  (publicHeadersPath: "include"), giving dependents the CocoaPods
  header-map spelling `#import <SwiftName/Header.h>`.
- scaffold: root podspec globs matched the scaffolder's own output on
  re-runs — Package.swift's `.swift` extension tripped the
  mixed-language gate into DELETING the manifest it had just written
  (runs alternated between scaffolding and self-destructing). Scaffold
  artifacts (Package.swift, the prefix header, include/ shims) are now
  excluded from source expansion.
- autolinking: the one-time legacy-layout cleanup unconditionally
  removed `<dep>/include/`, nuking the scaffolder's shim dirs on every
  sync. It now only removes include/ together with a marker-bearing
  legacy in-place Package.swift.
- scaffold: transitive spm.dependencies entries carry no podspecPath,
  so podspec-name sibling deps (s.dependency "TestLibraryCommon")
  could not be wired ("Unknown dependency"). The sibling map now
  discovers podspecs at those deps' roots.
- add --deintegrate: a ${PODS_ROOT}-anchored REACT_NATIVE_PATH (the
  CocoaPods template default) dangles once CocoaPods is deintegrated,
  breaking the "Bundle React Native code and images" phase. The
  injection now replaces it with the SPM-computed path, recording the
  original in the marker so deinit restores it exactly.

Also declares TestLibraryApple's real dependency on TestLibraryCommon
in its podspec (CocoaPods resolved the undeclared edge leniently
through the shared Public headers dir; SPM needs it for sibling
package wiring).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- download-spm-artifacts.js: the tar/cp invocations interpolated
  URL/env-derived paths into shell strings (js/shell-command-injection,
  incl. one new critical on the companion backfill path). All four now
  use execFileSync argv form — no shell involved.
- spm-pbxproj.js: generateUUID used md5 (js/weak-cryptographic-algorithm).
  Not a security use — the hash only derives deterministic pbxproj
  UUIDs — but sha256 (truncated to the same 24 hex chars) is just as
  deterministic and keeps the scanner green. Affects only not-yet-
  released injections; the marker records actual UUIDs so deinit of
  md5-era injections is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chrfalch chrfalch force-pushed the chrfalch/swift-package-manager branch from c47b761 to f05129b Compare July 6, 2026 16:37
@chrfalch chrfalch marked this pull request as ready for review July 6, 2026 16:53
@chrfalch chrfalch requested a review from cipolleschi July 6, 2026 16:53
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Jul 6, 2026
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 Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants