1import { ExpoConfig, PackageJSONConfig } from '@expo/config';
2import { ModPlatform } from '@expo/config-plugins';
3import chalk from 'chalk';
4
5import * as Log from '../log';
6import { AbortCommandError, SilentError } from '../utils/errors';
7import { logNewSection } from '../utils/ora';
8import { profile } from '../utils/profile';
9import { copyTemplateFilesAsync, createCopyFilesSuccessMessage } from './copyTemplateFiles';
10import { cloneTemplateAsync } from './resolveTemplate';
11import { DependenciesModificationResults, updatePackageJSONAsync } from './updatePackageJson';
12import { writeMetroConfig } from './writeMetroConfig';
13
14/**
15 * Creates local native files from an input template file path.
16 *
17 * @return `true` if the project is ejecting, and `false` if it's syncing.
18 */
19export async function updateFromTemplateAsync(
20  projectRoot: string,
21  {
22    exp,
23    pkg,
24    template,
25    templateDirectory,
26    platforms,
27    skipDependencyUpdate,
28  }: {
29    /** Expo Config */
30    exp: ExpoConfig;
31    /** package.json as JSON */
32    pkg: PackageJSONConfig;
33    /** Template reference ID. */
34    template?: string;
35    /** Directory to write the template to before copying into the project. */
36    templateDirectory?: string;
37    /** List of platforms to clone. */
38    platforms: ModPlatform[];
39    /** List of dependencies to skip updating. */
40    skipDependencyUpdate?: string[];
41  }
42): Promise<
43  {
44    /** Indicates if new files were created in the project. */
45    hasNewProjectFiles: boolean;
46    /** Indicates that the project needs to run `pod install` */
47    needsPodInstall: boolean;
48  } & DependenciesModificationResults
49> {
50  if (!templateDirectory) {
51    const temporary = await import('tempy');
52    templateDirectory = temporary.directory();
53  }
54
55  const copiedPaths = await profile(cloneTemplateAndCopyToProjectAsync)({
56    projectRoot,
57    template,
58    templateDirectory,
59    exp,
60    platforms,
61  });
62
63  profile(writeMetroConfig)(projectRoot, { pkg, templateDirectory });
64
65  const depsResults = await profile(updatePackageJSONAsync)(projectRoot, {
66    templateDirectory,
67    pkg,
68    skipDependencyUpdate,
69  });
70
71  return {
72    hasNewProjectFiles: !!copiedPaths.length,
73    // If the iOS folder changes or new packages are added, we should rerun pod install.
74    needsPodInstall:
75      copiedPaths.includes('ios') ||
76      depsResults.hasNewDependencies ||
77      depsResults.hasNewDevDependencies,
78    ...depsResults,
79  };
80}
81
82/**
83 * Extract the template and copy the ios and android directories over to the project directory.
84 *
85 * @return `true` if any project files were created.
86 */
87async function cloneTemplateAndCopyToProjectAsync({
88  projectRoot,
89  templateDirectory,
90  template,
91  exp,
92  platforms,
93}: {
94  projectRoot: string;
95  templateDirectory: string;
96  template?: string;
97  exp: Pick<ExpoConfig, 'name' | 'sdkVersion'>;
98  platforms: ModPlatform[];
99}): Promise<string[]> {
100  const ora = logNewSection(
101    'Creating native project directories (./ios and ./android) and updating .gitignore'
102  );
103
104  try {
105    await cloneTemplateAsync({ templateDirectory, template, exp, ora });
106
107    const results = await copyTemplateFilesAsync(projectRoot, {
108      templateDirectory,
109      platforms,
110    });
111
112    ora.succeed(createCopyFilesSuccessMessage(platforms, results));
113
114    return results.copiedPaths;
115  } catch (e: any) {
116    if (!(e instanceof AbortCommandError)) {
117      Log.error(e.message);
118    }
119    ora.fail('Failed to create the native project.');
120    Log.log(
121      chalk.yellow(
122        'You may want to delete the `./ios` and/or `./android` directories before trying again.'
123      )
124    );
125    throw new SilentError(e);
126  }
127}
128