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