1import { IOptions as GlobOptions } from 'glob'; 2import glob from 'glob-promise'; 3import chalk from 'chalk'; 4import basicSpawnAsync, { SpawnResult, SpawnOptions, SpawnPromise } from '@expo/spawn-async'; 5 6import { EXPO_DIR } from './Constants'; 7 8export { SpawnResult, SpawnOptions }; 9 10/** 11 * Asynchronously spawns a process with given command, args and options. Working directory is set to repo's root by default. 12 */ 13export function spawnAsync( 14 command: string, 15 args: Readonly<string[]> = [], 16 options: SpawnOptions = {} 17): SpawnPromise<SpawnResult> { 18 return basicSpawnAsync(command, args, { 19 env: { ...process.env }, 20 cwd: EXPO_DIR, 21 ...options, 22 }); 23} 24 25/** 26 * Does the same as `spawnAsync` but parses the output to JSON object. 27 */ 28export async function spawnJSONCommandAsync<T = object>( 29 command: string, 30 args: Readonly<string[]> = [], 31 options: SpawnOptions = {} 32): Promise<T> { 33 const child = await spawnAsync(command, args, options); 34 try { 35 return JSON.parse(child.stdout); 36 } catch (e) { 37 e.message += 38 '\n' + chalk.red('Cannot parse this output as JSON: ') + chalk.yellow(child.stdout.trim()); 39 throw e; 40 } 41} 42 43/** 44 * Deeply clones an object. It's used to make a backup of home's `app.json` file. 45 */ 46export function deepCloneObject<ObjectType extends object = object>( 47 object: ObjectType 48): ObjectType { 49 return JSON.parse(JSON.stringify(object)); 50} 51 52/** 53 * Waits given amount of time (in milliseconds). 54 */ 55export function sleepAsync(duration: number): Promise<void> { 56 return new Promise((resolve) => { 57 setTimeout(resolve, duration); 58 }); 59} 60 61/** 62 * Filters an array asynchronously. 63 */ 64export async function filterAsync<T = any>( 65 arr: T[], 66 filter: (item: T, index: number) => boolean | Promise<boolean> 67): Promise<T[]> { 68 const results = await Promise.all(arr.map(filter)); 69 return arr.filter((item, index) => results[index]); 70} 71 72/** 73 * Retries executing the function with given interval and with given retry limit. 74 * It resolves immediately once the callback returns anything else than `undefined`. 75 */ 76export async function retryAsync<T = any>( 77 interval: number, 78 limit: number, 79 callback: () => T | Promise<T> 80): Promise<T | undefined> { 81 return new Promise((resolve) => { 82 let count = 0; 83 84 const timeoutCallback = async () => { 85 const result = await callback(); 86 87 if (result !== undefined) { 88 resolve(result); 89 return; 90 } 91 if (++count < limit) { 92 setTimeout(timeoutCallback, interval); 93 } else { 94 resolve(undefined); 95 } 96 }; 97 timeoutCallback(); 98 }); 99} 100 101/** 102 * Executes regular expression against a string until the last match is found. 103 */ 104export function execAll(rgx: RegExp, str: string, index: number = 0): string[] { 105 const globalRgx = new RegExp(rgx.source, 'g' + rgx.flags.replace('g', '')); 106 const matches: string[] = []; 107 let match; 108 while ((match = globalRgx.exec(str))) { 109 matches.push(match[index]); 110 } 111 return matches; 112} 113 114/** 115 * Searches for files matching given glob patterns. 116 */ 117export async function searchFilesAsync( 118 rootPath: string, 119 patterns: string | string[], 120 options?: GlobOptions 121): Promise<Set<string>> { 122 const files = await Promise.all( 123 arrayize(patterns).map((pattern) => 124 glob(pattern, { 125 cwd: rootPath, 126 nodir: true, 127 ...options, 128 }) 129 ) 130 ); 131 return new Set(([] as string[]).concat(...files)); 132} 133 134/** 135 * Ensures the value is an array. 136 */ 137export function arrayize<T>(value: T | T[]): T[] { 138 if (Array.isArray(value)) { 139 return value; 140 } 141 return value != null ? [value] : []; 142} 143