diff --git a/package.json b/package.json index 2970a6212e1eca..339ed581fde6ae 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "rn-get-polyfills.js", "scripts/compose-source-maps.js", "scripts/find-node-for-xcode.sh", - "scripts/generate-artifacts.js", + "scripts/generate-codegen-artifacts.js", "scripts/generate-provider-cli.js", "scripts/generate-specs-cli.js", "scripts/codegen/codegen-utils.js", @@ -95,6 +95,8 @@ "test-android-instrumentation": "yarn run docker-build-android && yarn run test-android-run-instrumentation", "test-android-unit": "yarn run docker-build-android && yarn run test-android-run-unit", "test-android-e2e": "yarn run docker-build-android && yarn run test-android-run-e2e", + "test-e2e-local": "node ./scripts/test-e2e-local.js", + "test-e2e-local-clean": "node ./scripts/test-e2e-local-clean.js", "test-ios": "./scripts/objc-test.sh test", "test-typescript": "dtslint types" }, diff --git a/scripts/cocoapods/__tests__/codegen_utils-test.rb b/scripts/cocoapods/__tests__/codegen_utils-test.rb index cdceb5d8510d8e..258875ebf9a650 100644 --- a/scripts/cocoapods/__tests__/codegen_utils-test.rb +++ b/scripts/cocoapods/__tests__/codegen_utils-test.rb @@ -361,7 +361,7 @@ def testUseReactCodegenDiscovery_whenParametersAreGood_executeCodegen assert_equal(Pod::Executable.executed_commands, [ { "command" => "node", - "arguments"=> ["~/app/ios/../node_modules/react-native/scripts/generate-artifacts.js", + "arguments"=> ["~/app/ios/../node_modules/react-native/scripts/generate-codegen-artifacts.js", "-p", "~/app", "-o", Pod::Config.instance.installation_root, "-e", "false", diff --git a/scripts/cocoapods/codegen_utils.rb b/scripts/cocoapods/codegen_utils.rb index 406e9936d236f8..1d2e89bcc55197 100644 --- a/scripts/cocoapods/codegen_utils.rb +++ b/scripts/cocoapods/codegen_utils.rb @@ -201,8 +201,8 @@ def get_react_codegen_script_phases( end # Generate input files for in-app libaraies which will be used to check if the script needs to be run. - # TODO: Ideally, we generate the input_files list from generate-artifacts.js and read the result here. - # Or, generate this podspec in generate-artifacts.js as well. + # TODO: Ideally, we generate the input_files list from generate-codegen-artifacts.js and read the result here. + # Or, generate this podspec in generate-codegen-artifacts.js as well. app_package_path = File.join(app_path, 'package.json') app_codegen_config = codegen_utils.get_codegen_config_from_file(app_package_path, config_key) input_files = codegen_utils.get_list_of_js_specs(app_codegen_config, app_path) @@ -270,7 +270,7 @@ def use_react_native_codegen_discovery!( out = Pod::Executable.execute_command( 'node', [ - "#{relative_installation_root}/#{react_native_path}/scripts/generate-artifacts.js", + "#{relative_installation_root}/#{react_native_path}/scripts/generate-codegen-artifacts.js", "-p", "#{app_path}", "-o", Pod::Config.instance.installation_root, "-e", "#{fabric_enabled}", diff --git a/scripts/generate-artifacts.js b/scripts/generate-codegen-artifacts.js similarity index 100% rename from scripts/generate-artifacts.js rename to scripts/generate-codegen-artifacts.js diff --git a/scripts/publish-npm.js b/scripts/publish-npm.js index 036db010fdbbbc..e57321dea70ddb 100755 --- a/scripts/publish-npm.js +++ b/scripts/publish-npm.js @@ -31,14 +31,17 @@ * * or otherwise `{major}.{minor}-stable` */ -const {exec, echo, exit, env, test} = require('shelljs'); +const {exec, echo, exit} = require('shelljs'); const {parseVersion} = require('./version-utils'); const { exitIfNotOnGit, getCurrentCommit, isTaggedLatest, - saveFiles, } = require('./scm-utils'); +const { + generateAndroidArtifacts, + saveFilesToRestore, +} = require('./release-utils'); const fs = require('fs'); const os = require('os'); const path = require('path'); @@ -72,22 +75,7 @@ const dryRunBuild = argv.dryRun; const includeHermes = argv.includeHermes; const isCommitly = nightlyBuild || dryRunBuild; -const filesToSaveAndRestore = [ - 'template/Gemfile', - 'template/_ruby-version', - 'template/package.json', - '.ruby-version', - 'Gemfile.lock', - 'Gemfile', - 'package.json', - 'ReactAndroid/gradle.properties', - 'Libraries/Core/ReactNativeVersion.js', - 'React/Base/RCTVersion.m', - 'ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java', - 'ReactCommon/cxxreact/ReactNativeVersion.h', -]; - -saveFiles(filesToSaveAndRestore, tmpPublishingFolder); +saveFilesToRestore(tmpPublishingFolder); if (includeHermes) { const HERMES_INSTALL_LOCATION = 'sdks'; @@ -195,48 +183,7 @@ if (isCommitly) { } } -// -------- Generating Android Artifacts -env.REACT_NATIVE_SKIP_PREFAB = true; -if (exec('./gradlew :ReactAndroid:installArchives').code) { - echo('Could not generate artifacts'); - exit(1); -} - -// -------- Generating the Hermes Engine Artifacts -env.REACT_NATIVE_HERMES_SKIP_PREFAB = true; -if (exec('./gradlew :ReactAndroid:hermes-engine:installArchives').code) { - echo('Could not generate artifacts'); - exit(1); -} - -echo('Generated artifacts for Maven'); - -let artifacts = [ - '.module', - '.pom', - '-debug.aar', - '-release.aar', - '-debug-sources.jar', - '-release-sources.jar', -].map(suffix => { - return `react-native-${releaseVersion}${suffix}`; -}); - -artifacts.forEach(name => { - if ( - !test( - '-e', - `./android/com/facebook/react/react-native/${releaseVersion}/${name}`, - ) - ) { - echo( - `Failing as expected file: \n\ - android/com/facebook/react/react-native/${releaseVersion}/${name}\n\ - was not correctly generated.`, - ); - exit(1); - } -}); +generateAndroidArtifacts(releaseVersion, tmpPublishingFolder); if (dryRunBuild) { echo('Skipping `npm publish` because --dry-run is set.'); diff --git a/scripts/react_native_pods_utils/script_phases.sh b/scripts/react_native_pods_utils/script_phases.sh index 6c41ce1cbaa2b0..32c59234407ad8 100755 --- a/scripts/react_native_pods_utils/script_phases.sh +++ b/scripts/react_native_pods_utils/script_phases.sh @@ -100,7 +100,7 @@ generateCodegenArtifactsFromSchema () { generateArtifacts () { describe "Generating codegen artifacts" pushd "$RCT_SCRIPT_RN_DIR" >/dev/null || exit 1 - "$NODE_BINARY" "scripts/generate-artifacts.js" --path "$RCT_SCRIPT_APP_PATH" --outputPath "$TEMP_OUTPUT_DIR" --fabricEnabled "$RCT_SCRIPT_FABRIC_ENABLED" --configFileDir "$RCT_SCRIPT_CONFIG_FILE_DIR" --nodeBinary "$NODE_BINARY" + "$NODE_BINARY" "scripts/generate-codegen-artifacts.js" --path "$RCT_SCRIPT_APP_PATH" --outputPath "$TEMP_OUTPUT_DIR" --fabricEnabled "$RCT_SCRIPT_FABRIC_ENABLED" --configFileDir "$RCT_SCRIPT_CONFIG_FILE_DIR" --nodeBinary "$NODE_BINARY" popd >/dev/null || exit 1 } diff --git a/scripts/release-utils.js b/scripts/release-utils.js new file mode 100644 index 00000000000000..c20adbf6f5fbe7 --- /dev/null +++ b/scripts/release-utils.js @@ -0,0 +1,85 @@ +/** + * 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, echo, exit, test, env} = require('shelljs'); +const {revertFiles, saveFiles} = require('./scm-utils'); + +function saveFilesToRestore(tmpPublishingFolder) { + const filesToSaveAndRestore = [ + 'template/Gemfile', + 'template/_ruby-version', + 'template/package.json', + '.ruby-version', + 'Gemfile.lock', + 'Gemfile', + 'package.json', + 'ReactAndroid/gradle.properties', + 'Libraries/Core/ReactNativeVersion.js', + 'React/Base/RCTVersion.m', + 'ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java', + 'ReactCommon/cxxreact/ReactNativeVersion.h', + ]; + + saveFiles(filesToSaveAndRestore, tmpPublishingFolder); +} + +function generateAndroidArtifacts(releaseVersion, tmpPublishingFolder) { + // -------- Generating Android Artifacts + env.REACT_NATIVE_SKIP_PREFAB = true; + if (exec('./gradlew :ReactAndroid:installArchives').code) { + echo('Could not generate artifacts'); + exit(1); + } + + // -------- Generating the Hermes Engine Artifacts + env.REACT_NATIVE_HERMES_SKIP_PREFAB = true; + if (exec('./gradlew :ReactAndroid:hermes-engine:installArchives').code) { + echo('Could not generate artifacts'); + exit(1); + } + + // undo uncommenting javadoc setting + revertFiles(['ReactAndroid/gradle.properties'], tmpPublishingFolder); + + echo('Generated artifacts for Maven'); + + let artifacts = [ + '.module', + '.pom', + '-debug.aar', + '-release.aar', + '-debug-sources.jar', + '-release-sources.jar', + ].map(suffix => { + return `react-native-${releaseVersion}${suffix}`; + }); + + artifacts.forEach(name => { + if ( + !test( + '-e', + `./android/com/facebook/react/react-native/${releaseVersion}/${name}`, + ) + ) { + echo( + `Failing as expected file: \n\ + android/com/facebook/react/react-native/${releaseVersion}/${name}\n\ + was not correctly generated.`, + ); + exit(1); + } + }); +} + +module.exports = { + generateAndroidArtifacts, + saveFilesToRestore, +}; diff --git a/scripts/test-e2e-local-clean.js b/scripts/test-e2e-local-clean.js new file mode 100644 index 00000000000000..6dd5f9080db770 --- /dev/null +++ b/scripts/test-e2e-local-clean.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 + */ + +'use strict'; + +/* + * This script, paired with test-e2e-local.js, is the full suite of + * tooling needed for a successful local testing experience. + * This script is an helper to clean up the environment fully + * before running the test suite. + * + * You should use this when switching between branches. + * + * It will: + * - clean up node modules + * - clean up the build folder (derived data, gradlew cleanAll) + * - clean up the pods folder for RNTester (pod install) (and Podfile.lock too) + * - kill all packagers + * - remove RNTestProject folder + * + * an improvements to consider: + * - an option to uninstall the apps (RNTester, RNTestProject) from emulators + */ + +const {exec, exit} = require('shelljs'); + +const {isPackagerRunning} = require('./testing-utils'); + +console.info('\n** Starting the clean up process **\n'); + +// 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"); + console.info('\n** Killed Metro **\n'); +} + +// Android +console.info('\n** Cleaning Gradle build artifacts **\n'); +exec('./gradlew cleanAll'); + +// iOS +console.info('\n** Nuking the derived data folder **\n'); +exec('rm -rf ~/Library/Developer/Xcode/DerivedData'); + +// RNTester Pods +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'); + +// final clean up +console.info('\n** Final git level wipe **\n'); +// clean unstaged changes from git +exec('git checkout -- .'); +// remove all the untracked files +exec('git clean -fdx'); + +console.info( + '\n** Clean up process completed\nPlease remember to run yarn install if you are planning to test again\n', +); +exit(0); diff --git a/scripts/test-e2e-local.js b/scripts/test-e2e-local.js new file mode 100644 index 00000000000000..da5d72a8097ebd --- /dev/null +++ b/scripts/test-e2e-local.js @@ -0,0 +1,205 @@ +/** + * 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'; + +/* + * This script is a re-interpretation of the old test-manual.e2e.sh script. + * the idea is to provide a better DX for the manual testing. + * It's using Javascript over Bash for consistency with the rest of the recent scripts + * and to make it more accessible for other devs to play around with. + */ + +const {exec, exit, pushd, popd, pwd, cd} = require('shelljs'); +const yargs = require('yargs'); +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +const { + launchAndroidEmulator, + isPackagerRunning, + launchPackagerInSeparateWindow, +} = require('./testing-utils'); + +const { + generateAndroidArtifacts, + saveFilesToRestore, +} = require('./release-utils'); + +const argv = yargs + .option('t', { + alias: 'target', + default: 'RNTester', + choices: ['RNTester', 'RNTestProject'], + }) + .option('p', { + alias: 'platform', + default: 'iOS', + choices: ['iOS', 'Android'], + }) + .option('h', { + alias: 'hermes', + type: 'boolean', + default: true, + }).argv; + +/* + * 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 + +// 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"); +} + +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 + + if (argv.platform === 'iOS') { + console.info( + `We're going to test the ${ + argv.hermes ? 'Hermes' : 'JSC' + } version of RNTester iOS`, + ); + exec( + `cd packages/rn-tester && USE_HERMES=${ + argv.hermes ? 1 : 0 + } bundle exec pod install --ansi`, + ); + + // if everything succeeded so far, we can launch Metro and the app + // start the Metro server in a separate window + launchPackagerInSeparateWindow(); + + // launch the app on iOS simulator + pushd('packages/rn-tester'); + exec('npx react-native run-ios --scheme RNTester'); + popd(); + } else { + // we do the android path here + + launchAndroidEmulator(); + + console.info( + `We're going to test the ${ + argv.hermes ? 'Hermes' : 'JSC' + } version of RNTester Android`, + ); + exec( + `./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. + + // 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', + ); + } +} else { + 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; + + const dateIdentifier = new Date() + .toISOString() + .slice(0, -8) + .replace(/[-:]/g, '') + .replace(/[T]/g, '-'); + + const releaseVersion = `${baseVersion}-${dateIdentifier}`; + + // this is needed to generate the Android artifacts correctly + exec(`node scripts/set-rn-version.js --to-version ${releaseVersion}`).code; + + // Generate native files (Android only for now) + generateAndroidArtifacts(releaseVersion, tmpPublishingFolder); + + // create locally the node module + exec('npm pack'); + + const localNodeTGZPath = `${pwd()}/react-native-${releaseVersion}.tgz`; + exec(`node scripts/set-rn-template-version.js "file:${localNodeTGZPath}"`); + + const repoRoot = pwd(); + + pushd('/tmp/'); + // need to avoid the pod install step because it will fail! (see above) + exec( + `node ${repoRoot}/cli.js init RNTestProject --template ${repoRoot} --skip-install`, + ); + + cd('RNTestProject'); + exec('yarn install'); + + if (argv.platform === 'iOS') { + // if we want iOS, we need to do pod install - but with a trick + cd('ios'); + exec('bundle install'); + + // TODO: we should be able to also use HERMES_ENGINE_TARBALL_PATH + // if we can make RNTester step generate it already so that it gets reused + + // need to discern if it's main branch or release branch + if (baseVersion === '1000.0.0') { + // main branch + exec(`USE_HERMES=${argv.hermes ? 1 : 0} bundle exec pod install --ansi`); + } else { + // TODO: to test this, I need to apply changes on top of a release branch + // a release branch + // copy over the .hermesversion file from react-native core into the RNTestProject + exec(`cp -f ${repoRoot}/sdks/.hermesversion .`); + exec( + `CI=true USE_HERMES=${ + argv.hermes ? 1 : 0 + } bundle exec pod install --ansi`, + ); + } + cd('..'); + exec('yarn ios'); + } else { + // android + exec('yarn android'); + } + popd(); + + // just cleaning up the temp folder, the rest is done by the test clean script + exec(`rm -rf ${tmpPublishingFolder}`); +} + +exit(0); diff --git a/scripts/testing-utils.js b/scripts/testing-utils.js new file mode 100644 index 00000000000000..a687a37cd562ca --- /dev/null +++ b/scripts/testing-utils.js @@ -0,0 +1,111 @@ +/** + * 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 os = require('os'); +const {spawn} = require('node:child_process'); + +/* + * Android related utils - leverages android tooling + */ + +// this code is taken from the CLI repo, slightly readapted to our needs +// here's the reference folder: +// https://github.com/react-native-community/cli/blob/main/packages/cli-platform-android/src/commands/runAndroid + +const emulatorCommand = process.env.ANDROID_HOME + ? `${process.env.ANDROID_HOME}/emulator/emulator` + : 'emulator'; + +const getEmulators = () => { + const emulatorsOutput = exec(`${emulatorCommand} -list-avds`).stdout; + return emulatorsOutput.split(os.EOL).filter(name => name !== ''); +}; + +const launchEmulator = emulatorName => { + // we need both options 'cause reasons: + // 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}`], { + detached: true, + stdio: 'ignore', + }); + + cp.unref(); +}; + +function tryLaunchEmulator() { + const emulators = getEmulators(); + if (emulators.length > 0) { + try { + launchEmulator(emulators[0]); + + return {success: true}; + } catch (error) { + return {success: false, error}; + } + } + return { + success: false, + error: 'No emulators found as an output of `emulator -list-avds`', + }; +} + +function launchAndroidEmulator() { + const result = tryLaunchEmulator(); + if (result.success) { + console.info('Successfully launched emulator.'); + } else { + console.error(`Failed to launch emulator. Reason: ${result.error || ''}.`); + console.warn( + 'Please launch an emulator manually or connect a device. Otherwise app may fail to launch.', + ); + } +} + +/* + * iOS related utils - leverages xcodebuild + */ + +/* + * Metro related utils + */ + +// inspired by CLI again https://github.com/react-native-community/cli/blob/main/packages/cli-tools/src/isPackagerRunning.ts + +function isPackagerRunning( + packagerPort = process.env.RCT_METRO_PORT || '8081', +) { + try { + const status = exec(`curl http://localhost:${packagerPort}/status`, { + silent: true, + }).stdout; + + return status === 'packager-status:running' ? 'running' : 'unrecognized'; + } catch (_error) { + return 'not_running'; + } +} + +// 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"); +} + +module.exports = { + launchAndroidEmulator, + isPackagerRunning, + launchPackagerInSeparateWindow, +}; diff --git a/sdks/hermes-engine/hermes-engine.podspec b/sdks/hermes-engine/hermes-engine.podspec index d06bf545d0e3eb..72606fa2f848c3 100644 --- a/sdks/hermes-engine/hermes-engine.podspec +++ b/sdks/hermes-engine/hermes-engine.podspec @@ -28,7 +28,7 @@ git = "https://github.com/facebook/hermes.git" if ENV.has_key?('HERMES_ENGINE_TARBALL_PATH') Pod::UI.puts '[Hermes] Using pre-built Hermes binaries from local path.' if Object.const_defined?("Pod::UI") source[:http] = "file://#{ENV['HERMES_ENGINE_TARBALL_PATH']}" -elsif version == '1000.0.0' || version.start_with?('0.0.0-') +elsif version.include? '1000.0.0' Pod::UI.puts '[Hermes] Installing hermes-engine may take a while, building Hermes from source...'.yellow if Object.const_defined?("Pod::UI") source[:git] = git source[:commit] = `git ls-remote https://github.com/facebook/hermes main | cut -f 1`.strip