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 { ensureDirectoryAsync } from './dir'; 9import { env } from './env'; 10import { AbortCommandError } from './errors'; 11import { logNewSection } from './ora'; 12import * as Log from '../log'; 13import { hashForDependencyMap } from '../prebuild/updatePackageJson'; 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 || env.CI), 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({ 114 // @ts-expect-error: multiple versions in the monorepo 115 spinner: step, 116 }); 117 // Create cached list for later 118 await hasPackageJsonDependencyListChangedAsync(projectRoot).catch(() => null); 119 step.succeed('Installed pods and initialized Xcode workspace.'); 120 return true; 121 } catch (error: any) { 122 step.stopAndPersist({ 123 symbol: '⚠️ ', 124 text: chalk.red('Something went wrong running `pod install` in the `ios` directory.'), 125 }); 126 if (error instanceof PackageManager.CocoaPodsError) { 127 Log.log(error.message); 128 } else { 129 Log.log(`Unknown error: ${error.message}`); 130 } 131 return false; 132 } 133} 134 135function doesProjectUseCocoaPods(projectRoot: string): boolean { 136 return fs.existsSync(path.join(projectRoot, 'ios', 'Podfile')); 137} 138 139function isLockfileCreated(projectRoot: string): boolean { 140 const podfileLockPath = path.join(projectRoot, 'ios', 'Podfile.lock'); 141 return fs.existsSync(podfileLockPath); 142} 143 144function isPodFolderCreated(projectRoot: string): boolean { 145 const podFolderPath = path.join(projectRoot, 'ios', 'Pods'); 146 return fs.existsSync(podFolderPath); 147} 148 149// TODO: Same process but with app.config changes + default plugins. 150// This will ensure the user is prompted for extra setup. 151export async function maybePromptToSyncPodsAsync(projectRoot: string) { 152 if (!doesProjectUseCocoaPods(projectRoot)) { 153 // Project does not use CocoaPods 154 return; 155 } 156 if (!isLockfileCreated(projectRoot) || !isPodFolderCreated(projectRoot)) { 157 if (!(await installCocoaPodsAsync(projectRoot))) { 158 throw new AbortCommandError(); 159 } 160 return; 161 } 162 163 // Getting autolinked packages can be heavy, optimize around checking every time. 164 if (!(await hasPackageJsonDependencyListChangedAsync(projectRoot))) { 165 return; 166 } 167 168 await promptToInstallPodsAsync(projectRoot, []); 169} 170 171async function promptToInstallPodsAsync(projectRoot: string, missingPods?: string[]) { 172 if (missingPods?.length) { 173 Log.log( 174 `Could not find the following native modules: ${missingPods 175 .map((pod) => chalk.bold(pod)) 176 .join(', ')}. Did you forget to run "${chalk.bold('pod install')}" ?` 177 ); 178 } 179 180 try { 181 if (!(await installCocoaPodsAsync(projectRoot))) { 182 throw new AbortCommandError(); 183 } 184 } catch (error) { 185 await fs.promises.rm(path.join(getTempPrebuildFolder(projectRoot), CACHED_PACKAGE_JSON), { 186 recursive: true, 187 force: true, 188 }); 189 throw error; 190 } 191} 192