xref: /expo/packages/create-expo/src/utils/args.ts (revision b7d15820)
1// Common utilities for interacting with `args` library.
2// These functions should be used by every command.
3import arg, { Spec } from 'arg';
4import chalk from 'chalk';
5
6import { replaceValue } from './array';
7import * as Log from '../log';
8
9/**
10 * Parse args and assert unknown options.
11 *
12 * @param schema the `args` schema for parsing the command line arguments.
13 * @param argv extra strings
14 * @returns processed args object.
15 */
16export function assertWithOptionsArgs(
17  schema: arg.Spec,
18  options: arg.Options
19): arg.Result<arg.Spec> {
20  try {
21    return arg(schema, options);
22  } catch (error: any) {
23    // Ensure unknown options are handled the same way.
24    if (error.code === 'ARG_UNKNOWN_OPTION') {
25      Log.exit(error.message, 1);
26    }
27    // Otherwise rethrow the error.
28    throw error;
29  }
30}
31
32export function printHelp(info: string, usage: string, options: string, extra: string = ''): never {
33  Log.exit(
34    chalk`
35  {bold Info}
36    ${info}
37
38  {bold Usage}
39    {dim $} ${usage}
40
41  {bold Options}
42    ${options.split('\n').join('\n    ')}
43` + extra,
44    0
45  );
46}
47
48/**
49 * Enables the resolution of arguments that can either be a string or a boolean.
50 *
51 * @param args arguments that were passed to the command.
52 * @param rawMap raw map of arguments that are passed to the command.
53 * @param extraArgs extra arguments and aliases that should be resolved as string or boolean.
54 * @returns parsed arguments and project root.
55 */
56export async function resolveStringOrBooleanArgsAsync(
57  args: string[],
58  rawMap: arg.Spec,
59  extraArgs: arg.Spec
60) {
61  const combined = {
62    ...rawMap,
63    ...extraArgs,
64  };
65  // Assert any missing arguments
66  assertUnknownArgs(combined, args);
67
68  // Collapse aliases into fully qualified arguments.
69  args = collapseAliases(combined, args);
70  // Resolve all of the string or boolean arguments and the project root.
71  return _resolveStringOrBooleanArgs(extraArgs, args);
72}
73
74export function _resolveStringOrBooleanArgs(multiTypeArgs: Spec, args: string[]) {
75  // Default project root, if a custom one is defined then it will overwrite this.
76  let projectRoot: string = '';
77  // The resolved arguments.
78  const settings: Record<string, string | true | undefined> = {};
79
80  // Create a list of possible arguments, this will filter out aliases.
81  const possibleArgs = Object.entries(multiTypeArgs)
82    .filter(([, value]) => typeof value !== 'string')
83    .map(([key]) => key);
84
85  // Loop over arguments in reverse order so we can resolve if a value belongs to a flag.
86  for (let i = args.length - 1; i > -1; i--) {
87    const value = args[i];
88    // At this point we should have converted all aliases to fully qualified arguments.
89    if (value.startsWith('--')) {
90      // If we ever find an argument then it must be a boolean because we are checking in reverse
91      // and removing arguments from the array if we find a string.
92      settings[value] = true;
93    } else {
94      // Get the previous argument in the array.
95      const nextValue = i > 0 ? args[i - 1] : null;
96      if (nextValue && possibleArgs.includes(nextValue)) {
97        settings[nextValue] = value;
98        i--;
99      } else if (
100        // Prevent finding two values that are dangling
101        !projectRoot &&
102        // If the last value is not a flag and it doesn't have a recognized flag before it (instead having a string value or nothing)
103        // then it must be the project root.
104        (i === args.length - 1 || i === 0)
105      ) {
106        projectRoot = value;
107      } else {
108        // This will asserts if two strings are passed in a row and not at the end of the line.
109        throw new Error(`Unknown argument: ${value}`);
110      }
111    }
112  }
113
114  return {
115    args: settings,
116    projectRoot,
117  };
118}
119
120/** Convert all aliases to fully qualified flag names. */
121export function collapseAliases(arg: Spec, args: string[]): string[] {
122  const aliasMap = getAliasTuples(arg);
123
124  for (const [arg, alias] of aliasMap) {
125    args = replaceValue(args, arg, alias);
126  }
127
128  // Assert if there are duplicate flags after we collapse the aliases.
129  assertDuplicateArgs(args, aliasMap);
130  return args;
131}
132
133/** Assert that the spec has unknown arguments. */
134export function assertUnknownArgs(arg: Spec, args: string[]) {
135  const allowedArgs = Object.keys(arg);
136  const unknownArgs = args.filter((arg) => !allowedArgs.includes(arg) && arg.startsWith('-'));
137  if (unknownArgs.length > 0) {
138    throw new Error(`Unknown arguments: ${unknownArgs.join(', ')}`);
139  }
140}
141
142function getAliasTuples(arg: Spec): [string, string][] {
143  return Object.entries(arg).filter(([, value]) => typeof value === 'string') as [string, string][];
144}
145
146/** Asserts that a duplicate flag has been used, this naively throws without knowing if an alias or flag were used as the duplicate. */
147export function assertDuplicateArgs(args: string[], argNameAliasTuple: [string, string][]) {
148  for (const [argName, argNameAlias] of argNameAliasTuple) {
149    if (args.filter((a) => [argName, argNameAlias].includes(a)).length > 1) {
150      throw new Error(`Can only provide one instance of ${argName} or ${argNameAlias}`);
151    }
152  }
153}
154