Commit 8dc9217b authored by Dan Abramov's avatar Dan Abramov Committed by GitHub
Browse files

Move more logic from react-scripts to react-dev-utils (#2209)

* Show warnings for builds

* Move WebpackDevServer helpers into react-dev-utils
parent 0681e245
Showing with 488 additions and 270 deletions
+488 -270
......@@ -265,7 +265,25 @@ const publicPath = config.output.publicPath;
printHostingInstructions(appPackage, publicUrl, publicPath, 'build', true);
```
#### `webpackHotDevClient.js`
#### `WebpackDevServerUtils`
##### `choosePort(host: string, defaultPort: number): Promise<number | null>`
Returns a Promise resolving to either `defaultPort` or next available port if the user confirms it is okay to do. If the port is taken and the user has refused to use another port, or if the terminal is not interactive and can’t present user with the choice, resolves to `null`.
##### `createCompiler(webpack: Function, config: Object, appName: string, urls: Object, useYarn: boolean): WebpackCompiler`
Creates a Webpack compiler instance for WebpackDevServer with built-in helpful messages. Takes the `require('webpack')` entry point as the first argument. To provide the `urls` argument, use `prepareUrls()` described below.
##### `prepareProxy(proxySetting: string): Object`
Creates a WebpackDevServer `proxy` configuration object from the `proxy` setting in `package.json`.
##### `prepareUrls(protocol: string, host: string, port: number): Object`
Returns an object with local and remote URLs for the development server. Pass this object to `createCompiler()` described above.
#### `webpackHotDevClient`
This is an alternative client for [WebpackDevServer](https://github.com/webpack/webpack-dev-server) that shows a syntax error overlay.
......
// @remove-on-eject-begin
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
......@@ -7,12 +6,171 @@
* 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.
*/
// @remove-on-eject-end
'use strict';
const address = require('address');
const chalk = require('chalk');
const url = require('url');
const chalk = require('chalk');
const detect = require('@timer/detect-port');
const inquirer = require('inquirer');
const clearConsole = require('./clearConsole');
const formatWebpackMessages = require('./formatWebpackMessages');
const getProcessForPort = require('./getProcessForPort');
const isInteractive = process.stdout.isTTY;
let handleCompile;
// You can safely remove this after ejecting.
// We only use this block for testing of Create React App itself:
const isSmokeTest = process.argv.some(arg => arg.indexOf('--smoke-test') > -1);
if (isSmokeTest) {
handleCompile = (err, stats) => {
if (err || stats.hasErrors() || stats.hasWarnings()) {
process.exit(1);
} else {
process.exit(0);
}
};
}
function prepareUrls(protocol, host, port) {
const formatUrl = hostname => url.format({
protocol,
hostname,
port,
pathname: '/',
});
const prettyPrintUrl = hostname => url.format({
protocol,
hostname,
port: chalk.bold(port),
pathname: '/',
});
const isUnspecifiedHost = host === '0.0.0.0' || host === '::';
let prettyHost, lanUrlForConfig, lanUrlForTerminal;
if (isUnspecifiedHost) {
prettyHost = 'localhost';
try {
lanUrlForConfig = address.ip();
if (lanUrlForConfig) {
lanUrlForTerminal = prettyPrintUrl(lanUrlForConfig);
}
} catch (_e) {
// ignored
}
} else {
prettyHost = host;
}
const localUrlForTerminal = prettyPrintUrl(prettyHost);
const localUrlForBrowser = formatUrl(prettyHost);
return {
lanUrlForConfig,
lanUrlForTerminal,
localUrlForTerminal,
localUrlForBrowser,
};
}
function printInstructions(appName, urls, useYarn) {
console.log();
console.log(`You can now view ${chalk.bold(appName)} in the browser.`);
console.log();
if (urls.lanUrlForTerminal) {
console.log(
` ${chalk.bold('Local:')} ${urls.localUrlForTerminal}`
);
console.log(
` ${chalk.bold('On Your Network:')} ${urls.lanUrlForTerminal}`
);
} else {
console.log(` ${urls.localUrlForTerminal}`);
}
console.log();
console.log('Note that the development build is not optimized.');
console.log(
`To create a production build, use ` +
`${chalk.cyan(`${useYarn ? 'yarn' : 'npm'} run build`)}.`
);
console.log();
}
function createCompiler(webpack, config, appName, urls, useYarn) {
// "Compiler" is a low-level interface to Webpack.
// It lets us listen to some events and provide our own custom messages.
let compiler;
try {
compiler = webpack(config, handleCompile);
} catch (err) {
console.log(chalk.red('Failed to compile.'));
console.log();
console.log(err.message || err);
console.log();
process.exit(1);
}
// "invalid" event fires when you have changed a file, and Webpack is
// recompiling a bundle. WebpackDevServer takes care to pause serving the
// bundle, so if you refresh, it'll wait instead of serving the old one.
// "invalid" is short for "bundle invalidated", it doesn't imply any errors.
compiler.plugin('invalid', () => {
if (isInteractive) {
clearConsole();
}
console.log('Compiling...');
});
let isFirstCompile = true;
// "done" event fires when Webpack has finished recompiling the bundle.
// Whether or not you have warnings or errors, you will get this event.
compiler.plugin('done', stats => {
if (isInteractive) {
clearConsole();
}
// We have switched off the default Webpack output in WebpackDevServer
// options so we are going to "massage" the warnings and errors and present
// them in a readable focused way.
const messages = formatWebpackMessages(stats.toJson({}, true));
const isSuccessful = !messages.errors.length && !messages.warnings.length;
if (isSuccessful) {
console.log(chalk.green('Compiled successfully!'));
}
if (isSuccessful && (isInteractive || isFirstCompile)) {
printInstructions(appName, urls, useYarn);
}
isFirstCompile = false;
// If errors exist, only show errors.
if (messages.errors.length) {
console.log(chalk.red('Failed to compile.\n'));
console.log(messages.errors.join('\n\n'));
return;
}
// Show warnings if no errors were found.
if (messages.warnings.length) {
console.log(chalk.yellow('Compiled with warnings.\n'));
console.log(messages.warnings.join('\n\n'));
// Teach some ESLint tricks.
console.log(
'\nSearch for the ' +
chalk.underline(chalk.yellow('rule keywords')) +
' to learn more about each warning.'
);
console.log(
'To ignore, add ' +
chalk.cyan('// eslint-disable-next-line') +
' to the line before.\n'
);
}
});
return compiler;
}
function resolveLoopback(proxy) {
const o = url.parse(proxy);
......@@ -69,7 +227,7 @@ function onProxyError(proxy) {
};
}
module.exports = function prepareProxy(proxy) {
function prepareProxy(proxy) {
// `proxy` lets you specify alternate servers for specific requests.
// It can either be a string or an object conforming to the Webpack dev server proxy configuration
// https://webpack.github.io/docs/webpack-dev-server.html
......@@ -184,4 +342,54 @@ module.exports = function prepareProxy(proxy) {
onError: onProxyError(target),
});
});
}
function choosePort(host, defaultPort) {
return detect(defaultPort, host).then(
port => new Promise(resolve => {
if (port === defaultPort) {
return resolve(port);
}
if (isInteractive) {
clearConsole();
const existingProcess = getProcessForPort(defaultPort);
const question = {
type: 'confirm',
name: 'shouldChangePort',
message: chalk.yellow(
`Something is already running on port ${defaultPort}.` +
`${existingProcess ? ` Probably:\n ${existingProcess}` : ''}`
) + '\n\nWould you like to run the app on another port instead?',
default: true,
};
inquirer.prompt(question).then(answer => {
if (answer.shouldChangePort) {
resolve(port);
} else {
resolve(null);
}
});
} else {
console.log(
chalk.red(`Something is already running on port ${defaultPort}.`)
);
resolve(null);
}
}),
err => {
throw new Error(
chalk.red(`Could not find an open port at ${chalk.bold(host)}.`) +
'\n' +
('Network error message: ' + err.message || err) +
'\n'
);
}
);
}
module.exports = {
choosePort,
createCompiler,
prepareProxy,
prepareUrls,
};
......@@ -24,12 +24,13 @@
"ModuleScopePlugin.js",
"openBrowser.js",
"openChrome.applescript",
"prepareProxy.js",
"printHostingInstructions.js",
"WatchMissingNodeModulesPlugin.js",
"WebpackDevServerUtils.js",
"webpackHotDevClient.js"
],
"dependencies": {
"@timer/detect-port": "1.1.3",
"address": "1.0.1",
"anser": "1.3.0",
"babel-code-frame": "6.22.0",
......@@ -39,6 +40,7 @@
"filesize": "3.3.0",
"gzip-size": "3.0.0",
"html-entities": "1.2.1",
"inquirer": "3.0.6",
"opn": "5.0.0",
"recursive-readdir": "2.2.1",
"shell-quote": "1.6.1",
......
......@@ -21,8 +21,6 @@
"react-scripts": "./bin/react-scripts.js"
},
"dependencies": {
"@timer/detect-port": "1.1.3",
"address": "1.0.1",
"autoprefixer": "7.1.0",
"babel-core": "6.24.1",
"babel-eslint": "7.2.3",
......
......@@ -56,12 +56,25 @@ measureFileSizesBeforeBuild(paths.appBuild)
return build(previousFileSizes);
})
.then(
({ stats, previousFileSizes }) => {
console.log(chalk.green('Compiled successfully.'));
console.log();
({ stats, previousFileSizes, warnings }) => {
if (warnings.length) {
console.log(chalk.yellow('Compiled with warnings.\n'));
console.log(warnings.join('\n\n'));
console.log(
'\nSearch for the ' +
chalk.underline(chalk.yellow('rule keywords')) +
' to learn more about each warning.'
);
console.log(
'To ignore, add ' +
chalk.cyan('// eslint-disable-next-line') +
' to the line before.\n'
);
} else {
console.log(chalk.green('Compiled successfully.\n'));
}
console.log('File sizes after gzip:');
console.log();
console.log('File sizes after gzip:\n');
printFileSizesAfterBuild(stats, previousFileSizes);
console.log();
......@@ -78,10 +91,8 @@ measureFileSizesBeforeBuild(paths.appBuild)
);
},
err => {
console.log(chalk.red('Failed to compile.'));
console.log();
console.log(err.message || err);
console.log();
console.log(chalk.red('Failed to compile.\n'));
console.log((err.message || err) + '\n');
process.exit(1);
}
);
......@@ -101,17 +112,19 @@ function build(previousFileSizes) {
return reject(new Error(messages.errors.join('\n\n')));
}
if (process.env.CI && messages.warnings.length) {
console.log();
console.log(
chalk.yellow(
'Treating warnings as errors because process.env.CI = true.\n' +
'Most CI servers set it automatically.'
'\nTreating warnings as errors because process.env.CI = true.\n' +
'Most CI servers set it automatically.\n'
)
);
console.log();
return reject(new Error(messages.warnings.join('\n\n')));
}
return resolve({ stats, previousFileSizes });
return resolve({
stats,
previousFileSizes,
warnings: messages.warnings,
});
});
});
}
......
......@@ -57,7 +57,7 @@ inquirer
}
}
const folders = ['config', 'config/jest', 'scripts', 'scripts/utils'];
const folders = ['config', 'config/jest', 'scripts'];
// Make shallow array of files paths
const files = folders.reduce(
......
......@@ -22,25 +22,24 @@ process.env.NODE_ENV = 'development';
// Ensure environment variables are read.
require('../config/env');
const address = require('address');
const fs = require('fs');
const chalk = require('chalk');
const detect = require('@timer/detect-port');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const getProcessForPort = require('react-dev-utils/getProcessForPort');
const {
choosePort,
createCompiler,
prepareProxy,
prepareUrls,
} = require('react-dev-utils/WebpackDevServerUtils');
const openBrowser = require('react-dev-utils/openBrowser');
const inquirer = require('inquirer');
const paths = require('../config/paths');
const config = require('../config/webpack.config.dev');
const devServerConfig = require('../config/webpackDevServer.config');
const createWebpackCompiler = require('./utils/createWebpackCompiler');
const prepareProxy = require('react-dev-utils/prepareProxy');
const url = require('url');
const createDevServerConfig = require('../config/webpackDevServer.config');
const useYarn = fs.existsSync(paths.yarnLockFile);
const cli = useYarn ? 'yarn' : 'npm';
const isInteractive = process.stdout.isTTY;
// Warn and crash if required files are missing
......@@ -52,130 +51,43 @@ if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';
function run(port) {
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const formatUrl = hostname => url.format({
protocol,
hostname,
port,
pathname: '/',
});
const prettyPrintUrl = hostname => url.format({
protocol,
hostname,
port: chalk.bold(port),
pathname: '/',
});
const isUnspecifiedAddress = HOST === '0.0.0.0' || HOST === '::';
let prettyHost, lanAddress, prettyLanUrl;
if (isUnspecifiedAddress) {
prettyHost = 'localhost';
try {
lanAddress = address.ip();
if (lanAddress) {
prettyLanUrl = prettyPrintUrl(lanAddress);
}
} catch (_e) {
// ignored
}
} else {
prettyHost = HOST;
}
const prettyLocalUrl = prettyPrintUrl(prettyHost);
// Create a webpack compiler that is configured with custom messages.
const compiler = createWebpackCompiler(
config,
function onReady(showInstructions) {
if (!showInstructions) {
return;
}
console.log();
console.log(
`You can now view ${chalk.bold(require(paths.appPackageJson).name)} in the browser.`
);
console.log();
if (prettyLanUrl) {
console.log(` ${chalk.bold('Local:')} ${prettyLocalUrl}`);
console.log(` ${chalk.bold('On Your Network:')} ${prettyLanUrl}`);
} else {
console.log(` ${prettyLocalUrl}`);
}
console.log();
console.log('Note that the development build is not optimized.');
console.log(
`To create a production build, use ${chalk.cyan(`${cli} run build`)}.`
);
console.log();
}
);
// Load proxy config
const proxy = require(paths.appPackageJson).proxy;
// Serve webpack assets generated by the compiler over a web sever.
const devServer = new WebpackDevServer(
compiler,
devServerConfig(prepareProxy(proxy), lanAddress)
);
// Launch WebpackDevServer.
devServer.listen(port, HOST, err => {
if (err) {
return console.log(err);
}
if (isInteractive) {
clearConsole();
}
console.log(chalk.cyan('Starting the development server...'));
console.log();
openBrowser(formatUrl(prettyHost));
});
}
// We attempt to use the default port but if it is busy, we offer the user to
// run on a different port. `detect()` Promise resolves to the next free port.
detect(DEFAULT_PORT, HOST).then(
port => {
if (port === DEFAULT_PORT) {
run(port);
choosePort(HOST, DEFAULT_PORT)
.then(port => {
if (port == null) {
// We have not found a port.
return;
}
if (isInteractive) {
clearConsole();
const existingProcess = getProcessForPort(DEFAULT_PORT);
const question = {
type: 'confirm',
name: 'shouldChangePort',
message: chalk.yellow(
`Something is already running on port ${DEFAULT_PORT}.` +
`${existingProcess ? ` Probably:\n ${existingProcess}` : ''}`
) + '\n\nWould you like to run the app on another port instead?',
default: true,
};
inquirer.prompt(question).then(answer => {
if (answer.shouldChangePort) {
run(port);
}
});
} else {
console.log(
chalk.red(`Something is already running on port ${DEFAULT_PORT}.`)
);
}
},
err => {
console.log(
chalk.red(`Could not find an open port at ${chalk.bold(HOST)}.`)
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;
const urls = prepareUrls(protocol, HOST, port);
// Create a webpack compiler that is configured with custom messages.
const compiler = createCompiler(webpack, config, appName, urls, useYarn);
// Load proxy config
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(proxySetting);
// Serve webpack assets generated by the compiler over a web sever.
const serverConfig = createDevServerConfig(
proxyConfig,
urls.lanUrlForConfig
);
console.log('Network error message: ' + err.message || err);
console.log();
}
);
const devServer = new WebpackDevServer(compiler, serverConfig);
// Launch WebpackDevServer.
devServer.listen(port, HOST, err => {
if (err) {
return console.log(err);
}
if (isInteractive) {
clearConsole();
}
console.log(chalk.cyan('Starting the development server...\n'));
openBrowser(urls.localUrlForBrowser);
});
})
.catch(err => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});
// @remove-on-eject-begin
/**
* 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.
*/
// @remove-on-eject-end
'use strict';
const chalk = require('chalk');
const webpack = require('webpack');
const clearConsole = require('react-dev-utils/clearConsole');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const isInteractive = process.stdout.isTTY;
let handleCompile;
// You can safely remove this after ejecting.
// We only use this block for testing of Create React App itself:
const isSmokeTest = process.argv.some(arg => arg.indexOf('--smoke-test') > -1);
if (isSmokeTest) {
handleCompile = (err, stats) => {
if (err || stats.hasErrors() || stats.hasWarnings()) {
process.exit(1);
} else {
process.exit(0);
}
};
}
module.exports = function createWebpackCompiler(config, onReadyCallback) {
// "Compiler" is a low-level interface to Webpack.
// It lets us listen to some events and provide our own custom messages.
let compiler;
try {
compiler = webpack(config, handleCompile);
} catch (err) {
console.log(chalk.red('Failed to compile.'));
console.log();
console.log(err.message || err);
console.log();
process.exit(1);
}
// "invalid" event fires when you have changed a file, and Webpack is
// recompiling a bundle. WebpackDevServer takes care to pause serving the
// bundle, so if you refresh, it'll wait instead of serving the old one.
// "invalid" is short for "bundle invalidated", it doesn't imply any errors.
compiler.plugin('invalid', () => {
if (isInteractive) {
clearConsole();
}
console.log('Compiling...');
});
let isFirstCompile = true;
// "done" event fires when Webpack has finished recompiling the bundle.
// Whether or not you have warnings or errors, you will get this event.
compiler.plugin('done', stats => {
if (isInteractive) {
clearConsole();
}
// We have switched off the default Webpack output in WebpackDevServer
// options so we are going to "massage" the warnings and errors and present
// them in a readable focused way.
const messages = formatWebpackMessages(stats.toJson({}, true));
const isSuccessful = !messages.errors.length && !messages.warnings.length;
const showInstructions = isSuccessful && (isInteractive || isFirstCompile);
if (isSuccessful) {
console.log(chalk.green('Compiled successfully!'));
}
if (typeof onReadyCallback === 'function') {
onReadyCallback(showInstructions);
}
isFirstCompile = false;
// If errors exist, only show errors.
if (messages.errors.length) {
console.log(chalk.red('Failed to compile.'));
console.log();
messages.errors.forEach(message => {
console.log(message);
console.log();
});
return;
}
// Show warnings if no errors were found.
if (messages.warnings.length) {
console.log(chalk.yellow('Compiled with warnings.'));
console.log();
messages.warnings.forEach(message => {
console.log(message);
console.log();
});
// Teach some ESLint tricks.
console.log(
'Search for the ' +
chalk.underline(chalk.yellow('rule keywords')) +
' to learn more about each warning.'
);
console.log(
'To ignore, add ' +
chalk.cyan('// eslint-disable-next-line') +
' to the line before.'
);
console.log();
}
});
return compiler;
};
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment