From 2edf2180f2aa6bf647807d0b1fcd95f4cfe4a558 Mon Sep 17 00:00:00 2001 From: Peter Indiola 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 59361f9ff3..7224589238 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 85e93dc849..ec53436295 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 60ec9b9a59..0319b00b34 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 81bed9f098..0babf3478d 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 0000000000..b1a806b676 --- /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 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); + }); + }); +};