1import { getConfig } from '@expo/config'; 2import * as PackageManager from '@expo/package-manager'; 3 4import * as Log from '../log'; 5import { findUpProjectRootOrAssert } from '../utils/findUp'; 6import { Options } from './resolveOptions'; 7import { getVersionedPackagesAsync } from './utils/getVersionedPackages'; 8 9export async function installAsync( 10 packages: string[], 11 options: Options, 12 packageManagerArguments: string[] = [] 13) { 14 // Locate the project root based on the process current working directory. 15 // This enables users to run `npx expo install` from a subdirectory of the project. 16 const projectRoot = findUpProjectRootOrAssert(process.cwd()); 17 18 // Resolve the package manager used by the project, or based on the provided arguments. 19 const packageManager = PackageManager.createForProject(projectRoot, { 20 npm: options.npm, 21 yarn: options.yarn, 22 log: Log.log, 23 }); 24 25 // Read the project Expo config without plugins. 26 const { exp } = getConfig(projectRoot, { 27 // Sometimes users will add a plugin to the config before installing the library, 28 // this wouldn't work unless we dangerously disable plugin serialization. 29 skipPlugins: true, 30 }); 31 32 // Resolve the versioned packages, then install them. 33 return installPackagesAsync(projectRoot, { 34 packageManager, 35 packages, 36 packageManagerArguments, 37 sdkVersion: exp.sdkVersion!, 38 }); 39} 40 41/** Version packages and install in a project. */ 42export async function installPackagesAsync( 43 projectRoot: string, 44 { 45 packages, 46 packageManager, 47 sdkVersion, 48 packageManagerArguments, 49 }: { 50 /** 51 * List of packages to version 52 * @example ['uuid', 'react-native-reanimated@latest'] 53 */ 54 packages: string[]; 55 /** Package manager to use when installing the versioned packages. */ 56 packageManager: PackageManager.NpmPackageManager | PackageManager.YarnPackageManager; 57 /** 58 * SDK to version `packages` for. 59 * @example '44.0.0' 60 */ 61 sdkVersion: string; 62 /** 63 * Extra parameters to pass to the `packageManager` when installing versioned packages. 64 * @example ['--no-save'] 65 */ 66 packageManagerArguments: string[]; 67 } 68): Promise<void> { 69 const versioning = await getVersionedPackagesAsync(projectRoot, { 70 packages, 71 // sdkVersion is always defined because we don't skipSDKVersionRequirement in getConfig. 72 sdkVersion, 73 }); 74 75 Log.log(`Installing ${versioning.messages.join(' and ')} using ${packageManager.name}.`); 76 77 await packageManager.addWithParametersAsync(versioning.packages, packageManagerArguments); 78 79 await applyPluginsAsync(projectRoot, versioning.packages); 80} 81 82/** 83 * A convenience feature for automatically applying Expo Config Plugins to the `app.json` after installing them. 84 * This should be dropped in favor of autolinking in the future. 85 */ 86async function applyPluginsAsync(projectRoot: string, packages: string[]) { 87 const { autoAddConfigPluginsAsync } = await import('./utils/autoAddConfigPlugins'); 88 89 try { 90 const { exp } = getConfig(projectRoot, { skipSDKVersionRequirement: true, skipPlugins: true }); 91 92 // Only auto add plugins if the plugins array is defined or if the project is using SDK +42. 93 await autoAddConfigPluginsAsync( 94 projectRoot, 95 exp, 96 // Split any possible NPM tags. i.e. `expo@latest` -> `expo` 97 packages.map((pkg) => pkg.split('@')[0]).filter(Boolean) 98 ); 99 } catch (error: any) { 100 // If we fail to apply plugins, the log a warning and continue. 101 if (error.isPluginError) { 102 Log.warn(`Skipping config plugin check: ` + error.message); 103 return; 104 } 105 // Any other error, rethrow. 106 throw error; 107 } 108} 109