1import { IOSConfig } from '@expo/config-plugins';
2import chalk from 'chalk';
3import path from 'path';
4
5import * as Log from '../../../log';
6import { CommandError } from '../../../utils/errors';
7import { profile } from '../../../utils/profile';
8import { selectAsync } from '../../../utils/prompts';
9import { Options, ProjectInfo, XcodeConfiguration } from '../XcodeBuild.types';
10
11type NativeSchemeProps = {
12  name: string;
13  osType?: string;
14};
15
16export async function resolveNativeSchemePropsAsync(
17  projectRoot: string,
18  options: Pick<Options, 'scheme' | 'configuration'>,
19  xcodeProject: ProjectInfo
20): Promise<NativeSchemeProps> {
21  return (
22    (await promptOrQueryNativeSchemeAsync(projectRoot, options)) ??
23    getDefaultNativeScheme(projectRoot, options, xcodeProject)
24  );
25}
26
27/** Resolve the native iOS build `scheme` for a given `configuration`. If the `scheme` isn't provided then the user will be prompted to select one. */
28export async function promptOrQueryNativeSchemeAsync(
29  projectRoot: string,
30  { scheme, configuration }: { scheme?: string | boolean; configuration?: XcodeConfiguration }
31): Promise<NativeSchemeProps | null> {
32  const schemes = IOSConfig.BuildScheme.getRunnableSchemesFromXcodeproj(projectRoot, {
33    configuration,
34  });
35  if (!schemes.length) {
36    throw new CommandError('IOS_MALFORMED', 'No native iOS build schemes found');
37  }
38
39  if (scheme === true) {
40    if (schemes.length === 1) {
41      Log.log(`Auto selecting only available scheme: ${schemes[0].name}`);
42      return schemes[0];
43    }
44    const resolvedSchemeName = await selectAsync(
45      'Select a scheme',
46      schemes.map((value) => {
47        const isApp =
48          value.type === IOSConfig.Target.TargetType.APPLICATION && value.osType === 'iOS';
49        return {
50          value: value.name,
51          title: isApp ? chalk.bold(value.name) + chalk.gray(' (app)') : value.name,
52        };
53      }),
54      {
55        nonInteractiveHelp: `--scheme: argument must be provided with a string in non-interactive mode. Valid choices are: ${schemes.join(
56          ', '
57        )}`,
58      }
59    );
60    return schemes.find(({ name }) => resolvedSchemeName === name) ?? null;
61  }
62  // Attempt to match the schemes up so we can open the correct simulator
63  return scheme ? schemes.find(({ name }) => name === scheme) || { name: scheme } : null;
64}
65
66export function getDefaultNativeScheme(
67  projectRoot: string,
68  options: Pick<Options, 'configuration'>,
69  xcodeProject: ProjectInfo
70): NativeSchemeProps {
71  // If the resolution failed then we should just use the first runnable scheme that
72  // matches the provided configuration.
73  const resolvedScheme = profile(IOSConfig.BuildScheme.getRunnableSchemesFromXcodeproj)(
74    projectRoot,
75    {
76      configuration: options.configuration,
77    }
78  )[0];
79
80  // If we couldn't find the scheme, then we'll guess at it,
81  // this is needed for cases where the native code hasn't been generated yet.
82  if (resolvedScheme) {
83    return resolvedScheme;
84  }
85  return {
86    name: path.basename(xcodeProject.name, path.extname(xcodeProject.name)),
87  };
88}
89