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