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 platformDirectories = unknownPlatforms
99    .map((platform) => `./${platform}`)
100    .reverse()
101    .join(' and ');
102
103  const pluralized = unknownPlatforms.length > 1 ? 'directories' : 'directory';
104  const ora = logNewSection(
105    `Creating native project ${pluralized} (${platformDirectories}) 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      templateDirectory,
118      platforms,
119    });
120
121    ora.succeed(createCopyFilesSuccessMessage(platforms, results));
122
123    return results.copiedPaths;
124  } catch (e: any) {
125    if (!(e instanceof AbortCommandError)) {
126      Log.error(e.message);
127    }
128    ora.fail('Failed to create the native project.');
129    Log.log(
130      chalk.yellow(
131        'You may want to delete the `./ios` and/or `./android` directories before trying again.'
132      )
133    );
134    throw new SilentError(e);
135  }
136}
137