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 checkablePlatforms = platforms.filter((platform) => platform in VERIFIERS); 99 const checkPlatforms = await filterPlatformsThatDoNotExistAsync(projectRoot, checkablePlatforms); 100 return ( 101 await Promise.all( 102 checkPlatforms.map(async (platform) => { 103 if (await VERIFIERS[platform](projectRoot)) { 104 return false; 105 } 106 return platform; 107 }) 108 ) 109 ).filter(Boolean) as ModPlatform[]; 110} 111 112export async function promptToClearMalformedNativeProjectsAsync( 113 projectRoot: string, 114 checkPlatforms: ModPlatform[] 115) { 116 const platforms = await getMalformedNativeProjectsAsync(projectRoot, checkPlatforms); 117 118 if (!platforms.length) { 119 return; 120 } 121 122 const displayPlatforms = platforms.map((platform) => chalk.cyan(platform)); 123 // Prompt which platforms to reset. 124 const message = 125 platforms.length > 1 126 ? `The ${displayPlatforms[0]} and ${displayPlatforms[1]} projects are malformed` 127 : `The ${displayPlatforms[0]} project is malformed`; 128 129 if ( 130 // If the process is non-interactive, default to clearing the malformed native project. 131 // This would only happen on re-running eject. 132 !isInteractive() || 133 // Prompt to clear the native folders. 134 (await confirmAsync({ 135 message: `${message}, would you like to clear the project files and reinitialize them?`, 136 initial: true, 137 })) 138 ) { 139 if (!isInteractive()) { 140 Log.warn(`${message}, project files will be cleared and reinitialized.`); 141 } 142 await clearNativeFolder(projectRoot, platforms); 143 } else { 144 // Warn the user that the process may fail. 145 Log.warn('Continuing with malformed native projects'); 146 } 147} 148