1import basicSpawnAsync, { SpawnResult, SpawnOptions, SpawnPromise } from '@expo/spawn-async'; 2import chalk from 'chalk'; 3import { IOptions as GlobOptions } from 'glob'; 4import glob from 'glob-promise'; 5import ora from 'ora'; 6 7import { EXPO_DIR } from './Constants'; 8 9export { SpawnResult, SpawnOptions }; 10 11/** 12 * Asynchronously spawns a process with given command, args and options. Working directory is set to repo's root by default. 13 */ 14export function spawnAsync( 15 command: string, 16 args: Readonly<string[]> = [], 17 options: SpawnOptions = {} 18): SpawnPromise<SpawnResult> { 19 return basicSpawnAsync(command, args, { 20 env: { ...process.env }, 21 cwd: EXPO_DIR, 22 ...options, 23 }); 24} 25 26/** 27 * Does the same as `spawnAsync` but parses the output to JSON object. 28 */ 29export async function spawnJSONCommandAsync<T = object>( 30 command: string, 31 args: Readonly<string[]> = [], 32 options: SpawnOptions = {} 33): Promise<T> { 34 const child = await spawnAsync(command, args, options); 35 try { 36 return JSON.parse(child.stdout); 37 } catch (e) { 38 e.message += 39 '\n' + chalk.red('Cannot parse this output as JSON: ') + chalk.yellow(child.stdout.trim()); 40 throw e; 41 } 42} 43 44/** 45 * Deeply clones an object. It's used to make a backup of home's `app.json` file. 46 */ 47export function deepCloneObject<ObjectType extends object = object>( 48 object: ObjectType 49): ObjectType { 50 return JSON.parse(JSON.stringify(object)); 51} 52 53/** 54 * Waits given amount of time (in milliseconds). 55 */ 56export function sleepAsync(duration: number): Promise<void> { 57 return new Promise((resolve) => { 58 setTimeout(resolve, duration); 59 }); 60} 61 62/** 63 * Filters an array asynchronously. 64 */ 65export async function filterAsync<T = any>( 66 arr: T[], 67 filter: (item: T, index: number) => boolean | Promise<boolean> 68): Promise<T[]> { 69 const results = await Promise.all(arr.map(filter)); 70 return arr.filter((item, index) => results[index]); 71} 72 73/** 74 * Retries executing the function with given interval and with given retry limit. 75 * It resolves immediately once the callback returns anything else than `undefined`. 76 */ 77export async function retryAsync<T = any>( 78 interval: number, 79 limit: number, 80 callback: () => T | Promise<T> 81): Promise<T | undefined> { 82 return new Promise((resolve) => { 83 let count = 0; 84 85 const timeoutCallback = async () => { 86 const result = await callback(); 87 88 if (result !== undefined) { 89 resolve(result); 90 return; 91 } 92 if (++count < limit) { 93 setTimeout(timeoutCallback, interval); 94 } else { 95 resolve(undefined); 96 } 97 }; 98 timeoutCallback(); 99 }); 100} 101 102/** 103 * Executes regular expression against a string until the last match is found. 104 */ 105export function execAll(rgx: RegExp, str: string, index: number = 0): string[] { 106 const globalRgx = new RegExp(rgx.source, 'g' + rgx.flags.replace('g', '')); 107 const matches: string[] = []; 108 let match; 109 while ((match = globalRgx.exec(str))) { 110 matches.push(match[index]); 111 } 112 return matches; 113} 114 115/** 116 * Searches for files matching given glob patterns. 117 */ 118export async function searchFilesAsync( 119 rootPath: string, 120 patterns: string | string[], 121 options?: GlobOptions 122): Promise<Set<string>> { 123 const files = await Promise.all( 124 arrayize(patterns).map((pattern) => 125 glob(pattern, { 126 cwd: rootPath, 127 nodir: true, 128 ...options, 129 }) 130 ) 131 ); 132 return new Set(([] as string[]).concat(...files)); 133} 134 135/** 136 * Ensures the value is an array. 137 */ 138export function arrayize<T>(value: T | T[]): T[] { 139 if (Array.isArray(value)) { 140 return value; 141 } 142 return value != null ? [value] : []; 143} 144 145/** 146 * Execute `patch` command for given patch content 147 */ 148export async function applyPatchAsync(options: { 149 patchContent: string; 150 cwd: string; 151 reverse?: boolean; 152 stripPrefixNum?: number; 153}) { 154 const args: string[] = []; 155 if (options.stripPrefixNum != null) { 156 // -pN passing to the `patch` command for striping slashed prefixes 157 args.push(`-p${options.stripPrefixNum}`); 158 } 159 if (options.reverse) { 160 args.push('-R'); 161 } 162 163 const procPromise = spawnAsync('patch', args, { 164 cwd: options.cwd, 165 }); 166 procPromise.child.stdin?.write(options.patchContent); 167 procPromise.child.stdin?.end(); 168 await procPromise; 169} 170 171export async function runWithSpinner<Result>( 172 title: string, 173 action: (step: ora.Ora) => Promise<Result> | Result, 174 succeedText: string | null = null, 175 options: ora.Options = {} 176): Promise<Result> { 177 const disabled = process.env.CI || process.env.EXPO_DEBUG; 178 const step = ora({ 179 text: chalk.bold(title), 180 isEnabled: !disabled, 181 stream: disabled ? process.stdout : process.stderr, 182 ...options, 183 }); 184 185 step.start(); 186 187 try { 188 const result = await action(step); 189 190 if (succeedText) { 191 step.succeed(succeedText); 192 } 193 return result; 194 } catch (error) { 195 step.fail(); 196 console.error(error); 197 process.exit(1); 198 } 199} 200