1import { ExpoConfig, getConfig } from '@expo/config'; 2import chalk from 'chalk'; 3import wrapAnsi from 'wrap-ansi'; 4 5import { getMissingPackagesAsync, ResolvedPackage } from './getMissingPackages'; 6import { installAsync } from '../../../install/installAsync'; 7import * as Log from '../../../log'; 8import { CommandError } from '../../../utils/errors'; 9import { isInteractive } from '../../../utils/interactive'; 10import { logNewSection } from '../../../utils/ora'; 11import { confirmAsync } from '../../../utils/prompts'; 12 13export async function ensureDependenciesAsync( 14 projectRoot: string, 15 { 16 exp = getConfig(projectRoot).exp, 17 requiredPackages, 18 warningMessage, 19 installMessage, 20 // Don't prompt in CI 21 skipPrompt = !isInteractive(), 22 isProjectMutable = isInteractive(), 23 }: { 24 exp?: ExpoConfig; 25 installMessage: string; 26 warningMessage: string; 27 requiredPackages: ResolvedPackage[]; 28 skipPrompt?: boolean; 29 /** Project can be mutated in the current environment. */ 30 isProjectMutable?: boolean; 31 } 32): Promise<boolean> { 33 const { missing } = await getMissingPackagesAsync(projectRoot, { 34 sdkVersion: exp.sdkVersion, 35 requiredPackages, 36 }); 37 if (!missing.length) { 38 return true; 39 } 40 41 // Prompt to install or bail out... 42 const readableMissingPackages = missing 43 .map(({ pkg, version }) => (version ? [pkg, version].join('@') : pkg)) 44 .join(', '); 45 46 let title = installMessage; 47 48 if (skipPrompt && !isProjectMutable) { 49 title += '\n\n'; 50 } else { 51 let confirm = skipPrompt; 52 if (skipPrompt) { 53 // Automatically install packages without prompting. 54 Log.log(wrapForTerminal(title + ` Installing ${chalk.cyan(readableMissingPackages)}`)); 55 } else { 56 confirm = await confirmAsync({ 57 message: wrapForTerminal( 58 title + ` Would you like to install ${chalk.cyan(readableMissingPackages)}?` 59 ), 60 initial: true, 61 }); 62 } 63 64 if (confirm) { 65 // Format with version if available. 66 const packages = missing.map(({ pkg, version }) => 67 version ? [pkg, version].join('@') : pkg 68 ); 69 // Install packages with versions 70 await installPackagesAsync(projectRoot, { 71 packages, 72 }); 73 // Try again but skip prompting twice, simply fail if the packages didn't install correctly. 74 return await ensureDependenciesAsync(projectRoot, { 75 skipPrompt: true, 76 installMessage, 77 warningMessage, 78 requiredPackages, 79 }); 80 } 81 82 // Reset the title so it doesn't print twice in interactive mode. 83 title = ''; 84 } 85 86 const installCommand = createInstallCommand({ 87 packages: missing, 88 }); 89 90 const disableMessage = warningMessage; 91 92 const solution = `Please install ${chalk.bold( 93 readableMissingPackages 94 )} by running:\n\n ${chalk.reset.bold(installCommand)}\n\n`; 95 96 // This prevents users from starting a misconfigured JS or TS project by default. 97 throw new CommandError(wrapForTerminal(title + solution + disableMessage + '\n')); 98} 99 100/** Wrap long messages to fit smaller terminals. */ 101function wrapForTerminal(message: string): string { 102 return wrapAnsi(message, process.stdout.columns || 80); 103} 104 105/** Create the bash install command from a given set of packages and settings. */ 106export function createInstallCommand({ 107 packages, 108}: { 109 packages: { 110 file: string; 111 pkg: string; 112 version?: string | undefined; 113 }[]; 114}) { 115 return ( 116 'npx expo install ' + 117 packages 118 .map(({ pkg, version }) => { 119 if (version) { 120 return [pkg, version].join('@'); 121 } 122 return pkg; 123 }) 124 .join(' ') 125 ); 126} 127 128/** Install packages in the project. */ 129async function installPackagesAsync(projectRoot: string, { packages }: { packages: string[] }) { 130 const packagesStr = chalk.bold(packages.join(', ')); 131 Log.log(); 132 const installingPackageStep = logNewSection(`Installing ${packagesStr}`); 133 try { 134 await installAsync(packages, { projectRoot }); 135 } catch (e: any) { 136 installingPackageStep.fail(`Failed to install ${packagesStr} with error: ${e.message}`); 137 throw e; 138 } 139 installingPackageStep.succeed(`Installed ${packagesStr}`); 140} 141