1import { ExpoConfig } from '@expo/config';
2import { ModPlatform } from '@expo/config-plugins';
3
4import { installAsync } from '../install/installAsync';
5import * as Log from '../log';
6import { env } from '../utils/env';
7import { clearNodeModulesAsync } from '../utils/nodeModules';
8import { logNewSection } from '../utils/ora';
9import { profile } from '../utils/profile';
10import { clearNativeFolder, promptToClearMalformedNativeProjectsAsync } from './clearNativeFolder';
11import { configureProjectAsync } from './configureProjectAsync';
12import { ensureConfigAsync } from './ensureConfigAsync';
13import { assertPlatforms, ensureValidPlatforms, resolveTemplateOption } from './resolveOptions';
14import { updateFromTemplateAsync } from './updateFromTemplate';
15
16export type PrebuildResults = {
17  /** Expo config. */
18  exp: ExpoConfig;
19  /** Indicates if the process created new files. */
20  hasNewProjectFiles: boolean;
21  /** The platforms that were prebuilt. */
22  platforms: ModPlatform[];
23  /** Indicates if pod install was run. */
24  podInstall: boolean;
25  /** Indicates if node modules were installed. */
26  nodeInstall: boolean;
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?: {
51      npm?: boolean;
52      yarn?: boolean;
53      pnpm?: boolean;
54    };
55    /** List of node modules to skip updating. */
56    skipDependencyUpdate?: string[];
57  }
58): Promise<PrebuildResults | null> {
59  if (options.clean) {
60    const { maybeBailOnGitStatusAsync } = await import('../utils/git');
61    // Clean the project folders...
62    if (await maybeBailOnGitStatusAsync()) {
63      return null;
64    }
65    // Clear the native folders before syncing
66    await clearNativeFolder(projectRoot, options.platforms);
67  } else {
68    // Check if the existing project folders are malformed.
69    await promptToClearMalformedNativeProjectsAsync(projectRoot, options.platforms);
70  }
71
72  // Warn if the project is attempting to prebuild an unsupported platform (iOS on Windows).
73  options.platforms = ensureValidPlatforms(options.platforms);
74  // Assert if no platforms are left over after filtering.
75  assertPlatforms(options.platforms);
76
77  // Get the Expo config, create it if missing.
78  const { exp, pkg } = await ensureConfigAsync(projectRoot, { platforms: options.platforms });
79
80  // Create native projects from template.
81  const { hasNewProjectFiles, needsPodInstall, hasNewDependencies } = await updateFromTemplateAsync(
82    projectRoot,
83    {
84      exp,
85      pkg,
86      template: options.template != null ? resolveTemplateOption(options.template) : undefined,
87      platforms: options.platforms,
88      skipDependencyUpdate: options.skipDependencyUpdate,
89    }
90  );
91
92  // Install node modules
93  if (options.install) {
94    if (hasNewDependencies && options.packageManager?.npm) {
95      await clearNodeModulesAsync(projectRoot);
96    }
97
98    await installAsync([], {
99      ...options.packageManager,
100      silent: !env.EXPO_DEBUG,
101    });
102  }
103
104  // Apply Expo config to native projects
105  const configSyncingStep = logNewSection('Config syncing');
106  try {
107    await profile(configureProjectAsync)(projectRoot, {
108      platforms: options.platforms,
109    });
110    configSyncingStep.succeed('Config synced');
111  } catch (error) {
112    configSyncingStep.fail('Config sync failed');
113    throw error;
114  }
115
116  // Install CocoaPods
117  let podsInstalled: boolean = false;
118  // err towards running pod install less because it's slow and users can easily run npx pod-install afterwards.
119  if (options.platforms.includes('ios') && options.install && needsPodInstall) {
120    const { installCocoaPodsAsync } = await import('../utils/cocoapods');
121
122    podsInstalled = await installCocoaPodsAsync(projectRoot);
123  } else {
124    Log.debug('Skipped pod install');
125  }
126
127  return {
128    nodeInstall: !!options.install,
129    podInstall: !podsInstalled,
130    platforms: options.platforms,
131    hasNewProjectFiles,
132    exp,
133  };
134}
135