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