1import { ExpoConfig, PackageJSONConfig } from '@expo/config';
2import { ModPlatform } from '@expo/config-plugins';
3import chalk from 'chalk';
4
5import { copyTemplateFilesAsync, createCopyFilesSuccessMessage } from './copyTemplateFiles';
6import { cloneTemplateAsync } from './resolveTemplate';
7import { DependenciesModificationResults, updatePackageJSONAsync } from './updatePackageJson';
8import { validateTemplatePlatforms } from './validateTemplatePlatforms';
9import * as Log from '../log';
10import { AbortCommandError, SilentError } from '../utils/errors';
11import { logNewSection } from '../utils/ora';
12import { profile } from '../utils/profile';
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  const depsResults = await profile(updatePackageJSONAsync)(projectRoot, {
64    templateDirectory,
65    pkg,
66    skipDependencyUpdate,
67  });
68
69  return {
70    hasNewProjectFiles: !!copiedPaths.length,
71    // If the iOS folder changes or new packages are added, we should rerun pod install.
72    needsPodInstall:
73      copiedPaths.includes('ios') ||
74      depsResults.hasNewDependencies ||
75      depsResults.hasNewDevDependencies,
76    ...depsResults,
77  };
78}
79
80/**
81 * Extract the template and copy the ios and android directories over to the project directory.
82 *
83 * @return `true` if any project files were created.
84 */
85async function cloneTemplateAndCopyToProjectAsync({
86  projectRoot,
87  templateDirectory,
88  template,
89  exp,
90  platforms: unknownPlatforms,
91}: {
92  projectRoot: string;
93  templateDirectory: string;
94  template?: string;
95  exp: Pick<ExpoConfig, 'name' | 'sdkVersion'>;
96  platforms: ModPlatform[];
97}): Promise<string[]> {
98  const ora = logNewSection(
99    'Creating native project directories (./ios and ./android) and updating .gitignore'
100  );
101
102  try {
103    await cloneTemplateAsync({ templateDirectory, template, exp, ora });
104
105    const platforms = await validateTemplatePlatforms({
106      templateDirectory,
107      platforms: unknownPlatforms,
108    });
109
110    const results = await copyTemplateFilesAsync(projectRoot, {
111      templateDirectory,
112      platforms,
113    });
114
115    ora.succeed(createCopyFilesSuccessMessage(platforms, results));
116
117    return results.copiedPaths;
118  } catch (e: any) {
119    if (!(e instanceof AbortCommandError)) {
120      Log.error(e.message);
121    }
122    ora.fail('Failed to create the native project.');
123    Log.log(
124      chalk.yellow(
125        'You may want to delete the `./ios` and/or `./android` directories before trying again.'
126      )
127    );
128    throw new SilentError(e);
129  }
130}
131