From 94711bfb06e46a7742dbddf5b1b6c48ecf345495 Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Tue, 1 Nov 2016 09:13:49 -0700 Subject: [PATCH] Use yarn when available Summary: **Motivation** `react-native init` takes minutes even on a fast network. Yarn makes it much quicker. When yarn is not installed on the system: screenshot 2016-10-31 22 21 19 When yarn is installed: screenshot 2016-10-31 22 02 20 Also added the option `react-native init AwesomeApp --npm` as a fallback in case yarn is not stable enough for some people (I saw some Github issues that yarn hangs for example; for me it works great though). **Test plan** 1. Publish to Sinopia: https://github.com/facebook/react-native/tree/master/react-native-cli 2. `react-native init AwesomeApp` ***Tested the following setups*** - New CLI - uses yarn, new react-native - uses yarn - Old CLI (1.0.0) - doesn't use yarn, new react-native - uses yarn - Closes https://github.com/facebook/react-native/pull/10626 Differential Revision: D4110883 Pulled By: bestander fbshipit-source-id: 8a3427e2bc9158cf5fadd8ddf5b31e4b50ce6453 --- local-cli/generator/index.js | 59 +++++++++++++++++++--- react-native-cli/index.js | 94 ++++++++++++++++++++++++++--------- react-native-cli/package.json | 2 +- 3 files changed, 123 insertions(+), 32 deletions(-) diff --git a/local-cli/generator/index.js b/local-cli/generator/index.js index 310387fe5..2385ba417 100644 --- a/local-cli/generator/index.js +++ b/local-cli/generator/index.js @@ -8,11 +8,40 @@ */ 'use strict'; +var execSync = require('child_process').execSync; var fs = require('fs'); var path = require('path'); +var semver = require('semver') var utils = require('../generator-utils'); var yeoman = require('yeoman-generator'); +// Use Yarn if available, it's much faster than the npm client. +// Return the version of yarn installed on the system, null if yarn is not available. +function getYarnVersionIfAvailable() { + let yarnVersion; + try { + // execSync returns a Buffer -> convert to string + if (process.platform.startsWith('win')) { + yarnVersion = (execSync('yarn --version').toString() || '').trim(); + } else { + yarnVersion = (execSync('yarn --version 2>/dev/null').toString() || '').trim(); + } + } catch (error) { + return null; + } + // yarn < 0.16 has a 'missing manifest' bug + try { + if (semver.gte(yarnVersion, '0.16.0')) { + return yarnVersion; + } else { + return null; + } + } catch (error) { + console.error('Cannot parse yarn version: ' + yarnVersion); + return null; + } +} + module.exports = yeoman.generators.NamedBase.extend({ constructor: function() { yeoman.generators.NamedBase.apply(this, arguments); @@ -36,6 +65,12 @@ module.exports = yeoman.generators.NamedBase.extend({ type: Boolean, defaults: false }); + // Temporary option until yarn becomes stable. + this.option('npm', { + desc: 'Use the npm client, even if yarn is available.', + type: Boolean, + defaults: false + }); // this passes command line arguments down to the composed generators var args = {args: arguments[0], options: this.options}; @@ -112,12 +147,24 @@ module.exports = yeoman.generators.NamedBase.extend({ return; } - this.npmInstall(`react@${reactVersion}`, { '--save': true, '--save-exact': true }); + const yarnVersion = (!this.options['npm']) && getYarnVersionIfAvailable(); + + console.log('Installing React...'); + if (yarnVersion) { + execSync(`yarn add react@${reactVersion}`); + } else { + this.npmInstall(`react@${reactVersion}`, { '--save': true, '--save-exact': true }); + } if (!this.options['skip-jest']) { - this.npmInstall(`jest babel-jest babel-preset-react-native react-test-renderer@${reactVersion}`.split(' '), { - saveDev: true, - '--save-exact': true - }); + console.log('Installing Jest...'); + if (yarnVersion) { + execSync(`yarn add jest babel-jest jest-react-native babel-preset-react-native react-test-renderer@${reactVersion} --dev --exact`); + } else { + this.npmInstall(`jest babel-jest babel-preset-react-native react-test-renderer@${reactVersion}`.split(' '), { + saveDev: true, + '--save-exact': true + }); + } fs.writeFileSync( path.join( this.destinationRoot(), @@ -125,7 +172,6 @@ module.exports = yeoman.generators.NamedBase.extend({ ), '{\n"presets": ["react-native"]\n}' ); - this.fs.copy( this.templatePath('__tests__'), this.destinationPath('__tests__'), @@ -133,7 +179,6 @@ module.exports = yeoman.generators.NamedBase.extend({ nodir: false } ); - var packageJSONPath = path.join( this.destinationRoot(), 'package.json' diff --git a/react-native-cli/index.js b/react-native-cli/index.js index 4beea40cc..fa1e5fc4e 100755 --- a/react-native-cli/index.js +++ b/react-native-cli/index.js @@ -38,6 +38,7 @@ var fs = require('fs'); var path = require('path'); var exec = require('child_process').exec; +var execSync = require('child_process').execSync; var spawn = require('child_process').spawn; var chalk = require('chalk'); var prompt = require('prompt'); @@ -74,6 +75,33 @@ var REACT_NATIVE_PACKAGE_JSON_PATH = function() { ); }; +// Use Yarn if available, it's much faster than the npm client. +// Return the version of yarn installed on the system, null if yarn is not available. +function getYarnVersionIfAvailable() { + let yarnVersion; + try { + // execSync returns a Buffer -> convert to string + if (process.platform.startsWith('win')) { + yarnVersion = (execSync('yarn --version').toString() || '').trim(); + } else { + yarnVersion = (execSync('yarn --version 2>/dev/null').toString() || '').trim(); + } + } catch (error) { + return null; + } + // yarn < 0.16 has a 'missing manifest' bug + try { + if (semver.gte(yarnVersion, '0.16.0')) { + return yarnVersion; + } else { + return null; + } + } catch (error) { + console.error('Cannot parse yarn version: ' + yarnVersion); + return null; + } +} + checkForVersionArgument(); var cli; @@ -102,8 +130,8 @@ if (cli) { ); process.exit(1); } else { - if (!argv.verbose) console.log('This may take some time...'); - init(commands[1], argv.verbose, argv.version); + const rnPackage = argv.version; + init(commands[1], argv.verbose, rnPackage, argv.npm); } break; default: @@ -117,7 +145,7 @@ if (cli) { } } -function validatePackageName(name) { +function validateProjectName(name) { if (!name.match(/^[$A-Z_][0-9A-Z_$]*$/i)) { console.error( '"%s" is not a valid name for a project. Please use a valid identifier ' + @@ -137,17 +165,24 @@ function validatePackageName(name) { } } -function init(name, verbose, rnPackage) { - validatePackageName(name); +/** + * @param name Project name, e.g. 'AwesomeApp'. + * @param verbose If true, will run 'npm install' in verbose mode (for debugging). + * @param rnPackage Version of React Native to install, e.g. '0.38.0'. + * @param forceNpmClient If true, always use the npm command line client, + * don't use yarn even if available. + */ +function init(name, verbose, rnPackage, forceNpmClient) { + validateProjectName(name); if (fs.existsSync(name)) { - createAfterConfirmation(name, verbose, rnPackage); + createAfterConfirmation(name, verbose, rnPackage, forceNpmClient); } else { - createProject(name, verbose, rnPackage); + createProject(name, verbose, rnPackage, forceNpmClient); } } -function createAfterConfirmation(name, verbose, rnPackage) { +function createAfterConfirmation(name, verbose, rnPackage, forceNpmClient) { prompt.start(); var property = { @@ -160,7 +195,7 @@ function createAfterConfirmation(name, verbose, rnPackage) { prompt.get(property, function (err, result) { if (result.yesno[0] === 'y') { - createProject(name, verbose, rnPackage); + createProject(name, verbose, rnPackage, forceNpmClient); } else { console.log('Project initialization canceled'); process.exit(); @@ -168,7 +203,7 @@ function createAfterConfirmation(name, verbose, rnPackage) { }); } -function createProject(name, verbose, rnPackage) { +function createProject(name, verbose, rnPackage, forceNpmClient) { var root = path.resolve(name); var projectName = path.basename(root); @@ -192,20 +227,18 @@ function createProject(name, verbose, rnPackage) { fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(packageJson)); process.chdir(root); - console.log('Installing react-native package from npm...'); - if (verbose) { - runVerbose(root, projectName, rnPackage); + runVerbose(root, projectName, rnPackage, forceNpmClient); } else { - run(root, projectName, rnPackage); + run(root, projectName, rnPackage, forceNpmClient); } } function getInstallPackage(rnPackage) { var packageToInstall = 'react-native'; - var valideSemver = semver.valid(rnPackage); - if (valideSemver) { - packageToInstall += '@' + valideSemver; + var isValidSemver = semver.valid(rnPackage); + if (isValidSemver) { + packageToInstall += '@' + isValidSemver; } else if (rnPackage) { // for tar.gz or alternative paths packageToInstall = rnPackage; @@ -213,23 +246,36 @@ function getInstallPackage(rnPackage) { return packageToInstall; } -function run(root, projectName, rnPackage) { - exec('npm install --save --save-exact ' + getInstallPackage(rnPackage), function(e, stdout, stderr) { - if (e) { +function run(root, projectName, rnPackage, forceNpmClient) { + const yarnVersion = (!forceNpmClient) && getYarnVersionIfAvailable(); + let installCommand; + if (yarnVersion) { + console.log('Using yarn v' + yarnVersion); + console.log('Installing ' + getInstallPackage(rnPackage) + '...'); + installCommand = 'yarn add ' + getInstallPackage(rnPackage) + ' --exact'; + } else { + console.log('Installing ' + getInstallPackage(rnPackage) + ' from npm...'); + if (!forceNpmClient) { + console.log('Consider installing yarn to make this faster: https://yarnpkg.com'); + } + installCommand = 'npm install --save --save-exact ' + getInstallPackage(rnPackage); + } + exec(installCommand, function(err, stdout, stderr) { + if (err) { console.log(stdout); console.error(stderr); - console.error('`npm install --save --save-exact react-native` failed'); + console.error('Command `' + installCommand + '` failed.'); process.exit(1); } - checkNodeVersion(); - cli = require(CLI_MODULE_PATH()); cli.init(root, projectName); }); } -function runVerbose(root, projectName, rnPackage) { +function runVerbose(root, projectName, rnPackage, forceNpmClient) { + // Use npm client, yarn doesn't support --verbose yet + console.log('Installing ' + getInstallPackage(rnPackage) + ' from npm. This might take a while...'); var proc = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['install', '--verbose', '--save', '--save-exact', getInstallPackage(rnPackage)], {stdio: 'inherit'}); proc.on('close', function (code) { if (code !== 0) { diff --git a/react-native-cli/package.json b/react-native-cli/package.json index 894186f84..554daf9bd 100644 --- a/react-native-cli/package.json +++ b/react-native-cli/package.json @@ -1,6 +1,6 @@ { "name": "react-native-cli", - "version": "1.0.0", + "version": "1.1.0", "license": "BSD-3-Clause", "description": "The React Native CLI tools", "main": "index.js",