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 { AbortCommandError } from './errors'; 13import { logNewSection } from './ora'; 14 15type PackageChecksums = { 16 /** checksum for the `package.json` dependency object. */ 17 dependencies: string; 18 /** checksum for the `package.json` devDependency object. */ 19 devDependencies: string; 20}; 21 22const PROJECT_PREBUILD_SETTINGS = '.expo/prebuild'; 23const CACHED_PACKAGE_JSON = 'cached-packages.json'; 24 25function getTempPrebuildFolder(projectRoot: string): string { 26 return path.join(projectRoot, PROJECT_PREBUILD_SETTINGS); 27} 28 29function hasNewDependenciesSinceLastBuild( 30 projectRoot: string, 31 packageChecksums: PackageChecksums 32): boolean { 33 // TODO: Maybe comparing lock files would be better... 34 const templateDirectory = getTempPrebuildFolder(projectRoot); 35 const tempPkgJsonPath = path.join(templateDirectory, CACHED_PACKAGE_JSON); 36 if (!fs.existsSync(tempPkgJsonPath)) { 37 return true; 38 } 39 const { dependencies, devDependencies } = JsonFile.read(tempPkgJsonPath); 40 // Only change the dependencies if the normalized hash changes, this helps to reduce meaningless changes. 41 const hasNewDependencies = packageChecksums.dependencies !== dependencies; 42 const hasNewDevDependencies = packageChecksums.devDependencies !== devDependencies; 43 44 return hasNewDependencies || hasNewDevDependencies; 45} 46 47function createPackageChecksums(pkg: PackageJSONConfig): PackageChecksums { 48 return { 49 dependencies: hashForDependencyMap(pkg.dependencies || {}), 50 devDependencies: hashForDependencyMap(pkg.devDependencies || {}), 51 }; 52} 53 54/** @returns `true` if the package.json dependency hash does not match the cached hash from the last run. */ 55export async function hasPackageJsonDependencyListChangedAsync( 56 projectRoot: string 57): Promise<boolean> { 58 const pkg = getPackageJson(projectRoot); 59 60 const packages = createPackageChecksums(pkg); 61 const hasNewDependencies = hasNewDependenciesSinceLastBuild(projectRoot, packages); 62 63 // Cache package.json 64 await ensureDirectoryAsync(getTempPrebuildFolder(projectRoot)); 65 const templateDirectory = path.join(getTempPrebuildFolder(projectRoot), CACHED_PACKAGE_JSON); 66 await JsonFile.writeAsync(templateDirectory, packages); 67 68 return hasNewDependencies; 69} 70 71export async function installCocoaPodsAsync(projectRoot: string): Promise<boolean> { 72 let step = logNewSection('Installing CocoaPods...'); 73 if (process.platform !== 'darwin') { 74 step.succeed('Skipped installing CocoaPods because operating system is not on macOS.'); 75 return false; 76 } 77 78 const packageManager = new PackageManager.CocoaPodsPackageManager({ 79 cwd: path.join(projectRoot, 'ios'), 80 silent: !env.EXPO_DEBUG, 81 }); 82 83 if (!(await packageManager.isCLIInstalledAsync())) { 84 try { 85 // prompt user -- do you want to install cocoapods right now? 86 step.text = 'CocoaPods CLI not found in your PATH, installing it now.'; 87 step.stopAndPersist(); 88 await PackageManager.CocoaPodsPackageManager.installCLIAsync({ 89 nonInteractive: true, 90 spawnOptions: { 91 ...packageManager.options, 92 // Don't silence this part 93 stdio: ['inherit', 'inherit', 'pipe'], 94 }, 95 }); 96 step.succeed('Installed CocoaPods CLI.'); 97 step = logNewSection('Running `pod install` in the `ios` directory.'); 98 } catch (error: any) { 99 step.stopAndPersist({ 100 symbol: '⚠️ ', 101 text: chalk.red('Unable to install the CocoaPods CLI.'), 102 }); 103 if (error instanceof PackageManager.CocoaPodsError) { 104 Log.log(error.message); 105 } else { 106 Log.log(`Unknown error: ${error.message}`); 107 } 108 return false; 109 } 110 } 111 112 try { 113 await packageManager.installAsync({ spinner: step }); 114 // Create cached list for later 115 await hasPackageJsonDependencyListChangedAsync(projectRoot).catch(() => null); 116 step.succeed('Installed pods and initialized Xcode workspace.'); 117 return true; 118 } catch (error: any) { 119 step.stopAndPersist({ 120 symbol: '⚠️ ', 121 text: chalk.red('Something went wrong running `pod install` in the `ios` directory.'), 122 }); 123 if (error instanceof PackageManager.CocoaPodsError) { 124 Log.log(error.message); 125 } else { 126 Log.log(`Unknown error: ${error.message}`); 127 } 128 return false; 129 } 130} 131 132function doesProjectUseCocoaPods(projectRoot: string): boolean { 133 return fs.existsSync(path.join(projectRoot, 'ios', 'Podfile')); 134} 135 136function isLockfileCreated(projectRoot: string): boolean { 137 const podfileLockPath = path.join(projectRoot, 'ios', 'Podfile.lock'); 138 return fs.existsSync(podfileLockPath); 139} 140 141function isPodFolderCreated(projectRoot: string): boolean { 142 const podFolderPath = path.join(projectRoot, 'ios', 'Pods'); 143 return fs.existsSync(podFolderPath); 144} 145 146// TODO: Same process but with app.config changes + default plugins. 147// This will ensure the user is prompted for extra setup. 148export async function maybePromptToSyncPodsAsync(projectRoot: string) { 149 if (!doesProjectUseCocoaPods(projectRoot)) { 150 // Project does not use CocoaPods 151 return; 152 } 153 if (!isLockfileCreated(projectRoot) || !isPodFolderCreated(projectRoot)) { 154 if (!(await installCocoaPodsAsync(projectRoot))) { 155 throw new AbortCommandError(); 156 } 157 return; 158 } 159 160 // Getting autolinked packages can be heavy, optimize around checking every time. 161 if (!(await hasPackageJsonDependencyListChangedAsync(projectRoot))) { 162 return; 163 } 164 165 await promptToInstallPodsAsync(projectRoot, []); 166} 167 168async function promptToInstallPodsAsync(projectRoot: string, missingPods?: string[]) { 169 if (missingPods?.length) { 170 Log.log( 171 `Could not find the following native modules: ${missingPods 172 .map((pod) => chalk.bold(pod)) 173 .join(', ')}. Did you forget to run "${chalk.bold('pod install')}" ?` 174 ); 175 } 176 177 try { 178 if (!(await installCocoaPodsAsync(projectRoot))) { 179 throw new AbortCommandError(); 180 } 181 } catch (error) { 182 await fs.promises.rm(path.join(getTempPrebuildFolder(projectRoot), CACHED_PACKAGE_JSON), { 183 recursive: true, 184 force: true, 185 }); 186 throw error; 187 } 188} 189