From d5c1d9493bbcb559c3fc0a694a802217808313f4 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 18 May 2017 01:24:42 +0100 Subject: [PATCH] Refactor and improve build output (#2202) --- packages/react-dev-utils/README.md | 13 ++ packages/react-dev-utils/eslintFormatter.js | 6 +- packages/react-dev-utils/package.json | 1 + .../printHostingInstructions.js | 114 ++++++++++ packages/react-scripts/scripts/build.js | 200 +++++------------- 5 files changed, 191 insertions(+), 143 deletions(-) create mode 100644 packages/react-dev-utils/printHostingInstructions.js diff --git a/packages/react-dev-utils/README.md b/packages/react-dev-utils/README.md index 11f3f8eaa8a..d65c43ec667 100644 --- a/packages/react-dev-utils/README.md +++ b/packages/react-dev-utils/README.md @@ -252,6 +252,19 @@ if (openBrowser('http://localhost:3000')) { } ``` +#### `printHostingInstructions(appPackage: Object, publicUrl: string, publicPath: string, buildFolder: string, useYarn: boolean): void` + +Prints hosting instructions after the project is built. + +Pass your parsed `package.json` object as `appPackage`, your the URL where you plan to host the app as `publicUrl`, `output.publicPath` from your Webpack configuration as `publicPath`, the `buildFolder` name, and whether to `useYarn` in instructions. + +```js +const appPackage = require(paths.appPackageJson); +const publicUrl = paths.publicUrl; +const publicPath = config.output.publicPath; +printHostingInstructions(appPackage, publicUrl, publicPath, 'build', true); +``` + #### `webpackHotDevClient.js` This is an alternative client for [WebpackDevServer](https://github.com/webpack/webpack-dev-server) that shows a syntax error overlay. diff --git a/packages/react-dev-utils/eslintFormatter.js b/packages/react-dev-utils/eslintFormatter.js index 674f2f06dec..c087734eadd 100644 --- a/packages/react-dev-utils/eslintFormatter.js +++ b/packages/react-dev-utils/eslintFormatter.js @@ -13,6 +13,7 @@ function isError(message) { function formatter(results) { let output = '\n'; let hasErrors = false; + let reportContainsErrorRuleIDs = false; results.forEach(result => { let messages = result.messages; @@ -25,6 +26,9 @@ function formatter(results) { if (isError(message)) { messageType = 'error'; hasErrors = true; + if (message.ruleId) { + reportContainsErrorRuleIDs = true; + } } else { messageType = 'warn'; } @@ -61,7 +65,7 @@ function formatter(results) { output += `${outputTable}\n\n`; }); - if (hasErrors) { + if (reportContainsErrorRuleIDs) { // Unlike with warnings, we have to do it here. // We have similar code in react-scripts for warnings, // but warnings can appear in multiple files so we only diff --git a/packages/react-dev-utils/package.json b/packages/react-dev-utils/package.json index 0a22499ca01..c63502cd8b0 100644 --- a/packages/react-dev-utils/package.json +++ b/packages/react-dev-utils/package.json @@ -25,6 +25,7 @@ "openBrowser.js", "openChrome.applescript", "prepareProxy.js", + "printHostingInstructions.js", "WatchMissingNodeModulesPlugin.js", "webpackHotDevClient.js" ], diff --git a/packages/react-dev-utils/printHostingInstructions.js b/packages/react-dev-utils/printHostingInstructions.js new file mode 100644 index 00000000000..4b31cbc4499 --- /dev/null +++ b/packages/react-dev-utils/printHostingInstructions.js @@ -0,0 +1,114 @@ +/** + * 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. + */ + +'use strict'; + +const chalk = require('chalk'); +const url = require('url'); + +function printHostingInstructions( + appPackage, + publicUrl, + publicPath, + buildFolder, + useYarn +) { + const publicPathname = url.parse(publicPath).pathname; + if (publicUrl && publicUrl.indexOf('.github.io/') !== -1) { + // "homepage": "http://user.github.io/project" + console.log( + `The project was built assuming it is hosted at ${chalk.green(publicPathname)}.` + ); + console.log( + `You can control this with the ${chalk.green('homepage')} field in your ${chalk.cyan('package.json')}.` + ); + console.log(); + console.log(`The ${chalk.cyan('build')} folder is ready to be deployed.`); + console.log(`To publish it at ${chalk.green(publicUrl)}, run:`); + // If script deploy has been added to package.json, skip the instructions + if (typeof appPackage.scripts.deploy === 'undefined') { + console.log(); + if (useYarn) { + console.log(` ${chalk.cyan('yarn')} add --dev gh-pages`); + } else { + console.log(` ${chalk.cyan('npm')} install --save-dev gh-pages`); + } + console.log(); + console.log( + `Add the following script in your ${chalk.cyan('package.json')}.` + ); + console.log(); + console.log(` ${chalk.dim('// ...')}`); + console.log(` ${chalk.yellow('"scripts"')}: {`); + console.log(` ${chalk.dim('// ...')}`); + console.log( + ` ${chalk.yellow('"predeploy"')}: ${chalk.yellow('"npm run build",')}` + ); + console.log( + ` ${chalk.yellow('"deploy"')}: ${chalk.yellow('"gh-pages -d build"')}` + ); + console.log(' }'); + console.log(); + console.log('Then run:'); + } + console.log(); + console.log(` ${chalk.cyan(useYarn ? 'yarn' : 'npm')} run deploy`); + console.log(); + } else if (publicPath !== '/') { + // "homepage": "http://mywebsite.com/project" + console.log( + `The project was built assuming it is hosted at ${chalk.green(publicPath)}.` + ); + console.log( + `You can control this with the ${chalk.green('homepage')} field in your ${chalk.cyan('package.json')}.` + ); + console.log(); + console.log(`The ${chalk.cyan('build')} folder is ready to be deployed.`); + console.log(); + } else { + if (publicUrl) { + // "homepage": "http://mywebsite.com" + console.log( + `The project was built assuming it is hosted at ${chalk.green(publicUrl)}.` + ); + console.log( + `You can control this with the ${chalk.green('homepage')} field in your ${chalk.cyan('package.json')}.` + ); + console.log(); + } else { + // no homepage + console.log( + 'The project was built assuming it is hosted at the server root.' + ); + console.log( + `To override this, specify the ${chalk.green('homepage')} in your ${chalk.cyan('package.json')}.` + ); + console.log('For example, add this to build it for GitHub Pages:'); + console.log(); + console.log( + ` ${chalk.green('"homepage"')} ${chalk.cyan(':')} ${chalk.green('"http://myname.github.io/myapp"')}${chalk.cyan(',')}` + ); + console.log(); + } + console.log( + `The ${chalk.cyan(buildFolder)} folder is ready to be deployed.` + ); + console.log('You may serve it with a static server:'); + console.log(); + if (useYarn) { + console.log(` ${chalk.cyan('yarn')} global add serve`); + } else { + console.log(` ${chalk.cyan('npm')} install -g serve`); + } + console.log(` ${chalk.cyan('serve')} -s build`); + console.log(); + } +} + +module.exports = printHostingInstructions; diff --git a/packages/react-scripts/scripts/build.js b/packages/react-scripts/scripts/build.js index da9004f9ee6..8a4149370d2 100644 --- a/packages/react-scripts/scripts/build.js +++ b/packages/react-scripts/scripts/build.js @@ -23,14 +23,15 @@ process.on('unhandledRejection', err => { // Ensure environment variables are read. require('../config/env'); +const path = require('path'); const chalk = require('chalk'); const fs = require('fs-extra'); -const path = require('path'); -const url = require('url'); const webpack = require('webpack'); const config = require('../config/webpack.config.prod'); const paths = require('../config/paths'); const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); +const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); +const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild; @@ -44,159 +45,74 @@ if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { // First, read the current file sizes in build directory. // This lets us display how much they changed later. -measureFileSizesBeforeBuild(paths.appBuild).then(previousFileSizes => { - // Remove all content but keep the directory so that - // if you're in it, you don't end up in Trash - fs.emptyDirSync(paths.appBuild); - - // Start the webpack build - build(previousFileSizes); - - // Merge with the public folder - copyPublicFolder(); -}); - -// Print out errors -function printErrors(summary, errors) { - console.log(chalk.red(summary)); - console.log(); - errors.forEach(err => { - console.log(err.message || err); - console.log(); - }); -} - -// Create the production build and print the deployment instructions. -function build(previousFileSizes) { - console.log('Creating an optimized production build...'); - - let compiler; - try { - compiler = webpack(config); - } catch (err) { - printErrors('Failed to compile.', [err]); - process.exit(1); - } - - compiler.run((err, stats) => { - if (err) { - printErrors('Failed to compile.', [err]); - process.exit(1); - } +measureFileSizesBeforeBuild(paths.appBuild) + .then(previousFileSizes => { + // Remove all content but keep the directory so that + // if you're in it, you don't end up in Trash + fs.emptyDirSync(paths.appBuild); + // Merge with the public folder + copyPublicFolder(); + // Start the webpack build + return build(previousFileSizes); + }) + .then( + ({ stats, previousFileSizes }) => { + console.log(chalk.green('Compiled successfully.')); + console.log(); - if (stats.compilation.errors.length) { - printErrors('Failed to compile.', stats.compilation.errors); - process.exit(1); - } + console.log('File sizes after gzip:'); + console.log(); + printFileSizesAfterBuild(stats, previousFileSizes); + console.log(); - if (process.env.CI && stats.compilation.warnings.length) { - printErrors( - 'Failed to compile. When process.env.CI = true, warnings are treated as failures. Most CI servers set this automatically.', - stats.compilation.warnings + const appPackage = require(paths.appPackageJson); + const publicUrl = paths.publicUrl; + const publicPath = config.output.publicPath; + const buildFolder = path.relative(process.cwd(), paths.appBuild); + printHostingInstructions( + appPackage, + publicUrl, + publicPath, + buildFolder, + useYarn ); + }, + err => { + console.log(chalk.red('Failed to compile.')); + console.log(); + console.log(err.message || err); + console.log(); process.exit(1); } + ); - console.log(chalk.green('Compiled successfully.')); - console.log(); - - console.log('File sizes after gzip:'); - console.log(); - printFileSizesAfterBuild(stats, previousFileSizes); - console.log(); +// Create the production build and print the deployment instructions. +function build(previousFileSizes) { + console.log('Creating an optimized production build...'); - const appPackage = require(paths.appPackageJson); - const publicUrl = paths.publicUrl; - const publicPath = config.output.publicPath; - const publicPathname = url.parse(publicPath).pathname; - if (publicUrl && publicUrl.indexOf('.github.io/') !== -1) { - // "homepage": "http://user.github.io/project" - console.log( - `The project was built assuming it is hosted at ${chalk.green(publicPathname)}.` - ); - console.log( - `You can control this with the ${chalk.green('homepage')} field in your ${chalk.cyan('package.json')}.` - ); - console.log(); - console.log(`The ${chalk.cyan('build')} folder is ready to be deployed.`); - console.log(`To publish it at ${chalk.green(publicUrl)}, run:`); - // If script deploy has been added to package.json, skip the instructions - if (typeof appPackage.scripts.deploy === 'undefined') { - console.log(); - if (useYarn) { - console.log(` ${chalk.cyan('yarn')} add --dev gh-pages`); - } else { - console.log(` ${chalk.cyan('npm')} install --save-dev gh-pages`); - } - console.log(); - console.log( - `Add the following script in your ${chalk.cyan('package.json')}.` - ); - console.log(); - console.log(` ${chalk.dim('// ...')}`); - console.log(` ${chalk.yellow('"scripts"')}: {`); - console.log(` ${chalk.dim('// ...')}`); - console.log( - ` ${chalk.yellow('"predeploy"')}: ${chalk.yellow('"npm run build",')}` - ); - console.log( - ` ${chalk.yellow('"deploy"')}: ${chalk.yellow('"gh-pages -d build"')}` - ); - console.log(' }'); - console.log(); - console.log('Then run:'); + let compiler = webpack(config); + return new Promise((resolve, reject) => { + compiler.run((err, stats) => { + if (err) { + return reject(err); } - console.log(); - console.log(` ${chalk.cyan(useYarn ? 'yarn' : 'npm')} run deploy`); - console.log(); - } else if (publicPath !== '/') { - // "homepage": "http://mywebsite.com/project" - console.log( - `The project was built assuming it is hosted at ${chalk.green(publicPath)}.` - ); - console.log( - `You can control this with the ${chalk.green('homepage')} field in your ${chalk.cyan('package.json')}.` - ); - console.log(); - console.log(`The ${chalk.cyan('build')} folder is ready to be deployed.`); - console.log(); - } else { - if (publicUrl) { - // "homepage": "http://mywebsite.com" - console.log( - `The project was built assuming it is hosted at ${chalk.green(publicUrl)}.` - ); - console.log( - `You can control this with the ${chalk.green('homepage')} field in your ${chalk.cyan('package.json')}.` - ); - console.log(); - } else { - // no homepage - console.log( - 'The project was built assuming it is hosted at the server root.' - ); - console.log( - `To override this, specify the ${chalk.green('homepage')} in your ${chalk.cyan('package.json')}.` - ); - console.log('For example, add this to build it for GitHub Pages:'); + const messages = formatWebpackMessages(stats.toJson({}, true)); + if (messages.errors.length) { + return reject(new Error(messages.errors.join('\n\n'))); + } + if (process.env.CI && messages.warnings.length) { console.log(); console.log( - ` ${chalk.green('"homepage"')} ${chalk.cyan(':')} ${chalk.green('"http://myname.github.io/myapp"')}${chalk.cyan(',')}` + chalk.yellow( + 'Treating warnings as errors because process.env.CI = true.\n' + + 'Most CI servers set it automatically.' + ) ); console.log(); + return reject(new Error(messages.warnings.join('\n\n'))); } - const build = path.relative(process.cwd(), paths.appBuild); - console.log(`The ${chalk.cyan(build)} folder is ready to be deployed.`); - console.log('You may serve it with a static server:'); - console.log(); - if (useYarn) { - console.log(` ${chalk.cyan('yarn')} global add serve`); - } else { - console.log(` ${chalk.cyan('npm')} install -g serve`); - } - console.log(` ${chalk.cyan('serve')} -s build`); - console.log(); - } + return resolve({ stats, previousFileSizes }); + }); }); }