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