1import { getConfig } from '@expo/config'; 2import * as PackageManager from '@expo/package-manager'; 3import chalk from 'chalk'; 4 5import * as Log from '../log'; 6import { 7 getOperationLog, 8 getVersionedPackagesAsync, 9} from '../start/doctor/dependencies/getVersionedPackages'; 10import { getVersionedDependenciesAsync } from '../start/doctor/dependencies/validateDependenciesVersions'; 11import { groupBy } from '../utils/array'; 12import { findUpProjectRootOrAssert } from '../utils/findUp'; 13import { checkPackagesAsync } from './checkPackages'; 14import { Options } from './resolveOptions'; 15 16export async function installAsync( 17 packages: string[], 18 options: Options & { projectRoot?: string }, 19 packageManagerArguments: string[] = [] 20) { 21 // Locate the project root based on the process current working directory. 22 // This enables users to run `npx expo install` from a subdirectory of the project. 23 const projectRoot = options.projectRoot ?? findUpProjectRootOrAssert(process.cwd()); 24 25 // Resolve the package manager used by the project, or based on the provided arguments. 26 const packageManager = PackageManager.createForProject(projectRoot, { 27 npm: options.npm, 28 yarn: options.yarn, 29 pnpm: options.pnpm, 30 silent: options.silent, 31 log: Log.log, 32 }); 33 34 if (options.check || options.fix) { 35 return await checkPackagesAsync(projectRoot, { 36 packages, 37 options, 38 packageManager, 39 packageManagerArguments, 40 }); 41 } 42 43 // Read the project Expo config without plugins. 44 const { exp } = getConfig(projectRoot, { 45 // Sometimes users will add a plugin to the config before installing the library, 46 // this wouldn't work unless we dangerously disable plugin serialization. 47 skipPlugins: true, 48 }); 49 50 // Resolve the versioned packages, then install them. 51 return installPackagesAsync(projectRoot, { 52 packageManager, 53 packages, 54 packageManagerArguments, 55 sdkVersion: exp.sdkVersion!, 56 }); 57} 58 59/** Version packages and install in a project. */ 60export async function installPackagesAsync( 61 projectRoot: string, 62 { 63 packages, 64 packageManager, 65 sdkVersion, 66 packageManagerArguments, 67 }: { 68 /** 69 * List of packages to version, grouped by the type of dependency. 70 * @example ['uuid', 'react-native-reanimated@latest'] 71 */ 72 packages: string[]; 73 /** Package manager to use when installing the versioned packages. */ 74 packageManager: PackageManager.NodePackageManager; 75 /** 76 * SDK to version `packages` for. 77 * @example '44.0.0' 78 */ 79 sdkVersion: string; 80 /** 81 * Extra parameters to pass to the `packageManager` when installing versioned packages. 82 * @example ['--no-save'] 83 */ 84 packageManagerArguments: string[]; 85 } 86): Promise<void> { 87 const versioning = await getVersionedPackagesAsync(projectRoot, { 88 packages, 89 // sdkVersion is always defined because we don't skipSDKVersionRequirement in getConfig. 90 sdkVersion, 91 }); 92 93 Log.log( 94 chalk`\u203A Installing ${ 95 versioning.messages.length ? versioning.messages.join(' and ') + ' ' : '' 96 }using {bold ${packageManager.name}}` 97 ); 98 99 await packageManager.addAsync([...packageManagerArguments, ...versioning.packages]); 100 101 await applyPluginsAsync(projectRoot, versioning.packages); 102} 103 104export async function fixPackagesAsync( 105 projectRoot: string, 106 { 107 packages, 108 packageManager, 109 sdkVersion, 110 packageManagerArguments, 111 }: { 112 packages: Awaited<ReturnType<typeof getVersionedDependenciesAsync>>; 113 /** Package manager to use when installing the versioned packages. */ 114 packageManager: PackageManager.NodePackageManager; 115 /** 116 * SDK to version `packages` for. 117 * @example '44.0.0' 118 */ 119 sdkVersion: string; 120 /** 121 * Extra parameters to pass to the `packageManager` when installing versioned packages. 122 * @example ['--no-save'] 123 */ 124 packageManagerArguments: string[]; 125 } 126): Promise<void> { 127 if (!packages.length) { 128 return; 129 } 130 131 const { dependencies = [], devDependencies = [] } = groupBy(packages, (dep) => dep.packageType); 132 const versioningMessages = getOperationLog({ 133 othersCount: 0, // All fixable packages are versioned 134 nativeModulesCount: packages.length, 135 sdkVersion, 136 }); 137 138 Log.log( 139 chalk`\u203A Installing ${ 140 versioningMessages.length ? versioningMessages.join(' and ') + ' ' : '' 141 }using {bold ${packageManager.name}}` 142 ); 143 144 if (dependencies.length) { 145 const versionedPackages = dependencies.map( 146 (dep) => `${dep.packageName}@${dep.expectedVersionOrRange}` 147 ); 148 149 await packageManager.addAsync([...packageManagerArguments, ...versionedPackages]); 150 151 await applyPluginsAsync(projectRoot, versionedPackages); 152 } 153 154 if (devDependencies.length) { 155 await packageManager.addDevAsync([ 156 ...packageManagerArguments, 157 ...devDependencies.map((dep) => `${dep.packageName}@${dep.expectedVersionOrRange}`), 158 ]); 159 } 160} 161 162/** 163 * A convenience feature for automatically applying Expo Config Plugins to the `app.json` after installing them. 164 * This should be dropped in favor of autolinking in the future. 165 */ 166async function applyPluginsAsync(projectRoot: string, packages: string[]) { 167 const { autoAddConfigPluginsAsync } = await import('./utils/autoAddConfigPlugins'); 168 169 try { 170 const { exp } = getConfig(projectRoot, { skipSDKVersionRequirement: true }); 171 172 // Only auto add plugins if the plugins array is defined or if the project is using SDK +42. 173 await autoAddConfigPluginsAsync( 174 projectRoot, 175 exp, 176 // Split any possible NPM tags. i.e. `expo@latest` -> `expo` 177 packages.map((pkg) => pkg.split('@')[0]).filter(Boolean) 178 ); 179 } catch (error: any) { 180 // If we fail to apply plugins, the log a warning and continue. 181 if (error.isPluginError) { 182 Log.warn(`Skipping config plugin check: ` + error.message); 183 return; 184 } 185 // Any other error, rethrow. 186 throw error; 187 } 188} 189