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