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