18d307f52SEvan Baconimport { AndroidConfig, IOSConfig, ModPlatform } from '@expo/config-plugins';
28d307f52SEvan Baconimport chalk from 'chalk';
38d307f52SEvan Baconimport fs from 'fs';
48d307f52SEvan Baconimport path from 'path';
58d307f52SEvan Bacon
68d307f52SEvan Baconimport * as Log from '../log';
78d307f52SEvan Baconimport { directoryExistsAsync } from '../utils/dir';
829128565SEvan Baconimport { isInteractive } from '../utils/interactive';
98d307f52SEvan Baconimport { logNewSection } from '../utils/ora';
108d307f52SEvan Baconimport { confirmAsync } from '../utils/prompts';
118d307f52SEvan Bacon
12*edc75823SEvan Bacontype ArbitraryPlatform = ModPlatform | string;
13*edc75823SEvan Bacon
148d307f52SEvan Bacon/** Delete the input native folders and print a loading step. */
158d307f52SEvan Baconexport async function clearNativeFolder(projectRoot: string, folders: string[]) {
168d307f52SEvan Bacon  const step = logNewSection(`Clearing ${folders.join(', ')}`);
178d307f52SEvan Bacon  try {
188d307f52SEvan Bacon    await Promise.all(
198d307f52SEvan Bacon      folders.map((folderName) =>
208d307f52SEvan Bacon        fs.promises.rm(path.join(projectRoot, folderName), {
218d307f52SEvan Bacon          recursive: true,
228d307f52SEvan Bacon          force: true,
238d307f52SEvan Bacon        })
248d307f52SEvan Bacon      )
258d307f52SEvan Bacon    );
268d307f52SEvan Bacon    step.succeed(`Cleared ${folders.join(', ')} code`);
278d307f52SEvan Bacon  } catch (error: any) {
288d307f52SEvan Bacon    step.fail(`Failed to delete ${folders.join(', ')} code: ${error.message}`);
298d307f52SEvan Bacon    throw error;
308d307f52SEvan Bacon  }
318d307f52SEvan Bacon}
328d307f52SEvan Bacon
338d307f52SEvan Bacon/**
348d307f52SEvan Bacon * Returns `true` if a certain subset of required Android project files are intact.
358d307f52SEvan Bacon *
368d307f52SEvan Bacon * This isn't perfect but it serves the purpose of indicating that the user should
378d307f52SEvan Bacon * be warned to nuke the project files, most commonly when git is cleared and the root folder
388d307f52SEvan Bacon * remains in memory.
398d307f52SEvan Bacon */
408d307f52SEvan Baconexport async function hasRequiredAndroidFilesAsync(projectRoot: string): Promise<boolean> {
418d307f52SEvan Bacon  try {
428d307f52SEvan Bacon    await Promise.all([
438d307f52SEvan Bacon      AndroidConfig.Paths.getAppBuildGradleAsync(projectRoot),
448d307f52SEvan Bacon      AndroidConfig.Paths.getProjectBuildGradleAsync(projectRoot),
458d307f52SEvan Bacon      AndroidConfig.Paths.getAndroidManifestAsync(projectRoot),
468d307f52SEvan Bacon      AndroidConfig.Paths.getMainApplicationAsync(projectRoot),
478d307f52SEvan Bacon    ]);
488d307f52SEvan Bacon    return true;
4998ecfc87SJames Ide  } catch {
508d307f52SEvan Bacon    return false;
518d307f52SEvan Bacon  }
528d307f52SEvan Bacon}
538d307f52SEvan Bacon
548d307f52SEvan Bacon/** Returns `true` if a certain subset of required iOS project files are intact. */
558d307f52SEvan Baconexport async function hasRequiredIOSFilesAsync(projectRoot: string) {
568d307f52SEvan Bacon  try {
578d307f52SEvan Bacon    // If any of the following required files are missing, then the project is malformed.
588d307f52SEvan Bacon    await Promise.all([
598d307f52SEvan Bacon      IOSConfig.Paths.getAllXcodeProjectPaths(projectRoot),
608d307f52SEvan Bacon      IOSConfig.Paths.getAllPBXProjectPaths(projectRoot),
618d307f52SEvan Bacon    ]);
628d307f52SEvan Bacon    return true;
638d307f52SEvan Bacon  } catch {
648d307f52SEvan Bacon    return false;
658d307f52SEvan Bacon  }
668d307f52SEvan Bacon}
678d307f52SEvan Bacon
688d307f52SEvan Bacon/**
698d307f52SEvan Bacon * Filter out platforms that do not have an existing platform folder.
708d307f52SEvan Bacon * If the user wants to validate that neither of ['ios', 'android'] are malformed then we should
718d307f52SEvan Bacon * first check that both `ios` and `android` folders exist.
728d307f52SEvan Bacon *
738d307f52SEvan Bacon * This optimization prevents us from prompting to clear a "malformed" project that doesn't exist yet.
748d307f52SEvan Bacon */
758d307f52SEvan Baconasync function filterPlatformsThatDoNotExistAsync(
768d307f52SEvan Bacon  projectRoot: string,
77*edc75823SEvan Bacon  platforms: ArbitraryPlatform[]
78*edc75823SEvan Bacon): Promise<ArbitraryPlatform[]> {
798d307f52SEvan Bacon  const valid = await Promise.all(
808d307f52SEvan Bacon    platforms.map(async (platform) => {
818d307f52SEvan Bacon      if (await directoryExistsAsync(path.join(projectRoot, platform))) {
828d307f52SEvan Bacon        return platform;
838d307f52SEvan Bacon      }
848d307f52SEvan Bacon      return null;
858d307f52SEvan Bacon    })
868d307f52SEvan Bacon  );
87*edc75823SEvan Bacon  return valid.filter(Boolean) as ArbitraryPlatform[];
888d307f52SEvan Bacon}
898d307f52SEvan Bacon
908d307f52SEvan Bacon/** Get a list of native platforms that have existing directories which contain malformed projects. */
918d307f52SEvan Baconexport async function getMalformedNativeProjectsAsync(
928d307f52SEvan Bacon  projectRoot: string,
93*edc75823SEvan Bacon  platforms: ArbitraryPlatform[]
94*edc75823SEvan Bacon): Promise<ArbitraryPlatform[]> {
95*edc75823SEvan Bacon  const VERIFIERS: Record<ArbitraryPlatform, (root: string) => Promise<boolean>> = {
968d307f52SEvan Bacon    android: hasRequiredAndroidFilesAsync,
978d307f52SEvan Bacon    ios: hasRequiredIOSFilesAsync,
988d307f52SEvan Bacon  };
998d307f52SEvan Bacon
100608dafddSCedric van Putten  const checkablePlatforms = platforms.filter((platform) => platform in VERIFIERS);
101608dafddSCedric van Putten  const checkPlatforms = await filterPlatformsThatDoNotExistAsync(projectRoot, checkablePlatforms);
1028d307f52SEvan Bacon  return (
1038d307f52SEvan Bacon    await Promise.all(
1048d307f52SEvan Bacon      checkPlatforms.map(async (platform) => {
105*edc75823SEvan Bacon        if (!VERIFIERS[platform]) {
106*edc75823SEvan Bacon          return false;
107*edc75823SEvan Bacon        }
1088d307f52SEvan Bacon        if (await VERIFIERS[platform](projectRoot)) {
1098d307f52SEvan Bacon          return false;
1108d307f52SEvan Bacon        }
1118d307f52SEvan Bacon        return platform;
1128d307f52SEvan Bacon      })
1138d307f52SEvan Bacon    )
114*edc75823SEvan Bacon  ).filter(Boolean) as ArbitraryPlatform[];
1158d307f52SEvan Bacon}
1168d307f52SEvan Bacon
1178d307f52SEvan Baconexport async function promptToClearMalformedNativeProjectsAsync(
1188d307f52SEvan Bacon  projectRoot: string,
119*edc75823SEvan Bacon  checkPlatforms: ArbitraryPlatform[]
1208d307f52SEvan Bacon) {
1218d307f52SEvan Bacon  const platforms = await getMalformedNativeProjectsAsync(projectRoot, checkPlatforms);
1228d307f52SEvan Bacon
1238d307f52SEvan Bacon  if (!platforms.length) {
1248d307f52SEvan Bacon    return;
1258d307f52SEvan Bacon  }
1268d307f52SEvan Bacon
1278d307f52SEvan Bacon  const displayPlatforms = platforms.map((platform) => chalk.cyan(platform));
1288d307f52SEvan Bacon  // Prompt which platforms to reset.
1298d307f52SEvan Bacon  const message =
1308d307f52SEvan Bacon    platforms.length > 1
1318d307f52SEvan Bacon      ? `The ${displayPlatforms[0]} and ${displayPlatforms[1]} projects are malformed`
1328d307f52SEvan Bacon      : `The ${displayPlatforms[0]} project is malformed`;
1338d307f52SEvan Bacon
1348d307f52SEvan Bacon  if (
1358d307f52SEvan Bacon    // If the process is non-interactive, default to clearing the malformed native project.
1368d307f52SEvan Bacon    // This would only happen on re-running eject.
13729128565SEvan Bacon    !isInteractive() ||
1388d307f52SEvan Bacon    // Prompt to clear the native folders.
1398d307f52SEvan Bacon    (await confirmAsync({
1408d307f52SEvan Bacon      message: `${message}, would you like to clear the project files and reinitialize them?`,
1418d307f52SEvan Bacon      initial: true,
1428d307f52SEvan Bacon    }))
1438d307f52SEvan Bacon  ) {
1448721fb84SWojciech Kozyra    if (!isInteractive()) {
1458721fb84SWojciech Kozyra      Log.warn(`${message}, project files will be cleared and reinitialized.`);
1468721fb84SWojciech Kozyra    }
1478d307f52SEvan Bacon    await clearNativeFolder(projectRoot, platforms);
1488d307f52SEvan Bacon  } else {
1498d307f52SEvan Bacon    // Warn the user that the process may fail.
1508d307f52SEvan Bacon    Log.warn('Continuing with malformed native projects');
1518d307f52SEvan Bacon  }
1528d307f52SEvan Bacon}
153