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 { env } 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 ?? !env.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