Skip to content

Commit 2edf218

Browse files
chocnutgaearon
authored andcommitted
Suggest another port when 3000 is busy (#101, #243)
Also fixes #194
1 parent a11d6a3 commit 2edf218

File tree

6 files changed

+149
-85
lines changed

6 files changed

+149
-85
lines changed

config/webpack.config.dev.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ var paths = require('./paths');
1616
module.exports = {
1717
devtool: 'eval',
1818
entry: [
19-
require.resolve('webpack-dev-server/client') + '?http://localhost:3000',
19+
require.resolve('webpack-dev-server/client'),
2020
require.resolve('webpack/hot/dev-server'),
2121
require.resolve('./polyfills'),
2222
path.join(paths.appSrc, 'index')

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"chalk": "1.1.3",
4242
"cross-spawn": "4.0.0",
4343
"css-loader": "0.23.1",
44+
"detect-port": "0.1.4",
4445
"eslint": "3.1.1",
4546
"eslint-loader": "1.4.1",
4647
"eslint-plugin-import": "1.10.3",

scripts/eject.js

+8-19
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,14 @@
99

1010
var fs = require('fs');
1111
var path = require('path');
12-
var rl = require('readline');
1312
var rimrafSync = require('rimraf').sync;
1413
var spawnSync = require('cross-spawn').sync;
15-
var paths = require('../config/paths');
14+
var prompt = require('./utils/prompt');
1615

17-
var prompt = function(question, cb) {
18-
var rlInterface = rl.createInterface({
19-
input: process.stdin,
20-
output: process.stdout,
21-
});
22-
rlInterface.question(question + '\n', function(answer) {
23-
rlInterface.close();
24-
cb(answer);
25-
})
26-
}
27-
28-
prompt('Are you sure you want to eject? This action is permanent. [y/N]', function(answer) {
29-
var shouldEject = answer && (
30-
answer.toLowerCase() === 'y' ||
31-
answer.toLowerCase() === 'yes'
32-
);
16+
prompt(
17+
'Are you sure you want to eject? This action is permanent.',
18+
false
19+
).then(shouldEject => {
3320
if (!shouldEject) {
3421
console.log('Close one! Eject aborted.');
3522
process.exit(1);
@@ -52,7 +39,8 @@ prompt('Are you sure you want to eject? This action is permanent. [y/N]', functi
5239
path.join('config', 'webpack.config.prod.js'),
5340
path.join('scripts', 'build.js'),
5441
path.join('scripts', 'start.js'),
55-
path.join('scripts', 'openChrome.applescript')
42+
path.join('scripts', 'utils', 'chrome.applescript'),
43+
path.join('scripts', 'utils', 'prompt.js')
5644
];
5745

5846
// 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
7260
fs.mkdirSync(path.join(appPath, 'config'));
7361
fs.mkdirSync(path.join(appPath, 'config', 'flow'));
7462
fs.mkdirSync(path.join(appPath, 'scripts'));
63+
fs.mkdirSync(path.join(appPath, 'scripts', 'utils'));
7564

7665
files.forEach(function(file) {
7766
console.log('Copying ' + file + ' to ' + appPath);

scripts/start.js

+99-65
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ var path = require('path');
1313
var chalk = require('chalk');
1414
var webpack = require('webpack');
1515
var WebpackDevServer = require('webpack-dev-server');
16-
var config = require('../config/webpack.config.dev');
1716
var execSync = require('child_process').execSync;
1817
var opn = require('opn');
18+
var detect = require('detect-port');
19+
var prompt = require('./utils/prompt');
20+
var config = require('../config/webpack.config.dev');
21+
22+
var DEFAULT_PORT = 3000;
23+
var compiler;
1924

2025
// TODO: hide this behind a flag and eliminate dead code on eject.
2126
// This shouldn't be exposed to the user.
@@ -63,72 +68,76 @@ function clearConsole() {
6368
process.stdout.write('\x1B[2J\x1B[0f');
6469
}
6570

66-
var compiler = webpack(config, handleCompile);
67-
compiler.plugin('invalid', function () {
68-
clearConsole();
69-
console.log('Compiling...');
70-
});
71-
compiler.plugin('done', function (stats) {
72-
clearConsole();
73-
var hasErrors = stats.hasErrors();
74-
var hasWarnings = stats.hasWarnings();
75-
if (!hasErrors && !hasWarnings) {
76-
console.log(chalk.green('Compiled successfully!'));
77-
console.log();
78-
console.log('The app is running at http://localhost:3000/');
79-
console.log();
80-
return;
81-
}
71+
function setupCompiler(port) {
72+
compiler = webpack(config, handleCompile);
8273

83-
var json = stats.toJson();
84-
var formattedErrors = json.errors.map(message =>
85-
'Error in ' + formatMessage(message)
86-
);
87-
var formattedWarnings = json.warnings.map(message =>
88-
'Warning in ' + formatMessage(message)
89-
);
74+
compiler.plugin('invalid', function() {
75+
clearConsole();
76+
console.log('Compiling...');
77+
});
9078

91-
if (hasErrors) {
92-
console.log(chalk.red('Failed to compile.'));
93-
console.log();
94-
if (formattedErrors.some(isLikelyASyntaxError)) {
95-
// If there are any syntax errors, show just them.
96-
// This prevents a confusing ESLint parsing error
97-
// preceding a much more useful Babel syntax error.
98-
formattedErrors = formattedErrors.filter(isLikelyASyntaxError);
99-
}
100-
formattedErrors.forEach(message => {
101-
console.log(message);
79+
compiler.plugin('done', function(stats) {
80+
clearConsole();
81+
var hasErrors = stats.hasErrors();
82+
var hasWarnings = stats.hasWarnings();
83+
if (!hasErrors && !hasWarnings) {
84+
console.log(chalk.green('Compiled successfully!'));
10285
console.log();
103-
});
104-
// If errors exist, ignore warnings.
105-
return;
106-
}
86+
console.log('The app is running at http://localhost:' + port + '/');
87+
console.log();
88+
return;
89+
}
10790

108-
if (hasWarnings) {
109-
console.log(chalk.yellow('Compiled with warnings.'));
110-
console.log();
111-
formattedWarnings.forEach(message => {
112-
console.log(message);
91+
var json = stats.toJson();
92+
var formattedErrors = json.errors.map(message =>
93+
'Error in ' + formatMessage(message)
94+
);
95+
var formattedWarnings = json.warnings.map(message =>
96+
'Warning in ' + formatMessage(message)
97+
);
98+
99+
if (hasErrors) {
100+
console.log(chalk.red('Failed to compile.'));
113101
console.log();
114-
});
102+
if (formattedErrors.some(isLikelyASyntaxError)) {
103+
// If there are any syntax errors, show just them.
104+
// This prevents a confusing ESLint parsing error
105+
// preceding a much more useful Babel syntax error.
106+
formattedErrors = formattedErrors.filter(isLikelyASyntaxError);
107+
}
108+
formattedErrors.forEach(message => {
109+
console.log(message);
110+
console.log();
111+
});
112+
// If errors exist, ignore warnings.
113+
return;
114+
}
115115

116-
console.log('You may use special comments to disable some warnings.');
117-
console.log('Use ' + chalk.yellow('// eslint-disable-next-line') + ' to ignore the next line.');
118-
console.log('Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.');
119-
}
120-
});
116+
if (hasWarnings) {
117+
console.log(chalk.yellow('Compiled with warnings.'));
118+
console.log();
119+
formattedWarnings.forEach(message => {
120+
console.log(message);
121+
console.log();
122+
});
123+
124+
console.log('You may use special comments to disable some warnings.');
125+
console.log('Use ' + chalk.yellow('// eslint-disable-next-line') + ' to ignore the next line.');
126+
console.log('Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.');
127+
}
128+
});
129+
}
121130

122-
function openBrowser() {
131+
function openBrowser(port) {
123132
if (process.platform === 'darwin') {
124133
try {
125134
// Try our best to reuse existing tab
126135
// on OS X Google Chrome with AppleScript
127136
execSync('ps cax | grep "Google Chrome"');
128137
execSync(
129138
'osascript ' +
130-
path.resolve(__dirname, './openChrome.applescript') +
131-
' http://localhost:3000/'
139+
path.resolve(__dirname, './utils/chrome.applescript') +
140+
' http://localhost:' + port + '/'
132141
);
133142
return;
134143
} catch (err) {
@@ -137,21 +146,46 @@ function openBrowser() {
137146
}
138147
// Fallback to opn
139148
// (It will always open new tab)
140-
opn('http://localhost:3000/');
149+
opn('http://localhost:' + port + '/');
150+
}
151+
152+
function runDevServer(port) {
153+
new WebpackDevServer(compiler, {
154+
historyApiFallback: true,
155+
hot: true, // Note: only CSS is currently hot reloaded
156+
publicPath: config.output.publicPath,
157+
quiet: true
158+
}).listen(port, (err, result) => {
159+
if (err) {
160+
return console.log(err);
161+
}
162+
163+
clearConsole();
164+
console.log(chalk.cyan('Starting the development server...'));
165+
console.log();
166+
openBrowser(port);
167+
});
141168
}
142169

143-
new WebpackDevServer(compiler, {
144-
historyApiFallback: true,
145-
hot: true, // Note: only CSS is currently hot reloaded
146-
publicPath: config.output.publicPath,
147-
quiet: true
148-
}).listen(3000, function (err, result) {
149-
if (err) {
150-
return console.log(err);
170+
function run(port) {
171+
setupCompiler(port);
172+
runDevServer(port);
173+
}
174+
175+
detect(DEFAULT_PORT).then(port => {
176+
if (port === DEFAULT_PORT) {
177+
run(port);
178+
return;
151179
}
152180

153181
clearConsole();
154-
console.log(chalk.cyan('Starting the development server...'));
155-
console.log();
156-
openBrowser();
182+
var question =
183+
chalk.yellow('Something is already running at port ' + DEFAULT_PORT + '.') +
184+
'\n\nWould you like to run the app at another port instead?';
185+
186+
prompt(question, true).then(shouldChangePort => {
187+
if (shouldChangePort) {
188+
run(port);
189+
}
190+
});
157191
});
File renamed without changes.

scripts/utils/prompt.js

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
10+
var rl = require('readline');
11+
12+
// Convention: "no" should be the conservative choice.
13+
// If you mistype the answer, we'll always take it as a "no".
14+
// You can control the behavior on <Enter> with `isYesDefault`.
15+
module.exports = function (question, isYesDefault) {
16+
if (typeof isYesDefault !== 'boolean') {
17+
throw new Error('Provide explicit boolean isYesDefault as second argument.');
18+
}
19+
return new Promise(resolve => {
20+
var rlInterface = rl.createInterface({
21+
input: process.stdin,
22+
output: process.stdout,
23+
});
24+
25+
var hint = isYesDefault === true ? '[Y/n]' : '[y/N]';
26+
var message = question + ' ' + hint + '\n';
27+
28+
rlInterface.question(message, function(answer) {
29+
rlInterface.close();
30+
31+
var useDefault = answer.trim().length === 0;
32+
if (useDefault) {
33+
return resolve(isYesDefault);
34+
}
35+
36+
var isYes = answer.match(/^(yes|y)$/i);
37+
return resolve(isYes);
38+
});
39+
});
40+
};

0 commit comments

Comments
 (0)