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.getAllXcodeProjectPaths(projectRoot),
58      IOSConfig.Paths.getAllPBXProjectPaths(projectRoot),
59    ]);
60    return true;
61  } catch {
62    return false;
63  }
64}
65
66/**
67 * Filter out platforms that do not have an existing platform folder.
68 * If the user wants to validate that neither of ['ios', 'android'] are malformed then we should
69 * first check that both `ios` and `android` folders exist.
70 *
71 * This optimization prevents us from prompting to clear a "malformed" project that doesn't exist yet.
72 */
73async function filterPlatformsThatDoNotExistAsync(
74  projectRoot: string,
75  platforms: ModPlatform[]
76): Promise<ModPlatform[]> {
77  const valid = await Promise.all(
78    platforms.map(async (platform) => {
79      if (await directoryExistsAsync(path.join(projectRoot, platform))) {
80        return platform;
81      }
82      return null;
83    })
84  );
85  return valid.filter(Boolean) as ModPlatform[];
86}
87
88/** Get a list of native platforms that have existing directories which contain malformed projects. */
89export async function getMalformedNativeProjectsAsync(
90  projectRoot: string,
91  platforms: ModPlatform[]
92): Promise<ModPlatform[]> {
93  const VERIFIERS: Record<ModPlatform, (root: string) => Promise<boolean>> = {
94    android: hasRequiredAndroidFilesAsync,
95    ios: hasRequiredIOSFilesAsync,
96  };
97
98  const checkPlatforms = await filterPlatformsThatDoNotExistAsync(projectRoot, platforms);
99  return (
100    await Promise.all(
101      checkPlatforms.map(async (platform) => {
102        if (await VERIFIERS[platform](projectRoot)) {
103          return false;
104        }
105        return platform;
106      })
107    )
108  ).filter(Boolean) as ModPlatform[];
109}
110
111export async function promptToClearMalformedNativeProjectsAsync(
112  projectRoot: string,
113  checkPlatforms: ModPlatform[]
114) {
115  const platforms = await getMalformedNativeProjectsAsync(projectRoot, checkPlatforms);
116
117  if (!platforms.length) {
118    return;
119  }
120
121  const displayPlatforms = platforms.map((platform) => chalk.cyan(platform));
122  // Prompt which platforms to reset.
123  const message =
124    platforms.length > 1
125      ? `The ${displayPlatforms[0]} and ${displayPlatforms[1]} projects are malformed`
126      : `The ${displayPlatforms[0]} project is malformed`;
127
128  if (
129    // If the process is non-interactive, default to clearing the malformed native project.
130    // This would only happen on re-running eject.
131    !isInteractive() ||
132    // Prompt to clear the native folders.
133    (await confirmAsync({
134      message: `${message}, would you like to clear the project files and reinitialize them?`,
135      initial: true,
136    }))
137  ) {
138    if (!isInteractive()) {
139      Log.warn(`${message}, project files will be cleared and reinitialized.`);
140    }
141    await clearNativeFolder(projectRoot, platforms);
142  } else {
143    // Warn the user that the process may fail.
144    Log.warn('Continuing with malformed native projects');
145  }
146}
147