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