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