diff --git a/packages/cli/package.json b/packages/cli/package.json index 65dab618f..4eafca169 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -59,7 +59,9 @@ "semver": "^6.3.0", "serve-static": "^1.13.1", "shell-quote": "1.6.1", + "strip-ansi": "^5.2.0", "sudo-prompt": "^9.0.0", + "wcwidth": "^1.0.1", "ws": "^1.1.0" }, "peerDependencies": { @@ -71,6 +73,7 @@ "@types/minimist": "^1.2.0", "@types/mkdirp": "^0.5.2", "@types/semver": "^6.0.2", + "@types/wcwidth": "^1.0.0", "slash": "^3.0.0", "snapshot-diff": "^0.5.0" } diff --git a/packages/cli/src/commands/doctor/healthchecks/cocoaPods.ts b/packages/cli/src/commands/doctor/healthchecks/cocoaPods.ts index 81e362bad..406d6da2a 100644 --- a/packages/cli/src/commands/doctor/healthchecks/cocoaPods.ts +++ b/packages/cli/src/commands/doctor/healthchecks/cocoaPods.ts @@ -1,11 +1,92 @@ +import execa from 'execa'; +import chalk from 'chalk'; +import readline from 'readline'; +import wcwidth from 'wcwidth'; +import {logger} from '@react-native-community/cli-tools'; import {checkSoftwareInstalled} from '../checkInstallation'; -import {installCocoaPods} from '../../../tools/installPods'; +import { + promptCocoaPodsInstallationQuestion, + runSudo, +} from '../../../tools/installPods'; +import {brewInstall} from '../../../tools/brewInstall'; import {HealthCheckInterface} from '../types'; +function calculateQuestionSize(promptQuestion: string) { + return Math.max( + 1, + Math.ceil(wcwidth(promptQuestion) / (process.stdout.columns || 80)), + ); +} + +function clearQuestion(promptQuestion: string) { + readline.moveCursor( + process.stdout, + 0, + -calculateQuestionSize(promptQuestion), + ); + readline.clearScreenDown(process.stdout); +} + export default { label: 'CocoaPods', getDiagnostics: async () => ({ needsToBeFixed: await checkSoftwareInstalled('pod'), }), - runAutomaticFix: async ({loader}) => await installCocoaPods(loader), + runAutomaticFix: async ({loader}) => { + loader.stop(); + + const { + installMethod, + promptQuestion, + } = await promptCocoaPodsInstallationQuestion(); + + // Capitalise `Homebrew` when printing on the screen + const installMethodCapitalized = + installMethod === 'homebrew' + ? installMethod.substr(0, 1).toUpperCase() + installMethod.substr(1) + : installMethod; + const loaderInstallationMessage = `CocoaPods (installing with ${installMethodCapitalized})`; + const loaderSucceedMessage = `CocoaPods (installed with ${installMethodCapitalized})`; + + // Remove the prompt after the question of how to install CocoaPods is answered + clearQuestion(promptQuestion); + + if (installMethod === 'gem') { + loader.start(loaderInstallationMessage); + + const options = ['install', 'cocoapods', '--no-document']; + + try { + // First attempt to install `cocoapods` + await execa('gem', options); + + return loader.succeed(loaderSucceedMessage); + } catch (_error) { + // If that doesn't work then try with sudo + try { + await runSudo(`gem ${options.join(' ')}`); + + return loader.succeed(loaderSucceedMessage); + } catch (error) { + loader.fail(); + logger.log(chalk.dim(`\n${error}`)); + + return logger.log( + `An error occured while trying to install CocoaPods. Please try again manually: ${chalk.bold( + 'sudo gem install cocoapods', + )}`, + ); + } + } + } + + if (installMethod === 'homebrew') { + return await brewInstall({ + pkg: 'cocoapods', + label: loaderInstallationMessage, + loader, + onSuccess: () => loader.succeed(loaderSucceedMessage), + }); + } + }, } as HealthCheckInterface; diff --git a/packages/cli/src/tools/brewInstall.ts b/packages/cli/src/tools/brewInstall.ts index 949f82bf3..dc97e2909 100644 --- a/packages/cli/src/tools/brewInstall.ts +++ b/packages/cli/src/tools/brewInstall.ts @@ -7,15 +7,32 @@ type InstallArgs = { pkg: string; label?: string; loader: ora.Ora; + onSuccess?: () => void; + onFail?: () => void; }; -async function brewInstall({pkg, label, loader}: InstallArgs) { +async function brewInstall({ + pkg, + label, + loader, + onSuccess, + onFail, +}: InstallArgs) { loader.start(label); + try { await execa('brew', ['install', pkg]); - loader.succeed(); + if (typeof onSuccess === 'function') { + return onSuccess(); + } + + return loader.succeed(); } catch (error) { + if (typeof onFail === 'function') { + return onFail(); + } + loader.fail(); logger.log(chalk.dim(`\n${error.stderr}`)); logger.log( diff --git a/packages/cli/src/tools/installPods.ts b/packages/cli/src/tools/installPods.ts index d4032b87f..e2012d123 100644 --- a/packages/cli/src/tools/installPods.ts +++ b/packages/cli/src/tools/installPods.ts @@ -4,12 +4,18 @@ import chalk from 'chalk'; import ora from 'ora'; // @ts-ignore untyped import inquirer from 'inquirer'; +import stripAnsi from 'strip-ansi'; import {logger} from '@react-native-community/cli-tools'; import {NoopLoader} from './loader'; // @ts-ignore untyped import sudo from 'sudo-prompt'; import {brewInstall} from './brewInstall'; +type PromptCocoaPodsInstallation = { + installMethod: 'gem' | 'homebrew'; + promptQuestion: string; +}; + async function updatePods(loader: ora.Ora) { try { loader.start( @@ -43,56 +49,79 @@ function runSudo(command: string): Promise { }); } -async function installCocoaPods(loader: ora.Ora) { - loader.stop(); - - const withGem = 'Yes, with gem (may require sudo)'; - const withHomebrew = 'Yes, with Homebrew'; +async function promptCocoaPodsInstallationQuestion(): Promise< + PromptCocoaPodsInstallation +> { + const promptQuestion = `CocoaPods ${chalk.dim.underline( + '(https://cocoapods.org/)', + )} ${chalk.reset.bold( + 'is not installed. CocoaPods is necessary for the iOS project to run correctly. Do you want to install it?', + )}`; + const installWithGem = 'Yes, with gem (may require sudo)'; + const installWithHomebrew = 'Yes, with Homebrew'; const {shouldInstallCocoaPods} = await inquirer.prompt([ { type: 'list', name: 'shouldInstallCocoaPods', - message: `CocoaPods ${chalk.dim.underline( - '(https://cocoapods.org/)', - )} ${chalk.reset.bold( - 'is not installed. CocoaPods is necessary for the iOS project to run correctly. Do you want to install it?', - )}`, - choices: [withGem, withHomebrew], + message: promptQuestion, + choices: [installWithGem, installWithHomebrew], }, ]); - switch (shouldInstallCocoaPods) { - case withGem: - const options = ['install', 'cocoapods', '--no-document']; - loader.start('Installing CocoaPods'); - try { - // First attempt to install `cocoapods` - await execa('gem', options); - } catch (_error) { - // If that doesn't work then try with sudo - try { - await runSudo(`gem ${options.join(' ')}`); - } catch (error) { - loader.fail(); - logger.error(error.stderr); - - throw new Error( - `An error occured while trying to install CocoaPods, which is required by this template.\nPlease try again manually: sudo gem install cocoapods.\nCocoaPods documentation: ${chalk.dim.underline( - 'https://cocoapods.org/', - )}`, - ); - } - } - loader.succeed(); - break; - case withHomebrew: - await brewInstall({ - pkg: 'cocoapods', - label: 'Installing CocoaPods', - loader, - }); - break; + const shouldInstallWithGem = shouldInstallCocoaPods === installWithGem; + + return { + installMethod: shouldInstallWithGem ? 'gem' : 'homebrew', + // This is used for removing the message in `doctor` after it's answered + promptQuestion: `? ${stripAnsi(promptQuestion)} ${ + shouldInstallWithGem ? installWithGem : installWithHomebrew + }`, + }; +} + +async function installCocoaPodsWithGem() { + const options = ['install', 'cocoapods', '--no-document']; + + try { + // First attempt to install `cocoapods` + await execa('gem', options); + } catch (_error) { + // If that doesn't work then try with sudo + await runSudo(`gem ${options.join(' ')}`); + } +} + +async function installCocoaPods(loader: ora.Ora) { + loader.stop(); + + const {installMethod} = await promptCocoaPodsInstallationQuestion(); + + if (installMethod === 'gem') { + loader.start('Installing CocoaPods'); + + try { + await installCocoaPodsWithGem(); + + return loader.succeed(); + } catch (error) { + loader.fail(); + logger.error(error.stderr); + + throw new Error( + `An error occured while trying to install CocoaPods, which is required by this template.\nPlease try again manually: sudo gem install cocoapods.\nCocoaPods documentation: ${chalk.dim.underline( + 'https://cocoapods.org/', + )}`, + ); + } + } + + if (installMethod === 'homebrew') { + return await brewInstall({ + pkg: 'cocoapods', + label: 'Installing CocoaPods', + loader, + }); } } @@ -134,7 +163,6 @@ async function installPods({ } try { - loader.succeed(); loader.start( `Installing CocoaPods dependencies ${chalk.dim( '(this may take a few minutes)', @@ -158,6 +186,6 @@ async function installPods({ } } -export {installCocoaPods}; +export {promptCocoaPodsInstallationQuestion, runSudo, installCocoaPods}; export default installPods; diff --git a/yarn.lock b/yarn.lock index 1c5e31d92..962931f1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2163,6 +2163,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/wcwidth@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/wcwidth/-/wcwidth-1.0.0.tgz#a58f4673050f98c46ae8f852340889343b21a1f5" + integrity sha512-X/WFfwGCIisEnd9EOSsX/jt7BHPDkcvQVYwVzc1nsE2K5bC56mWKnmNs0wyjcGcQsP7Wxq2zWSmhDDbF5Z7dDg== + "@types/xmldoc@^1.1.4": version "1.1.4" resolved "https://registry.yarnpkg.com/@types/xmldoc/-/xmldoc-1.1.4.tgz#5867d4e29739719c633bf16413c5a4a4c1c3c802"