diff --git a/packages/expo-cli/src/commands/eject.ts b/packages/expo-cli/src/commands/eject.ts index a627e0aa47..72bbab525c 100644 --- a/packages/expo-cli/src/commands/eject.ts +++ b/packages/expo-cli/src/commands/eject.ts @@ -19,7 +19,7 @@ async function userWantsToEjectWithoutUpgradingAsync() { async function action( projectDir: string, - options: LegacyEject.EjectAsyncOptions | Eject.EjectAsyncOptions + options: (LegacyEject.EjectAsyncOptions | Eject.EjectAsyncOptions) & { npm?: boolean } ) { let exp: ExpoConfig; try { @@ -31,6 +31,10 @@ async function action( process.exit(1); } + if (options.npm) { + options.packageManager = 'npm'; + } + // Set EXPO_VIEW_DIR to universe/exponent to pull expo view code locally instead of from S3 for ExpoKit if (Versions.lteSdkVersion(exp, '36.0.0')) { // Don't show a warning if we haven't released SDK 37 yet @@ -67,5 +71,7 @@ export default function (program: Command) { '-f --force', 'Will attempt to generate an iOS project even when the system is not running macOS. Unsafe and may fail.' ) + .option('--no-install', 'Skip installing npm packages and CocoaPods.') + .option('--npm', 'Use npm to install dependencies. (default when Yarn is not installed)') .asyncActionProjectDir(action); } diff --git a/packages/expo-cli/src/commands/eject/Eject.ts b/packages/expo-cli/src/commands/eject/Eject.ts index 170cdbe8a5..eeec0f3171 100644 --- a/packages/expo-cli/src/commands/eject/Eject.ts +++ b/packages/expo-cli/src/commands/eject/Eject.ts @@ -4,6 +4,7 @@ import { PackageJSONConfig, WarningAggregator, getConfig, + projectHasModule, resolveModule, } from '@expo/config'; import JsonFile from '@expo/json-file'; @@ -36,6 +37,7 @@ type DependenciesMap = { [key: string]: string | number }; export type EjectAsyncOptions = { verbose?: boolean; force?: boolean; + install?: boolean; packageManager?: 'npm' | 'yarn'; }; @@ -53,14 +55,33 @@ export async function ejectAsync(projectRoot: string, options?: EjectAsyncOption if (await maybeBailOnGitStatusAsync()) return; await createNativeProjectsFromTemplateAsync(projectRoot); - await installNodeModulesAsync(projectRoot); + // TODO: Set this to true when we can detect that the user is running eject to sync new changes rather than ejecting to bare. + // This will be used to prevent the node modules from being nuked every time. + const isSyncing = false; + + // Install node modules + const shouldInstall = options?.install !== false; + + const packageManager = CreateApp.resolvePackageManager({ + install: shouldInstall, + npm: options?.packageManager === 'npm', + yarn: options?.packageManager === 'yarn', + }); + + if (shouldInstall) { + await installNodeDependenciesAsync(projectRoot, packageManager, { clean: !isSyncing }); + } // Apply Expo config to native projects await configureIOSStepAsync(projectRoot); await configureAndroidStepAsync(projectRoot); - const podsInstalled = await CreateApp.installCocoaPodsAsync(projectRoot); - await warnIfDependenciesRequireAdditionalSetupAsync(projectRoot); + // Install CocoaPods + let podsInstalled: boolean = false; + if (shouldInstall) { + podsInstalled = await CreateApp.installCocoaPodsAsync(projectRoot); + } + await warnIfDependenciesRequireAdditionalSetupAsync(projectRoot, options); log.newLine(); log.nested(`โžก๏ธ ${chalk.bold('Next steps')}`); @@ -70,11 +91,12 @@ export async function ejectAsync(projectRoot: string, options?: EjectAsyncOption `- ๐Ÿ‘† Review the logs above and look for any warnings (โš ๏ธ ) that might need follow-up.` ); } - log.nested( - `- ๐Ÿ’ก You may want to run ${chalk.bold( - 'npx @react-native-community/cli doctor' - )} to help install any tools that your app may need to run your native projects.` - ); + + // Log a warning about needing to install node modules + if (options?.install === false) { + const installCmd = packageManager === 'npm' ? 'npm install' : 'yarn'; + log.nested(`- โš ๏ธ Install node modules: ${log.chalk.bold(installCmd)}`); + } if (!podsInstalled) { log.nested( `- ๐Ÿซ When CocoaPods is installed, initialize the project workspace: ${chalk.bold( @@ -82,6 +104,11 @@ export async function ejectAsync(projectRoot: string, options?: EjectAsyncOption )}` ); } + log.nested( + `- ๐Ÿ’ก You may want to run ${chalk.bold( + 'npx @react-native-community/cli doctor' + )} to help install any tools that your app may need to run your native projects.` + ); log.nested( `- ๐Ÿ”‘ Download your Android keystore (if you're not sure if you need to, just run the command and see): ${chalk.bold( 'expo fetch:android:keystore' @@ -107,7 +134,7 @@ export async function ejectAsync(projectRoot: string, options?: EjectAsyncOption log.nested( 'To compile and run your project in development, execute one of the following commands:' ); - const packageManager = PackageManager.isUsingYarn(projectRoot) ? 'yarn' : 'npm'; + log.nested(`- ${chalk.bold(packageManager === 'npm' ? 'npm run ios' : 'yarn ios')}`); log.nested(`- ${chalk.bold(packageManager === 'npm' ? 'npm run android' : 'yarn android')}`); log.nested(`- ${chalk.bold(packageManager === 'npm' ? 'npm run web' : 'yarn web')}`); @@ -134,20 +161,31 @@ async function configureIOSStepAsync(projectRoot: string) { * * @param projectRoot */ -async function installNodeModulesAsync(projectRoot: string) { - const installingDependenciesStep = CreateApp.logNewSection('Installing JavaScript dependencies.'); - await fse.remove('node_modules'); - const packageManager = PackageManager.createForProject(projectRoot, { log, silent: true }); +async function installNodeDependenciesAsync( + projectRoot: string, + packageManager: 'yarn' | 'npm', + { clean = true }: { clean: boolean } +) { + if (clean) { + // This step can take a couple seconds, if the installation logs are enabled (with EXPO_DEBUG) then it + // ends up looking odd to see "Installing JavaScript dependencies" for ~5 seconds before the logs start showing up. + const cleanJsDepsStep = CreateApp.logNewSection('Cleaning JavaScript dependencies.'); + // nuke the node modules + // TODO: this is substantially slower, we should find a better alternative to ensuring the modules are installed. + await fse.remove('node_modules'); + cleanJsDepsStep.succeed('Cleaned JavaScript dependencies.'); + } + + const installJsDepsStep = CreateApp.logNewSection('Installing JavaScript dependencies.'); + try { - await packageManager.installAsync(); - installingDependenciesStep.succeed('Installed JavaScript dependencies.'); - } catch (e) { - installingDependenciesStep.fail( + await CreateApp.installNodeDependenciesAsync(projectRoot, packageManager); + installJsDepsStep.succeed('Installed JavaScript dependencies.'); + } catch { + installJsDepsStep.fail( chalk.red( - `Something when wrong installing JavaScript dependencies, check your ${ - packageManager.name - } logfile or run ${chalk.bold( - `${packageManager.name.toLowerCase()} install` + `Something when wrong installing JavaScript dependencies, check your ${packageManager} logfile or run ${chalk.bold( + `${packageManager} install` )} again manually.` ) ); @@ -496,15 +534,32 @@ ${sourceGitIgnore}`; * Some packages are not configured automatically on eject and may require * users to add some code, eg: to their AppDelegate. */ -async function warnIfDependenciesRequireAdditionalSetupAsync(projectRoot: string): Promise { +async function warnIfDependenciesRequireAdditionalSetupAsync( + projectRoot: string, + options?: EjectAsyncOptions +): Promise { // We just need the custom `nodeModulesPath` from the config. const { exp, pkg } = getConfig(projectRoot, { skipSDKVersionRequirement: true, }); - const pkgsWithExtraSetup = await JsonFile.readAsync( - resolveModule('expo/requiresExtraSetup.json', projectRoot, exp) - ); + const extraSetupPath = projectHasModule('expo/requiresExtraSetup.json', projectRoot, exp); + if (!extraSetupPath) { + const expoPath = projectHasModule('expo', projectRoot, exp); + // Check if expo is installed just in case the user has some version of expo that doesn't include a `requiresExtraSetup.json`. + if (!expoPath) { + log.addNewLineIfNone(); + // This can occur when --no-install is used. + log.nestedWarn( + `โš ๏ธ Not sure if any modules require extra setup because the ${log.chalk.bold( + 'expo' + )} package is not installed.` + ); + } + return; + } + + const pkgsWithExtraSetup = await JsonFile.readAsync(extraSetupPath); const packagesToWarn: string[] = Object.keys(pkg.dependencies).filter( pkgName => pkgName in pkgsWithExtraSetup ); diff --git a/packages/expo-cli/src/commands/init.ts b/packages/expo-cli/src/commands/init.ts index 56a346fb5e..487a5709a3 100644 --- a/packages/expo-cli/src/commands/init.ts +++ b/packages/expo-cli/src/commands/init.ts @@ -1,5 +1,4 @@ import { AndroidConfig, BareAppConfig, ExpoConfig, IOSConfig, getConfig } from '@expo/config'; -import * as PackageManager from '@expo/package-manager'; import spawnAsync from '@expo/spawn-async'; import { Exp, IosPlist, UserManager } from '@expo/xdl'; import chalk from 'chalk'; @@ -263,7 +262,7 @@ async function action(projectDir: string, command: Command) { // Install dependencies - const packageManager = resolvePackageManager(options); + const packageManager = CreateApp.resolvePackageManager(options); // TODO: not this const workflow = isBare ? 'bare' : 'managed'; @@ -330,39 +329,10 @@ async function action(projectDir: string, command: Command) { } } -type PackageManagerName = 'npm' | 'yarn'; - -// TODO: Use in eject as well -function resolvePackageManager( - options: Pick -): PackageManagerName { - let packageManager: PackageManagerName = 'npm'; - if (options.yarn || (!options.npm && PackageManager.shouldUseYarn())) { - packageManager = 'yarn'; - } else { - packageManager = 'npm'; - } - if (options.install) { - log.addNewLineIfNone(); - log( - packageManager === 'yarn' - ? '๐Ÿงถ Using Yarn to install packages. You can pass --npm to use npm instead.' - : '๐Ÿ“ฆ Using npm to install packages.' - ); - log.newLine(); - } - - return packageManager; -} - -async function installNodeDependenciesAsync( - projectRoot: string, - packageManager: 'yarn' | 'npm', - flags: { silent: boolean } = { silent: true } -) { +async function installNodeDependenciesAsync(projectRoot: string, packageManager: 'yarn' | 'npm') { const installJsDepsStep = CreateApp.logNewSection('Installing JavaScript dependencies.'); try { - await CreateApp.installNodeDependenciesAsync(projectRoot, packageManager, flags); + await CreateApp.installNodeDependenciesAsync(projectRoot, packageManager); installJsDepsStep.succeed('Installed JavaScript dependencies.'); } catch { installJsDepsStep.fail( diff --git a/packages/expo-cli/src/commands/utils/CreateApp.ts b/packages/expo-cli/src/commands/utils/CreateApp.ts index 941ee2bde1..8d49b1dde2 100644 --- a/packages/expo-cli/src/commands/utils/CreateApp.ts +++ b/packages/expo-cli/src/commands/utils/CreateApp.ts @@ -79,10 +79,41 @@ export async function assertFolderEmptyAsync({ return true; } +export type PackageManagerName = 'npm' | 'yarn'; + +export function resolvePackageManager(options: { + yarn?: boolean; + npm?: boolean; + install?: boolean; +}): PackageManagerName { + let packageManager: PackageManagerName = 'npm'; + if (options.yarn || (!options.npm && PackageManager.shouldUseYarn())) { + packageManager = 'yarn'; + } else { + packageManager = 'npm'; + } + if (options.install) { + log.addNewLineIfNone(); + log( + packageManager === 'yarn' + ? '๐Ÿงถ Using Yarn to install packages. You can pass --npm to use npm instead.' + : '๐Ÿ“ฆ Using npm to install packages.' + ); + log.newLine(); + } + + return packageManager; +} + +const EXPO_DEBUG = getenv.boolish('EXPO_DEBUG', false); + export async function installNodeDependenciesAsync( projectRoot: string, - packageManager: 'yarn' | 'npm', - flags: { silent: boolean } = { silent: false } + packageManager: PackageManagerName, + flags: { silent: boolean } = { + // default to silent + silent: !EXPO_DEBUG, + } ) { const options = { cwd: projectRoot, silent: flags.silent }; if (packageManager === 'yarn') { @@ -116,6 +147,8 @@ export async function installNodeDependenciesAsync( export function logNewSection(title: string) { const spinner = ora(log.chalk.bold(title)); + // respect loading indicators + log.setSpinner(spinner); spinner.start(); return spinner; } @@ -138,7 +171,7 @@ export async function installCocoaPodsAsync(projectRoot: string) { const packageManager = new PackageManager.CocoaPodsPackageManager({ cwd: path.join(projectRoot, 'ios'), log, - silent: getenv.boolish('EXPO_DEBUG', true), + silent: !EXPO_DEBUG, }); if (!(await packageManager.isCLIInstalledAsync())) { diff --git a/packages/expo-cli/src/log.ts b/packages/expo-cli/src/log.ts index 29d473c85e..a50169cd81 100644 --- a/packages/expo-cli/src/log.ts +++ b/packages/expo-cli/src/log.ts @@ -1,5 +1,6 @@ import chalk from 'chalk'; import program from 'commander'; +import { Ora } from 'ora'; import terminalLink from 'terminal-link'; type Color = (...text: string[]) => string; @@ -120,8 +121,23 @@ log.setBundleProgressBar = function setBundleProgressBar(bar: any) { _bundleProgressBar = bar; }; -log.setSpinner = function setSpinner(oraSpinner: any) { +log.setSpinner = function setSpinner(oraSpinner: Ora | null) { _oraSpinner = oraSpinner; + if (_oraSpinner) { + const originalStart = _oraSpinner.start.bind(_oraSpinner); + _oraSpinner.start = (text: any) => { + // Reset the new line tracker + _isLastLineNewLine = false; + return originalStart(text); + }; + // All other methods of stopping will invoke the stop method. + const originalStop = _oraSpinner.stop.bind(_oraSpinner); + _oraSpinner.stop = () => { + // Reset the target spinner + log.setSpinner(null); + return originalStop(); + }; + } }; log.error = function error(...args: any[]) {