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