1import { getConfig } from '@expo/config';
2import * as PackageManager from '@expo/package-manager';
3import chalk from 'chalk';
4
5import * as Log from '../log';
6import {
7  getVersionedDependenciesAsync,
8  logIncorrectDependencies,
9} from '../start/doctor/dependencies/validateDependenciesVersions';
10import { isInteractive } from '../utils/interactive';
11import { learnMore } from '../utils/link';
12import { confirmAsync } from '../utils/prompts';
13import { joinWithCommasAnd } from '../utils/strings';
14import { fixPackagesAsync } from './installAsync';
15import { Options } from './resolveOptions';
16
17const debug = require('debug')('expo:install:check') as typeof console.log;
18
19// Exposed for testing.
20export async function checkPackagesAsync(
21  projectRoot: string,
22  {
23    packages,
24    packageManager,
25    options: { fix },
26    packageManagerArguments,
27  }: {
28    /**
29     * List of packages to version
30     * @example ['uuid', 'react-native-reanimated@latest']
31     */
32    packages: string[];
33    /** Package manager to use when installing the versioned packages. */
34    packageManager: PackageManager.NodePackageManager;
35
36    /** How the check should resolve */
37    options: Pick<Options, 'fix'>;
38    /**
39     * Extra parameters to pass to the `packageManager` when installing versioned packages.
40     * @example ['--no-save']
41     */
42    packageManagerArguments: string[];
43  }
44) {
45  // Read the project Expo config without plugins.
46  const { exp, pkg } = getConfig(projectRoot, {
47    // Sometimes users will add a plugin to the config before installing the library,
48    // this wouldn't work unless we dangerously disable plugin serialization.
49    skipPlugins: true,
50  });
51
52  if (pkg.expo?.install?.exclude?.length) {
53    Log.log(
54      chalk`Skipped ${fix ? 'fixing' : 'checking'} dependencies: ${joinWithCommasAnd(
55        pkg.expo.install.exclude
56      )}. These dependencies are listed in {bold expo.install.exclude} in package.json. ${learnMore(
57        'https://expo.dev/more/expo-cli/#configuring-dependency-validation'
58      )}`
59    );
60  }
61
62  const dependencies = await getVersionedDependenciesAsync(projectRoot, exp, pkg, packages);
63
64  if (!dependencies.length) {
65    Log.exit(chalk.greenBright('Dependencies are up to date'), 0);
66  }
67
68  logIncorrectDependencies(dependencies);
69
70  const value =
71    // If `--fix` then always fix.
72    fix ||
73    // Otherwise prompt to fix when not running in CI.
74    (isInteractive() && (await confirmAsync({ message: 'Fix dependencies?' }).catch(() => false)));
75
76  if (value) {
77    debug('Installing fixed dependencies:', dependencies);
78    // Install the corrected dependencies.
79    return fixPackagesAsync(projectRoot, {
80      packageManager,
81      packages: dependencies,
82      packageManagerArguments,
83      sdkVersion: exp.sdkVersion!,
84    });
85  }
86  // Exit with non-zero exit code if any of the dependencies are out of date.
87  Log.exit(chalk.red('Found outdated dependencies'), 1);
88}
89