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