Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
58b68d9
Merge React Native 0.87 stable into macOS fork
Copilot Jul 19, 2026
028c7f0
fix(jest): preserve setup-env mapping
Copilot Jul 19, 2026
dbd2357
fix(types): canonicalize local package imports
Copilot Jul 19, 2026
a141b4c
chore(types): regenerate React Native API
Copilot Jul 19, 2026
b743b3e
fix(changesets): retain origin fallback
Copilot Jul 19, 2026
ff4c929
chore(release): seed 0.87 rc prerelease
Copilot Jul 19, 2026
853d387
chore(deps): regenerate prerelease lock
Copilot Jul 19, 2026
1352761
fix(release): align constrained dependencies
Copilot Jul 19, 2026
303e161
fix(ios-prebuild): copy dependency headers safely
Copilot Jul 19, 2026
4e44b70
fix(types): resolve macOS typetest workspaces
Copilot Jul 19, 2026
6fec720
chore(jest): regenerate stable sync snapshots
Copilot Jul 19, 2026
61ea663
chore(deps): refresh validation lock
Copilot Jul 19, 2026
b5696f4
fix(pods): resolve dependency package fallback
Copilot Jul 19, 2026
52ffb3f
fix(pods): preserve facade platforms
Copilot Jul 19, 2026
a510265
chore(pods): regenerate prerelease lock
Copilot Jul 19, 2026
3c5dbfe
fix(prebuild): fail invalid platform requests
Copilot Jul 19, 2026
f8a0569
fix(prebuild): resolve packaged header paths
Copilot Jul 19, 2026
6eb3e13
style(prebuild): sort header test imports
Copilot Jul 19, 2026
4a9ec30
chore(deps): correct 0.87 stable lock to remove opportunistic upgrades
Saadnajmi Jul 20, 2026
61fa190
fix(babel-preset): restore upstream-canonical transform snapshots
Saadnajmi Jul 20, 2026
79b7624
chore(0.87): sync upstream React Native 0.87.1
Saadnajmi Sep 16, 2026
8f1def8
fix(hermes): resolve prebuild artifacts from version metadata
Saadnajmi Sep 16, 2026
cffd120
fix(ci): pass Hermes subgroup to snapshot URL resolver
Saadnajmi Sep 16, 2026
3714488
fix(prebuild): derive header slices from binary platforms
Saadnajmi Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"mode": "pre",
"tag": "rc",
"initialVersions": {
"@react-native/asset-utils": "0.87.0-rc.1",
"@react-native/assets-registry": "0.87.0-rc.1",
"@react-native/babel-plugin-codegen": "0.87.0-rc.1",
"@react-native/community-cli-plugin": "0.87.0-rc.1",
"@react-native/debugger-frontend": "0.87.0-rc.1",
"@react-native/debugger-shell": "0.87.0-rc.1",
"@react-native/dev-middleware": "0.87.0-rc.1",
"@react-native/eslint-config": "0.87.0-rc.1",
"@react-native/eslint-plugin": "0.87.0-rc.1",
"@react-native/eslint-plugin-specs": "0.87.0-rc.1",
"@react-native/gradle-plugin": "0.87.0-rc.1",
"@react-native/jest-preset": "0.87.0-rc.1",
"@react-native/metro-config": "0.87.0-rc.1",
"@react-native/new-app-screen": "0.87.0-rc.1",
"@react-native/normalize-colors": "0.87.0-rc.1",
"@react-native/js-polyfills": "0.87.0-rc.1",
"react-native-macos": "0.87.0-rc.0",
"@react-native/babel-preset": "0.87.0-rc.1",
"@react-native/metro-babel-transformer": "0.87.0-rc.1",
"@react-native/codegen": "0.87.0-rc.1",
"@react-native/compatibility-check": "0.87.0-rc.1",
"react-native-macos-init": "2.1.3",
"@react-native/popup-menu-android": "0.87.0-rc.1",
"@react-native/oss-library-example": "0.81.0-main",
"react-native-test-library-apple": "0.87.0-main",
"react-native-test-library-common": "0.87.0-main",
"@react-native/tester": "0.87.0-rc.1",
"@react-native/typescript-config": "0.87.0-rc.1",
"@react-native-macos/virtualized-lists": "0.87.0-rc.0",
"@react-native/core-cli-utils": "0.0.0",
"@react-native/eslint-plugin-monorepo": "0.0.0",
"@react-native/monorepo-tests": "0.0.0",
"@react-native/codegen-typescript-test": "0.0.0",
"@react-native/fantom": "0.0.0"
},
"changesets": []
}
6 changes: 6 additions & 0 deletions .changeset/stable-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"react-native-macos": patch
"@react-native-macos/virtualized-lists": patch
---

