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