109bb6093SEvan Baconimport { getConfig } from '@expo/config';
209bb6093SEvan Baconimport * as PackageManager from '@expo/package-manager';
36caf5755SEvan Baconimport chalk from 'chalk';
409bb6093SEvan Bacon
58a424bebSJames Ideimport { checkPackagesAsync } from './checkPackages';
68a424bebSJames Ideimport { Options } from './resolveOptions';
709bb6093SEvan Baconimport * as Log from '../log';
88d3f3824SCedric van Puttenimport {
98d3f3824SCedric van Putten  getOperationLog,
108d3f3824SCedric van Putten  getVersionedPackagesAsync,
118d3f3824SCedric van Putten} from '../start/doctor/dependencies/getVersionedPackages';
128d3f3824SCedric van Puttenimport { getVersionedDependenciesAsync } from '../start/doctor/dependencies/validateDependenciesVersions';
138d3f3824SCedric van Puttenimport { groupBy } from '../utils/array';
1409bb6093SEvan Baconimport { findUpProjectRootOrAssert } from '../utils/findUp';
156fb32dddSKeith Kurakimport { learnMore } from '../utils/link';
162dd43328SEvan Baconimport { setNodeEnv } from '../utils/nodeEnv';
176fb32dddSKeith Kurakimport { joinWithCommasAnd } from '../utils/strings';
1809bb6093SEvan Bacon
1909bb6093SEvan Baconexport async function installAsync(
2009bb6093SEvan Bacon  packages: string[],
217b57a441SEvan Bacon  options: Options & { projectRoot?: string },
2209bb6093SEvan Bacon  packageManagerArguments: string[] = []
2309bb6093SEvan Bacon) {
242dd43328SEvan Bacon  setNodeEnv('development');
2509bb6093SEvan Bacon  // Locate the project root based on the process current working directory.
2609bb6093SEvan Bacon  // This enables users to run `npx expo install` from a subdirectory of the project.
277b57a441SEvan Bacon  const projectRoot = options.projectRoot ?? findUpProjectRootOrAssert(process.cwd());
286a750d06SEvan Bacon  require('@expo/env').load(projectRoot);
2909bb6093SEvan Bacon
3009bb6093SEvan Bacon  // Resolve the package manager used by the project, or based on the provided arguments.
3109bb6093SEvan Bacon  const packageManager = PackageManager.createForProject(projectRoot, {
3209bb6093SEvan Bacon    npm: options.npm,
3309bb6093SEvan Bacon    yarn: options.yarn,
349b1b5ec6SEvan Bacon    bun: options.bun,
356caf5755SEvan Bacon    pnpm: options.pnpm,
366caf5755SEvan Bacon    silent: options.silent,
378d3f3824SCedric van Putten    log: Log.log,
3809bb6093SEvan Bacon  });
3909bb6093SEvan Bacon
40c94ad8a2SEvan Bacon  if (options.check || options.fix) {
41c94ad8a2SEvan Bacon    return await checkPackagesAsync(projectRoot, {
42c94ad8a2SEvan Bacon      packages,
43c94ad8a2SEvan Bacon      options,
44c94ad8a2SEvan Bacon      packageManager,
45c94ad8a2SEvan Bacon      packageManagerArguments,
46c94ad8a2SEvan Bacon    });
47c94ad8a2SEvan Bacon  }
48c94ad8a2SEvan Bacon
4909bb6093SEvan Bacon  // Read the project Expo config without plugins.
5009bb6093SEvan Bacon  const { exp } = getConfig(projectRoot, {
5109bb6093SEvan Bacon    // Sometimes users will add a plugin to the config before installing the library,
5209bb6093SEvan Bacon    // this wouldn't work unless we dangerously disable plugin serialization.
5309bb6093SEvan Bacon    skipPlugins: true,
5409bb6093SEvan Bacon  });
5509bb6093SEvan Bacon
5609bb6093SEvan Bacon  // Resolve the versioned packages, then install them.
5709bb6093SEvan Bacon  return installPackagesAsync(projectRoot, {
5809bb6093SEvan Bacon    packageManager,
5909bb6093SEvan Bacon    packages,
6009bb6093SEvan Bacon    packageManagerArguments,
6109bb6093SEvan Bacon    sdkVersion: exp.sdkVersion!,
6209bb6093SEvan Bacon  });
6309bb6093SEvan Bacon}
6409bb6093SEvan Bacon
6509bb6093SEvan Bacon/** Version packages and install in a project. */
6609bb6093SEvan Baconexport async function installPackagesAsync(
6709bb6093SEvan Bacon  projectRoot: string,
6809bb6093SEvan Bacon  {
6909bb6093SEvan Bacon    packages,
7009bb6093SEvan Bacon    packageManager,
7109bb6093SEvan Bacon    sdkVersion,
7209bb6093SEvan Bacon    packageManagerArguments,
7309bb6093SEvan Bacon  }: {
7409bb6093SEvan Bacon    /**
758d3f3824SCedric van Putten     * List of packages to version, grouped by the type of dependency.
7609bb6093SEvan Bacon     * @example ['uuid', 'react-native-reanimated@latest']
7709bb6093SEvan Bacon     */
7809bb6093SEvan Bacon    packages: string[];
7909bb6093SEvan Bacon    /** Package manager to use when installing the versioned packages. */
808d3f3824SCedric van Putten    packageManager: PackageManager.NodePackageManager;
8109bb6093SEvan Bacon    /**
8209bb6093SEvan Bacon     * SDK to version `packages` for.
8309bb6093SEvan Bacon     * @example '44.0.0'
8409bb6093SEvan Bacon     */
8509bb6093SEvan Bacon    sdkVersion: string;
8609bb6093SEvan Bacon    /**
8709bb6093SEvan Bacon     * Extra parameters to pass to the `packageManager` when installing versioned packages.
8809bb6093SEvan Bacon     * @example ['--no-save']
8909bb6093SEvan Bacon     */
9009bb6093SEvan Bacon    packageManagerArguments: string[];
9109bb6093SEvan Bacon  }
9209bb6093SEvan Bacon): Promise<void> {
936fb32dddSKeith Kurak  // Read the project Expo config without plugins.
946fb32dddSKeith Kurak  const { pkg } = getConfig(projectRoot, {
956fb32dddSKeith Kurak    // Sometimes users will add a plugin to the config before installing the library,
966fb32dddSKeith Kurak    // this wouldn't work unless we dangerously disable plugin serialization.
976fb32dddSKeith Kurak    skipPlugins: true,
986fb32dddSKeith Kurak  });
996fb32dddSKeith Kurak
1006fb32dddSKeith Kurak  //assertNotInstallingExcludedPackages(projectRoot, packages, pkg);
1016fb32dddSKeith Kurak
10209bb6093SEvan Bacon  const versioning = await getVersionedPackagesAsync(projectRoot, {
10309bb6093SEvan Bacon    packages,
10409bb6093SEvan Bacon    // sdkVersion is always defined because we don't skipSDKVersionRequirement in getConfig.
10509bb6093SEvan Bacon    sdkVersion,
1066fb32dddSKeith Kurak    pkg,
10709bb6093SEvan Bacon  });
10809bb6093SEvan Bacon
1096caf5755SEvan Bacon  Log.log(
1106caf5755SEvan Bacon    chalk`\u203A Installing ${
1116caf5755SEvan Bacon      versioning.messages.length ? versioning.messages.join(' and ') + ' ' : ''
1126caf5755SEvan Bacon    }using {bold ${packageManager.name}}`
1136caf5755SEvan Bacon  );
11409bb6093SEvan Bacon
1156fb32dddSKeith Kurak  if (versioning.excludedNativeModules.length) {
1166fb32dddSKeith Kurak    Log.log(
1176fb32dddSKeith Kurak      chalk`\u203A Using latest version instead of ${joinWithCommasAnd(
1186fb32dddSKeith Kurak        versioning.excludedNativeModules.map(
1196fb32dddSKeith Kurak          ({ bundledNativeVersion, name }) => `${bundledNativeVersion} for ${name}`
1206fb32dddSKeith Kurak        )
1216fb32dddSKeith Kurak      )} because ${
1226fb32dddSKeith Kurak        versioning.excludedNativeModules.length > 1 ? 'they are' : 'it is'
1236fb32dddSKeith Kurak      } listed in {bold expo.install.exclude} in package.json. ${learnMore(
1246fb32dddSKeith Kurak        'https://expo.dev/more/expo-cli/#configuring-dependency-validation'
1256fb32dddSKeith Kurak      )}`
1266fb32dddSKeith Kurak    );
1276fb32dddSKeith Kurak  }
1286fb32dddSKeith Kurak
1298d3f3824SCedric van Putten  await packageManager.addAsync([...packageManagerArguments, ...versioning.packages]);
13009bb6093SEvan Bacon
13109bb6093SEvan Bacon  await applyPluginsAsync(projectRoot, versioning.packages);
13209bb6093SEvan Bacon}
13309bb6093SEvan Bacon
1348d3f3824SCedric van Puttenexport async function fixPackagesAsync(
1358d3f3824SCedric van Putten  projectRoot: string,
1368d3f3824SCedric van Putten  {
1378d3f3824SCedric van Putten    packages,
1388d3f3824SCedric van Putten    packageManager,
1398d3f3824SCedric van Putten    sdkVersion,
1408d3f3824SCedric van Putten    packageManagerArguments,
1418d3f3824SCedric van Putten  }: {
1428d3f3824SCedric van Putten    packages: Awaited<ReturnType<typeof getVersionedDependenciesAsync>>;
1438d3f3824SCedric van Putten    /** Package manager to use when installing the versioned packages. */
1448d3f3824SCedric van Putten    packageManager: PackageManager.NodePackageManager;
1458d3f3824SCedric van Putten    /**
1468d3f3824SCedric van Putten     * SDK to version `packages` for.
1478d3f3824SCedric van Putten     * @example '44.0.0'
1488d3f3824SCedric van Putten     */
1498d3f3824SCedric van Putten    sdkVersion: string;
1508d3f3824SCedric van Putten    /**
1518d3f3824SCedric van Putten     * Extra parameters to pass to the `packageManager` when installing versioned packages.
1528d3f3824SCedric van Putten     * @example ['--no-save']
1538d3f3824SCedric van Putten     */
1548d3f3824SCedric van Putten    packageManagerArguments: string[];
1558d3f3824SCedric van Putten  }
1568d3f3824SCedric van Putten): Promise<void> {
1578d3f3824SCedric van Putten  if (!packages.length) {
1588d3f3824SCedric van Putten    return;
1598d3f3824SCedric van Putten  }
1608d3f3824SCedric van Putten
1618d3f3824SCedric van Putten  const { dependencies = [], devDependencies = [] } = groupBy(packages, (dep) => dep.packageType);
1628d3f3824SCedric van Putten  const versioningMessages = getOperationLog({
1638d3f3824SCedric van Putten    othersCount: 0, // All fixable packages are versioned
1648d3f3824SCedric van Putten    nativeModulesCount: packages.length,
1658d3f3824SCedric van Putten    sdkVersion,
1668d3f3824SCedric van Putten  });
1678d3f3824SCedric van Putten
1688d3f3824SCedric van Putten  Log.log(
1698d3f3824SCedric van Putten    chalk`\u203A Installing ${
1708d3f3824SCedric van Putten      versioningMessages.length ? versioningMessages.join(' and ') + ' ' : ''
1718d3f3824SCedric van Putten    }using {bold ${packageManager.name}}`
1728d3f3824SCedric van Putten  );
1738d3f3824SCedric van Putten
1748d3f3824SCedric van Putten  if (dependencies.length) {
1758d3f3824SCedric van Putten    const versionedPackages = dependencies.map(
1768d3f3824SCedric van Putten      (dep) => `${dep.packageName}@${dep.expectedVersionOrRange}`
1778d3f3824SCedric van Putten    );
1788d3f3824SCedric van Putten
1798d3f3824SCedric van Putten    await packageManager.addAsync([...packageManagerArguments, ...versionedPackages]);
1808d3f3824SCedric van Putten
1818d3f3824SCedric van Putten    await applyPluginsAsync(projectRoot, versionedPackages);
1828d3f3824SCedric van Putten  }
1838d3f3824SCedric van Putten
1848d3f3824SCedric van Putten  if (devDependencies.length) {
1858d3f3824SCedric van Putten    await packageManager.addDevAsync([
1868d3f3824SCedric van Putten      ...packageManagerArguments,
1878d3f3824SCedric van Putten      ...devDependencies.map((dep) => `${dep.packageName}@${dep.expectedVersionOrRange}`),
1888d3f3824SCedric van Putten    ]);
1898d3f3824SCedric van Putten  }
1908d3f3824SCedric van Putten}
1918d3f3824SCedric van Putten
19209bb6093SEvan Bacon/**
19309bb6093SEvan Bacon * A convenience feature for automatically applying Expo Config Plugins to the `app.json` after installing them.
19409bb6093SEvan Bacon * This should be dropped in favor of autolinking in the future.
19509bb6093SEvan Bacon */
19609bb6093SEvan Baconasync function applyPluginsAsync(projectRoot: string, packages: string[]) {
197*1a3a1db5SEvan Bacon  const { autoAddConfigPluginsAsync } = await import('./utils/autoAddConfigPlugins.js');
19809bb6093SEvan Bacon
19909bb6093SEvan Bacon  try {
2000f1a896eSKim Brandwijk    const { exp } = getConfig(projectRoot, { skipSDKVersionRequirement: true });
20109bb6093SEvan Bacon
20209bb6093SEvan Bacon    // Only auto add plugins if the plugins array is defined or if the project is using SDK +42.
20309bb6093SEvan Bacon    await autoAddConfigPluginsAsync(
20409bb6093SEvan Bacon      projectRoot,
20509bb6093SEvan Bacon      exp,
20609bb6093SEvan Bacon      // Split any possible NPM tags. i.e. `expo@latest` -> `expo`
20709bb6093SEvan Bacon      packages.map((pkg) => pkg.split('@')[0]).filter(Boolean)
20809bb6093SEvan Bacon    );
20909bb6093SEvan Bacon  } catch (error: any) {
21009bb6093SEvan Bacon    // If we fail to apply plugins, the log a warning and continue.
21109bb6093SEvan Bacon    if (error.isPluginError) {
21209bb6093SEvan Bacon      Log.warn(`Skipping config plugin check: ` + error.message);
21309bb6093SEvan Bacon      return;
21409bb6093SEvan Bacon    }
21509bb6093SEvan Bacon    // Any other error, rethrow.
21609bb6093SEvan Bacon    throw error;
21709bb6093SEvan Bacon  }
21809bb6093SEvan Bacon}
219