diff --git a/e2e/__tests__/init.test.js b/e2e/__tests__/init.test.js index 08cd0f780..d8760d17b 100644 --- a/e2e/__tests__/init.test.js +++ b/e2e/__tests__/init.test.js @@ -50,7 +50,7 @@ test('init --template', () => { 'TestInit', ]); - expect(stdout).toContain('Initializing new project from external template'); + expect(stdout).toContain('Welcome to React Native!'); expect(stdout).toContain('Run instructions'); // make sure we don't leave garbage diff --git a/packages/cli/package.json b/packages/cli/package.json index e1a7fcfdf..4a7684e49 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -48,6 +48,7 @@ "node-fetch": "^2.2.0", "node-notifier": "^5.2.1", "opn": "^3.0.2", + "ora": "^3.4.0", "plist": "^3.0.0", "semver": "^5.0.3", "serve-static": "^1.13.1", diff --git a/packages/cli/src/cliEntry.js b/packages/cli/src/cliEntry.js index fded0f87f..af2827b28 100644 --- a/packages/cli/src/cliEntry.js +++ b/packages/cli/src/cliEntry.js @@ -18,7 +18,7 @@ import {getCommands} from './commands'; import init from './commands/init/initCompat'; import assertRequiredOptions from './tools/assertRequiredOptions'; import logger from './tools/logger'; -import {setProjectDir} from './tools/PackageManager'; +import {setProjectDir} from './tools/packageManager'; import pkgJson from '../package.json'; import loadConfig from './tools/config'; diff --git a/packages/cli/src/commands/init/__tests__/template.test.js b/packages/cli/src/commands/init/__tests__/template.test.js index 657a2ca1f..829dcd2f6 100644 --- a/packages/cli/src/commands/init/__tests__/template.test.js +++ b/packages/cli/src/commands/init/__tests__/template.test.js @@ -1,7 +1,8 @@ // @flow +jest.mock('execa', () => jest.fn()); +import execa from 'execa'; import path from 'path'; -import ChildProcess from 'child_process'; -import * as PackageManger from '../../../tools/PackageManager'; +import * as PackageManger from '../../../tools/packageManager'; import { installTemplatePackage, getTemplateConfig, @@ -14,15 +15,17 @@ const TEMPLATE_NAME = 'templateName'; afterEach(() => { jest.restoreAllMocks(); + jest.clearAllMocks(); }); -test('installTemplatePackage', () => { +test('installTemplatePackage', async () => { jest.spyOn(PackageManger, 'install').mockImplementationOnce(() => {}); - installTemplatePackage(TEMPLATE_NAME, true); + await installTemplatePackage(TEMPLATE_NAME, true); expect(PackageManger.install).toHaveBeenCalledWith([TEMPLATE_NAME], { preferYarn: false, + silent: true, }); }); @@ -68,21 +71,20 @@ test('copyTemplate', () => { expect(copyFiles.default).toHaveBeenCalledWith(expect.any(String), CWD); }); -test('executePostInitScript', () => { +test('executePostInitScript', async () => { const RESOLVED_PATH = '/some/path/script.js'; const SCRIPT_PATH = './script.js'; jest.spyOn(path, 'resolve').mockImplementationOnce(() => RESOLVED_PATH); - jest.spyOn(ChildProcess, 'execFileSync').mockImplementationOnce(() => {}); - executePostInitScript(TEMPLATE_NAME, SCRIPT_PATH); + await executePostInitScript(TEMPLATE_NAME, SCRIPT_PATH); expect(path.resolve).toHaveBeenCalledWith( 'node_modules', TEMPLATE_NAME, SCRIPT_PATH, ); - expect(ChildProcess.execFileSync).toHaveBeenCalledWith(RESOLVED_PATH, { + expect(execa).toHaveBeenCalledWith(RESOLVED_PATH, { stdio: 'inherit', }); }); diff --git a/packages/cli/src/commands/init/banner.js b/packages/cli/src/commands/init/banner.js new file mode 100644 index 000000000..bf865302d --- /dev/null +++ b/packages/cli/src/commands/init/banner.js @@ -0,0 +1,43 @@ +// @flow +import chalk from 'chalk'; + +const reactLogoArray = [ + ' ', + ' ###### ###### ', + ' ### #### #### ### ', + ' ## ### ### ## ', + ' ## #### ## ', + ' ## #### ## ', + ' ## ## ## ## ', + ' ## ### ### ## ', + ' ## ######################## ## ', + ' ###### ### ### ###### ', + ' ### ## ## ## ## ### ', + ' ### ## ### #### ### ## ### ', + ' ## #### ######## #### ## ', + ' ## ### ########## ### ## ', + ' ## #### ######## #### ## ', + ' ### ## ### #### ### ## ### ', + ' ### ## ## ## ## ### ', + ' ###### ### ### ###### ', + ' ## ######################## ## ', + ' ## ### ### ## ', + ' ## ## ## ## ', + ' ## #### ## ', + ' ## #### ## ', + ' ## ### ### ## ', + ' ### #### #### ### ', + ' ###### ###### ', + ' ', +]; + +const welcomeMessage = + ' Welcome to React Native! '; +const learnOnceMessage = + ' Learn Once Write Anywhere '; + +export default `${chalk.blue(reactLogoArray.join('\n'))} + +${chalk.yellow.bold(welcomeMessage)} +${chalk.gray(learnOnceMessage)} +`; diff --git a/packages/cli/src/commands/init/init.js b/packages/cli/src/commands/init/init.js index 24c8b965a..e39421d70 100644 --- a/packages/cli/src/commands/init/init.js +++ b/packages/cli/src/commands/init/init.js @@ -14,8 +14,10 @@ import { executePostInitScript, } from './template'; import {changePlaceholderInTemplate} from './editTemplate'; -import * as PackageManager from '../../tools/PackageManager'; +import * as PackageManager from '../../tools/packageManager'; import {processTemplateName} from './templateName'; +import banner from './banner'; +import {getLoader} from '../../tools/loader'; type Options = {| template?: string, @@ -39,21 +41,46 @@ async function createFromExternalTemplate( templateName: string, npm?: boolean, ) { - logger.info('Initializing new project from external template'); + logger.debug('Initializing new project from external template'); + logger.log(banner); - let {uri, name} = await processTemplateName(templateName); + const Loader = getLoader(); - installTemplatePackage(uri, npm); - name = adjustNameIfUrl(name); - const templateConfig = getTemplateConfig(name); - copyTemplate(name, templateConfig.templateDir); - changePlaceholderInTemplate(projectName, templateConfig.placeholderName); + const loader = new Loader({text: 'Downloading template'}); + loader.start(); - if (templateConfig.postInitScript) { - executePostInitScript(name, templateConfig.postInitScript); - } + try { + let {uri, name} = await processTemplateName(templateName); + + await installTemplatePackage(uri, npm); + loader.succeed(); + loader.start('Copying template'); + + name = adjustNameIfUrl(name); + const templateConfig = getTemplateConfig(name); + copyTemplate(name, templateConfig.templateDir); + + loader.succeed(); + loader.start('Preparing template'); - PackageManager.installAll({preferYarn: !npm}); + changePlaceholderInTemplate(projectName, templateConfig.placeholderName); + + loader.succeed(); + const {postInitScript} = templateConfig; + if (postInitScript) { + // Leaving trailing space because there may be stdout from the script + loader.start('Executing post init script '); + await executePostInitScript(name, postInitScript); + loader.succeed(); + } + + loader.start('Installing all required dependencies'); + await PackageManager.installAll({preferYarn: !npm, silent: true}); + loader.succeed(); + } catch (e) { + loader.fail(); + throw new Error(e); + } } async function createFromReactNativeTemplate( @@ -61,28 +88,52 @@ async function createFromReactNativeTemplate( version: string, npm?: boolean, ) { - logger.info('Initializing new project'); + logger.debug('Initializing new project'); + logger.log(banner); - if (semver.valid(version) && !semver.satisfies(version, '0.60.0')) { - throw new Error( - 'Cannot use React Native CLI to initialize project with version less than 0.60.0', - ); - } + const Loader = getLoader(); + const loader = new Loader({text: 'Downloading template'}); + loader.start(); - const TEMPLATE_NAME = 'react-native'; + try { + if (semver.valid(version) && !semver.gte(version, '0.60.0')) { + throw new Error( + 'Cannot use React Native CLI to initialize project with version less than 0.60.0', + ); + } - const {uri} = await processTemplateName(`${TEMPLATE_NAME}@${version}`); + const TEMPLATE_NAME = 'react-native'; - installTemplatePackage(uri, npm); - const templateConfig = getTemplateConfig(TEMPLATE_NAME); - copyTemplate(TEMPLATE_NAME, templateConfig.templateDir); - changePlaceholderInTemplate(projectName, templateConfig.placeholderName); + const {uri} = await processTemplateName(`${TEMPLATE_NAME}@${version}`); - if (templateConfig.postInitScript) { - executePostInitScript(TEMPLATE_NAME, templateConfig.postInitScript); - } + await installTemplatePackage(uri, npm); + + loader.succeed(); + loader.start('Copying template'); + + const templateConfig = getTemplateConfig(TEMPLATE_NAME); + copyTemplate(TEMPLATE_NAME, templateConfig.templateDir); - PackageManager.installAll({preferYarn: !npm}); + loader.succeed(); + loader.start('Processing template'); + + changePlaceholderInTemplate(projectName, templateConfig.placeholderName); + + loader.succeed(); + const {postInitScript} = templateConfig; + if (postInitScript) { + loader.start('Executing post init script'); + await executePostInitScript(TEMPLATE_NAME, postInitScript); + loader.succeed(); + } + + loader.start('Installing all required dependencies'); + await PackageManager.installAll({preferYarn: !npm, silent: true}); + loader.succeed(); + } catch (e) { + loader.fail(); + throw new Error(e); + } } function createProject(projectName: string, options: Options, version: string) { diff --git a/packages/cli/src/commands/init/initCompat.js b/packages/cli/src/commands/init/initCompat.js index 4d059f29d..9c18fca00 100644 --- a/packages/cli/src/commands/init/initCompat.js +++ b/packages/cli/src/commands/init/initCompat.js @@ -13,7 +13,7 @@ import path from 'path'; import process from 'process'; import printRunInstructions from './printRunInstructions'; import {createProjectFromTemplate} from '../../tools/generator/templates'; -import * as PackageManager from '../../tools/PackageManager'; +import * as PackageManager from '../../tools/packageManager'; import logger from '../../tools/logger'; /** @@ -25,7 +25,7 @@ import logger from '../../tools/logger'; * @param options Command line options passed from the react-native-cli directly. * E.g. `{ version: '0.43.0', template: 'navigation' }` */ -function initCompat(projectDir, argsOrName) { +async function initCompat(projectDir, argsOrName) { const args = Array.isArray(argsOrName) ? argsOrName // argsOrName was e.g. ['AwesomeApp', '--verbose'] : [argsOrName].concat(process.argv.slice(4)); // argsOrName was e.g. 'AwesomeApp' @@ -40,7 +40,7 @@ function initCompat(projectDir, argsOrName) { const options = minimist(args); logger.info(`Setting up new React Native app in ${projectDir}`); - generateProject(projectDir, newProjectName, options); + await generateProject(projectDir, newProjectName, options); } /** @@ -48,12 +48,12 @@ function initCompat(projectDir, argsOrName) { * @param Absolute path at which the project folder should be created. * @param options Command line arguments parsed by minimist. */ -function generateProject(destinationRoot, newProjectName, options) { +async function generateProject(destinationRoot, newProjectName, options) { const pkgJson = require('react-native/package.json'); const reactVersion = pkgJson.peerDependencies.react; - PackageManager.setProjectDir(destinationRoot); - createProjectFromTemplate( + await PackageManager.setProjectDir(destinationRoot); + await createProjectFromTemplate( destinationRoot, newProjectName, options.template, @@ -61,10 +61,10 @@ function generateProject(destinationRoot, newProjectName, options) { ); logger.info('Adding required dependencies'); - PackageManager.install([`react@${reactVersion}`]); + await PackageManager.install([`react@${reactVersion}`]); logger.info('Adding required dev dependencies'); - PackageManager.installDev([ + await PackageManager.installDev([ '@babel/core', '@babel/runtime', '@react-native-community/eslint-config', diff --git a/packages/cli/src/commands/init/template.js b/packages/cli/src/commands/init/template.js index dd7180ece..ef5066c55 100644 --- a/packages/cli/src/commands/init/template.js +++ b/packages/cli/src/commands/init/template.js @@ -1,7 +1,8 @@ // @flow -import {execFileSync} from 'child_process'; + +import execa from 'execa'; import path from 'path'; -import * as PackageManager from '../../tools/PackageManager'; +import * as PackageManager from '../../tools/packageManager'; import logger from '../../tools/logger'; import copyFiles from '../../tools/copyFiles'; @@ -13,7 +14,10 @@ export type TemplateConfig = { export function installTemplatePackage(templateName: string, npm?: boolean) { logger.debug(`Installing template from ${templateName}`); - PackageManager.install([templateName], {preferYarn: !npm}); + return PackageManager.install([templateName], { + preferYarn: !npm, + silent: true, + }); } export function getTemplateConfig(templateName: string): TemplateConfig { @@ -44,5 +48,5 @@ export function executePostInitScript( logger.debug(`Executing post init script located ${scriptPath}`); - execFileSync(scriptPath, {stdio: 'inherit'}); + return execa(scriptPath, {stdio: 'inherit'}); } diff --git a/packages/cli/src/commands/install/install.js b/packages/cli/src/commands/install/install.js index ffcb7f0df..f5c00ff15 100644 --- a/packages/cli/src/commands/install/install.js +++ b/packages/cli/src/commands/install/install.js @@ -9,7 +9,7 @@ import type {ContextT} from '../../tools/types.flow'; import logger from '../../tools/logger'; -import * as PackageManager from '../../tools/PackageManager'; +import * as PackageManager from '../../tools/packageManager'; import link from '../link/link'; import loadConfig from '../../tools/config'; @@ -17,7 +17,7 @@ async function install(args: Array, ctx: ContextT) { const name = args[0]; logger.info(`Installing "${name}"...`); - PackageManager.install([name]); + await PackageManager.install([name]); // Reload configuration to see newly installed dependency const newConfig = loadConfig(); diff --git a/packages/cli/src/commands/install/uninstall.js b/packages/cli/src/commands/install/uninstall.js index 654c594a8..d0b70ed8a 100644 --- a/packages/cli/src/commands/install/uninstall.js +++ b/packages/cli/src/commands/install/uninstall.js @@ -9,7 +9,7 @@ import type {ContextT} from '../../tools/types.flow'; import logger from '../../tools/logger'; -import * as PackageManager from '../../tools/PackageManager'; +import * as PackageManager from '../../tools/packageManager'; import link from '../link/unlink'; async function uninstall(args: Array, ctx: ContextT) { @@ -19,7 +19,7 @@ async function uninstall(args: Array, ctx: ContextT) { await link.func([name], ctx); logger.info(`Uninstalling "${name}"...`); - PackageManager.uninstall([name]); + await PackageManager.uninstall([name]); logger.success(`Successfully uninstalled and unlinked "${name}"`); } diff --git a/packages/cli/src/commands/upgrade/__tests__/upgrade.test.js b/packages/cli/src/commands/upgrade/__tests__/upgrade.test.js index 4e91f187d..812156fa6 100644 --- a/packages/cli/src/commands/upgrade/__tests__/upgrade.test.js +++ b/packages/cli/src/commands/upgrade/__tests__/upgrade.test.js @@ -34,7 +34,7 @@ jest.mock( () => ({name: 'TestApp', dependencies: {'react-native': '^0.57.8'}}), {virtual: true}, ); -jest.mock('../../../tools/PackageManager', () => ({ +jest.mock('../../../tools/packageManager', () => ({ install: args => { mockPushLog('$ yarn add', ...args); }, diff --git a/packages/cli/src/commands/upgrade/upgrade.js b/packages/cli/src/commands/upgrade/upgrade.js index 148cfd99b..514041a12 100644 --- a/packages/cli/src/commands/upgrade/upgrade.js +++ b/packages/cli/src/commands/upgrade/upgrade.js @@ -6,7 +6,7 @@ import semver from 'semver'; import execa from 'execa'; import type {ContextT} from '../../tools/types.flow'; import logger from '../../tools/logger'; -import * as PackageManager from '../../tools/PackageManager'; +import * as PackageManager from '../../tools/packageManager'; import {fetch} from '../../tools/fetch'; import legacyUpgrade from './legacyUpgrade'; @@ -115,7 +115,7 @@ const installDeps = async (newVersion, projectDir) => { `react-native@${newVersion}`, ...Object.keys(peerDeps).map(module => `${module}@${peerDeps[module]}`), ]; - PackageManager.install(deps, { + await PackageManager.install(deps, { silent: true, }); await execa('git', ['add', 'package.json']); diff --git a/packages/cli/src/tools/__tests__/PackageManager-test.js b/packages/cli/src/tools/__tests__/packageManager-test.js similarity index 54% rename from packages/cli/src/tools/__tests__/PackageManager-test.js rename to packages/cli/src/tools/__tests__/packageManager-test.js index 33c4ce210..647daea21 100644 --- a/packages/cli/src/tools/__tests__/PackageManager-test.js +++ b/packages/cli/src/tools/__tests__/packageManager-test.js @@ -1,17 +1,16 @@ // @flow -import ChildProcess from 'child_process'; -import * as PackageManager from '../PackageManager'; +jest.mock('execa', () => jest.fn()); +import execa from 'execa'; import * as yarn from '../yarn'; +import logger from '../logger'; +import * as PackageManager from '../packageManager'; const PACKAGES = ['react', 'react-native']; const EXEC_OPTS = {stdio: 'inherit'}; const PROJECT_ROOT = '/some/dir'; -beforeEach(() => { - jest.spyOn(ChildProcess, 'execSync').mockImplementation(() => {}); -}); afterEach(() => { - (ChildProcess.execSync: any).mockRestore(); + jest.resetAllMocks(); }); describe('yarn', () => { @@ -19,22 +18,22 @@ describe('yarn', () => { jest .spyOn(yarn, 'getYarnVersionIfAvailable') .mockImplementation(() => true); + + jest.spyOn(logger, 'isVerbose').mockImplementation(() => false); }); it('should install', () => { PackageManager.install(PACKAGES, {preferYarn: true}); - expect(ChildProcess.execSync).toHaveBeenCalledWith( - 'yarn add react react-native', - EXEC_OPTS, - ); + expect(execa).toHaveBeenCalledWith('yarn', ['add', ...PACKAGES], EXEC_OPTS); }); it('should installDev', () => { PackageManager.installDev(PACKAGES, {preferYarn: true}); - expect(ChildProcess.execSync).toHaveBeenCalledWith( - 'yarn add -D react react-native', + expect(execa).toHaveBeenCalledWith( + 'yarn', + ['add', '-D', ...PACKAGES], EXEC_OPTS, ); }); @@ -42,8 +41,9 @@ describe('yarn', () => { it('should uninstall', () => { PackageManager.uninstall(PACKAGES, {preferYarn: true}); - expect(ChildProcess.execSync).toHaveBeenCalledWith( - 'yarn remove react react-native', + expect(execa).toHaveBeenCalledWith( + 'yarn', + ['remove', ...PACKAGES], EXEC_OPTS, ); }); @@ -53,8 +53,9 @@ describe('npm', () => { it('should install', () => { PackageManager.install(PACKAGES, {preferYarn: false}); - expect(ChildProcess.execSync).toHaveBeenCalledWith( - 'npm install react react-native --save --save-exact', + expect(execa).toHaveBeenCalledWith( + 'npm', + ['install', ...PACKAGES, '--save', '--save-exact'], EXEC_OPTS, ); }); @@ -62,8 +63,9 @@ describe('npm', () => { it('should installDev', () => { PackageManager.installDev(PACKAGES, {preferYarn: false}); - expect(ChildProcess.execSync).toHaveBeenCalledWith( - 'npm install react react-native --save-dev --save-exact', + expect(execa).toHaveBeenCalledWith( + 'npm', + ['install', ...PACKAGES, '--save-dev', '--save-exact'], EXEC_OPTS, ); }); @@ -71,8 +73,9 @@ describe('npm', () => { it('should uninstall', () => { PackageManager.uninstall(PACKAGES, {preferYarn: false}); - expect(ChildProcess.execSync).toHaveBeenCalledWith( - 'npm uninstall react react-native --save', + expect(execa).toHaveBeenCalledWith( + 'npm', + ['uninstall', ...PACKAGES, '--save'], EXEC_OPTS, ); }); @@ -82,8 +85,9 @@ it('should use npm if yarn is not available', () => { jest.spyOn(yarn, 'getYarnVersionIfAvailable').mockImplementation(() => false); PackageManager.install(PACKAGES, {preferYarn: true}); - expect(ChildProcess.execSync).toHaveBeenCalledWith( - 'npm install react react-native --save --save-exact', + expect(execa).toHaveBeenCalledWith( + 'npm', + ['install', ...PACKAGES, '--save', '--save-exact'], EXEC_OPTS, ); }); @@ -94,8 +98,9 @@ it('should use npm if project is not using yarn', () => { PackageManager.setProjectDir(PROJECT_ROOT); PackageManager.install(PACKAGES); - expect(ChildProcess.execSync).toHaveBeenCalledWith( - 'npm install react react-native --save --save-exact', + expect(execa).toHaveBeenCalledWith( + 'npm', + ['install', ...PACKAGES, '--save', '--save-exact'], EXEC_OPTS, ); expect(yarn.isProjectUsingYarn).toHaveBeenCalledWith(PROJECT_ROOT); @@ -108,9 +113,23 @@ it('should use yarn if project is using yarn', () => { PackageManager.setProjectDir(PROJECT_ROOT); PackageManager.install(PACKAGES); - expect(ChildProcess.execSync).toHaveBeenCalledWith( - 'yarn add react react-native', - EXEC_OPTS, - ); + expect(execa).toHaveBeenCalledWith('yarn', ['add', ...PACKAGES], EXEC_OPTS); expect(yarn.isProjectUsingYarn).toHaveBeenCalledWith(PROJECT_ROOT); }); + +test.each([[false, 'pipe'], [true, 'inherit']])( + 'when verbose is set to %s should use "%s" stdio', + (isVerbose: boolean, stdioType: string) => { + jest + .spyOn(yarn, 'getYarnVersionIfAvailable') + .mockImplementation(() => true); + jest.spyOn(yarn, 'isProjectUsingYarn').mockImplementation(() => true); + jest.spyOn(logger, 'isVerbose').mockImplementation(() => isVerbose); + + PackageManager.install(PACKAGES, {silent: true}); + + expect(execa).toHaveBeenCalledWith('yarn', ['add', ...PACKAGES], { + stdio: stdioType, + }); + }, +); diff --git a/packages/cli/src/tools/generator/templates.js b/packages/cli/src/tools/generator/templates.js index b771efa72..460cdead2 100644 --- a/packages/cli/src/tools/generator/templates.js +++ b/packages/cli/src/tools/generator/templates.js @@ -13,7 +13,7 @@ import fs from 'fs'; import path from 'path'; import copyProjectTemplateAndReplace from './copyProjectTemplateAndReplace'; import logger from '../logger'; -import * as PackageManager from '../PackageManager'; +import * as PackageManager from '../packageManager'; /** * @param destPath Create the new project at this path. @@ -22,7 +22,7 @@ import * as PackageManager from '../PackageManager'; * @param yarnVersion Version of yarn available on the system, or null if * yarn is not available. For example '0.18.1'. */ -function createProjectFromTemplate( +async function createProjectFromTemplate( destPath: string, newProjectName: string, template: string, @@ -44,7 +44,12 @@ function createProjectFromTemplate( // This way we don't have to duplicate the native files in every template. // If we duplicated them we'd make RN larger and risk that people would // forget to maintain all the copies so they would go out of sync. - createFromRemoteTemplate(template, destPath, newProjectName, destinationRoot); + await createFromRemoteTemplate( + template, + destPath, + newProjectName, + destinationRoot, + ); } /** @@ -52,7 +57,7 @@ function createProjectFromTemplate( * - 'demo' -> Fetch the package react-native-template-demo from npm * - git://..., http://..., file://... or any other URL supported by npm */ -function createFromRemoteTemplate( +async function createFromRemoteTemplate( template: string, destPath: string, newProjectName: string, @@ -73,7 +78,7 @@ function createFromRemoteTemplate( // Check if the template exists logger.info(`Fetching template ${installPackage}...`); try { - PackageManager.install([installPackage]); + await PackageManager.install([installPackage]); const templatePath = path.resolve('node_modules', templateName); copyProjectTemplateAndReplace(templatePath, destPath, newProjectName, { // Every template contains a dummy package.json file included @@ -86,12 +91,12 @@ function createFromRemoteTemplate( 'devDependencies.json', ], }); - installTemplateDependencies(templatePath, destinationRoot); - installTemplateDevDependencies(templatePath, destinationRoot); + await installTemplateDependencies(templatePath, destinationRoot); + await installTemplateDevDependencies(templatePath, destinationRoot); } finally { // Clean up the temp files try { - PackageManager.uninstall([templateName]); + await PackageManager.uninstall([templateName]); } catch (err) { // Not critical but we still want people to know and report // if this the clean up fails. @@ -103,7 +108,7 @@ function createFromRemoteTemplate( } } -function installTemplateDependencies(templatePath, destinationRoot) { +async function installTemplateDependencies(templatePath, destinationRoot) { // dependencies.json is a special file that lists additional dependencies // that are required by this template const dependenciesJsonPath = path.resolve(templatePath, 'dependencies.json'); @@ -124,12 +129,12 @@ function installTemplateDependencies(templatePath, destinationRoot) { const dependenciesToInstall = Object.keys(dependencies).map( depName => `${depName}@${dependencies[depName]}`, ); - PackageManager.install(dependenciesToInstall); + await PackageManager.install(dependenciesToInstall); logger.info("Linking native dependencies into the project's build files..."); execSync('react-native link', {stdio: 'inherit'}); } -function installTemplateDevDependencies(templatePath, destinationRoot) { +async function installTemplateDevDependencies(templatePath, destinationRoot) { // devDependencies.json is a special file that lists additional develop dependencies // that are required by this template const devDependenciesJsonPath = path.resolve( @@ -154,7 +159,7 @@ function installTemplateDevDependencies(templatePath, destinationRoot) { const dependenciesToInstall = Object.keys(dependencies).map( depName => `${depName}@${dependencies[depName]}`, ); - PackageManager.installDev(dependenciesToInstall); + await PackageManager.installDev(dependenciesToInstall); } export {createProjectFromTemplate}; diff --git a/packages/cli/src/tools/loader.js b/packages/cli/src/tools/loader.js new file mode 100644 index 000000000..525cc5c9d --- /dev/null +++ b/packages/cli/src/tools/loader.js @@ -0,0 +1,15 @@ +// @flow +import Ora from 'ora'; +import logger from './logger'; + +class OraMock { + succeed() {} + fail() {} + start() {} +} + +function getLoader(): typeof Ora { + return logger.isVerbose() ? OraMock : Ora; +} + +export {getLoader}; diff --git a/packages/cli/src/tools/logger.js b/packages/cli/src/tools/logger.js index 42e6e262f..8bb9cbddd 100644 --- a/packages/cli/src/tools/logger.js +++ b/packages/cli/src/tools/logger.js @@ -40,6 +40,8 @@ const setVerbose = (level: boolean) => { verbose = level; }; +const isVerbose = () => verbose; + export default { success, info, @@ -48,4 +50,5 @@ export default { debug, log, setVerbose, + isVerbose, }; diff --git a/packages/cli/src/tools/PackageManager.js b/packages/cli/src/tools/packageManager.js similarity index 54% rename from packages/cli/src/tools/PackageManager.js rename to packages/cli/src/tools/packageManager.js index fa4b999f1..941a218c7 100644 --- a/packages/cli/src/tools/PackageManager.js +++ b/packages/cli/src/tools/packageManager.js @@ -1,5 +1,6 @@ // @flow -import {execSync} from 'child_process'; +import execa from 'execa'; +import logger from './logger'; import {getYarnVersionIfAvailable, isProjectUsingYarn} from './yarn'; type Options = {| @@ -9,9 +10,14 @@ type Options = {| let projectDir; -function executeCommand(command: string, options?: Options) { - return execSync(command, { - stdio: options && options.silent ? 'pipe' : 'inherit', +function executeCommand( + command: string, + args: Array, + options?: Options, +) { + return execa(command, args, { + stdio: + options && options.silent && !logger.isVerbose() ? 'pipe' : 'inherit', }); } @@ -29,30 +35,32 @@ export function setProjectDir(dir: string) { export function install(packageNames: Array, options?: Options) { return shouldUseYarn(options) - ? executeCommand(`yarn add ${packageNames.join(' ')}`, options) + ? executeCommand('yarn', ['add', ...packageNames], options) : executeCommand( - `npm install ${packageNames.join(' ')} --save --save-exact`, + 'npm', + ['install', ...packageNames, '--save', '--save-exact'], options, ); } export function installDev(packageNames: Array, options?: Options) { return shouldUseYarn(options) - ? executeCommand(`yarn add -D ${packageNames.join(' ')}`, options) + ? executeCommand('yarn', ['add', '-D', ...packageNames], options) : executeCommand( - `npm install ${packageNames.join(' ')} --save-dev --save-exact`, + 'npm', + ['install', ...packageNames, '--save-dev', '--save-exact'], options, ); } export function uninstall(packageNames: Array, options?: Options) { return shouldUseYarn(options) - ? executeCommand(`yarn remove ${packageNames.join(' ')}`, options) - : executeCommand(`npm uninstall ${packageNames.join(' ')} --save`, options); + ? executeCommand('yarn', ['remove', ...packageNames], options) + : executeCommand('npm', ['uninstall', ...packageNames, '--save'], options); } export function installAll(options?: Options) { return shouldUseYarn(options) - ? executeCommand('yarn install') - : executeCommand('npm install'); + ? executeCommand('yarn', ['install'], options) + : executeCommand('npm', ['install'], options); } diff --git a/yarn.lock b/yarn.lock index 745260fc3..61eb22770 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2042,6 +2042,11 @@ ansi-regex@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.0.0.tgz#70de791edf021404c3fd615aa89118ae0432e5a9" +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -2703,6 +2708,11 @@ cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" +cli-spinners@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.0.0.tgz#4b078756fc17a8f72043fdc9f1f14bf4fa87e2df" + integrity sha512-yiEBmhaKPPeBj7wWm4GEdtPZK940p9pl3EANIrnJ3JnvWyrPjcFcsEq6qRUuQ7fzB0+Y82ld3p6B34xo95foWw== + cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" @@ -5779,6 +5789,13 @@ lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5 version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" +log-symbols@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" + integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== + dependencies: + chalk "^2.0.1" + loose-envify@^1.0.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -6719,6 +6736,18 @@ options@>=0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" +ora@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318" + integrity sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg== + dependencies: + chalk "^2.4.2" + cli-cursor "^2.1.0" + cli-spinners "^2.0.0" + log-symbols "^2.2.0" + strip-ansi "^5.2.0" + wcwidth "^1.0.1" + os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -8163,6 +8192,13 @@ strip-ansi@^5.0.0: dependencies: ansi-regex "^4.0.0" +strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -8669,7 +8705,7 @@ watch@~0.18.0: exec-sh "^0.2.0" minimist "^1.2.0" -wcwidth@^1.0.0: +wcwidth@^1.0.0, wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=