xref: /expo/packages/@expo/cli/src/utils/nodeModules.ts (revision b52e368f)
1import * as PackageManager from '@expo/package-manager';
2import chalk from 'chalk';
3import fs from 'fs';
4import yaml from 'js-yaml';
5import path from 'path';
6import semver from 'semver';
7
8import * as Log from '../log';
9import { EXPO_DEBUG } from './env';
10import { SilentError } from './errors';
11import { logNewSection } from './ora';
12
13export type PackageManagerName = 'npm' | 'yarn';
14
15export function resolvePackageManager(options: {
16  yarn?: boolean;
17  npm?: boolean;
18  install?: boolean;
19}): PackageManagerName {
20  let packageManager: PackageManagerName = 'npm';
21  if (options.yarn || (!options.npm && PackageManager.shouldUseYarn())) {
22    packageManager = 'yarn';
23  } else {
24    packageManager = 'npm';
25  }
26  if (options.install) {
27    Log.log(
28      packageManager === 'yarn'
29        ? `�� Using Yarn to install packages. ${chalk.dim('Pass --npm to use npm instead.')}`
30        : '�� Using npm to install packages.'
31    );
32  }
33
34  return packageManager;
35}
36
37export async function installNodeDependenciesAsync(
38  projectRoot: string,
39  packageManager: PackageManagerName,
40  flags: { silent?: boolean; clean?: boolean } = {}
41) {
42  // Default to silent unless debugging.
43  const isSilent = flags.silent ?? !EXPO_DEBUG;
44  if (flags.clean && packageManager !== 'yarn') {
45    // This step can take a couple seconds, if the installation logs are enabled (with EXPO_DEBUG) then it
46    // ends up looking odd to see "Installing JavaScript dependencies" for ~5 seconds before the logs start showing up.
47    const cleanJsDepsStep = logNewSection('Cleaning JavaScript dependencies');
48    const time = Date.now();
49    // nuke the node modules
50    // TODO: this is substantially slower, we should find a better alternative to ensuring the modules are installed.
51    await fs.promises.rm('node_modules', { recursive: true, force: true });
52    cleanJsDepsStep.succeed(
53      `Cleaned JavaScript dependencies ${chalk.gray(Date.now() - time + 'ms')}`
54    );
55  }
56
57  const installJsDepsStep = logNewSection('Installing JavaScript dependencies');
58  try {
59    const time = Date.now();
60    await installNodeDependenciesInternalAsync(projectRoot, packageManager, { silent: isSilent });
61    installJsDepsStep.succeed(
62      `Installed JavaScript dependencies ${chalk.gray(Date.now() - time + 'ms')}`
63    );
64  } catch {
65    const message = `Something went wrong installing JavaScript dependencies, check your ${packageManager} logfile or run ${chalk.bold(
66      `${packageManager} install`
67    )} again manually.`;
68    installJsDepsStep.fail(chalk.red(message));
69    // TODO: actually show the error message from the package manager! :O
70    throw new SilentError(message);
71  }
72}
73
74async function installNodeDependenciesInternalAsync(
75  projectRoot: string,
76  packageManager: PackageManagerName,
77  flags: { silent: boolean }
78) {
79  const options = { cwd: projectRoot, silent: flags.silent };
80  if (packageManager === 'yarn') {
81    const yarn = new PackageManager.YarnPackageManager(options);
82    const version = await yarn.versionAsync();
83    const nodeLinker = await yarn.getConfigAsync('nodeLinker');
84    if (semver.satisfies(version, '>=2.0.0-rc.24') && nodeLinker !== 'node-modules') {
85      const yarnRc = path.join(projectRoot, '.yarnrc.yml');
86      let yamlString = '';
87      try {
88        yamlString = fs.readFileSync(yarnRc, 'utf8');
89      } catch (error: any) {
90        if (error.code !== 'ENOENT') {
91          throw error;
92        }
93      }
94      const config = yamlString ? yaml.safeLoad(yamlString) : {};
95      // @ts-ignore
96      config.nodeLinker = 'node-modules';
97      !flags.silent &&
98        Log.warn(
99          `Yarn v${version} detected, enabling experimental Yarn v2 support using the node-modules plugin.`
100        );
101      !flags.silent && Log.log(`Writing ${yarnRc}...`);
102      fs.writeFileSync(yarnRc, yaml.safeDump(config));
103    }
104    await yarn.installAsync();
105  } else {
106    await new PackageManager.NpmPackageManager(options).installAsync();
107  }
108}
109