-
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: Helper methods to help the cli grab system state, devices and run react-native/core-cli-utils tasks using Listr. Changelog: [Internal] Differential Revision: D57067037
- Loading branch information
1 parent
603fcbd
commit 0730886
Showing
4 changed files
with
867 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,48 @@ | ||
/** | ||
* 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 {watch} = JSON.parse( | ||
execSync('watchman watch-project .', {cwd: process.cwd()}).toString(), | ||
); | ||
|
||
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,139 @@ | ||
/** | ||
* 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 allSimIPhones: IOSDevice[] = allAvailableDevices.filter(device => | ||
/SimDeviceType\.iPhone/.test(device.deviceTypeIdentifier), | ||
); | ||
|
||
// Pick anything that is booted, otherwise get your user to help out | ||
const available = allSimIPhones.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', | ||
); | ||
} | ||
|
||
const booted = allSimIPhones | ||
.filter(device => device.state === 'Booted') | ||
.pop(); | ||
|
||
if (booted != null) { | ||
return booted; | ||
} | ||
|
||
return allSimIPhones[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, | ||
]); | ||
} | ||
|
||
export async function launchApp( | ||
udid: string, | ||
bundleId: string, | ||
): Promise<Result> { | ||
return execa('xcrun', ['simctl', 'launch', udid, bundleId]); | ||
} | ||
|
||
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); | ||
} |
Oops, something went wrong.