Compare commits

..
Author SHA1 Message Date
Nicola CortiandFacebook GitHub Bot 5983edd21b Migrate ClipboardModuleTest to use BridgelessReactContext (#53131)
Summary:
This test was still using the old `BridgeReactContext`, I'm migrating it to `BridgelessReactContext`.

## Changelog:

[INTERNAL] -


Test Plan: CI

Reviewed By: mdvacca

Differential Revision: D79801564

Pulled By: cortinico
2025-08-07 09:35:26 -07:00
1184 changed files with 12850 additions and 19870 deletions
+2 -2
View File
@@ -75,7 +75,7 @@ module.system.haste.module_ref_prefix=m#
react.runtime=automatic
experimental.error_code_migration=new
suppress_type=$FlowFixMe
ban_spread_key_props=true
@@ -100,4 +100,4 @@ untyped-import
untyped-type-import
[version]
^0.279.0
^0.278.0
@@ -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
@@ -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:
+2 -2
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:
@@ -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}}
@@ -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
RCT_USE_RN_DEP=1 RCT_USE_PREBUILT_RNCORE=1 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
@@ -0,0 +1,897 @@
/**
* 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 {
FirebaseClient,
compareResults,
getYesterdayDate,
getTodayDate,
} = require('../firebaseUtils');
describe('FirebaseClient', () => {
const originalFetch = global.fetch;
const originalEnv = process.env;
beforeEach(() => {
global.fetch = jest.fn();
process.env = {
...originalEnv,
FIREBASE_APP_EMAIL: 'test@example.com',
FIREBASE_APP_PASS: 'testpassword',
FIREBASE_APP_APIKEY: 'test-api-key',
FIREBASE_APP_PROJECTNAME: 'test-project',
};
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
global.fetch = originalFetch;
process.env = originalEnv;
jest.restoreAllMocks();
});
describe('constructor', () => {
it('should initialize with environment variables', () => {
const client = new FirebaseClient();
expect(client.email).toBe('test@example.com');
expect(client.password).toBe('testpassword');
expect(client.apiKey).toBe('test-api-key');
expect(client.projectId).toBe('test-project');
expect(client.databaseUrl).toBe(
'test-project-default-rtdb.firebaseio.com',
);
expect(client.idToken).toBeNull();
});
});
describe('authenticate', () => {
it('should authenticate successfully', async () => {
const mockResponse = {
idToken: 'mock-id-token',
refreshToken: 'mock-refresh-token',
};
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResponse)),
});
const client = new FirebaseClient();
await client.authenticate();
expect(client.idToken).toBe('mock-id-token');
expect(global.fetch).toHaveBeenCalledWith(
'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=test-api-key',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'test@example.com',
password: 'testpassword',
returnSecureToken: true,
}),
},
);
});
it('should throw error when email is missing', async () => {
delete process.env.FIREBASE_APP_EMAIL;
const client = new FirebaseClient();
await expect(client.authenticate()).rejects.toThrow(
'Firebase credentials not found in environment variables',
);
});
it('should throw error when password is missing', async () => {
delete process.env.FIREBASE_APP_PASS;
const client = new FirebaseClient();
await expect(client.authenticate()).rejects.toThrow(
'Firebase credentials not found in environment variables',
);
});
it('should handle authentication failure', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 400,
text: jest.fn().mockResolvedValueOnce(
JSON.stringify({
error: {message: 'Invalid credentials'},
}),
),
});
const client = new FirebaseClient();
await expect(client.authenticate()).rejects.toThrow(
'HTTP 400: Invalid credentials',
);
});
});
describe('makeRequest', () => {
it('should make successful GET request', async () => {
const mockData = {test: 'data'};
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockData)),
});
const client = new FirebaseClient();
const result = await client.makeRequest('example.com', '/test', 'GET');
expect(result).toEqual(mockData);
expect(global.fetch).toHaveBeenCalledWith('https://example.com/test', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
});
it('should make successful POST request with data', async () => {
const mockData = {success: true};
const postData = {test: 'post data'};
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockData)),
});
const client = new FirebaseClient();
const result = await client.makeRequest(
'example.com',
'/test',
'POST',
postData,
);
expect(result).toEqual(mockData);
expect(global.fetch).toHaveBeenCalledWith('https://example.com/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(postData),
});
});
it('should handle non-JSON response', async () => {
const textResponse = 'plain text response';
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(textResponse),
});
const client = new FirebaseClient();
const result = await client.makeRequest('example.com', '/test', 'GET');
expect(result).toBe(textResponse);
});
it('should handle HTTP error with JSON error message', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 404,
text: jest.fn().mockResolvedValueOnce(
JSON.stringify({
error: {message: 'Not found'},
}),
),
});
const client = new FirebaseClient();
await expect(
client.makeRequest('example.com', '/test', 'GET'),
).rejects.toThrow('HTTP 404: Not found');
});
it('should handle HTTP error with plain text error message', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 500,
text: jest.fn().mockResolvedValueOnce('Internal Server Error'),
});
const client = new FirebaseClient();
await expect(
client.makeRequest('example.com', '/test', 'GET'),
).rejects.toThrow('HTTP 500: Internal Server Error');
});
});
describe('makeDatabaseRequest', () => {
it('should make database request with existing token', async () => {
const mockData = {test: 'data'};
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockData)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.makeDatabaseRequest('2023-12-01', 'GET');
expect(result).toEqual(mockData);
expect(global.fetch).toHaveBeenCalledWith(
'https://test-project-default-rtdb.firebaseio.com/nightly-results/2023-12-01.json?auth=existing-token',
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
},
);
});
it('should authenticate before making request if no token exists', async () => {
const authResponse = {idToken: 'new-token'};
const dataResponse = {test: 'data'};
global.fetch
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(authResponse)),
})
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(dataResponse)),
});
const client = new FirebaseClient();
const result = await client.makeDatabaseRequest('2023-12-01', 'GET');
expect(result).toEqual(dataResponse);
expect(global.fetch).toHaveBeenCalledTimes(2);
expect(client.idToken).toBe('new-token');
});
it('should make PUT request with data', async () => {
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce('null'),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const testData = [{library: 'test', status: 'success'}];
await client.makeDatabaseRequest('2023-12-01', 'PUT', testData);
expect(global.fetch).toHaveBeenCalledWith(
'https://test-project-default-rtdb.firebaseio.com/nightly-results/2023-12-01.json?auth=existing-token',
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(testData),
},
);
});
});
describe('storeResults', () => {
it('should store results successfully', async () => {
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce('null'),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const results = [{library: 'test', status: 'success'}];
await client.storeResults('2023-12-01', results);
expect(global.fetch).toHaveBeenCalledWith(
'https://test-project-default-rtdb.firebaseio.com/nightly-results/2023-12-01.json?auth=existing-token',
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(results),
},
);
expect(console.log).toHaveBeenCalledWith(
'Successfully stored results for 2023-12-01',
);
});
});
describe('getResults', () => {
it('should retrieve results successfully', async () => {
const mockResults = [{library: 'test', status: 'success'}];
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const results = await client.getResults('2023-12-01');
expect(results).toEqual(mockResults);
});
it('should return null for 404 errors', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 404,
text: jest.fn().mockResolvedValueOnce('Not Found'),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const results = await client.getResults('2023-12-01');
expect(results).toBeNull();
});
it('should throw error for non-404 HTTP errors', async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 500,
text: jest.fn().mockResolvedValueOnce('Internal Server Error'),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
await expect(client.getResults('2023-12-01')).rejects.toThrow(
'HTTP 500: Internal Server Error',
);
});
});
describe('getLatestResults', () => {
it('should authenticate before making requests if no token exists', async () => {
const authResponse = {idToken: 'new-token'};
const mockResults = [{library: 'test', status: 'success'}];
global.fetch
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(authResponse)),
})
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
const result = await client.getLatestResults('2023-12-15', 1);
expect(result).toEqual({
results: mockResults,
date: '2023-12-14',
});
expect(client.idToken).toBe('new-token');
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-14 (1 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Found results from 2023-12-14 (1 days back)',
);
});
it('should find results from the previous day', async () => {
const mockResults = [{library: 'test', status: 'success'}];
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15', 7);
expect(result).toEqual({
results: mockResults,
date: '2023-12-14',
});
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-14 (1 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Found results from 2023-12-14 (1 days back)',
);
});
it('should find results from several days back', async () => {
const mockResults = [{library: 'test', status: 'success'}];
// Mock 404 responses for first 2 days, then success on 3rd day
global.fetch
.mockResolvedValueOnce({
ok: false,
status: 404,
text: jest.fn().mockResolvedValueOnce('Not Found'),
})
.mockResolvedValueOnce({
ok: false,
status: 404,
text: jest.fn().mockResolvedValueOnce('Not Found'),
})
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15', 7);
expect(result).toEqual({
results: mockResults,
date: '2023-12-12',
});
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-14 (1 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-13 (2 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-12 (3 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Found results from 2023-12-12 (3 days back)',
);
});
it('should skip empty results and continue searching', async () => {
const mockResults = [{library: 'test', status: 'success'}];
// Mock empty array for first day, then valid results on second day
global.fetch
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify([])),
})
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15', 7);
expect(result).toEqual({
results: mockResults,
date: '2023-12-13',
});
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-14 (1 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-13 (2 days back)...',
);
expect(console.log).toHaveBeenCalledWith(
'Found results from 2023-12-13 (2 days back)',
);
});
it('should return null when no results found within maxDaysBack', async () => {
// Mock 404 responses for all days
global.fetch.mockResolvedValue({
ok: false,
status: 404,
text: jest.fn().mockResolvedValue('Not Found'),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15', 3);
expect(result).toEqual({
results: null,
date: null,
});
expect(console.log).toHaveBeenCalledWith(
'No previous results found within the last 3 days',
);
expect(global.fetch).toHaveBeenCalledTimes(3);
});
it('should use default maxDaysBack of 7 when not specified', async () => {
// Mock 404 responses for all days
global.fetch.mockResolvedValue({
ok: false,
status: 404,
text: jest.fn().mockResolvedValue('Not Found'),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15');
expect(result).toEqual({
results: null,
date: null,
});
expect(console.log).toHaveBeenCalledWith(
'No previous results found within the last 7 days',
);
expect(global.fetch).toHaveBeenCalledTimes(7);
});
it('should handle non-404 errors and continue searching', async () => {
const mockResults = [{library: 'test', status: 'success'}];
// Mock 500 error for first day, then success on second day
global.fetch
.mockResolvedValueOnce({
ok: false,
status: 500,
text: jest.fn().mockResolvedValueOnce('Internal Server Error'),
})
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15', 7);
expect(result).toEqual({
results: mockResults,
date: '2023-12-13',
});
expect(console.log).toHaveBeenCalledWith(
'No results found for 2023-12-14: HTTP 500: Internal Server Error',
);
expect(console.log).toHaveBeenCalledWith(
'Found results from 2023-12-13 (2 days back)',
);
});
it('should handle date boundaries correctly', async () => {
const mockResults = [{library: 'test', status: 'success'}];
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
// Test month boundary
const result = await client.getLatestResults('2023-12-01', 1);
expect(result).toEqual({
results: mockResults,
date: '2023-11-30',
});
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-11-30 (1 days back)...',
);
});
it('should handle year boundary correctly', async () => {
const mockResults = [{library: 'test', status: 'success'}];
global.fetch.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
// Test year boundary
const result = await client.getLatestResults('2024-01-01', 1);
expect(result).toEqual({
results: mockResults,
date: '2023-12-31',
});
expect(console.log).toHaveBeenCalledWith(
'Checking for results on 2023-12-31 (1 days back)...',
);
});
it('should handle null results and continue searching', async () => {
const mockResults = [{library: 'test', status: 'success'}];
// Mock null for first day, then valid results on second day
global.fetch
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce('null'),
})
.mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)),
});
const client = new FirebaseClient();
client.idToken = 'existing-token';
const result = await client.getLatestResults('2023-12-15', 7);
expect(result).toEqual({
results: mockResults,
date: '2023-12-13',
});
});
});
});
describe('compareResults', () => {
it('should handle null previous results', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const result = compareResults(currentResults, null);
expect(result).toEqual({
broken: [],
recovered: [],
newFailures: [{library: 'lib1', platform: 'iOS', status: 'failed'}],
});
});
it('should handle undefined previous results', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
];
const result = compareResults(currentResults, undefined);
expect(result).toEqual({
broken: [],
recovered: [],
newFailures: [{library: 'lib1', platform: 'iOS', status: 'failed'}],
});
});
it('should identify broken tests', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const previousResults = [
{library: 'lib1', platform: 'iOS', status: 'success'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([
{
library: 'lib1',
platform: 'iOS',
previousStatus: 'success',
currentStatus: 'failed',
},
]);
expect(result.recovered).toEqual([]);
});
it('should identify recovered tests', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'success'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const previousResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([]);
expect(result.recovered).toEqual([
{
library: 'lib1',
platform: 'iOS',
previousStatus: 'failed',
currentStatus: 'success',
},
]);
});
it('should identify both broken and recovered tests', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
{library: 'lib2', platform: 'Android', status: 'success'},
{library: 'lib3', platform: 'iOS', status: 'success'},
];
const previousResults = [
{library: 'lib1', platform: 'iOS', status: 'success'},
{library: 'lib2', platform: 'Android', status: 'failed'},
{library: 'lib3', platform: 'iOS', status: 'success'},
];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([
{
library: 'lib1',
platform: 'iOS',
previousStatus: 'success',
currentStatus: 'failed',
},
]);
expect(result.recovered).toEqual([
{
library: 'lib2',
platform: 'Android',
previousStatus: 'failed',
currentStatus: 'success',
},
]);
});
it('should handle tests that are not in previous results', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const previousResults = [
{library: 'lib1', platform: 'iOS', status: 'success'},
];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([
{
library: 'lib1',
platform: 'iOS',
previousStatus: 'success',
currentStatus: 'failed',
},
]);
expect(result.recovered).toEqual([]);
});
it('should handle empty current results', () => {
const currentResults = [];
const previousResults = [
{library: 'lib1', platform: 'iOS', status: 'success'},
];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([]);
expect(result.recovered).toEqual([]);
});
it('should handle empty previous results', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'failed'},
];
const previousResults = [];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([]);
expect(result.recovered).toEqual([]);
// When previousResults is an empty array (not null/undefined),
// the function doesn't return newFailures property
expect(result.newFailures).toBeUndefined();
});
it('should handle different status values', () => {
const currentResults = [
{library: 'lib1', platform: 'iOS', status: 'timeout'},
{library: 'lib2', platform: 'Android', status: 'success'},
];
const previousResults = [
{library: 'lib1', platform: 'iOS', status: 'success'},
{library: 'lib2', platform: 'Android', status: 'error'},
];
const result = compareResults(currentResults, previousResults);
expect(result.broken).toEqual([
{
library: 'lib1',
platform: 'iOS',
previousStatus: 'success',
currentStatus: 'timeout',
},
]);
expect(result.recovered).toEqual([
{
library: 'lib2',
platform: 'Android',
previousStatus: 'error',
currentStatus: 'success',
},
]);
});
});
describe('getYesterdayDate', () => {
it("should return yesterday's date in YYYY-MM-DD format", () => {
const mockDate = new Date('2023-12-15T10:30:00Z');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const result = getYesterdayDate();
expect(result).toBe('2023-12-14');
global.Date.mockRestore();
});
it('should handle month boundary correctly', () => {
const mockDate = new Date('2023-12-01T10:30:00Z');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const result = getYesterdayDate();
expect(result).toBe('2023-11-30');
global.Date.mockRestore();
});
it('should handle year boundary correctly', () => {
const mockDate = new Date('2024-01-01T10:30:00Z');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const result = getYesterdayDate();
expect(result).toBe('2023-12-31');
global.Date.mockRestore();
});
});
describe('getTodayDate', () => {
it("should return today's date in YYYY-MM-DD format", () => {
const mockDate = new Date('2023-12-15T10:30:00Z');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const result = getTodayDate();
expect(result).toBe('2023-12-15');
global.Date.mockRestore();
});
it('should handle different times of day correctly', () => {
const mockDate = new Date('2023-12-15T23:59:59Z');
jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
const result = getTodayDate();
expect(result).toBe('2023-12-15');
global.Date.mockRestore();
});
});
@@ -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',
);
});
});
@@ -0,0 +1,174 @@
/**
* 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,
prepareComparisonPayload,
sendMessageToDiscord,
} = require('./notifyDiscord');
const {
FirebaseClient,
compareResults,
getYesterdayDate,
getTodayDate,
} = require('./firebaseUtils');
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);
// Send failure notification if there are current failures
if (failures.length > 0) {
if (discordWebHook) {
console.log('Sending current failures to Discord...');
await notifyDiscord(discordWebHook, failures);
} else {
console.log('Discord webhook not set');
}
}
// Initialize Firebase client
const firebaseClient = new FirebaseClient();
const today = getTodayDate();
try {
// Store today's results in Firebase
console.log(`Storing results for ${today} in Firebase...`);
await firebaseClient.storeResults(today, outcomes);
// Get the most recent previous results for comparison
console.log(`Looking for most recent previous results before ${today}...`);
const {results: previousResults, date: previousDate} =
await firebaseClient.getLatestResults(today);
let broken = [];
let recovered = [];
if (previousResults) {
console.log(`Comparing with results from ${previousDate}`);
// Compare results and identify broken/recovered jobs
const comparison = compareResults(outcomes, previousResults);
broken = comparison.broken;
recovered = comparison.recovered;
console.log(
`Found ${broken.length} newly broken jobs and ${recovered.length} recovered jobs compared to ${previousDate}`,
);
} else {
console.log(
'No previous results found for comparison - this might be the first run or no recent data available',
);
}
// Send comparison message to Discord if there are changes
if (discordWebHook && (broken.length > 0 || recovered.length > 0)) {
console.log('Sending comparison results to Discord...');
const comparisonMessage = prepareComparisonPayload(broken, recovered);
await sendMessageToDiscord(discordWebHook, comparisonMessage);
}
console.log('✅ All tests passed!');
} catch (error) {
console.error('Error in collectResults:', error);
// If Firebase fails but there are no test failures, don't fail the workflow
console.log('⚠️ Firebase operations failed, but all tests passed');
}
}
module.exports = {
collectResults,
notifyDiscord,
};
+261
View File
@@ -0,0 +1,261 @@
/**
* 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
*/
// We connect to firebase using a plain HTTP request because we don't want to
// add yet another devDependency to the react-native monorepo.
class FirebaseClient {
constructor() {
this.email = process.env.FIREBASE_APP_EMAIL;
this.password = process.env.FIREBASE_APP_PASS;
this.apiKey = process.env.FIREBASE_APP_APIKEY;
this.projectId = process.env.FIREBASE_APP_PROJECTNAME;
this.databaseUrl = `${this.projectId}-default-rtdb.firebaseio.com`;
this.idToken = null;
}
async authenticate() {
if (!this.email || !this.password) {
throw new Error(
'Firebase credentials not found in environment variables',
);
}
const authData = {
email: this.email,
password: this.password,
returnSecureToken: true,
};
const response = await this.makeRequest(
'identitytoolkit.googleapis.com',
`/v1/accounts:signInWithPassword?key=${this.apiKey}`,
'POST',
authData,
);
this.idToken = response.idToken;
return;
}
/**
* Make a database request for a specific date
* @param {string} date - Date in YYYY-MM-DD format
* @param {string} method - HTTP method
* @param {*} data - Data to send (optional)
* @returns {Promise<*>} - Response data
*/
async makeDatabaseRequest(date, method, data = null) {
if (!this.idToken) {
await this.authenticate();
}
const path = `/nightly-results/${date}.json?auth=${this.idToken}`;
return this.makeRequest(this.databaseUrl, path, method, data);
}
/**
* Store test results for a specific date
* @param {string} date - Date in YYYY-MM-DD format
* @param {Array<Object>} results - Array of test results
* @returns {Promise<void>}
*/
async storeResults(date, results) {
await this.makeDatabaseRequest(date, 'PUT', results);
console.log(`Successfully stored results for ${date}`);
}
/**
* Retrieve test results for a specific date
* @param {string} date - Date in YYYY-MM-DD format
* @returns {Promise<Array<Object>|null>} - Array of test results or null if not found
*/
async getResults(date) {
try {
return await this.makeDatabaseRequest(date, 'GET');
} catch (error) {
if (error.message.includes('404')) {
return null; // No results found for this specific date.
}
throw error;
}
}
/**
* Find the most recent available job results before the given date
* @param {string} currentDate - Current date in YYYY-MM-DD format
* @param {number} maxDaysBack - Maximum number of days to look back (default: 7)
* @returns {Promise<{results: Array<Object>|null, date: string|null}>} - Most recent results and their date
*/
async getLatestResults(currentDate, maxDaysBack = 7) {
if (!this.idToken) {
await this.authenticate();
}
const currentDateObj = new Date(currentDate);
for (let daysBack = 1; daysBack <= maxDaysBack; daysBack++) {
const checkDate = new Date(currentDateObj);
checkDate.setDate(checkDate.getDate() - daysBack);
const checkDateStr = checkDate.toISOString().split('T')[0];
console.log(
`Checking for results on ${checkDateStr} (${daysBack} days back)...`,
);
try {
const results = await this.getResults(checkDateStr);
if (results && results.length > 0) {
console.log(
`Found results from ${checkDateStr} (${daysBack} days back)`,
);
return {results, date: checkDateStr};
}
} catch (error) {
console.log(`No results found for ${checkDateStr}: ${error.message}`);
continue;
}
}
console.log(
`No previous results found within the last ${maxDaysBack} days`,
);
return {results: null, date: null};
}
async makeRequest(hostname, path, method, data = null) {
const url = `https://${hostname}${path}`;
const options = {
method,
headers: {
'Content-Type': 'application/json',
},
};
if (data) {
options.body = JSON.stringify(data);
}
const response = await fetch(url, options);
const responseText = await response.text();
if (!response.ok) {
let errorMessage;
try {
const parsedError = JSON.parse(responseText);
errorMessage = parsedError.error?.message || responseText;
} catch {
errorMessage = responseText;
}
throw new Error(`HTTP ${response.status}: ${errorMessage}`);
}
try {
return JSON.parse(responseText);
} catch {
return responseText;
}
}
}
/**
* Compare current results with previous day's results
* @param {Array<Object>} currentResults - Today's test results
* @param {Array<Object>} previousResults - Yesterday's test results
* @returns {Object} - Object containing broken and recovered tests
*/
function compareResults(currentResults, previousResults) {
if (!previousResults) {
return {
broken: [],
recovered: [],
newFailures: currentResults.filter(result => result.status !== 'success'),
};
}
// Create maps for easier lookup
const currentMap = new Map();
const previousMap = new Map();
currentResults.forEach(result => {
const key = `${result.library}-${result.platform}`;
currentMap.set(key, result);
});
previousResults.forEach(result => {
const key = `${result.library}-${result.platform}`;
previousMap.set(key, result);
});
const broken = [];
const recovered = [];
// Check for broken tests (was success, now failed)
for (const [key, currentResult] of currentMap) {
const previousResult = previousMap.get(key);
if (previousResult) {
if (
previousResult.status === 'success' &&
currentResult.status !== 'success'
) {
broken.push({
library: currentResult.library,
platform: currentResult.platform,
previousStatus: previousResult.status,
currentStatus: currentResult.status,
});
}
}
}
// Check for recovered tests (was failed, now success)
for (const [key, currentResult] of currentMap) {
const previousResult = previousMap.get(key);
if (previousResult) {
if (
previousResult.status !== 'success' &&
currentResult.status === 'success'
) {
recovered.push({
library: currentResult.library,
platform: currentResult.platform,
previousStatus: previousResult.status,
currentStatus: currentResult.status,
});
}
}
}
return {broken, recovered};
}
/**
* Get yesterday's date in YYYY-MM-DD format
* @returns {string} - Yesterday's date
*/
function getYesterdayDate() {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return yesterday.toISOString().split('T')[0];
}
/**
* Get today's date in YYYY-MM-DD format
* @returns {string} - Today's date
*/
function getTodayDate() {
const today = new Date();
return today.toISOString().split('T')[0];
}
module.exports = {
FirebaseClient,
compareResults,
getYesterdayDate,
getTodayDate,
};
+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, {
+136
View File
@@ -0,0 +1,136 @@
/**
* 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}`);
}
}
/**
* Sorts jobs by platform first, then by library name.
* @param {Array<Object>} jobs - Array of jobs with platform and library properties
* @returns {Array<Object>} - Sorted array of jobs
*/
function sortResultsByPlatformAndLibrary(jobs) {
return [...jobs].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);
});
}
/**
* 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 = sortResultsByPlatformAndLibrary(failures);
// 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}`,
};
}
/**
* Prepares a formatted Discord message payload for broken and recovered nightly jobs.
* @param {Array<Object>} broken - List of newly broken jobs
* @param {Array<Object>} recovered - List of recovered jobs
* @returns {Object} - The formatted Discord message payload
*/
function prepareComparisonPayload(broken, recovered) {
let content = '📊 **React Native Nightly Integration Status Update** 📊\n\n';
if (broken.length === 0 && recovered.length === 0) {
content +=
'No changes from yesterday - all nightly jobs maintained their previous status.';
} else {
if (broken.length > 0) {
content += '🔴 **Newly Broken Jobs:**\n';
const sortedBroken = sortResultsByPlatformAndLibrary(broken);
sortedBroken.forEach(job => {
content += `❌ [${job.platform}] ${job.library} (was ${job.previousStatus}, now ${job.currentStatus})\n`;
});
content += '\n';
}
if (recovered.length > 0) {
content += '🟢 **Recovered Jobs:**\n';
const sortedRecovered = sortResultsByPlatformAndLibrary(recovered);
sortedRecovered.forEach(job => {
content += `✅ [${job.platform}] ${job.library} (was ${job.previousStatus}, now ${job.currentStatus})\n`;
});
}
}
return {content};
}
// Export the functions using CommonJS syntax
module.exports = {
prepareFailurePayload,
prepareComparisonPayload,
sendMessageToDiscord,
};
+38
View File
@@ -0,0 +1,38 @@
# 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 }}
firebase_app_email: ${{ secrets.FIREBASE_APP_EMAIL }}
firebase_app_pass: ${{ secrets.FIREBASE_APP_PASS }}
firebase_app_projectname: ${{ secrets.FIREBASE_APP_PROJECTNAME }}
firebase_app_apikey: ${{ secrets.FIREBASE_APP_APIKEY }}
@@ -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:
-5
View File
@@ -246,11 +246,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")
@@ -0,0 +1,113 @@
name: Test Libraries on Nightlies
on:
workflow_call:
secrets:
discord_webhook_url:
required: true
firebase_app_email:
required: true
firebase_app_pass:
required: true
firebase_app_apikey:
required: true
firebase_app_projectname:
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
env:
FIREBASE_APP_EMAIL: ${{ secrets.firebase_app_email }}
FIREBASE_APP_PASS: ${{ secrets.firebase_app_pass }}
FIREBASE_APP_APIKEY: ${{ secrets.firebase_app_apikey }}
FIREBASE_APP_PROJECTNAME: ${{ secrets.firebase_app_projectname }}
with:
script: |
const {collectResults} = require('./.github/workflow-scripts/collectNightlyOutcomes.js');
await collectResults('${{secrets.discord_webhook_url}}');
-22
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
+73 -24
View File
@@ -1,6 +1,71 @@
# Changelog
## v0.81.0
## v0.81.0-rc.5
### Fixed
#### Android specific
- **Runtime:** Fixed `ReactHostImpl.nativeModules` always returning an empty list ([2f46a49](https://github.com/facebook/react-native/commit/2f46a49b8d8a11d5cf4342eee83c469b545c6779) by [@lukmccall](https://github.com/lukmccall))
## v0.81.0-rc.4 - Burned
## v0.81.0-rc.3
### Changed
- **Metro:** Metro to ^0.83.1 ([e247be793c](https://github.com/facebook/react-native/commit/e247be793c70a374955d798d8cbbc6eba58080ec) by [@motiz88](https://github.com/motiz88))
### Fixed
#### Android specific
- **rngp:** Fix a race condition with codegen libraries missing sources ([9013a9e666](https://github.com/facebook/react-native/commit/9013a9e66629677c47e1b69703f9fc8f4cbc1c2c) by [@cortinico](https://github.com/cortinico))
- **API:** Make accessors inside HeadlessJsTaskService open again ([7ef57163cb](https://github.com/facebook/react-native/commit/7ef57163cb016317e43e563da7ea181989f6abca) by [@cortinico](https://github.com/cortinico))
## v0.81.0-rc.2
### Changed
- **API:** `NewAppScreen` no longer internally handles device safe area, use optional `safeAreaInsets` prop (aligned in 0.81 template) ([732bd12dc2](https://github.com/facebook/react-native/commit/732bd12dc21460641ef01b23f2eb722f26b060d5) by [@huntie](https://github.com/huntie))
- **Babel:** Added support to `react-native/babel-preset` for a `hermesParserOptions` option, that expects an object that enables overriding `hermes-parser` options. ([0508eddfe6](https://github.com/facebook/react-native/commit/0508eddfe60df60cb3bfa4074ae199bd0e492d5f) by [@yungsters](https://github.com/yungsters))
### Fixed
#### iOS specific
- **Podspec:** Fixed issue with RNDeps release/debug switch failing ([4ee2b60a1e](https://github.com/facebook/react-native/commit/4ee2b60a1eacca744d58a7ad336ca9d3714289f6) by [@chrfalch](https://github.com/chrfalch))
- **Podspec:** Fixed missing script for resolving prebuilt xcframework when switching between release/debug ([2e55241a90](https://github.com/facebook/react-native/commit/2e55241a901b4cd95917de68ce9078928820a208) by [@chrfalch](https://github.com/chrfalch))
## v0.81.0-rc.1
### Added
#### iOS specific
- **CocoaPods** Add the `ENTERPRISE_REPOSITORY` env variable to cocoapods infra ([23f3bf9239](https://github.com/facebook/react-native/commit/23f3bf9239a849590f1c72b25732d0090780128c) by [@cipolleschi](https://github.com/cipolleschi))
- **Prebuild:** Add release/debug switch script for React-Core-prebuilt ([42d1a7934c](https://github.com/facebook/react-native/commit/42d1a7934cad4b2c92653e3fa7781c2af8f44df4) by [@chrfalch](https://github.com/chrfalch))
- **Prebuild:** Added support for using USE_FRAMEWORKS with prebuilt React Native Core ([40e45f5366](https://github.com/facebook/react-native/commit/40e45f53661ce80c3a6fbbf07f52dc900afcad52) by [@chrfalch](https://github.com/chrfalch))
### Changed
- **Metro:** Bump Metro to 0.83.0 ([6b9f5d622f](https://github.com/facebook/react-native/commit/6b9f5d622ffbe79da8f4e7b7d8094504a480425e) by [@robhogan](https://github.com/robhogan))
#### Android specific
- **Gradle:** Gradle to 8.14.3 ([6892dde363](https://github.com/facebook/react-native/commit/6892dde36373bbef2d0afe535ae818b1a7164f08) by [@cortinico](https://github.com/cortinico))
- **Gradle:** Expose `react_renderer_bridging` headers via prefab ([d1730ff960](https://github.com/facebook/react-native/commit/d1730ff960fcb9a01ee94b9e46e5a9fbb7d73f4a) by [@tomekzaw](https://github.com/tomekzaw))
- **Legacy Arch:** Introduce more deprecation warnings for Legacy Arch classes ([625f69f284](https://github.com/facebook/react-native/commit/625f69f284ddfd9c6beecaa4052a871d092053ef) by [@cortinico](https://github.com/cortinico))
### Fixed
- **Yoga:** Fixed nodes with `display: contents` set being cloned with the wrong owner ([d4b36b0300](https://github.com/facebook/react-native/commit/d4b36b03003eb2de9eaf5b57bb639bae8cc12f20) by [@j-piasecki](https://github.com/j-piasecki))
#### iOS specific
- **Podspec:** Fixed premature return in header file generation from podspec globs ([f2b064c2d4](https://github.com/facebook/react-native/commit/f2b064c2d40c39017ac2a31bf3caf8acef23038c) by [@chrfalch](https://github.com/chrfalch))
## v0.81.0-rc.0
### Breaking
@@ -61,7 +126,6 @@
#### iOS specific
- **borderWidth:** Add support for different `borderWidth`s ([70962ef3ed](https://github.com/facebook/react-native/commit/70962ef3ed06a76a96cb2e72c374dc028628c829) by [@a-klotz-p8](https://github.com/a-klotz-p8))
- **CocoaPods:** Add the `ENTERPRISE_REPOSITORY` env variable to cocoapods infra ([23f3bf9239](https://github.com/facebook/react-native/commit/23f3bf9239a849590f1c72b25732d0090780128c) by [@cipolleschi](https://github.com/cipolleschi))
- **Modal:** Allow to interactively swipe down `Modal`s. ([28986a7599](https://github.com/facebook/react-native/commit/28986a7599952a77b8b8e433f72ca837afde310e) by [@okwasniewski](https://github.com/okwasniewski))
- **Package.swift:** Added missing search path to `Package.swift` ([592b09781b](https://github.com/facebook/react-native/commit/592b09781bb94fe6dc00ba49c7a86649980fed5d) by [@chrfalch](https://github.com/chrfalch))
- **Prebuild:** Add more logging around `computeNightlyTarballURL` in ios pre-build ([1a6887bd70](https://github.com/facebook/react-native/commit/1a6887bd70cdefb8fbc421467de841ece74d5c6b) by [@cortinico](https://github.com/cortinico))
@@ -72,17 +136,13 @@
- **Prebuild:** Added building `XCFframework` from the prebuild script ([55534f518a](https://github.com/facebook/react-native/commit/55534f518aab53bcdc3fe12d987ab7ef6e620c77) by [@chrfalch](https://github.com/chrfalch))
- **Prebuild:** Added building swift package from the prebuild script ([3c01b1b6f0](https://github.com/facebook/react-native/commit/3c01b1b6f04d285c97bb182131135903b0c1cdd5) by [@chrfalch](https://github.com/chrfalch))
- **Prebuild:** Added downloading of hermes artifacts when pre-building for iOS. ([41d2b5de0a](https://github.com/facebook/react-native/commit/41d2b5de0af21c72227ef030dcddf208e2eb221a) by [@chrfalch](https://github.com/chrfalch))
- **Prebuild:** Add release/debug switch script for React-Core-prebuilt ([42d1a7934c](https://github.com/facebook/react-native/commit/42d1a7934cad4b2c92653e3fa7781c2af8f44df4) by [@chrfalch](https://github.com/chrfalch))
- **Prebuild:** Added support for using USE_FRAMEWORKS with prebuilt React Native Core ([40e45f5366](https://github.com/facebook/react-native/commit/40e45f53661ce80c3a6fbbf07f52dc900afcad52) by [@chrfalch](https://github.com/chrfalch))
- **runtime:** Added `HERMES_ENABLE_DEBUGGER` to debug configuration for the `reactRuntime` target. ([560ac23001](https://github.com/facebook/react-native/commit/560ac23001b02f19d4c6ca4ea493c17060cfaf5f) by [@chrfalch](https://github.com/chrfalch))
### Changed
- **API:** `NewAppScreen` no longer internally handles device safe area, use optional `safeAreaInsets` prop (aligned in 0.81 template) ([732bd12dc2](https://github.com/facebook/react-native/commit/732bd12dc21460641ef01b23f2eb722f26b060d5) by [@huntie](https://github.com/huntie))
- **Animated:** Animated now always flattens `props.style`, which fixes an error that results from `props.style` objects in which `AnimatedNode` instances are shadowed (i.e. flattened to not exist in the resulting `props.style` object). ([da520848c9](https://github.com/facebook/react-native/commit/da520848c931f356d013623c412af11dce7ff114) by [@yungsters](https://github.com/yungsters))
- **Animated:** Creates a feature flag that changes `Animated` to no longer produce invalid `props.style` if every `AnimatedNode` instance is shadowed via style flattening. ([5c8c5388fc](https://github.com/facebook/react-native/commit/5c8c5388fc53ef2430b0bb6bbbc628479819e23d) by [@yungsters](https://github.com/yungsters))
- **Animated:** Enabled a feature flag that optimizes `Animated` to reduce memory usage. ([2a13d20085](https://github.com/facebook/react-native/commit/2a13d200850e4f161a29b252a0ccedc158e53937) by [@yungsters](https://github.com/yungsters))
- **Babel:** Added support to `react-native/babel-preset` for a `hermesParserOptions` option, that expects an object that enables overriding `hermes-parser` options. ([0508eddfe6](https://github.com/facebook/react-native/commit/0508eddfe60df60cb3bfa4074ae199bd0e492d5f) by [@yungsters](https://github.com/yungsters))
- **Error handling:** Errors will no longer have the "js engine" suffix. ([a293925280](https://github.com/facebook/react-native/commit/a2939252803d5cd4b68340da08820174c30a53e6) by [@yungsters](https://github.com/yungsters))
- **Fibers:** Reduces memory usage, by improving memory management of parent alternate fibers. (Previously, a parent fiber might retain memory associated with shadow nodes from a previous commit.) ([0411c43b3a](https://github.com/facebook/react-native/commit/0411c43b3a239384c778baad22c7b4c501008449) by [@yungsters](https://github.com/yungsters))
- **infoLog:** Removed `infoLog` from `react-native` package ([8a0cfec815](https://github.com/facebook/react-native/commit/8a0cfec81584e966c9e6ea0f5e438022e0129bcd) by [@coado](https://github.com/coado))
@@ -90,26 +150,24 @@
- **Jest:** Improved default mocking for Jest unit tests. ([1fd9508ecc](https://github.com/facebook/react-native/commit/1fd9508ecc499df89b086e0c46035f43f6f78ad9) by [@yungsters](https://github.com/yungsters))
- **LegacyArchitecture:** Raise loglevel for assertion of `LegacyArchitecture` classes ([38a4b62211](https://github.com/facebook/react-native/commit/38a4b6221164d36eb4ac95c9f3bc7f7e7235e383) by [@mdvacca](https://github.com/mdvacca))
- **LegacyArchitecture:** Raise logLevel of `LegacyArchitecture` classes when minimizing of legacy architecture is enabled ([0d1cde7f36](https://github.com/facebook/react-native/commit/0d1cde7f36e9de72c997fc812bba023694c2a369) by [@mdvacca](https://github.com/mdvacca))
- **Metro:** Metro to ^0.83.1 ([e247be793c](https://github.com/facebook/react-native/commit/e247be793c70a374955d798d8cbbc6eba58080ec) by [@motiz88](https://github.com/motiz88))
- **Metro:** Bump Metro to `^0.82.5` ([083644647e](https://github.com/facebook/react-native/commit/083644647eff502f484b3ba24f9d361d5df56546) by [@robhogan](https://github.com/robhogan))
- **React DevTools:** Bumped React DevTools to `6.1.5` ([c302902b1d](https://github.com/facebook/react-native/commit/c302902b1db7e8f8ac5b61472c095dc0755d6d1c) by [@hoxyq](https://github.com/hoxyq))
- **RuntimeExecutor:** `RuntimeExecutor`: Remove noexcept from sync ui thread utils ([7ef278af50](https://github.com/facebook/react-native/commit/7ef278af505deba6b8a47876c6824f9a7fefa427) by [@RSNara](https://github.com/RSNara))
- **Typescript:** Bump `types/react` to `19.1` ([3ae9328571](https://github.com/facebook/react-native/commit/3ae932857174e9c39cd5d9c53922f849aa1401b1) by [@gabrieldonadel](https://github.com/gabrieldonadel))
#### Android specific
- **Android SDK:** Updated targetSdk to 36 in Android. ([477d8df312](https://github.com/facebook/react-native/commit/477d8df3126b325b8cc9b410f1eaeb56b727d4d9) by [@kikoso](https://github.com/kikoso))
- **APIs:** Deprecate `DefaultNewArchitectureEntryPoint.load(Boolean, Boolean, Boolean)` ([efdf73983c](https://github.com/facebook/react-native/commit/efdf73983cef1f371511b6e1efa5e01835ebcabb) by [@cortinico](https://github.com/cortinico))
- **APIs:** Make `com.facebook.react.views.common.ContextUtils` internal ([d1ef8f1fa3](https://github.com/facebook/react-native/commit/d1ef8f1fa36cbfc34d05c409abf693e4e1cac3de) by [@cortinico](https://github.com/cortinico))
- **deps:** Bump `AGP` to `8.11.0` ([04858ecbab](https://github.com/facebook/react-native/commit/04858ecbab808ddca80e20e76f1359619bb5e865) by [@cortinico](https://github.com/cortinico))
- **deps:** Bump `Gradle` to 8.14.3 ([6892dde363](https://github.com/facebook/react-native/commit/6892dde36373bbef2d0afe535ae818b1a7164f08) by [@cortinico](https://github.com/cortinico))
- **Gradle:** Expose `react_renderer_bridging` headers via prefab ([d1730ff960](https://github.com/facebook/react-native/commit/d1730ff960fcb9a01ee94b9e46e5a9fbb7d73f4a) by [@tomekzaw](https://github.com/tomekzaw))
- **deps:** Bump `Gradle` to `8.14.2` ([e20bb56f3b](https://github.com/facebook/react-native/commit/e20bb56f3b4db0d3e69154b95b952b1fe8e29959) by [@cortinico](https://github.com/cortinico))
- **JS FPS:** Hide JS FPS on performance overlay as not accurate ([feec8d0148](https://github.com/facebook/react-native/commit/feec8d014877b2177f1c7dded7eb9664f53ee471) by [@cortinico](https://github.com/cortinico))
- Updated targetSdk to 36 in Android. ([477d8df312](https://github.com/facebook/react-native/commit/477d8df3126b325b8cc9b410f1eaeb56b727d4d9) by [@kikoso](https://github.com/kikoso))
- **Kotlin:** Convert `UIManagerModuleConstantsHelper` to Kotlin ([45fd7feb9f](https://github.com/facebook/react-native/commit/45fd7feb9f083e5c8afc916732aed9795d344e09) by [@cortinico](https://github.com/cortinico))
- **Kotlin:** Migrate `ThemedReactContext` to Kotlin ([78c9671c24](https://github.com/facebook/react-native/commit/78c9671c241a86bedb17862e549842b7e36d77ea) by [@cortinico](https://github.com/cortinico))
- **Kotlin:** Convert `ReactViewGroup` to Kotlin ([48395d346b](https://github.com/facebook/react-native/commit/48395d346bc89f63d38889e58508304df0088e4f) by [@cortinico](https://github.com/cortinico))
- **Kotlin:** Migrate `com.facebook.react.LazyReactPackage` to Kotlin. ([b4ae5c1de1](https://github.com/facebook/react-native/commit/b4ae5c1de1003c343d43c3be1b59ee2b800b9258) by [@Xintre](https://github.com/Xintre))
- **Kotlin:** Apply Collections Kotlin DSL helpers in `ReactAndroid` package ([b2ffd34a39](https://github.com/facebook/react-native/commit/b2ffd34a392de2bddba5ee13248796ccc2db6039) by [@l2hyunwoo](https://github.com/l2hyunwoo))
- **Legacy Arch:** Introduce more deprecation warnings for Legacy Arch classes ([625f69f284](https://github.com/facebook/react-native/commit/625f69f284ddfd9c6beecaa4052a871d092053ef) by [@cortinico](https://github.com/cortinico))
#### iOS specific
@@ -165,21 +223,19 @@
- **Typescript:** Add `ImageSource` type to TypeScript ([42ca46b95c](https://github.com/facebook/react-native/commit/42ca46b95cf9938de00b76dc61948a4ae7116e2b) by [@okwasniewski](https://github.com/okwasniewski))
- **Typescript:** Devtools TS Types ([8f189fce03](https://github.com/facebook/react-native/commit/8f189fce03db367abdceca6ad57ae28b613fdd7d) by [@krystofwoldrich](https://github.com/krystofwoldrich))
- **Yoga:** Fix possible invalid measurements with width or height is zero pixels ([5cc4d0a086](https://github.com/facebook/react-native/commit/5cc4d0a086d450e0f9d8ab6194013348f9de1f58) by [@NickGerleman](https://github.com/NickGerleman))
- **Yoga:** Fixed nodes with `display: contents` set being cloned with the wrong owner ([d4b36b0300](https://github.com/facebook/react-native/commit/d4b36b03003eb2de9eaf5b57bb639bae8cc12f20) by [@j-piasecki](https://github.com/j-piasecki))
#### Android specific
- **API:** Make accessors inside HeadlessJsTaskService open again ([7ef57163cb](https://github.com/facebook/react-native/commit/7ef57163cb016317e43e563da7ea181989f6abca) by [@cortinico](https://github.com/cortinico))
- **BaseViewManager:** Remove focus change listener when dropping/recycling view instances ([94cbf206d6](https://github.com/facebook/react-native/commit/94cbf206d607477257c65039d97565a79e94c7dd) by [@Abbondanzo](https://github.com/Abbondanzo))
- **BoringLayout:** Include fallback line spacing in `BoringLayout` ([2fe6c1a947](https://github.com/facebook/react-native/commit/2fe6c1a94758223a5342fdfa90163971eb588e6a) by [@NickGerleman](https://github.com/NickGerleman))
- **Bridgeless:** Adding `shouldForwardToReactInstance` check in `ReactDelegate` for Bridgeless ([0f7bf66bba](https://github.com/facebook/react-native/commit/0f7bf66bba8498c89384e96ad9219cdad0107b0c) by [@arushikesarwani94](https://github.com/arushikesarwani94))
- **Codegen:** Fix combining schema in Codegen process to exclude platforms correctly ([6104ccdc6e](https://github.com/facebook/react-native/commit/6104ccdc6ef89c2d4da25e60dcc55d73038e023f) by [@arushikesarwani94](https://github.com/arushikesarwani94))
- **Edge To Edge:** Fix `Dimensions` `window` values on Android < 15 when edge-to-edge is enabled ([85d10ed904](https://github.com/facebook/react-native/commit/85d10ed90401a13de1f74aeddd773736195da285) by [@zoontek](https://github.com/zoontek))
- **FBReactNativeSpec:** Extract out `FBReactNativeSpec`'s core components including Unimplemented from auto-generated registry ([b417b0c2d5](https://github.com/facebook/react-native/commit/b417b0c2d56dc37f824c0e77e98d1014d21cd8f8) by [@arushikesarwani94](https://github.com/arushikesarwani94))
- **Gradle:** Fix Gradle v8.0 builds by using .set() for Property ([777397667c](https://github.com/facebook/react-native/commit/777397667c2625aab3fc907b9ef4bd564963d8bb) by [@meghancampbel9](https://github.com/meghancampbel9))
- **ImageFetcher:** Change `free` to `delete` to call destructor of `ImageFetcher` and release `contextContainer`. ([90da666691](https://github.com/facebook/react-native/commit/90da666691745ab9bf3930dc3347d8e51683099f) by [@WoLewicki](https://github.com/WoLewicki))
- **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))
- **onTextLayout:** Fix `onTextLayout` metrics not incorporating `ReactTextViewManagerCallback` ([a6a2884d63](https://github.com/facebook/react-native/commit/a6a2884d63717a42ac2bafd2054991ce8b32a2e9) by [@NickGerleman](https://github.com/NickGerleman))
- **RNGP:** Fix a race condition with codegen libraries missing sources ([9013a9e666](https://github.com/facebook/react-native/commit/9013a9e66629677c47e1b69703f9fc8f4cbc1c2c) by [@cortinico](https://github.com/cortinico))
- **Runtime:** Fixed `ReactHostImpl.nativeModules` always returning an empty list ([2f46a49](https://github.com/facebook/react-native/commit/2f46a49b8d8a11d5cf4342eee83c469b545c6779) by [@lukmccall](https://github.com/lukmccall))
- **Text:** Fix more text rounding bugs ([1fe3ff86c3](https://github.com/facebook/react-native/commit/1fe3ff86c364fad023ad1e426f26608699314339) by [@NickGerleman](https://github.com/NickGerleman))
- **Text:** Fix `TextLayoutManager` `MeasureMode` Regression ([99119a2104](https://github.com/facebook/react-native/commit/99119a210487af18983145fd374ff7ebc88931f3) by [@NickGerleman](https://github.com/NickGerleman))
- **TextInput:** Fix bug where focus would jump to top text input upon clearing a separate text input. ([79c47987b7](https://github.com/facebook/react-native/commit/79c47987b74ab044574fc542fd4b13a9f11aa491) by [@joevilches](https://github.com/joevilches))
@@ -187,13 +243,10 @@
#### iOS specific
- **Gradient**: Gradient interpolation for transparent colors ([097d482446](https://github.com/facebook/react-native/commit/097d482446b7a03ca0f8c7e0254f4d770e05c79c) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
- **Package.swift:** Add missing `React-RCTSettings` to `Package.swift` ([e40c1d265a](https://github.com/facebook/react-native/commit/e40c1d265a2045730dcf751eed4ebf32e099f0c7) by [@chrfalch](https://github.com/chrfalch))
- **Package.swift:** Fixed defines in `Package.swift` ([e2f6ce4ddf](https://github.com/facebook/react-native/commit/e2f6ce4ddfbea5814fc5d8632df14daeae3636d1) by [@chrfalch](https://github.com/chrfalch))
- **Podspec:** Fixed premature return in header file generation from podspec globs ([f2b064c2d4](https://github.com/facebook/react-native/commit/f2b064c2d40c39017ac2a31bf3caf8acef23038c) by [@chrfalch](https://github.com/chrfalch))
- **Podspec:** Fixed issue with RNDeps release/debug switch failing ([4ee2b60a1e](https://github.com/facebook/react-native/commit/4ee2b60a1eacca744d58a7ad336ca9d3714289f6) by [@chrfalch](https://github.com/chrfalch))
- **Podspec:** Fixed missing script for resolving prebuilt xcframework when switching between release/debug ([2e55241a90](https://github.com/facebook/react-native/commit/2e55241a901b4cd95917de68ce9078928820a208) by [@chrfalch](https://github.com/chrfalch))
- **Prebuild:** Fixed wrong path in prebuild hermes check ([be11f2ee77](https://github.com/facebook/react-native/commit/be11f2ee77fd793efe0a1aa225897a1924163925) by [@chrfalch](https://github.com/chrfalch))
- **Prebuild:** Fixed resolving build type when downloading hermes artifacts ([9371e20192](https://github.com/facebook/react-native/commit/9371e201927fd105e797bf06e43943dd21e04381) by [@chrfalch](https://github.com/chrfalch))
- **Package.swift:** Add missing `React-RCTSettings` to `Package.swift` ([e40c1d265a](https://github.com/facebook/react-native/commit/e40c1d265a2045730dcf751eed4ebf32e099f0c7) by [@chrfalch](https://github.com/chrfalch))
- **Package.swift:** Fixed defines in `Package.swift` ([e2f6ce4ddf](https://github.com/facebook/react-native/commit/e2f6ce4ddfbea5814fc5d8632df14daeae3636d1) by [@chrfalch](https://github.com/chrfalch))
- **RCTImage:** Allow for consuming `RCTImage` in Swift codebase by enabling "Defines Module" option ([1d80586730](https://github.com/facebook/react-native/commit/1d8058673085580f402ec3a320fce810db7ad2ef) by [@kkafar](https://github.com/kkafar))
- **RCTImageComponentView:** Fix `RCTImageComponentView` image loading after source props change with no layout invalidation ([cd5d74518b](https://github.com/facebook/react-native/commit/cd5d74518becb3355519373211d2f54ff7dbd208) by Nick Lefever)
- **RCTScreenSize:** Make `RCTScreenSize` take horizontal orientation into account ([50ce8c77a7](https://github.com/facebook/react-native/commit/50ce8c77a74f2f2574030db04dc88c6092e68ba8) by [@okwasniewski](https://github.com/okwasniewski))
@@ -554,10 +607,6 @@
- **Text:** Selection range not respected when changing text or selection when selection is forced ([d32ea66e6a](https://github.com/facebook/react-native/commit/d32ea66e6a945dd84092532401b265b12d482668) by Olivier Bouillet)
- **TextInput:** Fix TextInput `onContentSizeChange` event being dispatched only once on iOS on the new architecture ([5fd5188172](https://github.com/facebook/react-native/commit/5fd51881727b2d86f87abf04db032940ac0ec8c4) by [@j-piasecki](https://github.com/j-piasecki))
## v0.79.6
See [CHANGELOG-0.7x](./CHANGELOG-0.7x.md#v0796)
## v0.79.5
See [CHANGELOG-0.7x](./CHANGELOG-0.7x.md#v0795)
+12 -28
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,12 +152,10 @@ 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
@@ -177,7 +165,3 @@ allprojects {
}
}
}
// 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,
+8 -8
View File
@@ -63,7 +63,7 @@
"@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.30.0",
"babel-plugin-transform-define": "^2.1.4",
"babel-plugin-transform-flow-enums": "^0.0.2",
"clang-format": "^1.8.0",
@@ -75,17 +75,17 @@
"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.279.0",
"flow-api-translator": "0.30.0",
"flow-bin": "^0.278.0",
"glob": "^7.1.1",
"hermes-eslint": "0.32.0",
"hermes-transform": "0.32.0",
"hermes-eslint": "0.30.0",
"hermes-transform": "0.30.0",
"ini": "^5.0.0",
"inquirer": "^7.1.0",
"jest": "^29.7.0",
@@ -102,7 +102,7 @@
"node-fetch": "^2.2.0",
"nullthrows": "^1.1.1",
"prettier": "3.6.2",
"prettier-plugin-hermes-parser": "0.32.0",
"prettier-plugin-hermes-parser": "0.31.1",
"react": "19.1.1",
"react-test-renderer": "19.1.1",
"rimraf": "^3.0.2",
@@ -110,7 +110,7 @@
"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"
},
@@ -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 | }
+6 -58
View File
@@ -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'
@@ -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 = {
@@ -153,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,
};
+2 -2
View File
@@ -1,5 +1,5 @@
@generated SignedSource<<b6a779e724ecce5e727ee674dc20f809>>
Git revision: e87564a24cf233c60aaebee8c418ec85724f7214
@generated SignedSource<<9252db36d4b1db907a38c08935ceeb38>>
Git revision: 921566790e9e16d0ecace6e49b3cfaace205958c
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
@@ -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());
}
@@ -22,7 +22,7 @@ describe('Electron dependency', () => {
// $FlowFixMe[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
@@ -1,75 +1,62 @@
#!/usr/bin/env dotslash
// @generated SignedSource<<134c50a92ea8bbff7e42738b3e38c148>>
// @generated SignedSource<<686df5695b32a90cd465412d979a1a3f>>
{
"name": "React Native DevTools",
"platforms": {
"linux-aarch64": {
"size": 116052435,
"size": 113511487,
"hash": "sha256",
"digest": "957719cc5fc97f7a8841b3d2a7e1f8d625ab47b9031814ed11e0576b53552555",
"digest": "c22f7d5e029357f05055ce7c56cc284a0d441d558e103a7b7ebae118c67d0b95",
"providers": [
{
"type": "http",
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQOj5i7H4UTSZnrzYlWsTuSz6NHYS2de67x1ZurivOvqQdO7dsCf5R7C06hj7a2PydZR1HOmVEoGsuswoiGRg80pmS2AhhCds1lkchqG5puV5Bx2IY1rUosYTsZceD2AXgR7ptTD6NqgEGVG7O2KFpruI08RIQZUm60Kdn8LBeGfN3JddCIdsVQEsOk"
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQPRilWDJV8xyKEv9lqYn7Hf2kyZg6nqW8KctGZCXsU95lS_MTFyAmEULrB3J6hGCjqBY2Zl-uq_FjERAIgzbZFWX2eceacTbutBSOKEj6QTZzkVN_L1zPh2lGh24x29MHpkwUzw4E-kAXV43f-11QxqD7orI1QicyOnsmUqeRNKE_QjM9rNRsw6iE8"
}
],
"format": "tar.gz",
"path": "React Native DevTools-linux-arm64/React Native DevTools"
},
"linux-x86_64": {
"size": 115924513,
"size": 113244728,
"hash": "sha256",
"digest": "d8fe8e8df471e486697d5a42c7e77e7260ad3c44b10830f28529dca9f290535a",
"digest": "d749aee18d6c969f033511ed631b439c578068cc20786974bc0ee707c6c2f177",
"providers": [
{
"type": "http",
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQO_hyY-qUuuZXCew5qzp-tMfPL8fLLTUr5h4TLRbynsM3CmTlSILlmDcWP_xKAi5BJZSb6sOhATNKtRxn4pduIVkniIcL_6m1mGbt-1Qy8iKt0aUmANj5Nv8Wpryw6XL5OKYqtxHIsBg6YiZHW9JXYRCBk6oGCl1LGbhUZp2LGIFXAsOJy_DRcV"
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQP8NAAXjHJ-9NJThip1nnmVim4yiS1tdYAXmWl2Remiluq5f13nXA8YEadsmGRLWxXz2WJouHXSK47ea4a30fxewyKeTt0niFn3T-lr_91m3Ve5ZS-FOZ_9CRVCkv5zC-Z1FlXJGOnphfWetIU10Pw9tho3IzjdSNlXle0XTrkJ2AyKSl4cKPaX"
}
],
"format": "tar.gz",
"path": "React Native DevTools-linux-x64/React Native DevTools"
},
"macos-aarch64": {
"size": 110899100,
"size": 108805847,
"hash": "sha256",
"digest": "fe4dd8fa12bcb5a82e9843c6d80a9199d7b144487bde3d05e62cb4492d050f49",
"digest": "dc4a6cfa3d2d8646db8793ef497e43ad72be54bb14ed96345d059880ff72ce1e",
"providers": [
{
"type": "http",
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQOFvf5d4K1OypXm39lo851JiwM_hB2yfmdcFhZxgP6rN9jYNH0kg97ZF8HDc6DMYIBxVojrm7jlFgpk4Inc6AzXF6z5ypQfZwgM54F8wLaZqYY9Yk46nnQlQefjwGqrXKsniE_S9S2nqFQDTmQUbGGQIuM_bQjGievQsxXLrUadSGAcMu-MEB79a64Z"
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQPuZx9G4h30mUDM0hZ8y74LbzAi7-S6jnOqq8uyfDdIptLwIgpb8CKB8F2tduvaBFMGd7obwXd2NVy01r6XKVdgAK5-QTp8PYUPUE7VSCi6QAqobXcQ2uq_lY-jgyj8XkV4Ua4gA6KR609Bmh-beSTOrSqMymqVodaGqoGjB_9jVwfD99_PfFlex-Ta"
}
],
"format": "tar.gz",
"path": "React Native DevTools.app/Contents/MacOS/React Native DevTools"
},
"macos-x86_64": {
"size": 117756587,
"size": 113766610,
"hash": "sha256",
"digest": "3fe8a73843c3f0e1b72ffff0d069f9f1064f845be6fd44380d55aa1ab87220ac",
"digest": "74178859ce6a1c26c32055e192b1b123822c76c827ea86a6dfdb5463b89b1ace",
"providers": [
{
"type": "http",
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQNzRlsBElUT0wqSwTvxhvwPnuYpfI8Aka8Z4Wlmkmw_0pIwU0iEIh16wwH3OZTAMJ4_pM8YpyBl51FQXR5bai58MVtI_cI7aRxTfF63HKvk0iU0IxV06etgiaYiEmcyneLTb7UdYVdaZiwx7x_a0oaZU8FiuBlhg_YRzE6tM8oTD1hQf0T-a17USg"
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQN6DjGhr8_L7N_cwrh8pi3cfNcbn9dot_QQpgwe3Vv-780FDgi6JUeMvbo_wbFoT2rogxfHZ0-NbXbKdWYwhGUoGeQuikT57QnVMOYuPUTjlQUgYy0Ng9XPhq3iSEYwlMV1XsUJuPFRUg-hIHcgaaA55JoTLXZ1fLYydhHnmgRxGHc4pxaHvSpTtQ"
}
],
"format": "tar.gz",
"path": "React Native DevTools.app/Contents/MacOS/React Native DevTools"
},
"windows-x86_64": {
"size": 125513328,
"hash": "sha256",
"digest": "2477ee80171fc9f1c1f0b57418502bc103c6b40138f97f74d0b6470bdfee634c",
"providers": [
{
"type": "http",
"url": "https://scontent.xx.fbcdn.net/mci_ab/uap/?ab_b=m&ab_page=react_native_devtools_binaries&ab_entry=AQMwDMTAKLhSOn54OBOfhm6F9HDvQN0kUXp7ekfFik2NdgGoL_eVBu9ZcB2LM8x3YNpk0KxCAJE75jDfNTvGD2CnmLLRNOPCkdQP3tko7TTNkTSA2uTSd-ov8butKl1w4RP7ZMOy-0tmjyUoJMQtE1DlN5_4FdCo9DsHmQ7aB3wq_nz90nH0V_iS"
}
],
"format": "tar.gz",
"path": "React Native DevTools-win32-x64/React Native DevTools.exe"
}
}
}
+4 -10
View File
@@ -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,37 +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'),
});
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, {
@@ -86,7 +90,6 @@ function handleLaunchArgs(argv: string[]) {
steal: true,
});
}
frontendWindow.focus();
}
app.whenReady().then(() => {
@@ -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 [
// $FlowIssue[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(
// $FlowIssue[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};
-1
View File
@@ -38,7 +38,6 @@
"node": ">= 20.19.4"
},
"devDependencies": {
"@react-native/debugger-shell": "0.82.0-main",
"selfsigned": "^2.4.1",
"undici": "^5.29.0",
"wait-for-expect": "^3.0.2"
@@ -105,9 +105,8 @@ export class DebuggerMock extends DebuggerAgent {
: this.handle.mock.calls;
// $FlowFixMe[incompatible-use]
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-type]
const [response] = newHandleCalls.find(args => args[0].id === message.id);
// $FlowFixMe[incompatible-type]
// $FlowFixMe[incompatible-return]
// $FlowFixMe[incompatible-indexer]
return response;
}
@@ -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});
}
}
@@ -66,7 +66,7 @@ export async function sendFromTargetToDebugger<Message: CdpMessageFromTarget>(
: debugger_.handle.mock.calls;
// $FlowFixMe[incompatible-type]
const [receivedMessage]: [Message] = newHandleCalls.find(
// $FlowFixMe[incompatible-type]
// $FlowFixMe[incompatible-call]
(call: [Message]) => call[0].method === message.method,
);
return receivedMessage;
@@ -103,7 +103,7 @@ export async function sendFromDebuggerToTarget<Message: CdpMessageToTarget>(
// $FlowFixMe[incompatible-use]
call => call[0].wrappedEvent.id === message.id,
);
// $FlowFixMe[incompatible-type]
// $FlowFixMe[incompatible-return]
return receivedMessage.wrappedEvent;
}
@@ -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 {
@@ -40,7 +40,7 @@ function makeRequest(
host: ?string,
encrypted: boolean,
): http$IncomingMessage<> | http$IncomingMessage<tls$TLSSocket> {
// $FlowFixMe[incompatible-type] Partial mock of request
// $FlowFixMe[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 = {},
@@ -170,35 +170,6 @@ function createWrappedEventReporter(
'\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 -4
View File
@@ -10,10 +10,7 @@
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,
@@ -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
) {
@@ -970,10 +968,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,
@@ -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.
@@ -47,17 +43,4 @@ export interface BrowserLauncher {
* 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>;
}
@@ -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,
};
/**
@@ -24,7 +24,6 @@ export default function getDevToolsFrontendUrl(
/** Whether to use the modern `rn_fusebox.html` entry point. */
useFuseboxEntryPoint?: boolean,
appId?: string,
panel?: string,
}>,
): string {
const wsParam = getWsParam({
@@ -55,9 +54,6 @@ export default function getDevToolsFrontendUrl(
if (options?.telemetryInfo != null && options.telemetryInfo !== '') {
searchParams.append('telemetryInfo', options.telemetryInfo);
}
if (options?.panel != null && options.panel !== '') {
searchParams.append('panel', options.panel);
}
return appUrl + '?' + searchParams.toString();
}
@@ -28,7 +28,7 @@
"eslint-config-prettier": "^8.5.0",
"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-react": "^7.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-native": "^4.0.0"
@@ -18,8 +18,8 @@
"bugs": "https://github.com/facebook/react-native/issues",
"main": "index.js",
"devDependencies": {
"babel-plugin-syntax-hermes-parser": "0.32.0",
"hermes-eslint": "0.32.0"
"babel-plugin-syntax-hermes-parser": "0.30.0",
"hermes-eslint": "0.30.0"
},
"engines": {
"node": ">= 20.19.4"
+2 -2
View File
@@ -32,8 +32,8 @@
"source-map-support": "0.5.0"
},
"devDependencies": {
"babel-plugin-syntax-hermes-parser": "0.32.0",
"hermes-eslint": "0.32.0"
"babel-plugin-syntax-hermes-parser": "0.30.0",
"hermes-eslint": "0.30.0"
},
"engines": {
"node": ">= 20.19.4"
-4
View File
@@ -45,7 +45,3 @@ tasks.named("ktfmtFormat") {
":shared:ktfmtFormat",
)
}
// 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 } }
Binary file not shown.
@@ -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
+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.
@@ -68,8 +68,7 @@ tasks.withType<KotlinCompile>().configureEach {
// See comment above on JDK 11 support
jvmTarget.set(JvmTarget.JVM_11)
allWarningsAsErrors.set(
project.properties["enableWarningsAsErrors"]?.toString()?.toBoolean() ?: false
)
project.properties["enableWarningsAsErrors"]?.toString()?.toBoolean() ?: false)
}
}
@@ -208,8 +208,7 @@ abstract class ReactExtension @Inject constructor(val project: Project) {
} else {
buildTypes.forEach { buildType ->
result.add(
(dependencyConfiguration ?: "${buildType}Implementation") to ":$nameCleansed"
)
(dependencyConfiguration ?: "${buildType}Implementation") to ":$nameCleansed")
}
}
}
@@ -29,6 +29,7 @@ import com.facebook.react.utils.JdkConfiguratorUtils.configureJavaToolChains
import com.facebook.react.utils.JsonUtils
import com.facebook.react.utils.NdkConfiguratorUtils.configureReactNativeNdk
import com.facebook.react.utils.ProjectUtils.needsCodegenFromPackageJson
import com.facebook.react.utils.PropertyUtils
import com.facebook.react.utils.findPackageJsonFile
import java.io.File
import kotlin.system.exitProcess
@@ -42,6 +43,7 @@ import org.gradle.internal.jvm.Jvm
class ReactPlugin : Plugin<Project> {
override fun apply(project: Project) {
checkJvmVersion(project)
checkLegacyArchProperty(project)
val extension = project.extensions.create("react", ReactExtension::class.java, project)
// We register a private extension on the rootProject so that project wide configs
@@ -49,10 +51,7 @@ class ReactPlugin : Plugin<Project> {
val rootExtension =
project.rootProject.extensions.findByType(PrivateReactExtension::class.java)
?: project.rootProject.extensions.create(
"privateReact",
PrivateReactExtension::class.java,
project,
)
"privateReact", PrivateReactExtension::class.java, project)
// App Only Configuration
project.pluginManager.withPlugin("com.android.application") {
@@ -112,12 +111,35 @@ class ReactPlugin : Plugin<Project> {
********************************************************************************
"""
.trimIndent()
)
.trimIndent())
exitProcess(1)
}
}
private fun checkLegacyArchProperty(project: Project) {
if ((project.hasProperty(PropertyUtils.NEW_ARCH_ENABLED) &&
!project.property(PropertyUtils.NEW_ARCH_ENABLED).toString().toBoolean()) ||
(project.hasProperty(PropertyUtils.SCOPED_NEW_ARCH_ENABLED) &&
!project.property(PropertyUtils.SCOPED_NEW_ARCH_ENABLED).toString().toBoolean())) {
project.logger.error(
"""
********************************************************************************
WARNING: Setting `newArchEnabled=false` in your `gradle.properties` file is not
supported anymore since React Native 0.82.
You can remove the line from your `gradle.properties` file.
The application will run with the New Architecture enabled by default.
********************************************************************************
"""
.trimIndent())
}
}
/** This function configures Android resources - in this case just the bundle */
private fun configureResources(project: Project, reactExtension: ReactExtension) {
project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).finalizeDsl {
@@ -135,7 +157,7 @@ class ReactPlugin : Plugin<Project> {
project: Project,
localExtension: ReactExtension,
rootExtension: PrivateReactExtension,
isLibrary: Boolean,
isLibrary: Boolean
) {
// First, we set up the output dir for the codegen.
val generatedSrcDir: Provider<Directory> =
@@ -153,72 +175,70 @@ class ReactPlugin : Plugin<Project> {
// We create the task to produce schema from JS files.
val generateCodegenSchemaTask =
project.tasks.register(
"generateCodegenSchemaFromJavaScript",
GenerateCodegenSchemaTask::class.java,
) { it ->
it.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
it.codegenDir.set(rootExtension.codegenDir)
it.generatedSrcDir.set(generatedSrcDir)
it.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath)
"generateCodegenSchemaFromJavaScript", GenerateCodegenSchemaTask::class.java) { it ->
it.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
it.codegenDir.set(rootExtension.codegenDir)
it.generatedSrcDir.set(generatedSrcDir)
it.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath)
// We're reading the package.json at configuration time to properly feed
// the `jsRootDir` @Input property of this task & the onlyIf. Therefore, the
// parsePackageJson should be invoked inside this lambda.
val packageJson = findPackageJsonFile(project, rootExtension.root)
val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }
// We're reading the package.json at configuration time to properly feed
// the `jsRootDir` @Input property of this task & the onlyIf. Therefore, the
// parsePackageJson should be invoked inside this lambda.
val packageJson = findPackageJsonFile(project, rootExtension.root)
val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }
val jsSrcsDirInPackageJson = parsedPackageJson?.codegenConfig?.jsSrcsDir
val includesGeneratedCode =
parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false
if (jsSrcsDirInPackageJson != null) {
it.jsRootDir.set(File(packageJson.parentFile, jsSrcsDirInPackageJson))
} else {
it.jsRootDir.set(localExtension.jsRootDir)
}
it.jsInputFiles.set(
project.fileTree(it.jsRootDir) { tree ->
tree.include("**/*.js")
tree.include("**/*.jsx")
tree.include("**/*.ts")
tree.include("**/*.tsx")
tree.exclude("node_modules/**/*")
tree.exclude("**/*.d.ts")
// We want to exclude the build directory, to don't pick them up for execution
// avoidance.
tree.exclude("**/build/**/*")
val jsSrcsDirInPackageJson = parsedPackageJson?.codegenConfig?.jsSrcsDir
val includesGeneratedCode =
parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false
if (jsSrcsDirInPackageJson != null) {
it.jsRootDir.set(File(packageJson.parentFile, jsSrcsDirInPackageJson))
} else {
it.jsRootDir.set(localExtension.jsRootDir)
}
)
it.jsInputFiles.set(
project.fileTree(it.jsRootDir) { tree ->
tree.include("**/*.js")
tree.include("**/*.jsx")
tree.include("**/*.ts")
tree.include("**/*.tsx")
val needsCodegenFromPackageJson = project.needsCodegenFromPackageJson(rootExtension.root)
it.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode }
}
tree.exclude("node_modules/**/*")
tree.exclude("**/*.d.ts")
// We want to exclude the build directory, to don't pick them up for execution
// avoidance.
tree.exclude("**/build/**/*")
})
val needsCodegenFromPackageJson =
project.needsCodegenFromPackageJson(rootExtension.root)
it.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode }
}
// We create the task to generate Java code from schema.
val generateCodegenArtifactsTask =
project.tasks.register(
"generateCodegenArtifactsFromSchema",
GenerateCodegenArtifactsTask::class.java,
) { task ->
task.dependsOn(generateCodegenSchemaTask)
task.reactNativeDir.set(rootExtension.reactNativeDir)
task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
task.generatedSrcDir.set(generatedSrcDir)
task.packageJsonFile.set(findPackageJsonFile(project, rootExtension.root))
task.codegenJavaPackageName.set(localExtension.codegenJavaPackageName)
task.libraryName.set(localExtension.libraryName)
task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath)
"generateCodegenArtifactsFromSchema", GenerateCodegenArtifactsTask::class.java) { task
->
task.dependsOn(generateCodegenSchemaTask)
task.reactNativeDir.set(rootExtension.reactNativeDir)
task.nodeExecutableAndArgs.set(rootExtension.nodeExecutableAndArgs)
task.generatedSrcDir.set(generatedSrcDir)
task.packageJsonFile.set(findPackageJsonFile(project, rootExtension.root))
task.codegenJavaPackageName.set(localExtension.codegenJavaPackageName)
task.libraryName.set(localExtension.libraryName)
task.nodeWorkingDir.set(project.layout.projectDirectory.asFile.absolutePath)
// Please note that appNeedsCodegen is triggering a read of the package.json at
// configuration time as we need to feed the onlyIf condition of this task.
// Therefore, the appNeedsCodegen needs to be invoked inside this lambda.
val needsCodegenFromPackageJson = project.needsCodegenFromPackageJson(rootExtension.root)
val packageJson = findPackageJsonFile(project, rootExtension.root)
val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }
val includesGeneratedCode =
parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false
task.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode }
}
// Please note that appNeedsCodegen is triggering a read of the package.json at
// configuration time as we need to feed the onlyIf condition of this task.
// Therefore, the appNeedsCodegen needs to be invoked inside this lambda.
val needsCodegenFromPackageJson =
project.needsCodegenFromPackageJson(rootExtension.root)
val packageJson = findPackageJsonFile(project, rootExtension.root)
val parsedPackageJson = packageJson?.let { JsonUtils.fromPackageJson(it) }
val includesGeneratedCode =
parsedPackageJson?.codegenConfig?.includesGeneratedCode ?: false
task.onlyIf { (isLibrary || needsCodegenFromPackageJson) && !includesGeneratedCode }
}
// We update the android configuration to include the generated sources.
// This equivalent to this DSL:
@@ -261,34 +281,29 @@ class ReactPlugin : Plugin<Project> {
// dependency.
val generatePackageListTask =
project.tasks.register(
"generateAutolinkingPackageList",
GeneratePackageListTask::class.java,
) { task ->
task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
task.generatedOutputDirectory.set(generatedAutolinkingJavaDir)
}
"generateAutolinkingPackageList", GeneratePackageListTask::class.java) { task ->
task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
task.generatedOutputDirectory.set(generatedAutolinkingJavaDir)
}
// We add a task called generateAutolinkingPackageList to do not clash with the existing task
// called generatePackageList. This can to be renamed once we unlink the rn <-> cli
// dependency.
val generateEntryPointTask =
project.tasks.register(
"generateReactNativeEntryPoint",
GenerateEntryPointTask::class.java,
) { task ->
task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
task.generatedOutputDirectory.set(generatedAutolinkingJavaDir)
}
"generateReactNativeEntryPoint", GenerateEntryPointTask::class.java) { task ->
task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
task.generatedOutputDirectory.set(generatedAutolinkingJavaDir)
}
// We also need to generate code for C++ Autolinking
val generateAutolinkingNewArchitectureFilesTask =
project.tasks.register(
"generateAutolinkingNewArchitectureFiles",
GenerateAutolinkingNewArchitecturesFileTask::class.java,
) { task ->
task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
task.generatedOutputDirectory.set(generatedAutolinkingJniDir)
}
GenerateAutolinkingNewArchitecturesFileTask::class.java) { task ->
task.autolinkInputFile.set(rootGeneratedAutolinkingFile)
task.generatedOutputDirectory.set(generatedAutolinkingJniDir)
}
project.tasks
.named("preBuild", Task::class.java)
.dependsOn(generateAutolinkingNewArchitectureFilesTask)
@@ -305,8 +320,7 @@ class ReactPlugin : Plugin<Project> {
project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java).apply {
onVariants(selector().all()) { variant ->
variant.sources.java?.addStaticSourceDirectory(
generatedAutolinkingJavaDir.get().asFile.absolutePath
)
generatedAutolinkingJavaDir.get().asFile.absolutePath)
}
}
}
@@ -7,10 +7,8 @@
package com.facebook.react
import com.facebook.react.utils.PropertyUtils
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.plugin.extraProperties
/**
* Gradle plugin applied to the `android/build.gradle` file.
@@ -20,25 +18,13 @@ import org.jetbrains.kotlin.gradle.plugin.extraProperties
*/
class ReactRootProjectPlugin : Plugin<Project> {
override fun apply(project: Project) {
checkLegacyArchProperty(project)
project.subprojects { subproject ->
project.subprojects {
// As the :app project (i.e. ReactPlugin) configures both namespaces and JVM toolchains
// for libraries, its evaluation must happen before the libraries' evaluation.
// Eventually the configuration of namespace/JVM toolchain can be moved inside this plugin.
if (subproject.path != ":app") {
subproject.evaluationDependsOn(":app")
if (it.path != ":app") {
it.evaluationDependsOn(":app")
}
// We set the New Architecture properties to true for all subprojects. So that
// libraries don't need to be modified and can keep on using the isNewArchEnabled()
// function to check if property is set.
if (subproject.hasProperty(PropertyUtils.SCOPED_NEW_ARCH_ENABLED)) {
subproject.setProperty(PropertyUtils.SCOPED_NEW_ARCH_ENABLED, "true")
}
if (subproject.hasProperty(PropertyUtils.NEW_ARCH_ENABLED)) {
subproject.setProperty(PropertyUtils.NEW_ARCH_ENABLED, "true")
}
subproject.extraProperties.set(PropertyUtils.NEW_ARCH_ENABLED, "true")
subproject.extraProperties.set(PropertyUtils.SCOPED_NEW_ARCH_ENABLED, "true")
}
// We need to make sure that `:app:preBuild` task depends on all other subprojects' preBuild
// tasks. This is necessary in order to have all the codegen generated code before the CMake
@@ -57,30 +43,4 @@ class ReactRootProjectPlugin : Plugin<Project> {
}
}
}
private fun checkLegacyArchProperty(project: Project) {
if (
(project.hasProperty(PropertyUtils.NEW_ARCH_ENABLED) &&
!project.property(PropertyUtils.NEW_ARCH_ENABLED).toString().toBoolean()) ||
(project.hasProperty(PropertyUtils.SCOPED_NEW_ARCH_ENABLED) &&
!project.property(PropertyUtils.SCOPED_NEW_ARCH_ENABLED).toString().toBoolean())
) {
project.logger.error(
"""
********************************************************************************
WARNING: Setting `newArchEnabled=false` in your `gradle.properties` file is not
supported anymore since React Native 0.82.
You can remove the line from your `gradle.properties` file.
The application will run with the New Architecture enabled by default.
********************************************************************************
"""
.trimIndent()
)
}
}
}
@@ -54,11 +54,9 @@ internal fun Project.configureReactTasks(variant: Variant, config: ReactExtensio
configureNewArchPackagingOptions(project, config, variant)
configureJsEnginePackagingOptions(config, variant, isHermesEnabledInThisVariant, useThirdPartyJSC)
if (
!isHermesEnabledInThisVariant &&
!useThirdPartyJSC &&
rootProject.name != "react-native-github"
) {
if (!isHermesEnabledInThisVariant &&
!useThirdPartyJSC &&
rootProject.name != "react-native-github") {
showJSCRemovalMessage(project)
}
@@ -39,15 +39,12 @@ abstract class PrivateReactExtension @Inject constructor(project: Project) {
// - We're inside a user project, so inside the ./android folder. Default should be
// ../
// User can always override this default by setting a `root =` inside the template.
if (
project.rootProject.name == "react-native-github" ||
project.rootProject.name == "react-native-build-from-source"
) {
if (project.rootProject.name == "react-native-github" ||
project.rootProject.name == "react-native-build-from-source") {
project.rootProject.layout.projectDirectory.dir("../../")
} else {
project.rootProject.layout.projectDirectory.dir("../")
}
)
})
val reactNativeDir: DirectoryProperty =
objects.directoryProperty().convention(root.dir("node_modules/react-native"))
@@ -111,11 +111,7 @@ abstract class BundleHermesCTask : DefaultTask() {
val composeScriptFile = File(reactNativeDir, "scripts/compose-source-maps.js")
val composeSourceMapsCommand =
getComposeSourceMapsCommand(
composeScriptFile,
packagerSourceMap,
compilerSourceMap,
outputSourceMap,
)
composeScriptFile, packagerSourceMap, compilerSourceMap, outputSourceMap)
runCommand(composeSourceMapsCommand)
}
}
@@ -176,7 +172,7 @@ abstract class BundleHermesCTask : DefaultTask() {
internal fun getHermescCommand(
hermesCommand: String,
bytecodeFile: File,
bundleFile: File,
bundleFile: File
): List<Any> {
val rootFile = root.get().asFile
return windowsAwareCommandLine(
@@ -187,15 +183,14 @@ abstract class BundleHermesCTask : DefaultTask() {
"-out",
bytecodeFile.cliPath(rootFile),
bundleFile.cliPath(rootFile),
*hermesFlags.get().toTypedArray(),
)
*hermesFlags.get().toTypedArray())
}
internal fun getComposeSourceMapsCommand(
composeScript: File,
packagerSourceMap: File,
compilerSourceMap: File,
outputSourceMap: File,
outputSourceMap: File
): List<Any> {
val rootFile = root.get().asFile
return windowsAwareCommandLine(
@@ -204,7 +199,6 @@ abstract class BundleHermesCTask : DefaultTask() {
packagerSourceMap.cliPath(rootFile),
compilerSourceMap.cliPath(rootFile),
"-o",
outputSourceMap.cliPath(rootFile),
)
outputSourceMap.cliPath(rootFile))
}
}
@@ -165,88 +165,88 @@ abstract class GenerateAutolinkingNewArchitecturesFileTask : DefaultTask() {
// language=cmake
val CMAKE_TEMPLATE =
"""
# This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin)
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
# We set REACTNATIVE_MERGED_SO so libraries/apps can selectively decide to depend on either libreactnative.so
# or link against a old prefab target (this is needed for React Native 0.76 on).
set(REACTNATIVE_MERGED_SO true)
{{ libraryIncludes }}
set(AUTOLINKED_LIBRARIES
{{ libraryModules }}
)
"""
# This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin)
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
# We set REACTNATIVE_MERGED_SO so libraries/apps can selectively decide to depend on either libreactnative.so
# or link against a old prefab target (this is needed for React Native 0.76 on).
set(REACTNATIVE_MERGED_SO true)
{{ libraryIncludes }}
set(AUTOLINKED_LIBRARIES
{{ libraryModules }}
)
"""
.trimIndent()
// language=cpp
val CPP_TEMPLATE =
"""
/**
* This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
*/
#include "autolinking.h"
{{ autolinkingCppIncludes }}
namespace facebook {
namespace react {
std::shared_ptr<TurboModule> autolinking_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params) {
{{ autolinkingCppTurboModuleJavaProviders }}
return nullptr;
}
std::shared_ptr<TurboModule> autolinking_cxxModuleProvider(const std::string moduleName, const std::shared_ptr<CallInvoker>& jsInvoker) {
{{ autolinkingCppTurboModuleCxxProviders }}
return nullptr;
}
void autolinking_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry) {
{{ autolinkingCppComponentDescriptors }}
return;
}
} // namespace react
} // namespace facebook
"""
/**
* This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
*/
#include "autolinking.h"
{{ autolinkingCppIncludes }}
namespace facebook {
namespace react {
std::shared_ptr<TurboModule> autolinking_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params) {
{{ autolinkingCppTurboModuleJavaProviders }}
return nullptr;
}
std::shared_ptr<TurboModule> autolinking_cxxModuleProvider(const std::string moduleName, const std::shared_ptr<CallInvoker>& jsInvoker) {
{{ autolinkingCppTurboModuleCxxProviders }}
return nullptr;
}
void autolinking_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry) {
{{ autolinkingCppComponentDescriptors }}
return;
}
} // namespace react
} // namespace facebook
"""
.trimIndent()
// language=cpp
val hTemplate =
"""
/**
* This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
*/
#pragma once
#include <ReactCommon/CallInvoker.h>
#include <ReactCommon/JavaTurboModule.h>
#include <ReactCommon/TurboModule.h>
#include <jsi/jsi.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
namespace facebook {
namespace react {
std::shared_ptr<TurboModule> autolinking_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params);
std::shared_ptr<TurboModule> autolinking_cxxModuleProvider(const std::string moduleName, const std::shared_ptr<CallInvoker>& jsInvoker);
void autolinking_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry);
} // namespace react
} // namespace facebook
"""
/**
* This code was generated by [React Native](https://www.npmjs.com/package/@react-native/gradle-plugin).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
*/
#pragma once
#include <ReactCommon/CallInvoker.h>
#include <ReactCommon/JavaTurboModule.h>
#include <ReactCommon/TurboModule.h>
#include <jsi/jsi.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
namespace facebook {
namespace react {
std::shared_ptr<TurboModule> autolinking_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params);
std::shared_ptr<TurboModule> autolinking_cxxModuleProvider(const std::string moduleName, const std::shared_ptr<CallInvoker>& jsInvoker);
void autolinking_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry);
} // namespace react
} // namespace facebook
"""
.trimIndent()
}
}
@@ -81,8 +81,6 @@ abstract class GenerateCodegenArtifactsTask : Exec() {
"--libraryName",
libraryName,
"--javaPackageName",
codegenJavaPackageName,
)
)
codegenJavaPackageName))
}
}
@@ -69,7 +69,6 @@ abstract class GenerateCodegenSchemaTask : Exec() {
"NativeSampleTurboModule",
generatedSchemaFile.get().asFile.cliPath(workingDir),
jsRootDir.asFile.get().cliPath(workingDir),
)
)
))
}
}
@@ -32,19 +32,17 @@ abstract class GenerateEntryPointTask : DefaultTask() {
JsonUtils.fromAutolinkingConfigJson(autolinkInputFile.get().asFile)
?: error(
"""
RNGP - Autolinking: Could not parse autolinking config file:
${autolinkInputFile.get().asFile.absolutePath}
The file is either missing or not containing valid JSON so the build won't succeed.
"""
.trimIndent()
)
RNGP - Autolinking: Could not parse autolinking config file:
${autolinkInputFile.get().asFile.absolutePath}
The file is either missing or not containing valid JSON so the build won't succeed.
"""
.trimIndent())
val packageName =
model.project?.android?.packageName
?: error(
"RNGP - Autolinking: Could not find project.android.packageName in react-native config output! Could not autolink packages without this field."
)
"RNGP - Autolinking: Could not find project.android.packageName in react-native config output! Could not autolink packages without this field.")
val generatedFileContents = composeFileContent(packageName)
val outputDir = generatedOutputDirectory.get().asFile
@@ -64,45 +62,45 @@ abstract class GenerateEntryPointTask : DefaultTask() {
// language=java
val generatedFileContentsTemplate =
"""
package com.facebook.react;
import android.app.Application;
import android.content.Context;
import android.content.res.Resources;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger;
import com.facebook.react.views.view.WindowUtilKt;
import com.facebook.react.soloader.OpenSourceMergedSoMapping;
import com.facebook.soloader.SoLoader;
import java.io.IOException;
/**
* This class is the entry point for loading React Native using the configuration
* that the users specifies in their .gradle files.
*
* The `loadReactNative(this)` method invocation should be called inside the
* application onCreate otherwise the app won't load correctly.
*/
public class ReactNativeApplicationEntryPoint {
public static void loadReactNative(Context context) {
try {
SoLoader.init(context, OpenSourceMergedSoMapping.INSTANCE);
} catch (IOException error) {
throw new RuntimeException(error);
}
if ({{packageName}}.BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
DefaultNewArchitectureEntryPoint.load();
}
if ({{packageName}}.BuildConfig.IS_EDGE_TO_EDGE_ENABLED) {
WindowUtilKt.setEdgeToEdgeFeatureFlagOn();
}
package com.facebook.react;
import android.app.Application;
import android.content.Context;
import android.content.res.Resources;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.common.annotations.internal.LegacyArchitectureLogger;
import com.facebook.react.views.view.WindowUtilKt;
import com.facebook.react.soloader.OpenSourceMergedSoMapping;
import com.facebook.soloader.SoLoader;
import java.io.IOException;
/**
* This class is the entry point for loading React Native using the configuration
* that the users specifies in their .gradle files.
*
* The `loadReactNative(this)` method invocation should be called inside the
* application onCreate otherwise the app won't load correctly.
*/
public class ReactNativeApplicationEntryPoint {
public static void loadReactNative(Context context) {
try {
SoLoader.init(context, OpenSourceMergedSoMapping.INSTANCE);
} catch (IOException error) {
throw new RuntimeException(error);
}
if ({{packageName}}.BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
DefaultNewArchitectureEntryPoint.load();
}
if ({{packageName}}.BuildConfig.IS_EDGE_TO_EDGE_ENABLED) {
WindowUtilKt.setEdgeToEdgeFeatureFlagOn();
}
}
"""
}
"""
.trimIndent()
}
}
@@ -34,19 +34,17 @@ abstract class GeneratePackageListTask : DefaultTask() {
JsonUtils.fromAutolinkingConfigJson(autolinkInputFile.get().asFile)
?: error(
"""
RNGP - Autolinking: Could not parse autolinking config file:
${autolinkInputFile.get().asFile.absolutePath}
The file is either missing or not containing valid JSON so the build won't succeed.
"""
.trimIndent()
)
RNGP - Autolinking: Could not parse autolinking config file:
${autolinkInputFile.get().asFile.absolutePath}
The file is either missing or not containing valid JSON so the build won't succeed.
"""
.trimIndent())
val packageName =
model.project?.android?.packageName
?: error(
"RNGP - Autolinking: Could not find project.android.packageName in react-native config output! Could not autolink packages without this field."
)
"RNGP - Autolinking: Could not find project.android.packageName in react-native config output! Could not autolink packages without this field.")
val androidPackages = filterAndroidPackages(model)
val packageImports = composePackageImports(packageName, androidPackages)
@@ -63,7 +61,7 @@ abstract class GeneratePackageListTask : DefaultTask() {
internal fun composePackageImports(
packageName: String,
packages: Map<String, ModelAutolinkingDependenciesPlatformAndroidJson>,
packages: Map<String, ModelAutolinkingDependenciesPlatformAndroidJson>
) =
packages.entries.joinToString("\n") { (name, dep) ->
val packageImportPath =
@@ -75,7 +73,7 @@ abstract class GeneratePackageListTask : DefaultTask() {
internal fun composePackageInstance(
packageName: String,
packages: Map<String, ModelAutolinkingDependenciesPlatformAndroidJson>,
packages: Map<String, ModelAutolinkingDependenciesPlatformAndroidJson>
) =
if (packages.isEmpty()) {
""
@@ -136,69 +134,69 @@ abstract class GeneratePackageListTask : DefaultTask() {
// language=java
val generatedFileContentsTemplate =
"""
package com.facebook.react;
import android.app.Application;
import android.content.Context;
import android.content.res.Resources;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainPackageConfig;
import com.facebook.react.shell.MainReactPackage;
import java.util.Arrays;
import java.util.ArrayList;
{{ packageImports }}
@SuppressWarnings("deprecation")
public class PackageList {
private Application application;
private ReactNativeHost reactNativeHost;
private MainPackageConfig mConfig;
public PackageList(ReactNativeHost reactNativeHost) {
this(reactNativeHost, null);
}
public PackageList(Application application) {
this(application, null);
}
public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) {
this.reactNativeHost = reactNativeHost;
mConfig = config;
}
public PackageList(Application application, MainPackageConfig config) {
this.reactNativeHost = null;
this.application = application;
mConfig = config;
}
private ReactNativeHost getReactNativeHost() {
return this.reactNativeHost;
}
private Resources getResources() {
return this.getApplication().getResources();
}
private Application getApplication() {
if (this.reactNativeHost == null) return this.application;
return this.reactNativeHost.getApplication();
}
private Context getApplicationContext() {
return this.getApplication().getApplicationContext();
}
public ArrayList<ReactPackage> getPackages() {
return new ArrayList<>(Arrays.<ReactPackage>asList(
new MainReactPackage(mConfig){{ packageClassInstances }}
));
}
}
"""
package com.facebook.react;
import android.app.Application;
import android.content.Context;
import android.content.res.Resources;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainPackageConfig;
import com.facebook.react.shell.MainReactPackage;
import java.util.Arrays;
import java.util.ArrayList;
{{ packageImports }}
@SuppressWarnings("deprecation")
public class PackageList {
private Application application;
private ReactNativeHost reactNativeHost;
private MainPackageConfig mConfig;
public PackageList(ReactNativeHost reactNativeHost) {
this(reactNativeHost, null);
}
public PackageList(Application application) {
this(application, null);
}
public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) {
this.reactNativeHost = reactNativeHost;
mConfig = config;
}
public PackageList(Application application, MainPackageConfig config) {
this.reactNativeHost = null;
this.application = application;
mConfig = config;
}
private ReactNativeHost getReactNativeHost() {
return this.reactNativeHost;
}
private Resources getResources() {
return this.getApplication().getResources();
}
private Application getApplication() {
if (this.reactNativeHost == null) return this.application;
return this.reactNativeHost.getApplication();
}
private Context getApplicationContext() {
return this.getApplication().getApplicationContext();
}
public ArrayList<ReactPackage> getPackages() {
return new ArrayList<>(Arrays.<ReactPackage>asList(
new MainReactPackage(mConfig){{ packageClassInstances }}
));
}
}
"""
.trimIndent()
}
}
@@ -54,8 +54,7 @@ abstract class BuildCodegenCLITask : Exec() {
windowsAwareBashCommandLine(
codegenDir.asFile.get().canonicalPath.unixifyPath().plus(BUILD_SCRIPT_PATH),
bashWindowsHome = bashWindowsHome.orNull,
)
)
))
super.exec()
}
@@ -29,10 +29,8 @@ abstract class CustomExecTask : Exec() {
@get:Input @get:Optional abstract val onlyIfProvidedPathDoesNotExists: Property<String>
override fun exec() {
if (
onlyIfProvidedPathDoesNotExists.isPresent &&
File(onlyIfProvidedPathDoesNotExists.get()).exists()
) {
if (onlyIfProvidedPathDoesNotExists.isPresent &&
File(onlyIfProvidedPathDoesNotExists.get()).exists()) {
return
}
if (standardOutputFile.isPresent) {
@@ -40,8 +40,7 @@ abstract class PrepareBoostTask : DefaultTask() {
"CMakeLists.txt",
"boost_${boostVersion.get()}/boost/**/*.hpp",
"boost/boost/**/*.hpp",
"asm/**/*.S",
)
"asm/**/*.S")
it.includeEmptyDirs = false
it.into(outputDir)
}
@@ -46,8 +46,7 @@ abstract class PrepareGflagsTask : DefaultTask() {
action.include(
"gflags-${gflagsVersion.get()}/src/*.h",
"gflags-${gflagsVersion.get()}/src/*.cc",
"CMakeLists.txt",
)
"CMakeLists.txt")
action.filesMatching("*/src/*") { matchedFile ->
matchedFile.path = "gflags/${matchedFile.name}"
}
@@ -64,10 +63,8 @@ abstract class PrepareGflagsTask : DefaultTask() {
.replace(Regex("@GFLAGS_NAMESPACE@"), "gflags")
.replace(
Regex(
"@(HAVE_STDINT_H|HAVE_SYS_TYPES_H|HAVE_INTTYPES_H|GFLAGS_INTTYPES_FORMAT_C99)@"
),
"1",
)
"@(HAVE_STDINT_H|HAVE_SYS_TYPES_H|HAVE_INTTYPES_H|GFLAGS_INTTYPES_FORMAT_C99)@"),
"1")
.replace(Regex("@([A-Z0-9_]+)@"), "1")
}
matchedFile.path = "gflags/${matchedFile.name.removeSuffix(".in")}"
@@ -60,11 +60,8 @@ abstract class PrepareGlogTask : DefaultTask() {
"ac_cv___attribute___noinline" to "__attribute__ ((noinline))",
"ac_cv___attribute___noreturn" to "__attribute__ ((noreturn))",
"ac_cv___attribute___printf_4_5" to
"__attribute__((__format__ (__printf__, 4, 5)))",
)
),
ReplaceTokens::class.java,
)
"__attribute__((__format__ (__printf__, 4, 5)))")),
ReplaceTokens::class.java)
matchedFile.path = (matchedFile.name.removeSuffix(".in"))
}
action.into(outputDir)
@@ -77,8 +74,7 @@ abstract class PrepareGlogTask : DefaultTask() {
"logging.h",
"raw_logging.h",
"vlog_is_on.h",
"**/src/glog/log_severity.h",
)
"**/src/glog/log_severity.h")
action.eachFile { file -> file.path = file.name }
action.includeEmptyDirs = false
action.into(exportedDir)
@@ -23,6 +23,6 @@ data class PrefabPreprocessingEntry(
) : Serializable {
constructor(
libraryName: String,
pathToPrefixCouple: Pair<String, String>,
pathToPrefixCouple: Pair<String, String>
) : this(libraryName, listOf(pathToPrefixCouple))
}
@@ -66,15 +66,9 @@ internal object AgpConfiguratorUtils {
ext.buildFeatures.buildConfig = true
ext.defaultConfig.buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", "true")
ext.defaultConfig.buildConfigField(
"boolean",
"IS_HERMES_ENABLED",
project.isHermesEnabled.toString(),
)
"boolean", "IS_HERMES_ENABLED", project.isHermesEnabled.toString())
ext.defaultConfig.buildConfigField(
"boolean",
"IS_EDGE_TO_EDGE_ENABLED",
project.isEdgeToEdgeEnabled.toString(),
)
"boolean", "IS_EDGE_TO_EDGE_ENABLED", project.isEdgeToEdgeEnabled.toString())
}
}
project.pluginManager.withPlugin("com.android.application", action)
@@ -101,10 +95,7 @@ internal object AgpConfiguratorUtils {
.getByType(ApplicationAndroidComponentsExtension::class.java)
.finalizeDsl { ext ->
ext.defaultConfig.resValue(
"string",
"react_native_dev_server_ip",
getHostIpAddress(),
)
"string", "react_native_dev_server_ip", getHostIpAddress())
ext.defaultConfig.resValue("integer", "react_native_dev_server_port", devServerPort)
}
}
@@ -39,8 +39,7 @@ internal object BackwardCompatUtils {
********************************************************************************
"""
.trimIndent()
)
.trimIndent())
}
}
@@ -56,14 +55,14 @@ internal object BackwardCompatUtils {
val message =
"""
=============== JavaScriptCore is being moved ===============
JavaScriptCore has been extracted from react-native core
and will be removed in a future release. It can now be
installed from `@react-native-community/javascriptcore`
See: https://github.com/react-native-community/javascriptcore
=============================================================
=============== JavaScriptCore is being moved ===============
JavaScriptCore has been extracted from react-native core
and will be removed in a future release. It can now be
installed from `@react-native-community/javascriptcore`
See: https://github.com/react-native-community/javascriptcore
=============================================================
"""
"""
.trimIndent()
project.logger.warn(message)
hasShownJSCRemovalMessage = true
@@ -33,8 +33,7 @@ internal object DependencyUtils {
val exclusiveEnterpriseRepository = project.rootProject.exclusiveEnterpriseRepository()
if (exclusiveEnterpriseRepository != null) {
project.logger.lifecycle(
"Replacing ALL Maven Repositories with: $exclusiveEnterpriseRepository"
)
"Replacing ALL Maven Repositories with: $exclusiveEnterpriseRepository")
}
project.rootProject.allprojects { eachProject ->
@@ -100,7 +99,7 @@ internal object DependencyUtils {
fun configureDependencies(
project: Project,
versionString: String,
groupString: String = DEFAULT_INTERNAL_PUBLISHING_GROUP,
groupString: String = DEFAULT_INTERNAL_PUBLISHING_GROUP
) {
if (versionString.isBlank()) return
project.rootProject.allprojects { eachProject ->
@@ -128,38 +127,30 @@ internal object DependencyUtils {
internal fun getDependencySubstitutions(
versionString: String,
groupString: String = DEFAULT_INTERNAL_PUBLISHING_GROUP,
groupString: String = DEFAULT_INTERNAL_PUBLISHING_GROUP
): List<Triple<String, String, String>> {
val dependencySubstitution = mutableListOf<Triple<String, String, String>>()
dependencySubstitution.add(
Triple(
"com.facebook.react:react-native",
"${groupString}:react-android:${versionString}",
"The react-native artifact was deprecated in favor of react-android due to https://github.com/facebook/react-native/issues/35210.",
)
)
"The react-native artifact was deprecated in favor of react-android due to https://github.com/facebook/react-native/issues/35210."))
dependencySubstitution.add(
Triple(
"com.facebook.react:hermes-engine",
"${groupString}:hermes-android:${versionString}",
"The hermes-engine artifact was deprecated in favor of hermes-android due to https://github.com/facebook/react-native/issues/35210.",
)
)
"The hermes-engine artifact was deprecated in favor of hermes-android due to https://github.com/facebook/react-native/issues/35210."))
if (groupString != DEFAULT_INTERNAL_PUBLISHING_GROUP) {
dependencySubstitution.add(
Triple(
"com.facebook.react:react-android",
"${groupString}:react-android:${versionString}",
"The react-android dependency was modified to use the correct Maven group.",
)
)
"The react-android dependency was modified to use the correct Maven group."))
dependencySubstitution.add(
Triple(
"com.facebook.react:hermes-android",
"${groupString}:hermes-android:${versionString}",
"The hermes-android dependency was modified to use the correct Maven group.",
)
)
"The hermes-android dependency was modified to use the correct Maven group."))
}
return dependencySubstitution
}
@@ -184,7 +175,7 @@ internal object DependencyUtils {
fun Project.mavenRepoFromUrl(
url: String,
action: (MavenArtifactRepository) -> Unit = {},
action: (MavenArtifactRepository) -> Unit = {}
): MavenArtifactRepository =
project.repositories.maven {
it.url = URI.create(url)
@@ -193,7 +184,7 @@ internal object DependencyUtils {
fun Project.mavenRepoFromURI(
uri: URI,
action: (MavenArtifactRepository) -> Unit = {},
action: (MavenArtifactRepository) -> Unit = {}
): MavenArtifactRepository =
project.repositories.maven {
it.url = uri
@@ -30,8 +30,7 @@ internal object NdkConfiguratorUtils {
ext.externalNativeBuild.cmake.path =
File(
extension.reactNativeDir.get().asFile,
"ReactAndroid/cmake-utils/default-app-setup/CMakeLists.txt",
)
"ReactAndroid/cmake-utils/default-app-setup/CMakeLists.txt")
}
// Parameters should be provided in an additive manner (do not override what
@@ -45,8 +44,7 @@ internal object NdkConfiguratorUtils {
}
if (cmakeArgs.none { it.startsWith("-DREACT_ANDROID_DIR") }) {
cmakeArgs.add(
"-DREACT_ANDROID_DIR=${extension.reactNativeDir.file("ReactAndroid").get().asFile}"
)
"-DREACT_ANDROID_DIR=${extension.reactNativeDir.file("ReactAndroid").get().asFile}")
}
if (cmakeArgs.none { it.startsWith("-DANDROID_STL") }) {
cmakeArgs.add("-DANDROID_STL=c++_shared")
@@ -73,7 +71,7 @@ internal object NdkConfiguratorUtils {
fun configureNewArchPackagingOptions(
project: Project,
extension: ReactExtension,
variant: Variant,
variant: Variant
) {
// We set some packagingOptions { pickFirst ... } for our users for libraries we own.
variant.packaging.jniLibs.pickFirsts.addAll(
@@ -87,8 +85,7 @@ internal object NdkConfiguratorUtils {
"**/libjsi.so",
// AGP will give priority of libc++_shared coming from App modules.
"**/libc++_shared.so",
)
)
))
}
/**
@@ -110,7 +107,7 @@ internal object NdkConfiguratorUtils {
fun getPackagingOptionsForVariant(
hermesEnabled: Boolean,
useThirdPartyJSC: Boolean,
useThirdPartyJSC: Boolean
): Pair<List<String>, List<String>> {
val excludes = mutableListOf<String>()
val includes = mutableListOf<String>()
@@ -120,17 +117,17 @@ internal object NdkConfiguratorUtils {
hermesEnabled -> {
excludes.add("**/libjsc.so")
excludes.add("**/libjsctooling.so")
includes.add("**/libhermesvm.so")
includes.add("**/libhermes.so")
includes.add("**/libhermestooling.so")
}
useThirdPartyJSC -> {
excludes.add("**/libhermesvm.so")
excludes.add("**/libhermes.so")
excludes.add("**/libhermestooling.so")
excludes.add("**/libjsctooling.so")
includes.add("**/libjsc.so")
}
else -> {
excludes.add("**/libhermesvm.so")
excludes.add("**/libhermes.so")
excludes.add("**/libhermestooling.so")
includes.add("**/libjsc.so")
includes.add("**/libjsctooling.so")
@@ -30,8 +30,7 @@ internal fun detectedEntryFile(config: ReactExtension, envVariableOverride: Stri
detectEntryFile(
entryFile = config.entryFile.orNull?.asFile,
reactRoot = config.root.get().asFile,
envVariableOverride = envVariableOverride,
)
envVariableOverride = envVariableOverride)
/**
* Computes the CLI file for React Native. The Algo follows this order:
@@ -43,8 +42,7 @@ internal fun detectedEntryFile(config: ReactExtension, envVariableOverride: Stri
internal fun detectedCliFile(config: ReactExtension): File =
detectCliFile(
reactNativeRoot = config.root.get().asFile,
preconfiguredCliFile = config.cliFile.asFile.orNull,
)
preconfiguredCliFile = config.cliFile.asFile.orNull)
/**
* Computes the `hermesc` command location. The Algo follows this order:
@@ -62,7 +60,7 @@ internal fun detectedHermesCommand(config: ReactExtension): String =
private fun detectEntryFile(
entryFile: File?,
reactRoot: File,
envVariableOverride: String? = null,
envVariableOverride: String? = null
): File =
when {
envVariableOverride != null -> File(reactRoot, envVariableOverride)
@@ -85,8 +83,7 @@ private fun detectCliFile(reactNativeRoot: File, preconfiguredCliFile: File?): F
.exec(
arrayOf("node", "--print", "require.resolve('react-native/cli');"),
emptyArray(),
reactNativeRoot,
)
reactNativeRoot)
val nodeProcessOutput = nodeProcess.inputStream.use { it.bufferedReader().readText().trim() }
@@ -105,14 +102,13 @@ private fun detectCliFile(reactNativeRoot: File, preconfiguredCliFile: File?): F
error(
"""
Couldn't determine CLI location!
Couldn't determine CLI location!
Please set `react { cliFile = file(...) }` inside your
build.gradle to the path of the react-native cli.js file.
This file typically resides in `node_modules/react-native/cli.js`
"""
.trimIndent()
)
Please set `react { cliFile = file(...) }` inside your
build.gradle to the path of the react-native cli.js file.
This file typically resides in `node_modules/react-native/cli.js`
"""
.trimIndent())
}
/**
@@ -161,8 +157,7 @@ internal fun detectOSAwareHermesCommand(projectRoot: File, hermesCommand: String
error(
"Couldn't determine Hermesc location. " +
"Please set `react.hermesCommand` to the path of the hermesc binary file. " +
"node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc"
)
"node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc")
}
/**
@@ -188,8 +183,7 @@ internal fun getHermesOSBin(): String {
if (Os.isLinuxAmd64()) return "linux64-bin"
error(
"OS not recognized. Please set project.react.hermesCommand " +
"to the path of a working Hermes compiler."
)
"to the path of a working Hermes compiler.")
}
internal fun projectPathToLibraryName(projectPath: String): String =
@@ -227,7 +221,7 @@ internal fun findPackageJsonFile(project: Project, rootProperty: DirectoryProper
*/
internal fun readPackageJsonFile(
project: Project,
rootProperty: DirectoryProperty,
rootProperty: DirectoryProperty
): ModelPackageJson? {
val packageJson = findPackageJsonFile(project, rootProperty)
return packageJson?.let { JsonUtils.fromPackageJson(it) }
@@ -23,12 +23,11 @@ class ReactExtensionTest {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0"
}
"""
.trimIndent()
)
{
"reactNativeVersion": "1000.0.0"
}
"""
.trimIndent())
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps).isEmpty()
@@ -39,24 +38,23 @@ class ReactExtensionTest {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react"
}
}
}
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react"
}
}
"""
.trimIndent()
)
}
}
}
"""
.trimIndent())
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps).containsExactly("implementation" to ":react-native_oss-library-example")
@@ -67,25 +65,24 @@ class ReactExtensionTest {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react",
"dependencyConfiguration": "compileOnly"
}
}
}
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react",
"dependencyConfiguration": "compileOnly"
}
}
"""
.trimIndent()
)
}
}
}
"""
.trimIndent())
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps).containsExactly("compileOnly" to ":react-native_oss-library-example")
@@ -96,32 +93,30 @@ class ReactExtensionTest {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react",
"buildTypes": ["debug", "release"]
}
}
}
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react",
"buildTypes": ["debug", "release"]
}
}
"""
.trimIndent()
)
}
}
}
"""
.trimIndent())
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps)
.containsExactly(
"debugImplementation" to ":react-native_oss-library-example",
"releaseImplementation" to ":react-native_oss-library-example",
)
"releaseImplementation" to ":react-native_oss-library-example")
}
@Test
@@ -129,41 +124,39 @@ class ReactExtensionTest {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react"
}
}
},
"@react-native/another-library-for-testing": {
"root": "./node_modules/@react-native/another-library-for-testing",
"name": "@react-native/another-library-for-testing",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react"
}
}
}
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react"
}
}
"""
.trimIndent()
)
},
"@react-native/another-library-for-testing": {
"root": "./node_modules/@react-native/another-library-for-testing",
"name": "@react-native/another-library-for-testing",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react"
}
}
}
}
}
"""
.trimIndent())
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps)
.containsExactly(
"implementation" to ":react-native_oss-library-example",
"implementation" to ":react-native_another-library-for-testing",
)
"implementation" to ":react-native_another-library-for-testing")
}
@Test
@@ -171,27 +164,26 @@ class ReactExtensionTest {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"ios": {
"podspecPath": "./node_modules/@react-native/oss-library-example/oss-library-example.podspec",
"version": "0.0.0",
"configurations": [],
"scriptPhases": []
},
"android": null
}
}
}
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/oss-library-example",
"name": "@react-native/oss-library-example",
"platforms": {
"ios": {
"podspecPath": "./node_modules/@react-native/oss-library-example/oss-library-example.podspec",
"version": "0.0.0",
"configurations": [],
"scriptPhases": []
},
"android": null
}
"""
.trimIndent()
)
}
}
}
"""
.trimIndent())
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps).isEmpty()
@@ -202,35 +194,34 @@ class ReactExtensionTest {
val validJsonFile =
createJsonFile(
"""
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/android-example",
"name": "@react-native/android-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react"
}
}
},
"@react-native/another-library-for-testing": {
"root": "./node_modules/@react-native/cxx-testing",
"name": "@react-native/cxx-testing",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react",
"isPureCxxDependency": true
}
}
}
{
"reactNativeVersion": "1000.0.0",
"dependencies": {
"@react-native/oss-library-example": {
"root": "./node_modules/@react-native/android-example",
"name": "@react-native/android-example",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react"
}
}
"""
.trimIndent()
)
},
"@react-native/another-library-for-testing": {
"root": "./node_modules/@react-native/cxx-testing",
"name": "@react-native/cxx-testing",
"platforms": {
"android": {
"sourceDir": "src/main/java",
"packageImportPath": "com.facebook.react",
"isPureCxxDependency": true
}
}
}
}
}
"""
.trimIndent())
val deps = getGradleDependenciesToApply(validJsonFile)
assertThat(deps).containsExactly("implementation" to ":react-native_android-example")
@@ -37,12 +37,8 @@ class ModelAutolinkingDependenciesJsonTest {
.isEqualTo("react-native_package")
assertThat(
ModelAutolinkingDependenciesJson(
"",
"@this*is~a(more)complicated/example!of~weird)packages",
null,
)
.nameCleansed
)
"", "@this*is~a(more)complicated/example!of~weird)packages", null)
.nameCleansed)
.isEqualTo("this_is_a_more_complicated_example_of_weird_packages")
}
}
@@ -47,8 +47,7 @@ class BundleHermesCTaskTest {
File(rootDir, "file.js"),
File(rootDir, "file.jsx"),
File(rootDir, "file.ts"),
File(rootDir, "file.tsx"),
)
File(rootDir, "file.tsx"))
}
@Test
@@ -72,11 +71,7 @@ class BundleHermesCTaskTest {
assertThat(task.sources.excludes)
.containsExactlyInAnyOrder(
"**/android/**/*",
"**/ios/**/*",
"**/build/**/*",
"**/node_modules/**/*",
)
"**/android/**/*", "**/ios/**/*", "**/build/**/*", "**/node_modules/**/*")
assertThat(task.sources.files.size).isEqualTo(1)
assertThat(task.sources.files).containsExactly(File(rootDir, "afolder/includedfile.js"))
}
@@ -241,8 +236,7 @@ class BundleHermesCTaskTest {
"--minify",
"true",
"--read-global-cache",
"--verbose",
)
"--verbose")
}
@Test
@@ -297,8 +291,7 @@ class BundleHermesCTaskTest {
"--minify",
"true",
"--read-global-cache",
"--verbose",
)
"--verbose")
}
@Test
@@ -348,8 +341,7 @@ class BundleHermesCTaskTest {
"-out",
bytecodeFile.absolutePath,
bundleFile.absolutePath,
"my-custom-hermes-flag",
)
"my-custom-hermes-flag")
}
@Test
@@ -377,8 +369,7 @@ class BundleHermesCTaskTest {
"-out",
bytecodeFile.relativeTo(tempFolder.root).path,
bundleFile.relativeTo(tempFolder.root).path,
"my-custom-hermes-flag",
)
"my-custom-hermes-flag")
}
@Test
@@ -406,8 +397,7 @@ class BundleHermesCTaskTest {
packagerMap.absolutePath,
compilerMap.absolutePath,
"-o",
outputMap.absolutePath,
)
outputMap.absolutePath)
}
@Test
@@ -438,7 +428,6 @@ class BundleHermesCTaskTest {
packagerMap.relativeTo(tempFolder.root).path,
compilerMap.relativeTo(tempFolder.root).path,
"-o",
outputMap.relativeTo(tempFolder.root).path,
)
outputMap.relativeTo(tempFolder.root).path)
}
}
@@ -71,12 +71,8 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest {
root = "./a/directory",
name = "a-dependency",
platforms =
ModelAutolinkingDependenciesPlatformJson(android = null),
)
),
project = null,
)
)
ModelAutolinkingDependenciesPlatformJson(android = null))),
project = null))
assertThat(result).isEmpty()
}
@@ -102,12 +98,8 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest {
root = "./a/directory",
name = "a-dependency",
platforms =
ModelAutolinkingDependenciesPlatformJson(android = android),
)
),
project = null,
)
)
ModelAutolinkingDependenciesPlatformJson(android = android))),
project = null))
assertThat(result).containsExactly(android)
}
@@ -134,8 +126,7 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest {
)
"""
.trimIndent()
)
.trimIndent())
}
@Test
@@ -165,8 +156,7 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest {
another_cxxModule
)
"""
.trimIndent()
)
.trimIndent())
}
@Test
@@ -210,8 +200,7 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest {
} // namespace react
} // namespace facebook
"""
.trimIndent()
)
.trimIndent())
}
@Test
@@ -267,8 +256,7 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest {
} // namespace react
} // namespace facebook
"""
.trimIndent()
)
.trimIndent())
}
@Test
@@ -311,6 +299,5 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest {
cxxModuleCMakeListsPath = "./another/directory/cxx/CMakeLists.txt",
cxxModuleHeaderName = "AnotherCxxModule",
cxxModuleCMakeListsModuleName = "another_cxxModule",
),
)
))
}
@@ -148,8 +148,7 @@ class GenerateCodegenArtifactsTaskTest {
}
}
"""
.trimIndent()
)
.trimIndent())
}
val task =
@@ -178,8 +177,7 @@ class GenerateCodegenArtifactsTaskTest {
}
}
"""
.trimIndent()
)
.trimIndent())
}
val task =
@@ -198,10 +196,10 @@ class GenerateCodegenArtifactsTaskTest {
@Test
fun resolveTaskParameters_withMissingPackageJson_usesGradleOne() {
val task =
createTestTask<GenerateCodegenArtifactsTask> { task ->
task.packageJsonFile.set(File(tempFolder.root, "package.json"))
task.codegenJavaPackageName.set("com.example.test")
task.libraryName.set("a-library-name-from-gradle")
createTestTask<GenerateCodegenArtifactsTask> {
it.packageJsonFile.set(File(tempFolder.root, "package.json"))
it.codegenJavaPackageName.set("com.example.test")
it.libraryName.set("a-library-name-from-gradle")
}
val (libraryName, javaPackageName) = task.resolveTaskParameters()
@@ -86,7 +86,6 @@ class GenerateEntryPointTaskTest {
}
}
"""
.trimIndent()
)
.trimIndent())
}
}
@@ -64,8 +64,7 @@ class GeneratePackageListTaskTest {
// @react-native/another-package
import com.facebook.react.anotherPackage;
"""
.trimIndent()
)
.trimIndent())
}
@Test
@@ -89,8 +88,7 @@ class GeneratePackageListTaskTest {
new APackage(),
new AnotherPackage()
"""
.trimIndent()
)
.trimIndent())
}
@Test
@@ -148,12 +146,8 @@ class GeneratePackageListTaskTest {
root = "./a/directory",
name = "a-dependency",
platforms =
ModelAutolinkingDependenciesPlatformJson(android = null),
)
),
project = null,
)
)
ModelAutolinkingDependenciesPlatformJson(android = null))),
project = null))
assertThat(result)
.isEqualTo(emptyMap<String, ModelAutolinkingDependenciesPlatformAndroidJson>())
}
@@ -180,12 +174,8 @@ class GeneratePackageListTaskTest {
root = "./a/directory",
name = "a-dependency",
platforms =
ModelAutolinkingDependenciesPlatformJson(android = android),
)
),
project = null,
)
)
ModelAutolinkingDependenciesPlatformJson(android = android))),
project = null))
assertThat(result.entries.size).isEqualTo(1)
assertThat(result["a-dependency"]).isEqualTo(android)
}
@@ -199,8 +189,7 @@ class GeneratePackageListTaskTest {
packageImportPath = "import com.facebook.react.aPackage;",
packageInstance = "new APackage()",
buildTypes = emptyList(),
isPureCxxDependency = true,
)
isPureCxxDependency = true)
val result =
task.filterAndroidPackages(
@@ -213,12 +202,8 @@ class GeneratePackageListTaskTest {
root = "./a/directory",
name = "a-pure-cxx-dependency",
platforms =
ModelAutolinkingDependenciesPlatformJson(android = android),
)
),
project = null,
)
)
ModelAutolinkingDependenciesPlatformJson(android = android))),
project = null))
assertThat(result)
.isEqualTo(emptyMap<String, ModelAutolinkingDependenciesPlatformAndroidJson>())
}
@@ -297,8 +282,7 @@ class GeneratePackageListTaskTest {
}
}
"""
.trimIndent()
)
.trimIndent())
}
@Test
@@ -380,8 +364,7 @@ class GeneratePackageListTaskTest {
}
}
"""
.trimIndent()
)
.trimIndent())
}
private val testDependencies =
@@ -405,6 +388,5 @@ class GeneratePackageListTaskTest {
libraryName = "anotherPackage",
componentDescriptors = emptyList(),
cmakeListsPath = "./another/directory/CMakeLists.txt",
),
)
))
}
@@ -26,8 +26,7 @@ class PrepareBoostTaskTest {
assertThatThrownBy { task.taskAction() }
.isInstanceOf(IllegalStateException::class.java)
.hasMessage(
"Cannot query the value of task ':PrepareBoostTask' property 'boostVersion' because it has no value available."
)
"Cannot query the value of task ':PrepareBoostTask' property 'boostVersion' because it has no value available.")
}
@Test
@@ -58,11 +57,11 @@ class PrepareBoostTaskTest {
val boostThirdPartyJniPath = tempFolder.newFolder("boostpath/jni")
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareBoostTask> { task ->
task.boostPath.setFrom(boostpath)
task.boostThirdPartyJniPath.set(boostThirdPartyJniPath)
task.boostVersion.set("1.0.0")
task.outputDir.set(output)
createTestTask<PrepareBoostTask> {
it.boostPath.setFrom(boostpath)
it.boostThirdPartyJniPath.set(boostThirdPartyJniPath)
it.boostVersion.set("1.0.0")
it.outputDir.set(output)
}
File(boostpath, "asm/asm.S").apply {
parentFile.mkdirs()
@@ -79,11 +78,11 @@ class PrepareBoostTaskTest {
val boostThirdPartyJniPath = tempFolder.newFolder("boostpath/jni")
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareBoostTask> { task ->
task.boostPath.setFrom(boostpath)
task.boostThirdPartyJniPath.set(boostThirdPartyJniPath)
task.boostVersion.set("1.0.0")
task.outputDir.set(output)
createTestTask<PrepareBoostTask> {
it.boostPath.setFrom(boostpath)
it.boostThirdPartyJniPath.set(boostThirdPartyJniPath)
it.boostVersion.set("1.0.0")
it.outputDir.set(output)
}
File(boostpath, "boost_1.0.0/boost/config.hpp").apply {
parentFile.mkdirs()
@@ -100,11 +99,11 @@ class PrepareBoostTaskTest {
val boostThirdPartyJniPath = tempFolder.newFolder("boostpath/jni")
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareBoostTask> { task ->
task.boostPath.setFrom(boostpath)
task.boostThirdPartyJniPath.set(boostThirdPartyJniPath)
task.boostVersion.set("1.0.0")
task.outputDir.set(output)
createTestTask<PrepareBoostTask> {
it.boostPath.setFrom(boostpath)
it.boostThirdPartyJniPath.set(boostThirdPartyJniPath)
it.boostVersion.set("1.0.0")
it.outputDir.set(output)
}
File(boostpath, "boost/boost/config.hpp").apply {
parentFile.mkdirs()

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