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