From bcc469c9a5c7916ec10786f133769cdda2c80188 Mon Sep 17 00:00:00 2001
From: Ville Immonen <ville.immonen@iki.fi>
Date: Thu, 17 Nov 2016 22:55:00 +0200
Subject: [PATCH] Support Yarn (#898)

In the `create-react-app` command, try to install packages using Yarn.
If Yarn is not installed, use npm instead.

In `react-scripts`, detect if the project is using Yarn by checking if
a `yarn.lock` file exists. If the project is using Yarn, display all
the instructions with Yarn commands and use Yarn to install packages
in `init` and `eject` scripts.
---
 .travis.yml                             |  3 ++
 packages/create-react-app/index.js      | 52 +++++++++++++++++++------
 packages/react-scripts/config/paths.js  |  3 ++
 packages/react-scripts/scripts/build.js | 17 ++++++--
 packages/react-scripts/scripts/eject.js | 14 +++++--
 packages/react-scripts/scripts/init.js  | 45 +++++++++++++--------
 packages/react-scripts/scripts/start.js |  6 ++-
 tasks/e2e.sh                            |  6 +++
 8 files changed, 110 insertions(+), 36 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 75383b878..ec44c3294 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,3 +9,6 @@ cache:
   - packages/create-react-app/node_modules
   - packages/react-scripts/node_modules
 script: tasks/e2e.sh
+env:
+  - USE_YARN=no
+  - USE_YARN=yes
diff --git a/packages/create-react-app/index.js b/packages/create-react-app/index.js
index d6478a135..23d8b5e8d 100644
--- a/packages/create-react-app/index.js
+++ b/packages/create-react-app/index.js
@@ -101,26 +101,54 @@ function createApp(name, verbose, version) {
   process.chdir(root);
 
   console.log('Installing packages. This might take a couple minutes.');
-  console.log('Installing react-scripts from npm...');
+  console.log('Installing react-scripts...');
   console.log();
 
   run(root, appName, version, verbose, originalDirectory);
 }
 
-function run(root, appName, version, verbose, originalDirectory) {
-  var installPackage = getInstallPackage(version);
-  var packageName = getPackageName(installPackage);
+function install(packageToInstall, verbose, callback) {
   var args = [
-    'install',
-    verbose && '--verbose',
-    '--save-dev',
-    '--save-exact',
-    installPackage,
-  ].filter(function(e) { return e; });
-  var proc = spawn('npm', args, {stdio: 'inherit'});
+    'add',
+    '--dev',
+    '--exact',
+    packageToInstall,
+  ];
+  var proc = spawn('yarn', args, {stdio: 'inherit'});
+
+  var yarnExists = true;
+  proc.on('error', function (err) {
+    if (err.code === 'ENOENT') {
+      yarnExists = false;
+    }
+  });
   proc.on('close', function (code) {
+    if (yarnExists) {
+      callback(code, 'yarn', args);
+      return;
+    }
+    // No Yarn installed, continuing with npm.
+    args = [
+      'install',
+      verbose && '--verbose',
+      '--save-dev',
+      '--save-exact',
+      packageToInstall,
+    ].filter(function(e) { return e; });
+    var npmProc = spawn('npm', args, {stdio: 'inherit'});
+    npmProc.on('close', function (code) {
+      callback(code, 'npm', args);
+    });
+  });
+}
+
+function run(root, appName, version, verbose, originalDirectory) {
+  var packageToInstall = getInstallPackage(version);
+  var packageName = getPackageName(packageToInstall);
+
+  install(packageToInstall, verbose, function (code, command, args) {
     if (code !== 0) {
-      console.error('`npm ' + args.join(' ') + '` failed');
+      console.error('`' + command + ' ' + args.join(' ') + '` failed');
       return;
     }
 
diff --git a/packages/react-scripts/config/paths.js b/packages/react-scripts/config/paths.js
index 1c154c361..89cd2059c 100644
--- a/packages/react-scripts/config/paths.js
+++ b/packages/react-scripts/config/paths.js
@@ -43,6 +43,7 @@ module.exports = {
   appIndexJs: resolveApp('src/index.js'),
   appPackageJson: resolveApp('package.json'),
   appSrc: resolveApp('src'),
+  yarnLockFile: resolveApp('yarn.lock'),
   testsSetup: resolveApp('src/setupTests.js'),
   appNodeModules: resolveApp('node_modules'),
   ownNodeModules: resolveApp('node_modules'),
@@ -62,6 +63,7 @@ module.exports = {
   appIndexJs: resolveApp('src/index.js'),
   appPackageJson: resolveApp('package.json'),
   appSrc: resolveApp('src'),
+  yarnLockFile: resolveApp('yarn.lock'),
   testsSetup: resolveApp('src/setupTests.js'),
   appNodeModules: resolveApp('node_modules'),
   // this is empty with npm3 but node resolution searches higher anyway:
@@ -79,6 +81,7 @@ if (__dirname.indexOf(path.join('packages', 'react-scripts', 'config')) !== -1)
     appIndexJs: resolveOwn('../template/src/index.js'),
     appPackageJson: resolveOwn('../package.json'),
     appSrc: resolveOwn('../template/src'),
+    yarnLockFile: resolveOwn('../template/yarn.lock'),
     testsSetup: resolveOwn('../template/src/setupTests.js'),
     appNodeModules: resolveOwn('../node_modules'),
     ownNodeModules: resolveOwn('../node_modules'),
diff --git a/packages/react-scripts/scripts/build.js b/packages/react-scripts/scripts/build.js
index d0b92f6a7..8b1cd4cc4 100644
--- a/packages/react-scripts/scripts/build.js
+++ b/packages/react-scripts/scripts/build.js
@@ -21,6 +21,7 @@ require('dotenv').config({silent: true});
 var chalk = require('chalk');
 var fs = require('fs-extra');
 var path = require('path');
+var pathExists = require('path-exists');
 var filesize = require('filesize');
 var gzipSize = require('gzip-size').sync;
 var rimrafSync = require('rimraf').sync;
@@ -31,6 +32,8 @@ var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
 var recursive = require('recursive-readdir');
 var stripAnsi = require('strip-ansi');
 
+var useYarn = pathExists.sync(paths.yarnLockFile);
+
 // Warn and crash if required files are missing
 if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
   process.exit(1);
@@ -161,7 +164,11 @@ function build(previousSizeMap) {
       console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
       console.log('To publish it at ' + chalk.green(homepagePath) + ', run:');
       console.log();
-      console.log('  ' + chalk.cyan('npm') +  ' install --save-dev gh-pages');
+      if (useYarn) {
+        console.log('  ' + chalk.cyan('yarn') +  ' add gh-pages');
+      } else {
+        console.log('  ' + chalk.cyan('npm') +  ' install --save-dev gh-pages');
+      }
       console.log();
       console.log('Add the following script in your ' + chalk.cyan('package.json') + '.');
       console.log();
@@ -173,7 +180,7 @@ function build(previousSizeMap) {
       console.log();
       console.log('Then run:');
       console.log();
-      console.log('  ' + chalk.cyan('npm') +  ' run deploy');
+      console.log('  ' + chalk.cyan(useYarn ? 'yarn' : 'npm') +  ' run deploy');
       console.log();
     } else if (publicPath !== '/') {
       // "homepage": "http://mywebsite.com/project"
@@ -200,7 +207,11 @@ function build(previousSizeMap) {
       console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
       console.log('You may also serve it locally with a static server:')
       console.log();
-      console.log('  ' + chalk.cyan('npm') +  ' install -g pushstate-server');
+      if (useYarn) {
+        console.log('  ' + chalk.cyan('yarn') +  ' global add pushstate-server');
+      } else {
+        console.log('  ' + chalk.cyan('npm') +  ' install -g pushstate-server');
+      }
       console.log('  ' + chalk.cyan('pushstate-server') + ' build');
       console.log('  ' + chalk.cyan(openCommand) + ' http://localhost:9000');
       console.log();
diff --git a/packages/react-scripts/scripts/eject.js b/packages/react-scripts/scripts/eject.js
index dbd4d64e4..7d4996665 100644
--- a/packages/react-scripts/scripts/eject.js
+++ b/packages/react-scripts/scripts/eject.js
@@ -10,6 +10,8 @@
 var createJestConfig = require('../utils/createJestConfig');
 var fs = require('fs');
 var path = require('path');
+var pathExists = require('path-exists');
+var paths = require('../config/paths');
 var prompt = require('react-dev-utils/prompt');
 var rimrafSync = require('rimraf').sync;
 var spawnSync = require('cross-spawn').sync;
@@ -143,9 +145,15 @@ prompt(
   );
   console.log();
 
-  console.log(cyan('Running npm install...'));
-  rimrafSync(ownPath);
-  spawnSync('npm', ['install'], {stdio: 'inherit'});
+  if (pathExists.sync(paths.yarnLockFile)) {
+    console.log(cyan('Running yarn...'));
+    rimrafSync(ownPath);
+    spawnSync('yarn', [], {stdio: 'inherit'});
+  } else {
+    console.log(cyan('Running npm install...'));
+    rimrafSync(ownPath);
+    spawnSync('npm', ['install'], {stdio: 'inherit'});
+  }
   console.log(green('Ejected successfully!'));
   console.log();
 
diff --git a/packages/react-scripts/scripts/init.js b/packages/react-scripts/scripts/init.js
index fa42f6dce..c9a4ea14a 100644
--- a/packages/react-scripts/scripts/init.js
+++ b/packages/react-scripts/scripts/init.js
@@ -17,6 +17,7 @@ module.exports = function(appPath, appName, verbose, originalDirectory) {
   var ownPackageName = require(path.join(__dirname, '..', 'package.json')).name;
   var ownPath = path.join(appPath, 'node_modules', ownPackageName);
   var appPackage = require(path.join(appPath, 'package.json'));
+  var useYarn = pathExists.sync(path.join(appPath, 'yarn.lock'));
 
   // Copy over some of the devDependencies
   appPackage.dependencies = appPackage.dependencies || {};
@@ -58,21 +59,31 @@ module.exports = function(appPath, appName, verbose, originalDirectory) {
     }
   });
 
-  // Run another npm install for react and react-dom
-  console.log('Installing react and react-dom from npm...');
+  // Run yarn or npm for react and react-dom
+  // TODO: having to do two npm/yarn installs is bad, can we avoid it?
+  var command;
+  var args;
+
+  if (useYarn) {
+    command = 'yarn';
+    args = ['add'];
+  } else {
+    command = 'npm';
+    args = [
+      'install',
+      '--save',
+      verbose && '--verbose'
+    ].filter(function(e) { return e; });
+  }
+  args.push('react', 'react-dom');
+
+  console.log('Installing react and react-dom using ' + command + '...');
   console.log();
-  // TODO: having to do two npm installs is bad, can we avoid it?
-  var args = [
-    'install',
-    'react',
-    'react-dom',
-    '--save',
-    verbose && '--verbose'
-  ].filter(function(e) { return e; });
-  var proc = spawn('npm', args, {stdio: 'inherit'});
+
+  var proc = spawn(command, args, {stdio: 'inherit'});
   proc.on('close', function (code) {
     if (code !== 0) {
-      console.error('`npm ' + args.join(' ') + '` failed');
+      console.error('`' + command + ' ' + args.join(' ') + '` failed');
       return;
     }
 
@@ -91,23 +102,23 @@ module.exports = function(appPath, appName, verbose, originalDirectory) {
     console.log('Success! Created ' + appName + ' at ' + appPath);
     console.log('Inside that directory, you can run several commands:');
     console.log();
-    console.log(chalk.cyan('  npm start'));
+    console.log(chalk.cyan('  ' + command + ' start'));
     console.log('    Starts the development server.');
     console.log();
-    console.log(chalk.cyan('  npm run build'));
+    console.log(chalk.cyan('  ' + command + ' run build'));
     console.log('    Bundles the app into static files for production.');
     console.log();
-    console.log(chalk.cyan('  npm test'));
+    console.log(chalk.cyan('  ' + command + ' test'));
     console.log('    Starts the test runner.');
     console.log();
-    console.log(chalk.cyan('  npm run eject'));
+    console.log(chalk.cyan('  ' + command + ' run eject'));
     console.log('    Removes this tool and copies build dependencies, configuration files');
     console.log('    and scripts into the app directory. If you do this, you can’t go back!');
     console.log();
     console.log('We suggest that you begin by typing:');
     console.log();
     console.log(chalk.cyan('  cd'), cdpath);
-    console.log('  ' + chalk.cyan('npm start'));
+    console.log('  ' + chalk.cyan(command + ' start'));
     if (readmeExists) {
       console.log();
       console.log(chalk.yellow('You had a `README.md` file, we renamed it to `README.old.md`'));
diff --git a/packages/react-scripts/scripts/start.js b/packages/react-scripts/scripts/start.js
index 8a115dd8e..5e996c71d 100644
--- a/packages/react-scripts/scripts/start.js
+++ b/packages/react-scripts/scripts/start.js
@@ -28,9 +28,13 @@ var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
 var formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
 var openBrowser = require('react-dev-utils/openBrowser');
 var prompt = require('react-dev-utils/prompt');
+var pathExists = require('path-exists');
 var config = require('../config/webpack.config.dev');
 var paths = require('../config/paths');
 
+var useYarn = pathExists.sync(paths.yarnLockFile);
+var cli = useYarn ? 'yarn' : 'npm';
+
 // Warn and crash if required files are missing
 if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
   process.exit(1);
@@ -85,7 +89,7 @@ function setupCompiler(host, port, protocol) {
       console.log('  ' + chalk.cyan(protocol + '://' + host + ':' + port + '/'));
       console.log();
       console.log('Note that the development build is not optimized.');
-      console.log('To create a production build, use ' + chalk.cyan('npm run build') + '.');
+      console.log('To create a production build, use ' + chalk.cyan(cli + ' run build') + '.');
       console.log();
     }
 
diff --git a/tasks/e2e.sh b/tasks/e2e.sh
index 88e1fdf4e..094fba9e2 100755
--- a/tasks/e2e.sh
+++ b/tasks/e2e.sh
@@ -53,6 +53,12 @@ set -x
 cd ..
 root_path=$PWD
 
+if [ "$USE_YARN" = "yes" ]
+then
+  # Install Yarn so that the test can use it to install packages.
+  npm install -g yarn
+fi
+
 npm install
 
 # Lint own code
-- 
GitLab