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