41137 Commits
Author SHA1 Message Date
React Native Bot a59eff64fa Release 0.87.1
#publish-packages-to-npm&latest
latest v0.87.1
2026-08-26 12:51:22 +00:00
Riccardo Cipolleschi 1a6b526d0e [0.87] Regenerate ReactNativeApi snapshot with CI toolchain 2026-08-26 11:39:54 +01:00
Riccardo Cipolleschi dda5dd3846 [0.87] Regenerate ReactNativeApi snapshot 2026-08-26 11:26:41 +01:00
Riccardo Cipolleschi 476887c8d5 Merge remote-tracking branch 'refs/remotes/origin/pr/58052' into 0.87-stable 2026-08-26 11:13:15 +01:00
Christian Falch e6e404eb6c Hold an app's spm.modules names to the same rules as a library's (#58060)
Summary:
An app declares extra native modules through spm.modules in its react-native.config.js. Those names went into the generated package graph unvalidated, and two of the ways they can go wrong fail silently.

This PR fixes this by using the same Swift name collision detection/resolving as we introduced in https://github.com/react/react-native/issues/58044

> **NOTE:** https://github.com/react/react-native/issues/58044 must be merged before this one so that we can change the base branch for this one to `main`

## Changelog:
[IOS] [FIXED] - Reject colliding or invalid spm.modules names instead of silently dropping a module from the build

Pull Request resolved: https://github.com/react/react-native/pull/58060

Test Plan: ✅ Unit tests/CI

Reviewed By: mdvacca

Differential Revision: D117360298

Pulled By: cipolleschi

fbshipit-source-id: d2531d51b6d371cf6bc98dc85c9071ee81fb7a37
(cherry picked from commit 39cd1dfc4c)
2026-08-26 11:13:08 +01:00
Christian Falch c9511295ad Resolve 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).

