Files
react-native/scripts/release-testing/utils/testing-utils.js
T
Rob Hogan 2e9b7fb2ad Align Flow lib defs for Node.js child_process with v24 (#55186)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/55186

This is an AI-assisted change to align the Flow definitions for the `child_process` module with the Node.js docs as at v24.

**New v18+ Features:**

1. **AbortSignal Support** (v18.0.0) - Process cancellation
   - Added `signal?: AbortSignal` option to all async functions (exec, execFile, fork, spawn)
   - Enables cancellation of child processes using `AbortController`
   - Example: `const controller = new AbortController(); spawn('cmd', [], {signal: controller.signal}); controller.abort();`
   - https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback

2. **Enhanced Process Properties** (v20.x)
   - `spawnargs: Array<string>` - Full command-line arguments used to launch the process
   - `spawnfile: string` - Executable file name of the child process
   - `signalCode: string | null` - Signal that terminated the child process
   - https://nodejs.org/api/child_process.html#subprocessspawnargs

**ChildProcess Class Updates:**

3. **Improved Property Types**
   - `stdio` is now a properly typed tuple array with readonly modifier
   - `channel` changed from `Object` to `mixed` for better type safety

4. **Enhanced Method Signatures**
   - `kill(signal?: string | number): boolean` - Now returns boolean and accepts numeric signals
   - `send()` overloads properly typed with `child_process$Serializable` messages
   - `send()` accepts `keepOpen` option: `Readonly<{keepOpen?: boolean}>`
   - https://nodejs.org/api/child_process.html#subprocesskillsignal

**Option Type Improvements:**

6. **Modern Readonly Syntax** - All input options use `Readonly<{...}>`
   - `execOpts`, `execFileOpts`, `forkOpts`, `spawnOpts`, `spawnSyncOpts`, etc.
   - Allows passing readonly types safely

7. **Enhanced Environment Variables**
   - Changed `env?: Object` to `env?: {[key: string]: string | void}`
   - More precise typing for environment variable dictionaries

8. **New Fork Options** (IPC enhancements)
   - `serialization?: 'json' | 'advanced'` - IPC serialization format
   - `killSignal` and `timeout` options added to fork
   - https://nodejs.org/api/child_process.html#child_processforkmodulepath-args-options

9. **Enhanced Spawn Options**
   - Added `killSignal`, `timeout`, `serialization` to `spawnOpts`
   - Better consistency across all spawn functions

**Return Type Fixes:**

10. **spawnRet/spawnSyncRet** - Made fields properly nullable:
    - `status: number | null` - null if process never exited
    - `signal: string | null` - null if not terminated by signal
    - `error: Error | void` - void if no error occurred
    - Removed spread operator for exact types

**Helper Types:**

11. **`child_process$Serializable`** - Union type for IPC messages:
    - `string | number | boolean | bigint | {...} | Array<mixed>`
    - Used in send() method signatures

12. **`child_process$SendHandle`** - Type for handle passing:
    - `net$Server | net$Socket`
    - Used for passing server/socket handles to child processes

**References:**
- Node.js child_process module docs: https://nodejs.org/api/child_process.html
- AbortSignal integration: https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback

Changelog: [Internal]
 ---
> Generated by [Confucius Code Assist (CCA)](https://www.internalfb.com/wiki/Confucius/Analect/Shared_Analects/Confucius_Code_Assist_(CCA)/)
[Confucius Session](https://www.internalfb.com/confucius?host=devvm45708.cln0.facebook.com&port=8086&tab=Chat&session_id=1a3aa26e-e5a9-11f0-8d47-71a4a90f0494&entry_name=Code+Assist), [Trace](https://www.internalfb.com/confucius?session_id=1a3aa26e-e5a9-11f0-8d47-71a4a90f0494&tab=Trace)

Reviewed By: vzaidman

Differential Revision: D89944331

fbshipit-source-id: 744ae04051c2214d97269c48c115060f5f30fb1f
2026-01-18 12:52:52 -08:00

278 lines
8.5 KiB
JavaScript

/**
* 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
*/
'use strict';
const {
generateAndroidArtifacts,
} = require('../../releases/utils/release-utils');
const ghaArtifactsUtils = require('./github-actions-utils.js');
const fs = require('fs');
// $FlowFixMe[cannot-resolve-module]
const {spawn} = require('node:child_process');
const os = require('os');
const path = require('path');
const {exec} = require('shelljs');
/*::
type BuildType = 'dry-run' | 'release' | 'nightly';
*/
/*
* 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 != null
? `${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 /*: string */) => {
// 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
spawn(emulatorCommand, [`@${emulatorName}`], {
detached: true,
stdio: 'ignore',
}).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 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.');
} 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 /*: string */ = process.env.RCT_METRO_PORT ?? '8081',
) /*: 'running' | 'unrecognized' | 'not_running' */ {
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
function launchPackagerInSeparateWindow(folderPath /*: string */) {
const command = `tell application "Terminal" to do script "cd ${folderPath} && yarn start"`;
exec(`osascript -e '${command}' >/dev/null <<EOF`);
}
/**
* Checks if Metro is running and it kills it if that's the case
*/
function checkPackagerRunning() {
if (isPackagerRunning() === 'running') {
exec(
"lsof -i :8081 | grep LISTEN | /usr/bin/awk '{print $2}' | xargs kill",
);
}
}
// === ARTIFACTS === //
/**
* Setups the ciArtifacts if a token has been passed
*
* Parameters:
* - @ciToken a valid GHA Token.
* - @branchName the branch of the name we want to use to fetch the artifacts.
*/
async function setupGHAArtifacts(
ciToken /*: ?string */,
branchName /*: string */,
useLastSuccessfulPipeline /*: boolean */,
) /*: Promise<?typeof ghaArtifactsUtils> */ {
if (ciToken == null) {
return null;
}
const baseTmpPath = '/tmp/react-native-tmp';
await ghaArtifactsUtils.initialize(
ciToken,
baseTmpPath,
branchName,
useLastSuccessfulPipeline,
);
return ghaArtifactsUtils;
}
async function downloadArtifacts(
ciArtifacts /*: typeof ghaArtifactsUtils */,
mavenLocalPath /*: string */,
localNodeTGZPath /*: string */,
) {
const mavenLocalURL = await ciArtifacts.artifactURLForMavenLocal();
const reactNativeURLZip = await ciArtifacts.artifactURLForReactNative();
// Cleanup destination folder
exec(`rm -rf ${ciArtifacts.baseTmpPath()}`);
exec(`mkdir ${ciArtifacts.baseTmpPath()}`);
const mavenLocalZipPath = `${mavenLocalPath}.zip`;
console.info(
`\n[Download] Maven Local Artifacts from ${mavenLocalURL} into ${mavenLocalZipPath}`,
);
ciArtifacts.downloadArtifact(mavenLocalURL, mavenLocalZipPath);
console.info(`Unzipping into ${mavenLocalPath}`);
exec(`unzip -oq ${mavenLocalZipPath} -d ${mavenLocalPath}`);
console.info(`\n[Download] React Native from ${reactNativeURLZip}`);
const reactNativeDestPath = path.join(
ciArtifacts.baseTmpPath(),
'react-native',
);
const reactNativeZipDestPath = `${reactNativeDestPath}.zip`;
ciArtifacts.downloadArtifact(reactNativeURLZip, reactNativeZipDestPath);
exec(`unzip ${reactNativeZipDestPath} -d ${reactNativeDestPath}`);
// For some reason, the commit on which the Github Action is running is not the same as the one
// that is running locally. This make so that the react-native package is created with a different
// commit sha in CI wrt what is used locally.
// As a result the react-native tgz is different. The next section of code use package that is created
// in CI as source of truth and sends back the new localNodeTGZ path so that the new apps can
// use it.
const tgzName = fs.readdirSync(reactNativeDestPath).filter(file => {
console.log(file);
return file.endsWith('.tgz');
})[0];
if (tgzName == null) {
throw new Error('Could not find the tgz file in the react-native folder');
}
const basePath = path.dirname(localNodeTGZPath);
const newLocalNodeTGZ = path.join(basePath, tgzName);
const reactNativeTGZ = path.join(reactNativeDestPath, tgzName);
exec(`mv ${reactNativeTGZ} ${newLocalNodeTGZ}`);
return {newLocalNodeTGZ};
}
function buildArtifactsLocally(
releaseVersion /*: string */,
buildType /*: BuildType */,
reactNativePackagePath /*: string */,
) {
// this is needed to generate the Android artifacts correctly
const exitCode = exec(
`node scripts/releases/set-rn-artifacts-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);
}
/**
* It prepares the artifacts required to run a new project created from the template
*
* Parameters:
* - @ciArtifacts manager object to manage all the download of ciArtifacts. 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.
*/
async function prepareArtifacts(
ciArtifacts /*: ?typeof ghaArtifactsUtils */,
mavenLocalPath /*: string */,
localNodeTGZPath /*: string */,
releaseVersion /*: string */,
buildType /*: BuildType */,
reactNativePackagePath /*: string */,
) /*: Promise<{newLocalNodeTGZ: string }> */ {
if (ciArtifacts == null) {
buildArtifactsLocally(releaseVersion, buildType, reactNativePackagePath);
return {
newLocalNodeTGZ: localNodeTGZPath,
};
}
return downloadArtifacts(ciArtifacts, mavenLocalPath, localNodeTGZPath);
}
module.exports = {
checkPackagerRunning,
maybeLaunchAndroidEmulator,
isPackagerRunning,
launchPackagerInSeparateWindow,
setupGHAArtifacts,
prepareArtifacts,
};