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