Compare commits

..
Author SHA1 Message Date
Nicola CortiandFacebook GitHub Bot 185443e299 Migrate RNTester to use {usesCleartextTraffic} Manifest Placeholder (#52620)
Summary:
This creates a `debugOptimized` build type for React Native Android, meaning that we can run C++ optimization on the debug build, while still having the debugger enabled. This is aimed at improving the developer experience for folks developing on low-end devices or emulators.

Users that intend to debug can still use the `debug` variant where the full debug symbols are shipped.

## Changelog:

[ANDROID] [ADDED] - Create a debugOptimized buildType for Android


Test Plan:
Tested locally with RNTester by doing:

```
./gradlew installDebugOptimized
```

This is the output of the 3 generated .aar. The size difference is a proof that we're correctly stripping out the C++ debug symbols:

<img width="193" height="54" alt="Screenshot 2025-07-15 at 17 49 50" src="https://github.com/user-attachments/assets/584a0e8d-2d17-40d4-ac29-da09049d6554" />
<img width="235" height="51" alt="Screenshot 2025-07-15 at 17 49 39" src="https://github.com/user-attachments/assets/eda8f9e7-3509-4334-8c16-990e55caa04d" />
<img width="184" height="52" alt="Screenshot 2025-07-15 at 17 49 32" src="https://github.com/user-attachments/assets/a5c94385-bc00-4484-b43e-088ee039827f" />

Rollback Plan:

Reviewed By: cipolleschi

Differential Revision: D78351347

Pulled By: cortinico
2025-07-18 07:43:44 -07:00
2000 changed files with 24488 additions and 38242 deletions
+6 -5
View File
@@ -75,10 +75,11 @@ module.system.haste.module_ref_prefix=m#
react.runtime=automatic
experimental.only_support_flow_fixme_and_expected_error=true
experimental.require_suppression_with_error_code=true
experimental.invariant_subtyping_error_message_improvement=true
experimental.natural_inference.local_object_literals.followup_fix=true
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_type=$FlowFixMeEmpty
ban_spread_key_props=true
@@ -103,4 +104,4 @@ untyped-import
untyped-type-import
[version]
^0.280.0
^0.275.0
@@ -1,6 +1,6 @@
name: 🔍 Debugger - Bug Report
description: Report a bug with React Native DevTools and the New Debugger
labels: ["Needs: Triage :mag:", "Debugging"]
labels: ["Needs: Triage :mag:", "Debugger"]
body:
- type: markdown
+12 -6
View File
@@ -4,6 +4,9 @@ inputs:
release-type:
required: true
description: The type of release we are building. It could be nightly, release or dry-run
run-e2e-tests:
default: 'false'
description: If we need to build to run E2E tests. If yes, we need to build also x86.
gradle-cache-encryption-key:
description: "The encryption key needed to store the Gradle Configuration cache"
runs:
@@ -28,11 +31,10 @@ runs:
uses: actions/cache/restore@v4
with:
path: /github/home/.cache/ccache
key: v2-ccache-android-${{ github.job }}-${{ github.ref }}-${{ hashFiles('packages/react-native/ReactAndroid/**/*.cpp', 'packages/react-native/ReactAndroid/**/*.h', 'packages/react-native/ReactCommon/**/*.cpp', 'packages/react-native/ReactAndroid/**/CMakeLists.txt', 'packages/react-native/ReactCommon/**/CMakeLists.txt') }}
key: v1-ccache-android-${{ github.job }}-${{ github.ref }}
restore-keys: |
v2-ccache-android-${{ github.job }}-${{ github.ref }}-
v2-ccache-android-${{ github.job }}-
v2-ccache-android-
v1-ccache-android-${{ github.job }}-
v1-ccache-android-
- name: Show ccache stats
shell: bash
run: ccache -s -v
@@ -41,7 +43,11 @@ runs:
run: |
if [[ "${{ inputs.release-type }}" == "dry-run" ]]; then
# dry-run: we only build ARM64 to save time/resources. For release/nightlies the default is to build all archs.
export ORG_GRADLE_PROJECT_reactNativeArchitectures="arm64-v8a,x86" # x86 is required for E2E testing
if [[ "${{ inputs.run-e2e-tests }}" == 'true' ]]; then
export ORG_GRADLE_PROJECT_reactNativeArchitectures="arm64-v8a,x86" # x86 is required for E2E testing
else
export ORG_GRADLE_PROJECT_reactNativeArchitectures="arm64-v8a"
fi
TASKS="publishAllToMavenTempLocal build"
elif [[ "${{ inputs.release-type }}" == "nightly" ]]; then
# nightly: we set isSnapshot to true so artifacts are sent to the right repository on Maven Central.
@@ -57,7 +63,7 @@ runs:
uses: actions/cache/save@v4
with:
path: /github/home/.cache/ccache
key: v2-ccache-android-${{ github.job }}-${{ github.ref }}-${{ hashFiles('packages/react-native/ReactAndroid/**/*.cpp', 'packages/react-native/ReactAndroid/**/*.h', 'packages/react-native/ReactCommon/**/*.cpp', 'packages/react-native/ReactAndroid/**/CMakeLists.txt', 'packages/react-native/ReactCommon/**/CMakeLists.txt') }}
key: v1-ccache-android-${{ github.job }}-${{ github.ref }}
- name: Show ccache stats
shell: bash
run: ccache -s -v
@@ -71,17 +71,17 @@ runs:
mv build_"$SLICE" "$FINAL_PATH"
# check whether everything is there
if [[ -d "$FINAL_PATH/lib/hermesvm.framework" ]]; then
echo "Successfully built hermesvm.framework for $SLICE in $FLAVOR"
if [[ -d "$FINAL_PATH/API/hermes/hermes.framework" ]]; then
echo "Successfully built hermes.framework for $SLICE in $FLAVOR"
else
echo "Failed to built hermesvm.framework for $SLICE in $FLAVOR"
echo "Failed to built hermes.framework for $SLICE in $FLAVOR"
exit 1
fi
if [[ -d "$FINAL_PATH/lib/hermesvm.framework.dSYM" ]]; then
echo "Successfully built hermesvm.framework.dSYM for $SLICE in $FLAVOR"
if [[ -d "$FINAL_PATH/API/hermes/hermes.framework.dSYM" ]]; then
echo "Successfully built hermes.framework.dSYM for $SLICE in $FLAVOR"
else
echo "Failed to built hermesvm.framework.dSYM for $SLICE in $FLAVOR"
echo "Failed to built hermes.framework.dSYM for $SLICE in $FLAVOR"
echo "Please try again"
exit 1
fi
@@ -15,6 +15,8 @@ runs:
steps:
- name: Setup xcode
uses: ./.github/actions/setup-xcode
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Restore Hermes workspace
uses: ./.github/actions/restore-hermes-workspace
- name: Restore Cached Artifacts
@@ -43,8 +45,6 @@ runs:
echo "ARTIFACTS_EXIST=true" >> $GITHUB_ENV
echo "ARTIFACTS_EXIST=true" >> $GITHUB_OUTPUT
fi
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Yarn- Install Dependencies
if: ${{ steps.check_if_apple_artifacts_are_there.outputs.ARTIFACTS_EXIST != 'true' }}
uses: ./.github/actions/yarn-install
@@ -186,7 +186,7 @@ runs:
cd ./packages/react-native/sdks/hermes || exit 1
DSYM_FILE_PATH=lib/hermesvm.framework.dSYM
DSYM_FILE_PATH=API/hermes/hermes.framework.dSYM
cp -r build_macosx/$DSYM_FILE_PATH "$WORKING_DIR/macosx/"
cp -r build_catalyst/$DSYM_FILE_PATH "$WORKING_DIR/catalyst/"
cp -r build_iphoneos/$DSYM_FILE_PATH "$WORKING_DIR/iphoneos/"
@@ -197,10 +197,10 @@ runs:
cp -r build_xrsimulator/$DSYM_FILE_PATH "$WORKING_DIR/xrsimulator/"
DEST_DIR="/tmp/hermes/dSYM/$FLAVOR"
tar -C "$WORKING_DIR" -czvf "hermesvm.framework.dSYM" .
tar -C "$WORKING_DIR" -czvf "hermes.framework.dSYM" .
mkdir -p "$DEST_DIR"
mv "hermesvm.framework.dSYM" "$DEST_DIR"
mv "hermes.framework.dSYM" "$DEST_DIR"
- name: Upload hermes dSYM artifacts
uses: actions/upload-artifact@v4.3.4
with:
+4 -4
View File
@@ -99,8 +99,8 @@ runs:
mkdir -p ./packages/react-native/ReactAndroid/external-artifacts/artifacts/
cp ${{ inputs.hermes-ws-dir }}/hermes-runtime-darwin/hermes-ios-Debug.tar.gz ./packages/react-native/ReactAndroid/external-artifacts/artifacts/hermes-ios-debug.tar.gz
cp ${{ inputs.hermes-ws-dir }}/hermes-runtime-darwin/hermes-ios-Release.tar.gz ./packages/react-native/ReactAndroid/external-artifacts/artifacts/hermes-ios-release.tar.gz
cp ${{ inputs.hermes-ws-dir }}/dSYM/Debug/hermesvm.framework.dSYM ./packages/react-native/ReactAndroid/external-artifacts/artifacts/hermes-framework-dSYM-debug.tar.gz
cp ${{ inputs.hermes-ws-dir }}/dSYM/Release/hermesvm.framework.dSYM ./packages/react-native/ReactAndroid/external-artifacts/artifacts/hermes-framework-dSYM-release.tar.gz
cp ${{ inputs.hermes-ws-dir }}/dSYM/Debug/hermes.framework.dSYM ./packages/react-native/ReactAndroid/external-artifacts/artifacts/hermes-framework-dSYM-debug.tar.gz
cp ${{ inputs.hermes-ws-dir }}/dSYM/Release/hermes.framework.dSYM ./packages/react-native/ReactAndroid/external-artifacts/artifacts/hermes-framework-dSYM-release.tar.gz
- name: Download ReactNativeDependencies
uses: actions/download-artifact@v4
with:
@@ -116,12 +116,12 @@ runs:
- name: Print Artifacts Directory
shell: bash
run: ls -lR ./packages/react-native/ReactAndroid/external-artifacts/artifacts/
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Setup gradle
uses: ./.github/actions/setup-gradle
with:
cache-encryption-key: ${{ inputs.gradle-cache-encryption-key }}
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Install dependencies
uses: ./.github/actions/yarn-install
- name: Build packages
@@ -14,8 +14,6 @@ inputs:
runs:
using: composite
steps:
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Yarn install
uses: ./.github/actions/yarn-install
- name: Configure Git
-2
View File
@@ -35,8 +35,6 @@ runs:
with:
java-version: '17'
distribution: 'zulu'
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Run yarn install
uses: ./.github/actions/yarn-install
- name: Start Metro in Debug
@@ -17,6 +17,9 @@ outputs:
runs:
using: composite
steps:
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Setup hermes version
shell: bash
id: hermes-version
@@ -64,8 +67,6 @@ runs:
echo "HERMES_CACHED=true" >> "$GITHUB_OUTPUT"
fi
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Yarn- Install Dependencies
if: ${{ steps.meaningful-cache.outputs.HERMES_CACHED != 'true' }}
uses: ./.github/actions/yarn-install
@@ -1,83 +0,0 @@
name: Run Fantom Tests
inputs:
release-type:
required: true
description: The type of release we are building. It could be nightly, release or dry-run
gradle-cache-encryption-key:
description: "The encryption key needed to store the Gradle Configuration cache"
runs:
using: composite
steps:
- name: Install dependencies
shell: bash
run: |
sudo apt update
sudo apt install -y git cmake openssl libssl-dev clang
- name: Setup git safe folders
shell: bash
run: git config --global --add safe.directory '*'
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Install node dependencies
uses: ./.github/actions/yarn-install
- name: Setup gradle
uses: ./.github/actions/setup-gradle
with:
cache-read-only: "false"
cache-encryption-key: ${{ inputs.gradle-cache-encryption-key }}
- name: Restore Fantom ccache
uses: actions/cache/restore@v4
with:
path: /github/home/.cache/ccache
key: v2-ccache-fantom-${{ github.job }}-${{ github.ref }}-${{ hashFiles(
'packages/react-native/ReactAndroid/**/*.cpp',
'packages/react-native/ReactAndroid/**/*.h',
'packages/react-native/ReactAndroid/**/CMakeLists.txt',
'packages/react-native/ReactCommon/**/*.cpp',
'packages/react-native/ReactCommon/**/*.h',
'packages/react-native/ReactCommon/**/CMakeLists.txt',
'private/react-native-fantom/tester/**/*.cpp',
'private/react-native-fantom/tester/**/*.h',
'private/react-native-fantom/tester/**/CMakeLists.txt'
) }}
restore-keys: |
v2-ccache-fantom-${{ github.job }}-${{ github.ref }}-
v2-ccache-fantom-${{ github.job }}-
v2-ccache-fantom-
- name: Show ccache stats
shell: bash
run: ccache -s -v
- name: Run Fantom Tests
shell: bash
run: yarn fantom
env:
CC: clang
CXX: clang++
- name: Save Fantom ccache
if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, '-stable') }}
uses: actions/cache/save@v4
with:
path: /github/home/.cache/ccache
key: v2-ccache-fantom-${{ github.job }}-${{ github.ref }}-${{ hashFiles(
'packages/react-native/ReactAndroid/**/*.cpp',
'packages/react-native/ReactAndroid/**/*.h',
'packages/react-native/ReactAndroid/**/CMakeLists.txt',
'packages/react-native/ReactCommon/**/*.cpp',
'packages/react-native/ReactCommon/**/*.h',
'packages/react-native/ReactCommon/**/CMakeLists.txt',
'private/react-native-fantom/tester/**/*.cpp',
'private/react-native-fantom/tester/**/*.h',
'private/react-native-fantom/tester/**/CMakeLists.txt'
) }}
- name: Show ccache stats
shell: bash
run: ccache -s -v
- name: Upload test results
if: ${{ always() }}
uses: actions/upload-artifact@v4.3.4
with:
name: run-fantom-tests-results
compression-level: 1
path: |
private/react-native-fantom/build/reports
@@ -0,0 +1,32 @@
name: setup-xcode-build-cache
description: Add caching to iOS jobs to speed up builds
inputs:
hermes-version:
description: The version of hermes
required: true
flavor:
description: The flavor that is going to be built
default: Debug
use-frameworks:
description: Whether we are bulding with DynamicFrameworks or StaticLibraries
default: StaticLibraries
ruby-version:
description: The ruby version we are going to use
default: 2.6.10
runs:
using: composite
steps:
- name: See commands.yml with_xcodebuild_cache
shell: bash
run: echo "See commands.yml with_xcodebuild_cache"
- name: Cache podfile lock
uses: actions/cache@v4
with:
path: packages/rn-tester/Podfile.lock
key: v13-podfilelock-${{ github.job }}-NewArch-${{ inputs.flavor }}-${{ inputs.use-frameworks }}-${{ inputs.ruby-version }}-${{ hashfiles('packages/rn-tester/Podfile') }}-${{ inputs.hermes-version }}
- name: Cache cocoapods
uses: actions/cache@v4
with:
path: packages/rn-tester/Pods
key: v15-cocoapods-${{ github.job }}-NewArch-${{ inputs.flavor }}-${{ inputs.use-frameworks }}-${{ inputs.ruby-version }}-${{ hashfiles('packages/rn-tester/Podfile.lock') }}-${{ hashfiles('packages/rn-tester/Podfile') }}-${{ inputs.hermes-version}}
@@ -23,8 +23,6 @@ runs:
uses: ./.github/actions/setup-xcode
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Run yarn install
uses: ./.github/actions/yarn-install
- name: Create Hermes folder
shell: bash
run: mkdir -p "$HERMES_WS_DIR"
@@ -36,6 +34,8 @@ runs:
- name: Print Downloaded hermes
shell: bash
run: ls -lR "$HERMES_WS_DIR"
- name: Run yarn
uses: ./.github/actions/yarn-install
- name: Setup ruby
uses: ruby/setup-ruby@v1
with:
@@ -102,6 +102,13 @@ runs:
- name: Print ReactCore folder
shell: bash
run: ls -lR /tmp/ReactCore
- name: Setup xcode build cache
uses: ./.github/actions/setup-xcode-build-cache
with:
hermes-version: ${{ inputs.hermes-version }}
use-frameworks: ${{ inputs.use-frameworks }}
flavor: ${{ inputs.flavor }}
ruby-version: ${{ inputs.ruby-version }}
- name: Install CocoaPods dependencies
shell: bash
run: |
@@ -0,0 +1,52 @@
name: test-library-on-nightly
description: Tests a library on a nightly
inputs:
library-npm-package:
description: The library npm package to add
required: true
platform:
description: whether we want to build for iOS or Android
required: true
runs:
using: composite
steps:
- name: Create new app
shell: bash
run: |
cd /tmp
npx @react-native-community/cli init RNApp --skip-install --version nightly
- name: Add library
shell: bash
run: |
cd /tmp/RNApp
yarn add ${{ inputs.library-npm-package }}
# iOS
- name: Setup xcode
if: ${{ inputs.platform == 'ios' }}
uses: ./.github/actions/setup-xcode
- name: Build iOS
shell: bash
if: ${{ inputs.platform == 'ios' }}
run: |
cd /tmp/RNApp/ios
bundle install
bundle exec pod install
xcodebuild build \
-workspace RNApp.xcworkspace \
-scheme RNApp \
-sdk iphonesimulator
# Android
- name: Setup Java for Android
if: ${{ inputs.platform == 'android' }}
uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'zulu'
- name: Build Android
shell: bash
if: ${{ inputs.platform == 'android' }}
run: |
cd /tmp/RNApp/android
./gradlew assembleDebug
+2
View File
@@ -2,6 +2,8 @@ name: yarn-install
runs:
using: composite
steps:
- name: Setup node.js
uses: ./.github/actions/setup-node
- name: Install dependencies
shell: bash
run: |
@@ -0,0 +1,189 @@
/**
* 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
*/
'use strict';
const {
prepareFailurePayload,
sendMessageToDiscord,
} = require('../notifyDiscord');
describe('prepareFailurePayload', () => {
it('should handle undefined failures', () => {
const message = prepareFailurePayload(undefined);
expect(message).toEqual({
content:
'⚠️ **React Native Nightly Integration Failures** ⚠️\n\nNo failures to report.',
});
});
it('should handle empty failures array', () => {
const message = prepareFailurePayload([]);
expect(message).toEqual({
content:
'⚠️ **React Native Nightly Integration Failures** ⚠️\n\nNo failures to report.',
});
});
it('should format a single failure correctly', () => {
const failures = [
{
library: 'react-native-reanimated',
platform: 'iOS',
},
];
const message = prepareFailurePayload(failures);
expect(message).toEqual({
content:
'⚠️ **React Native Nightly Integration Failures** ⚠️\n\nThe integration of libraries with React Native nightly failed for the following libraries:\n\n❌ [iOS] react-native-reanimated',
});
});
it('should sort multiple failures by platform and library name', () => {
const failures = [
{
library: 'react-native-reanimated',
platform: 'iOS',
},
{
library: 'react-native-gesture-handler',
platform: 'Android',
},
{
library: 'react-native-screens',
platform: 'iOS',
},
{
library: 'react-native-svg',
platform: 'Android',
},
];
const message = prepareFailurePayload(failures);
// The failures should be sorted: first Android (alphabetically), then iOS
// Within each platform, libraries should be sorted alphabetically
expect(message).toEqual({
content:
'⚠️ **React Native Nightly Integration Failures** ⚠️\n\nThe integration of libraries with React Native nightly failed for the following libraries:\n\n❌ [Android] react-native-gesture-handler\n❌ [Android] react-native-svg\n❌ [iOS] react-native-reanimated\n❌ [iOS] react-native-screens',
});
});
it('should handle failures with missing properties', () => {
const failures = [
{
// Missing library
platform: 'iOS',
},
{
library: 'react-native-gesture-handler',
// Missing platform
},
{
// Both missing
},
];
const message = prepareFailurePayload(failures);
expect(message).toEqual({
content:
'⚠️ **React Native Nightly Integration Failures** ⚠️\n\nThe integration of libraries with React Native nightly failed for the following libraries:\n\n❌ [iOS] Unknown\n❌ [Unknown] react-native-gesture-handler\n❌ [Unknown] Unknown',
});
});
});
describe('sendMessageToDiscord', () => {
// Store the original fetch function
const originalFetch = global.fetch;
// Setup and teardown for each test
beforeEach(() => {
// Mock the global fetch function
global.fetch = jest.fn();
// Silence console logs during tests
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
// Restore the original fetch function
global.fetch = originalFetch;
// Restore console functions
jest.restoreAllMocks();
});
it('should throw an error if webhook URL is missing', async () => {
await expect(sendMessageToDiscord(null, {})).rejects.toThrow(
'Discord webhook URL is missing',
);
});
it('should send a message successfully', async () => {
// Mock a successful response
global.fetch.mockResolvedValueOnce({
ok: true,
status: 200,
});
const webhook = 'https://discord.com/api/webhooks/123/abc';
const message = {content: 'Test message'};
await expect(sendMessageToDiscord(webhook, message)).resolves.not.toThrow();
// Verify fetch was called with the right arguments
expect(global.fetch).toHaveBeenCalledWith(webhook, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(message),
});
// Verify console.log was called
expect(console.log).toHaveBeenCalledWith(
'Successfully sent message to Discord',
);
});
it('should throw an error if the response is not ok', async () => {
// Mock a failed response
global.fetch.mockResolvedValueOnce({
ok: false,
status: 400,
text: jest.fn().mockResolvedValueOnce('Bad Request'),
});
const webhook = 'https://discord.com/api/webhooks/123/abc';
const message = {content: 'Test message'};
await expect(sendMessageToDiscord(webhook, message)).rejects.toThrow(
'HTTP status code: 400',
);
// Verify console.error was called
expect(console.error).toHaveBeenCalledWith(
'Failed to send message to Discord: 400 Bad Request',
);
});
it('should throw an error if fetch fails', async () => {
// Mock a network error
const networkError = new Error('Network error');
global.fetch.mockRejectedValueOnce(networkError);
const webhook = 'https://discord.com/api/webhooks/123/abc';
const message = {content: 'Test message'};
await expect(sendMessageToDiscord(webhook, message)).rejects.toThrow(
'Network error',
);
});
});
@@ -117,13 +117,13 @@ describe('#verifyPublishedTemplate', () => {
it('will timeout if npm does not update package version after a set number of retries', async () => {
const RETRIES = 2;
(await verifyPublishedTemplate('0.77.0', true, RETRIES),
await verifyPublishedTemplate('0.77.0', true, RETRIES),
expect(mockVerifyPublishedPackage).toHaveBeenCalledWith(
'@react-native-community/template',
'0.77.0',
'latest',
2,
));
);
});
});
});
@@ -83,13 +83,13 @@ describe('#verifyReleaseOnNPM', () => {
it('will timeout if npm does not update package version after a set number of retries', async () => {
const RETRIES = 2;
(await verifyReleaseOnNpm('0.77.0', true, RETRIES),
await verifyReleaseOnNpm('0.77.0', true, RETRIES),
expect(mockVerifyPublishedPackage).toHaveBeenCalledWith(
'react-native',
'0.77.0',
'latest',
2,
));
);
});
it('will timeout if npm does not update latest tag after a set number of retries', async () => {
@@ -0,0 +1,121 @@
/**
* 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
*/
const fs = require('fs');
const path = require('path');
const {
prepareFailurePayload,
sendMessageToDiscord,
} = require('./notifyDiscord');
function readOutcomes() {
const baseDir = '/tmp';
let outcomes = [];
fs.readdirSync(baseDir).forEach(file => {
const fullPath = path.join(baseDir, file);
if (fullPath.endsWith('outcome') && fs.statSync(fullPath).isDirectory) {
fs.readdirSync(fullPath).forEach(subFile => {
const subFullPath = path.join(fullPath, subFile);
if (subFullPath.endsWith('outcome')) {
const [library, status] = String(fs.readFileSync(subFullPath, 'utf8'))
.trim()
.split(':');
const platform = subFile.includes('android') ? 'Android' : 'iOS';
console.log(
`[${platform}] ${library} completed with status ${status}`,
);
outcomes.push({
library: library.trim(),
platform,
status: status.trim(),
});
}
});
} else if (fullPath.endsWith('outcome')) {
const [library, status] = String(fs.readFileSync(fullPath, 'utf8'))
.trim()
.split(':');
const platform = file.includes('android') ? 'Android' : 'iOS';
console.log(`[${platform}] ${library} completed with status ${status}`);
outcomes.push({
library: library.trim(),
platform,
status: status.trim(),
});
}
});
return outcomes;
}
function printFailures(outcomes) {
console.log('Printing failures...');
let failedLibraries = [];
outcomes.forEach(entry => {
if (entry.status !== 'success') {
console.log(
`❌ [${entry.platform}] ${entry.library} failed with status ${entry.status}`,
);
failedLibraries.push({
library: entry.library,
platform: entry.platform,
});
}
});
return failedLibraries;
}
/**
* Sends a message to Discord with the list of failures.
* @param {string} webHook - The Discord webhook URL
* @param {Array<Object>} failures - List of failures to report
* @returns {Promise<void>} - A promise that resolves when the message is sent
*/
async function notifyDiscord(webHook, failures) {
if (!webHook) {
console.error('Discord webhook URL is missing');
return;
}
if (!failures || failures.length === 0) {
console.log('No failures to report to Discord');
return;
}
try {
// Use the prepareFailurePayload function to format the message
const message = prepareFailurePayload(failures);
// Use the sendMessageToDiscord function to send the message
await sendMessageToDiscord(webHook, message);
} catch (error) {
console.error('Error in notifyDiscord function:', error);
throw error;
}
}
async function collectResults(discordWebHook) {
const outcomes = readOutcomes();
const failures = printFailures(outcomes);
if (failures.length > 0) {
if (discordWebHook) {
console.log('Sending to discord');
await notifyDiscord(discordWebHook, failures);
} else {
console.log('Web hook not set');
}
process.exit(1);
}
console.log('✅ All tests passed!');
}
module.exports = {
collectResults,
notifyDiscord,
};
@@ -25,31 +25,6 @@ function extractUsersFromScheduleAndDate(schedule, userMap, date) {
return [user1, user2];
}
/**
* You can invoke this script by doing:
* ```
* node .github/workflow-scripts/extractIssueOncalls.js $DATA
* ```
*
* the $DATA is stored in the github secrets as ONCALL_SCHEDULE variable.
* The format of the data is:
* ```
* {
* \"userMap\": {
* \"discord_handle1\": \"discord_id1\",
* \"discord_handle2\": \"discord_id2\",
* ...
* },
* \"schedule\": {
* \"2025-07-29\": [\"discord_handle1\", \"discord_handle2\"],
* \"2025-08-05\": [\"discord_handle3\", \"discord_handle4\"],
* ...
* }
* ```
*
* When uploading the secret, make sure that the JSON strings are escaped!
* The script will fail otherwise, because GitHub will remove the `"` characters.
*/
function main() {
const configuration = process.argv[2];
const {userMap, schedule} = JSON.parse(configuration);
+2 -2
View File
@@ -87,11 +87,11 @@ async function launchAppOnSimulator(appId, udid, isDebug) {
function startVideoRecording(jsengine, currentAttempt) {
console.log(
`Start video record using pid: video_record_${currentAttempt}.pid`,
`Start video record using pid: video_record_${jsengine}_${currentAttempt}.pid`,
);
const recordingArgs =
`simctl io booted recordVideo video_record_${currentAttempt}.mov`.split(
`simctl io booted recordVideo video_record_${jsengine}_${currentAttempt}.mov`.split(
' ',
);
const recordingProcess = childProcess.spawn('xcrun', recordingArgs, {
+90
View File
@@ -0,0 +1,90 @@
/**
* 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
*/
/**
* Sends a message to Discord using the webhook URL.
* @param {string} webHook - The Discord webhook URL
* @param {Object} message - The message to send
* @returns {Promise<void>} - A promise that resolves when the message is sent
*/
async function sendMessageToDiscord(webHook, message) {
if (!webHook) {
throw new Error('Discord webhook URL is missing');
}
// Send the request using fetch
const response = await fetch(webHook, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(message),
});
// Handle the response
if (response.ok) {
console.log('Successfully sent message to Discord');
return;
} else {
const errorText = await response.text();
console.error(
`Failed to send message to Discord: ${response.status} ${errorText}`,
);
throw new Error(`HTTP status code: ${response.status}`);
}
}
/**
* Prepares a formatted Discord message payload from a list of failures.
* @param {Array<Object>} failures - List of failures to format
* @returns {Object} - The formatted Discord message payload
*/
function prepareFailurePayload(failures) {
if (!failures || failures.length === 0) {
return {
content:
'⚠️ **React Native Nightly Integration Failures** ⚠️\n\nNo failures to report.',
};
}
// Sort failures by platform and then by library name
const sortedFailures = [...failures].sort((a, b) => {
// First sort by platform
const platformA = a.platform || 'Unknown';
const platformB = b.platform || 'Unknown';
if (platformA !== platformB) {
return platformA.localeCompare(platformB);
}
// Then sort by library name
const libraryA = a.library || 'Unknown';
const libraryB = b.library || 'Unknown';
return libraryA.localeCompare(libraryB);
});
// Format the failures into a message
const formattedFailures = sortedFailures
.map(failure => {
const library = failure.library || 'Unknown';
const platform = failure.platform || 'Unknown';
return `❌ [${platform}] ${library}`;
})
.join('\n');
return {
content: `⚠️ **React Native Nightly Integration Failures** ⚠️\n\nThe integration of libraries with React Native nightly failed for the following libraries:\n\n${formattedFailures}`,
};
}
// Export the functions using CommonJS syntax
module.exports = {
prepareFailurePayload,
sendMessageToDiscord,
};
+34
View File
@@ -0,0 +1,34 @@
# This jobs runs every day 2 hours after the nightly job and its purpose is to report
# a failure in case the nightly failed to be published. We are going to hook this to an internal automation.
name: Check Nightlies
on:
workflow_dispatch:
# nightly build @ 4:15 AM UTC
schedule:
- cron: '15 4 * * *'
jobs:
check-nightly:
runs-on: ubuntu-latest
if: github.repository == 'facebook/react-native'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check nightly
run: |
TODAY=$(date "+%Y%m%d")
echo "Checking nightly for $TODAY"
NIGHTLY="$(npm view react-native | grep $TODAY)"
if [[ -z $NIGHTLY ]]; then
echo 'Nightly job failed.'
exit 1
else
echo 'Nightly Worked, All Good!'
fi
test-libraries:
uses: ./.github/workflows/test-libraries-on-nightlies.yml
needs: check-nightly
secrets:
discord_webhook_url: ${{ secrets.NIGHTLY_DISCORD_WEBHOOK }}
+4
View File
@@ -27,6 +27,10 @@ jobs:
ONCALL2=$(echo $ONCALLS | cut -d ' ' -f 2)
echo "oncall1=$ONCALL1" >> $GITHUB_ENV
echo "oncall2=$ONCALL2" >> $GITHUB_ENV
- name: Print oncalls
run: |
echo "oncall1: ${{ env.oncall1 }}"
echo "oncall2: ${{ env.oncall2 }}"
- name: Monitor New Issues
uses: react-native-community/repo-monitor@v1.0.1
with:
+4 -4
View File
@@ -23,7 +23,7 @@ jobs:
id: restore-ios-slice
uses: actions/cache/restore@v4
with:
key: v3-ios-core-${{ matrix.slice }}-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift', 'packages/react-native/scripts/ios-prebuild/*.js', 'packages/react-native/scripts/ios-prebuild.js', 'packages/react-native/React/**/*', 'packages/react-native/ReactCommon/**/*', 'packages/react-native/Libraries/**/*') }}
key: v3-ios-core-${{ matrix.slice }}-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift', 'packages/react-native/scripts/ios-prebuild/setup.js', 'packages/react-native/React/**/*', 'packages/react-native/ReactCommon/**/*', 'packages/react-native/Libraries/**/*') }}
path: packages/react-native/
- name: Setup node.js
if: steps.restore-ios-slice.outputs.cache-hit != 'true'
@@ -117,7 +117,7 @@ jobs:
uses: actions/cache/save@v4
if: ${{ github.ref == 'refs/heads/main' }} # To avoid that the cache explode
with:
key: v3-ios-core-${{ matrix.slice }}-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift', 'packages/react-native/scripts/ios-prebuild/*.js', 'packages/react-native/scripts/ios-prebuild.js', 'packages/react-native/React/**/*', 'packages/react-native/ReactCommon/**/*', 'packages/react-native/Libraries/**/*') }}
key: v3-ios-core-${{ matrix.slice }}-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift', 'packages/react-native/scripts/ios-prebuild/setup.js', 'packages/react-native/React/**/*', 'packages/react-native/ReactCommon/**/*', 'packages/react-native/Libraries/**/*') }}
path: |
packages/react-native/.build/output/spm/${{ matrix.flavor }}/Build/Products
packages/react-native/.build/headers
@@ -140,7 +140,7 @@ jobs:
uses: actions/cache/restore@v4
with:
path: packages/react-native/.build/output/xcframeworks
key: v2-ios-core-xcframework-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift', 'packages/react-native/scripts/ios-prebuild/*.js', 'packages/react-native/scripts/ios-prebuild.js', 'packages/react-native/React/**/*', 'packages/react-native/ReactCommon/**/*', 'packages/react-native/Libraries/**/*') }}
key: v2-ios-core-xcframework-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift', 'packages/react-native/scripts/ios-prebuild/setup.js', 'packages/react-native/React/**/*', 'packages/react-native/ReactCommon/**/*', 'packages/react-native/Libraries/**/*') }}
- name: Setup node.js
if: steps.restore-ios-xcframework.outputs.cache-hit != 'true'
uses: ./.github/actions/setup-node
@@ -209,4 +209,4 @@ jobs:
path: |
packages/react-native/.build/output/xcframeworks/ReactCore${{matrix.flavor}}.xcframework.tar.gz
packages/react-native/.build/output/xcframeworks/ReactCore${{matrix.flavor}}.framework.dSYM.tar.gz
key: v2-ios-core-xcframework-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift', 'packages/react-native/scripts/ios-prebuild/*.js', 'packages/react-native/scripts/ios-prebuild.js', 'packages/react-native/React/**/*', 'packages/react-native/ReactCommon/**/*', 'packages/react-native/Libraries/**/*') }}
key: v2-ios-core-xcframework-${{ matrix.flavor }}-${{ hashFiles('packages/react-native/Package.swift', 'packages/react-native/scripts/ios-prebuild/setup.js', 'packages/react-native/React/**/*', 'packages/react-native/ReactCommon/**/*', 'packages/react-native/Libraries/**/*') }}
@@ -179,9 +179,8 @@ jobs:
- name: Compress and Rename dSYM
if: steps.restore-xcframework.outputs.cache-hit != 'true'
run: |
cd packages/react-native/third-party/Symbols/
tar -cz -f ../ReactNativeDependencies${{ matrix.flavor }}.framework.dSYM.tar.gz .
mv ../ReactNativeDependencies${{ matrix.flavor }}.framework.dSYM.tar.gz ./ReactNativeDependencies${{ matrix.flavor }}.framework.dSYM.tar.gz
tar -cz -f packages/react-native/third-party/Symbols/ReactNativeDependencies${{ matrix.flavor }}.framework.dSYM.tar.gz \
packages/react-native/third-party/Symbols/ReactNativeDependencies.framework.dSYM
- name: Upload XCFramework Artifact
uses: actions/upload-artifact@v4
with:
+12 -26
View File
@@ -2,6 +2,11 @@ name: Test All
on:
workflow_dispatch:
inputs:
run-e2e-tests:
description: Whether to run E2E tests or not
type: boolean
default: false
pull_request:
push:
branches:
@@ -163,6 +168,7 @@ jobs:
flavor: ${{ matrix.flavor }}
test_e2e_ios_rntester:
if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests }}
runs-on: macos-14-large
needs:
[test_ios_rntester]
@@ -196,6 +202,7 @@ jobs:
flavor: ${{ matrix.flavor }}
test_e2e_ios_templateapp:
if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests }}
runs-on: macos-14-large
needs: [build_npm_package, prebuild_apple_dependencies]
env:
@@ -246,11 +253,6 @@ jobs:
- name: Print ReactCore folder
shell: bash
run: ls -lR /tmp/ReactCore
- name: Configure git
shell: bash
run: |
git config --global user.email "react-native-bot@meta.com"
git config --global user.name "React Native Bot"
- name: Prepare artifacts
run: |
REACT_NATIVE_PKG=$(find /tmp/react-native-tmp -type f -name "*.tgz")
@@ -293,6 +295,7 @@ jobs:
working-directory: /tmp/RNTestProject
test_e2e_android_templateapp:
if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests }}
runs-on: 4-core-ubuntu
needs: build_npm_package
strategy:
@@ -375,25 +378,6 @@ jobs:
hermes-version: ${{ needs.prepare_hermes_workspace.outputs.hermes-version }}
react-native-version: ${{ needs.prepare_hermes_workspace.outputs.react-native-version }}
run_fantom_tests:
runs-on: 8-core-ubuntu
needs: [set_release_type]
container:
image: reactnativecommunity/react-native-android:latest
env:
TERM: "dumb"
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }}
ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build and Test Fantom
uses: ./.github/actions/run-fantom-tests
with:
release-type: ${{ needs.set_release_type.outputs.RELEASE_TYPE }}
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
build_hermesc_windows:
runs-on: windows-2025
needs: prepare_hermes_workspace
@@ -430,9 +414,11 @@ jobs:
uses: ./.github/actions/build-android
with:
release-type: ${{ needs.set_release_type.outputs.RELEASE_TYPE }}
run-e2e-tests: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests }}
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
test_e2e_android_rntester:
if: ${{ github.ref == 'refs/heads/main' || contains(github.ref, 'stable') || inputs.run-e2e-tests }}
runs-on: 4-core-ubuntu
needs: [build_android]
strategy:
@@ -597,7 +583,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: ["24.4.1", "22", "20.19.4"]
node-version: ["24", "22", "20"]
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -630,7 +616,7 @@ jobs:
rerun-failed-jobs:
runs-on: ubuntu-latest
needs: [test_e2e_ios_rntester, test_e2e_android_rntester, test_e2e_ios_templateapp, test_e2e_android_templateapp]
if: ${{ github.ref == 'refs/heads/main' && always() }}
if: always()
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -0,0 +1,100 @@
name: Test Libraries on Nightlies
on:
workflow_call:
secrets:
discord_webhook_url:
required: true
# We use the matrix.library entry to specify the dependency we want to use
# The key is used directly as the <pkg> in the `yarn add <pkg>` command.
jobs:
runner-setup:
runs-on: ubuntu-latest
outputs:
runners: '{"ios":"macos-14-large", "android": "ubuntu-latest"}'
steps:
- run: echo no-op
test-library-on-nightly:
name: "[${{ matrix.platform }}] ${{ matrix.library }}"
needs: runner-setup
runs-on: ${{ fromJSON(needs.runner-setup.outputs.runners)[matrix.platform] }}
continue-on-error: true
strategy:
matrix:
library: [
"react-native-async-storage",
"react-native-blob-util",
"@react-native-clipboard/clipboard",
"@react-native-community/datetimepicker",
"react-native-gesture-handler",
"react-native-image-picker",
"react-native-linear-gradient",
"@react-native-masked-view/masked-view",
# "react-native-maps", React Native Maps with the New Arch support has a complex cocoapods setup for iOS. It needs a dedicated workflow.
"@react-native-community/netinfo",
"react-native-reanimated@nightly react-native-worklets@nightly", #reanimated requires worklet to be explicitly installed as a separate package
"react-native-svg",
"react-native-video",
"react-native-webview",
"react-native-mmkv",
"react-native-screens",
"react-native-pager-view",
"@react-native-community/slider",
# additional OSS libs used internally
"scandit-react-native-datacapture-barcode scandit-react-native-datacapture-core",
"react-native-contacts",
"react-native-device-info",
"react-native-email-link",
"@dr.pogodin/react-native-fs",
"react-native-permissions",
"react-native-vector-icons",
"react-native-masked-view",
"@react-native-community/image-editor",
]
platform: [ios, android]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: ./.github/actions/setup-node
- name: Test ${{ matrix.library }}
id: run-test
uses: ./.github/actions/test-library-on-nightly
with:
library-npm-package: ${{ matrix.library }}
platform: ${{ matrix.platform}}
- name: Save outcome
id: save-outcome
if: always()
run: |
LIB_FOLDER=$(echo "${{matrix.library}}" | tr ' ' '_' | tr '/' '_')
echo "${{matrix.library}}: ${{steps.run-test.outcome}}" > "/tmp/$LIB_FOLDER-${{ matrix.platform }}-outcome"
echo "lib_folder=$LIB_FOLDER" >> $GITHUB_OUTPUT
- name: Upload Artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: ${{ steps.save-outcome.outputs.lib_folder }}-${{ matrix.platform }}-outcome
path: /tmp/${{ steps.save-outcome.outputs.lib_folder }}-${{ matrix.platform }}-outcome
collect-results:
runs-on: ubuntu-latest
needs: [test-library-on-nightly]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Restore outcomes
uses: actions/download-artifact@v4
with:
pattern: '*-outcome'
path: /tmp
- name: Collect failures
uses: actions/github-script@v6
with:
script: |
const {collectResults} = require('./.github/workflow-scripts/collectNightlyOutcomes.js');
await collectResults('${{secrets.discord_webhook_url}}');
+1 -1
View File
@@ -43,6 +43,7 @@ project.xcworkspace
/private/helloworld/android/app/build/
/private/helloworld/android/build/
/packages/react-native-popup-menu-android/android/build/
/packages/react-native-test-library/android/build/
# Buck
.buckd
@@ -170,7 +171,6 @@ fix_*.patch
# Jest Integration
/private/react-native-fantom/build/
/private/react-native-fantom/.out/
/private/react-native-fantom/tester/build/
# [Experimental] Generated TS type definitions
+36
View File
@@ -0,0 +1,36 @@
{
"arrowParens": "avoid",
"bracketSameLine": true,
"bracketSpacing": false,
"requirePragma": true,
"singleQuote": true,
"trailingComma": "all",
"endOfLine": "lf",
"overrides": [
{
"files": ["*.code-workspace"],
"options": {
"parser": "json"
}
},
{
"files": [
"*.js",
"*.js.flow"
],
"options": {
"parser": "hermes"
}
},
{
"files": [
"**/__docs__/*.md"
],
"options": {
"parser": "markdown",
"proseWrap": "always",
"requirePragma": false
}
}
]
}
-46
View File
@@ -1,46 +0,0 @@
/**
* 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
*/
let plugins = ['prettier-plugin-hermes-parser'];
try {
plugins = require('./.prettier-plugins.fb.js');
} catch {}
module.exports = {
arrowParens: 'avoid',
bracketSameLine: true,
bracketSpacing: false,
requirePragma: true,
singleQuote: true,
trailingComma: 'all',
endOfLine: 'lf',
plugins,
overrides: [
{
files: ['*.code-workspace'],
options: {
parser: 'json',
},
},
{
files: ['*.js', '*.js.flow'],
options: {
parser: 'hermes',
},
},
{
files: ['**/__docs__/*.md'],
options: {
parser: 'markdown',
proseWrap: 'always',
requirePragma: false,
},
},
],
};
-53
View File
@@ -1,27 +1,5 @@
# Changelog (pre 0.80)
## v0.79.6
### Added
#### Android specific
- **RNGP** Add support for `exclusiveEnterpriseRepository` ([df5ac988ce](https://github.com/facebook/react-native/commit/df5ac988cec936c430d41b0fcc15181dc06e46a1) by [@cortinico](https://github.com/cortinico))
#### iOS specific
- **Cocoapods:** Add the ENTERPRISE_REPOSITORY env var to let user consume artifacts from their personal maven mirror ([a74d930c93](https://github.com/facebook/react-native/commit/a74d930c93ffae8c02142e8cc016a4c390a5f784) by [@cipolleschi](https://github.com/cipolleschi))
### Fixed
- **Codegen:** Add missing Babel dependencies ([bf2c3af93b](https://github.com/facebook/react-native/commit/bf2c3af93b146943cb35866fa9badcd188e63f5b) by [@tido64](https://github.com/tido64))
#### Android specific
- **Legacy Arch:** Fix Legacy arch crashing or freezing upon reload ([db600b2e9e](https://github.com/facebook/react-native/commit/db600b2e9e87863cad6dd5ce262dc1f793bcaeb0) by [@robhogan](https://github.com/robhogan))
- **Modal:** Fix Modal first frame being rendered on top-left corner ([5a315f8d6b](https://github.com/facebook/react-native/commit/5a315f8d6b0ea54442c7ef94b7346b0c73fd0b4c) by [@cortinico](https://github.com/cortinico))
- **TurboModule:** Fix emitting event from turbo module crashes on 32bit android ([43bc43e5e8](https://github.com/facebook/react-native/commit/43bc43e5e85519d2924c4fc80765e66d0c48b1a9) by [@vladimirivanoviliev](https://github.com/vladimirivanoviliev))
## v0.79.5
### Fixed
@@ -544,37 +522,6 @@ ChuiHW))
- **Style:** Fixed `centerContent` losing taps and causing jitter ([fe7e97a2fd](https://github.com/facebook/react-native/commit/fe7e97a2fd272db0d9d9aa7d0561337a7c8e2c30) by [@gaearon](https://github.com/gaearon))
- **Xcode:** Properly escape paths in Xcode build script used when bundling an app. ([2fee13094b](https://github.com/facebook/react-native/commit/2fee13094b3d384c071978776fd8b7cff0b6530f) by [@kraenhansen](https://github.com/kraenhansen))
# Changelog
## v0.77.3
### Added
#### Android specific
- **Gradle**: RNGP - Add support for `exclusiveEnterpriseRepository` to specify an internal Maven mirror. ([6cb8dc37c7](https://github.com/facebook/react-native/commit/6cb8dc37c74995cba3f9f0a845919f305de53c3d) by [@cortinico](https://github.com/cortinico))
### Changed
- **Metro**: Bump Metro minimum version from `^0.81.3` to `^0.81.5`. ([dfa81638dd](https://github.com/facebook/react-native/commit/dfa81638dd17e46f70f10b25c4f4fd9f370a4b0e) by [@robhogan](https://github.com/robhogan))
### Fixed
- **Timers**: Align timer IDs and timer function argument error handling with web standards. ([480a4642e5](https://github.com/facebook/react-native/commit/480a4642e5a644becf1c477d3d239f9b57efff3a) by [@kitten](https://github.com/kitten))
#### Android specific
- **Modal**: Fix Modal first frame being rendered on top-left corner. ([b950fa2afb](https://github.com/facebook/react-native/commit/b950fa2afb20e2213ff6c733cb1c2465b90406ef) by [@cortinico](https://github.com/cortinico))
- **layout**: Fix wrong `borderBottomEndRadius` on RTL. ([68d6ada448](https://github.com/facebook/react-native/commit/68d6ada44893701b6006a6b1753131c7e880a30a) by [@riteshshukla04](https://github.com/riteshshukla04))
- **Modal**: Fix `FLAG_SECURE` not respected in Modal dialog. ([7e029b0dcf](https://github.com/facebook/react-native/commit/7e029b0dcf6d1a6455a8a6343457b70e353d0ff6) by [@mateoguzmana](https://github.com/mateoguzmana))
- **lifecylcle**: Legacy arch: fix #50274, Fast Refresh sometimes breaks after full refresh. ([c43952ac22](https://github.com/facebook/react-native/commit/c43952ac22b2356be3130c906329f61e246082cb) by [@robhogan](https://github.com/robhogan))
#### iOS specific
- **Interop Layer**: Fixed adding child views to a native view using the interop layer. ([d53a60dd23](https://github.com/facebook/react-native/commit/d53a60dd23c5df8afca058a867c50df8b61f62e2) by [@chrfalch](https://github.com/chrfalch))
- **Debugger**: Restore "Paused in debugger" overlay icon. ([f30c46efbd](https://github.com/facebook/react-native/commit/f30c46efbd964d367f678181589865a3faa931cd) by [@robhogan](https://github.com/robhogan))
- **layout**: Layout direction changes are now honored on bundle reload. ([36f29beac4](https://github.com/facebook/react-native/commit/36f29beac47259768612bf56e5d9acfa4b94ab1a) by [@chrsmys](https://github.com/chrsmys))
- **file reads**: Fix crash caused by `[RCTFileRequestHanlder invalidate]`. ([789ed7d5ad](https://github.com/facebook/react-native/commit/789ed7d5ad75ad4c20ecd1eb19d1fc18275fc500) by [@zhouzh1](https://github.com/zhouzh1))
## v0.77.2
### Added
+299 -436
View File
File diff suppressed because it is too large Load Diff
+12 -35
View File
@@ -26,12 +26,10 @@ fun getListReactAndroidProperty(name: String) = reactAndroidProperties.getProper
apiValidation {
ignoredPackages.addAll(
getListReactAndroidProperty("binaryCompatibilityValidator.ignoredPackages")
)
getListReactAndroidProperty("binaryCompatibilityValidator.ignoredPackages"))
ignoredClasses.addAll(getListReactAndroidProperty("binaryCompatibilityValidator.ignoredClasses"))
nonPublicMarkers.addAll(
getListReactAndroidProperty("binaryCompatibilityValidator.nonPublicMarkers")
)
getListReactAndroidProperty("binaryCompatibilityValidator.nonPublicMarkers"))
validationDisabled =
reactAndroidProperties
.getProperty("binaryCompatibilityValidator.validationDisabled")
@@ -39,9 +37,8 @@ apiValidation {
}
version =
if (
project.hasProperty("isSnapshot") && (project.property("isSnapshot") as? String).toBoolean()
) {
if (project.hasProperty("isSnapshot") &&
(project.property("isSnapshot") as? String).toBoolean()) {
"${reactAndroidProperties.getProperty("VERSION_NAME")}-SNAPSHOT"
} else {
reactAndroidProperties.getProperty("VERSION_NAME")
@@ -69,10 +66,8 @@ tasks.register("clean", Delete::class.java) {
description = "Remove all the build files and intermediate build outputs"
dependsOn(gradle.includedBuild("gradle-plugin").task(":clean"))
subprojects.forEach {
if (
it.project.plugins.hasPlugin("com.android.library") ||
it.project.plugins.hasPlugin("com.android.application")
) {
if (it.project.plugins.hasPlugin("com.android.library") ||
it.project.plugins.hasPlugin("com.android.application")) {
dependsOn(it.tasks.named("clean"))
}
}
@@ -82,13 +77,10 @@ tasks.register("clean", Delete::class.java) {
delete(rootProject.file("./packages/react-native/sdks/download/"))
delete(rootProject.file("./packages/react-native/sdks/hermes/"))
delete(
rootProject.file("./packages/react-native/ReactAndroid/src/main/jni/prebuilt/lib/arm64-v8a/")
)
rootProject.file("./packages/react-native/ReactAndroid/src/main/jni/prebuilt/lib/arm64-v8a/"))
delete(
rootProject.file(
"./packages/react-native/ReactAndroid/src/main/jni/prebuilt/lib/armeabi-v7a/"
)
)
"./packages/react-native/ReactAndroid/src/main/jni/prebuilt/lib/armeabi-v7a/"))
delete(rootProject.file("./packages/react-native/ReactAndroid/src/main/jni/prebuilt/lib/x86/"))
delete(rootProject.file("./packages/react-native/ReactAndroid/src/main/jni/prebuilt/lib/x86_64/"))
delete(rootProject.file("./packages/react-native-codegen/lib"))
@@ -106,8 +98,7 @@ tasks.register("publishAllToMavenTempLocal") {
dependsOn(":packages:react-native:ReactAndroid:publishAllPublicationsToMavenTempLocalRepository")
// We don't publish the external-artifacts to Maven Local as ci is using it via workspace.
dependsOn(
":packages:react-native:ReactAndroid:hermes-engine:publishAllPublicationsToMavenTempLocalRepository"
)
":packages:react-native:ReactAndroid:hermes-engine:publishAllPublicationsToMavenTempLocalRepository")
}
tasks.register("publishAndroidToSonatype") {
@@ -129,8 +120,7 @@ if (project.findProperty("react.internal.useHermesNightly")?.toString()?.toBoole
That's fine for local development, but you should not commit this change.
********************************************************************************
"""
.trimIndent()
)
.trimIndent())
allprojects {
configurations.all {
resolutionStrategy.dependencySubstitution {
@@ -162,22 +152,9 @@ allprojects {
"**/build/**",
"**/hermes-engine/**",
"**/internal/featureflags/**",
"**/systeminfo/ReactNativeVersion.kt",
)
"**/systeminfo/ReactNativeVersion.kt")
listOf(
com.ncorti.ktfmt.gradle.tasks.KtfmtCheckTask::class,
com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask::class,
)
com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask::class)
.forEach { tasks.withType(it) { exclude(excludePatterns) } }
// Disable the problematic ktfmt script tasks due to symbolic link issues in subprojects
afterEvaluate {
listOf("ktfmtCheckScripts", "ktfmtFormatScripts").forEach {
tasks.findByName(it)?.enabled = false
}
}
}
// We intentionally disable the `ktfmtCheck` tasks as the formatting is primarly handled inside
// fbsource
allprojects { tasks.withType<com.ncorti.ktfmt.gradle.tasks.KtfmtCheckTask>() { enabled = false } }
@@ -91,19 +91,7 @@ declare module 'tinybench' {
beforeEach?: (this: Task) => void | Promise<void>,
};
// This is defined as an interface in tinybench but we define it as an object
// to catch problems like `overriddenDuration` being misspelled.
export type FnReturnedObject = {
overriddenDuration?: number,
};
// This type is defined as returning `unknown` instead of `void` in tinybench,
// but we type it this way to avoid mistakes (we can make breaking changes
// in our definition that they can't).
export type Fn = () =>
| Promise<void | FnReturnedObject>
| void
| FnReturnedObject;
export type Fn = () => Promise<mixed> | mixed;
declare export class Bench extends EventTarget {
concurrency: null | 'task' | 'bench';
+2 -2
View File
@@ -21,7 +21,7 @@ declare type ws$PerMessageDeflateOptions = {
maxPayload?: number,
};
/* $FlowFixMe[incompatible-type] - Found with Flow v0.143.1 upgrade
/* $FlowFixMe[incompatible-extend] - Found with Flow v0.143.1 upgrade
* "on" definition failing with string is incompatible with string literal */
declare class ws$WebSocketServer extends events$EventEmitter {
/**
@@ -141,7 +141,7 @@ declare type ws$UnexpectedResponseListener = (
) => mixed;
declare type ws$UpgradeListener = (response: http$IncomingMessage<>) => mixed;
/* $FlowFixMe[incompatible-type] - Found with Flow v0.143.1 upgrade
/* $FlowFixMe[incompatible-extend] - Found with Flow v0.143.1 upgrade
* "on" definition failing with string is incompatible with string literal */
declare class ws$WebSocket extends events$EventEmitter {
static Server: typeof ws$WebSocketServer;
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+2 -2
View File
@@ -86,7 +86,7 @@ module.exports = {
globalPrefix: '',
hermesParser: true,
hot: false,
// $FlowFixMe[incompatible-type] TODO: Remove when `inlineRequires` has been removed from metro-babel-transformer in OSS
// $FlowFixMe[incompatible-call] TODO: Remove when `inlineRequires` has been removed from metro-babel-transformer in OSS
inlineRequires: true,
minify: false,
platform: '',
@@ -111,7 +111,7 @@ module.exports = {
return generate(
ast,
// $FlowFixMe[incompatible-type] Error found when improving flow typing for libs
// $FlowFixMe[prop-missing] Error found when improving flow typing for libs
{
code: true,
comments: false,
+17 -19
View File
@@ -56,14 +56,14 @@
"@electron/packager": "^18.3.6",
"@jest/create-cache-key-function": "^29.7.0",
"@microsoft/api-extractor": "^7.52.2",
"@react-native/metro-babel-transformer": "0.82.0-main",
"@react-native/metro-config": "0.82.0-main",
"@react-native/metro-babel-transformer": "0.81.0-main",
"@react-native/metro-config": "0.81.0-main",
"@tsconfig/node22": "22.0.2",
"@types/react": "^19.1.0",
"@typescript-eslint/parser": "^8.36.0",
"ansi-styles": "^4.2.1",
"babel-plugin-minify-dead-code-elimination": "^0.5.2",
"babel-plugin-syntax-hermes-parser": "0.32.0",
"babel-plugin-syntax-hermes-parser": "0.29.1",
"babel-plugin-transform-define": "^2.1.4",
"babel-plugin-transform-flow-enums": "^0.0.2",
"clang-format": "^1.8.0",
@@ -75,49 +75,47 @@
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-ft-flow": "^2.0.1",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-jsx-a11y": "^6.6.0",
"eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-native": "^4.0.0",
"eslint-plugin-redundant-undefined": "^0.4.0",
"eslint-plugin-relay": "^1.8.3",
"flow-api-translator": "0.32.0",
"flow-bin": "^0.280.0",
"flow-api-translator": "0.29.1",
"flow-bin": "^0.275.0",
"glob": "^7.1.1",
"hermes-eslint": "0.32.0",
"hermes-transform": "0.32.0",
"hermes-eslint": "0.29.1",
"hermes-transform": "0.29.1",
"ini": "^5.0.0",
"inquirer": "^7.1.0",
"jest": "^29.7.0",
"jest-config": "^29.7.0",
"jest-diff": "^29.7.0",
"jest-junit": "^16.0.0",
"jest-junit": "^10.0.0",
"jest-snapshot": "^29.7.0",
"markdownlint-cli2": "^0.17.2",
"markdownlint-rule-relative-links": "^3.0.0",
"memfs": "^4.7.7",
"metro-babel-register": "^0.83.1",
"metro-transform-plugins": "^0.83.1",
"metro-babel-register": "^0.83.0",
"metro-transform-plugins": "^0.83.0",
"micromatch": "^4.0.4",
"node-fetch": "^2.2.0",
"nullthrows": "^1.1.1",
"prettier": "3.6.2",
"prettier-plugin-hermes-parser": "0.32.0",
"react": "19.1.1",
"react-test-renderer": "19.1.1",
"prettier": "2.8.8",
"prettier-plugin-hermes-parser": "0.29.1",
"react": "19.1.0",
"react-test-renderer": "19.1.0",
"rimraf": "^3.0.2",
"shelljs": "^0.8.5",
"signedsource": "^1.0.0",
"supports-color": "^7.1.0",
"temp-dir": "^2.0.0",
"tinybench": "^4.1.0",
"tinybench": "^3.1.0",
"typescript": "5.8.3",
"ws": "^6.2.3"
},
"resolutions": {
"eslint-plugin-react-hooks": "6.1.0-canary-12bc60f5-20250613",
"react-is": "19.1.1",
"on-headers": "1.1.0",
"compression": "1.8.1"
"react-is": "19.1.0"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@react-native/assets-registry",
"version": "0.82.0-main",
"version": "0.81.0-main",
"description": "Asset support code for React Native.",
"license": "MIT",
"repository": {
@@ -81,147 +81,10 @@ export {Commands};
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
`;
const COMMANDS_WITH_COVERAGE_INVALID = `
// @flow
const codegenNativeComponent = require('codegenNativeComponent');
import type {NativeComponentType} from 'codegenNativeComponent';
import type {ViewProps} from 'ViewPropTypes';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
// Coverage instrumentation of invalid Commands export - should still fail
export const Commands = (cov_1234567890().s[0]++, {
hotspotUpdate: () => {},
scrollTo: () => {},
});
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
`;
const COMMANDS_WITH_COVERAGE_WRONG_FUNCTION = `
// @flow
const codegenNativeComponent = require('codegenNativeComponent');
import type {NativeComponentType} from 'codegenNativeComponent';
import type {ViewProps} from 'ViewPropTypes';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
// Coverage instrumentation of wrong function call - should fail
export const Commands = (cov_abcdef123().s[0]++, someOtherFunction({
supportedCommands: ['pause', 'play'],
}));
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
`;
const COMMANDS_WITH_COMPLEX_COVERAGE_INVALID = `
// @flow
const codegenNativeComponent = require('codegenNativeComponent');
import type {NativeComponentType} from 'codegenNativeComponent';
import type {ViewProps} from 'ViewPropTypes';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
// Complex coverage instrumentation with invalid nested structure - should fail
export const Commands = (
cov_xyz789().f[1]++,
cov_xyz789().s[2]++,
{
pause: (ref) => {},
play: (ref) => {},
}
);
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
`;
const COMMANDS_WITH_COVERAGE_WRONG_NAME = `
// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type {NativeComponentType} from 'codegenNativeComponent';
import type {ViewProps} from 'ViewPropTypes';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+pause: (viewRef: React.ElementRef<NativeType>) => void;
+play: (viewRef: React.ElementRef<NativeType>) => void;
}
// Coverage instrumentation with correct function but wrong export name - should fail
export const WrongName = (cov_wrong123().s[0]++, codegenNativeCommands<NativeCommands>({
supportedCommands: ['pause', 'play'],
}));
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
`;
const COMMANDS_WITH_COVERAGE_TYPE_CAST_INVALID = `
// @flow
const codegenNativeComponent = require('codegenNativeComponent');
import type {NativeComponentType} from 'codegenNativeComponent';
import type {ViewProps} from 'ViewPropTypes';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+pause: (viewRef: React.ElementRef<NativeType>) => void;
+play: (viewRef: React.ElementRef<NativeType>) => void;
}
// Coverage instrumentation with type cast but wrong function - should fail
export const Commands: NativeCommands = (cov_cast123().s[0]++, invalidFunction({
supportedCommands: ['pause', 'play'],
}));
export default (codegenNativeComponent<ModuleProps>('Module'): NativeType);
`;
module.exports = {
'CommandsExportedWithDifferentNameNativeComponent.js':
COMMANDS_EXPORTED_WITH_DIFFERENT_NAME,
'CommandsExportedWithShorthandNativeComponent.js':
COMMANDS_EXPORTED_WITH_SHORTHAND,
'OtherCommandsExportNativeComponent.js': OTHER_COMMANDS_EXPORT,
'CommandsWithCoverageInvalidNativeComponent.js':
COMMANDS_WITH_COVERAGE_INVALID,
'CommandsWithCoverageWrongFunctionNativeComponent.js':
COMMANDS_WITH_COVERAGE_WRONG_FUNCTION,
'CommandsWithComplexCoverageInvalidNativeComponent.js':
COMMANDS_WITH_COMPLEX_COVERAGE_INVALID,
'CommandsWithCoverageWrongNameNativeComponent.js':
COMMANDS_WITH_COVERAGE_WRONG_NAME,
'CommandsWithCoverageTypeCastInvalidNativeComponent.js':
COMMANDS_WITH_COVERAGE_TYPE_CAST_INVALID,
};
@@ -59,92 +59,6 @@ export default codegenNativeComponent<ModuleProps>('Module', {
});
`;
// Coverage instrumentation test cases - should be recognized as valid
const COMMANDS_WITH_SIMPLE_COVERAGE = `
// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type {ViewProps} from 'ViewPropTypes';
import type {NativeComponentType} from 'codegenNativeComponent';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+pause: (viewRef: React.ElementRef<NativeType>) => void;
+play: (viewRef: React.ElementRef<NativeType>) => void;
}
export const Commands = (cov_1234567890.s[0]++, codegenNativeCommands<NativeCommands>({
supportedCommands: ['pause', 'play'],
}));
export default codegenNativeComponent<ModuleProps>('Module');
`;
const COMMANDS_WITH_COMPLEX_COVERAGE = `
// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type {ViewProps} from 'ViewPropTypes';
import type {NativeComponentType} from 'codegenNativeComponent';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+seek: (viewRef: React.ElementRef<NativeType>, position: number) => void;
+stop: (viewRef: React.ElementRef<NativeType>) => void;
}
export const Commands = (
cov_abcdef123().f[2]++,
cov_abcdef123().s[5]++,
codegenNativeCommands<NativeCommands>({
supportedCommands: ['seek', 'stop'],
})
);
export default codegenNativeComponent<ModuleProps>('Module');
`;
const COMMANDS_WITH_TYPE_CAST_COVERAGE = `
// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type {ViewProps} from 'ViewPropTypes';
import type {NativeComponentType} from 'codegenNativeComponent';
type ModuleProps = $ReadOnly<{|
...ViewProps,
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+mute: (viewRef: React.ElementRef<NativeType>) => void;
+unmute: (viewRef: React.ElementRef<NativeType>) => void;
}
export const Commands: NativeCommands = (cov_xyz789().s[1]++, codegenNativeCommands<NativeCommands>({
supportedCommands: ['mute', 'unmute'],
}));
export default codegenNativeComponent<ModuleProps>('Module');
`;
const FULL_NATIVE_COMPONENT_WITH_TYPE_EXPORT = `
// @flow
@@ -193,9 +107,4 @@ module.exports = {
'NotANativeComponent.js': NOT_A_NATIVE_COMPONENT,
'FullNativeComponent.js': FULL_NATIVE_COMPONENT,
'FullTypedNativeComponent.js': FULL_NATIVE_COMPONENT_WITH_TYPE_EXPORT,
'CommandsWithSimpleCoverageNativeComponent.js': COMMANDS_WITH_SIMPLE_COVERAGE,
'CommandsWithComplexCoverageNativeComponent.js':
COMMANDS_WITH_COMPLEX_COVERAGE,
'CommandsWithTypeCastCoverageNativeComponent.js':
COMMANDS_WITH_TYPE_CAST_COVERAGE,
};
@@ -1,77 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Babel plugin inline view configs can inline config for CommandsWithComplexCoverageNativeComponent.js 1`] = `
"// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type { ViewProps } from 'ViewPropTypes';
import type { NativeComponentType } from 'codegenNativeComponent';
type ModuleProps = $ReadOnly<{|
...ViewProps
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+seek: (viewRef: React.ElementRef<NativeType>, position: number) => void,
+stop: (viewRef: React.ElementRef<NativeType>) => void,
}
const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');
let nativeComponentName = 'Module';
export const __INTERNAL_VIEW_CONFIG = {
uiViewClassName: \\"Module\\",
validAttributes: {}
};
export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG);"
`;
exports[`Babel plugin inline view configs can inline config for CommandsWithSimpleCoverageNativeComponent.js 1`] = `
"// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type { ViewProps } from 'ViewPropTypes';
import type { NativeComponentType } from 'codegenNativeComponent';
type ModuleProps = $ReadOnly<{|
...ViewProps
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+pause: (viewRef: React.ElementRef<NativeType>) => void,
+play: (viewRef: React.ElementRef<NativeType>) => void,
}
const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');
let nativeComponentName = 'Module';
export const __INTERNAL_VIEW_CONFIG = {
uiViewClassName: \\"Module\\",
validAttributes: {}
};
export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG);"
`;
exports[`Babel plugin inline view configs can inline config for CommandsWithTypeCastCoverageNativeComponent.js 1`] = `
"// @flow
const codegenNativeCommands = require('codegenNativeCommands');
const codegenNativeComponent = require('codegenNativeComponent');
import type { ViewProps } from 'ViewPropTypes';
import type { NativeComponentType } from 'codegenNativeComponent';
type ModuleProps = $ReadOnly<{|
...ViewProps
|}>;
type NativeType = NativeComponentType<ModuleProps>;
interface NativeCommands {
+mute: (viewRef: React.ElementRef<NativeType>) => void,
+unmute: (viewRef: React.ElementRef<NativeType>) => void,
}
const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');
let nativeComponentName = 'Module';
export const __INTERNAL_VIEW_CONFIG = {
uiViewClassName: \\"Module\\",
validAttributes: {}
};
export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG);"
`;
exports[`Babel plugin inline view configs can inline config for FullNativeComponent.js 1`] = `
"// @flow
@@ -225,61 +153,6 @@ exports[`Babel plugin inline view configs fails on inline config for CommandsExp
24 |"
`;
exports[`Babel plugin inline view configs fails on inline config for CommandsWithComplexCoverageInvalidNativeComponent.js 1`] = `
"/CommandsWithComplexCoverageInvalidNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
14 |
15 | // Complex coverage instrumentation with invalid nested structure - should fail
> 16 | export const Commands = (
| ^
17 | cov_xyz789().f[1]++,
18 | cov_xyz789().s[2]++,
19 | {"
`;
exports[`Babel plugin inline view configs fails on inline config for CommandsWithCoverageInvalidNativeComponent.js 1`] = `
"/CommandsWithCoverageInvalidNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
14 |
15 | // Coverage instrumentation of invalid Commands export - should still fail
> 16 | export const Commands = (cov_1234567890().s[0]++, {
| ^
17 | hotspotUpdate: () => {},
18 | scrollTo: () => {},
19 | });"
`;
exports[`Babel plugin inline view configs fails on inline config for CommandsWithCoverageTypeCastInvalidNativeComponent.js 1`] = `
"/CommandsWithCoverageTypeCastInvalidNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
19 |
20 | // Coverage instrumentation with type cast but wrong function - should fail
> 21 | export const Commands: NativeCommands = (cov_cast123().s[0]++, invalidFunction({
| ^
22 | supportedCommands: ['pause', 'play'],
23 | }));
24 |"
`;
exports[`Babel plugin inline view configs fails on inline config for CommandsWithCoverageWrongFunctionNativeComponent.js 1`] = `
"/CommandsWithCoverageWrongFunctionNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
14 |
15 | // Coverage instrumentation of wrong function call - should fail
> 16 | export const Commands = (cov_abcdef123().s[0]++, someOtherFunction({
| ^
17 | supportedCommands: ['pause', 'play'],
18 | }));
19 |"
`;
exports[`Babel plugin inline view configs fails on inline config for CommandsWithCoverageWrongNameNativeComponent.js 1`] = `
"/CommandsWithCoverageWrongNameNativeComponent.js: Native commands must be exported with the name 'Commands'
20 |
21 | // Coverage instrumentation with correct function but wrong export name - should fail
> 22 | export const WrongName = (cov_wrong123().s[0]++, codegenNativeCommands<NativeCommands>({
| ^
23 | supportedCommands: ['pause', 'play'],
24 | }));
25 |"
`;
exports[`Babel plugin inline view configs fails on inline config for OtherCommandsExportNativeComponent.js 1`] = `
"/OtherCommandsExportNativeComponent.js: 'Commands' is a reserved export and may only be used to export the result of codegenNativeCommands.
17 | }
+9 -61
View File
@@ -24,12 +24,12 @@ try {
} catch (e) {
// Fallback to lib when source doesn't exit (e.g. when installed as a dev dependency)
FlowParser =
// $FlowFixMe[cannot-resolve-module]
// $FlowIgnore[cannot-resolve-module]
require('@react-native/codegen/lib/parsers/flow/parser').FlowParser;
TypeScriptParser =
// $FlowFixMe[cannot-resolve-module]
// $FlowIgnore[cannot-resolve-module]
require('@react-native/codegen/lib/parsers/typescript/parser').TypeScriptParser;
// $FlowFixMe[cannot-resolve-module]
// $FlowIgnore[cannot-resolve-module]
RNCodegen = require('@react-native/codegen/lib/generators/RNCodegen');
}
@@ -102,58 +102,6 @@ function isCodegenDeclaration(declaration) {
return false;
}
function isCodegenNativeCommandsDeclaration(declaration) {
if (!declaration) {
return false;
}
// Handle direct calls: codegenNativeCommands()
if (
declaration.type === 'CallExpression' &&
declaration.callee &&
declaration.callee.type === 'Identifier' &&
declaration.callee.name === 'codegenNativeCommands'
) {
return true;
}
// Handle coverage instrumentation: (cov_xxx().s[0]++, codegenNativeCommands())
if (declaration.type === 'SequenceExpression' && declaration.expressions) {
// Get the last expression in the sequence (the actual function call)
const lastExpression =
declaration.expressions[declaration.expressions.length - 1];
// Recursively check if the last expression is a valid codegenNativeCommands call
return isCodegenNativeCommandsDeclaration(lastExpression);
}
// Handle Flow type casts: (codegenNativeCommands(): NativeCommands)
if (
(declaration.type === 'TypeCastExpression' ||
declaration.type === 'AsExpression') &&
declaration.expression &&
declaration.expression.type === 'CallExpression' &&
declaration.expression.callee &&
declaration.expression.callee.type === 'Identifier' &&
declaration.expression.callee.name === 'codegenNativeCommands'
) {
return true;
}
// Handle TypeScript assertions: codegenNativeCommands() as NativeCommands
if (
declaration.type === 'TSAsExpression' &&
declaration.expression &&
declaration.expression.type === 'CallExpression' &&
declaration.expression.callee &&
declaration.expression.callee.type === 'Identifier' &&
declaration.expression.callee.name === 'codegenNativeCommands'
) {
return true;
}
return false;
}
module.exports = function ({parse, types: t}) {
return {
pre(state) {
@@ -177,12 +125,12 @@ module.exports = function ({parse, types: t}) {
const firstDeclaration = path.node.declaration.declarations[0];
if (firstDeclaration.type === 'VariableDeclarator') {
// Check if this is a valid codegenNativeCommands call, handling type annotations
const isValidCommandsExport = isCodegenNativeCommandsDeclaration(
firstDeclaration.init,
);
if (isValidCommandsExport) {
if (
firstDeclaration.init &&
firstDeclaration.init.type === 'CallExpression' &&
firstDeclaration.init.callee.type === 'Identifier' &&
firstDeclaration.init.callee.name === 'codegenNativeCommands'
) {
if (
firstDeclaration.id.type === 'Identifier' &&
firstDeclaration.id.name !== 'Commands'
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@react-native/babel-plugin-codegen",
"version": "0.82.0-main",
"version": "0.81.0-main",
"description": "Babel plugin to generate native module and view manager code for React Native.",
"license": "MIT",
"repository": {
@@ -26,7 +26,7 @@
],
"dependencies": {
"@babel/traverse": "^7.25.3",
"@react-native/codegen": "0.82.0-main"
"@react-native/codegen": "0.81.0-main"
},
"devDependencies": {
"@babel/core": "^7.25.2"
+6 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@react-native/community-cli-plugin",
"version": "0.82.0-main",
"version": "0.81.0-main",
"description": "Core CLI commands for React Native",
"keywords": [
"react-native",
@@ -22,16 +22,16 @@
"dist"
],
"dependencies": {
"@react-native/dev-middleware": "0.82.0-main",
"@react-native/dev-middleware": "0.81.0-main",
"debug": "^4.4.0",
"invariant": "^2.2.4",
"metro": "^0.83.1",
"metro-config": "^0.83.1",
"metro-core": "^0.83.1",
"metro": "^0.83.0",
"metro-config": "^0.83.0",
"metro-core": "^0.83.0",
"semver": "^7.1.3"
},
"devDependencies": {
"metro-resolver": "^0.83.1"
"metro-resolver": "^0.83.0"
},
"peerDependencies": {
"@react-native-community/cli": "*",
@@ -40,9 +40,6 @@
"peerDependenciesMeta": {
"@react-native-community/cli": {
"optional": true
},
"@react-native/metro-config": {
"optional": true
}
},
"engines": {
@@ -116,7 +116,7 @@ function copyAll(filesToCopy: CopiedFiles) {
const src = queue.shift();
// $FlowFixMe[incompatible-type]
const dest = filesToCopy[src];
// $FlowFixMe[incompatible-type]
// $FlowFixMe[incompatible-call]
copy(src, dest, copyNext);
}
};
@@ -31,9 +31,9 @@ type MiddlewareReturn = {
...
};
// $FlowFixMe[incompatible-type]
// $FlowFixMe
const unusedStubWSServer: ws$WebSocketServer = {};
// $FlowFixMe[incompatible-type]
// $FlowFixMe
const unusedMiddlewareStub: Server = {};
const communityMiddlewareFallback = {
@@ -80,7 +80,7 @@ try {
'@react-native-community/cli-server-api',
{paths: [communityCliPath]},
);
// $FlowFixMe[unsupported-syntax] dynamic import
// $FlowIgnore[unsupported-syntax] dynamic import
communityMiddlewareFallback.createDevServerMiddleware = require(
communityCliServerApiPath,
).createDevServerMiddleware as CreateDevServerMiddleware;
@@ -92,14 +92,14 @@ async function runServer(
console.info(`Starting dev server on ${devServerUrl}\n`);
if (args.assetPlugins) {
// $FlowFixMe[cannot-write] Assigning to readonly property
// $FlowIgnore[cannot-write] Assigning to readonly property
metroConfig.transformer.assetPlugins = args.assetPlugins.map(plugin =>
require.resolve(plugin),
);
}
// TODO(T214991636): Remove legacy Metro log forwarding
if (!args.clientLogs) {
// $FlowFixMe[cannot-write] Assigning to readonly property
// $FlowIgnore[cannot-write] Assigning to readonly property
metroConfig.server.forwardClientLogs = false;
}
@@ -127,8 +127,6 @@ async function runServer(
const reporter: Reporter = {
update(event: TerminalReportableEvent) {
terminalReporter.update(event);
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
if (reportEvent) {
reportEvent(event);
}
@@ -146,7 +144,7 @@ async function runServer(
}
},
};
// $FlowFixMe[cannot-write] Assigning to readonly property
// $FlowIgnore[cannot-write] Assigning to readonly property
metroConfig.reporter = reporter;
await Metro.runServer(metroConfig, {
@@ -175,7 +173,7 @@ function getReporterImpl(
try {
// First we let require resolve it, so we can require packages in node_modules
// as expected. eg: require('my-package/reporter');
// $FlowFixMe[unsupported-syntax]
// $FlowIgnore[unsupported-syntax]
return require(customLogReporterPath);
} catch (e) {
if (e.code !== 'MODULE_NOT_FOUND') {
@@ -183,7 +181,7 @@ function getReporterImpl(
}
// If that doesn't work, then we next try relative to the cwd, eg:
// require('./reporter');
// $FlowFixMe[unsupported-syntax]
// $FlowIgnore[unsupported-syntax]
return require(path.resolve(customLogReporterPath));
}
}
@@ -88,6 +88,7 @@ Diff: ${styleText(['dim', 'underline'], newVersion?.diffUrl ?? 'none')}
}
}
// $FlowFixMe
function isDiffPurgeEntry(data: Partial<DiffPurge>): data is DiffPurge {
return (
// $FlowFixMe[incompatible-type-guard]
@@ -152,7 +153,7 @@ function buildDiffUrl(oldVersion: string, newVersion: string) {
* Returns the most recent React Native version available to upgrade to.
*/
async function getLatestRnDiffPurgeVersion(): Promise<LatestVersions | void> {
const options: RequestOptions = {
const options = {
// https://developer.github.com/v3/#user-agent-required
headers: {'User-Agent': '@react-native/community-cli-plugin'} as Headers,
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@react-native/core-cli-utils",
"version": "0.82.0-main",
"version": "0.81.0-main",
"description": "React Native CLI library for Frameworks to build on",
"license": "MIT",
"main": "./src/index.flow.js",
+1 -1
View File
@@ -71,7 +71,7 @@ const FIRST = 1,
FOURTH = 4;
function getNodePackagePath(packageName: string): string {
// $FlowFixMe[prop-missing] type definition is incomplete
// $FlowIgnore[prop-missing] type definition is incomplete
return require.resolve(packageName, {cwd: [process.cwd(), ...module.paths]});
}
+1 -1
View File
@@ -75,7 +75,7 @@ const FIRST = 1,
FIFTH = 5;
function getNodePackagePath(packageName: string): string {
// $FlowFixMe[prop-missing] type definition is incomplete
// $FlowIgnore[prop-missing] type definition is incomplete
return require.resolve(packageName, {cwd: [process.cwd(), ...module.paths]});
}
+2 -2
View File
@@ -1,5 +1,5 @@
@generated SignedSource<<b6a779e724ecce5e727ee674dc20f809>>
Git revision: e87564a24cf233c60aaebee8c418ec85724f7214
@generated SignedSource<<e2d97b04634bf3b566f33d455f38658f>>
Git revision: 8dc0d5b365e6b4600b78e5a7381aa861d8c1c81e
Built with --nohooks: false
Is local checkout: false
Remote URL: https://github.com/facebook/react-native-devtools-frontend
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@react-native/debugger-frontend",
"version": "0.82.0-main",
"version": "0.81.0-main",
"description": "Debugger frontend for React Native based on Chrome DevTools",
"keywords": [
"react-native",
@@ -1,33 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`prepareDebuggerShellFromDotSlashFile fails with the expected error message for a missing dotslash file 1`] = `
Object {
"code": "unexpected_error",
"humanReadableMessage": "An unexpected error occured while installing the latest version of React Native DevTools. Using a fallback version instead.",
"verboseInfo": Any<String>,
}
`;
exports[`prepareDebuggerShellFromDotSlashFile fails with the expected error message for missing platforms 1`] = `
Object {
"code": "platform_not_supported",
"humanReadableMessage": "The latest version of React Native DevTools is not supported on this platform. Using a fallback version instead.",
"verboseInfo": Any<String>,
}
`;
exports[`prepareDebuggerShellFromDotSlashFile scenarios requiring a local HTTP server fails with the expected error message for a corrupted tarball 1`] = `
Object {
"code": "possible_corruption",
"humanReadableMessage": "Failed to verify the latest version of React Native DevTools. Using a fallback version instead. ",
"verboseInfo": Any<String>,
}
`;
exports[`prepareDebuggerShellFromDotSlashFile scenarios requiring a local HTTP server fails with the expected error message for a network error 1`] = `
Object {
"code": "likely_offline",
"humanReadableMessage": "Failed to download the latest version of React Native DevTools. Using a fallback version instead. Connect to the internet or check your network settings.",
"verboseInfo": Any<String>,
}
`;
@@ -1,59 +0,0 @@
#!/usr/bin/env dotslash
{
"name": "React Native DevTools",
"platforms": {
"linux-aarch64": {
"size": 113510892,
"hash": "sha256",
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"providers": [
{
"type": "http",
"url": "http://$HOST:$PORT/corrupted.tar.gz"
}
],
"format": "tar.gz",
"path": "React Native DevTools-linux-arm64/React Native DevTools"
},
"linux-x86_64": {
"size": 113243910,
"hash": "sha256",
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"providers": [
{
"type": "http",
"url": "http://$HOST:$PORT/corrupted.tar.gz"
}
],
"format": "tar.gz",
"path": "React Native DevTools-linux-x64/React Native DevTools"
},
"macos-aarch64": {
"size": 108810433,
"hash": "sha256",
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"providers": [
{
"type": "http",
"url": "http://$HOST:$PORT/corrupted.tar.gz"
}
],
"format": "tar.gz",
"path": "React Native DevTools.app/Contents/MacOS/React Native DevTools"
},
"macos-x86_64": {
"size": 113769989,
"hash": "sha256",
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"providers": [
{
"type": "http",
"url": "http://$HOST:$PORT/corrupted.tar.gz"
}
],
"format": "tar.gz",
"path": "React Native DevTools.app/Contents/MacOS/React Native DevTools"
}
}
}
@@ -1,59 +0,0 @@
#!/usr/bin/env dotslash
{
"name": "React Native DevTools",
"platforms": {
"linux-aarch64": {
"size": 113510892,
"hash": "sha256",
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"providers": [
{
"type": "http",
"url": "https://$HOST:$PORT/does-not-exist"
}
],
"format": "tar.gz",
"path": "React Native DevTools-linux-arm64/React Native DevTools"
},
"linux-x86_64": {
"size": 113243910,
"hash": "sha256",
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"providers": [
{
"type": "http",
"url": "https://$HOST:$PORT/does-not-exist"
}
],
"format": "tar.gz",
"path": "React Native DevTools-linux-x64/React Native DevTools"
},
"macos-aarch64": {
"size": 108810433,
"hash": "sha256",
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"providers": [
{
"type": "http",
"url": "https://$HOST:$PORT/does-not-exist"
}
],
"format": "tar.gz",
"path": "React Native DevTools.app/Contents/MacOS/React Native DevTools"
},
"macos-x86_64": {
"size": 113769989,
"hash": "sha256",
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"providers": [
{
"type": "http",
"url": "https://$HOST:$PORT/does-not-exist"
}
],
"format": "tar.gz",
"path": "React Native DevTools.app/Contents/MacOS/React Native DevTools"
}
}
}
@@ -1,6 +0,0 @@
#!/usr/bin/env dotslash
{
"name": "React Native DevTools",
"platforms": {}
}
@@ -1,139 +0,0 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
const {
prepareDebuggerShellFromDotSlashFile,
} = require('../src/node/private/LaunchUtils');
const fs = require('fs').promises;
const http = require('http');
const os = require('os');
const path = require('path');
// The implementation of prepareDebuggerShellFromDotSlashFile relies on
// details of DotSlash that are not guaranteed to be stable (support for
// `dotslash -- fetch <file>`, certain strings being printed to stderr).
// This (admittedly elaborate) test suite ensures we'll fail loudly if we
// try to upgrade DotSlash to a version that breaks our assumptions.
describe('prepareDebuggerShellFromDotSlashFile', () => {
test('fails with the expected error message for missing platforms', async () => {
const result = await prepareDebuggerShellFromDotSlashFile(
path.join(__dirname, 'dotslash-file-with-missing-platforms.jsonc'),
);
expect(result).toMatchSnapshot({
verboseInfo: expect.any(String),
});
});
test('fails with the expected error message for a missing dotslash file', async () => {
const result = await prepareDebuggerShellFromDotSlashFile(
path.join(__dirname, 'dotslash-file-that-does-not-exist.jsonc'),
);
expect(result).toMatchSnapshot({
verboseInfo: expect.any(String),
});
});
describe('scenarios requiring a local HTTP server', () => {
let server, scratchDir;
beforeEach(async () => {
scratchDir = await fs.mkdtemp(path.join(os.tmpdir(), 'dotslash-test-'));
server = http.createServer((request, response) => {
if (request.url === '/corrupted.tar.gz') {
response.writeHead(200, {'Content-Type': 'application/gzip'});
response.end(
'Hello, world!\n' + 'This simulated a corrupted tarball.',
);
} else {
response.writeHead(404);
response.end();
}
});
await new Promise((resolve, reject) => {
server.on('error', reject);
server.listen(0, 'localhost', () => {
server.removeListener('error', reject);
resolve();
});
});
});
afterEach(async () => {
await fs.rm(scratchDir, {recursive: true, force: true});
if (server.listening) {
await new Promise((resolve, reject) => {
server.close(error => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
});
test('fails with the expected error message for a corrupted tarball', async () => {
const dotslashFileContents = injectHostPort(
await fs.readFile(
path.join(
__dirname,
'dotslash-file-simulating-data-corruption.jsonc',
),
'utf8',
),
server.address(),
);
await fs.writeFile(
path.join(scratchDir, 'dotslash-file.jsonc'),
dotslashFileContents,
);
const result = await prepareDebuggerShellFromDotSlashFile(
path.join(scratchDir, 'dotslash-file.jsonc'),
);
expect(result).toMatchSnapshot({
verboseInfo: expect.any(String),
});
});
test('fails with the expected error message for a network error', async () => {
const dotslashFileContents = injectHostPort(
await fs.readFile(
path.join(__dirname, 'dotslash-file-simulating-network-error.jsonc'),
'utf8',
),
server.address(),
);
await fs.writeFile(
path.join(scratchDir, 'dotslash-file.jsonc'),
dotslashFileContents,
);
const result = await prepareDebuggerShellFromDotSlashFile(
path.join(scratchDir, 'dotslash-file.jsonc'),
);
expect(result).toMatchSnapshot({
verboseInfo: expect.any(String),
});
});
});
});
function injectHostPort(
dotslashFileContents: string,
address: net$Socket$address,
) {
const host =
address.family === 'IPv6' ? `[${address.address}]` : address.address;
return dotslashFileContents
.replaceAll('$HOST', host)
.replaceAll('$PORT', address.port.toString());
}
@@ -19,13 +19,13 @@ const semver = require('semver');
// safety to ensure the target of the resolution is in sync with the declared dependency.
describe('Electron dependency', () => {
test('should be semver-satisfied by the actual electron version', () => {
// $FlowFixMe[untyped-import] - package.json is not typed
// $FlowIssue[untyped-import] - package.json is not typed
const ourPackageJson = require('../package.json');
const declaredElectronVersion = ourPackageJson.devDependencies.electron;
const declaredElectronVersion = ourPackageJson.dependencies.electron;
expect(declaredElectronVersion).toBeTruthy();
// $FlowFixMe[untyped-import] - package.json is not typed
// $FlowIssue[untyped-import] - package.json is not typed
const electronPackageJson = require('electron/package.json');
const actualElectronVersion = electronPackageJson.version;
@@ -1,75 +0,0 @@
#!/usr/bin/env dotslash
// @generated SignedSource<<e93d55b5e28943e44271f5d3738083e0>>
{
"name": "React Native DevTools",
"platforms": {
"linux-aarch64": {
"size": 116060647,
"hash": "sha256",
"digest": "4352f1c9848ca919101ec628bd08b87a72a828d1ab55fa43a02098329fa452fa",
"providers": [
{
"type": "http",
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQM24LW0nPZRk0ypuIG9prz_72YjBJNcVlGSIEpO4zdlLXgw4dFNodH7MKg9eKNTnx7wrVDDBNACrnEPt_OfXOjZyZsV9Oaqu0-vNFRdlEyis4YqmpqGLtz3LvD-9R6fzcWxJI9zrhdPvOvlXP-3Syt1UNITaxXDVqIwAAYpCaAh0oLpupAFCc2yvYw"
}
],
"format": "tar.gz",
"path": "React Native DevTools-linux-arm64/React Native DevTools"
},
"linux-x86_64": {
"size": 115930333,
"hash": "sha256",
"digest": "11c7b07942928a6301b07fbf2bc77ce1229b2a52891f23541cdd9858b5250e64",
"providers": [
{
"type": "http",
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQNDwm7HZRhtNxHqMr1FfSb0afHFGrn1OHxH0gOiggrLrht9QRUgJ3GG5jj7huhQzMRogE-LCMsnxh1ioOZks-YYX4KRt6Kj1-whdWsGFc7lBhPOpk1ssbYFGN1NNyuyFRmH-3nCY3lBC4AmbCUkbDTUeCi9DidCtJeyc73CZJEu7M62rIzxR2yV"
}
],
"format": "tar.gz",
"path": "React Native DevTools-linux-x64/React Native DevTools"
},
"macos-aarch64": {
"size": 110891041,
"hash": "sha256",
"digest": "3cbe8b1b3d17e433347f1601435bb9a6cb758528a5c176a66fc52d9977223175",
"providers": [
{
"type": "http",
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQOmC2cqqSv4OrSJKJroYVg_NE8OE4O73AXqY7wXiYqiWQVkDt0Xnyw3ZeUpQT_Qb0-OoT5F8REKoFrB6eqwat8Ovkyina30peYTTwNUzmwnnGQEg7J0fOHNxLF4dkmU1FagXtsoWgex4dKgsK_VpcMsHj3Vp7diomkYvWBVTf_gPVEseYSN9oKq92qa"
}
],
"format": "tar.gz",
"path": "React Native DevTools.app/Contents/MacOS/React Native DevTools"
},
"macos-x86_64": {
"size": 117766158,
"hash": "sha256",
"digest": "6fb79bc2ba3008401b4c9c128248657b95b98581ccde60f8fadb622163779775",
"providers": [
{
"type": "http",
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQMMtGn-YGdfLfTVWC8zbQkQx65Asq6iArKt1t__cjZ8UY_s6-sX5XBHr8k1SaexAO21dFZENQVZ1jW_wn_gJ9ENvosQDG1KfWMViKsHli0xRzZ1HVsgPIj_KVXe907QZwwtJf2XhgH0HT8dfH-AQdDcd0_TB5DFUwOsHzhH0nBrHet7YFkbJtPTaA"
}
],
"format": "tar.gz",
"path": "React Native DevTools.app/Contents/MacOS/React Native DevTools"
},
"windows-x86_64": {
"size": 125527537,
"hash": "sha256",
"digest": "579a5b0944c51c3b1b541ad5af66c1ffedf93cae2a891ecdf88cb7219fd9b096",
"providers": [
{
"type": "http",
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQOQ3E8lBXqdVHbDPyVb5AOQMrDWjFrFV8fnLLBsygQvLWdpu6ixyG9PgWdwpi5jM-XcDdCHkhBdhaq-5dwT_tgRWKCAMsEBoAIUk0Xg77mGyHG2VF7bNfQ2qFBMuObrsTmrKy1nJ-UFDDm29pJD4GkFQW5NesiBwndJj8t3B8Ur8cczh_XR8rF5"
}
],
"format": "tar.gz",
"path": "React Native DevTools-win32-x64/React Native DevTools.exe"
}
}
}
+5 -11
View File
@@ -1,6 +1,6 @@
{
"name": "@react-native/debugger-shell",
"version": "0.82.0-main",
"version": "0.81.0-main",
"description": "Experimental debugger shell for React Native for use with @react-native/debugger-frontend",
"keywords": [
"react-native",
@@ -26,20 +26,14 @@
},
"license": "MIT",
"engines": {
"node": ">= 20.19.4"
"node": ">= 20.19.4",
"electron": ">=36.3.0"
},
"dependencies": {
"cross-spawn": "^7.0.6",
"fb-dotslash": "0.5.8"
"electron": "36.3.0"
},
"devDependencies": {
"electron": "37.2.6",
"semver": "^7.1.3"
},
"files": [
"!**/__tests__/**",
"bin",
"dist",
"!src/electron"
]
}
}
@@ -36,7 +36,7 @@ function handleLaunchArgs(argv: string[]) {
});
// Find an existing window for this app and launch configuration.
let frontendWindow = BrowserWindow.getAllWindows().find(window => {
const existingWindow = BrowserWindow.getAllWindows().find(window => {
const metadata = windowMetadata.get(window);
if (!metadata) {
return false;
@@ -44,39 +44,41 @@ function handleLaunchArgs(argv: string[]) {
return metadata.windowKey === windowKey;
});
if (frontendWindow) {
if (existingWindow) {
// If the window is already visible, flash it.
if (frontendWindow.isVisible()) {
frontendWindow.flashFrame(true);
if (existingWindow.isVisible()) {
existingWindow.flashFrame(true);
setTimeout(() => {
frontendWindow.flashFrame(false);
existingWindow.flashFrame(false);
}, 1000);
}
} else {
// Create the browser window.
frontendWindow = new BrowserWindow({
width: 1200,
height: 600,
webPreferences: {
partition: 'persist:react-native-devtools',
preload: require.resolve('./preload.js'),
},
// Icon for Linux
icon: path.join(__dirname, 'resources', 'icon.png'),
});
// Auto-hide the Windows/Linux menu bar
frontendWindow.setMenuBarVisibility(false);
if (process.platform === 'darwin') {
app.focus({
steal: true,
});
}
existingWindow.focus();
return;
}
// Create the browser window.
const frontendWindow = new BrowserWindow({
width: 1200,
height: 600,
webPreferences: {
partition: 'persist:react-native-devtools',
preload: require.resolve('./preload.js'),
},
// Icon for Linux
icon: path.join(__dirname, 'resources', 'icon.png'),
});
// Open links in the default browser instead of in new Electron windows.
frontendWindow.webContents.setWindowOpenHandler(({url}) => {
shell.openExternal(url);
return {action: 'deny'};
});
// TODO: If the window contains a live, working frontend instance with a valid connection to the backend,
// we should avoid this reload and instead send the frontend a message to handle the launch arguments
// dynamically (e.g. update the launch ID for telemetry purposes, handle deeplinking to a specific CDT panel, etc).
frontendWindow.loadURL(frontendUrl);
windowMetadata.set(frontendWindow, {
@@ -88,7 +90,6 @@ function handleLaunchArgs(argv: string[]) {
steal: true,
});
}
frontendWindow.focus();
}
app.whenReady().then(() => {
@@ -8,18 +8,9 @@
* @format
*/
import buildInfo from './BuildInfo';
// $FlowFixMe[untyped-import] Flow doesn't infer JSON types
const pkg = require('../../package.json');
const util = require('util');
// $FlowFixMe[unclear-type] We have no Flow types for the Electron API.
const {app} = require('electron') as any;
// Set the app name and version early - these are used in --version as well as
// in the User-Agent string.
app.setName(pkg.name);
app.setVersion(pkg.version + '-' + buildInfo.revision);
const util = require('util');
// Handle global command line arguments which don't require a window
// or the single instance lock to be held.
@@ -16,7 +16,7 @@ contextBridge.executeInMainWorld({
let didDecorateInspectorFrontendHostInstance = false;
// reactNativeDecorateInspectorFrontendHostInstance was introduced in
// https://github.com/facebook/react-native-devtools-frontend/pull/168
// $FlowFixMe[prop-missing]
// $FlowIgnore[prop-missing]
globalThis.reactNativeDecorateInspectorFrontendHostInstance = (
InspectorFrontendHostInstance: $FlowFixMe,
) => {
+2 -2
View File
@@ -18,9 +18,9 @@ declare module.exports: typeof Node;
// Because Electron doesn't support package.json `exports`, we need to
// switch at runtime.
if ('electron' in process.versions) {
// $FlowFixMe[invalid-export]
// $FlowIgnore[invalid-export]
module.exports = require('./electron');
} else {
// $FlowFixMe[invalid-export]
// $FlowIgnore[invalid-export]
module.exports = require('./node');
}
@@ -1,48 +0,0 @@
/**
* 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.
*
* @flow strict-local
* @format
* @oncall react_native
*/
const {unstable_spawnDebuggerShellWithArgs} = require('../../');
describe('debugger-shell Node package', () => {
test('can spawn in detached+prebuilt mode without crashing', async () => {
await expect(
unstable_spawnDebuggerShellWithArgs(['--version'], {
flavor: 'prebuilt',
mode: 'detached',
}),
).resolves.toBeUndefined();
});
// When running in the internal react-native-oss-js job, Electron isn't
// installed correctly (postinstall scripts don't run) but the internal
// `electron` workspace isn't available either. Detecting this dynamically
// weakens the test somewhat in environments where it *should* pass, but this
// is a dev-only feature anyway so this is fine.
if (isElectronInstalled()) {
test('can spawn in detached+dev mode without crashing', async () => {
await expect(
unstable_spawnDebuggerShellWithArgs(['--version'], {
flavor: 'dev',
mode: 'detached',
}),
).resolves.toBeUndefined();
});
}
});
function isElectronInstalled() {
try {
require('electron');
return true;
} catch {
return false;
}
}
+17 -114
View File
@@ -8,54 +8,44 @@
* @format
*/
import {
prepareDebuggerShellFromDotSlashFile,
spawnAndGetStderr,
} from './private/LaunchUtils';
const {spawn} = require('cross-spawn');
const path = require('path');
// The 'prebuilt' flavor will use the prebuilt shell binary (and the JavaScript embedded in it).
// The 'dev' flavor will use a stock Electron binary and run the shell code from the `electron/` directory.
type DebuggerShellFlavor = 'prebuilt' | 'dev';
const DEVTOOLS_BINARY_DOTSLASH_FILE = path.join(
__dirname,
'../../bin/react-native-devtools',
);
async function unstable_spawnDebuggerShellWithArgs(
args: string[],
{
mode = 'detached',
flavor = 'prebuilt',
}: $ReadOnly<{
// In 'syncAndExit' mode, the current process will block until the spawned process exits, and then it will exit
// with the same exit code as the spawned process.
// In 'detached' mode, the spawned process will be detached from the current process and the current process will
// continue to run normally.
mode?: 'syncThenExit' | 'detached',
flavor?: DebuggerShellFlavor,
}> = {},
): Promise<void> {
const [binaryPath, baseArgs] = getShellBinaryAndArgs(flavor);
// NOTE: Internally at Meta, this is aliased to a workspace that is
// API-compatible with the 'electron' package, but contains prebuilt binaries
// that do not need to be downloaded in a postinstall action.
const electronPath = require('electron');
return new Promise((resolve, reject) => {
const child = spawn(binaryPath, [...baseArgs, ...args], {
stdio: 'inherit',
windowsHide: true,
detached: mode === 'detached',
});
const child = spawn(
electronPath,
[require.resolve('../electron'), ...args],
{
stdio: 'inherit',
windowsHide: true,
detached: mode === 'detached',
},
);
if (mode === 'detached') {
child.on('spawn', () => {
resolve();
});
child.on('close', (code: number) => {
child.on('close', (code /*: number */) => {
if (code !== 0) {
reject(
new Error(
`Failed to open debugger shell: exited with code ${code}`,
`Failed to open debugger shell: ${electronPath} exited with code ${code}`,
),
);
}
@@ -64,7 +54,7 @@ async function unstable_spawnDebuggerShellWithArgs(
} else if (mode === 'syncThenExit') {
child.on('close', function (code, signal) {
if (code === null) {
console.error('Debugger shell exited with signal', signal);
console.error(electronPath, 'exited with signal', signal);
process.exit(1);
}
process.exit(code);
@@ -84,91 +74,4 @@ async function unstable_spawnDebuggerShellWithArgs(
});
}
export type DebuggerShellPreparationResult = $ReadOnly<{
code:
| 'success'
| 'not_implemented'
| 'likely_offline'
| 'platform_not_supported'
| 'possible_corruption'
| 'unexpected_error',
humanReadableMessage?: string,
verboseInfo?: string,
}>;
/**
* Attempts to prepare the debugger shell for use and returns a coded result
* that can be used to advise the user on how to proceed in case of failure.
* In particular, this function will attempt to download and extract an
* appropriate binary for the "prebuilt" flavor.
*
* This function should be called early during dev server startup, in parallel
* with other initialization steps, so that the debugger shell is ready to use
* instantly when the user tries to open it (and conversely, the user is
* informed ASAP if it is not ready to use).
*/
async function unstable_prepareDebuggerShell(
flavor: DebuggerShellFlavor,
): Promise<DebuggerShellPreparationResult> {
const [binaryPath, baseArgs] = getShellBinaryAndArgs(flavor);
try {
switch (flavor) {
case 'prebuilt':
const prebuiltResult = await prepareDebuggerShellFromDotSlashFile(
DEVTOOLS_BINARY_DOTSLASH_FILE,
);
if (prebuiltResult.code !== 'success') {
return prebuiltResult;
}
break;
case 'dev':
break;
default:
flavor as empty;
throw new Error(`Unknown flavor: ${flavor}`);
}
const {code, stderr} = await spawnAndGetStderr(binaryPath, [
...baseArgs,
'--version',
]);
if (code !== 0) {
return {
code: 'unexpected_error',
verboseInfo: stderr,
};
}
return {code: 'success'};
} catch (e) {
return {
code: 'unexpected_error',
verboseInfo: e.message,
};
}
}
function getShellBinaryAndArgs(
flavor: DebuggerShellFlavor,
): [string, Array<string>] {
switch (flavor) {
case 'prebuilt':
return [
// $FlowFixMe[cannot-resolve-module] fb-dotslash includes Flow types but Flow does not pick them up
require('fb-dotslash'),
[DEVTOOLS_BINARY_DOTSLASH_FILE],
];
case 'dev':
return [
// NOTE: Internally at Meta, this is aliased to a workspace that is
// API-compatible with the 'electron' package, but contains prebuilt binaries
// that do not need to be downloaded in a postinstall action.
require('electron'),
[require.resolve('../electron')],
];
default:
flavor as empty;
throw new Error(`Unknown flavor: ${flavor}`);
}
}
export {unstable_spawnDebuggerShellWithArgs, unstable_prepareDebuggerShell};
export {unstable_spawnDebuggerShellWithArgs};
@@ -1,98 +0,0 @@
/**
* 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.
*
* @flow strict-local
* @format
*/
import type {DebuggerShellPreparationResult} from '../';
const {spawn} = require('cross-spawn');
async function spawnAndGetStderr(
command: string,
args: string[],
): Promise<{
code: number,
stderr: string,
}> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: ['ignore', 'ignore', 'pipe'],
encoding: 'utf8',
windowsHide: true,
});
let stderr = '';
child.stderr.on('data', data => {
stderr += data;
});
child.on('error', error => {
reject(error);
});
child.on('close', (code, signal) => {
resolve({
code,
stderr,
});
});
});
}
async function prepareDebuggerShellFromDotSlashFile(
filePath: string,
): Promise<DebuggerShellPreparationResult> {
const {code, stderr} = await spawnAndGetStderr(
// $FlowFixMe[cannot-resolve-module] fb-dotslash includes Flow types but Flow does not pick them up
require('fb-dotslash'),
['--', 'fetch', filePath],
);
if (code === 0) {
return {code: 'success'};
}
if (
stderr.includes('dotslash error') &&
stderr.includes('no providers succeeded')
) {
if (stderr.includes('failed to verify artifact')) {
return {
code: 'possible_corruption',
humanReadableMessage:
'Failed to verify the latest version of React Native DevTools. ' +
'Using a fallback version instead. ',
verboseInfo: stderr,
};
}
return {
code: 'likely_offline',
humanReadableMessage:
'Failed to download the latest version of React Native DevTools. ' +
'Using a fallback version instead. ' +
'Connect to the internet or check your network settings.',
verboseInfo: stderr,
};
}
if (
stderr.includes('dotslash error') &&
stderr.includes('platform not supported')
) {
return {
code: 'platform_not_supported',
humanReadableMessage:
'The latest version of React Native DevTools is not supported on this platform. ' +
'Using a fallback version instead.',
verboseInfo: stderr,
};
}
return {
code: 'unexpected_error',
humanReadableMessage:
'An unexpected error occured while installing the latest version of React Native DevTools. ' +
'Using a fallback version instead.',
verboseInfo: stderr,
};
}
export {spawnAndGetStderr, prepareDebuggerShellFromDotSlashFile};
-10
View File
@@ -88,16 +88,6 @@ WebSocket handler for registering device connections.
WebSocket handler that proxies CDP messages to/from the corresponding device.
## Experimental features
React Native frameworks may pass an `unstable_experiments` option to `createDevMiddleware` to configure experimental features. Note that these features might not work correctly, and they may change or be removed in the future without notice. Some of the experiment flags available are documented below.
### `unstable_experiments.enableStandaloneFuseboxShell`
When `true`, the debugger frontend will launch in a standalone app shell (provided by the `@react-native/debugger-shell` package) rather than in a browser window. The standalone shell provides an improved experience and will become the default in a future version of React Native.
The shell is powered by a separate binary that is downloaded and cached in the background (immediately after the call to `createDevMiddleware`). If there is a problem downloading or invoking this binary for the first time, the debugger frontend will revert to launching in a browser window until the next time `createDevMiddleware` is called (typically, on the next dev server start).
## Contributing
Changes to this package can be made locally and tested against the `rn-tester` app, per the [Contributing guide](https://reactnative.dev/contributing/overview#contributing-code). During development, this package is automatically run from source with no build step.
+3 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@react-native/dev-middleware",
"version": "0.82.0-main",
"version": "0.81.0-main",
"description": "Dev server middleware for React Native",
"keywords": [
"react-native",
@@ -23,8 +23,7 @@
],
"dependencies": {
"@isaacs/ttlcache": "^1.4.1",
"@react-native/debugger-frontend": "0.82.0-main",
"@react-native/debugger-shell": "0.82.0-main",
"@react-native/debugger-frontend": "0.81.0-main",
"chrome-launcher": "^0.15.2",
"chromium-edge-launcher": "^0.2.0",
"connect": "^3.6.5",
@@ -39,9 +38,8 @@
"node": ">= 20.19.4"
},
"devDependencies": {
"@react-native/debugger-shell": "0.82.0-main",
"selfsigned": "^2.4.1",
"undici": "^5.29.0",
"undici": "^5.28.5",
"wait-for-expect": "^3.0.2"
}
}
@@ -70,7 +70,7 @@ export class DebuggerAgent {
this.#ws = null;
}
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get socket(): WebSocket {
return nullthrows(this.#ws);
}
@@ -103,12 +103,11 @@ export class DebuggerMock extends DebuggerAgent {
originalHandleCallsArray === this.handle.mock.calls
? this.handle.mock.calls.slice(originalHandleCallCount)
: this.handle.mock.calls;
// $FlowFixMe[incompatible-use]
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-type]
// $FlowIgnore[incompatible-use]
// $FlowIgnore[prop-missing]
const [response] = newHandleCalls.find(args => args[0].id === message.id);
// $FlowFixMe[incompatible-type]
// $FlowFixMe[incompatible-indexer]
// $FlowIgnore[incompatible-return]
// $FlowIgnore[incompatible-indexer]
return response;
}
}
@@ -90,7 +90,7 @@ export class DeviceAgent {
});
}
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get socket(): WebSocket {
return nullthrows(this.#ws);
}
@@ -152,12 +152,12 @@ export class DeviceMock extends DeviceAgent {
if (!payload) {
return;
}
// $FlowFixMe[incompatible-type] TODO(moti) Figure out the right way to type maybePayload generically
// $FlowFixMe[incompatible-call] TODO(moti) Figure out the right way to type maybePayload generically
this.send({event, payload});
});
return;
}
// $FlowFixMe[incompatible-type] TODO(moti) Figure out the right way to type maybePayload generically
// $FlowFixMe[incompatible-call] TODO(moti) Figure out the right way to type maybePayload generically
this.send({event, payload: maybePayload});
}
}
@@ -64,9 +64,9 @@ export async function sendFromTargetToDebugger<Message: CdpMessageFromTarget>(
originalHandleCallsArray === debugger_.handle.mock.calls
? debugger_.handle.mock.calls.slice(originalHandleCallCount)
: debugger_.handle.mock.calls;
// $FlowFixMe[incompatible-type]
// $FlowIgnore[incompatible-type]
const [receivedMessage]: [Message] = newHandleCalls.find(
// $FlowFixMe[incompatible-type]
// $FlowIgnore[incompatible-call]
(call: [Message]) => call[0].method === message.method,
);
return receivedMessage;
@@ -97,13 +97,13 @@ export async function sendFromDebuggerToTarget<Message: CdpMessageToTarget>(
originalEventCallsArray === device.wrappedEventParsed.mock.calls
? device.wrappedEventParsed.mock.calls.slice(originalEventCallCount)
: device.wrappedEventParsed.mock.calls;
// $FlowFixMe[incompatible-use]
// $FlowIgnore[incompatible-use]
const [receivedMessage] = newEventCalls.find(
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-use]
// $FlowIgnore[prop-missing]
// $FlowIgnore[incompatible-use]
call => call[0].wrappedEvent.id === message.id,
);
// $FlowFixMe[incompatible-type]
// $FlowIgnore[incompatible-return]
return receivedMessage.wrappedEvent;
}
@@ -141,7 +141,7 @@ export async function createAndConnectTarget(
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toHaveLength(1);
});
@@ -184,7 +184,7 @@ describe.each(['HTTP', 'HTTPS'])(
describe.each(['10.0.2.2:8080', '[::1]', 'example.com:2000'])(
'%s aliasing to and from localhost',
sourceHost => {
test('Debugger.scriptParsed - in source map fetching', async () => {
test('in source map fetching during Debugger.scriptParsed', async () => {
serverRef.app.use('/source-map', serveStaticJson({version: 3}));
const {device, debugger_} = await createAndConnectTarget(
serverRef,
@@ -220,53 +220,6 @@ describe.each(['HTTP', 'HTTPS'])(
}
});
test('Runtime.consoleAPICalled - in location resolutions', async () => {
const {device, debugger_} = await createAndConnectTarget(
serverRef,
autoCleanup.signal,
{
app: 'bar-app',
id: 'page1',
title: 'bar-title',
vm: 'bar-vm',
},
{
deviceHostHeader: sourceHost,
},
);
try {
const consoleMessage = await sendFromTargetToDebugger(
device,
debugger_,
'page1',
{
method: 'Runtime.consoleAPICalled',
params: {
stackTrace: {
callFrames: [
{
url: `${protocol.toLowerCase()}://${sourceHost}/bundleFile:1:2`,
field2: 'aaa',
},
{
url: `${protocol.toLowerCase()}://${sourceHost}/bundleFile:5:4`,
field3: 'bbb',
},
],
},
},
},
);
expect(consoleMessage.params.stackTrace.callFrames).toEqual([
{url: `${serverRef.serverBaseUrl}/bundleFile:1:2`, field2: 'aaa'},
{url: `${serverRef.serverBaseUrl}/bundleFile:5:4`, field3: 'bbb'},
]);
} finally {
device.close();
debugger_.close();
}
});
test('in Debugger.setBreakpointByUrl', async () => {
const {device, debugger_} = await createAndConnectTarget(
serverRef,
@@ -60,7 +60,7 @@ describe.each(['HTTP', 'HTTPS'])(
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toHaveLength(1);
});
@@ -119,7 +119,7 @@ describe.each(['HTTP', 'HTTPS'])(
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toHaveLength(1);
});
@@ -187,7 +187,7 @@ describe.each(['HTTP', 'HTTPS'])(
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toHaveLength(1);
});
@@ -288,7 +288,7 @@ describe.each(['HTTP', 'HTTPS'])(
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toHaveLength(1);
});
@@ -338,7 +338,7 @@ describe.each(['HTTP', 'HTTPS'])(
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toHaveLength(1);
});
@@ -108,7 +108,7 @@ describe('inspector proxy device message middleware', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList.length).toBeGreaterThan(0);
});
@@ -314,7 +314,7 @@ describe('inspector-proxy device socket handoff', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toEqual(
expect.arrayContaining(
@@ -55,7 +55,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList.length).toBeGreaterThan(0);
});
@@ -113,7 +113,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toContainEqual(
expect.objectContaining({
@@ -169,7 +169,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList.length).toBeGreaterThan(0);
});
@@ -222,7 +222,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toContainEqual(
expect.objectContaining({
@@ -273,7 +273,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList.length).toBeGreaterThan(0);
});
@@ -423,7 +423,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList.length).toBeGreaterThan(0);
});
@@ -459,7 +459,7 @@ describe('inspector proxy React Native reloads', () => {
await until(async () => {
pageList = (await fetchJson(
`${serverRef.serverBaseUrl}/json`,
// $FlowFixMe[unclear-type]
// $FlowIgnore[unclear-type]
): any);
expect(pageList).toContainEqual(
expect.objectContaining({
@@ -10,7 +10,7 @@
export function withAbortSignalForEachTest(): $ReadOnly<{signal: AbortSignal}> {
const ref: {signal: AbortSignal} = {
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get signal() {
throw new Error(
'The return value of withAbortSignalForEachTest is lazily initialized and can only be accessed in tests.',
@@ -39,19 +39,19 @@ export function withServerForEachTest(options: CreateServerOptions): $ReadOnly<{
app: ConnectApp,
port: number,
} = {
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get serverBaseUrl() {
throw new Error(EAGER_ACCESS_ERROR_MESSAGE);
},
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get serverBaseWsUrl() {
throw new Error(EAGER_ACCESS_ERROR_MESSAGE);
},
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get app() {
throw new Error(EAGER_ACCESS_ERROR_MESSAGE);
},
// $FlowFixMe[unsafe-getters-setters]
// $FlowIgnore[unsafe-getters-setters]
get port() {
throw new Error(EAGER_ACCESS_ERROR_MESSAGE);
},
@@ -62,11 +62,11 @@ export function withServerForEachTest(options: CreateServerOptions): $ReadOnly<{
({server, app} = await createServer(options));
const serverBaseUrl = baseUrlForServer(
server,
(options.secure ?? false) ? 'https' : 'http',
options.secure ?? false ? 'https' : 'http',
);
const serverBaseWsUrl = baseUrlForServer(
server,
(options.secure ?? false) ? 'wss' : 'ws',
options.secure ?? false ? 'wss' : 'ws',
);
Object.defineProperty(ref, 'serverBaseUrl', {value: serverBaseUrl});
Object.defineProperty(ref, 'serverBaseWsUrl', {value: serverBaseWsUrl});
@@ -93,7 +93,7 @@ export async function createServer(options: CreateServerOptions): Promise<{
);
httpServer = https.createServer(
{cert, key},
// $FlowFixMe[incompatible-type] The types for `connect` and `https` are subtly incompatible as written.
// $FlowFixMe[incompatible-call] The types for `connect` and `https` are subtly incompatible as written.
app,
);
} else {
@@ -28,9 +28,6 @@ describe('enableStandaloneFuseboxShell experiment', () => {
unstable_showFuseboxShell: () => {
throw new Error('Not implemented');
},
unstable_prepareFuseboxShell: async () => {
return {code: 'not_implemented'};
},
};
const serverRef = withServerForEachTest({
logger: undefined,
@@ -130,7 +127,5 @@ describe('enableStandaloneFuseboxShell experiment', () => {
device.close();
}
});
// TODO(moti): Add tests around unstable_prepareFuseboxShell
});
});
@@ -40,7 +40,7 @@ function makeRequest(
host: ?string,
encrypted: boolean,
): http$IncomingMessage<> | http$IncomingMessage<tls$TLSSocket> {
// $FlowFixMe[incompatible-type] Partial mock of request
// $FlowIgnore[incompatible-return] Partial mock of request
return {
socket: encrypted ? {encrypted: true} : {},
headers: host != null ? {host} : {},
@@ -81,7 +81,7 @@ export default function createDevMiddleware({
projectRoot,
serverBaseUrl,
logger,
// $FlowFixMe[incompatible-type]
// $FlowFixMe[prop-missing]
unstable_browserLauncher = DefaultBrowserLauncher,
unstable_eventReporter,
unstable_experiments: experimentConfig = {},
@@ -92,7 +92,6 @@ export default function createDevMiddleware({
const eventReporter = createWrappedEventReporter(
unstable_eventReporter,
logger,
experiments,
);
const inspectorProxy = new InspectorProxy(
@@ -153,7 +152,6 @@ function getExperiments(config: ExperimentsConfig): Experiments {
function createWrappedEventReporter(
reporter: ?EventReporter,
logger: ?Logger,
experiments: Experiments,
): EventReporter {
return {
logEvent(event: ReportableEvent) {
@@ -168,42 +166,10 @@ function createWrappedEventReporter(
logger?.info(
'\u001B[1m\u001B[7m💡 JavaScript logs have moved!\u001B[22m They can now be ' +
'viewed in React Native DevTools. Tip: Type \u001B[1mj\u001B[22m in ' +
'the terminal to open' +
(experiments.enableStandaloneFuseboxShell
? ''
: ' (requires Google Chrome or Microsoft Edge)') +
'.\u001B[27m',
'the terminal to open (requires Google Chrome or Microsoft Edge).' +
'\u001B[27m',
);
break;
case 'fusebox_shell_preparation_attempt':
switch (event.result.code) {
case 'success':
case 'not_implemented':
break;
case 'unexpected_error': {
let message =
event.result.humanReadableMessage ??
'An unknown error occurred while installing React Native DevTools.';
if (event.result.verboseInfo != null) {
message += ` Details:\n\n${event.result.verboseInfo}`;
} else {
message += '.';
}
logger?.error(message);
break;
}
case 'possible_corruption':
case 'platform_not_supported':
case 'likely_offline':
logger?.warn(
event.result.humanReadableMessage ??
`An error of type ${event.result.code} occurred while installing React Native DevTools.`,
);
break;
default:
(event.result.code: empty);
break;
}
}
reporter?.logEvent(event);
+1 -6
View File
@@ -10,15 +10,10 @@
export {default as createDevMiddleware} from './createDevMiddleware';
export type {
BrowserLauncher,
DebuggerShellPreparationResult,
} from './types/BrowserLauncher';
export type {BrowserLauncher} from './types/BrowserLauncher';
export type {EventReporter, ReportableEvent} from './types/EventReporter';
export type {
CustomMessageHandler,
CustomMessageHandlerConnection,
CreateCustomMessageHandlerFn,
} from './inspector-proxy/CustomMessageHandler';
export {default as unstable_DefaultBrowserLauncher} from './utils/DefaultBrowserLauncher';
@@ -179,7 +179,7 @@ export default class Device {
this.#deviceEventReporter?.logProfilingTargetRegistered();
}
// $FlowFixMe[incompatible-type]
// $FlowFixMe[incompatible-call]
this.#deviceSocket.on('message', (message: string) => {
try {
const parsedMessage = JSON.parse(message);
@@ -346,7 +346,7 @@ export default class Device {
frontendUserAgent: userAgent,
});
const debuggerInfo: ?DebuggerConnection & DebuggerConnection = {
const debuggerInfo = {
socket,
prependedFilePrefix: false,
pageId,
@@ -408,7 +408,7 @@ export default class Device {
this.#sendConnectEventToDevice(this.#mapToDevicePageId(pageId));
// $FlowFixMe[incompatible-type]
// $FlowFixMe[incompatible-call]
socket.on('message', (message: string) => {
this.#cdpDebugLogging.log('DebuggerToProxy', message);
const debuggerRequest = JSON.parse(message);
@@ -809,8 +809,6 @@ export default class Device {
}
if (
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/4oq3zi07. */
payload.method === 'Runtime.executionContextCreated' &&
this.#isLegacyPageReloading
) {
@@ -846,23 +844,6 @@ export default class Device {
this.#isLegacyPageReloading = false;
}
if (payload.method === 'Runtime.consoleAPICalled') {
const callFrames = payload.params?.stackTrace?.callFrames ?? [];
for (const callFrame of callFrames) {
if (callFrame.url) {
const parsedUrl = this.#tryParseHTTPURL(callFrame.url);
if (parsedUrl) {
// Rewrite device-relative URLs pointing to the server so that they're
// reachable from the frontend.
callFrame.url = this.#deviceRelativeUrlToDebuggerRelativeUrl(
parsedUrl,
debuggerInfo,
).href;
}
}
}
}
}
/**
@@ -970,10 +951,7 @@ export default class Device {
socket: WS,
): void {
const sendSuccessResponse = (scriptSource: string) => {
const result: {
scriptSource: string,
bytecode?: string,
} = {scriptSource};
const result = {scriptSource};
const response: CDPResponse<'Debugger.getScriptSource'> = {
id: req.id,
result,
@@ -48,7 +48,6 @@ export type CDPClientMessage =
export type CDPServerMessage =
| CDPEvent<'Debugger.scriptParsed'>
| CDPEvent<'Runtime.consoleAPICalled'>
| CDPEvent<>
| CDPResponse<'Debugger.getScriptSource'>
| CDPResponse<>;
@@ -84,27 +84,10 @@ export interface Debugger {
*/
sourceMapURL: string,
};
ConsoleAPICalled: {
args: Array<{type: string, value: string}>,
executionContextId: number,
stackTrace: {
timestamp: number,
type: string,
callFrames: Array<{
columnNumber: number,
lineNumber: number,
functionName: string,
scriptId: string,
url: string,
}>,
},
};
}
export type Events = {
'Debugger.scriptParsed': Debugger['ScriptParsedEvent'],
'Runtime.consoleAPICalled': Debugger['ConsoleAPICalled'],
[method: string]: JSONSerializable,
};
@@ -10,10 +10,7 @@
import type {InspectorProxyQueries} from '../inspector-proxy/InspectorProxy';
import type {PageDescription} from '../inspector-proxy/types';
import type {
BrowserLauncher,
DebuggerShellPreparationResult,
} from '../types/BrowserLauncher';
import type {BrowserLauncher} from '../types/BrowserLauncher';
import type {EventReporter} from '../types/EventReporter';
import type {Experiments} from '../types/Experiments';
import type {Logger} from '../types/Logger';
@@ -51,19 +48,6 @@ export default function openDebuggerMiddleware({
experiments,
inspectorProxy,
}: Options): NextHandleFunction {
let shellPreparationPromise: Promise<DebuggerShellPreparationResult>;
if (experiments.enableStandaloneFuseboxShell) {
shellPreparationPromise =
browserLauncher?.unstable_prepareFuseboxShell?.() ??
Promise.resolve({code: 'not_implemented'});
shellPreparationPromise = shellPreparationPromise.then(result => {
eventReporter?.logEvent({
type: 'fusebox_shell_preparation_attempt',
result,
});
return result;
});
}
return async (
req: IncomingMessage,
res: ServerResponse,
@@ -73,8 +57,7 @@ export default function openDebuggerMiddleware({
req.method === 'POST' ||
(experiments.enableOpenDebuggerRedirect && req.method === 'GET')
) {
const parsedUrl = url.parse(req.url, true);
const paresedUrl = url.parse(req.url, true);
const query: {
/** @deprecated Will only match legacy Hermes targets */
appId?: string,
@@ -83,9 +66,8 @@ export default function openDebuggerMiddleware({
launchId?: string,
telemetryInfo?: string,
target?: string,
panel?: string,
...
} = parsedUrl.query;
} = paresedUrl.query;
const targets = inspectorProxy
.getPageDescriptions({requestorRelativeBaseUrl: new URL(serverBaseUrl)})
@@ -168,28 +150,12 @@ export default function openDebuggerMiddleware({
telemetryInfo: query.telemetryInfo,
appId: target.appId,
useFuseboxEntryPoint,
panel: query.panel,
},
);
let shouldUseStandaloneFuseboxShell =
useFuseboxEntryPoint && experiments.enableStandaloneFuseboxShell;
if (shouldUseStandaloneFuseboxShell) {
const shellPreparationResult = await shellPreparationPromise;
switch (shellPreparationResult.code) {
case 'success':
case 'not_implemented':
break;
case 'platform_not_supported':
case 'possible_corruption':
case 'likely_offline':
case 'unexpected_error':
shouldUseStandaloneFuseboxShell = false;
break;
default:
(shellPreparationResult.code: empty);
}
}
if (shouldUseStandaloneFuseboxShell) {
if (
useFuseboxEntryPoint &&
experiments.enableStandaloneFuseboxShell
) {
const windowKey = [
serverBaseUrl,
target.webSocketDebuggerUrl,
@@ -8,10 +8,6 @@
* @format
*/
import type {DebuggerShellPreparationResult} from '@react-native/debugger-shell';
export type {DebuggerShellPreparationResult};
/**
* An interface for integrators to provide a custom implementation for
* opening URLs in a web browser.
@@ -46,21 +42,5 @@ export interface BrowserLauncher {
* the host of dev-middleware. Implementations are responsible for rewriting
* this as necessary where the server is remote.
*/
+unstable_showFuseboxShell?: (
url: string,
windowKey: string,
) => Promise<void>;
/**
* Attempt to prepare the debugger shell for use and returns a coded result
* that can be used to advise the user on how to proceed in case of failure.
*
* This function MAY be called multiple times or not at all. Implementers
* SHOULD use the opportunity to prefetch and cache any expensive resources (e.g
* platform-specific binaries needed in order to show the Fusebox shell). After a
* successful call, subsequent calls SHOULD complete quickly. The implementation
* SHOULD NOT return a rejecting promise in any case, and instead SHOULD report
* errors via the returned result object.
*/
+unstable_prepareFuseboxShell?: () => Promise<DebuggerShellPreparationResult>;
unstable_showFuseboxShell?: (url: string, windowKey: string) => Promise<void>;
}
@@ -8,8 +8,6 @@
* @format
*/
import type {DebuggerShellPreparationResult} from './BrowserLauncher';
type SuccessResult<Props: {...} | void = {}> = {
status: 'success',
...Props,
@@ -134,10 +132,6 @@ export type ReportableEvent =
duration: number,
...ConnectionUptime,
...DebuggerSessionIDs,
}
| {
type: 'fusebox_shell_preparation_attempt',
result: DebuggerShellPreparationResult,
};
/**

Some files were not shown because too many files have changed in this diff Show More