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 12type ArbitraryPlatform = ModPlatform | string; 13 14/** Delete the input native folders and print a loading step. */ 15export async function clearNativeFolder(projectRoot: string, folders: string[]) { 16 const step = logNewSection(`Clearing ${folders.join(', ')}`); 17 try { 18 await Promise.all( 19 folders.map((folderName) => 20 fs.promises.rm(path.join(projectRoot, folderName), { 21 recursive: true, 22 force: true, 23 }) 24 ) 25 ); 26 step.succeed(`Cleared ${folders.join(', ')} code`); 27 } catch (error: any) { 28 step.fail(`Failed to delete ${folders.join(', ')} code: ${error.message}`); 29 throw error; 30 } 31} 32 33/** 34 * Returns `true` if a certain subset of required Android project files are intact. 35 * 36 * This isn't perfect but it serves the purpose of indicating that the user should 37 * be warned to nuke the project files, most commonly when git is cleared and the root folder 38 * remains in memory. 39 */ 40export async function hasRequiredAndroidFilesAsync(projectRoot: string): Promise<boolean> { 41 try { 42 await Promise.all([ 43 AndroidConfig.Paths.getAppBuildGradleAsync(projectRoot), 44 AndroidConfig.Paths.getProjectBuildGradleAsync(projectRoot), 45 AndroidConfig.Paths.getAndroidManifestAsync(projectRoot), 46 AndroidConfig.Paths.getMainApplicationAsync(projectRoot), 47 ]); 48 return true; 49 } catch { 50 return false; 51 } 52} 53 54/** Returns `true` if a certain subset of required iOS project files are intact. */ 55export async function hasRequiredIOSFilesAsync(projectRoot: string) { 56 try { 57 // If any of the following required files are missing, then the project is malformed. 58 await Promise.all([ 59 IOSConfig.Paths.getAllXcodeProjectPaths(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: ArbitraryPlatform[] 78): Promise<ArbitraryPlatform[]> { 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 ArbitraryPlatform[]; 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: ArbitraryPlatform[] 94): Promise<ArbitraryPlatform[]> { 95 const VERIFIERS: Record<ArbitraryPlatform, (root: string) => Promise<boolean>> = { 96 android: hasRequiredAndroidFilesAsync, 97 ios: hasRequiredIOSFilesAsync, 98 }; 99 100 const checkablePlatforms = platforms.filter((platform) => platform in VERIFIERS); 101 const checkPlatforms = await filterPlatformsThatDoNotExistAsync(projectRoot, checkablePlatforms); 102 return ( 103 await Promise.all( 104 checkPlatforms.map(async (platform) => { 105 if (!VERIFIERS[platform]) { 106 return false; 107 } 108 if (await VERIFIERS[platform](projectRoot)) { 109 return false; 110 } 111 return platform; 112 }) 113 ) 114 ).filter(Boolean) as ArbitraryPlatform[]; 115} 116 117export async function promptToClearMalformedNativeProjectsAsync( 118 projectRoot: string, 119 checkPlatforms: ArbitraryPlatform[] 120) { 121 const platforms = await getMalformedNativeProjectsAsync(projectRoot, checkPlatforms); 122 123 if (!platforms.length) { 124 return; 125 } 126 127 const displayPlatforms = platforms.map((platform) => chalk.cyan(platform)); 128 // Prompt which platforms to reset. 129 const message = 130 platforms.length > 1 131 ? `The ${displayPlatforms[0]} and ${displayPlatforms[1]} projects are malformed` 132 : `The ${displayPlatforms[0]} project is malformed`; 133 134 if ( 135 // If the process is non-interactive, default to clearing the malformed native project. 136 // This would only happen on re-running eject. 137 !isInteractive() || 138 // Prompt to clear the native folders. 139 (await confirmAsync({ 140 message: `${message}, would you like to clear the project files and reinitialize them?`, 141 initial: true, 142 })) 143 ) { 144 if (!isInteractive()) { 145 Log.warn(`${message}, project files will be cleared and reinitialized.`); 146 } 147 await clearNativeFolder(projectRoot, platforms); 148 } else { 149 // Warn the user that the process may fail. 150 Log.warn('Continuing with malformed native projects'); 151 } 152} 153