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