1import { ExpoConfig } from '@expo/config';
2import { ModPlatform } from '@expo/config-plugins';
3
4import * as Log from '../log';
5import { installNodeDependenciesAsync, resolvePackageManager } from '../utils/nodeModules';
6import { logNewSection } from '../utils/ora';
7import { profile } from '../utils/profile';
8import { clearNativeFolder, promptToClearMalformedNativeProjectsAsync } from './clearNativeFolder';
9import { configureProjectAsync } from './configureProjectAsync';
10import { ensureConfigAsync } from './ensureConfigAsync';
11import { assertPlatforms, ensureValidPlatforms, resolveTemplateOption } from './resolveOptions';
12import { updateFromTemplateAsync } from './updateFromTemplate';
13
14export type PrebuildResults = {
15  /** Expo config. */
16  exp: ExpoConfig;
17  /** Indicates if the process created new files. */
18  hasNewProjectFiles: boolean;
19  /** The platforms that were prebuilt. */
20  platforms: ModPlatform[];
21  /** Indicates if pod install was run. */
22  podInstall: boolean;
23  /** Indicates if node modules were installed. */
24  nodeInstall: boolean;
25  /** Indicates which package manager the project is using. */
26  packageManager: string;
27};
28
29/**
30 * Entry point into the prebuild process, delegates to other helpers to perform various steps.
31 *
32 * 0. Attempt to clean the project folders.
33 * 1. Create native projects (ios, android).
34 * 2. Install node modules.
35 * 3. Apply config to native projects.
36 * 4. Install CocoaPods.
37 */
38export async function prebuildAsync(
39  projectRoot: string,
40  options: {
41    /** Should install node modules and cocoapods. */
42    install: boolean;
43    /** List of platforms to prebuild. */
44    platforms: ModPlatform[];
45    /** Should delete the native folders before attempting to prebuild. */
46    clean?: boolean;
47    /** URL or file path to the prebuild template. */
48    template?: string;
49    /** Name of the node package manager to install with. */
50    packageManager?: 'npm' | 'yarn';
51    /** List of node modules to skip updating. */
52    skipDependencyUpdate?: string[];
53  }
54): Promise<PrebuildResults | null> {
55  if (options.clean) {
56    const { maybeBailOnGitStatusAsync } = await import('../utils/git');
57    // Clean the project folders...
58    if (await maybeBailOnGitStatusAsync()) {
59      return null;
60    }
61    // Clear the native folders before syncing
62    await clearNativeFolder(projectRoot, options.platforms);
63  } else {
64    // Check if the existing project folders are malformed.
65    await promptToClearMalformedNativeProjectsAsync(projectRoot, options.platforms);
66  }
67
68  // Warn if the project is attempting to prebuild an unsupported platform (iOS on Windows).
69  options.platforms = ensureValidPlatforms(options.platforms);
70  // Assert if no platforms are left over after filtering.
71  assertPlatforms(options.platforms);
72
73  // Get the Expo config, create it if missing.
74  const { exp, pkg } = await ensureConfigAsync(projectRoot, { platforms: options.platforms });
75
76  // Create native projects from template.
77  const { hasNewProjectFiles, needsPodInstall, hasNewDependencies } = await updateFromTemplateAsync(
78    projectRoot,
79    {
80      exp,
81      pkg,
82      template: options.template != null ? resolveTemplateOption(options.template) : undefined,
83      platforms: options.platforms,
84      skipDependencyUpdate: options.skipDependencyUpdate,
85    }
86  );
87
88  // Install node modules
89  const packageManager = resolvePackageManager({
90    install: options.install,
91    npm: options.packageManager === 'npm',
92    yarn: options.packageManager === 'yarn',
93  });
94
95  if (options.install) {
96    await installNodeDependenciesAsync(projectRoot, packageManager, {
97      // We delete the dependencies when new ones are added because native packages are more fragile.
98      // npm doesn't work well so we always run the cleaning step when npm is used in favor of yarn.
99      clean: hasNewDependencies || packageManager === 'npm',
100    });
101  }
102
103  // Apply Expo config to native projects
104  const configSyncingStep = logNewSection('Config syncing');
105  try {
106    await profile(configureProjectAsync)(projectRoot, {
107      platforms: options.platforms,
108    });
109    configSyncingStep.succeed('Config synced');
110  } catch (error) {
111    configSyncingStep.fail('Config sync failed');
112    throw error;
113  }
114
115  // Install CocoaPods
116  let podsInstalled: boolean = false;
117  // err towards running pod install less because it's slow and users can easily run npx pod-install afterwards.
118  if (options.platforms.includes('ios') && options.install && needsPodInstall) {
119    const { installCocoaPodsAsync } = await import('../utils/cocoapods');
120
121    podsInstalled = await installCocoaPodsAsync(projectRoot);
122  } else {
123    Log.debug('Skipped pod install');
124  }
125
126  return {
127    packageManager,
128    nodeInstall: options.install,
129    podInstall: !podsInstalled,
130    platforms: options.platforms,
131    hasNewProjectFiles,
132    exp,
133  };
134}
135