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