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