[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: 23269f4d959b84ac484be1911e8a863c74008ff9
(cherry picked from commit 962aeec5e5)
2026-08-26 11:13:05 +01:00
Christian Falch 33624ceda4 Honor a library's spm.name when scaffolding its Package.swift (#58033)
Summary:
The scaffolder derived every target name with `toSwiftName(dep.name)`, ignoring the `spm.name` override in the library's own react-native.config.js.

A library setting that override got a manifest whose product name differed from the name the autolinker registers it under, and SPM failed resolution on `.product(name: "X", package: "X")` — the exact mismatch the override exists to prevent.

This setting is documented - but not followed up in the implementation.

Use the name the autolinker already resolved (`dep.swiftName`), falling back to `toSwiftName` when a caller runs the translation without it. Sibling references resolve the same way, through the new `SpmScaffoldSpec.siblingSwiftNames`.

Fixed after community feedback here: https://github.com/powersync-ja/powersync-js/pull/1076

## Changelog:

[IOS] [FIXED] - Fixed not reading spm.name from react-native.config.js when scaffolding the Swift manifest

Pull Request resolved: https://github.com/react/react-native/pull/58033

Test Plan: ✅ unit tests

Reviewed By: zeyap

Differential Revision: D116794063

Pulled By: cipolleschi

fbshipit-source-id: 8b2f204efbc4d7537839ccef5b90cf4eba0d27bf
(cherry picked from commit 482acee824)
2026-08-26 11:12:53 +01:00
Christian Falch 617a82ff29 Fail with clear error message when a dep depends on an autolinking plugin host (#58087)
Summary:
A library can ship with its own SwiftPM autolinking plugin, making the RN autolinker skip generating a target for it since the plugin now owns its native contribution.

If another library depends on a library that contains such a plugin, we currently just fail without any explanation to why and how we can fix this.

This PR diagnoses this at the declaration level, checking if a dependency is an existing target or not - emitting a clear error about what happens and why it happens.

## Changelog:

[IOS] [FIXED] - Added hard fail and clear error message when an autolinking plugin host is referenced as a dependency

Pull Request resolved: https://github.com/react/react-native/pull/58087

Test Plan: ✅ Unit tests passes

Reviewed By: Abbondanzo

Differential Revision: D117196705

Pulled By: cipolleschi

fbshipit-source-id: fa7865ec6a5c07232e67240d322c315dad570d17
(cherry picked from commit 90a539c92d)
2026-08-26 11:12:53 +01:00
Christian Falch 5e24a6952f Drop the inert publicHeadersPath from spm.modules (#58059)
Summary:
When using `react-native.config.js` to declare app-side modules using the `spm.modules` field, there is a field for providing the public header files for a module which is not used by the code. This field does not have any meaning either, a local app-side module is registered through module discovery anyway.

This PR removes this field from the SPM config.

## Changelog:

[IOS] [FIXED] - Removed unused field spm.modules.publicHeaderFiles from react-native.config.js's spm section

Pull Request resolved: https://github.com/react/react-native/pull/58059

Test Plan: ✅ Unit tests/CI

Reviewed By: christophpurrer

Differential Revision: D116935666

Pulled By: cipolleschi

fbshipit-source-id: 78c9576cb6bd9996a4972ba6f38f21bb4743d4af
(cherry picked from commit 5168f5f831)
2026-08-26 11:12:52 +01:00
Christian Falch 40af900182 Read both export styles from a library's react-native.config.js (#58034)
Summary:
When saving settings in `react-native.config.js`, we should support all styles of exported data:

```js
module.exports = { spm: { name: 'worklets' } };   // old style
export const spm = { name: 'worklets' };          // new style, with a label
export default { spm: { name: 'worklets' } };     // new style, no label — "just this"
```

We only read the two first in the SwiftPM pipeline, silently ignoring anything written with `export default`.

This PR fixes this by adding a catch that tries to decode the default export.

Fixed after community feedback here: https://github.com/powersync-ja/powersync-js/pull/1076

[IOS] [FIXED] - SwiftPM pipeline now reads react-native.config.js with default exports correctly

Pull Request resolved: https://github.com/react/react-native/pull/58034

Test Plan: ✅ Added and ran unit tests.

Reviewed By: christophpurrer

Differential Revision: D116935856

Pulled By: cipolleschi

fbshipit-source-id: 7ebf7af3b3aaee2ffb506f94a15fabc244a4db30
(cherry picked from commit ed76640ba6)
2026-08-26 11:12:46 +01:00
Christian Falch cdb180ce4f Derive the generated SPM manifests from shared name constants (#58035)
Summary:
After community feedback from this one https://github.com/powersync-ja/powersync-js/pull/1076 we've decided to fix the issue of clashing name constants - to avoid the SwiftPM pipeline scaffolding packages that collides with built-in packages.

This PR is the first step fixing this - refactoring the names of internal packages into constants.

## Changelog:

[IOS] [FIXED] - Created constants for internal SwiftPM packages so that we can create guards to avoid collisions

Pull Request resolved: https://github.com/react/react-native/pull/58035

Test Plan: ✅ Unit tests

Reviewed By: Abbondanzo

Differential Revision: D116936194

Pulled By: cipolleschi

fbshipit-source-id: 2882e512489cc4784a09aeb03769e97d64d286f7
(cherry picked from commit d36aa4961d)
2026-08-26 11:12:26 +01:00
Christian Falch f4cdbc403f docs(ios-prebuild): correct how SwiftPM consumes the prebuilt React headers (#58007)
Summary:
`ios-prebuild/__docs__/README.md` said `React.framework`'s headers-spec layout
"is what both CocoaPods and SwiftPM consume". That is right for CocoaPods and
misleading for SwiftPM: `React.xcframework` is not a member of the Swift package
graph at all, so nothing on the SwiftPM side reads its framework module map.

What actually happens is a staging step on the consumer side —
`stageReactHeadersTarget` in `scripts/spm/flavored-frameworks.js` copies
`React.framework/Headers` into `ReactHeadersTarget/include/React` and rewrites
`framework module React` to a plain `module React`, which is then vended as the
`ReactHeaders` target. The prebuild output is still the source of those headers,
which is why the sentence was nearly right; the consumption path is what differs.

Says so, and keeps the CocoaPods half explicit about `FRAMEWORK_SEARCH_PATHS` so
the two paths read as the distinct mechanisms they are.

## Changelog:

[Internal] - Clarify that SwiftPM consumes the prebuilt React headers through a
staged `ReactHeaders` target, not through the XCFramework's module map

Pull Request resolved: https://github.com/react/react-native/pull/58007

Test Plan:
Docs only. Verified against `scripts/spm/generate-spm-package.js`, whose generated
`ReactNative` manifest declares exactly three headers-only products and no runtime
`binaryTarget`, and against `stageReactHeadersTarget`, which does the copy and the
module-map rewrite only after Debug and Release are asserted to expose identical
headers. Prettier clean — the file was formatted before this change and still is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Reviewed By: fabriziocucci

Differential Revision: D116598314

Pulled By: cipolleschi

fbshipit-source-id: 3b58101e64410d35f0346c696992dda8261e00a4
(cherry picked from commit 0c86952e40)
2026-08-26 11:12:25 +01:00
Christian Falch 115a9ecc26 docs(swiftpm): rename __doc__ to __docs__, drop two superseded docs, correct the rest (#58006)
Summary:
Rebased onto `main` after https://github.com/react/react-native/issues/57757, https://github.com/react/react-native/issues/57756, https://github.com/react/react-native/issues/57762 and https://github.com/react/react-native/issues/57744 landed. Because this
PR renames `__doc__` and Prettier-formats every file, a hunk-level rebase was the
wrong tool — it stopped on the second of nine commits and the reformat commit then
conflicted with everything after it. So the history is **replayed** on top of the
new `main` instead, and reordered to put Prettier *before* the content edits, which
makes the substantive diff reviewable rather than tangled with reflow.

Verified nothing those four PRs added was lost: every heading on `main` survives
except the two this PR deliberately renames, and their facts — the
`artifactsVersionOverride` pin, the autolinking-config-command pin, the
pre-injection build-setting values, and the promoted-scalar caveat — are carried
into the new "Files the tool touches" table rather than sitting in a row this PR
deletes.

Housekeeping and accuracy pass over `packages/react-native/scripts/spm/__docs__`.
Docs only — no code or test changes.

**Rename.** `scripts/spm/__doc__` was the only `__doc__` directory in the repo;
every other subsystem uses `__docs__` (27+ directories, and the convention
documented in `__docs__/GUIDELINES.md`). The misspelling also meant these files
were **published to npm**: `package.json` includes `scripts/spm` in `files` and
excludes `!**/__docs__/**`, which the misspelled directory dodged.

**Removed two superseded documents.** `rfc-spm-xcframework.md` (707 lines) was a
stale ancestor of RFC0994, still describing `spm init`, xcodeproj *generation*, the
`.xcodeproj.legacy` rename migration, a `spm clean` command that does not exist,
stub `Package.swift` files and VFS overlays. `spm-plugins-assessment.md` evaluated
SwiftPM plugins as a replacement for the injected build phases — conclusion valid,
detail stale (it listed a "Prepare VFS Overlay" phase). Both have had their durable
content folded into RFC0994.

**Corrected the remaining docs against the implementation.** The substantive one:
auto-sync is **two** hooks, not one — a scheme pre-action in the app's *shared*
scheme plus the build phase. The docs described only the phase.

Two claims here are corrections of my own earlier drafts, made after testing them
on `private/helloworld` rather than reading the code:

- Neither hook can bootstrap a clean checkout. With `build/` deleted,
  `xcodebuild -scheme … build` fails in nine lines of log, `Resolve Package Graph`
  first, the pre-action never running. The one-time setup run really is required,
  and the ordering table now shows the measured sequence.
- A sync failure is not unconditionally non-fatal. Exit 2 — a dependency with no
  `Package.swift` — maps to `exit 1` and fails the build deliberately; only other
  non-zero codes warn. As written, the doc also contradicted the exit-2 behaviour
  documented elsewhere in the same file.

Smaller fixes: `hermesvm` → `hermes-engine` (that name is in no code), plugin
`watchPaths` added to the staleness inputs, and the `#auto-sync-build-phase`
anchors left dangling by the heading rename.

**Added a `__docs__/README.md` index**, per `GUIDELINES.md`, plus a **"Files the
tool touches"** table — there was no single answer to what the tool creates or
modifies. Writing it surfaced that `add` creates or appends to `ios/.gitignore`
(documented nowhere), and that `deinit` does not revert that block, anything
`--deintegrate` changed, or a promoted array setting — so the "exact inverse of
`add`" claim needed qualifying in three places.

**Documented `spm.dependencies`**, the SwiftPM analog of a podspec's
`s.dependency`, which was implemented but absent from every doc, and corrected the
scaffold prerequisite: it is `add`/`update` that stops, with a distinct exit code 2,
not the build.

**Prettier-formatted the docs, in its own commit.** These were the only unformatted
markdown files in the repository, so `prettier --list-different "./**/*.md"` goes
from three failures to clean.

## Changelog:

[Internal] - SwiftPM docs: rename `__doc__` to `__docs__`, remove two superseded
documents, and correct the rest against the implementation

Pull Request resolved: https://github.com/react/react-native/pull/58006

Test Plan:
- `npx prettier --check "packages/react-native/scripts/spm/__docs__/*.md"` passes;
  `npx prettier --list-different "./**/*.md"` is empty for the whole repo.
- All 19 intra-doc anchor links verified against GitHub's slug rules; none dead.
  Every relative path out of the new README resolves, including the root-index hop.
- `npm pack --dry-run --ignore-scripts` lists no `scripts/spm/__docs__/` entries
  while every `scripts/spm/*.js` still ships.
- The Prettier commit is content-neutral: comparing `[A-Za-z0-9]+` token streams
  across it, all three files are identical word for word.
- Behavioural claims traced to source: `VALID_ACTIONS` in `setup-apple-spm.js`;
  `injectOrCreateScheme` / `addPreActionToScheme` and the `RC -eq 2` branch of
  `buildSyncAutolinkingScript` in `generate-spm-xcodeproj.js`; `BUILTIN_FRAMEWORKS`
  in `flavored-frameworks.js`; `generateXCFrameworksPackageSwift` in
  `generate-spm-package.js`; `ensureGitignoreSpmEntries` and its `action =--sanitized--

Reviewed By: fabriziocucci

Differential Revision: D116598871

Pulled By: cipolleschi

fbshipit-source-id: b1822dcc885b9c882ec3a2626addc42553244b51
(cherry picked from commit a6898cc4cf)
2026-08-26 11:12:25 +01:00
Kudo Chien 5ad5425914 - Keep the quotes on header search paths (#57981)
Summary:
a regression from https://github.com/react/react-native/commit/a8156acf8bbc9ee15901cf0d8935d73454768aa7 and breaks react-native nightly build at expo: https://github.com/expo/expo/actions/runs/31990917142/job/95274214849

the `shellsplit` will stripe quotes and break paths with spaces like `Swift Compatibility Header`. this pr tries to re-quote.

here's small demo script

```ruby
require 'shellwords'

# What main does today: shellsplit the existing value, append, write it back.
def main_behaviour(existing, add)
  paths = existing || []
  paths = Shellwords.shellsplit(paths) if paths.is_a?(String)
  (paths + add).uniq
end

# What this PR does: quote only the paths we add, never touch what is there.
def this_pr(existing, add)
  quoted = add.map { |path| "\"#{path}\"" }
  case existing
  when nil
    quoted
  when Array
    existing + quoted.reject { |path| existing.include?(path) }
  else
    ([existing] + quoted.reject { |path| existing.include?(path) }).join(" ")
  end
end

# Xcode joins an Array setting with spaces, then splits it on whitespace while
# honouring quotes. This is what the compiler ends up with.
def as_xcode_reads_it(value)
  Shellwords.shellsplit(value.is_a?(Array) ? value.join(" ") : value)
end

ADD = ['$(PODS_ROOT)/ReactNativeDependencies/Headers']

# A pod that exports a Swift compatibility header. The directory name has
# spaces, so the podspec quotes it. Podspecs write this as a String or as an
# Array, and both forms reach add_rn_third_party_dependencies.
SWIFT_HEADER = '${PODS_CONFIGURATION_BUILD_DIR}/MyPod/Swift Compatibility Header'

CASES = {
  'String' => "\"$(PODS_ROOT)/DoubleConversion\" \"#{SWIFT_HEADER}\"",
  'Array'  => ['"$(PODS_ROOT)/DoubleConversion"', "\"#{SWIFT_HEADER}\""],
}

CASES.each do |label, existing|
  puts "#{label} HEADER_SEARCH_PATHS"
  { 'main' => method(:main_behaviour), 'this PR' => method(:this_pr) }.each do |name, fn|
    paths = as_xcode_reads_it(fn.call(existing, ADD))
    verdict = paths.include?(SWIFT_HEADER) ? 'ok' : 'BROKEN'
    puts "  #{name.ljust(7)} -> #{paths.size} paths, #{verdict}: #{paths.inspect}"
  end
  puts
end
```

output

```
String HEADER_SEARCH_PATHS
  main    -> 5 paths, BROKEN: ["$(PODS_ROOT)/DoubleConversion", "${PODS_CONFIGURATION_BUILD_DIR}/MyPod/Swift", "Compatibility", "Header", "$(PODS_ROOT)/ReactNativeDependencies/Headers"]
  this PR -> 3 paths, ok: ["$(PODS_ROOT)/DoubleConversion", "${PODS_CONFIGURATION_BUILD_DIR}/MyPod/Swift Compatibility Header", "$(PODS_ROOT)/ReactNativeDependencies/Headers"]

Array HEADER_SEARCH_PATHS
  main    -> 3 paths, ok: ["$(PODS_ROOT)/DoubleConversion", "${PODS_CONFIGURATION_BUILD_DIR}/MyPod/Swift Compatibility Header", "$(PODS_ROOT)/ReactNativeDependencies/Headers"]
  this PR -> 3 paths, ok: ["$(PODS_ROOT)/DoubleConversion", "${PODS_CONFIGURATION_BUILD_DIR}/MyPod/Swift Compatibility Header", "$(PODS_ROOT)/ReactNativeDependencies/Headers"]

```

## Changelog:

[IOS][FIXED] - Keep the quotes on header search paths containing spaces in `add_rn_third_party_dependencies`

Pull Request resolved: https://github.com/react/react-native/pull/57981

Test Plan: apply the patch on create-expo-nightly and the ios build should pass https://github.com/expo/expo/pull/49023

Reviewed By: javache

Differential Revision: D116600347

Pulled By: cipolleschi

fbshipit-source-id: 73c3c7560e5c4970b678298c05743d1e2404cc9b
(cherry picked from commit 71c6bfbfdc)
2026-08-26 11:12:25 +01:00
Alex Hunt 11ef67b94d Type Animated.Value.addListener callback (#57992)
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/57992

Addresses a type narrowness regression (Strict API vs legacy manual types), raised by user feedback:
https://github.com/react-native-community/discussions-and-proposals/discussions/1015#discussioncomment-18053943

Changelog:
[General][Fixed] - **Animated**: `Animated.Value.addListener()` types its callback payload as `{value: number}` rather than `any`

Reviewed By: vzaidman

Differential Revision: D116440622

fbshipit-source-id: b85c7d7fbd8e81cc2e953c9a6f60701f3991ba6e
(cherry picked from commit 3eab03df4c)
2026-08-26 11:12:20 +01:00
Alex Hunt a1c3cfe630 Type Animated.event() as an event handler instead of any (#57991)
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/57991

Addresses a type narrowness regression (Strict API vs legacy manual types), raised by user feedback:
https://github.com/react-native-community/discussions-and-proposals/discussions/1015#discussioncomment-18053943

Changelog:
[General][Fixed] - **Animated**: `Animated.event()` is typed as an event handler rather than `any`

Reviewed By: vzaidman

Differential Revision: D116440620

fbshipit-source-id: 4ba629c511fcf28981700d1bc841a4ce8dcf17f8
(cherry picked from commit 3eb330a365)
2026-08-26 11:11:56 +01:00
Christian Falch f473b0367b fix(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: 08bc81f80855721bf2123143063462109a99ac25
(cherry picked from commit 8ac3e2fa58)
2026-08-26 11:11:36 +01:00
Radosław Rolka f0800d9ec4 Set SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG when injecting SPM (#57914)
Summary:
Swift's `#if DEBUG` is gated by SWIFT_ACTIVE_COMPILATION_CONDITIONS, not by GCC_PREPROCESSOR_DEFINITIONS (which only reaches C/ObjC/C++). The app template does not commit that setting; CocoaPods injects it at `pod install` time (react_native_post_install -> set_build_setting
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ["$(inherited)", "DEBUG"] on Debug).

An app set up with the experimental SwiftPM support never runs CocoaPods, so `#if DEBUG` is false even in a Debug build: AppDelegate.swift's `bundleURL()` skips the Metro URL and falls back to a main.jsbundle a Debug build never produced, and the app dies at launch with "No script url provided ... unsanitizedScriptURLString = (null)" while Metro is running.

Info: https://github.com/react-native-community/template/pull/244

## Changelog:

Inject the setting from `spm add`/`update` alongside the other React build settings, into debug-flavored configurations only (the same flavorForBuildConfiguration test that selects the debug xcframeworks), so a config linking the debug binaries also compiles its Swift with DEBUG.

<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

[IOS] [FIXED] - Set SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG when injecting SPM

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests

Pull Request resolved: https://github.com/react/react-native/pull/57914

Test Plan:
1. Build spm-only rn app in debug
    - App will fail to connect to the Metro
2. Apply changes and run `react-native spm`
3. Build again
    - App will work as expected in debug and Metro will be connected

Reviewed By: Abbondanzo

Differential Revision: D115736551

Pulled By: cipolleschi

fbshipit-source-id: 1e42ca26ac1992bfa827ce85a63cf4564d313e15
(cherry picked from commit 4433cdbe62)
2026-08-26 11:11:35 +01:00
Aditya Singh 6e007c30cb fix: make an interrupted React-Core-prebuilt swap recoverable (#57831)
Summary:
`replace-rncore-version.js` writes `.last_build_configuration` only after it has already replaced `React.xcframework`, so a build cancelled between those two steps leaves the marker naming a flavor that is no longer on disk, and when the marker is absent entirely the script assumes the on-disk flavor is Debug. Either state makes the next build for that configuration take the "no need to replace" path and link against the other configuration's core, which fails with undefined C++ symbols for `Props`, `DebugStringConvertible` and the Fabric vtables, and nothing corrects it afterwards because the pod directory survives a clean.

This change writes the marker before the framework is touched, using a value that is not a valid configuration, so a later run can tell that the previous swap did not finish and replaces again. The fresh install path now also records the configuration it assumed instead of leaving that state implicit, which is sound because a swap can no longer leave the marker missing.

Fixes https://github.com/react/react-native/issues/57598.

## Changelog:

[IOS] [FIXED] - Recover React-Core-prebuilt configuration swaps that were interrupted before the marker was updated

Pull Request resolved: https://github.com/react/react-native/pull/57831

Test Plan:
Four new cases in `packages/react-native/scripts/__tests__/replace-rncore-version-test.js`, driven through the real CLI entry point the podspec `[RNCore]` build phase uses, so no new export was needed and the production diff is logic only. Against unmodified upstream two of them fail, one with `ENOENT` on `.last_build_configuration` because the skip path never wrote the marker, and one showing the marker still naming the stale flavor after a failed swap. With the change the file is 8 passed, including all 4 pre-existing tests. `prettier --check` is clean on both files.

The actual iOS link failure needs a prebuilt-core CocoaPods install and an Xcode build, so that was not reproduced locally.

Reviewed By: cipolleschi

Differential Revision: D114900617

Pulled By: fabriziocucci

fbshipit-source-id: 5d7f9b7be9cf1dd1728daa1e98ff51b87b924d2c
(cherry picked from commit a772a7ca16)
2026-08-26 11:11:35 +01:00
Christian Falch 1216c1b651 feat(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.

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.

- **`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.

`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).

[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.

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: 4aa7958c302323299eda9db17adcec0d86edeebe
(cherry picked from commit 6ff47ef958)
2026-08-26 11:11:29 +01:00
Christian Falch 8fafd46b69 fix(swiftpm): actually read the artifacts version pin back (#57762)
Summary:
**`spm add --version <ver>` pinned a value that nothing ever read back.**

The marker write has been there all along, and so has the reader — `readArtifactsVersionOverride()` in `spm/generate-spm-xcodeproj.js`, exported and unit-tested. It just had **zero production callers**. Every reference to it was a test.

`determineVersion` — the only resolver — went straight from the flag to `package.json`:

```js
let version = args.version;      // --version
if (version == null) {
  version = pkgJson.version;     // react-native/package.json; marker never consulted
}
```

That value picks which artifact slots the project gets wired to. So:

1. `spm add --version 0.88.0-nightly-…` → wires the nightly's slots, pins the label ✅
2. `spm update` (no flag) → silently resolves `package.json`'s version instead, re-pointing the project at different slots, while the marker still claims the nightly ❌

`--version` was effectively single-use. In this monorepo `package.json` is `1000.0.0`, which has no published artifacts, so a flagless run after a pinned `add` fails outright — which is why the standing advice has been to pass `--version` on *every* invocation. That advice was working around this bug.

**Fix:** insert the pin between the two existing sources.

```
--version  →  pinned override  →  react-native/package.json
```

15 lines of logic. `spm download` and the scaffold path pick it up for free, since both consume the same resolved value. Because this is persistent state, one line is logged when the pin is the source, so a stale pin is diagnosable instead of silent; there is still no way to clear it short of `deinit`.

**Three comments were actively wrong** and are corrected here: the reader's doc block, the marker-field comment, and `findInjectedXcodeproj`'s comment all asserted that the build-time sync calls this via `readArtifactsVersionOverride`. It does not and never did — the `sync` action returns before artifacts are resolved at all. Those comments are what made the gap invisible; they misled me while investigating.

## Changelog:

[Internal] [Fixed] - SwiftPM: `spm --version` now sticks, so a later run without the flag keeps using the pinned artifact version

Pull Request resolved: https://github.com/react/react-native/pull/57762

Test Plan:
`yarn jest packages/react-native/scripts` → **31 suites, 684 tests**, all green.

New tests, written red first — the load-bearing one failed with `Expected: "0.80.0" / Received: "1000.0.0"`, i.e. exactly the reported bug:

- `--version` given → wins, even with a different value pinned
- no flag, pin present → the pinned version is used
- no flag, no pin → `package.json`'s version (unchanged behaviour)
- no flag, corrupt or absent marker → `package.json`'s version, no throw
- the log line fires only when the pin is the source

The three fallback cases passed *before* the fix too, which is the point — they pin today's behaviour so this change can't regress it.

## Note on landing order

Touches `setup-apple-spm.js` and `spm/generate-spm-xcodeproj.js`, which https://github.com/react/react-native/issues/57744, https://github.com/react/react-native/issues/57756 and https://github.com/react/react-native/issues/57757 also touch. All are cut independently from `main`; whichever lands first, I'll rebase the rest. https://github.com/react/react-native/issues/57756 is the closest relative — it does the same wiring for `--config-command`, which had the identical write-only-pin shape.

Reviewed By: fabriziocucci

Differential Revision: D114318107

Pulled By: cipolleschi

fbshipit-source-id: 157da5b2eaaef9adee924eaa1832b7a38b008f4c
(cherry picked from commit b0b5409e42)
2026-08-26 11:11:16 +01:00
Nick Cernera f4ef5bdde6 Fix ConcurrentModificationException in IntentModule.getInitialURL re-entrancy (#57667)
Summary:
`IntentModule.onHostResume()` iterates `pendingOpenURLPromises` (an `ArrayList`) directly and calls `getInitialURL()` for each pending promise. When `getCurrentActivity()` returns `null` at that moment — e.g. a deep link or notification tap landing mid activity-transition during a rapid pause/resume — `getInitialURL()` re-enters `waitForActivityAndGetInitialURL()`, which calls `pendingOpenURLPromises.add(promise)` on the very list being iterated. The next iteration then throws `java.util.ConcurrentModificationException`:

```
java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
    at java.util.ArrayList$Itr.next(ArrayList.java:967)
    at com.facebook.react.modules.intent.IntentModule$waitForActivityAndGetInitialURL$1.onHostResume(IntentModule.kt:90)
```

The `synchronized(this@IntentModule)` guard does not prevent this: the re-entrancy is on the same thread, which already holds the (reentrant) lock, so no second lock acquisition happens. The crash is the `ArrayList` iterator's `modCount` check, not a cross-thread race.

The fix snapshots the pending promises into a local copy, clears the shared list, and nulls the listener **before** draining. Re-queued promises then land in the now-empty `pendingOpenURLPromises` and register a fresh listener for the next resume, instead of mutating the list being iterated. Behaviour is otherwise unchanged.

## Changelog:

[ANDROID] [FIXED] - Fix ConcurrentModificationException when getInitialURL re-enters during onHostResume

Pull Request resolved: https://github.com/react/react-native/pull/57667

Test Plan:
Added `IntentModuleTest.getInitialURL_onHostResumeWithNullActivity_doesNotThrowAndPreservesPromise`, a Robolectric regression test that registers a pending promise while the current activity is `null`, then drives `onHostResume` so the drain re-queues, and asserts the drain does not throw and the promise is preserved (neither resolved nor rejected).

Ran locally against `main` with the exact reproduction scenario:

**With the fix** — passes:
```
$ ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest \
    --tests "com.facebook.react.modules.intent.IntentModuleTest"
BUILD SUCCESSFUL
# tests=1, failures=0, errors=0
```

**Without the fix** (reverting `IntentModule.kt` only) — fails with the exact crash, confirming the test is a genuine regression test:
```
IntentModuleTest > getInitialURL_onHostResumeWithNullActivity_doesNotThrowAndPreservesPromise FAILED
    java.util.ConcurrentModificationException
        at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
        at java.util.ArrayList$Itr.next(ArrayList.java:967)
        at com.facebook.react.modules.intent.IntentModule$waitForActivityAndGetInitialURL$1.onHostResume(IntentModule.kt:90)
```

`ktfmt` (Meta style, matching the repo's `ktfmt` configuration) reports both changed files as already formatted.

Reviewed By: cortinico

Differential Revision: D113595327

Pulled By: fabriziocucci

fbshipit-source-id: b1247cb6e473fd7c550dda8b2b584d720ea3e46c
(cherry picked from commit 20d04aefa1)
2026-08-26 11:11:15 +01:00
jensdev 566f469009 Fix custom font weight rendering heaviest face on iOS Fabric (#57483)
Summary:
On the New Architecture (Fabric), a custom font referenced by its PostScript / full font name (e.g. `fontFamily: "Foo-Medium"`) combined with an explicit non-regular `fontWeight` renders at the heaviest available face (bold/black) instead of the requested weight. This is an iOS-only regression introduced in 0.83; 0.82 was correct. Android is unaffected.

**Root cause.** In `RCTFontUtils.mm`, `RCTFontWithFontProperties()` handles the case where the given `fontFamily` is actually a font name rather than a family name (`fontNames.count == 0`) and resolves the effective weight with:

```objc
fontWeight = (fontWeight != 0.0) ?: RCTGetFontWeight(font);
```

`fontWeight` is a `UIFontWeight` (a `double`: Regular = 0.0, Medium = 0.23, Bold = 0.4, Black = 0.62). The Objective-C "Elvis" operator `A ?: B` evaluates to **`A` itself** when `A` is truthy — and here `A` is the *comparison* `(fontWeight != 0.0)`, a `BOOL`. So whenever a weight was set (e.g. 0.23), `fontWeight` was reassigned to `1.0` (heavier than Black), and the subsequent "closest weight in the family" search always picked the heaviest face.

This was introduced by the automated implicit-bool-conversion sweep in https://github.com/react/react-native/issues/53591 (D81571883), which rewrote the original correct line `fontWeight ?: RCTGetFontWeight(font)` into `(fontWeight != 0.0) ?: …`. Making the truthiness check explicit is fine as a *condition*, but with `?:` the left-hand side is also the *returned value*, so the numeric weight got replaced by the boolean.

**Fix.**

```objc
fontWeight = (fontWeight != 0.0) ? fontWeight : RCTGetFontWeight(font);
```

For a `double`, `(A != 0.0) ? A : B` is exactly equivalent to the original `A ?: B`, so this restores the 0.81/0.82 behavior while keeping the explicit `!= 0.0` form used elsewhere in the file. The no-weight case is unchanged: `RCTResolveFontProperties` fills an unspecified weight with `UIFontWeightRegular` (0.0), so the weight is still inferred from the font name via `RCTGetFontWeight(font)` in that case. The legacy (Paper) path in `React/Views/RCTFont.mm` uses a different construct and is not affected — consistent with his only reproducing on the New Architecture.

## Changelog:

[iOS] [Fixed] - Custom fonts with an explicit fontWeight no longer render at the heaviest weight on the New Architecture

Pull Request resolved: https://github.com/react/react-native/pull/57483

Test Plan:
Fixes https://github.com/react/react-native/issues/54934.

New Architecture, iOS, with a custom font bundled and referenced by its PostScript name,
e.g. `<Text style={{ fontFamily: 'Foo-Medium', fontWeight: '500' }}>`:

| Version | Result |
| --- | --- |
| 0.82 | renders at the requested medium weight ✅ |
| 0.83 / `main` (before fix) | renders bold/black ❌ |
| removing `fontWeight` on 0.83 | renders correctly ✅ (confirms the weight branch is the culprit) |

After this change the text renders at the requested weight on both the old and new architecture.

Reviewed By: cortinico

Differential Revision: D111217210

Pulled By: javache

fbshipit-source-id: 76858e1b83f8f1032cb599aec6c4b0d49a6a7427
(cherry picked from commit 918fb15bfe)
2026-08-26 11:11:15 +01:00
Riccardo Cipolleschi 861e2d0bde [LOCAL] Bump hermes-v1 to 250829098.0.17 2026-08-21 14:19:25 +01:00
Riccardo Cipolleschi 363a116632 [0.87] Use macOS 26 runners for iOS E2E tests (#58058) 2026-08-21 14:17:43 +01:00
Nycollas b8d50a9dff Fix Hermes bytecode version mismatch in SwiftPM Release builds (#57928)
Summary:
`react-native spm add`'s Release builds crash on launch with:

```
Compiling JS failed: Wrong bytecode version. Expected 99 but got 98
```

This happens because the SwiftPM integration resolves the Hermes **runtime**
(the downloaded `hermes-engine.xcframework`) and the Hermes **compiler** (the
`hermesc` binary that turns the JS bundle into bytecode) from two independent,
unsynced sources:

- `download-spm-artifacts.js`'s `resolveHermesArtifact()` picked the runtime
  by querying the `hermes-compiler` package's `latest-v1` dist-tag on the npm
  registry **live, at build time**.
- `generate-spm-xcodeproj.js`'s `resolveHermesCliPathSetting()` (and
  `react-native-xcode.sh`, for the CocoaPods-free fallback) points
  `HERMES_CLI_PATH` at the `hermes-compiler` package **already installed in
  this project's own `node_modules`** — whatever got pinned the last time
  `npm install` ran.

If the `latest-v1` dist-tag advances on npm between `npm install` and the
Release build (which happens routinely as new Hermes builds are published),
the downloaded VM and the locally pinned `hermesc` fall out of sync and the
app crashes at launch. `react-native-xcode.sh` already documents this exact
invariant ("react native pins the hermes-compiler version, so the compiler's
bytecode version always matches the prebuilt hermes VM artifacts") — SwiftPM's
artifact download just wasn't honoring it.

This PR makes `resolveHermesArtifact()` read the pinned `hermes-compiler`
version from `node_modules` first (the same `require.resolve` lookup already
used for `HERMES_CLI_PATH`), so the runtime download and the compiler always
agree. It falls back to the previous `latest-v1` npm lookup only when
`hermes-compiler` isn't locally resolvable (e.g. `USE_HERMES=false` apps that
never installed it). Explicit `HERMES_VERSION` overrides (`nightly`,
`latest-v1`, a literal version) are unchanged.

Fixes https://github.com/react/react-native/issues/57917.

## Changelog:

[IOS] [FIXED] - Fix Hermes runtime/compiler version mismatch causing "Wrong bytecode version" crashes in SwiftPM Release builds

Pull Request resolved: https://github.com/react/react-native/pull/57928

Test Plan:
Added unit tests covering the new local-resolution path, the fallback when
`hermes-compiler` isn't installed, and confirming existing `HERMES_VERSION`
overrides still take precedence over the local pin.

```
$ node_modules/.bin/jest packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js
Test Suites: 1 passed, 1 total
Tests:       70 passed, 70 total

$ node_modules/.bin/flow check packages/react-native/scripts/spm/download-spm-artifacts.js
No errors!

$ node_modules/.bin/eslint packages/react-native/scripts/spm/download-spm-artifacts.js packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js
(no output — clean)

$ node_modules/.bin/prettier --check packages/react-native/scripts/spm/download-spm-artifacts.js packages/react-native/scripts/spm/__tests__/download-spm-artifacts-test.js
All matched files use Prettier code style!
```

Reproduced the crash and confirmed the fix end-to-end using the public
reproducer linked from the issue
(https://github.com/marandaneto/react-native-087-swiftpm-hermes-bytecode-repro):

- Before the fix: `npm run reproduce` builds successfully but launching the
  app in the iOS Simulator crashes with `Compiling JS failed: Wrong bytecode
  version. Expected 99 but got 98`.
- After applying the equivalent fix to the reproducer's installed
  `react-native` copy: the log shows `Using locally pinned hermes-compiler:
  250829098.0.16`, and both the debug and release Hermes runtime artifacts
  resolve to that exact version — matching the `hermesc` used for
  `HERMES_CLI_PATH`. `xcodebuild ... -configuration Release` succeeds, and
  the app installs and launches cleanly on an iPhone 17 Pro (iOS 26.5)
  simulator with no crash.

Reviewed By: cortinico

Differential Revision: D115859923

Pulled By: cipolleschi

fbshipit-source-id: 1b65a7aa28f502374a3553c1849b8ff29b5afd10
2026-08-17 13:17:14 +00:00
React Native Bot c6ebb060d9 [LOCAL] Bump Podfile.lock 2026-08-11 15:34:25 +00:00
React Native Bot 4bc2473f5d Release 0.87.0
#publish-packages-to-npm&latest
v0.87.0
2026-08-11 13:31:13 +00:00
React Native Bot a1942a74d6 [LOCAL] Bump Podfile.lock 2026-08-04 23:56:15 +00:00
React Native Bot 01f752d73b Release 0.87.0-rc.4
#publish-packages-to-npm&next
v0.87.0-rc.4
2026-08-04 22:03:14 +00:00
Christian Falch 24353daa87 fix(iOS): keep prebuilt Headers/ in place on a Debug/Release swap (#57814)
Summary:
Fixes https://github.com/react/react-native/issues/57803. An iOS Release build can fail in `PrecompileModule React` with seven `include of non-modular header inside framework module` errors — but only when the build follows a Debug/Release configuration switch.

`replace-rncore-version.js` deleted and recreated `Pods/React-Core-prebuilt/Headers/` on a swap. That directory holds `module.modulemap`, which `rncore.rb` activates on every target through `-fmodule-map-file`. Nothing orders an unrelated target's dependency scan against this script phase, so a scan can run while the module map is missing. The React module is then precompiled without it, and `<yoga/...>`, `<react/...>` and `<RCTDeprecation/...>` resolve non-modularly.

Those headers never needed replacing. The prebuild compose job emits one set of ReactNativeHeaders for both configurations, so they are identical in the Debug and Release tarballs — only the compiled framework differs. This replaces `React.xcframework` and nothing else.

## Changelog:

[IOS] [FIXED] - Keep the prebuilt `Headers/` in place on a Debug/Release configuration switch so the React explicit module still resolves its module map

Pull Request resolved: https://github.com/react/react-native/pull/57814

Test Plan:
The premise, on the published 0.87.0-rc.3 artifacts (`ios-arm64_x86_64-simulator`):

| compared between the Debug and Release tarballs | result |
| --- | --- |
| `ReactNativeHeaders…/Headers/module.modulemap` | identical |
| `React.framework/Modules/module.modulemap` | identical |
| `ReactNativeHeaders…/Headers` tree (`diff -rq`) | 0 differences |
| `React.framework/Headers` tree (`diff -rq`) | 0 differences |

The reproducer from https://github.com/react/react-native/issues/57803, on Xcode 26.3 with CocoaPods 1.15.2:

| build | result |
| --- | --- |
| 0.87.0-rc.3 | **FAIL** — exit 65, 7 errors |
| 0.87.0-rc.3 + this PR | **PASS** — `** BUILD SUCCEEDED **`, 0 errors |

The swap still does its job in the passing build — it logs `Replacing React-Core-prebuilt/React.xcframework`, and the installed binary is the Release one:

```
installed:   55225ccbc283c57c614ff4caf263cb63bad3828240e62cee8893e7001774bd6c
rc3 release: 55225ccbc283c57c614ff4caf263cb63bad3828240e62cee8893e7001774bd6c
rc3 debug:   516215801a6f8a86640aae13c2f2de1bbdb95189edf124e208f528b1497c7e4c
```

A Release→Debug swap was verified the same way. Across a swap, `Headers/module.modulemap` keeps its inode while `React.xcframework` gets a new one.

## Unit tests

Adds a unit test for the script, 4 cases: correct framework installed, `Headers/module.modulemap` untouched, an Expo-generated `React-use-frameworks.modulemap` left in place, and a fail-closed case on a tarball with no `React.xcframework`. The script needed a `require.main === module` guard and one export to be importable.

```
js1 test xplat/js/react-native-github/packages/react-native/scripts/__tests__/replace-rncore-version-test.js
→ 4 passed, 4 total
```

The module-map case is a real regression test, not just a pin. Restoring the pre-fix delete-and-recreate makes it fail on the inode assertion while the other three keep passing:

```
✕ leaves Headers/module.modulemap untouched
  Expected: 735095485
  Received: 735095515
```

That only works because the fixture tarball also ships `ReactNativeHeaders.xcframework`. Without it the pre-fix code throws its fail-closed error before reaching the assertion, so the test would go red for the wrong reason and would not actually be guarding #57803.

The Expo case covers behaviour this diff removes the explicit protection for. The old save-and-restore of `React-use-frameworks.modulemap` (ada39220a0) is unnecessary now that only `React.xcframework` is replaced, but nothing else pins it.

## Argument parsing

`yargs` parsing moved inside the `require.main === module` guard, so importing the module no longer parses `process.argv`. Verified in both directions.

The command line still performs the swap end to end:

```
$ node replace-rncore-version.js -c Release -r <version> -p <podsRoot>
Replacing React-Core-prebuilt/React.xcframework
Updating React-Core-prebuilt/.last_build_configuration with Release
Done replacing React Native prebuilt

installed binary: binary-Release
module.modulemap inode before=735141703 after=735141703
last_build marker: Release
```

Importing with hostile argv (`-c` collides with jest's `--config`) has no side effects:

```
$ node -e "process.argv = ['node','jest','-c','jest.config.js','--version']; require('./replace-rncore-version.js')"
imported OK, exports: replaceRNCoreConfiguration
```

`arc lint` is clean on both files.

Reviewed By: zeyap

Differential Revision: D114735639

Pulled By: fabriziocucci

fbshipit-source-id: 35ead7dae9ce7ad7160005a15ecb3975817ae728
2026-08-04 20:20:03 +00:00
Christian Falch 329f8640bf fix(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: a0fe47de3da55b25e320b000a2b0b1b44b88af9a
(cherry picked from commit ed3229aa64)
2026-08-03 18:02:24 +01:00
Christian Falch 4dd4d31458 Write the prebuilt module-map flag to OTHER_CPLUSPLUSFLAGS too (#57742)
Summary:
`add_prebuilt_header_search_paths` (`scripts/cocoapods/rncore.rb`) injects the prebuilt ReactNativeHeaders module map into `OTHER_CFLAGS` and `OTHER_SWIFT_FLAGS`, but not `OTHER_CPLUSPLUSFLAGS`. C++ and ObjC++ translation units therefore lose modular resolution of the relocated `react/`, `yoga/` and `RCTDeprecation/` namespaces.

This is a regression: 0.86's VFS implementation (`add_vfs_overlay_flags`) wrote all three compiler-flag keys, the modular rewrite writes two. This adds the third back, reusing the existing quoted `module_map_flag`.

Xcode's `OTHER_CPLUSPLUSFLAGS = $(OTHER_CFLAGS)` default doesn't cover for it, because an explicit value *replaces* the C flags instead of merging — and React Native sets the key explicitly, both in `new_architecture.rb` and in the `pod_target_xcconfig` of ReactCodegen, React-RCTAppDelegate and React-RCTAnimatedModuleProvider.

To be clear about what this is: a correctness fix, not a bug fix. No build fails today, because `HEADER_SEARCH_PATHS` is injected unconditionally and the includes still resolve textually. What is lost is modular resolution.

## Changelog:

[IOS] [FIXED] - Write the prebuilt module-map flag to `OTHER_CPLUSPLUSFLAGS` so C++/ObjC++ sources resolve the relocated namespaces modularly

Pull Request resolved: https://github.com/react/react-native/pull/57742

Test Plan:
On an app using the prebuilt RNCore (`RCT_USE_PREBUILT_RNCORE=1`, artifacts from Maven), counting pods whose xcconfig carries the module-map flag per key:

| | `OTHER_CFLAGS` | `OTHER_CPLUSPLUSFLAGS` | `OTHER_SWIFT_FLAGS` |
|---|---|---|---|
| before | 211 | **0** | 211 |
| after | 211 | 211 | 211 |

`xcodebuild` succeeds after the change (`** BUILD SUCCEEDED **`, 0 errors), so the flag is accepted by `ScanDependencies` and the explicit-module precompile. This was a static-library build; framework linkage was not exercised.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed By: huntie

Differential Revision: D114044307

Pulled By: cipolleschi

fbshipit-source-id: 5c04f753d10876f36d31358593f1dfc29698b633
(cherry picked from commit 14fe96ab51)
2026-08-03 18:01:39 +01:00
Alex Hunt f3e11c2557 Fix compat with extensionless scripts/* imports (#57718)
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/57718

**Context**

Replaces https://github.com/react/react-native/pull/57709, which identified a real bug in 0.87 from the combination of:

- https://github.com/react/react-native/pull/57276
- https://github.com/react/react-native/pull/57652

```
$ npx react-native spm add
error Cannot find module '.../node_modules/react-native/scripts/setup-apple-spm'

$ npx react-native codegen
error Cannot find module '.../node_modules/react-native/scripts/codegen/generate-artifacts-executor'
```

**This diff**

- Fix — and exclusively switch to — extensionless imports rather than requiring `.js`.
- Update in-repo consumers.

The previous single mapping is now an extension-aware mapping:
- `./scripts/*` now appends `.js`, so `react-native/scripts/foo` resolves to `foo.js`.
- `.sh` and `.rb` stay reachable via explicit `./scripts/*.sh` and `./scripts/*.rb` passthrough patterns.

**Impact**

- Explicit `react-native/scripts/*.js` specifiers no longer resolve, so JavaScript paths must be imported without an extension.
- Files under `scripts/` with extensions other than `.js`, `.sh`, or `.rb` are no longer exposed through `./scripts/*`.
    - These have no open source consumers.

Changelog:
[General][Fixed] - (RC4 only, drop for main changelog): `react-native/scripts/*` imports once again expand `.js` extensions
[General][Breaking] - Extensionless `react-native/scripts/*` imports are now **mandated**; explicit `.js` import specifiers are rejected.

Reviewed By: rubennorte

Differential Revision: D113898792

fbshipit-source-id: d72f60be2c08ab97871e336645856c9029e74ae2
(cherry picked from commit 97727ba67f)
2026-08-03 18:00:35 +01:00
Christian Falch 8fffb64288 fix(rn-tester): use framework-style import for RCTFabricComponentsPlugins.h (#57697)
Summary:
rn-tester's `MyNativeView` example imports `RCTFabricComponentsPlugins.h`. The quoted form resolves under CocoaPods (the bare filename is re-vended via `FACADE_REEXPOSED_HEADERS` in `rncore_facades.rb`) and under the internal Meta build (the per-target header generated by buck's `plugins_header`), but not under SwiftPM. Under SwiftPM the header is exposed as an include directory, so only `<React/RCTFabricComponentsPlugins.h>` resolves. The quoted import makes `MyNativeView` fail to compile when rn-tester is converted to SwiftPM (surfaced by the new SPM CI lane in https://github.com/react/react-native/issues/57659), and switching to the angle form alone would instead break the internal build.

The fix uses a `__has_include` conditional: the angle form when it is available (SwiftPM and CocoaPods) with the quoted form as a fallback (the internal build). This matches the existing pattern in rn-tester's own `NativeCxxModuleExample.h` and keeps all three build stacks compiling.

## Changelog:

[INTERNAL] [FIXED] - Make rn-tester's NativeComponentExample header import resolve under SwiftPM, CocoaPods and the internal build

Pull Request resolved: https://github.com/react/react-native/pull/57697

Test Plan:
- Repro on clean `main`: converting rn-tester to SwiftPM (`spm add --deintegrate`) and building fails at `RNTMyNativeViewComponentView.mm` with `fatal error: 'RCTFabricComponentsPlugins.h' file not found`.
- With this change: the same SwiftPM conversion plus `xcodebuild -configuration Debug -sdk iphonesimulator` gives `BUILD SUCCEEDED`.
- Internal build: `buck2 build fbsource//xplat/js/react-native-github/packages/rn-tester:NativeComponentExampleApple` succeeds (it takes the quoted fallback).
- Existing CocoaPods CI (`test_ios_rntester`) covers the CocoaPods path.

Reviewed By: cipolleschi

Differential Revision: D113771766

Pulled By: fabriziocucci

fbshipit-source-id: e4549508f8e56942b45fa128b2391a290499d4a8
(cherry picked from commit 6d0612c27c)
2026-08-03 17:58:32 +01:00
Christian Falch d8b52df809 SPM: allow overriding the autolinking config command (#57662)
Summary:
The SwiftPM autolinking flow hardcodes `react-native-community/cli config` to generate `autolinking.json` (`generate-spm-autolinking-config.js`). Apps that replace community autolinking — most notably **Expo**, which ships `expo-modules-autolinking` instead of `react-native-community/cli` — had no way to override that command, and any failure was swallowed. The result: the config command fails, `autolinking.json` is never written, the `Autolinked` SwiftPM package comes out empty, and `import Expo` (and every Expo module) fails to resolve — surfacing much later as an inscrutable `unable to resolve module dependency: 'Expo'`.

CocoaPods already solves the injection half: `use_native_modules!(config_command = $default_command)` accepts the command as a parameter, so an Expo `Podfile` passes `expo-modules-autolinking react-native-config` in place of the `rncli` default. This PR adds the equivalent hook to the SwiftPM path **and** closes the silent-failure trap.

### 1. Allow overriding the config command

`generateAutolinkingConfig` already accepted a `configCommand` option internally; it was just never reachable. Two ways to supply it, mirroring the CocoaPods hook:

- **`--config-command '<json>'`** — CLI flag taking a JSON array of the argv.
- **`RCT_SPM_AUTOLINKING_CONFIG_COMMAND`** — env var in the same JSON-array format. This is the vehicle for the injected Xcode build phase, which usually can't rewrite the script's argv but can read env.

Both go through one `parseConfigCommandJson` validator (rejects non-JSON, non-arrays, empty arrays, and non-string / empty-string elements, with a `source`-named error). Precedence: **`--config-command` > `RCT_SPM_AUTOLINKING_CONFIG_COMMAND` > default** (local `rncli` → `npx --no-install` fallback, unchanged). JSON (rather than whitespace-splitting) because real commands contain dashed flags and quoted script strings.

The value is the **command to execute** — its stdout is captured as the config JSON and written verbatim — exactly matching CocoaPods, not a precomputed result. An Expo app feeds the same argv it already builds for `use_native_modules!`:

```jsonc
RCT_SPM_AUTOLINKING_CONFIG_COMMAND='["node","--no-warnings","--eval","require('expo/bin/autolinking')","expo-modules-autolinking","react-native-config","--json","--platform","ios","--source-dir","/abs/path"]'
```

(Use `--platform ios`: the generator requires `project.ios.sourceDir` and everything downstream is iOS-only.)

### 2. Fail closed when the config command errors

Previously `main()` swallowed a config-command failure as a warning and continued, which is what let the empty package be produced silently. That policy is now extracted into `generateAutolinkingConfigOrFailClosed`: on a config-command error (non-zero exit, unparseable output, or a config missing `project.ios.sourceDir`) it logs an actionable message naming `RCT_SPM_AUTOLINKING_CONFIG_COMMAND` / `--config-command`, sets `process.exitCode = 2` (a hard Xcode build-phase error, matching the existing `RemoteVersionError` path), and stops.

The guard is deliberately narrow: a **genuinely native-module-free app never reaches the error path** — its command exits 0 with valid, empty-dependency JSON, so the generator returns normally and the legitimate empty-package path stays valid. Only an *erroring* command fails the build.

## Changelog:

[IOS] [ADDED] - Allow overriding the SwiftPM autolinking config command via `--config-command` / `RCT_SPM_AUTOLINKING_CONFIG_COMMAND`
[IOS] [CHANGED] - Fail closed with an actionable error when the SwiftPM autolinking config command fails, instead of silently emitting an empty Autolinked package

Pull Request resolved: https://github.com/react/react-native/pull/57662

Test Plan:
New unit tests, developed red → green:

- `generate-spm-autolinking-config-test.js` — env var honored; explicit `configCommand` beats env; invalid-JSON and invalid-shape (`[]`, `[1,2]`) throw with the source name; env unset falls back to the default command; env state saved/restored per test.
- `setup-apple-spm-test.js` — `parseArgs` parses `--config-command` into an argv array, defaults to `null` when omitted, and throws on an invalid value; `generateAutolinkingConfigOrFailClosed` returns the result on success (exit code untouched), passes `projectRoot`/`configCommand` through, and on a config-command error returns `null`, sets exit 2, and logs an actionable error that names the env var and preserves the underlying cause.

```
$ yarn jest --no-cache -i \
    packages/react-native/scripts/spm/__tests__/generate-spm-autolinking-config-test.js \
    packages/react-native/scripts/spm/__tests__/setup-apple-spm-test.js
Test Suites: 2 passed, 2 total
Tests:       39 passed, 39 total
```

`prettier` and `eslint` clean on all changed files.

Reviewed By: zeyap

Differential Revision: D113554857

Pulled By: cipolleschi

fbshipit-source-id: d0baeeefc91aed144cf9e405e198531ff88088a7
(cherry picked from commit a7ba4ce522)
2026-08-03 17:57:29 +01:00
Christian Falch 0cacb90bc0 SPM: fix raw Flow type annotations in bare-node scripts (#57660)
Summary:
The scripts under `packages/react-native/scripts/spm/` are executed as plain `node` (via `setup-apple-spm.js` during the SwiftPM Xcode build), with **no Babel** to strip Flow. They use Flow-in-comment syntax (`/*: T */`) throughout so they parse un-transpiled.

A handful of uninitialized `let X: T;` declarations had slipped in with **raw** Flow annotations. Node parses the whole file on `require`, so each one throws `SyntaxError: Unexpected token ':'` at load time — taking the entire module down before it can run.

The jest suites didn't catch it because jest runs these files through `react-native/babel-preset`, which strips raw and comment-form annotations alike. Only the bare-`node` shipped path (SwiftPM setup) hits the error.

## Fix

Convert each offending declaration to Flow-comment form. Prettier's `flow` parser only keeps a comment type attached to the binding when the declaration is **initialized**, so each site gets a type-appropriate (inert) initializer — every variable is reassigned in the immediately-following `try`/branch before any use:

| file | lines | form |
|---|---|---|
| `autolinking-plugins.js` | 120, 178 | `/*: unknown */ = undefined` |
| `download-spm-artifacts.js` | 946, 973 | `/*: string */ = ''` |
| `download-spm-artifacts.js` | 1352 | `/*: {...} */ = {}` |
| `generate-spm-autolinking.js` | 429 | `/*: Array<...> */ = []` |
| `generate-spm-xcodeproj.js` | 1802 | `/*: string */ = ''` |
| `generate-spm-xcodeproj.js` | 1808 | `/*: unknown */ = undefined` |
| `generate-spm-xcodeproj.js` | 1863 | `/*: Array<...> */ = []` |
| `scaffold-package-swift.js` | 200, 1129 | `/*: Array<...> */ = []` |
| `scaffold-package-swift.js` | 933 | annotation dropped — Flow infers `PodspecModel` from `readPodspec()` |

## Changelog:
[INTERNAL] [FIXED] - Fix raw Flow type annotations that broke bare-node execution of the SwiftPM setup scripts

Pull Request resolved: https://github.com/react/react-native/pull/57660

Test Plan:
- `node --check` passes on all 14 `scripts/spm/*.js` (previously `autolinking-plugins.js` and `download-spm-artifacts.js` threw `SyntaxError` at parse time).
- `flow focus-check` reports **0 errors** in the touched files.
- Prettier: clean.
- SPM jest suites: green.

## Changelog:

[INTERNAL] [FIXED] - Fix raw Flow type annotations that broke bare-node execution of the SwiftPM setup scripts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed By: cortinico

Differential Revision: D113554595

Pulled By: cipolleschi

fbshipit-source-id: b7fa29dd54266b749e1671333fb0530f6b6a8aab
(cherry picked from commit e9eac585e2)
2026-08-03 17:36:08 +01:00
Christian Falch 034245a015 fix(iOS): stage Hermes headers in prebuild compose job so ReactNativeHeaders resolves <hermes/...> (#57661)
Summary:
The iOS prebuild workflow (`.github/workflows/prebuild-ios-core.yml`) splits into two jobs on separate runners:

- **`build-rn-slice`** stages the hermes-ios headers during setup (`.build/artifacts/hermes/destroot/include/hermes`), but uploads only `.build/headers` + the SPM Products — **not** the hermes artifact.
- **`compose-xcframework`** (fresh runner) runs the compose (`-c` → `buildXCFrameworks`), which computed the hermes include path from `.build/artifacts/hermes/destroot/include`. That directory never exists on the compose runner, so `hermesHeaders` resolved to `null` and the hermes-header fold in `headers-compose.js` was **silently skipped**.

Net effect: the published `ReactNativeHeaders.xcframework` shipped without the `hermes/` namespace, so consumers (e.g. Expo prebuilt) couldn't resolve `<hermes/...>`. Because the value was `null` rather than a bad path, not even the existing warning fired — every nightly regressed silently.

Build-time staging, matching the artifact's self-contained design (the orphaned consumer-side sidecar + health check in `download-spm-artifacts.js` confirm the bake was intended to happen at compose time):

- **Workflow:** `compose-xcframework` now re-stages the hermes-ios headers before composing — a `Set Hermes version` step (`$GITHUB_ENV` doesn't cross jobs) and a `Stage Hermes headers` step that extracts the tarball into `.build/artifacts/hermes`. Both are guarded by the same `cache-hit` condition as the sibling steps.
- **Fail-closed guard:** the inline hermes resolution is extracted into an exported `resolveHermesHeaders(buildFolder, required)` with a `findFirst` fallback (mirroring the consumer-side stager). When a version-stamped CI cut can't find the headers it now **throws** instead of silently shipping without `hermes/`. Gated behind a new `--require-hermes` flag, which the workflow passes only when `version-type` is set — so local `-c` keeps the previous no-fold behavior.

[IOS] [FIXED] - Prebuilt `ReactNativeHeaders.xcframework` now ships the Hermes public headers so consumers resolve `<hermes/...>` out of the box

Pull Request resolved: https://github.com/react/react-native/pull/57661

Test Plan:
- New unit tests for `resolveHermesHeaders` (`__tests__/xcframework-test.js`): resolves at the standard path, resolves via the `findFirst` fallback, returns `null` when absent + not required, throws when absent + required.
- `yarn jest packages/react-native/scripts/ios-prebuild/__tests__/xcframework-test.js --no-cache -i` → 4/4 pass (red before the resolver was exported).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed By: javache

Differential Revision: D113554720

Pulled By: cipolleschi

fbshipit-source-id: 30d21240ae97978e58a95ffdfad4088ed85536d4
(cherry picked from commit 43b44ed7c3)
2026-08-03 17:22:43 +01:00
React Native Bot ed8973a04e [LOCAL] Bump Podfile.lock 2026-07-27 18:00:09 +00:00
React Native Bot b67c5cf8b4 Release 0.87.0-rc.3
#publish-packages-to-npm&next
v0.87.0-rc.3
2026-07-27 15:24:12 +00:00
Zeya PengandAlex Hunt 9a7b821a02 Fix TS exactOptionalPropertyTypes compatibility for generated types (#57628) (#57699)
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/57628

NOTE: Patches over a `flow-api-translator` bug, which I'll fix upstream later. We need to pick this to `0.87-stable` to resolve user integration issues.

**Context**

TypeScript's `exactOptionalPropertyTypes` flag (strict mode) creates a distinction between `foo?: T` and `foo?: T | undefined`.

```js
// Flow's semantics

interface Props {
  onRefresh?: () => void;
}
const a: Props = { onRefresh: undefined }; // ✅ ok
```
```ts
// TypeScript with exactOptionalPropertyTypes: true (i.e. strict mode)

interface Props {
  onRefresh?: () => void;
}
const a: Props = { onRefresh: undefined }; // ❌ error

interface PropsFixed {
  onRefresh?: (() => void) | undefined;
}
const b: PropsFixed = { onRefresh: undefined }; // ✅ ok
```

With this added strictness in TypeScript, our generated types via `flow-api-translator` could create downstream type incompatibility in apps.

**This diff**

Patches the above issue in React Native's Flow → TS `types_generated/` pipeline. We transform all instances to the wider `foo?: T | undefined` format, for maximum compatibility.

**Notes**

`foo?: T [| undefined]` **remains stripped** in the API snapshot (existing transform with the aim of a concise format). There is a net, nonfunctional snapshot diff around function members, which (as a positive result) are re-ordered.

Changelog:
[General][Fixed] - **Strict TypeScript API**: Optional property types are now widened to explicitly include `| undefined` for `exactOptionalPropertyTypes` compatibility

Reviewed By: cipolleschi

Differential Revision: D113030161

fbshipit-source-id: 3ab005edab6b80b18fbb9ae7125ba56e3bd94195

Co-authored-by: Alex Hunt <huntie@meta.com>
2026-07-27 09:35:50 -04:00
Christian Falch 330080c46b fix(iOS): make jsinspector-modern tracing state types move-only for Swift C++ interop (#57605)
Summary:
The nightly-tests job `[ios] react-native-unistyles` fails on Xcode 26.3 with:

```
error: no matching function for call to '__construct_at'
note: in instantiation of member function 'std::vector<...RuntimeSamplingProfile>::vector' requested here
note: in implicit copy constructor for 'facebook::react::jsinspector_modern::tracing::TraceRecordingState' first required here
```

while compiling the **Swift** files of the Unistyles pod. The same failure hits any library built with Swift C++ interop (`-cxx-interoperability-mode=default`) — in practice, every Nitro-based library — against the prebuilt React Native core.

### The error

`TraceRecordingState` and `HostTracingProfile` hold `std::vector`s of move-only types (`RuntimeSamplingProfile` and `FrameTimingSequence` explicitly delete their copy constructors). Here's the C++ subtlety: `std::vector<T>`'s copy constructor is **declared for every `T`** — it only becomes ill-formed when *instantiated*. So the implicit copy constructors of these two structs are not implicitly deleted; they exist as declared-but-broken constructors that hard-error the moment anything asks for a copy.

### Why React Native compiles fine today

Nothing in RN ever asks. Every usage passes these types by reference; the single constructions move. A pure C++ (or ObjC++) build never instantiates the implicit copy constructors, so this code has always compiled — and always would, no matter how much C++ CI you throw at it. The defect is unobservable from within C++.

### What fails, and why now

The prebuilt-core headers now ship as real clang modules. A Swift target with C++ interop imports them (directly or transitively — e.g. via a module member whose `#ifdef __cplusplus` body opens because interop builds modules with C++ enabled), and Swift's ClangImporter surfaces the C++ value types to Swift as copyable. When the consumer's generated interop code then uses such a type as a Swift value — for a Nitro-based library, the nitrogen-generated `*_cxx.swift` bridging does exactly this — the compiler **synthesizes a copy of the type, instantiating the ill-formed implicit copy constructor**. That is the "ask" that plain C++ never makes; on Xcode 26.3 it hard-errors the entire module import, killing every Swift file in the consumer. (Newer Swift toolchains treat such types as non-copyable instead of failing.)

Before the prebuilt-modules work there was no Swift-visible module containing these headers, so no interop consumer ever imported these types — which is why this surfaces now despite the C++ being unchanged.

We deliberately did **not** fix this by removing headers from the module maps: the guarded-C++-in-modules pattern is shared by ~30 legitimately modular headers and is benign in all but this one shape, and experiments showed the type is reachable through multiple independent module surfaces (removing one member just moved the error to the next path).

### The fix

Declare the truth: make both types explicitly move-only.

```cpp
TraceRecordingState(const TraceRecordingState &) = delete;
TraceRecordingState &operator=(const TraceRecordingState &) = delete;
TraceRecordingState(TraceRecordingState &&) = default;
TraceRecordingState &operator=(TraceRecordingState &&) = default;
```

With the copy constructor explicitly deleted, Swift's importer sees a non-copyable type and imports it as such instead of instantiating a broken copy. It is also simply more correct C++: these types were never copyable in practice, and the explicit deletion turns any future accidental copy into a clear compile error at the call site instead of a template backtrace.

Declaring special members makes `HostTracingProfile` a non-aggregate, so its one designated-initializer construction site (`HostTargetTraceRecording.cpp`) is converted to member-wise assignment.

A sweep of the affected header surface (`std::vector`/`std::map`/`std::deque` of move-only element types) found exactly these two types; a follow-up adds a `headers-verify.js` gate that imports the shipped modules under Swift C++ interop at prebuild time, so the next type with this shape fails RN's own CI instead of community nightlies.

## Changelog:

[IOS] [FIXED] - Fix Swift C++-interop build failure (implicit copy constructor of TraceRecordingState/HostTracingProfile) for libraries using cxx interop with prebuilt React Native core

Pull Request resolved: https://github.com/react/react-native/pull/57605

Test Plan:
On a fresh RN-nightly app with stock `react-native-unistyles@3.3.0` + `react-native-nitro-modules`, prebuilt core (`RCT_USE_RN_DEP=1 RCT_USE_PREBUILT_RNCORE=1`), Xcode 26.3:

- **Red**: stock headers reproduce the CI failure exactly (`__construct_at` → `TraceRecordingState`). Fixing only `TraceRecordingState` then surfaces the identical failure on `HostTracingProfile` — confirming the shape, not the type, is the bug.
- **Green**: with both headers fixed (stock module maps, nothing else changed): BUILD SUCCEEDED — zero `__construct_at`, zero `shadowNodeFromValue`, zero module errors.
- All RN-internal usages audited: references and moves only; no behavior change. Plain C++/ObjC++ compilation unaffected by construction.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed By: fabriziocucci

Differential Revision: D112802806

Pulled By: cipolleschi

fbshipit-source-id: d2ae201c1fbb4de040f51ebc88263c7347b5979b
2026-07-27 13:24:52 +00:00
Christian Falch 514f104ca6 fix(iOS): stamp the React Native version in the prebuild compose job (#57603)
Summary:
`prebuild-ios-core.yml` builds the prebuilt iOS core in two jobs: **`build-rn-slice`** compiles the React binary per platform slice, and **`compose-xcframework`** runs afterwards in its **own fresh checkout**, downloads the slice artifacts, and composes the shipped `React.xcframework` + `ReactNativeHeaders.xcframework` — re-deriving the shipped *headers* from that checkout.

The "Set React Native version" step runs only in `build-rn-slice`. So the compiled **binary is stamped**, but the composed **headers are not**: the artifact ships `ReactNativeVersion.h` with the `1000.0.0` dev sentinel, internally inconsistent with its own binary (and with the npm package, which is stamped in its own publish job).

This was latent for as long as the compose job has existed — it only started breaking consumers now because the header-facades / VFS-overlay-removal work changed *which file libraries actually read*. Previously header resolution (the VFS overlay, and the `Pods/Headers/Public` symlinks of the source pods) redirected reads to the **stamped npm copy**, so the sentinel bytes in the tarball were never consumed. With real materialized headers, the prebuilt copy now wins the search path, and any library gating on `REACT_NATIVE_VERSION_MAJOR/MINOR` compiles the wrong branch. That breaks `[ios] react-native-unistyles` in nightly-tests: its `#if REACT_NATIVE_VERSION_MINOR >= 81` sees `MINOR 0` and picks a removed pre-0.81 path (`shadowNodeFromValue`).

**Fix — built-headers overlay (no re-stamp/revert).** The `build-rn-slice` job already stamps its checkout before building and uploads `.build/headers`, and the compose job already downloads it — it was just unused. `stageEntries` now takes an overlay dir and, **for `ReactNativeVersion.h` only**, prefers the built copy (stamped in the slice job) over the source sentinel. Header **layout** is still spec-derived from source (podspec inventory, collision detection, classification), and every **other** header still copies from source — so a stale build tree can never ship wrong header content, only the one build-generated version header is overlaid. No stamping of the compose checkout, no `git revert`, no cache-key desync.

The compose cache key is bumped so pre-fix (unstamped) composed artifacts cached under the old key are not served.

**Guard.** `headers-verify.js` gains `--require-stamped-version` (passed when a `version-type` is set) that hard-fails the compose verification if any composed `ReactNativeVersion.h` still contains the sentinel — turning any future regression into a red prebuild instead of silently broken community libraries.

## Changelog:

[IOS] [FIXED] - Ship a version-stamped ReactNativeVersion.h in the prebuilt iOS core artifacts instead of the 1000.0.0 dev sentinel

Pull Request resolved: https://github.com/react/react-native/pull/57603

Test Plan:
- Local compose with a stamped `ReactNativeVersion.h` seeded into `.build/headers` and the source tree left at the `1000` sentinel: all composed `ReactNativeVersion.h` copies come out stamped (overlay won), while a deliberately **stale** `.build/headers/…/TraceRecordingState.h` is correctly ignored — the composed `TraceRecordingState.h`/`HostTracingProfile.h` come from source, and other headers + module maps/umbrellas are unaffected (structural gate passes).
- Guard: composed layout with a sentinel header → `verifyVersionStamp` throws listing the offending files; stamped → `version stamp OK (N copies checked)`; no effect without the flag.
- Verified against a fresh RN-nightly app + `react-native-unistyles@3.3.0` on Xcode 26.3 (prebuilt mode) and in the Expo prebuilt harness (26.3 + 26.6): stock prebuilt headers reproduce the `shadowNodeFromValue` error; the stamped prebuilt header resolves it.
- End-to-end proof is the next nightly run: the composed tarball must carry a stamped header.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed By: javache

Differential Revision: D113005220

Pulled By: cipolleschi

fbshipit-source-id: 5fbba8bb4e84fd1458a2f4a0baa623cf613b6e3b
2026-07-27 13:24:36 +00:00
Riccardo Cipolleschi 50bad79b42 Fix post-release workflow failures (#57627)
Summary:
Fixes the three post-release failures from the [0.87.0-rc.2 publish run](https://github.com/react/react-native/actions/runs/29775529785/):

- Keep Flow annotations in `verifyArtifactsAreOnMaven.js` as comments so `actions/github-script` can load the file with plain Node.
- Pin the Podfile lock workflow to `macos-15`, which provides the configured Xcode 16.4 version.
- Use the canonical `react/react-native` owner for release asset API operations so upload POST requests are not redirected from the former owner.

## Changelog:

[INTERNAL] [FIXED] - Fix post-release Maven verification, Podfile lock, and release asset jobs

Pull Request resolved: https://github.com/react/react-native/pull/57627

Test Plan:
```sh
node --check .github/workflow-scripts/verifyArtifactsAreOnMaven.js
node --check scripts/releases/upload-release-assets-for-dotslash.js
yarn jest .github/workflow-scripts/__tests__/verifyArtifactsAreOnMaven-test.js scripts/releases/__tests__/upload-release-assets-for-dotslash-test.js --runInBand
```

13 tests and 15 snapshots pass.

Reviewed By: zeyap

Differential Revision: D113032768

Pulled By: cipolleschi

fbshipit-source-id: df5f493603e6c25157fd7660b54aa409dcb742e7
2026-07-27 13:24:23 +00:00
Rubén Norte 65c816879c Fix parentNode for nested root host views like <Modal> (#57623)
Summary:
Pull Request resolved: https://github.com/react/react-native/pull/57623

`NativeDOM::getParentNode` returned the document for any shadow node carrying the `RootNodeKind` trait. That trait is set not only on a surface's actual root node but also on nested host views such as `<Modal>` (and portal/overlay-style components), so their `parentNode`/`parentElement` incorrectly resolved to the document instead of their real containing element.

Because the EventTarget-based event dispatch builds its capture/bubble ancestor path by walking `getParentNode` up the shadow tree, this truncated the path at the modal boundary: a listener registered on an ancestor rendered above the modal never received events (e.g. focus/blur) originating inside it.

Fix by returning the document only for the surface's actual root node, identified via `ShadowNode::sameFamily(*currentRevision, *shadowNode)`. Every other node — including nested root-kind nodes — now reports its real structural parent, while disconnected nodes still report none.

Changelog:
[General][Fixed] - Fix `parentNode`/`parentElement` returning the document instead of the containing element for `<Modal>` and other nested root host views, which severed capture/bubble event propagation to ancestors rendered above them

Reviewed By: javache

Differential Revision: D113012362

fbshipit-source-id: 88043a9b9de128e7c3c96d8516b0950cbd6c3c68
2026-07-22 12:04:45 +00:00
Zeya Peng 651bbdffc4 [0.87] Update RNTester Podfile.lock for 0.87.0-rc.2 (#57624) 2026-07-21 09:13:13 -04:00
React Native Bot 9518500909 Release 0.87.0-rc.2
#publish-packages-to-npm&next
v0.87.0-rc.2
2026-07-20 20:17:41 +00:00
Zeya Peng 17bf68a4c3 [LOCAL] Set .hermesv1version to hermes-v250829098.0.16 (#57620)
Summary:
The Android Gradle build downloads the Hermes source from
https://github.com/facebook/hermes/tarball/<ref>, where <ref> is the contents
of sdks/.hermesv1version. Release tags on facebook/hermes are named
hermes-v<version>, so the bare value 250829098.0.16 404s and breaks the
build_fantom_runner / downloadHermes build. Pin to the hermes-v prefixed tag.

Test Plan:
curl -sL -o /dev/null -w '%{http_code}' \
  https://github.com/facebook/hermes/tarball/hermes-v250829098.0.16
=> 200
2026-07-20 13:58:06 -04:00
Zeya Peng d045236281 Bump hermes version (#57616) 2026-07-20 11:55:24 -04:00
Riccardo Cipolleschi 88f43bc69a [LOCAL] Fix CI: build rntester dynamic-frameworks lane from source (#57615) 2026-07-20 16:41:02 +01:00