From 2edf2180f2aa6bf647807d0b1fcd95f4cfe4a558 Mon Sep 17 00:00:00 2001
From: Peter Indiola <pindiola@gmail.com>
Date: Wed, 27 Jul 2016 21:20:34 +0100
Subject: [PATCH] Suggest another port when 3000 is busy (#101, #243)

Also fixes #194
---
 config/webpack.config.dev.js                  |   2 +-
 package.json                                  |   1 +
 scripts/eject.js                              |  27 +--
 scripts/start.js                              | 164 +++++++++++-------
 .../chrome.applescript}                       |   0
 scripts/utils/prompt.js                       |  40 +++++
 6 files changed, 149 insertions(+), 85 deletions(-)
 rename scripts/{openChrome.applescript => utils/chrome.applescript} (100%)
 create mode 100644 scripts/utils/prompt.js

diff --git a/config/webpack.config.dev.js b/config/webpack.config.dev.js
index 59361f9ff..722458923 100644
--- a/config/webpack.config.dev.js
+++ b/config/webpack.config.dev.js
@@ -16,7 +16,7 @@ var paths = require('./paths');
 module.exports = {
   devtool: 'eval',
   entry: [
-    require.resolve('webpack-dev-server/client') + '?http://localhost:3000',
+    require.resolve('webpack-dev-server/client'),
     require.resolve('webpack/hot/dev-server'),
     require.resolve('./polyfills'),
     path.join(paths.appSrc, 'index')
diff --git a/package.json b/package.json
index 85e93dc84..ec5343629 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
     "chalk": "1.1.3",
     "cross-spawn": "4.0.0",
     "css-loader": "0.23.1",
+    "detect-port": "0.1.4",
     "eslint": "3.1.1",
     "eslint-loader": "1.4.1",
     "eslint-plugin-import": "1.10.3",
diff --git a/scripts/eject.js b/scripts/eject.js
index 60ec9b9a5..0319b00b3 100644
--- a/scripts/eject.js
+++ b/scripts/eject.js
@@ -9,27 +9,14 @@
 
 var fs = require('fs');
 var path = require('path');
-var rl = require('readline');
 var rimrafSync = require('rimraf').sync;
 var spawnSync = require('cross-spawn').sync;
-var paths = require('../config/paths');
+var prompt = require('./utils/prompt');
 
-var prompt = function(question, cb) {
-  var rlInterface = rl.createInterface({
-    input: process.stdin,
-    output: process.stdout,
-  });
-  rlInterface.question(question + '\n', function(answer) {
-    rlInterface.close();
-    cb(answer);
-  })
-}
-
-prompt('Are you sure you want to eject? This action is permanent. [y/N]', function(answer) {
-  var shouldEject = answer && (
-    answer.toLowerCase() === 'y' ||
-    answer.toLowerCase() === 'yes'
-  );
+prompt(
+  'Are you sure you want to eject? This action is permanent.',
+  false
+).then(shouldEject => {
   if (!shouldEject) {
     console.log('Close one! Eject aborted.');
     process.exit(1);
@@ -52,7 +39,8 @@ prompt('Are you sure you want to eject? This action is permanent. [y/N]', functi
     path.join('config', 'webpack.config.prod.js'),
     path.join('scripts', 'build.js'),
     path.join('scripts', 'start.js'),
-    path.join('scripts', 'openChrome.applescript')
+    path.join('scripts', 'utils', 'chrome.applescript'),
+    path.join('scripts', 'utils', 'prompt.js')
   ];
 
   // Ensure that the app folder is clean and we won't override any files
@@ -72,6 +60,7 @@ prompt('Are you sure you want to eject? This action is permanent. [y/N]', functi
   fs.mkdirSync(path.join(appPath, 'config'));
   fs.mkdirSync(path.join(appPath, 'config', 'flow'));
   fs.mkdirSync(path.join(appPath, 'scripts'));
+  fs.mkdirSync(path.join(appPath, 'scripts', 'utils'));
 
   files.forEach(function(file) {
     console.log('Copying ' + file + ' to ' + appPath);
diff --git a/scripts/start.js b/scripts/start.js
index 81bed9f09..0babf3478 100644
--- a/scripts/start.js
+++ b/scripts/start.js
@@ -13,9 +13,14 @@ var path = require('path');
 var chalk = require('chalk');
 var webpack = require('webpack');
 var WebpackDevServer = require('webpack-dev-server');
-var config = require('../config/webpack.config.dev');
 var execSync = require('child_process').execSync;
 var opn = require('opn');
+var detect = require('detect-port');
+var prompt = require('./utils/prompt');
+var config = require('../config/webpack.config.dev');
+
+var DEFAULT_PORT = 3000;
+var compiler;
 
 // TODO: hide this behind a flag and eliminate dead code on eject.
 // This shouldn't be exposed to the user.
@@ -63,63 +68,67 @@ function clearConsole() {
   process.stdout.write('\x1B[2J\x1B[0f');
 }
 
-var compiler = webpack(config, handleCompile);
-compiler.plugin('invalid', function () {
-  clearConsole();
-  console.log('Compiling...');
-});
-compiler.plugin('done', function (stats) {
-  clearConsole();
-  var hasErrors = stats.hasErrors();
-  var hasWarnings = stats.hasWarnings();
-  if (!hasErrors && !hasWarnings) {
-    console.log(chalk.green('Compiled successfully!'));
-    console.log();
-    console.log('The app is running at http://localhost:3000/');
-    console.log();
-    return;
-  }
+function setupCompiler(port) {
+  compiler = webpack(config, handleCompile);
 
-  var json = stats.toJson();
-  var formattedErrors = json.errors.map(message =>
-    'Error in ' + formatMessage(message)
-  );
-  var formattedWarnings = json.warnings.map(message =>
-    'Warning in ' + formatMessage(message)
-  );
+  compiler.plugin('invalid', function() {
+    clearConsole();
+    console.log('Compiling...');
+  });
 
-  if (hasErrors) {
-    console.log(chalk.red('Failed to compile.'));
-    console.log();
-    if (formattedErrors.some(isLikelyASyntaxError)) {
-      // If there are any syntax errors, show just them.
-      // This prevents a confusing ESLint parsing error
-      // preceding a much more useful Babel syntax error.
-      formattedErrors = formattedErrors.filter(isLikelyASyntaxError);
-    }
-    formattedErrors.forEach(message => {
-      console.log(message);
+  compiler.plugin('done', function(stats) {
+    clearConsole();
+    var hasErrors = stats.hasErrors();
+    var hasWarnings = stats.hasWarnings();
+    if (!hasErrors && !hasWarnings) {
+      console.log(chalk.green('Compiled successfully!'));
       console.log();
-    });
-    // If errors exist, ignore warnings.
-    return;
-  }
+      console.log('The app is running at http://localhost:' + port + '/');
+      console.log();
+      return;
+    }
 
-  if (hasWarnings) {
-    console.log(chalk.yellow('Compiled with warnings.'));
-    console.log();
-    formattedWarnings.forEach(message => {
-      console.log(message);
+    var json = stats.toJson();
+    var formattedErrors = json.errors.map(message =>
+      'Error in ' + formatMessage(message)
+    );
+    var formattedWarnings = json.warnings.map(message =>
+      'Warning in ' + formatMessage(message)
+    );
+
+    if (hasErrors) {
+      console.log(chalk.red('Failed to compile.'));
       console.log();
-    });
+      if (formattedErrors.some(isLikelyASyntaxError)) {
+        // If there are any syntax errors, show just them.
+        // This prevents a confusing ESLint parsing error
+        // preceding a much more useful Babel syntax error.
+        formattedErrors = formattedErrors.filter(isLikelyASyntaxError);
+      }
+      formattedErrors.forEach(message => {
+        console.log(message);
+        console.log();
+      });
+      // If errors exist, ignore warnings.
+      return;
+    }
 
-    console.log('You may use special comments to disable some warnings.');
-    console.log('Use ' + chalk.yellow('// eslint-disable-next-line') + ' to ignore the next line.');
-    console.log('Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.');
-  }
-});
+    if (hasWarnings) {
+      console.log(chalk.yellow('Compiled with warnings.'));
+      console.log();
+      formattedWarnings.forEach(message => {
+        console.log(message);
+        console.log();
+      });
+
+      console.log('You may use special comments to disable some warnings.');
+      console.log('Use ' + chalk.yellow('// eslint-disable-next-line') + ' to ignore the next line.');
+      console.log('Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.');
+    }
+  });
+}
 
-function openBrowser() {
+function openBrowser(port) {
   if (process.platform === 'darwin') {
     try {
       // Try our best to reuse existing tab
@@ -127,8 +136,8 @@ function openBrowser() {
       execSync('ps cax | grep "Google Chrome"');
       execSync(
         'osascript ' +
-        path.resolve(__dirname, './openChrome.applescript') +
-        ' http://localhost:3000/'
+        path.resolve(__dirname, './utils/chrome.applescript') +
+        ' http://localhost:' + port + '/'
       );
       return;
     } catch (err) {
@@ -137,21 +146,46 @@ function openBrowser() {
   }
   // Fallback to opn
   // (It will always open new tab)
-  opn('http://localhost:3000/');
+  opn('http://localhost:' + port + '/');
+}
+
+function runDevServer(port) {
+  new WebpackDevServer(compiler, {
+    historyApiFallback: true,
+    hot: true, // Note: only CSS is currently hot reloaded
+    publicPath: config.output.publicPath,
+    quiet: true
+  }).listen(port, (err, result) => {
+    if (err) {
+      return console.log(err);
+    }
+
+    clearConsole();
+    console.log(chalk.cyan('Starting the development server...'));
+    console.log();
+    openBrowser(port);
+  });
 }
 
-new WebpackDevServer(compiler, {
-  historyApiFallback: true,
-  hot: true, // Note: only CSS is currently hot reloaded
-  publicPath: config.output.publicPath,
-  quiet: true
-}).listen(3000, function (err, result) {
-  if (err) {
-    return console.log(err);
+function run(port) {
+  setupCompiler(port);
+  runDevServer(port);
+}
+
+detect(DEFAULT_PORT).then(port => {
+  if (port === DEFAULT_PORT) {
+    run(port);
+    return;
   }
 
   clearConsole();
-  console.log(chalk.cyan('Starting the development server...'));
-  console.log();
-  openBrowser();
+  var question =
+    chalk.yellow('Something is already running at port ' + DEFAULT_PORT + '.') +
+    '\n\nWould you like to run the app at another port instead?';
+
+  prompt(question, true).then(shouldChangePort => {
+    if (shouldChangePort) {
+      run(port);
+    }
+  });
 });
diff --git a/scripts/openChrome.applescript b/scripts/utils/chrome.applescript
similarity index 100%
rename from scripts/openChrome.applescript
rename to scripts/utils/chrome.applescript
diff --git a/scripts/utils/prompt.js b/scripts/utils/prompt.js
new file mode 100644
index 000000000..b1a806b67
--- /dev/null
+++ b/scripts/utils/prompt.js
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+var rl = require('readline');
+
+// Convention: "no" should be the conservative choice.
+// If you mistype the answer, we'll always take it as a "no".
+// You can control the behavior on <Enter> with `isYesDefault`.
+module.exports = function (question, isYesDefault) {
+  if (typeof isYesDefault !== 'boolean') {
+    throw new Error('Provide explicit boolean isYesDefault as second argument.');
+  }
+  return new Promise(resolve => {
+    var rlInterface = rl.createInterface({
+      input: process.stdin,
+      output: process.stdout,
+    });
+
+    var hint = isYesDefault === true ? '[Y/n]' : '[y/N]';
+    var message = question + ' ' + hint + '\n';
+
+    rlInterface.question(message, function(answer) {
+      rlInterface.close();
+
+      var useDefault = answer.trim().length === 0;
+      if (useDefault) {
+        return resolve(isYesDefault);
+      }
+
+      var isYes = answer.match(/^(yes|y)$/i);
+      return resolve(isYes);
+    });
+  });
+};
-- 
GitLab