Christian Falch
90d · built 2026-09-30
Performance
What Christian Falch shipped in the selected window, measured in ETV, and how it compares with the 90 days before it.
Effective capacity
+7.9engineers
delivers like 8.9 (8.9x pre-AI)
Output (ETV)
22.9ETV
+6638.2% vs 0.3 prior
Features share
37.1%
+37.1 pp vs prior window
Fixes share
8.7%
−82.5 pp vs prior window
Work mix
37.1% Features4.8% Maintenance42.1% Tests7.2% Docs8.7% Fixes
35 commits over 90 days, ending 2026-09-30.
Daily performance
Daily ETV, stacked by Features, Maintenance, Tests, Docs and Fixes.
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
| Repo | Commits | ETV |
|---|---|---|
| react-native | 35 | 22.9 |
Most impactful commits
Top 10 by ETV in the last 90 days.
- 8.4ETVfeat(iOS) swiftpm support in iOS (#57332) Summary: ⚠️ This is a stacked PR — review/merge the base PRs first Builds on, and should land after (bottom-up): 1. https://github.com/react/react-native/issues/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. https://github.com/react/react-native/issues/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. https://github.com/react/react-native/issues/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. https://github.com/react/react-native/issues/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. 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/<SwiftName>/ 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 - **Tool docs** (usage, quick start, architecture): [`scripts/spm/__doc__/spm-scripts.md`](https://github.com/react/react-native/blob/chrfalch/swift-package-manager/packages/react-native/scripts/spm/__doc__/spm-scripts.md) - **Header-paths contract** (how the five artifacts serve headers): [`scripts/spm/__doc__/spm-header-paths-contract.md`](https://github.com/react/react-native/blob/chrfalch/swift-package-manager/packages/react-native/scripts/spm/__doc__/spm-header-paths-contract.md) - **RFC** (updated): [react-native-community/discussions-and-proposals#994](https://github.com/react-native-community/discussions-and-proposals/pull/994) ## Changelog: [IOS] [ADDED] - `npx react-native spm` command + SwiftPM package-generation tooling (opt-in; CocoaPods stays supported) Pull Request resolved: https://github.com/react/react-native/pull/57332 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 init`→`spm 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. Reviewed By: mdvacca Differential Revision: D111449548 Pulled By: cipolleschi fbshipit-source-id: 87a873bf5c8be2d60f7893611e16924bfa730a46github.com-facebook-react-native · 47fad096 · 2026-07-15
- 3.5ETVDerive an SPM library's Swift name from its podspec (#58290) Summary: An autolinked library's SwiftPM target name is also its header import prefix, so deriving it from the npm package name was wrong for most of the ecosystem (`react-native-svg` publishes `RNSVG`, not `ReactNativeSvg`) and wrong silently — no error, just headers nobody can import under the expected name. ## How: This PR reads the podspec file on scaffolding, and will use the name from the podspec if available. In addition it deprecates the react-native.config.js `spm` section in favor of the library's `package.json` file. ### Configuration A package's SwiftPM settings now live in `swiftpmConfig` in its `package.json`, following `codegenConfig`'s conventions: `name`, `dependencies`, `autolinkingPlugin` and `scaffold` for a library, `modules` and `denyPlugins` for an app. The `spm` block in react-native.config.js is deprecated — still read, so nothing breaks, but it warns once per file and package.json wins field by field. ### Resolving A name resolves from `swiftpmConfig.name`, then the deprecated `spm.name`, then the podspec's `header_dir` or name, then the npm name. The podspec is thereby transitional rather than permanent: `spm scaffold` records the name it derived as `swiftpmConfig.name` in the library's package.json, so the next run needs no podspec to name it. It never overwrites a name the library already declares, never records a name guessed from the npm name, and reports what it did. ### Failsafety A prefix Swift cannot spell is normalized to the identifier SwiftPM would compile it as, with a warning. A reserved name or two deps landing on one name is a hard error naming swiftpmConfig.name as the fix; the scope-borrowing that auto-corrected collisions is removed, since a name the build invents is a name no #import can predict. Collision checks key on SwiftPM's c99 name, so react-native-svg and react_native_svg no longer pass and then compile as one module. ### Implementation Name resolution reads the two podspec fields it needs with the regex parser, so it adds no `pod ipc spec` spawn, and the full read is memoized on the resolved path so one run reads a podspec once across name resolution, header search paths and scaffolding. `rn-tester` and the Apple test library move to the new location. ## Changelog: [IOS] [FIXED] - Read SwiftPM name from podspec and store in package.json when scaffolding Pull Request resolved: https://github.com/react/react-native/pull/58290 Test Plan: ✅ Unit tests Reviewed By: cortinico Differential Revision: D119159904 Pulled By: cipolleschi fbshipit-source-id: 59a7c62ecb601498060a5721f362dae8c5cc92degithub.com-facebook-react-native · 1cfc5f29 · 2026-09-09
- 1.6ETVfeat(swiftpm): let autolinking plugins declare build-time script phases (#57757) Summary: **SwiftPM has no equivalent of CocoaPods' `script_phase`, so a framework that generates content at build time can't get one.** The first casualty is expo-constants: nothing writes `EXConstants.bundle/app.config`, which shows up at runtime as *"Unable to find the embedded app config"*. This adds a 6th field to the SwiftPM autolinking plugin contract: ```js scriptPhases: [{ id: 'expo-constants.app-config', // stable: ledger key + deterministic UUID seed name: 'Generate Expo app.config', // Xcode's display name script: '…', position: 'end', // 'end' (default) | 'beforeCompile' inputPaths: ['$(SRCROOT)/../app.json'], outputPaths: ['$(TARGET_BUILD_DIR)/…/EXConstants.bundle/app.config'], alwaysOutOfDate: true, }] ``` The plugin returns data; RN validates it, records it to a `.spm-plugin-script-phases.json` sidecar (written even when empty, so removing a plugin clears stale entries), and `spm add`/`update` emits one `PBXShellScriptBuildPhase` per entry — tracked in `.spm-injected.json` by `id`, so `update` reconciles and `deinit` reverts. ### Verified end to end by the Expo team On a real Expo app, against a local cut of this branch. A `position: 'end'` phase lands last, after the JS bundle phase: ``` 5. Resources 6. Bundle React Native code and images 7. [Expo Dev Launcher] Strip Local Network Keys for Release 8. Generate Expo app.config ← last ``` `BUILD SUCCEEDED`, and `EXConstants.bundle/app.config` is written with `sdkVersion: 56.0.0` — precisely the value whose absence caused the original bug. Red baseline confirmed first: before the declaration, the same app built with `0 script phase(s)`, an empty sidecar, and no `EXConstants.bundle` at all. They also independently confirmed `deinit` leaves zero residue, `add` is idempotent (same sha1 twice), and no phase duplicates. Their side is expo/expo#47647. ### Design decisions worth a reviewer's attention - **`end` appends at the true end of `buildPhases`**, which is *after* the JS bundle phase — where expo-constants must write, since it targets `$TARGET_BUILD_DIR`. Anchoring relative to the Frameworks phase (the obvious-looking choice) lands it *before* the bundle phase, because real template order is `Sources, Frameworks, Resources, Bundle React Native code and images`. - **`beforeCompile` anchors after RN's own "Sync SPM Autolinking" phase**, which must stay first since it regenerates autolinking — a plugin phase ahead of it would run against stale generated content. The anchor chains forward so declared order survives. Position and relative order are re-derived every sync, so a phase dragged by hand in Xcode returns to its declared slot. - **Validation is fatal**, matching `flavoredFrameworks` rather than the lenient `watchPaths`. A silently dropped phase means the content is never written and the app fails at runtime with no build-time signal — which is the bug being fixed. - **`id` is the ledger key and the UUID seed.** The charset allows a scoped npm name (`expo/log-box`) but excludes `:`, which separates the `plugin:<id>` seed. `__proto__`/`constructor`/`prototype` are rejected because `plainObject['__proto__'] = v` vanishes through `JSON.stringify`, which would record a phase that `deinit` could never remove. - **A plugin-supplied `name` reaches pbxproj comments**, and those are scanned by single-line regexes. What lands in a comment is therefore normalized: a name containing `*/`, `{` or `,` otherwise produced a brace-unbalanced project Xcode couldn't open, or an orphan phase `deinit` reported removing but didn't. The full name still goes verbatim into the `name` field Xcode displays. The same normalization now covers generated-source filenames, which had the identical hole. ### Also fixed in passing `add → update → deinit` did **not** restore `project.pbxproj` byte-for-byte, even with zero script phases: the second run's marker forgot what the first had created, leaving an empty `packageReferences` / `packageProductDependencies` and the generated `.xcscheme` behind. The created-record now carries forward and `scheme.created` is sticky. Two guards came with that, both tested: a created array field is removed only when it is **empty** after RN's own members come out (so a package a user added to it survives `deinit`), and the scheme is deleted only if it is still RN's own (so a scheme the user has taken over is left alone). ## Changelog: [Internal] [Added] - SwiftPM: autolinking plugins can declare build-time script phases via `scriptPhases` Pull Request resolved: https://github.com/react/react-native/pull/57757 Test Plan: `yarn jest packages/react-native/scripts` → **853 tests**, all green. The SwiftPM suite specifically went from **462 → 637** tests. Written red first throughout. Coverage includes: - one declared phase → exactly one `PBXShellScriptBuildPhase`, correct `name`/`shellScript`/serialized paths; `alwaysOutOfDate` emitted as unquoted `1` only when set - `end` lands last; `beforeCompile` lands after the sync phase and before Sources; declared order preserved for multiple phases of each position and for a mix; a changed `position` is re-seated on the next sync - add / update-in-place / remove keyed on `id`; unchanged re-sync byte-identical; `deinit` byte-identical with no orphan object or section - a 17-row hostile-`name` matrix (`{ } ( ) , ; = */ /* * /`, unbalanced quote, tab, unicode, 300 chars, a name that normalizes to empty) × {balanced after add, byte-identical re-inject, clean deinit} - scripts containing quotes, backslashes, newlines and `$(VAR)` round-trip through emission, refresh and deinit - contract validation: 16 malformed-entry cases, duplicate `id` across plugins, reserved ids, scoped ids accepted, `:` rejected - `add → update → deinit` byte-identity with zero phases and with two **Not covered by unit tests, deliberately:** that `end` runs after the JS bundle phase. The `plain-app.pbxproj` fixture has no bundle phase, so it is unassertable here — this is disclosed in a comment at the test rather than papered over, and is exactly what the Expo verification above establishes. **Known limitation, not addressed here:** against a project Xcode has previously saved, `add → deinit → add` is structurally identical (same UUIDs, same reference counts) but not byte-identical — Xcode writes multi-line dicts in sorted order, the injector writes single-line dicts in insertion order, which shows up as formatting churn in a committed `project.pbxproj`. Pre-existing for every object the injector emits, not specific to script phases, and filed separately. ## Note on landing order This touches `generate-spm-xcodeproj.js` and `spm-pbxproj.js`, which https://github.com/react/react-native/issues/57744 and https://github.com/react/react-native/issues/57756 also touch — including the same marker-write block. All three are cut independently from `main`; whichever lands first, I'll rebase the others. Happy to restack in whatever order is easiest to review. Reviewed By: fabriziocucci Differential Revision: D114318236 Pulled By: cipolleschi fbshipit-source-id: 4aa7958c302323299eda9db17adcec0d86edeebegithub.com-facebook-react-native · 6ff47ef9 · 2026-08-10
- 1.4ETVrefactor(ios): remove clang VFS overlay, resolve headers via new ReactNativeHeaders framework (#57285) Summary: The prebuilt `React.xcframework` previously 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 `-ivfsoverlay` flags 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's `React.framework` carries every `<React/...>` header plus a framework module map, so `#import <React/...>` and `import React` resolve through `FRAMEWORK_SEARCH_PATHS` automatically. - **`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. - This makes `ReactNativeDependencies` binary-only. No clang VFS overlay, no per-target `-ivfsoverlay` flags. 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): - Private headers (`RCTBridge+Private.h` + the Fabric `RCTComponentView*` family) are exposed in the `React` module map — modular where safe, `textual` where they reach C++ — so frameworks like Expo compile unchanged, incl. Swift access to `RCTBridge.moduleRegistry`. - `React_RCTAppDelegate-umbrella.h` is re-emitted (derived from the live header set) for consumers probing it via `__has_include`. - Sources shipping under multiple include spellings (`React/X.h` + legacy `CoreModules/…`, `RCTImage/…`, bare aliases — 116 today) keep content at ONE module-owned spelling; other spellings become generated redirect shims, so `-fmodules` consumers cannot hit duplicate declarations. - The `React-RCTFabric` facade re-vends `RCTFabricComponentsPlugins.h` at `header_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 (the `React` module, every namespace module, an Expo-shaped ObjC++ fixture, and a Swift `moduleRegistry` fixture). Fail-closed guards cover header collisions, allowlist drift, and missing OR undeclared third-party deps namespaces (the latter surfaced `SocketRocket`, which is deliberately NOT relocated — the real pod vends it, and textual copies collide under `use_frameworks`; the gate asserts its absence). **Key changes** - **New**: `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). - **Removed**: `vfs.js`, VFS types in `types.js`, and the VFS processing/flag-injection paths in `rncore.rb` and `xcframework.js`. - **Updated**: `React-Core-prebuilt.podspec` (vends both xcframeworks, flattens `ReactNativeHeaders` headers into `Headers/` via `prepare_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). - Added facades to the Podspecs that shouldn't be in use when running using precompiled frameworks to satisfy dependencies as empty pod-specs (with the single `RCTFabricComponentsPlugins.h` re-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 Pull Request resolved: https://github.com/react/react-native/pull/57285 Test Plan: - [x] rn-tester builds against the prebuilt `React-Core-prebuilt` pod (Debug + Release) with no `-ivfsoverlay` flags present in the generated xcconfigs. - [x] rn-tester builds against React native source code (without any prebuilt artifacts) - [x] `#import <React/...>`, `import React;`, and the relocated namespaces (`<react/...>`, `<yoga/...>`, `folly`/`glog`) all resolve. - [x] Prebuilt tarball contains both `React.xcframework` and `ReactNativeHeaders.xcframework`; pod install flattens headers into `React-Core-prebuilt/Headers`. - [x] Switch RN-tester between Debug/Release and verify that both `React.xcframework` and `ReactNativeHeaders.xcframework` are changed between debug and release correctly. - [x] `headers-verify.js` gate green on Debug and Release composes (`-Werror=non-modular-include-in-framework-module` never trips in consumer builds). - [x] `private/helloworld` builds against the prebuilt core (CocoaPods path). - [x] Expo SDK compiles against the prebuilt artifacts (private headers, `React_RCTAppDelegate` umbrella probe, Fabric quoted imports). Reviewed By: fabriziocucci Differential Revision: D111448598 Pulled By: cipolleschi fbshipit-source-id: 72bc2f37765ad425722161a038e410ba976858a5github.com-facebook-react-native · 376bd0e4 · 2026-07-15
- 1.2ETVDerive the generated manifests' iOS platform floor from the app (#58379) Summary: The SwiftPM manifests React Native generates — the `Autolinked` aggregate, the per-dependency synth packages, and the scaffolded community packages — hardcoded `platforms: [.iOS(.v15)]`. SwiftPM refuses to link a product whose minimum platform is above the depending target's, so any self-managed package with a higher floor could not be autolinked. Every Expo package declares iOS 16.4, so a stock Expo app on 0.87.1 fails at build time with: ``` error: The package product 'Expo' requires minimum platform version 16.4 for the iOS platform, but this target supports 15.0 (in target 'AutolinkedAggregate' from project 'Autolinked') ``` The app itself is correct (`IPHONEOS_DEPLOYMENT_TARGET = 16.4`); the only disagreement was the generated manifests. The floor is now the app's own `IPHONEOS_DEPLOYMENT_TARGET`, read from the injected `.xcodeproj`: - Target selection: the injection marker's `targetUuid`, else the application target named by `--product-name`, else every application target. - Per configuration: the target's own value, else the project-level configuration of the same name. The lowest value wins, since one manifest floor has to hold for every configuration. - Clamped to React Native's minimum (15.1, `min_ios_version_supported`) and emitted in string form (`.iOS("16.4")`), because the `.vNN` enum form cannot express a minor version. `setup-apple-spm.js` resolves the value once per `add`/`update`/`sync`/`scaffold` run, logs it (`iOS deployment target: 16.4 (from MyApp.xcodeproj)`), and passes it to the generator, the sync script, and the scaffolder via a new `--ios-deployment-target` flag. The flag value is sanitized at the generator boundary as well. `SCAFFOLDER_VERSION` is bumped to 20 so existing scaffolds regenerate with the new floor. A floor set in an `.xcconfig` the configuration is based on is honored, including `#include` chains (target literal → target xcconfig → project literal → project xcconfig). A floor supplied through a build-setting variable (`$(MY_FLOOR)`) is not resolved and falls back to the minimum; the log line says so. `spm add` and `spm update` also refresh the `platforms:` element of existing scaffolded manifests (those carrying the scaffolder marker, including the pre-v20 `.iOS(.v15)` form, over the same expanded dependency set the scaffolder uses, transitive SwiftPM dependencies included) without creating new scaffolds or touching anything else in them. Known limitation, documented in `scripts/spm/__docs__/spm-scripts.md`: the project file is not one of the auto-sync build phase's staleness inputs, so a deployment target changed after `spm add` takes effect on the next `spm update`. The default floor moves from 15.0 to 15.1 for apps without a readable project setting; React Native already requires 15.1 for the app target, so this is not observable. ## Changelog: [IOS] [FIXED] - SwiftPM: generated manifests derive their iOS platform floor from the app's `IPHONEOS_DEPLOYMENT_TARGET` instead of hardcoding iOS 15, so dependencies with a higher minimum (e.g. Expo, 16.4) can be autolinked Pull Request resolved: https://github.com/react/react-native/pull/58379 Test Plan: Unit tests were written first and seen failing, then made green: ``` yarn jest --no-cache -i packages/react-native/scripts/spm Test Suites: 37 passed, 37 total Tests: 1733 passed, 1733 total ``` New/updated coverage: `ios-deployment-target-test.js` (pbxproj reading: target-level, project-level fallback, xcconfig chain with `#include` and `<group>` anchoring, minimum across configurations, marker/`--product-name`/all-targets selection, invalid values, clamping, unreadable project), `scaffold-package-swift-test.js` (emitted floor, refresh of existing scaffolded manifests), `generate-spm-autolinking-test.js` (aggregate and synth templates, `--ios-deployment-target` through `main()`), `scaffold-package-swift-test.js`, `sync-spm-autolinking-test.js` (flag forwarding), `setup-apple-spm-test.js` (resolution and log line). ESLint (`--max-warnings 0`), Prettier, and Flow are clean on the touched files. Not run here: an end-to-end Xcode build of an Expo app on this branch. The failing scenario and the fix were verified by the Expo team against 0.87.1 with the same manifest change patched in locally (build proceeds past SwiftPM resolution into compiling `Expo`). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed By: cortinico Differential Revision: D119653699 Pulled By: cipolleschi fbshipit-source-id: 7c69dd4d97bed719f7bbd28fb0832cdf2f151f55github.com-facebook-react-native · 92564f33 · 2026-09-15
- 1.2ETVResolve SwiftPM manifest naming collisions (#58044) Summary: A dependency's Swift name is derived from its npm package name with the scope dropped, which makes two collisions unavoidable: `powersync/react-native` derives `ReactNative`, one of React Native's own names, and `a/foo` and `b/foo` both derive `Foo`. Either was emitted into the package graph as-is, and SwiftPM then failed deep inside dependency resolution with a duplicate-name error that named neither the library nor the react-native.config.js that caused it. This was seen in the PR here https://github.com/powersync-ja/powersync-js/pull/1076 - and was hard to fix. This PR fixes this by adding support for prefixing the name with the scope if the name without scope crashes. In addition the same logic is added when two packages have names that collide with each other. If the name given by the SwiftPM scaffolder doesn't work for you - you can use the `spm.name` field in the `react-native.config.js` file to set a specific name (see SwiftPM docs). ## Changelog: [IOS] [FIXED] - Resolve Swift manifest naming collisions Pull Request resolved: https://github.com/react/react-native/pull/58044 Test Plan: ✅ Unit tests green Reviewed By: mdvacca, cortinico Differential Revision: D117178818 Pulled By: cipolleschi fbshipit-source-id: 23269f4d959b84ac484be1911e8a863c74008ff9github.com-facebook-react-native · 962aeec5 · 2026-08-25
- 0.6ETVfix(swiftpm): fix two ways array build settings were mishandled (#57744) Summary: Two bugs in `addArrayStringValues`, which adds members to an array build setting (`HEADER_SEARCH_PATHS`, `OTHER_LDFLAGS`, `FRAMEWORK_SEARCH_PATHS`, `LD_RUNPATH_SEARCH_PATHS`). Both hit real projects; neither was visible from the existing fixture. **1. A promoted scalar was never restored.** A setting that already exists as a scalar gets promoted to a `( … )` array, but that was recorded as a plain member-append — so `deinit` stripped the members and left the array shell plus its injected `"$(inherited)"` behind. Stock Xcode projects hit this: the app template sets `LD_RUNPATH_SEARCH_PATHS = "$(inherited) executable_path/Frameworks";` as a target-level scalar. Fixed by pinning the pre-injection value in `.spm-injected.json` and restoring it in place. It is stored raw (a bare scalar's token runs to the `;`, carrying whitespace that must come back), recorded only if the merge actually changed the field, and not restored if the field is gone. **2. A one-line array was corrupted.** The append anchored on `lastIndexOf('\n', tokenEnd - 1)`, which assumes multi-line. With no newline in the value that lands on the *previous* line, so members were spliced above the field, outside the array: ``` { "/new", ← bare entry in the dict body: invalid pbxproj HEADER_SEARCH_PATHS = ("/vendor", ); ← member never added } ``` The result is a project Xcode cannot open, and `deinit` could not remove the stray line. Xcode writes multi-line arrays, but hand-edited projects and other generators (XcodeGen, Tuist) emit compact ones. Fixed by splicing inline ahead of the `)`; removal gained matching delimiter-anchored patterns, so the span removed is the span inserted. The dedupe parse was also quote-blind — a member holding a quoted comma parsed as two tokens — and is now quote-aware. **Tradeoff:** reversing a promotion rewrites the whole field, so members hand-added to a promoted array afterwards are lost. **Rebase note:** `main` has since grown an overlapping guard (`buildSettingValueTokens`) that skips the append when every value is already present, avoiding the *no-op* promotion. It is kept and complements this change: main still promotes irreversibly when there *is* a fresh value to add to a scalar, which is what the restore here covers. The two records stay mutually exclusive per key, pinned by a test. ## Changelog: [Internal] [Fixed] - SwiftPM: `spm deinit` restores a promoted scalar build setting, and `spm add` no longer corrupts a one-line array Pull Request resolved: https://github.com/react/react-native/pull/57744 Test Plan: `yarn jest packages/react-native/scripts` → **963 tests, 32 suites** green; eslint, prettier and flow clean. Written red first: byte-identical `add` → `deinit` round-trips for each pre-existing shape (absent, multi-line, bare and quoted scalars, and the one-line forms), plus `add` → `update` → `deinit`. The multi-line path is unchanged byte-for-byte, verified by a differential harness over 48 add/remove cases against the previous implementation. Re-verified after the rebase on the committed `HelloWorld.xcodeproj`, driving the real `injectSpmIntoExistingXcodeproj` / `removeSpmInjection`. On `main` a one-line `HEADER_SEARCH_PATHS` gains a bare `"…/autolinking/headers",` entry above the field and never receives the member; with this change it lands inside the array, and a pre-existing scalar comes back exactly. Reviewed By: fabriziocucci Differential Revision: D114317839 Pulled By: cipolleschi fbshipit-source-id: 08bc81f80855721bf2123143063462109a99ac25github.com-facebook-react-native · 8ac3e2fa · 2026-08-18
- 0.6ETVfeat(iOS): ReactNativeDependenciesHeaders sidecar + pure-RN ReactNativeHeaders, published to Maven (#57442) Summary: Step 2 of the prebuilt-deps roadmap: ship the deps headers as a **SwiftPM-ready, self-contained artifact** and make every header namespace have exactly **one physical home**. 1. **New artifact: `ReactNativeDependenciesHeaders.xcframework`** — the binary `ReactNativeDependencies.xcframework` is framework-type, so its root `Headers/` is invisible to SwiftPM binaryTargets (`HeadersPath` is rejected on framework entries; verified empirically). The deps prebuild now emits a headers-only library-type sidecar (stub archives + per-slice `Headers/` + `HeadersPath` — the exact `ReactNativeHeaders` recipe, factored into a shared `headers-xcframework.js` emitter) carrying all seven deps namespaces incl. SocketRocket, with slice parity derived from the binary artifact's Info.plist. Ships inside the deps tarball *and* standalone. 2. **`ReactNativeHeaders` goes pure-RN** — the R2 relocation of deps namespaces (and the `DEPS_NAMESPACES_NOT_RELOCATED` SocketRocket exclusion list) is deleted. Relocated copies are what enabled the SocketRocket dual-copy regression (duplicate `interface` / poisoned module graph under `use_frameworks!`); that bug class is now structurally impossible. Headers gate flipped: deps namespaces must be **absent** from RNH; the sidecar emitter enforces set-equality with `DEPS_NAMESPACES` fail-closed in both directions. On the CocoaPods side, a new `ReactNativeDependenciesUtils.configure_aggregate_xcconfig` injects the deps pod's `Headers/` globally (aggregate + every pod target), mirroring the rncore injection — this replaces the folly/glog resolution pods previously got via the flattened `React-Core-prebuilt/Headers`. 3. **CI: prebuilt + dynamic-frameworks lane** — the regression's exact config had no coverage (the `test-ios-rntester` action hard-coupled `use-frameworks:true` to source builds). New `use-prebuilds` input; `test_ios_rntester`'s dynamic cells now consume the workflow-built prebuilt artifacts. 4. **Maven publishing** — `ReactNativeHeaders` and `ReactNativeDependenciesHeaders` publish standalone on `react-native-artifacts` (classifiers `reactnative-headers-*`, `reactnative-dependencies-headers-*`); `verifyArtifactsAreOnMaven` now HEAD-checks every classifier tarball instead of only the POM. Stacked on https://github.com/react/react-native/issues/57440. The SwiftPM preview (https://github.com/react/react-native/issues/57332) rebases on top and wires the sidecar as its 5th binaryTarget. ## Changelog: [IOS] [CHANGED] - Prebuilt artifacts: ReactNativeHeaders is pure-RN; third-party deps headers ship in the new ReactNativeDependenciesHeaders.xcframework sidecar (and the ReactNativeDependencies pod), published standalone to Maven Pull Request resolved: https://github.com/react/react-native/pull/57442 Test Plan: - Headers gate: include-health, structural (deps absent from RNH, byte-matched module maps), and compile smokes (React module + 14 namespace modules + Expo-shape ObjC++/Swift fixtures vs the deps include path) — ALL PASSED - jest: 33/33 (`scripts/ios-prebuild/__tests__`, incl. new sidecar set-equality tests) - ESLint (`--max-warnings 0`), Prettier, Flow (`yarn flow-check`): clean - E2E (locally built artifacts): rn-tester prebuilt static ✅, prebuilt `USE_FRAMEWORKS=dynamic` ✅ (the regression config — verified `React-Core-prebuilt/Headers` contains no deps namespaces and the deps pod serves all seven), helloworld static ✅, source-core + prebuilt-deps ✅ (React compiled from source resolves folly via the deps pod), source-mode control with unchanged dependency graph ✅ - Sidecar inspected: per-slice `HeadersPath`, 7 namespaces, slice parity with the binary - Publication validated end-to-end with `publishReleasePublicationToMavenLocal`: all 12 files + POM land with the expected classifier names 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed By: fabriziocucci Differential Revision: D111449462 Pulled By: cipolleschi fbshipit-source-id: e1217d14c0588d00a207622d346c9e6f4705a95dgithub.com-facebook-react-native · 6aa147f6 · 2026-07-15
- 0.4ETVrefactor(iOS): include resources like bundles into the precompiled XCFrameworks (#57305) Summary: **Depends on https://github.com/react/react-native/issues/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 Pull Request resolved: https://github.com/react/react-native/pull/57305 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. Reviewed By: fabriziocucci Differential Revision: D111448862 Pulled By: cipolleschi fbshipit-source-id: a95360f51cc1131510ef03f7e48c689575d1c22dgithub.com-facebook-react-native · 77b75122 · 2026-07-15
- 0.4ETVfix(swiftpm): persist --config-command so the in-build sync keeps using it (#57756) Summary: **`spm add --config-command` worked once, then broke every build.** An app that replaces `react-native-community/cli` autolinking — an Expo app, for instance — has to override the autolinking config command, which `--config-command` (and `RCT_SPM_AUTOLINKING_CONFIG_COMMAND`) exists to do. But the flag was never stored anywhere. So: 1. `npx react-native spm add --config-command '[...]'` → succeeds, writes a valid project ✅ 2. Build in Xcode → the "Sync SPM Autolinking" phase re-derives `autolinking.json`, doesn't know about the flag, falls back to the default command, and fails ❌ ``` PhaseScriptExecution failed with a nonzero exit code → Sync SPM Autolinking → 'npx --no-install react-native-community/cli config' exited with status 1 ``` The only real workaround was exporting `RCT_SPM_AUTOLINKING_CONFIG_COMMAND` into Xcode's environment — i.e. committing it to `.xcode.env` — which shouldn't be necessary when you already passed a flag. Reported by the Expo team while testing SwiftPM. **Fix:** pin the command into the `.spm-injected.json` marker at `add`/`update` time, and read it back on later runs — the same set-or-preserve pin the neighbouring `artifactsVersionOverride` already uses. Resolution order, unchanged at the front and only extended at the back: ``` --config-command → RCT_SPM_AUTOLINKING_CONFIG_COMMAND → pinned value → default ``` **Both input routes persist.** The help text advertises the env var as an equivalent way to supply the command, so pinning only the flag would have left half the documented interface broken in exactly the same way — export the env var, run `spm add`, and the build phase (which does not inherit your shell) still fails. The pin therefore stores the *resolved* command from either route. Two subtleties worth a reviewer's eye: - `generateAutolinkingConfig` resolves the env var *internally* when no explicit command is passed, so handing it the pin would silently outrank a developer's env override. The read path therefore **withholds** the pin while the env var is set, letting the existing precedence do its job. The four order cases are tested, including that a whitespace-only env var falls through to the pin rather than stranding it. - A pinned value is re-validated through the same `parseConfigCommandJson` the flag goes through, so a hand-edited or corrupt marker degrades to the env/default command instead of injecting a bogus argv into a build. Because this is now persistent state, `add`/`update` logs one line when the command comes from the pin, naming `.spm-injected.json` — a stale pin should be diagnosable from build output rather than invisible. There is no "clear" verb short of `deinit`, same as the version pin; that is noted in the marker comment. 122 added lines across three source files. No new mechanism, no changes to the sync scripts, and nothing baked into the generated build phase. **Noticed while here, filed separately, deliberately not fixed:** `readArtifactsVersionOverride` — the version pin this is modelled on — has **no production caller**. Only its write half is wired, and two comments claim the build-time sync reads it. Those comments are corrected here (they misled me while writing this); wiring the version pin up is its own change. ### This isn't blocking anyone Expo's SwiftPM verification is **not blocked** on this, so it needn't be rushed. The env-var half of the override already works at build time: adding ```sh export RCT_SPM_AUTOLINKING_CONFIG_COMMAND='["node","…/expo-modules-autolinking.js","react-native-config","--json","--platform","ios"]' ``` to the app's `.xcode.env` (or `.xcode.env.local`) gets the command into the sync phase, because the generated phase sources both files before dispatching. That is what unblocks Expo today, and it is exactly the "commit an env var to `.xcode.env`" step this PR removes the need for. One caveat that argues for fixing it properly rather than documenting the workaround: the phase sources `.xcode.env` **only when `NODE_BINARY` is unset** (`nodeAndRnDirPreamble`). An app that sets `NODE_BINARY` as an Xcode build setting — a documented RN practice — never sources those files, so the workaround silently does nothing there and the build fails with no hint as to why. Persisting the flag doesn't depend on any of that plumbing. ## Changelog: [Internal] [Fixed] - SwiftPM: persist `spm --config-command` so the in-build autolinking sync keeps using it Pull Request resolved: https://github.com/react/react-native/pull/57756 Test Plan: `yarn jest packages/react-native/scripts` → **31 suites, 703 tests** (25 new). Each new test written red first, covering: - the command round-trips through `.spm-injected.json` (marker content asserted) - `add` → `update` **without** the flag keeps the pin; a later flag overwrites it - `add` with **only the env var** set pins the env-derived command, and a later run with neither flag nor env resolves it back - all four resolution-order cases, including that **the env var beats the pin**, and that a whitespace-only env var pins nothing and falls through - an invalid non-blank env var still fails loud rather than pinning garbage - a corrupt or hand-edited pin (bare string, `[]`, non-string member, empty-string member, object) degrades to the default rather than throwing - `deinit` drops it with the marker Not covered: no test drives a real `sync` end to end, since that needs artifacts, codegen and a real pbxproj. The two halves are tested separately against the same marker field — the injector writes `configCommand`, and the resolver reads it. The original failure was reported from a real Expo app build; confirmation that this fixes that build is still pending on the Expo side. Reviewed By: fabriziocucci Differential Revision: D114317929 Pulled By: cipolleschi fbshipit-source-id: a0fe47de3da55b25e320b000a2b0b1b44b88af9agithub.com-facebook-react-native · ed3229aa · 2026-07-31