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