Sync to upstream React Native 0.87.0-rc.1.
2 changes: 2 additions & 0 deletions .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ experimental.multi_platform.extensions=.android
munge_underscores=true

module.name_mapper='^react-native$' -> '<PROJECT_ROOT>/packages/react-native/index.js'
module.name_mapper='^react-native/react-private-interface$' -> '<PROJECT_ROOT>/packages/react-native/src/react-private-interface.js'
module.name_mapper='^react-native/setup-env$' -> '<PROJECT_ROOT>/packages/react-native/src/setup-env.js'
module.name_mapper='^react-native/\(.*\)$' -> '<PROJECT_ROOT>/packages/react-native/\1'
module.name_mapper='^@react-native/dev-middleware$' -> '<PROJECT_ROOT>/packages/dev-middleware'
; [macOS Resolve workspace self-references (incl. @react-native/fantom) in yarn pnpm mode
Expand Down
9 changes: 9 additions & 0 deletions .github/actions/build-npm-package/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ runs:
pattern: ReactCore*
path: ./packages/react-native/ReactAndroid/external-artifacts/artifacts
merge-multiple: true
# ReactNativeDependenciesHeaders* is already covered by the
# ReactNativeDependencies* pattern above; ReactNativeHeaders* needs its own.
- name: Download ReactNativeHeaders artifacts
if: ${{ inputs.skip-apple-prebuilts != 'true' }}
uses: actions/download-artifact@v7
with:
pattern: ReactNativeHeaders*
path: ./packages/react-native/ReactAndroid/external-artifacts/artifacts
merge-multiple: true
- name: Print Artifacts Directory
if: ${{ inputs.skip-apple-prebuilts != 'true' }}
shell: bash
Expand Down
61 changes: 54 additions & 7 deletions .github/actions/test-ios-rntester/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,23 @@ inputs:
required: false
default: false
use-frameworks:
description: Whether we have to build with Dynamic Frameworks. If this is set to true, it builds from source
description: Whether we have to build with Dynamic Frameworks.
required: false
default: false
use-prebuilds:
description: >-
Whether to consume the prebuilt ReactCore/ReactNativeDependencies
artifacts. 'auto' (default) keeps the historical coupling: prebuilds for
static, source for dynamic frameworks. Pass 'true' with
use-frameworks:true for the prebuilt + dynamic-frameworks lane (the
config of the 2026-07-03 SocketRocket dual-copy regression).
required: false
default: auto
build-from-source:
description: >-
Force building ReactCore/ReactNativeDependencies from source, disabling the
prebuilt Maven fallback (RCT_USE_PREBUILT_RNCORE / RCT_USE_RN_DEP, which
otherwise default to '1'). Used by the from-source dynamic-frameworks lane.
required: false
default: false

Expand All @@ -41,36 +57,67 @@ runs:
- name: Prepare IOS Tests
if: ${{ inputs.run-unit-tests == 'true' }}
uses: ./.github/actions/prepare-ios-tests
- name: Resolve prebuilds mode
id: prebuilds
shell: bash
run: |
if [[ "${{ inputs.use-prebuilds }}" == "auto" ]]; then
# Historical coupling: prebuilds for static, source for dynamic frameworks.
if [[ "${{ inputs.use-frameworks }}" == "true" ]]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
else
echo "enabled=true" >> "$GITHUB_OUTPUT"
fi
elif [[ "${{ inputs.use-prebuilds }}" == "true" ]]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
elif [[ "${{ inputs.use-prebuilds }}" == "false" ]]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
else
# Don't silently treat a typo as 'disabled' — surface it.
echo "::warning::Unexpected use-prebuilds value '${{ inputs.use-prebuilds }}' (expected auto/true/false); treating as disabled."
echo "enabled=false" >> "$GITHUB_OUTPUT"
fi
- name: Download ReactNativeDependencies
if: ${{ inputs.use-frameworks == 'false' }}
if: ${{ steps.prebuilds.outputs.enabled == 'true' }}
uses: actions/download-artifact@v7
with:
name: ReactNativeDependencies${{ inputs.flavor }}.xcframework.tar.gz
path: /tmp/third-party/
- name: Print third-party folder
if: ${{ inputs.use-frameworks == 'false' }}
if: ${{ steps.prebuilds.outputs.enabled == 'true' }}
shell: bash
run: ls -lR /tmp/third-party
- name: Download React Native Prebuilds
if: ${{ inputs.use-frameworks == 'false' }}
if: ${{ steps.prebuilds.outputs.enabled == 'true' }}
uses: actions/download-artifact@v7
with:
name: ReactCore${{ inputs.flavor }}.xcframework.tar.gz
path: /tmp/ReactCore
- name: Print ReactCore folder
if: ${{ inputs.use-frameworks == 'false' }}
if: ${{ steps.prebuilds.outputs.enabled == 'true' }}
shell: bash
run: ls -lR /tmp/ReactCore
- name: Install CocoaPods dependencies
shell: bash
run: |
if [[ ${{ inputs.use-frameworks }} == "true" ]]; then
export USE_FRAMEWORKS=dynamic
else
# If use-frameworks is false, let's use prebuilds
fi
if [[ "${{ steps.prebuilds.outputs.enabled }}" == "true" ]]; then
export RCT_USE_LOCAL_RN_DEP="/tmp/third-party/ReactNativeDependencies${{ inputs.flavor }}.xcframework.tar.gz"
export RCT_TESTONLY_RNCORE_TARBALL_PATH="/tmp/ReactCore/ReactCore${{ inputs.flavor }}.xcframework.tar.gz"
fi
if [[ "${{ inputs.build-from-source }}" == "true" ]]; then
# Force building ReactCore/ReactNativeDependencies from source. Both
# RCT_USE_PREBUILT_RNCORE and RCT_USE_RN_DEP default to '1', so on a stable
# branch (where released Maven artifacts exist for the current version) the
# install would otherwise consume those prebuilts instead of building from
# source. That is wrong for the from-source dynamic-frameworks lane and also
# fails when a published artifact predates newer podspec layout changes
# (e.g. a React core tarball without ReactNativeHeaders.xcframework).
export RCT_USE_PREBUILT_RNCORE=0
export RCT_USE_RN_DEP=0
fi

cd packages/rn-tester

Expand Down
56 changes: 56 additions & 0 deletions .github/scripts/__tests__/resolve-hermes-url.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {createRequire, stripTypeScriptTypes} from 'node:module';
import {test} from 'node:test';
import {runInNewContext} from 'node:vm';

const callerSource = readFileSync(new URL('../resolve-hermes.mts', import.meta.url), 'utf8');
const helperSource = readFileSync(new URL(
'../../../packages/react-native/scripts/ios-prebuild/utils.js', import.meta.url,
), 'utf8');
// Execute the actual downloader without the CLI dispatch or the zx dependency.
const downloaderSource = stripTypeScriptTypes(callerSource.slice(
callerSource.indexOf('async function downloadUpstreamHermesTarball('),
callerSource.indexOf('/**\n * Extracts an upstream Hermes tarball'),
));

for (const buildType of ['Debug', 'Release']) {
for (const metadataAvailable of [true, false]) {
test(`${buildType}: ${metadataAvailable ? 'resolves nightly metadata' : 'missing metadata keeps release fallback'}`, async () => {
const version = buildType === 'Debug' ? '123.4.5' : '987.6.5';
const flavor = buildType.toLowerCase();
const snapshotRoot = `https://central.sonatype.com/repository/maven-snapshots/com/facebook/hermes/hermes-ios/${version}-SNAPSHOT`;
const metadataUrl = `${snapshotRoot}/maven-metadata.xml`;
const releaseUrl = `https://repo1.maven.org/maven2/com/facebook/hermes/hermes-ios/${version}/hermes-ios-${version}-hermes-ios-${flavor}.tar.gz`;
const nightlyUrl = `${snapshotRoot}/hermes-ios-${version}-20260915.123456-7-hermes-ios-${flavor}.tar.gz`;
const requests = [];
const fetch = async url => {
requests.push(url);
if (url === metadataUrl && metadataAvailable) {
return {
ok: true,
text: async () => '<metadata><versioning><snapshot><timestamp>20260915.123456</timestamp><buildNumber>7</buildNumber></snapshot></versioning></metadata>',
};
}
return {ok: false, status: 404, statusText: 'Not Found'};
};
const module = {exports: {}};
runInNewContext(helperSource, {
module,
require: createRequire(import.meta.url),
fetch,
});
const download = runInNewContext(`${downloaderSource}\ndownloadUpstreamHermesTarball`, {
computeNightlyTarballURL: module.exports.computeNightlyTarballURL,
resolveHermesArtifactVersion: () => version,
fetch,
echo: () => {},
});

assert.equal(await download(buildType), null);
assert.deepEqual(requests, metadataAvailable
? [metadataUrl, releaseUrl, nightlyUrl]
: [metadataUrl, releaseUrl]);
});
}
}
41 changes: 41 additions & 0 deletions .github/workflow-scripts/__tests__/maestro-ios-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/

jest.mock('child_process', () => ({
execSync: jest.fn(),
spawn: jest.fn(),
}));
const childProcess = require('child_process');

const {findAvailableSimulator} = require('../maestro-ios');

describe('Maestro iOS runner', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('selects an iPhone Pro simulator from the latest runtime', () => {
childProcess.execSync.mockReturnValue(
JSON.stringify({
devices: {
'iOS 18.5': [{name: 'iPhone 16 Pro', udid: 'old-pro'}],
'iOS 26.5': [
{name: 'iPhone 17 Pro Max', udid: 'new-pro-max'},
{name: 'iPhone 17 Pro', udid: 'new-pro'},
],
},
}),
);

expect(findAvailableSimulator()).toEqual({
name: 'iPhone 17 Pro',
udid: 'new-pro',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,47 @@ jest.mock('../utils.js', () => ({
process.exit = mockExit;
global.fetch = mockFetch;

const BASE_URL =
'https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts';

// The verifier HEAD-checks the POM plus every classifier tarball attached to
// the react-native-artifacts publication (external-artifacts/build.gradle.kts).
const expectedUrls = version => [
`${BASE_URL}/${version}/react-native-artifacts-${version}.pom`,
...[
'reactnative-core-debug',
'reactnative-core-release',
'reactnative-dependencies-debug',
'reactnative-dependencies-release',
'reactnative-headers-debug',
'reactnative-headers-release',
'reactnative-dependencies-headers-debug',
'reactnative-dependencies-headers-release',
].map(
classifier =>
`${BASE_URL}/${version}/react-native-artifacts-${version}-${classifier}.tar.gz`,
),
];

describe('#verifyArtifactsAreOnMaven', () => {
beforeEach(jest.clearAllMocks);

it('waits for the packages to be published on maven when version has no v', async () => {
mockSleep.mockReturnValueOnce(Promise.resolve()).mockImplementation(() => {
throw new Error('Should not be called again!');
});
// First attempt: the POM is not there yet. Second attempt: every URL is.
mockFetch
.mockReturnValueOnce(Promise.resolve({status: 404}))
.mockReturnValueOnce(Promise.resolve({status: 200}));
.mockReturnValue(Promise.resolve({status: 200}));

const version = '0.78.1';
await verifyArtifactsAreOnMaven(version);

expect(mockSleep).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith(
'https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/0.78.1/react-native-artifacts-0.78.1.pom',
);
for (const url of expectedUrls('0.78.1')) {
expect(mockFetch).toHaveBeenCalledWith(url, {method: 'HEAD'});
}
});

it('waits for the packages to be published on maven, when version starts with v', async () => {
Expand All @@ -48,27 +71,46 @@ describe('#verifyArtifactsAreOnMaven', () => {
});
mockFetch
.mockReturnValueOnce(Promise.resolve({status: 404}))
.mockReturnValueOnce(Promise.resolve({status: 200}));
.mockReturnValue(Promise.resolve({status: 200}));

const version = 'v0.78.1';
await verifyArtifactsAreOnMaven(version);

expect(mockSleep).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith(
'https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/0.78.1/react-native-artifacts-0.78.1.pom',
);
for (const url of expectedUrls('0.78.1')) {
expect(mockFetch).toHaveBeenCalledWith(url, {method: 'HEAD'});
}
});

it('passes immediately if packages are already on Maven', async () => {
mockFetch.mockReturnValueOnce(Promise.resolve({status: 200}));
mockFetch.mockReturnValue(Promise.resolve({status: 200}));

const version = '0.78.1';
await verifyArtifactsAreOnMaven(version);

expect(mockSleep).toHaveBeenCalledTimes(0);
expect(mockFetch).toHaveBeenCalledWith(
'https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/0.78.1/react-native-artifacts-0.78.1.pom',
);
// All nine URLs (POM + 8 classifier tarballs) are verified in one pass.
expect(mockFetch).toHaveBeenCalledTimes(9);
for (const url of expectedUrls('0.78.1')) {
expect(mockFetch).toHaveBeenCalledWith(url, {method: 'HEAD'});
}
});

it('waits when a classifier artifact is missing even though the POM exists', async () => {
mockSleep.mockReturnValueOnce(Promise.resolve()).mockImplementation(() => {
throw new Error('Should not be called again!');
});
// First attempt: POM ok, first classifier missing. Second attempt: all ok.
mockFetch
.mockReturnValueOnce(Promise.resolve({status: 200}))
.mockReturnValueOnce(Promise.resolve({status: 404}))
.mockReturnValue(Promise.resolve({status: 200}));

const version = '0.78.1';
await verifyArtifactsAreOnMaven(version);

expect(mockSleep).toHaveBeenCalledTimes(1);
expect(mockExit).not.toHaveBeenCalled();
});

it('tries 90 times and then exits', async () => {
Expand All @@ -82,6 +124,7 @@ describe('#verifyArtifactsAreOnMaven', () => {
expect(mockExit).toHaveBeenCalledWith(1);
expect(mockFetch).toHaveBeenCalledWith(
'https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/0.78.1/react-native-artifacts-0.78.1.pom',
{method: 'HEAD'},
);
});
});
Loading