-
Notifications
You must be signed in to change notification settings - Fork 24k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Summary: Pull Request resolved: #44463 Helper methods to help the cli grab system state, devices and run react-native/core-cli-utils tasks using Listr. Changelog: [Internal] Reviewed By: cipolleschi Differential Revision: D57067037 fbshipit-source-id: 28cb4239f3a93558b88417f366a2146f696cc411
- Loading branch information
1 parent
9b0072a
commit fedb145
Showing
4 changed files
with
378 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
/** | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @flow strict-local | ||
* @format | ||
* @oncall react_native | ||
*/ | ||
|
||
import type {Task} from '@react-native/core-cli-utils'; | ||
import type {Result} from 'execa'; | ||
import type {ExecaPromise} from 'execa'; | ||
import type {TaskSpec} from 'listr'; | ||
|
||
import chalk from 'chalk'; | ||
import Listr from 'listr'; | ||
import {Observable} from 'rxjs'; | ||
|
||
export function trim( | ||
line: string, | ||
// $FlowFixMe[prop-missing] | ||
maxLength: number = Math.min(process.stdout?.columns, 120), | ||
): string { | ||
const flattened = line.replaceAll('\n', ' ').trim(); | ||
return flattened.length >= maxLength | ||
? flattened.slice(0, maxLength - 3) + '...' | ||
: flattened.trim(); | ||
} | ||
|
||
type ExecaPromiseMetaized = Promise<Result> & child_process$ChildProcess; | ||
|
||
export function observe(result: ExecaPromiseMetaized): Observable<string> { | ||
return new Observable(observer => { | ||
result.stderr.on('data', (data: Buffer) => | ||
data | ||
.toString('utf8') | ||
.split('\n') | ||
.filter(line => line.length > 0) | ||
.forEach(line => observer.next('🟢 ' + trim(line))), | ||
); | ||
result.stdout.on('data', (data: Buffer) => | ||
data | ||
.toString('utf8') | ||
.split('\n') | ||
.filter(line => line.length > 0) | ||
.forEach(line => observer.next('🟠 ' + trim(line))), | ||
); | ||
|
||
// Terminal events | ||
result.stdout.on('error', error => observer.error(error.trim())); | ||
result.then( | ||
(_: Result) => observer.complete(), | ||
error => | ||
observer.error( | ||
new Error( | ||
`${chalk.red.bold(error.shortMessage)}\n${ | ||
error.stderr || error.stdout | ||
}`, | ||
), | ||
), | ||
); | ||
|
||
return () => { | ||
for (const out of [result.stderr, result.stdout]) { | ||
out.destroy(); | ||
out.removeAllListeners(); | ||
} | ||
}; | ||
}); | ||
} | ||
|
||
type MixedTasks = Task<ExecaPromise> | Task<void>; | ||
type Tasks = { | ||
+[label: string]: MixedTasks, | ||
}; | ||
|
||
export function run( | ||
tasks: Tasks, | ||
exclude: {[label: string]: boolean} = {}, | ||
): Promise<void> { | ||
let ordered: MixedTasks[] = []; | ||
for (const [label, task] of Object.entries(tasks)) { | ||
if (label in exclude) { | ||
continue; | ||
} | ||
ordered.push(task); | ||
} | ||
ordered = ordered.sort((a, b) => a.order - b.order); | ||
|
||
const spec: TaskSpec<void, Observable<string> | Promise<void> | void>[] = | ||
ordered.map(task => ({ | ||
title: task.label, | ||
task: () => { | ||
const action = task.action(); | ||
if (action != null) { | ||
return observe(action); | ||
} | ||
}, | ||
})); | ||
return new Listr(spec).run(); | ||
} | ||
|
||
export function handlePipedArgs(): Promise<string[]> { | ||
return new Promise(resolve => { | ||
if (process.stdin.isTTY == null) { | ||
return resolve([]); | ||
} | ||
|
||
const args: string[] = []; | ||
const msg: string[] = []; | ||
let count = 0; | ||
const assignment = /^(.+?)=(.*)$/; | ||
|
||
function processLine(line: string) { | ||
const match = assignment.exec(line); | ||
if (!match) { | ||
msg.push(chalk.red(line)); | ||
count++; | ||
return; | ||
} | ||
const [key, value] = match.slice(1); | ||
if (value == null) { | ||
msg.push(chalk.bold(line) + chalk.red('<missing value>')); | ||
count++; | ||
return; | ||
} | ||
msg.push(chalk.dim(line)); | ||
args.push(`--${key} ${value}`); | ||
} | ||
|
||
process.stdout.on('line', processLine); | ||
process.stdout.on('close', () => { | ||
process.stdout.removeListener('line', processLine); | ||
if (count > 0) { | ||
process.stderr.write( | ||
`The config piped into ${chalk.bold( | ||
'helloword/cli', | ||
)} contained errors:\n\n` + | ||
msg.map(line => ' ' + line).join('\n') + | ||
'\n', | ||
); | ||
} | ||
resolve(args); | ||
}); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
* | ||
* @flow strict-local | ||
* @format | ||
* @oncall react_native | ||
*/ | ||
|
||
import {execSync, spawn} from 'child_process'; | ||
import debug from 'debug'; | ||
|
||
const logWatchman = debug('helloworld:cli:watchman'); | ||
|
||
export async function pauseWatchman(command: () => Promise<mixed | void>) { | ||
let p: ReturnType<typeof spawn> | null = null; | ||
try { | ||
const raw: string = execSync('watchman watch-project .', { | ||
cwd: process.cwd(), | ||
}).toString(); | ||
const {watch} = JSON.parse(raw); | ||
|
||
p = spawn('watchman', [ | ||
'--no-pretty', | ||
'--persistent', | ||
'state-enter', | ||
watch, | ||
'yarn-install', | ||
]); | ||
logWatchman(`[PID:${p.pid}] started`); | ||
} catch (e) { | ||
logWatchman( | ||
`Unable to pause watchman: ${e.message}, running command anyway`, | ||
); | ||
} finally { | ||
try { | ||
// Always run our user, if watchman has problems or doesn't exist proceed. | ||
await command(); | ||
} finally { | ||
if (p?.killed || p?.exitCode != null) { | ||
return; | ||
} | ||
logWatchman(`[PID:${p?.pid ?? '????'}] killing with SIGTERM`); | ||
p?.kill('SIGTERM'); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
/** | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @flow strict-local | ||
* @format | ||
* @oncall react_native | ||
*/ | ||
|
||
import type {XcodeBuildSettings} from './xcode'; | ||
import type {Result} from 'execa'; | ||
|
||
import execa from 'execa'; | ||
|
||
export type IOSDevice = { | ||
lastBootedAt: Date, | ||
dataPath: string, | ||
dataPathSize: number, | ||
logPath: string, | ||
udid: string, | ||
isAvailable: boolean, | ||
availabilityError: string, | ||
logPathSize: number, | ||
deviceTypeIdentifier: string, | ||
state: 'Shutdown' | 'Booted' | 'Creating', | ||
name: string, | ||
}; | ||
|
||
export async function getSimulatorDetails( | ||
nameOrUDID: string, | ||
): Promise<IOSDevice> { | ||
const {stdout} = execa.sync('xcrun', [ | ||
'simctl', | ||
'list', | ||
'devices', | ||
'iPhone', | ||
'available', | ||
'--json', | ||
]); | ||
const json = JSON.parse(stdout); | ||
|
||
const allAvailableDevices: IOSDevice[] = Object.values(json.devices) | ||
.flatMap(devices => devices) | ||
.filter(device => device.isAvailable) | ||
.map(device => ({ | ||
...device, | ||
lastBootedAt: new Date(device.lastBootedAt), | ||
})); | ||
|
||
if (nameOrUDID.length > 0 && nameOrUDID.toLowerCase() !== 'simulator') { | ||
const namedDevice = allAvailableDevices.find( | ||
device => device.udid === nameOrUDID || device.name === nameOrUDID, | ||
); | ||
if (namedDevice == null) { | ||
const devices = allAvailableDevices | ||
.map(device => `- ${device.name}: ${device.udid}`) | ||
.join('\n - '); | ||
throw new Error( | ||
`Unable to find device with name or UDID: '${nameOrUDID}', found:\n\n${devices}`, | ||
); | ||
} | ||
return namedDevice; | ||
} | ||
|
||
const booted: IOSDevice[] = allAvailableDevices.filter( | ||
device => | ||
device.state === 'Booted' && | ||
/SimDeviceType\.iPhone/.test(device.deviceTypeIdentifier), | ||
); | ||
// Pick anything that is booted, otherwise get your user to help out | ||
const available = booted.sort( | ||
(a, b) => a.lastBootedAt.getTime() - b.lastBootedAt.getTime(), | ||
); | ||
|
||
if (available.length === 0) { | ||
throw new Error( | ||
'No simulator is available, please create on using the Simulator', | ||
); | ||
} | ||
|
||
return available[0]; | ||
} | ||
|
||
export async function bootSimulator( | ||
device: IOSDevice, | ||
): Promise<Result | string> { | ||
if (device.state === 'Shutdown') { | ||
return execa('xcrun', ['simctl', 'boot', device.udid]); | ||
} | ||
return Promise.resolve('Already booted'); | ||
} | ||
|
||
export async function launchSimulator(device: IOSDevice): Promise<Result> { | ||
return execa('open', [ | ||
'-a', | ||
'Simulator', | ||
'--args', | ||
'-CurrentDeviceUDID', | ||
device.udid, | ||
]); | ||
} | ||
|
||
/** | ||
* Launches the app on the simulator. | ||
* | ||
* @param udid The UDID of the simulator | ||
* @param bundleId The bundle ID of the app | ||
* @param env The environment variables to set in the app environment (optional) | ||
*/ | ||
export async function launchApp( | ||
udid: string, | ||
bundleId: string, | ||
env: {[string]: string} | null = null, | ||
): Promise<Result> { | ||
const _env: {[string]: string | void} = {}; | ||
if (env) { | ||
for (const [key, value] of Object.entries(env)) { | ||
_env['SIMCTL_CHILD_' + key] = value; | ||
} | ||
} | ||
return execa('xcrun', ['simctl', 'launch', udid, bundleId], { | ||
env: env == null ? _env : process.env, | ||
}); | ||
} | ||
|
||
export function getXcodeBuildSettings( | ||
iosProjectFolder: string, | ||
): XcodeBuildSettings[] { | ||
const {stdout} = execa.sync( | ||
'xcodebuild', | ||
[ | ||
'-workspace', | ||
'HelloWorld.xcworkspace', | ||
'-scheme', | ||
'HelloWorld', | ||
'-configuration', | ||
'Debug', | ||
'-sdk', | ||
'iphonesimulator', | ||
'-showBuildSettings', | ||
'-json', | ||
], | ||
{cwd: iosProjectFolder}, | ||
); | ||
return JSON.parse(stdout); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/** | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @flow strict-local | ||
* @format | ||
* @oncall react_native | ||
*/ | ||
|
||
// For the complete list: | ||
// | ||
// xcodebuild \ | ||
// -workspace HelloWorld.xcworkspace \ | ||
// -scheme HelloWorld \ | ||
// -configuration Debug \ | ||
// -sdk iphonesimulator \ | ||
// -showBuildSettings | ||
// -json | ||
|
||
export type XcodeBuildSettings = { | ||
action: string, | ||
buildSettings: { | ||
CONFIGURATION_BUILD_DIR: string, | ||
EXECUTABLE_FOLDER_PATH: string, | ||
PRODUCT_BUNDLE_IDENTIFIER: string, | ||
TARGET_BUILD_DIR: string, | ||
UNLOCALIZED_RESOURCES_FOLDER_PATH: string, | ||
... | ||
}, | ||
target: string, | ||
}; |