1import { AndroidConfig, IOSConfig, ModPlatform } from '@expo/config-plugins';
2import chalk from 'chalk';
3import fs from 'fs';
4import path from 'path';
5
6import * as Log from '../log';
7import { directoryExistsAsync } from '../utils/dir';
8import { isInteractive } from '../utils/interactive';
9import { logNewSection } from '../utils/ora';
10import { confirmAsync } from '../utils/prompts';
11
12/** Delete the input native folders and print a loading step. */
13export async function clearNativeFolder(projectRoot: string, folders: string[]) {
14  const step = logNewSection(`Clearing ${folders.join(', ')}`);
15  try {
16    await Promise.all(
17      folders.map((folderName) =>
18        fs.promises.rm(path.join(projectRoot, folderName), {
19          recursive: true,
20          force: true,
21        })
22      )
23    );
24    step.succeed(`Cleared ${folders.join(', ')} code`);
25  } catch (error: any) {
26    step.fail(`Failed to delete ${folders.join(', ')} code: ${error.message}`);
27    throw error;
28  }
29}
30
31/**
32 * Returns `true` if a certain subset of required Android project files are intact.
33 *
34 * This isn't perfect but it serves the purpose of indicating that the user should
35 * be warned to nuke the project files, most commonly when git is cleared and the root folder
36 * remains in memory.
37 */
38export async function hasRequiredAndroidFilesAsync(projectRoot: string): Promise<boolean> {
39  try {
40    await Promise.all([
41      AndroidConfig.Paths.getAppBuildGradleAsync(projectRoot),
42      AndroidConfig.Paths.getProjectBuildGradleAsync(projectRoot),
43      AndroidConfig.Paths.getAndroidManifestAsync(projectRoot),
44      AndroidConfig.Paths.getMainApplicationAsync(projectRoot),
45    ]);
46    return true;
47  } catch {
48    return false;
49  }
50}
51
52/** Returns `true` if a certain subset of required iOS project files are intact. */
53export async function hasRequiredIOSFilesAsync(projectRoot: string) {
54  try {
55    // If any of the following required files are missing, then the project is malformed.
56    await Promise.all([
57      IOSConfig.Paths.getAppDelegate(projectRoot),
58      IOSConfig.Paths.getAllXcodeProjectPaths(projectRoot),
59      IOSConfig.Paths.getAllInfoPlistPaths(projectRoot),
60      IOSConfig.Paths.getAllPBXProjectPaths(projectRoot),
61    ]);
62    return true;
63  } catch {
64    return false;
65  }
66}
67
68/**
69 * Filter out platforms that do not have an existing platform folder.
70 * If the user wants to validate that neither of ['ios', 'android'] are malformed then we should
71 * first check that both `ios` and `android` folders exist.
72 *
73 * This optimization prevents us from prompting to clear a "malformed" project that doesn't exist yet.
74 */
75async function filterPlatformsThatDoNotExistAsync(
76  projectRoot: string,
77  platforms: ModPlatform[]
78): Promise<ModPlatform[]> {
79  const valid = await Promise.all(
80    platforms.map(async (platform) => {
81      if (await directoryExistsAsync(path.join(projectRoot, platform))) {
82        return platform;
83      }
84      return null;
85    })
86  );
87  return valid.filter(Boolean) as ModPlatform[];
88}
89
90/** Get a list of native platforms that have existing directories which contain malformed projects. */
91export async function getMalformedNativeProjectsAsync(
92  projectRoot: string,
93  platforms: ModPlatform[]
94): Promise<ModPlatform[]> {
95  const VERIFIERS: Record<ModPlatform, (root: string) => Promise<boolean>> = {
96    android: hasRequiredAndroidFilesAsync,
97    ios: hasRequiredIOSFilesAsync,
98  };
99
100  const checkPlatforms = await filterPlatformsThatDoNotExistAsync(projectRoot, platforms);
101  return (
102    await Promise.all(
103      checkPlatforms.map(async (platform) => {
104        if (await VERIFIERS[platform](projectRoot)) {
105          return false;
106        }
107        return platform;
108      })
109    )
110  ).filter(Boolean) as ModPlatform[];
111}
112
113export async function promptToClearMalformedNativeProjectsAsync(
114  projectRoot: string,
115  checkPlatforms: ModPlatform[]
116) {
117  const platforms = await getMalformedNativeProjectsAsync(projectRoot, checkPlatforms);
118
119  if (!platforms.length) {
120    return;
121  }
122
123  const displayPlatforms = platforms.map((platform) => chalk.cyan(platform));
124  // Prompt which platforms to reset.
125  const message =
126    platforms.length > 1
127      ? `The ${displayPlatforms[0]} and ${displayPlatforms[1]} projects are malformed`
128      : `The ${displayPlatforms[0]} project is malformed`;
129
130  if (
131    // If the process is non-interactive, default to clearing the malformed native project.
132    // This would only happen on re-running eject.
133    !isInteractive() ||
134    // Prompt to clear the native folders.
135    (await confirmAsync({
136      message: `${message}, would you like to clear the project files and reinitialize them?`,
137      initial: true,
138    }))
139  ) {
140    if (!isInteractive()) {
141      Log.warn(`${message}, project files will be cleared and reinitialized.`);
142    }
143    await clearNativeFolder(projectRoot, platforms);
144  } else {
145    // Warn the user that the process may fail.
146    Log.warn('Continuing with malformed native projects');
147  }
148}
149