diff --git a/.circleci/config.yml b/.circleci/config.yml index 931a29901819c9..8ec8f5050344df 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -812,6 +812,10 @@ jobs: - report_bundle_size: platform: android + - store_artifacts: + path: ~/react-native/packages/rn-tester/android/app/build/outputs/apk/ + destination: rntester-apk + # Optionally, run disabled tests - when: condition: << parameters.run_disabled_tests >> @@ -1060,7 +1064,10 @@ jobs: - run: name: Display Environment info - command: npx envinfo@latest + command: | + npm install -g envinfo + envinfo -v + envinfo - restore_cache: keys: @@ -1562,6 +1569,19 @@ jobs: -d "{\"event_type\": \"publish\", \"client_payload\": { \"version\": \"${CIRCLE_TAG:1}\" }}" # END: Stable releases + poll_maven: + docker: + - image: cimg/node:current + resource_class: small + steps: + - checkout_code_with_cache + - run_yarn + - run: + name: Poll Maven for Artifacts + command: | + node scripts/circleci/poll-maven.js + + # ------------------------- # JOBS: Nightly # ------------------------- @@ -1873,6 +1893,9 @@ workflows: - build_hermesc_linux - build_hermes_macos - build_hermesc_windows + - poll_maven: + requires: + - build_and_publish_npm_package package_and_publish_release_dryrun: jobs: diff --git a/Libraries/ReactNative/AppContainer.js b/Libraries/ReactNative/AppContainer.js index cf3f1d0274e8e0..347bda2a2b4109 100644 --- a/Libraries/ReactNative/AppContainer.js +++ b/Libraries/ReactNative/AppContainer.js @@ -17,6 +17,8 @@ import {type EventSubscription} from '../vendor/emitter/EventEmitter'; import {RootTagContext, createRootTag} from './RootTag'; import * as React from 'react'; +const reactDevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; + type Props = $ReadOnly<{| children?: React.Node, fabric?: boolean, @@ -45,9 +47,17 @@ class AppContainer extends React.Component { }; _mainRef: ?React.ElementRef; _subscription: ?EventSubscription = null; + _reactDevToolsAgentListener: ?() => void = null; static getDerivedStateFromError: any = undefined; + mountReactDevToolsOverlays(): void { + const DevtoolsOverlay = require('../Inspector/DevtoolsOverlay').default; + const devtoolsOverlay = ; + + this.setState({devtoolsOverlay}); + } + componentDidMount(): void { if (__DEV__) { if (!this.props.internal_excludeInspector) { @@ -69,13 +79,21 @@ class AppContainer extends React.Component { this.setState({inspector}); }, ); - if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__ != null) { - const DevtoolsOverlay = - require('../Inspector/DevtoolsOverlay').default; - const devtoolsOverlay = ( - + + if (reactDevToolsHook != null) { + if (reactDevToolsHook.reactDevtoolsAgent) { + // In case if this is not the first AppContainer rendered and React DevTools are already attached + this.mountReactDevToolsOverlays(); + return; + } + + this._reactDevToolsAgentListener = () => + this.mountReactDevToolsOverlays(); + + reactDevToolsHook.on( + 'react-devtools', + this._reactDevToolsAgentListener, ); - this.setState({devtoolsOverlay}); } } } @@ -85,6 +103,10 @@ class AppContainer extends React.Component { if (this._subscription != null) { this._subscription.remove(); } + + if (reactDevToolsHook != null && this._reactDevToolsAgentListener != null) { + reactDevToolsHook.off('react-devtools', this._reactDevToolsAgentListener); + } } render(): React.Node { diff --git a/React/Base/RCTBundleURLProvider.h b/React/Base/RCTBundleURLProvider.h index e2e07da0313990..1626b51cb5f1be 100644 --- a/React/Base/RCTBundleURLProvider.h +++ b/React/Base/RCTBundleURLProvider.h @@ -22,7 +22,11 @@ RCT_EXTERN const NSUInteger kRCTBundleURLProviderDefaultPort; RCT_EXTERN void RCTBundleURLProviderAllowPackagerServerAccess(BOOL allowed); #endif -extern NSString *const kRCTPlatformName; // [macOS] +#if !TARGET_OS_OSX // [macOS] +static NSString *const kRCTPlatformName = @"ios"; +#else // [macOS +static NSString *const kRCTPlatformName = @"macos"; +#endif // macOS] // [macOS] @interface RCTBundleURLProvider : NSObject @@ -103,6 +107,7 @@ extern NSString *const kRCTPlatformName; // [macOS] @property (nonatomic, assign) BOOL enableMinification; @property (nonatomic, assign) BOOL enableDev; +@property (nonatomic, assign) BOOL inlineSourceMap; /** * The scheme/protocol used of the packager, the default is the http protocol @@ -127,13 +132,32 @@ extern NSString *const kRCTPlatformName; // [macOS] + (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot packagerHost:(NSString *)packagerHost enableDev:(BOOL)enableDev - enableMinification:(BOOL)enableMinification; + enableMinification:(BOOL)enableMinification + __deprecated_msg( + "Use `jsBundleURLForBundleRoot:packagerHost:enableDev:enableMinification:inlineSourceMap:` instead"); + ++ (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot + packagerHost:(NSString *)packagerHost + packagerScheme:(NSString *)scheme + enableDev:(BOOL)enableDev + enableMinification:(BOOL)enableMinification + modulesOnly:(BOOL)modulesOnly + runModule:(BOOL)runModule + __deprecated_msg( + "Use jsBundleURLForBundleRoot:packagerHost:enableDev:enableMinification:inlineSourceMap:modulesOnly:runModule:` instead"); + ++ (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot + packagerHost:(NSString *)packagerHost + enableDev:(BOOL)enableDev + enableMinification:(BOOL)enableMinification + inlineSourceMap:(BOOL)inlineSourceMap; + (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot packagerHost:(NSString *)packagerHost packagerScheme:(NSString *)scheme enableDev:(BOOL)enableDev enableMinification:(BOOL)enableMinification + inlineSourceMap:(BOOL)inlineSourceMap modulesOnly:(BOOL)modulesOnly runModule:(BOOL)runModule; /** @@ -144,6 +168,17 @@ extern NSString *const kRCTPlatformName; // [macOS] + (NSURL *)resourceURLForResourcePath:(NSString *)path packagerHost:(NSString *)packagerHost scheme:(NSString *)scheme - query:(NSString *)query; + query:(NSString *)query + __deprecated_msg("Use version with queryItems parameter instead"); + +/** + * Given a hostname for the packager and a resource path (including "/"), return the URL to the resource. + * In general, please use the instance method to decide if the packager is running and fallback to the pre-packaged + * resource if it is not: -resourceURLForResourceRoot:resourceName:resourceExtension:offlineBundle: + */ ++ (NSURL *)resourceURLForResourcePath:(NSString *)path + packagerHost:(NSString *)packagerHost + scheme:(NSString *)scheme + queryItems:(NSArray *)queryItems; @end diff --git a/React/Base/RCTBundleURLProvider.mm b/React/Base/RCTBundleURLProvider.mm index ec296d6d1d11d4..b671f7944f491f 100644 --- a/React/Base/RCTBundleURLProvider.mm +++ b/React/Base/RCTBundleURLProvider.mm @@ -15,12 +15,6 @@ const NSUInteger kRCTBundleURLProviderDefaultPort = RCT_METRO_PORT; -#if !TARGET_OS_OSX // [macOS] -NSString *const kRCTPlatformName = @"ios"; -#else // [macOS -NSString *const kRCTPlatformName = @"macos"; -#endif // macOS] - #if RCT_DEV_MENU | RCT_PACKAGER_LOADING_FUNCTIONALITY static BOOL kRCTAllowPackagerAccess = YES; void RCTBundleURLProviderAllowPackagerServerAccess(BOOL allowed) @@ -28,10 +22,12 @@ void RCTBundleURLProviderAllowPackagerServerAccess(BOOL allowed) kRCTAllowPackagerAccess = allowed; } #endif + static NSString *const kRCTPackagerSchemeKey = @"RCT_packager_scheme"; static NSString *const kRCTJsLocationKey = @"RCT_jsLocation"; static NSString *const kRCTEnableDevKey = @"RCT_enableDev"; static NSString *const kRCTEnableMinificationKey = @"RCT_enableMinification"; +static NSString *const kRCTInlineSourceMapKey = @"RCT_inlineSourceMap"; @implementation RCTBundleURLProvider @@ -189,6 +185,7 @@ - (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot fallbackURLProvider:( packagerScheme:[self packagerScheme] enableDev:[self enableDev] enableMinification:[self enableMinification] + inlineSourceMap:[self inlineSourceMap] modulesOnly:NO runModule:YES]; } @@ -201,6 +198,7 @@ - (NSURL *)jsBundleURLForSplitBundleRoot:(NSString *)bundleRoot packagerScheme:[self packagerScheme] enableDev:[self enableDev] enableMinification:[self enableMinification] + inlineSourceMap:[self inlineSourceMap] modulesOnly:YES runModule:NO]; } @@ -240,13 +238,29 @@ - (NSURL *)resourceURLForResourceRoot:(NSString *)root return [[self class] resourceURLForResourcePath:path packagerHost:packagerServerHostPort scheme:packagerServerScheme - query:nil]; + queryItems:nil]; } + (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot packagerHost:(NSString *)packagerHost enableDev:(BOOL)enableDev enableMinification:(BOOL)enableMinification +{ + return [self jsBundleURLForBundleRoot:bundleRoot + packagerHost:packagerHost + packagerScheme:nil + enableDev:enableDev + enableMinification:enableMinification + inlineSourceMap:NO + modulesOnly:NO + runModule:YES]; +} + ++ (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot + packagerHost:(NSString *)packagerHost + enableDev:(BOOL)enableDev + enableMinification:(BOOL)enableMinification + inlineSourceMap:(BOOL)inlineSourceMap { return [self jsBundleURLForBundleRoot:bundleRoot @@ -254,6 +268,7 @@ + (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot packagerScheme:nil enableDev:enableDev enableMinification:enableMinification + inlineSourceMap:inlineSourceMap modulesOnly:NO runModule:YES]; } @@ -265,28 +280,45 @@ + (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot enableMinification:(BOOL)enableMinification modulesOnly:(BOOL)modulesOnly runModule:(BOOL)runModule +{ + return [self jsBundleURLForBundleRoot:bundleRoot + packagerHost:packagerHost + packagerScheme:nil + enableDev:enableDev + enableMinification:enableMinification + inlineSourceMap:NO + modulesOnly:modulesOnly + runModule:runModule]; +} + ++ (NSURL *)jsBundleURLForBundleRoot:(NSString *)bundleRoot + packagerHost:(NSString *)packagerHost + packagerScheme:(NSString *)scheme + enableDev:(BOOL)enableDev + enableMinification:(BOOL)enableMinification + inlineSourceMap:(BOOL)inlineSourceMap + modulesOnly:(BOOL)modulesOnly + runModule:(BOOL)runModule { NSString *path = [NSString stringWithFormat:@"/%@.bundle", bundleRoot]; + BOOL lazy = enableDev; + NSArray *queryItems = @[ + [[NSURLQueryItem alloc] initWithName:@"platform" value:kRCTPlatformName], + [[NSURLQueryItem alloc] initWithName:@"dev" value:enableDev ? @"true" : @"false"], + [[NSURLQueryItem alloc] initWithName:@"minify" value:enableMinification ? @"true" : @"false"], + [[NSURLQueryItem alloc] initWithName:@"inlineSourceMap" value:inlineSourceMap ? @"true" : @"false"], + [[NSURLQueryItem alloc] initWithName:@"modulesOnly" value:modulesOnly ? @"true" : @"false"], + [[NSURLQueryItem alloc] initWithName:@"runModule" value:runModule ? @"true" : @"false"], #ifdef HERMES_BYTECODE_VERSION - NSString *runtimeBytecodeVersion = [NSString stringWithFormat:@"&runtimeBytecodeVersion=%u", HERMES_BYTECODE_VERSION]; -#else - NSString *runtimeBytecodeVersion = @""; + [[NSURLQueryItem alloc] initWithName:@"runtimeBytecodeVersion" value:HERMES_BYTECODE_VERSION], #endif - - // When we support only iOS 8 and above, use queryItems for a better API. - NSString *query = [NSString stringWithFormat:@"platform=%@&dev=%@&minify=%@&modulesOnly=%@&runModule=%@%@", - kRCTPlatformName, // [macOS] - enableDev ? @"true" : @"false", - enableMinification ? @"true" : @"false", - modulesOnly ? @"true" : @"false", - runModule ? @"true" : @"false", - runtimeBytecodeVersion]; + ]; NSString *bundleID = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleIdentifierKey]; if (bundleID) { - query = [NSString stringWithFormat:@"%@&app=%@", query, bundleID]; + queryItems = [queryItems arrayByAddingObject:[[NSURLQueryItem alloc] initWithName:@"app" value:bundleID]]; } - return [[self class] resourceURLForResourcePath:path packagerHost:packagerHost scheme:scheme query:query]; + return [[self class] resourceURLForResourcePath:path packagerHost:packagerHost scheme:scheme queryItems:queryItems]; } + (NSURL *)resourceURLForResourcePath:(NSString *)path @@ -303,6 +335,20 @@ + (NSURL *)resourceURLForResourcePath:(NSString *)path return components.URL; } ++ (NSURL *)resourceURLForResourcePath:(NSString *)path + packagerHost:(NSString *)packagerHost + scheme:(NSString *)scheme + queryItems:(NSArray *)queryItems +{ + NSURLComponents *components = [NSURLComponents componentsWithURL:serverRootWithHostPort(packagerHost, scheme) + resolvingAgainstBaseURL:NO]; + components.path = path; + if (queryItems != nil) { + components.queryItems = queryItems; + } + return components.URL; +} + - (void)updateValue:(id)object forKey:(NSString *)key { [[NSUserDefaults standardUserDefaults] setObject:object forKey:key]; @@ -320,6 +366,11 @@ - (BOOL)enableMinification return [[NSUserDefaults standardUserDefaults] boolForKey:kRCTEnableMinificationKey]; } +- (BOOL)inlineSourceMap +{ + return [[NSUserDefaults standardUserDefaults] boolForKey:kRCTInlineSourceMapKey]; +} + - (NSString *)jsLocation { return [[NSUserDefaults standardUserDefaults] stringForKey:kRCTJsLocationKey]; @@ -349,6 +400,11 @@ - (void)setEnableMinification:(BOOL)enableMinification [self updateValue:@(enableMinification) forKey:kRCTEnableMinificationKey]; } +- (void)setInlineSourceMap:(BOOL)inlineSourceMap +{ + [self updateValue:@(inlineSourceMap) forKey:kRCTInlineSourceMapKey]; +} + - (void)setPackagerScheme:(NSString *)packagerScheme { [self updateValue:packagerScheme forKey:kRCTPackagerSchemeKey]; diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java index 0141e70c3eb69b..8fece5579f943c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerBase.java @@ -20,6 +20,7 @@ import android.graphics.Color; import android.graphics.Typeface; import android.hardware.SensorManager; +import android.os.Build; import android.util.Pair; import android.view.Gravity; import android.view.View; @@ -1098,7 +1099,7 @@ private void reload() { if (!mIsReceiverRegistered) { IntentFilter filter = new IntentFilter(); filter.addAction(getReloadAppAction(mApplicationContext)); - mApplicationContext.registerReceiver(mReloadAppBroadcastReceiver, filter); + compatRegisterReceiver(mApplicationContext, mReloadAppBroadcastReceiver, filter, true); mIsReceiverRegistered = true; } @@ -1214,4 +1215,21 @@ public void setPackagerLocationCustomizer( return mSurfaceDelegateFactory.createSurfaceDelegate(moduleName); } + + /** + * Starting with Android 14, apps and services that target Android 14 and use context-registered + * receivers are required to specify a flag to indicate whether or not the receiver should be + * exported to all other apps on the device: either RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED + * + *

