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      ...options.packageManager,
104      silent: !env.EXPO_DEBUG,
105    });
106  }
107
108  // Apply Expo config to native projects
109  const configSyncingStep = logNewSection('Config syncing');
110  try {
111    await profile(configureProjectAsync)(projectRoot, {
112      platforms: options.platforms,
113    });
114    configSyncingStep.succeed('Config synced');
115  } catch (error) {
116    configSyncingStep.fail('Config sync failed');
117    throw error;
118  }
119
120  // Install CocoaPods
121  let podsInstalled: boolean = false;
122  // err towards running pod install less because it's slow and users can easily run npx pod-install afterwards.
123  if (options.platforms.includes('ios') && options.install && needsPodInstall) {
124    const { installCocoaPodsAsync } = await import('../utils/cocoapods');
125
126    podsInstalled = await installCocoaPodsAsync(projectRoot);
127  } else {
128    debug('Skipped pod install');
129  }
130
131  return {
132    nodeInstall: !!options.install,
133    podInstall: !podsInstalled,
134    platforms: options.platforms,
135    hasNewProjectFiles,
136    exp,
137  };
138}
139