1import { getPackageJson, PackageJSONConfig } from '@expo/config'; 2import JsonFile from '@expo/json-file'; 3import * as PackageManager from '@expo/package-manager'; 4import chalk from 'chalk'; 5import fs from 'fs'; 6import path from 'path'; 7 8import * as Log from '../log'; 9import { hashForDependencyMap } from '../prebuild/updatePackageJson'; 10import { ensureDirectoryAsync } from './dir'; 11import { env } from './env'; 12import { logNewSection } from './ora'; 13 14type PackageChecksums = { 15 /** checksum for the `package.json` dependency object. */ 16 dependencies: string; 17 /** checksum for the `package.json` devDependency object. */ 18 devDependencies: string; 19}; 20 21const PROJECT_PREBUILD_SETTINGS = '.expo/prebuild'; 22const CACHED_PACKAGE_JSON = 'cached-packages.json'; 23 24function getTempPrebuildFolder(projectRoot: string): string { 25 return path.join(projectRoot, PROJECT_PREBUILD_SETTINGS); 26} 27 28function hasNewDependenciesSinceLastBuild( 29 projectRoot: string, 30 packageChecksums: PackageChecksums 31): boolean { 32 // TODO: Maybe comparing lock files would be better... 33 const templateDirectory = getTempPrebuildFolder(projectRoot); 34 const tempPkgJsonPath = path.join(templateDirectory, CACHED_PACKAGE_JSON); 35 if (!fs.existsSync(tempPkgJsonPath)) { 36 return true; 37 } 38 const { dependencies, devDependencies } = JsonFile.read(tempPkgJsonPath); 39 // Only change the dependencies if the normalized hash changes, this helps to reduce meaningless changes. 40 const hasNewDependencies = packageChecksums.dependencies !== dependencies; 41 const hasNewDevDependencies = packageChecksums.devDependencies !== devDependencies; 42 43 return hasNewDependencies || hasNewDevDependencies; 44} 45 46function createPackageChecksums(pkg: PackageJSONConfig): PackageChecksums { 47 return { 48 dependencies: hashForDependencyMap(pkg.dependencies || {}), 49 devDependencies: hashForDependencyMap(pkg.devDependencies || {}), 50 }; 51} 52 53/** @returns `true` if the package.json dependency hash does not match the cached hash from the last run. */ 54export async function hasPackageJsonDependencyListChangedAsync( 55 projectRoot: string 56): Promise<boolean> { 57 const pkg = getPackageJson(projectRoot); 58 59 const packages = createPackageChecksums(pkg); 60 const hasNewDependencies = hasNewDependenciesSinceLastBuild(projectRoot, packages); 61 62 // Cache package.json 63 await ensureDirectoryAsync(getTempPrebuildFolder(projectRoot)); 64 const templateDirectory = path.join(getTempPrebuildFolder(projectRoot), CACHED_PACKAGE_JSON); 65 await JsonFile.writeAsync(templateDirectory, packages); 66 67 return hasNewDependencies; 68} 69 70export async function installCocoaPodsAsync(projectRoot: string): Promise<boolean> { 71 let step = logNewSection('Installing CocoaPods...'); 72 if (process.platform !== 'darwin') { 73 step.succeed('Skipped installing CocoaPods because operating system is not on macOS.'); 74 return false; 75 } 76 77 const packageManager = new PackageManager.CocoaPodsPackageManager({ 78 cwd: path.join(projectRoot, 'ios'), 79 silent: !env.EXPO_DEBUG, 80 }); 81 82 if (!(await packageManager.isCLIInstalledAsync())) { 83 try { 84 // prompt user -- do you want to install cocoapods right now? 85 step.text = 'CocoaPods CLI not found in your PATH, installing it now.'; 86 step.stopAndPersist(); 87 await PackageManager.CocoaPodsPackageManager.installCLIAsync({ 88 nonInteractive: true, 89 spawnOptions: { 90 ...packageManager.options, 91 // Don't silence this part 92 stdio: ['inherit', 'inherit', 'pipe'], 93 }, 94 }); 95 step.succeed('Installed CocoaPods CLI.'); 96 step = logNewSection('Running `pod install` in the `ios` directory.'); 97 } catch (error: any) { 98 step.stopAndPersist({ 99 symbol: '⚠️ ', 100 text: chalk.red('Unable to install the CocoaPods CLI.'), 101 }); 102 if (error instanceof PackageManager.CocoaPodsError) { 103 Log.log(error.message); 104 } else { 105 Log.log(`Unknown error: ${error.message}`); 106 } 107 return false; 108 } 109 } 110 111 try { 112 await packageManager.installAsync({ spinner: step }); 113 // Create cached list for later 114 await hasPackageJsonDependencyListChangedAsync(projectRoot).catch(() => null); 115 step.succeed('Installed pods and initialized Xcode workspace.'); 116 return true; 117 } catch (error: any) { 118 step.stopAndPersist({ 119 symbol: '⚠️ ', 120 text: chalk.red('Something went wrong running `pod install` in the `ios` directory.'), 121 }); 122 if (error instanceof PackageManager.CocoaPodsError) { 123 Log.log(error.message); 124 } else { 125 Log.log(`Unknown error: ${error.message}`); 126 } 127 return false; 128 } 129} 130