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

Document configuration and build process (#362)

parent c13f7f40
No related merge requests found
Showing with 203 additions and 21 deletions
+203 -21
...@@ -8,19 +8,31 @@ ...@@ -8,19 +8,31 @@
*/ */
module.exports = { module.exports = {
// Don't try to find .babelrc because we want to force this configuration.
babelrc: false, babelrc: false,
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in OS temporary directory for faster rebuilds.
cacheDirectory: true, cacheDirectory: true,
presets: [ presets: [
// let, const, destructuring, classes, modules
require.resolve('babel-preset-es2015'), require.resolve('babel-preset-es2015'),
// exponentiation
require.resolve('babel-preset-es2016'), require.resolve('babel-preset-es2016'),
// JSX, Flow
require.resolve('babel-preset-react') require.resolve('babel-preset-react')
], ],
plugins: [ plugins: [
// function x(a, b, c,) { }
require.resolve('babel-plugin-syntax-trailing-function-commas'), require.resolve('babel-plugin-syntax-trailing-function-commas'),
// await fetch()
require.resolve('babel-plugin-syntax-async-functions'), require.resolve('babel-plugin-syntax-async-functions'),
// class { handleClick = () => { } }
require.resolve('babel-plugin-transform-class-properties'), require.resolve('babel-plugin-transform-class-properties'),
// { ...todo, completed: true }
require.resolve('babel-plugin-transform-object-rest-spread'), require.resolve('babel-plugin-transform-object-rest-spread'),
// function* () { yield 42; yield 43; }
require.resolve('babel-plugin-transform-regenerator'), require.resolve('babel-plugin-transform-regenerator'),
// Polyfills the runtime needed for async/await and generators
[require.resolve('babel-plugin-transform-runtime'), { [require.resolve('babel-plugin-transform-runtime'), {
helpers: false, helpers: false,
polyfill: false, polyfill: false,
......
...@@ -8,23 +8,34 @@ ...@@ -8,23 +8,34 @@
*/ */
module.exports = { module.exports = {
// Don't try to find .babelrc because we want to force this configuration.
babelrc: false, babelrc: false,
presets: [ presets: [
// let, const, destructuring, classes, modules
require.resolve('babel-preset-es2015'), require.resolve('babel-preset-es2015'),
// exponentiation
require.resolve('babel-preset-es2016'), require.resolve('babel-preset-es2016'),
// JSX, Flow
require.resolve('babel-preset-react') require.resolve('babel-preset-react')
], ],
plugins: [ plugins: [
// function x(a, b, c,) { }
require.resolve('babel-plugin-syntax-trailing-function-commas'), require.resolve('babel-plugin-syntax-trailing-function-commas'),
// await fetch()
require.resolve('babel-plugin-syntax-async-functions'), require.resolve('babel-plugin-syntax-async-functions'),
// class { handleClick = () => { } }
require.resolve('babel-plugin-transform-class-properties'), require.resolve('babel-plugin-transform-class-properties'),
// { ...todo, completed: true }
require.resolve('babel-plugin-transform-object-rest-spread'), require.resolve('babel-plugin-transform-object-rest-spread'),
require.resolve('babel-plugin-transform-react-constant-elements'), // function* () { yield 42; yield 43; }
require.resolve('babel-plugin-transform-regenerator'), require.resolve('babel-plugin-transform-regenerator'),
// Polyfills the runtime needed for async/await and generators
[require.resolve('babel-plugin-transform-runtime'), { [require.resolve('babel-plugin-transform-runtime'), {
helpers: false, helpers: false,
polyfill: false, polyfill: false,
regenerator: true regenerator: true
}] }],
// Optimization: hoist JSX that never changes out of render()
require.resolve('babel-plugin-transform-react-constant-elements')
] ]
}; };
...@@ -7,6 +7,9 @@ ...@@ -7,6 +7,9 @@
* of patent rights can be found in the PATENTS file in the same directory. * of patent rights can be found in the PATENTS file in the same directory.
*/ */
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in Webpack configuration.
var REACT_APP = /^REACT_APP_/i; var REACT_APP = /^REACT_APP_/i;
var NODE_ENV = JSON.stringify(process.env.NODE_ENV || 'development'); var NODE_ENV = JSON.stringify(process.env.NODE_ENV || 'development');
......
...@@ -6,4 +6,5 @@ if (typeof Promise === 'undefined') { ...@@ -6,4 +6,5 @@ if (typeof Promise === 'undefined') {
window.Promise = require('promise/lib/es6-extensions.js'); window.Promise = require('promise/lib/es6-extensions.js');
} }
// fetch() polyfill for making API calls.
require('whatwg-fetch'); require('whatwg-fetch');
...@@ -16,22 +16,56 @@ var WatchMissingNodeModulesPlugin = require('../scripts/utils/WatchMissingNodeMo ...@@ -16,22 +16,56 @@ var WatchMissingNodeModulesPlugin = require('../scripts/utils/WatchMissingNodeMo
var paths = require('./paths'); var paths = require('./paths');
var env = require('./env'); var env = require('./env');
// This is the development configuration.
// It is focused on developer experience and fast rebuilds.
// The production configuration is different and lives in a separate file.
module.exports = { module.exports = {
// This makes the bundle appear split into separate modules in the devtools.
// We don't use source maps here because they can be confusing:
// https://github.com/facebookincubator/create-react-app/issues/343#issuecomment-237241875
// You may want 'cheap-module-source-map' instead if you prefer source maps.
devtool: 'eval', devtool: 'eval',
// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.
// The first two entry points enable "hot" CSS and auto-refreshes for JS.
entry: [ entry: [
// Include WebpackDevServer client. It connects to WebpackDevServer via
// sockets and waits for recompile notifications. When WebpackDevServer
// recompiles, it sends a message to the client by socket. If only CSS
// was changed, the app reload just the CSS. Otherwise, it will refresh.
// The "?/" bit at the end tells the client to look for the socket at
// the root path, i.e. /sockjs-node/. Otherwise visiting a client-side
// route like /todos/42 would make it wrongly request /todos/42/sockjs-node.
// The socket server is a part of WebpackDevServer which we are using.
// The /sockjs-node/ path I'm referring to is hardcoded in WebpackDevServer.
require.resolve('webpack-dev-server/client') + '?/', require.resolve('webpack-dev-server/client') + '?/',
// Include Webpack hot module replacement runtime. Webpack is pretty
// low-level so we need to put all the pieces together. The runtime listens
// to the events received by the client above, and applies updates (such as
// new CSS) to the running application.
require.resolve('webpack/hot/dev-server'), require.resolve('webpack/hot/dev-server'),
// We ship a few polyfills by default.
require.resolve('./polyfills'), require.resolve('./polyfills'),
// Finally, this is your app's code:
path.join(paths.appSrc, 'index') path.join(paths.appSrc, 'index')
// We include the app code last so that if there is a runtime error during
// initialization, it doesn't blow up the WebpackDevServer client, and
// changing JS code would still trigger a refresh.
], ],
output: { output: {
// Next line is not used in dev but WebpackDevServer crashes without it: // Next line is not used in dev but WebpackDevServer crashes without it:
path: paths.appBuild, path: paths.appBuild,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: true, pathinfo: true,
// This does not produce a real file. It's just the virtual path that is
// served by WebpackDevServer in development. This is the JS bundle
// containing code from all our entry points, and the Webpack runtime.
filename: 'static/js/bundle.js', filename: 'static/js/bundle.js',
// In development, we always serve from the root. This makes config easier.
publicPath: '/' publicPath: '/'
}, },
resolve: { resolve: {
// These are the reasonable defaults supported by the Node ecosystem.
extensions: ['.js', '.json', ''], extensions: ['.js', '.json', ''],
alias: { alias: {
// This `alias` section can be safely removed after ejection. // This `alias` section can be safely removed after ejection.
...@@ -45,11 +79,16 @@ module.exports = { ...@@ -45,11 +79,16 @@ module.exports = {
'babel-runtime/regenerator': require.resolve('babel-runtime/regenerator') 'babel-runtime/regenerator': require.resolve('babel-runtime/regenerator')
} }
}, },
// Resolve loaders (webpack plugins for CSS, images, transpilation) from the
// directory of `react-scripts` itself rather than the project directory.
// You can remove this after ejecting.
resolveLoader: { resolveLoader: {
root: paths.ownNodeModules, root: paths.ownNodeModules,
moduleTemplates: ['*-loader'] moduleTemplates: ['*-loader']
}, },
module: { module: {
// First, run the linter.
// It's important to do this before Babel processes the JS.
preLoaders: [ preLoaders: [
{ {
test: /\.js$/, test: /\.js$/,
...@@ -58,22 +97,33 @@ module.exports = { ...@@ -58,22 +97,33 @@ module.exports = {
} }
], ],
loaders: [ loaders: [
// Process JS with Babel.
{ {
test: /\.js$/, test: /\.js$/,
include: paths.appSrc, include: paths.appSrc,
loader: 'babel', loader: 'babel',
query: require('./babel.dev') query: require('./babel.dev')
}, },
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use a plugin to extract that CSS to a file, but
// in development "style" loader enables hot editing of CSS.
{ {
test: /\.css$/, test: /\.css$/,
include: [paths.appSrc, paths.appNodeModules], include: [paths.appSrc, paths.appNodeModules],
loader: 'style!css!postcss' loader: 'style!css!postcss'
}, },
// JSON is not enabled by default in Webpack but both Node and Browserify
// allow it implicitly so we also enable it.
{ {
test: /\.json$/, test: /\.json$/,
include: [paths.appSrc, paths.appNodeModules], include: [paths.appSrc, paths.appNodeModules],
loader: 'json' loader: 'json'
}, },
// "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder.
{ {
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/, test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/,
include: [paths.appSrc, paths.appNodeModules], include: [paths.appSrc, paths.appNodeModules],
...@@ -82,6 +132,8 @@ module.exports = { ...@@ -82,6 +132,8 @@ module.exports = {
name: 'static/media/[name].[ext]' name: 'static/media/[name].[ext]'
} }
}, },
// "url" loader works just like "file" loader but it also embeds
// assets smaller than specified size as data URLs to avoid requests.
{ {
test: /\.(mp4|webm)(\?.*)?$/, test: /\.(mp4|webm)(\?.*)?$/,
include: [paths.appSrc, paths.appNodeModules], include: [paths.appSrc, paths.appNodeModules],
...@@ -93,10 +145,12 @@ module.exports = { ...@@ -93,10 +145,12 @@ module.exports = {
} }
] ]
}, },
// Point ESLint to our predefined config.
eslint: { eslint: {
configFile: path.join(__dirname, 'eslint.js'), configFile: path.join(__dirname, 'eslint.js'),
useEslintrc: false useEslintrc: false
}, },
// We use PostCSS for autoprefixing only.
postcss: function() { postcss: function() {
return [ return [
autoprefixer({ autoprefixer({
...@@ -104,21 +158,31 @@ module.exports = { ...@@ -104,21 +158,31 @@ module.exports = {
'>1%', '>1%',
'last 4 versions', 'last 4 versions',
'Firefox ESR', 'Firefox ESR',
'not ie < 9', 'not ie < 9', // React doesn't support IE8 anyway
] ]
}), }),
]; ];
}, },
plugins: [ plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
inject: true, inject: true,
template: paths.appHtml, template: paths.appHtml,
favicon: paths.appFavicon, favicon: paths.appFavicon,
}), }),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'development') { ... }. See `env.js`.
new webpack.DefinePlugin(env), new webpack.DefinePlugin(env),
// Note: only CSS is currently hot reloaded // This is necessary to emit hot updates (currently CSS only):
new webpack.HotModuleReplacementPlugin(), new webpack.HotModuleReplacementPlugin(),
// Watcher doesn't work well if you mistype casing in a path so we use
// a plugin that prints an error when you attempt to do this.
// See https://github.com/facebookincubator/create-react-app/issues/240
new CaseSensitivePathsPlugin(), new CaseSensitivePathsPlugin(),
// If you require a missing module and then `npm install` it, you still have
// to restart the development server for Webpack to discover it. This plugin
// makes the discovery automatic so you don't have to restart.
// See https://github.com/facebookincubator/create-react-app/issues/186
new WatchMissingNodeModulesPlugin(paths.appNodeModules) new WatchMissingNodeModulesPlugin(paths.appNodeModules)
] ]
}; };
...@@ -16,27 +16,51 @@ var url = require('url'); ...@@ -16,27 +16,51 @@ var url = require('url');
var paths = require('./paths'); var paths = require('./paths');
var env = require('./env'); var env = require('./env');
// Assert this just to be safe.
// Development builds of React are slow and not intended for production.
if (env['process.env.NODE_ENV'] !== '"production"') {
throw new Error('Production builds must have NODE_ENV=production.');
}
// We use "homepage" field to infer "public path" at which the app is served.
// Webpack needs to know it to put the right <script> hrefs into HTML even in
// single-page apps that may serve index.html for nested URLs like /todos/42.
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
var homepagePath = require(paths.appPackageJson).homepage; var homepagePath = require(paths.appPackageJson).homepage;
var publicPath = homepagePath ? url.parse(homepagePath).pathname : '/'; var publicPath = homepagePath ? url.parse(homepagePath).pathname : '/';
if (!publicPath.endsWith('/')) { if (!publicPath.endsWith('/')) {
// Prevents incorrect paths in file-loader // If we don't do this, file assets will get incorrect paths.
publicPath += '/'; publicPath += '/';
} }
// This is the production configuration.
// It compiles slowly and is focused on producing a fast and minimal bundle.
// The development configuration is different and lives in a separate file.
module.exports = { module.exports = {
// Don't attempt to continue if there are any errors.
bail: true, bail: true,
// We generate sourcemaps in production. This is slow but gives good results.
// You can exclude the *.map files from the build during deployment.
devtool: 'source-map', devtool: 'source-map',
// In production, we only want to load the polyfills and the app code.
entry: [ entry: [
require.resolve('./polyfills'), require.resolve('./polyfills'),
path.join(paths.appSrc, 'index') path.join(paths.appSrc, 'index')
], ],
output: { output: {
// The build folder.
path: paths.appBuild, path: paths.appBuild,
// Generated JS file names (with nested folders).
// There will be one main bundle, and one file per asynchronous chunk.
// We don't currently advertise code splitting but Webpack supports it.
filename: 'static/js/[name].[chunkhash:8].js', filename: 'static/js/[name].[chunkhash:8].js',
chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js', chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath: publicPath publicPath: publicPath
}, },
resolve: { resolve: {
// These are the reasonable defaults supported by the Node ecosystem.
extensions: ['.js', '.json', ''], extensions: ['.js', '.json', ''],
alias: { alias: {
// This `alias` section can be safely removed after ejection. // This `alias` section can be safely removed after ejection.
...@@ -50,11 +74,16 @@ module.exports = { ...@@ -50,11 +74,16 @@ module.exports = {
'babel-runtime/regenerator': require.resolve('babel-runtime/regenerator') 'babel-runtime/regenerator': require.resolve('babel-runtime/regenerator')
} }
}, },
// Resolve loaders (webpack plugins for CSS, images, transpilation) from the
// directory of `react-scripts` itself rather than the project directory.
// You can remove this after ejecting.
resolveLoader: { resolveLoader: {
root: paths.ownNodeModules, root: paths.ownNodeModules,
moduleTemplates: ['*-loader'] moduleTemplates: ['*-loader']
}, },
module: { module: {
// First, run the linter.
// It's important to do this before Babel processes the JS.
preLoaders: [ preLoaders: [
{ {
test: /\.js$/, test: /\.js$/,
...@@ -63,26 +92,44 @@ module.exports = { ...@@ -63,26 +92,44 @@ module.exports = {
} }
], ],
loaders: [ loaders: [
// Process JS with Babel.
{ {
test: /\.js$/, test: /\.js$/,
include: paths.appSrc, include: paths.appSrc,
loader: 'babel', loader: 'babel',
query: require('./babel.prod') query: require('./babel.prod')
}, },
// The notation here is somewhat confusing.
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader normally turns CSS into JS modules injecting <style>,
// but unlike in development configuration, we do something different.
// `ExtractTextPlugin` first applies the "postcss" and "css" loaders
// (second argument), then grabs the result CSS and puts it into a
// separate file in our build process. This way we actually ship
// a single CSS file in production instead of JS code injecting <style>
// tags. If you use code splitting, however, any async bundles will still
// use the "style" loader inside the async code so CSS from them won't be
// in the main CSS file.
{ {
test: /\.css$/, test: /\.css$/,
include: [paths.appSrc, paths.appNodeModules], include: [paths.appSrc, paths.appNodeModules],
// Disable autoprefixer in css-loader itself: // "?-autoprefixer" disables autoprefixer in css-loader itself:
// https://github.com/webpack/css-loader/issues/281 // https://github.com/webpack/css-loader/issues/281
// We already have it thanks to postcss. // We already have it thanks to postcss.
loader: ExtractTextPlugin.extract('style', 'css?-autoprefixer!postcss') loader: ExtractTextPlugin.extract('style', 'css?-autoprefixer!postcss')
// Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
}, },
{ {
// JSON is not enabled by default in Webpack but both Node and Browserify
// allow it implicitly so we also enable it.
test: /\.json$/, test: /\.json$/,
include: [paths.appSrc, paths.appNodeModules], include: [paths.appSrc, paths.appNodeModules],
loader: 'json' loader: 'json'
}, },
{ {
// "file" loader makes sure those assets end up in the `build` folder.
// When you `import` an asset, you get its filename.
test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/, test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?$/,
include: [paths.appSrc, paths.appNodeModules], include: [paths.appSrc, paths.appNodeModules],
loader: 'file', loader: 'file',
...@@ -90,6 +137,8 @@ module.exports = { ...@@ -90,6 +137,8 @@ module.exports = {
name: 'static/media/[name].[hash:8].[ext]' name: 'static/media/[name].[hash:8].[ext]'
} }
}, },
// "url" loader works just like "file" loader but it also embeds
// assets smaller than specified size as data URLs to avoid requests.
{ {
test: /\.(mp4|webm)(\?.*)?$/, test: /\.(mp4|webm)(\?.*)?$/,
include: [paths.appSrc, paths.appNodeModules], include: [paths.appSrc, paths.appNodeModules],
...@@ -101,12 +150,14 @@ module.exports = { ...@@ -101,12 +150,14 @@ module.exports = {
} }
] ]
}, },
// Point ESLint to our predefined config.
eslint: { eslint: {
// TODO: consider separate config for production, // TODO: consider separate config for production,
// e.g. to enable no-console and no-debugger only in prod. // e.g. to enable no-console and no-debugger only in production.
configFile: path.join(__dirname, 'eslint.js'), configFile: path.join(__dirname, 'eslint.js'),
useEslintrc: false useEslintrc: false
}, },
// We use PostCSS for autoprefixing only.
postcss: function() { postcss: function() {
return [ return [
autoprefixer({ autoprefixer({
...@@ -114,12 +165,13 @@ module.exports = { ...@@ -114,12 +165,13 @@ module.exports = {
'>1%', '>1%',
'last 4 versions', 'last 4 versions',
'Firefox ESR', 'Firefox ESR',
'not ie < 9', 'not ie < 9', // React doesn't support IE8 anyway
] ]
}), }),
]; ];
}, },
plugins: [ plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
inject: true, inject: true,
template: paths.appHtml, template: paths.appHtml,
...@@ -137,12 +189,19 @@ module.exports = { ...@@ -137,12 +189,19 @@ module.exports = {
minifyURLs: true minifyURLs: true
} }
}), }),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `env.js`.
// It is absolutely essential that NODE_ENV was set to production here.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(env), new webpack.DefinePlugin(env),
// This helps ensure the builds are consistent if source hasn't changed:
new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.OccurrenceOrderPlugin(),
// Try to dedupe duplicated modules, if any:
new webpack.optimize.DedupePlugin(), new webpack.optimize.DedupePlugin(),
// Minify the code.
new webpack.optimize.UglifyJsPlugin({ new webpack.optimize.UglifyJsPlugin({
compress: { compress: {
screw_ie8: true, screw_ie8: true, // React doesn't support IE8
warnings: false warnings: false
}, },
mangle: { mangle: {
...@@ -153,6 +212,7 @@ module.exports = { ...@@ -153,6 +212,7 @@ module.exports = {
screw_ie8: true screw_ie8: true
} }
}), }),
// Note: this won't work without ExtractTextPlugin.extract(..) in `loaders`.
new ExtractTextPlugin('static/css/[name].[contenthash:8].css') new ExtractTextPlugin('static/css/[name].[contenthash:8].css')
] ]
}; };
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
* of patent rights can be found in the PATENTS file in the same directory. * of patent rights can be found in the PATENTS file in the same directory.
*/ */
// Do this as the first thing so that any code reading it knows the right env.
process.env.NODE_ENV = 'production'; process.env.NODE_ENV = 'production';
var chalk = require('chalk'); var chalk = require('chalk');
...@@ -21,12 +22,16 @@ var paths = require('../config/paths'); ...@@ -21,12 +22,16 @@ var paths = require('../config/paths');
var recursive = require('recursive-readdir'); var recursive = require('recursive-readdir');
var stripAnsi = require('strip-ansi'); var stripAnsi = require('strip-ansi');
// Input: /User/dan/app/build/static/js/main.82be8.js
// Output: /static/js/main.js
function removeFileNameHash(fileName) { function removeFileNameHash(fileName) {
return fileName return fileName
.replace(paths.appBuild, '') .replace(paths.appBuild, '')
.replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3); .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3);
} }
// Input: 1024, 2048
// Output: "(+1 KB)"
function getDifferenceLabel(currentSize, previousSize) { function getDifferenceLabel(currentSize, previousSize) {
var FIFTY_KILOBYTES = 1024 * 50; var FIFTY_KILOBYTES = 1024 * 50;
var difference = currentSize - previousSize; var difference = currentSize - previousSize;
...@@ -62,6 +67,7 @@ recursive(paths.appBuild, (err, fileNames) => { ...@@ -62,6 +67,7 @@ recursive(paths.appBuild, (err, fileNames) => {
build(previousSizeMap); build(previousSizeMap);
}); });
// Print a detailed summary of build files.
function printFileSizes(stats, previousSizeMap) { function printFileSizes(stats, previousSizeMap) {
var assets = stats.toJson().assets var assets = stats.toJson().assets
.filter(asset => /\.(js|css)$/.test(asset.name)) .filter(asset => /\.(js|css)$/.test(asset.name))
...@@ -78,7 +84,6 @@ function printFileSizes(stats, previousSizeMap) { ...@@ -78,7 +84,6 @@ function printFileSizes(stats, previousSizeMap) {
}; };
}); });
assets.sort((a, b) => b.size - a.size); assets.sort((a, b) => b.size - a.size);
var longestSizeLabelLength = Math.max.apply(null, var longestSizeLabelLength = Math.max.apply(null,
assets.map(a => stripAnsi(a.sizeLabel).length) assets.map(a => stripAnsi(a.sizeLabel).length)
); );
...@@ -96,6 +101,7 @@ function printFileSizes(stats, previousSizeMap) { ...@@ -96,6 +101,7 @@ function printFileSizes(stats, previousSizeMap) {
}); });
} }
// Create the production build and print the deployment instructions.
function build(previousSizeMap) { function build(previousSizeMap) {
console.log('Creating an optimized production build...'); console.log('Creating an optimized production build...');
webpack(config).run((err, stats) => { webpack(config).run((err, stats) => {
......
...@@ -22,7 +22,7 @@ var prompt = require('./utils/prompt'); ...@@ -22,7 +22,7 @@ var prompt = require('./utils/prompt');
var config = require('../config/webpack.config.dev'); var config = require('../config/webpack.config.dev');
var paths = require('../config/paths'); var paths = require('../config/paths');
// Tools like Cloud9 rely on this // Tools like Cloud9 rely on this.
var DEFAULT_PORT = process.env.PORT || 3000; var DEFAULT_PORT = process.env.PORT || 3000;
var compiler; var compiler;
...@@ -40,15 +40,13 @@ if (isSmokeTest) { ...@@ -40,15 +40,13 @@ if (isSmokeTest) {
}; };
} }
// Some custom utilities to prettify Webpack output.
// This is a little hacky.
// It would be easier if webpack provided a rich error object.
var friendlySyntaxErrorLabel = 'Syntax error:'; var friendlySyntaxErrorLabel = 'Syntax error:';
function isLikelyASyntaxError(message) { function isLikelyASyntaxError(message) {
return message.indexOf(friendlySyntaxErrorLabel) !== -1; return message.indexOf(friendlySyntaxErrorLabel) !== -1;
} }
// This is a little hacky.
// It would be easier if webpack provided a rich error object.
function formatMessage(message) { function formatMessage(message) {
return message return message
// Make some common errors shorter: // Make some common errors shorter:
...@@ -69,17 +67,27 @@ function formatMessage(message) { ...@@ -69,17 +67,27 @@ function formatMessage(message) {
} }
function clearConsole() { function clearConsole() {
// This seems to work best on Windows and other systems.
// The intention is to clear the output so you can focus on most recent build.
process.stdout.write('\x1bc'); process.stdout.write('\x1bc');
} }
function setupCompiler(port) { function setupCompiler(port) {
// "Compiler" is a low-level interface to Webpack.
// It lets us listen to some events and provide our own custom messages.
compiler = webpack(config, handleCompile); compiler = webpack(config, handleCompile);
// "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', function() { compiler.plugin('invalid', function() {
clearConsole(); clearConsole();
console.log('Compiling...'); console.log('Compiling...');
}); });
// "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', function(stats) { compiler.plugin('done', function(stats) {
clearConsole(); clearConsole();
var hasErrors = stats.hasErrors(); var hasErrors = stats.hasErrors();
...@@ -94,10 +102,12 @@ function setupCompiler(port) { ...@@ -94,10 +102,12 @@ function setupCompiler(port) {
console.log('Note that the development build is not optimized.'); 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('npm run build') + '.');
console.log(); console.log();
return; return;
} }
// 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.
var json = stats.toJson(); var json = stats.toJson();
var formattedErrors = json.errors.map(message => var formattedErrors = json.errors.map(message =>
'Error in ' + formatMessage(message) 'Error in ' + formatMessage(message)
...@@ -105,7 +115,6 @@ function setupCompiler(port) { ...@@ -105,7 +115,6 @@ function setupCompiler(port) {
var formattedWarnings = json.warnings.map(message => var formattedWarnings = json.warnings.map(message =>
'Warning in ' + formatMessage(message) 'Warning in ' + formatMessage(message)
); );
if (hasErrors) { if (hasErrors) {
console.log(chalk.red('Failed to compile.')); console.log(chalk.red('Failed to compile.'));
console.log(); console.log();
...@@ -122,7 +131,6 @@ function setupCompiler(port) { ...@@ -122,7 +131,6 @@ function setupCompiler(port) {
// If errors exist, ignore warnings. // If errors exist, ignore warnings.
return; return;
} }
if (hasWarnings) { if (hasWarnings) {
console.log(chalk.yellow('Compiled with warnings.')); console.log(chalk.yellow('Compiled with warnings.'));
console.log(); console.log();
...@@ -130,7 +138,7 @@ function setupCompiler(port) { ...@@ -130,7 +138,7 @@ function setupCompiler(port) {
console.log(message); console.log(message);
console.log(); console.log();
}); });
// Teach some ESLint tricks.
console.log('You may use special comments to disable some warnings.'); 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-next-line') + ' to ignore the next line.');
console.log('Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.'); console.log('Use ' + chalk.yellow('/* eslint-disable */') + ' to ignore all warnings in a file.');
...@@ -207,14 +215,29 @@ function addMiddleware(devServer) { ...@@ -207,14 +215,29 @@ function addMiddleware(devServer) {
function runDevServer(port) { function runDevServer(port) {
var devServer = new WebpackDevServer(compiler, { var devServer = new WebpackDevServer(compiler, {
hot: true, // Note: only CSS is currently hot reloaded // Enable hot reloading server. It will provide /sockjs-node/ endpoint
// for the WebpackDevServer client so it can learn when the files were
// updated. The WebpackDevServer client is included as an entry point
// in the Webpack development configuration. Note that only changes
// to CSS are currently hot reloaded. JS changes will refresh the browser.
hot: true,
// It is important to tell WebpackDevServer to use the same "root" path
// as we specified in the config. In development, we always serve from /.
publicPath: config.output.publicPath, publicPath: config.output.publicPath,
// WebpackDevServer is noisy by default so we emit custom message instead
// by listening to the compiler events with `compiler.plugin` calls above.
quiet: true, quiet: true,
// Reportedly, this avoids CPU overload on some systems.
// https://github.com/facebookincubator/create-react-app/issues/293
watchOptions: { watchOptions: {
ignored: /node_modules/ ignored: /node_modules/
} }
}); });
// Our custom middleware proxies requests to /index.html or a remote API.
addMiddleware(devServer); addMiddleware(devServer);
// Launch WebpackDevServer.
devServer.listen(port, (err, result) => { devServer.listen(port, (err, result) => {
if (err) { if (err) {
return console.log(err); return console.log(err);
...@@ -232,6 +255,8 @@ function run(port) { ...@@ -232,6 +255,8 @@ function run(port) {
runDevServer(port); runDevServer(port);
} }
// 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).then(port => { detect(DEFAULT_PORT).then(port => {
if (port === DEFAULT_PORT) { if (port === DEFAULT_PORT) {
run(port); run(port);
......
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