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