1import * as PackageManager from '@expo/package-manager';
2
3import * as Log from '../log';
4import { CommandError } from '../utils/errors';
5
6export type Options = Pick<PackageManager.CreateForProjectOptions, 'npm' | 'pnpm' | 'yarn'> & {
7  /** Check which packages need to be updated, does not install any provided packages. */
8  check?: boolean;
9  /** Should the dependencies be fixed automatically. */
10  fix?: boolean;
11  /** Should disable install output, used for commands like `prebuild` that run install internally. */
12  silent?: boolean;
13};
14
15function resolveOptions(options: Options): Options {
16  if (options.fix && options.check) {
17    throw new CommandError('BAD_ARGS', 'Specify at most one of: --check, --fix');
18  }
19  if ([options.npm, options.pnpm, options.yarn].filter(Boolean).length > 1) {
20    throw new CommandError('BAD_ARGS', 'Specify at most one of: --npm, --pnpm, --yarn');
21  }
22  return {
23    ...options,
24  };
25}
26
27/** Given a list of CLI args, return a sorted set of args based on categories used in a complex command. */
28export function parseVariadicArguments(argv: string[]): {
29  variadic: string[];
30  extras: string[];
31  flags: Record<string, boolean>;
32} {
33  const variadic: string[] = [];
34  const flags: Record<string, boolean> = {};
35
36  for (const arg of argv) {
37    if (!arg.startsWith('-')) {
38      variadic.push(arg);
39    } else if (arg === '--') {
40      break;
41    } else {
42      flags[arg] = true;
43    }
44  }
45
46  // Everything after `--` that is not an option is passed to the underlying install command.
47  const extras: string[] = [];
48
49  const extraOperator = argv.indexOf('--');
50  if (extraOperator > -1 && argv.length > extraOperator + 1) {
51    const extraArgs = argv.slice(extraOperator + 1);
52    if (extraArgs.includes('--')) {
53      throw new CommandError('BAD_ARGS', 'Unexpected multiple --');
54    }
55    extras.push(...extraArgs);
56    Log.debug('Extra arguments: ' + extras.join(', '));
57  }
58
59  Log.debug(`Parsed arguments (variadic: %O, flags: %O, extra: %O)`, variadic, flags, extras);
60
61  return {
62    variadic,
63    flags,
64    extras,
65  };
66}
67
68export async function resolveArgsAsync(
69  argv: string[]
70): Promise<{ variadic: string[]; options: Options; extras: string[] }> {
71  const { variadic, extras, flags } = parseVariadicArguments(argv);
72
73  assertUnexpectedObjectKeys(['--check', '--fix', '--npm', '--pnpm', '--yarn'], flags);
74
75  return {
76    // Variadic arguments like `npx expo install react react-dom` -> ['react', 'react-dom']
77    variadic,
78    options: resolveOptions({
79      fix: !!flags['--fix'],
80      check: !!flags['--check'],
81      yarn: !!flags['--yarn'],
82      npm: !!flags['--npm'],
83      pnpm: !!flags['--pnpm'],
84    }),
85    extras,
86  };
87}
88
89function assertUnexpectedObjectKeys(keys: string[], obj: Record<string, any>): void {
90  const unexpectedKeys = Object.keys(obj).filter((key) => !keys.includes(key));
91  if (unexpectedKeys.length > 0) {
92    throw new CommandError('BAD_ARGS', `Unexpected: ${unexpectedKeys.join(', ')}`);
93  }
94}
95