xref: /expo/tools/src/Utils.ts (revision 0dfbbd2a)
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