https://developer.android.com/about/versions/14/behavior-changes-14#runtime-receivers-exported + */ + private void compatRegisterReceiver( + Context context, BroadcastReceiver receiver, IntentFilter filter, boolean exported) { + if (Build.VERSION.SDK_INT >= 34 && context.getApplicationInfo().targetSdkVersion >= 34) { + context.registerReceiver( + receiver, filter, exported ? Context.RECEIVER_EXPORTED : Context.RECEIVER_NOT_EXPORTED); + } else { + context.registerReceiver(receiver, filter); + } + } } diff --git a/ReactCommon/jsc/JSCRuntime.cpp b/ReactCommon/jsc/JSCRuntime.cpp index bbc606561ee353..38ebedaf78d23f 100644 --- a/ReactCommon/jsc/JSCRuntime.cpp +++ b/ReactCommon/jsc/JSCRuntime.cpp @@ -300,6 +300,9 @@ class JSCRuntime : public jsi::Runtime { #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_10_0 #define _JSC_NO_ARRAY_BUFFERS #endif +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 160400 +#define _JSC_HAS_INSPECTABLE +#endif #endif #if defined(__MAC_OS_X_VERSION_MIN_REQUIRED) #if __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_11 @@ -396,6 +399,13 @@ JSCRuntime::JSCRuntime(JSGlobalContextRef ctx) stringCounter_(0) #endif { +#ifndef NDEBUG +#ifdef _JSC_HAS_INSPECTABLE + if (__builtin_available(macOS 13.3, iOS 16.4, tvOS 16.4, *)) { + JSGlobalContextSetInspectable(ctx_, true); + } +#endif +#endif } JSCRuntime::~JSCRuntime() { diff --git a/package.json b/package.json index 7c000cf0b524e5..d7f76a79fa666f 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,8 @@ "test-typescript": "dtslint types", "test-typescript-offline": "dtslint --localTs node_modules/typescript/lib types", "bump-all-updated-packages": "node ./scripts/monorepo/bump-all-updated-packages", - "align-package-versions": "node ./scripts/monorepo/align-package-versions.js" + "align-package-versions": "node ./scripts/monorepo/align-package-versions.js", + "trigger-react-native-release": "node ./scripts/trigger-react-native-release.js" }, "peerDependencies": { "react": "18.2.0" @@ -116,7 +117,6 @@ "@react-native-community/cli": "10.2.4", "@react-native-community/cli-platform-android": "10.2.0", "@react-native-community/cli-platform-ios": "10.2.4", - "@react-native-community/cli-tools": "10.1.1", "@react-native/assets": "1.0.0", "@react-native/normalize-color": "2.1.0", "@react-native/polyfills": "2.0.0", diff --git a/packages/rn-tester/RNTesterUnitTests/RCTBundleURLProviderTests.m b/packages/rn-tester/RNTesterUnitTests/RCTBundleURLProviderTests.m index 25a16cc177eab5..427e3f3682bce7 100644 --- a/packages/rn-tester/RNTesterUnitTests/RCTBundleURLProviderTests.m +++ b/packages/rn-tester/RNTesterUnitTests/RCTBundleURLProviderTests.m @@ -27,7 +27,7 @@ URLWithString: [NSString stringWithFormat: - @"http://localhost:8081/%@.bundle?platform=%@&dev=true&minify=false&modulesOnly=false&runModule=true&runtimeBytecodeVersion=%u&app=com.apple.dt.xctest.tool", + @"http://localhost:8081/%@.bundle?platform=%@&dev=true&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&runtimeBytecodeVersion=%u&app=com.apple.dt.xctest.tool", testFile, kRCTPlatformName, // [macOS] HERMES_BYTECODE_VERSION]]; @@ -36,7 +36,7 @@ URLWithString: [NSString stringWithFormat: - @"http://localhost:8081/%@.bundle?platform=%@&dev=true&minify=false&modulesOnly=false&runModule=true&app=com.apple.dt.xctest.tool", + @"http://localhost:8081/%@.bundle?platform=%@&dev=true&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&app=com.apple.dt.xctest.tool", testFile, kRCTPlatformName]]; // [macOS] #endif @@ -49,7 +49,7 @@ URLWithString: [NSString stringWithFormat: - @"http://192.168.1.1:8081/%@.bundle?platform=%@&dev=true&minify=false&modulesOnly=false&runModule=true&runtimeBytecodeVersion=%u&app=com.apple.dt.xctest.tool", + @"http://192.168.1.1:8081/%@.bundle?platform=%@&dev=true&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&runtimeBytecodeVersion=%u&app=com.apple.dt.xctest.tool", testFile, kRCTPlatformName, // [macOS] HERMES_BYTECODE_VERSION]]; @@ -58,7 +58,7 @@ URLWithString: [NSString stringWithFormat: - @"http://192.168.1.1:8081/%@.bundle?platform=%@&dev=true&minify=false&modulesOnly=false&runModule=true&app=com.apple.dt.xctest.tool", + @"http://192.168.1.1:8081/%@.bundle?platform=%@&dev=true&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&app=com.apple.dt.xctest.tool", testFile, kRCTPlatformName]]; // [macOS] #endif diff --git a/scripts/circle-ci-artifacts-utils.js b/scripts/circle-ci-artifacts-utils.js new file mode 100644 index 00000000000000..3a9d9d2a0c83f1 --- /dev/null +++ b/scripts/circle-ci-artifacts-utils.js @@ -0,0 +1,198 @@ +#!/usr/bin/env node +/** + * 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 {exec} = require('shelljs'); + +const util = require('util'); +const asyncRequest = require('request'); +const request = util.promisify(asyncRequest); + +let circleCIHeaders; +let jobs; +let baseTemporaryPath; + +async function initialize(circleCIToken, baseTempPath, branchName) { + console.info('Getting CircleCI information'); + circleCIHeaders = {'Circle-Token': circleCIToken}; + baseTemporaryPath = baseTempPath; + exec(`mkdir -p ${baseTemporaryPath}`); + const pipeline = await _getLastCircleCIPipelineID(branchName); + const packageAndReleaseWorkflow = await _getPackageAndReleaseWorkflow( + pipeline.id, + ); + const testsWorkflow = await _getTestsWorkflow(pipeline.id); + const jobsPromises = [ + _getCircleCIJobs(packageAndReleaseWorkflow.id), + _getCircleCIJobs(testsWorkflow.id), + ]; + + const jobsResults = await Promise.all(jobsPromises); + + jobs = jobsResults.flatMap(j => j); +} + +function baseTmpPath() { + return baseTemporaryPath; +} + +async function _getLastCircleCIPipelineID(branchName) { + const options = { + method: 'GET', + url: 'https://circleci.com/api/v2/project/gh/facebook/react-native/pipeline', + qs: { + branch: branchName, + }, + headers: circleCIHeaders, + }; + + const response = await request(options); + if (response.error) { + throw new Error(error); + } + + const items = JSON.parse(response.body).items; + + if (!items || items.length === 0) { + throw new Error( + 'No pipelines found on this branch. Make sure that the CI has run at least once, successfully', + ); + } + + const lastPipeline = items[0]; + return {id: lastPipeline.id, number: lastPipeline.number}; +} + +async function _getSpecificWorkflow(pipelineId, workflowName) { + const options = { + method: 'GET', + url: `https://circleci.com/api/v2/pipeline/${pipelineId}/workflow`, + headers: circleCIHeaders, + }; + const response = await request(options); + if (response.error) { + throw new Error(error); + } + + const body = JSON.parse(response.body); + let workflow = body.items.find(w => w.name === workflowName); + _throwIfWorkflowNotFound(workflow, workflowName); + return workflow; +} + +function _throwIfWorkflowNotFound(workflow, name) { + if (!workflow) { + throw new Error( + `Can't find a workflow named ${name}. Please check whether that workflow has started.`, + ); + } +} + +async function _getPackageAndReleaseWorkflow(pipelineId) { + return _getSpecificWorkflow(pipelineId, 'package_and_publish_release_dryrun'); +} + +async function _getTestsWorkflow(pipelineId) { + return _getSpecificWorkflow(pipelineId, 'tests'); +} + +async function _getCircleCIJobs(workflowId) { + const options = { + method: 'GET', + url: `https://circleci.com/api/v2/workflow/${workflowId}/job`, + headers: circleCIHeaders, + }; + const response = await request(options); + if (response.error) { + throw new Error(error); + } + + const body = JSON.parse(response.body); + return body.items; +} + +async function _getJobsArtifacts(jobNumber) { + const options = { + method: 'GET', + url: `https://circleci.com/api/v2/project/gh/facebook/react-native/${jobNumber}/artifacts`, + headers: circleCIHeaders, + }; + const response = await request(options); + if (response.error) { + throw new Error(error); + } + + const body = JSON.parse(response.body); + return body.items; +} + +async function _findUrlForJob(jobName, artifactPath) { + const job = jobs.find(j => j.name === jobName); + _throwIfJobIsNull(job); + _throwIfJobIsUnsuccessful(job); + + const artifacts = await _getJobsArtifacts(job.job_number); + return artifacts.find(artifact => artifact.path.indexOf(artifactPath) > -1) + .url; +} + +function _throwIfJobIsNull(job) { + if (!job) { + throw new Error( + `Can't find a job with name ${job.name}. Please verify that it has been executed and that all its dependencies completed successfully.`, + ); + } +} + +function _throwIfJobIsUnsuccessful(job) { + if (job.status !== 'success') { + throw new Error( + `The job ${job.name} status is ${job.status}. We need a 'success' status to proceed with the testing.`, + ); + } +} + +async function artifactURLHermesDebug() { + return _findUrlForJob('build_hermes_macos-Debug', 'hermes-ios-debug.tar.gz'); +} + +async function artifactURLForMavenLocal() { + return _findUrlForJob('build_and_publish_npm_package-2', 'maven-local.zip'); +} + +async function artifactURLForHermesRNTesterAPK(emulatorArch) { + return _findUrlForJob( + 'test_android', + `rntester-apk/hermes/debug/app-hermes-${emulatorArch}-debug.apk`, + ); +} + +async function artifactURLForJSCRNTesterAPK(emulatorArch) { + return _findUrlForJob( + 'test_android', + `rntester-apk/jsc/debug/app-jsc-${emulatorArch}-debug.apk`, + ); +} + +function downloadArtifact(artifactURL, destination) { + exec(`rm -rf ${destination}`); + exec(`curl ${artifactURL} -Lo ${destination}`); +} + +module.exports = { + initialize, + downloadArtifact, + artifactURLForJSCRNTesterAPK, + artifactURLForHermesRNTesterAPK, + artifactURLForMavenLocal, + artifactURLHermesDebug, + baseTmpPath, +}; diff --git a/scripts/circleci/poll-maven.js b/scripts/circleci/poll-maven.js new file mode 100644 index 00000000000000..298909691e4c93 --- /dev/null +++ b/scripts/circleci/poll-maven.js @@ -0,0 +1,72 @@ +/** + * 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 fetch = require('node-fetch'); +const fs = require('fs'); + +const baseMavenRepo = 'https://repo1.maven.org/maven2/com/facebook/react'; +const artifacts = ['react-native-artifacts', 'react-android', 'hermes-android']; +const humanNames = { + 'react-native-artifacts': 'Hermes for iOS', + 'react-android': 'React Native for Android', + 'hermes-android': 'Hermes for Android', +}; +const ping_minutes = 5; +const max_hours = 5; +const ping_interval = ping_minutes * 60 * 1000; // 5 minutes +const max_wait = max_hours * 60 * 60 * 1000; // 5 hours + +const startTime = Date.now(); + +async function pingMaven(artifact, rnVersion) { + const url = `${baseMavenRepo}/${artifact}/${rnVersion}`; + const response = await fetch(url, {method: 'HEAD'}); + if (response.status === 200) { + console.log(`Found artifact for ${humanNames[artifact]}\n`); + return; + } else if (response.status !== 404) { + throw new Error( + `Unexpected response code ${response.status} for ${humanNames[artifact]}`, + ); + } + + const elapsedTime = Date.now() - startTime; + if (elapsedTime > max_wait) { + throw new Error(`${max_hours} hours has passed. Exiting.`); + } + // Wait a bit + console.log( + `${humanNames[artifact]} not available yet. Waiting ${ping_minutes} minutes.\n`, + ); + await new Promise(resolve => setTimeout(resolve, ping_interval)); + await pingMaven(url); +} + +async function main() { + const package = JSON.parse( + fs.readFileSync('packages/react-native/package.json', 'utf8'), + ); + const rnVersion = package.version; + + if (rnVersion === '1000.0.0') { + console.log( + 'We are not on a release branch when a release has been initiated. Exiting.', + ); + return; + } + + console.log(`Checking artifacts for React Native version ${rnVersion}\n`); + + for (const artifact of artifacts) { + console.log(`Start pinging for ${humanNames[artifact]}`); + await pingMaven(artifact, rnVersion); + } +} + +main(); diff --git a/scripts/monorepo/bump-all-updated-packages/bump-utils.js b/scripts/monorepo/bump-all-updated-packages/bump-utils.js new file mode 100644 index 00000000000000..07c57e5313ab27 --- /dev/null +++ b/scripts/monorepo/bump-all-updated-packages/bump-utils.js @@ -0,0 +1,49 @@ +/** + * 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 chalk = require('chalk'); +const {echo, exec} = require('shelljs'); + +const detectPackageUnreleasedChanges = ( + packageRelativePathFromRoot, + packageName, + ROOT_LOCATION, +) => { + const hashOfLastCommitInsidePackage = exec( + `git log -n 1 --format=format:%H -- ${packageRelativePathFromRoot}`, + {cwd: ROOT_LOCATION, silent: true}, + ).stdout.trim(); + + const hashOfLastCommitThatChangedVersion = exec( + `git log -G\\"version\\": --format=format:%H -n 1 -- ${packageRelativePathFromRoot}/package.json`, + {cwd: ROOT_LOCATION, silent: true}, + ).stdout.trim(); + + if (hashOfLastCommitInsidePackage === hashOfLastCommitThatChangedVersion) { + echo( + `\uD83D\uDD0E No changes for package ${chalk.green( + packageName, + )} since last version bump`, + ); + return false; + } else { + echo(`\uD83D\uDCA1 Found changes for ${chalk.yellow(packageName)}:`); + exec( + `git log --pretty=oneline ${hashOfLastCommitThatChangedVersion}..${hashOfLastCommitInsidePackage} ${packageRelativePathFromRoot}`, + { + cwd: ROOT_LOCATION, + }, + ); + echo(); + + return true; + } +}; + +module.exports = detectPackageUnreleasedChanges; diff --git a/scripts/monorepo/bump-all-updated-packages/index.js b/scripts/monorepo/bump-all-updated-packages/index.js index 09291c0d723423..2501c86461c4d8 100644 --- a/scripts/monorepo/bump-all-updated-packages/index.js +++ b/scripts/monorepo/bump-all-updated-packages/index.js @@ -24,6 +24,7 @@ const { const forEachPackage = require('../for-each-package'); const checkForGitChanges = require('../check-for-git-changes'); const bumpPackageVersion = require('./bump-package-version'); +const detectPackageUnreleasedChanges = require('./bump-utils'); const ROOT_LOCATION = path.join(__dirname, '..', '..', '..'); @@ -61,35 +62,16 @@ const buildExecutor = return; } - const hashOfLastCommitInsidePackage = exec( - `git log -n 1 --format=format:%H -- ${packageRelativePathFromRoot}`, - {cwd: ROOT_LOCATION, silent: true}, - ).stdout.trim(); - - const hashOfLastCommitThatChangedVersion = exec( - `git log -G\\"version\\": --format=format:%H -n 1 -- ${packageRelativePathFromRoot}/package.json`, - {cwd: ROOT_LOCATION, silent: true}, - ).stdout.trim(); - - if (hashOfLastCommitInsidePackage === hashOfLastCommitThatChangedVersion) { - echo( - `\uD83D\uDD0E No changes for package ${chalk.green( - packageName, - )} since last version bump`, - ); - + if ( + !detectPackageUnreleasedChanges( + packageRelativePathFromRoot, + packageName, + ROOT_LOCATION, + ) + ) { return; } - echo(`\uD83D\uDCA1 Found changes for ${chalk.yellow(packageName)}:`); - exec( - `git log --pretty=oneline ${hashOfLastCommitThatChangedVersion}..${hashOfLastCommitInsidePackage} ${packageRelativePathFromRoot}`, - { - cwd: ROOT_LOCATION, - }, - ); - echo(); - await inquirer .prompt([ { @@ -133,9 +115,18 @@ const buildExecutor = const buildAllExecutors = () => { const executors = []; - forEachPackage((...params) => { - executors.push(buildExecutor(...params)); - }); + forEachPackage( + (...params) => { + executors.push(buildExecutor(...params)); + }, + [ + 'assets', + 'eslint-config-react-native-community', + 'eslint-plugin-react-native-community', + 'normalize-color', + 'polyfills', + ], + ); return executors; }; diff --git a/scripts/monorepo/for-each-package.js b/scripts/monorepo/for-each-package.js index 10991b5936da1d..2118139c1ac705 100644 --- a/scripts/monorepo/for-each-package.js +++ b/scripts/monorepo/for-each-package.js @@ -34,14 +34,16 @@ const getDirectories = source => */ /** - * Iterate through every package inside /packages (ignoring react-native) and call provided callback for each of them + * Iterate through every package inside /packages (ignoring react-native and any additional excluded packages) and call provided callback for each of them * - * @param {forEachPackageCallback} callback The callback which will be called for each package + * @param {forEachPackageCallback} callback - The callback which will be called for each package + * @param {string[]} [additionalExcludes] - Additional packages to exclude */ -const forEachPackage = callback => { - // We filter react-native package on purpose, so that no CI's script will be executed for this package in future +const forEachPackage = (callback, additionalExcludes = []) => { + const packagesToExclude = [...PACKAGES_BLOCK_LIST, ...additionalExcludes]; + const packagesDirectories = getDirectories(PACKAGES_LOCATION).filter( - directoryName => !PACKAGES_BLOCK_LIST.includes(directoryName), + directoryName => !packagesToExclude.includes(directoryName), ); packagesDirectories.forEach(packageDirectory => { diff --git a/scripts/npm-utils.js b/scripts/npm-utils.js new file mode 100644 index 00000000000000..effeb4ce33c0b1 --- /dev/null +++ b/scripts/npm-utils.js @@ -0,0 +1,41 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +/** + * `package` is an object form of package.json + * `dependencies` is a map of dependency to version string + * + * This replaces both dependencies and devDependencies in package.json + */ +function applyPackageVersions(originalPackageJson, packageVersions) { + const packageJson = {...originalPackageJson}; + + for (const name of Object.keys(packageVersions)) { + if ( + packageJson.dependencies != null && + packageJson.dependencies[name] != null + ) { + packageJson.dependencies[name] = packageVersions[name]; + } + + if ( + packageJson.devDependencies != null && + packageJson.devDependencies[name] != null + ) { + packageJson.devDependencies[name] = packageVersions[name]; + } + } + return packageJson; +} + +module.exports = { + applyPackageVersions, +}; diff --git a/scripts/publish-npm.js b/scripts/publish-npm.js index a3211953091d85..102893144443ea 100755 --- a/scripts/publish-npm.js +++ b/scripts/publish-npm.js @@ -112,7 +112,7 @@ const rawVersion = : // For nightly we continue to use 0.0.0 for clarity for npm nightlyBuild ? '0.0.0' - : // For pre-release and stable releases, we use the git tag of the version we're releasing (set in set-rn-version) + : // For pre-release and stable releases, we use the git tag of the version we're releasing (set in trigger-react-native-release) getPkgJsonVersion(); // [macOS] We can't use the CircleCI build tag, so we use the version argument instead. let version, diff --git a/scripts/release-utils.js b/scripts/release-utils.js index 9cb69c4c5ea1d9..cc5764749c3ab9 100644 --- a/scripts/release-utils.js +++ b/scripts/release-utils.js @@ -109,11 +109,6 @@ function generateiOSArtifacts( ) { pushd(`${hermesCoreSourceFolder}`); - //Need to generate hermesc - exec( - `${hermesCoreSourceFolder}/utils/build-hermesc-xcode.sh ${hermesCoreSourceFolder}/build_host_hermesc`, - ); - //Generating iOS Artifacts exec( `JSI_PATH=${jsiFolder} BUILD_TYPE=${buildType} ${hermesCoreSourceFolder}/utils/build-mac-framework.sh`, diff --git a/scripts/test-e2e-local-clean.js b/scripts/test-e2e-local-clean.js index 37ca37032732b5..6dd85f5bba7c8b 100644 --- a/scripts/test-e2e-local-clean.js +++ b/scripts/test-e2e-local-clean.js @@ -44,6 +44,7 @@ if (isPackagerRunning() === 'running') { console.info('\n** Cleaning Gradle build artifacts **\n'); exec('./gradlew cleanAll'); exec('rm -rf /tmp/maven-local'); +exec('rm -rf /tmp/react-native-tmp'); // iOS console.info('\n** Nuking the derived data folder **\n'); @@ -56,9 +57,6 @@ exec('rm -rf ~/Library/Caches/CocoaPods/Pods/External/hermes-engine'); console.info('\n** Removing the RNTester Pods **\n'); exec('rm -rf packages/rn-tester/Pods'); -// I'm not sure we want to also remove the lock file -// exec('rm -rf packages/rn-tester/Podfile.lock'); - // RNTestProject console.info('\n** Removing the RNTestProject folder **\n'); exec('rm -rf /tmp/RNTestProject'); diff --git a/scripts/test-e2e-local.js b/scripts/test-e2e-local.js index afae78ea83e6be..775e7e79ecd2b6 100644 --- a/scripts/test-e2e-local.js +++ b/scripts/test-e2e-local.js @@ -16,29 +16,21 @@ * and to make it more accessible for other devs to play around with. */ -const {exec, exit, pushd, popd, pwd, cd, cp} = require('shelljs'); +const {exec, pushd, popd, pwd, cd} = require('shelljs'); +const updateTemplatePackage = require('./update-template-package'); const yargs = require('yargs'); -const fs = require('fs'); const path = require('path'); +const fs = require('fs'); const os = require('os'); const { - launchAndroidEmulator, - isPackagerRunning, + checkPackagerRunning, + maybeLaunchAndroidEmulator, launchPackagerInSeparateWindow, + setupCircleCIArtifacts, + prepareArtifacts, } = require('./testing-utils'); -const { - generateAndroidArtifacts, - saveFilesToRestore, - generateiOSArtifacts, -} = require('./release-utils'); - -const { - downloadHermesSourceTarball, - expandHermesSourceTarball, -} = require('./hermes/hermes-utils'); - const argv = yargs .option('t', { alias: 'target', @@ -54,111 +46,152 @@ const argv = yargs alias: 'hermes', type: 'boolean', default: true, + }) + .option('c', { + alias: 'circleciToken', + type: 'string', }).argv; -/* - * see the test-local-e2e.js script for clean up process +// === RNTester === // + +/** + * Start the test for RNTester on iOS. + * + * Parameters: + * - @circleCIArtifacts manager object to manage all the download of CircleCIArtifacts. If null, it will fallback not to use them. + * - @onReleaseBranch whether we are on a release branch or not */ +async function testRNTesterIOS(circleCIArtifacts, onReleaseBranch) { + console.info( + `We're going to test the ${ + argv.hermes ? 'Hermes' : 'JSC' + } version of RNTester iOS with the new Architecture enabled`, + ); -// command order: we ask the user to select if they want to test RN tester -// or RNTestProject + // remember that for this to be successful + // you should have run bundle install once + // in your local setup + if (argv.hermes && circleCIArtifacts != null) { + const hermesURL = await circleCIArtifacts.artifactURLHermesDebug(); + const hermesPath = path.join( + circleCIArtifacts.baseTmpPath(), + 'hermes-ios-debug.tar.gz', + ); + // download hermes source code from manifold + circleCIArtifacts.downloadArtifact(hermesURL, hermesPath); + console.info(`Downloaded Hermes in ${hermesPath}`); + exec( + `HERMES_ENGINE_TARBALL_PATH=${hermesPath} RCT_NEW_ARCH_ENABLED=1 bundle exec pod install --ansi`, + ); + } else { + exec( + `USE_HERMES=${ + argv.hermes ? 1 : 0 + } REACT_NATIVE_CI=${onReleaseBranch} RCT_NEW_ARCH_ENABLED=1 bundle exec pod install --ansi`, + ); + } -// if they select RN tester, we ask if iOS or Android, and then we run the tests -// if they select RNTestProject, we run the RNTestProject test + // if everything succeeded so far, we can launch Metro and the app + // start the Metro server in a separate window + launchPackagerInSeparateWindow(pwd()); -// let's check if Metro is already running, if it is let's kill it and start fresh -if (isPackagerRunning() === 'running') { - exec("lsof -i :8081 | grep LISTEN | /usr/bin/awk '{print $2}' | xargs kill"); + // launch the app on iOS simulator + exec('npx react-native run-ios --scheme RNTester --simulator "iPhone 14"'); } -const onReleaseBranch = exec('git rev-parse --abbrev-ref HEAD', { - silent: true, -}) - .stdout.trim() - .endsWith('-stable'); +/** + * Start the test for RNTester on Android. + * + * Parameters: + * - @circleCIArtifacts manager object to manage all the download of CircleCIArtifacts. If null, it will fallback not to use them. + */ +async function testRNTesterAndroid(circleCIArtifacts) { + maybeLaunchAndroidEmulator(); -if (argv.target === 'RNTester') { - // FIXME: make sure that the commands retains colors - // (--ansi) doesn't always work - // see also https://github.com/shelljs/shelljs/issues/86 + console.info( + `We're going to test the ${ + argv.hermes ? 'Hermes' : 'JSC' + } version of RNTester Android with the new Architecture enabled`, + ); - if (argv.platform === 'iOS') { - console.info( - `We're going to test the ${ - argv.hermes ? 'Hermes' : 'JSC' - } version of RNTester iOS with the new Architecture enabled`, - ); + // Start the Metro server so it will be ready if the app can be built and installed successfully. + launchPackagerInSeparateWindow(pwd()); - // remember that for this to be successful - // you should have run bundle install once - // in your local setup - also: if I'm on release branch, I pick the - // hermes ref from the hermes ref file (see hermes-engine.podspec) - exec( - `cd packages/rn-tester && USE_HERMES=${ - argv.hermes ? 1 : 0 - } REACT_NATIVE_CI=${onReleaseBranch} RCT_NEW_ARCH_ENABLED=1 bundle exec pod install --ansi`, - ); + // Wait for the Android Emulator to be properly loaded and bootstrapped + exec( + "adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done; input keyevent 82'", + ); - // if everything succeeded so far, we can launch Metro and the app - // start the Metro server in a separate window - launchPackagerInSeparateWindow(); + if (circleCIArtifacts != null) { + const downloadPath = path.join( + circleCIArtifacts.baseTmpPath(), + 'rntester.apk', + ); - // launch the app on iOS simulator - pushd('packages/rn-tester'); - exec('npx react-native run-ios --scheme RNTester --simulator "iPhone 14"'); - popd(); - } else { - // we do the android path here + const emulatorArch = exec('adb shell getprop ro.product.cpu.abi').trim(); + const rntesterAPKURL = argv.hermes + ? await circleCIArtifacts.artifactURLForHermesRNTesterAPK(emulatorArch) + : await circleCIArtifacts.artifactURLForJSCRNTesterAPK(emulatorArch); - launchAndroidEmulator(); + console.info('Start Downloading APK'); + circleCIArtifacts.downloadArtifact(rntesterAPKURL, downloadPath); - console.info( - `We're going to test the ${ - argv.hermes ? 'Hermes' : 'JSC' - } version of RNTester Android with the new Architecture enabled`, - ); + exec(`adb install ${downloadPath}`); + } else { exec( - `./gradlew :packages:rn-tester:android:app:${ + `../../gradlew :packages:rn-tester:android:app:${ argv.hermes ? 'installHermesDebug' : 'installJscDebug' } --quiet`, ); + } - // launch the app on Android simulator - // TODO: we should find a way to make it work like for iOS, via npx react-native run-android - // currently, that fails with an error. + // launch the app + // TODO: we should find a way to make it work like for iOS, via npx react-native run-android + // currently, that fails with an error. + exec( + 'adb shell am start -n com.facebook.react.uiapp/com.facebook.react.uiapp.RNTesterActivity', + ); - // if everything succeeded so far, we can launch Metro and the app - // start the Metro server in a separate window - launchPackagerInSeparateWindow(); + // just to make sure that the Android up won't have troubles finding the Metro server + exec('adb reverse tcp:8081 tcp:8081'); +} - // launch the app - exec( - 'adb shell am start -n com.facebook.react.uiapp/com.facebook.react.uiapp.RNTesterActivity', - ); +/** + * Function that start testing on RNTester. + * + * Parameters: + * - @circleCIArtifacts manager object to manage all the download of CircleCIArtifacts. If null, it will fallback not to use them. + * - @onReleaseBranch whether we are on a release branch or not + */ +async function testRNTester(circleCIArtifacts, onReleaseBranch) { + // FIXME: make sure that the commands retains colors + // (--ansi) doesn't always work + // see also https://github.com/shelljs/shelljs/issues/86 + pushd('packages/rn-tester'); - // just to make sure that the Android up won't have troubles finding the Metro server - exec('adb reverse tcp:8081 tcp:8081'); + if (argv.platform === 'iOS') { + await testRNTesterIOS(circleCIArtifacts, onReleaseBranch); + } else { + await testRNTesterAndroid(circleCIArtifacts); } -} else { + popd(); +} + +// === RNTestProject === // + +async function testRNTestProject(circleCIArtifacts) { console.info("We're going to test a fresh new RN project"); // create the local npm package to feed the CLI // base setup required (specular to publish-npm.js) - const tmpPublishingFolder = fs.mkdtempSync( - path.join(os.tmpdir(), 'rn-publish-'), - ); - console.info(`The temp publishing folder is ${tmpPublishingFolder}`); - - saveFilesToRestore(tmpPublishingFolder); - - // we need to add the unique timestamp to avoid npm/yarn to use some local caches const baseVersion = require('../package.json').version; // in local testing, 1000.0.0 mean we are on main, every other case means we are // working on a release version const buildType = baseVersion !== '1000.0.0' ? 'release' : 'dry-run'; + // we need to add the unique timestamp to avoid npm/yarn to use some local caches const dateIdentifier = new Date() .toISOString() .slice(0, -8) @@ -167,59 +200,44 @@ if (argv.target === 'RNTester') { const releaseVersion = `${baseVersion}-${dateIdentifier}`; - // this is needed to generate the Android artifacts correctly - const exitCode = exec( - `node scripts/set-rn-version.js --to-version ${releaseVersion} --build-type ${buildType}`, - ).code; - - if (exitCode !== 0) { - console.error( - `Failed to set the RN version. Version ${releaseVersion} is not valid for ${buildType}`, - ); - process.exit(exitCode); - } - - // Generate native files for Android - generateAndroidArtifacts(releaseVersion, tmpPublishingFolder); - - // Setting up generating native iOS (will be done later) + // Prepare some variables for later use const repoRoot = pwd(); - const jsiFolder = `${repoRoot}/ReactCommon/jsi`; - const hermesCoreSourceFolder = `${repoRoot}/sdks/hermes`; - - if (!fs.existsSync(hermesCoreSourceFolder)) { - console.info('The Hermes source folder is missing. Downloading...'); - downloadHermesSourceTarball(); - expandHermesSourceTarball(); - } - - // need to move the scripts inside the local hermes cloned folder - // cp sdks/hermes-engine/utils/*.sh /utils/. - cp( - `${repoRoot}/sdks/hermes-engine/utils/*.sh`, - `${repoRoot}/sdks/hermes/utils/.`, + const reactNativePackagePath = `${repoRoot}`; + const localNodeTGZPath = `${reactNativePackagePath}/react-native-${releaseVersion}.tgz`; + + const mavenLocalPath = + circleCIArtifacts != null + ? path.join(circleCIArtifacts.baseTmpPath(), 'maven-local.zip') + : '/private/tmp/maven-local'; + const hermesPath = await prepareArtifacts( + circleCIArtifacts, + mavenLocalPath, + localNodeTGZPath, + releaseVersion, + buildType, + reactNativePackagePath, ); - // for this scenario, we only need to create the debug build - // (env variable PRODUCTION defines that podspec side) - const buildTypeiOSArtifacts = 'Debug'; - - // the android ones get set into /private/tmp/maven-local - const localMavenPath = '/private/tmp/maven-local'; - - // Generate native files for iOS - const tarballOutputPath = generateiOSArtifacts( - jsiFolder, - hermesCoreSourceFolder, - buildTypeiOSArtifacts, - localMavenPath, - ); - - const localNodeTGZPath = `${repoRoot}/react-native-${releaseVersion}.tgz`; - exec(`node scripts/set-rn-template-version.js "file:${localNodeTGZPath}"`); + updateTemplatePackage({ + 'react-native': `file:${localNodeTGZPath}`, + }); // create locally the node module - exec('npm pack'); + exec('npm pack --pack-destination ', {cwd: reactNativePackagePath}); + + // node pack does not creates a version of React Native with the right name on main. + // Let's add some defensive programming checks: + if (!fs.existsSync(localNodeTGZPath)) { + const tarfile = fs + .readdirSync(reactNativePackagePath) + .find(name => name.startsWith('react-native-') && name.endsWith('.tgz')); + if (!tarfile) { + throw new Error("Couldn't find a zipped version of react-native"); + } + exec( + `cp ${path.join(reactNativePackagePath, tarfile)} ${localNodeTGZPath}`, + ); + } pushd('/tmp/'); // need to avoid the pod install step - we'll do it later @@ -232,14 +250,14 @@ if (argv.target === 'RNTester') { // need to do this here so that Android will be properly setup either way exec( - 'echo "REACT_NATIVE_MAVEN_LOCAL_REPO=/private/tmp/maven-local" >> android/gradle.properties', + `echo "REACT_NATIVE_MAVEN_LOCAL_REPO=${mavenLocalPath}" >> android/gradle.properties`, ); // doing the pod install here so that it's easier to play around RNTestProject cd('ios'); exec('bundle install'); exec( - `HERMES_ENGINE_TARBALL_PATH=${tarballOutputPath} USE_HERMES=${ + `HERMES_ENGINE_TARBALL_PATH=${hermesPath} USE_HERMES=${ argv.hermes ? 1 : 0 } bundle exec pod install --ansi`, ); @@ -255,7 +273,37 @@ if (argv.target === 'RNTester') { popd(); // just cleaning up the temp folder, the rest is done by the test clean script - exec(`rm -rf ${tmpPublishingFolder}`); + exec(`rm -rf /tmp/react-native-tmp`); +} + +async function main() { + /* + * see the test-local-e2e.js script for clean up process + */ + + // command order: we ask the user to select if they want to test RN tester + // or RNTestProject + + // if they select RN tester, we ask if iOS or Android, and then we run the tests + // if they select RNTestProject, we run the RNTestProject test + + checkPackagerRunning(); + + const branchName = exec('git rev-parse --abbrev-ref HEAD', { + silent: true, + }).stdout.trim(); + const onReleaseBranch = branchName.endsWith('-stable'); + + let circleCIArtifacts = await setupCircleCIArtifacts( + argv.circleciToken, + branchName, + ); + + if (argv.target === 'RNTester') { + await testRNTester(circleCIArtifacts, onReleaseBranch); + } else { + await testRNTestProject(circleCIArtifacts); + } } -exit(0); +main(); diff --git a/scripts/testing-utils.js b/scripts/testing-utils.js index a687a37cd562ca..636bf9cfa1351b 100644 --- a/scripts/testing-utils.js +++ b/scripts/testing-utils.js @@ -9,9 +9,23 @@ 'use strict'; -const {exec} = require('shelljs'); +const {exec, cp} = require('shelljs'); +const fs = require('fs'); const os = require('os'); const {spawn} = require('node:child_process'); +const path = require('path'); + +const circleCIArtifactsUtils = require('./circle-ci-artifacts-utils.js'); + +const { + generateAndroidArtifacts, + generateiOSArtifacts, +} = require('./release-utils'); + +const { + downloadHermesSourceTarball, + expandHermesSourceTarball, +} = require('./hermes/hermes-utils.js'); /* * Android related utils - leverages android tooling @@ -35,12 +49,12 @@ const launchEmulator = emulatorName => { // from docs: "When using the detached option to start a long-running process, the process will not stay running in the background after the parent exits unless it is provided with a stdio configuration that is not connected to the parent. If the parent's stdio is inherited, the child will remain attached to the controlling terminal." // here: https://nodejs.org/api/child_process.html#optionsdetached - const cp = spawn(emulatorCommand, [`@${emulatorName}`], { + const child_process = spawn(emulatorCommand, [`@${emulatorName}`], { detached: true, stdio: 'ignore', }); - cp.unref(); + child_process.unref(); }; function tryLaunchEmulator() { @@ -60,7 +74,20 @@ function tryLaunchEmulator() { }; } -function launchAndroidEmulator() { +function hasConnectedDevice() { + const physicalDevices = exec('adb devices | grep -v emulator', {silent: true}) + .stdout.trim() + .split('\n') + .slice(1); + return physicalDevices.length > 0; +} + +function maybeLaunchAndroidEmulator() { + if (hasConnectedDevice()) { + console.info('Already have a device connected. Skip launching emulator.'); + return; + } + const result = tryLaunchEmulator(); if (result.success) { console.info('Successfully launched emulator.'); @@ -97,15 +124,157 @@ function isPackagerRunning( } // this is a very limited implementation of how this should work -// literally, this is macos only -// a more robust implementation can be found here: -// https://github.com/react-native-community/cli/blob/7c003f2b1d9d80ec5c167614ba533a004272c685/packages/cli-platform-android/src/commands/runAndroid/index.ts#L195 -function launchPackagerInSeparateWindow() { - exec("open -a 'Terminal' ./scripts/packager.sh"); +function launchPackagerInSeparateWindow(folderPath) { + const command = `tell application "Terminal" to do script "cd ${folderPath} && yarn start"`; + exec(`osascript -e '${command}' >/dev/null </utils/. + cp( + `${reactNativePackagePath}/sdks/hermes-engine/utils/*.sh`, + `${reactNativePackagePath}/sdks/hermes/utils/.`, + ); + + // for this scenario, we only need to create the debug build + // (env variable PRODUCTION defines that podspec side) + const buildTypeiOSArtifacts = 'Debug'; + + // the android ones get set into /private/tmp/maven-local + const localMavenPath = '/private/tmp/maven-local'; + + // Generate native files for iOS + const hermesPath = generateiOSArtifacts( + jsiFolder, + hermesCoreSourceFolder, + buildTypeiOSArtifacts, + localMavenPath, + ); + + return hermesPath; +} + +/** + * It prepares the artifacts required to run a new project created from the template + * + * Parameters: + * - @circleCIArtifacts manager object to manage all the download of CircleCIArtifacts. If null, it will fallback not to use them. + * - @mavenLocalPath path to the local maven repo that is needed by Android. + * - @localNodeTGZPath path where we want to store the react-native tgz. + * - @releaseVersion the version that is about to be released. + * - @buildType the type of build we want to execute if we build locally. + * - @reactNativePackagePath the path to the react native package within the repo. + * + * Returns: + * - @hermesPath the path to hermes for iOS + */ +async function prepareArtifacts( + circleCIArtifacts, + mavenLocalPath, + localNodeTGZPath, + releaseVersion, + buildType, + reactNativePackagePath, +) { + return circleCIArtifacts != null + ? await downloadArtifactsFromCircleCI( + circleCIArtifacts, + mavenLocalPath, + localNodeTGZPath, + ) + : buildArtifactsLocally(releaseVersion, buildType, reactNativePackagePath); } module.exports = { - launchAndroidEmulator, + checkPackagerRunning, + maybeLaunchAndroidEmulator, isPackagerRunning, launchPackagerInSeparateWindow, + setupCircleCIArtifacts, + prepareArtifacts, }; diff --git a/scripts/bump-oss-version.js b/scripts/trigger-react-native-release.js old mode 100755 new mode 100644 similarity index 63% rename from scripts/bump-oss-version.js rename to scripts/trigger-react-native-release.js index bf8c3d8e91394a..f1f79489686714 --- a/scripts/bump-oss-version.js +++ b/scripts/trigger-react-native-release.js @@ -14,14 +14,21 @@ * This script walks a releaser through bumping the version for a release * It will commit the appropriate tags to trigger the CircleCI jobs. */ -const {exit} = require('shelljs'); +const {exit, echo} = require('shelljs'); +const chalk = require('chalk'); const yargs = require('yargs'); const inquirer = require('inquirer'); const request = require('request'); +const path = require('path'); const {getBranchName, exitIfNotOnGit} = require('./scm-utils'); const {parseVersion, isReleaseBranch} = require('./version-utils'); const {failIfTagExists} = require('./release-utils'); +const checkForGitChanges = require('./monorepo/check-for-git-changes'); +const forEachPackage = require('./monorepo/for-each-package'); +const detectPackageUnreleasedChanges = require('./monorepo/bump-all-updated-packages/bump-utils.js'); + +const ROOT_LOCATION = path.join(__dirname, '..'); let argv = yargs .option('r', { @@ -42,7 +49,7 @@ let argv = yargs .check(() => { const branch = exitIfNotOnGit( () => getBranchName(), - "Not in git. You can't invoke bump-oss-versions.js from outside a git repo.", + "Not in git. You can't invoke trigger-react-native-release from outside a git repo.", ); exitIfNotOnReleaseBranch(branch); return true; @@ -57,6 +64,61 @@ function exitIfNotOnReleaseBranch(branch) { } } +const buildExecutor = + (packageAbsolutePath, packageRelativePathFromRoot, packageManifest) => + async () => { + const {name: packageName} = packageManifest; + if (packageManifest.private) { + return; + } + + if ( + detectPackageUnreleasedChanges( + packageRelativePathFromRoot, + packageName, + ROOT_LOCATION, + ) + ) { + // if I enter here, I want to throw an error upward + throw new Error( + `Package ${packageName} has unreleased changes. Please release it first.`, + ); + } + }; + +const buildAllExecutors = () => { + const executors = []; + + forEachPackage( + (...params) => { + executors.push(buildExecutor(...params)); + }, + [ + 'assets', + 'eslint-config-react-native-community', + 'eslint-plugin-react-native-community', + 'normalize-color', + 'polyfills', + ], + ); + + return executors; +}; + +async function exitIfUnreleasedPackages() { + // use the other script to verify that there's no packages in the monorepo + // that have changes that haven't been released + + const executors = buildAllExecutors(); + for (const executor of executors) { + await executor().catch(error => { + echo(chalk.red(error)); + // need to throw upward + throw error; + }); + } +} + function triggerReleaseWorkflow(options) { return new Promise((resolve, reject) => { request(options, function (error, response, body) { @@ -72,8 +134,26 @@ function triggerReleaseWorkflow(options) { async function main() { const branch = exitIfNotOnGit( () => getBranchName(), - "Not in git. You can't invoke bump-oss-versions.js from outside a git repo.", + "Not in git. You can't invoke trigger-react-native-release from outside a git repo.", ); + + // check for uncommitted changes + if (checkForGitChanges()) { + echo( + chalk.red( + 'Found uncommitted changes. Please commit or stash them before running this script', + ), + ); + exit(1); + } + + // now check for unreleased packages + try { + await exitIfUnreleasedPackages(); + } catch (error) { + exit(1); + } + const token = argv.token; const releaseVersion = argv.toVersion; failIfTagExists(releaseVersion, 'release'); diff --git a/scripts/update-template-package.js b/scripts/update-template-package.js new file mode 100644 index 00000000000000..77f0b74ecc5397 --- /dev/null +++ b/scripts/update-template-package.js @@ -0,0 +1,51 @@ +/** + * 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 fs = require('fs'); +const path = require('path'); +const {applyPackageVersions} = require('./npm-utils'); + +/** + * Updates the react-native template package.json with + * dependencies in `dependencyMap`. + * + * `dependencyMap` is a dict of package name to its version + * ex. {"react-native": "0.23.0", "other-dep": "nightly"} + */ +function updateTemplatePackage(dependencyMap) { + const jsonPath = path.join(__dirname, '../template/package.json'); + const templatePackageJson = require(jsonPath); + + const updatedPackageJson = applyPackageVersions( + templatePackageJson, + dependencyMap, + ); + + fs.writeFileSync( + jsonPath, + JSON.stringify(updatedPackageJson, null, 2) + '\n', + 'utf-8', + ); +} + +if (require.main === module) { + const dependencyMapStr = process.argv[2]; + if (!dependencyMapStr) { + console.error( + 'Please provide a json string of package name and their version. Ex. \'{"packageName":"0.23.0"}\'', + ); + process.exit(1); + } + + updateTemplatePackage(JSON.parse(dependencyMapStr)); +} + +module.exports = updateTemplatePackage; diff --git a/yarn.lock b/yarn.lock index 5b8b9e86237352..bf6ca93d7d020a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1684,7 +1684,7 @@ serve-static "^1.13.1" ws "^7.5.1" -"@react-native-community/cli-tools@10.1.1", "@react-native-community/cli-tools@^10.1.1": +"@react-native-community/cli-tools@^10.1.1": version "10.1.1" resolved "https://registry.yarnpkg.com/@react-native-community/cli-tools/-/cli-tools-10.1.1.tgz#fa66e509c0d3faa31f7bb87ed7d42ad63f368ddd" integrity sha512-+FlwOnZBV+ailEzXjcD8afY2ogFEBeHOw/8+XXzMgPaquU2Zly9B+8W089tnnohO3yfiQiZqkQlElP423MY74g==