build.js 8.89 KB
Newer Older
1
// @remove-on-eject-begin
Christopher Chedeau's avatar
Christopher Chedeau committed
2
3
4
5
6
7
8
9
/**
 * 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.
 */
10
// @remove-on-eject-end
Christopher Chedeau's avatar
Christopher Chedeau committed
11

12
// Do this as the first thing so that any code reading it knows the right env.
13
14
process.env.NODE_ENV = 'production';

Brian Ng's avatar
Brian Ng committed
15
// Load environment variables from .env file. Suppress warnings using silent
16
17
18
19
20
// if this file is missing. dotenv will never modify any environment variables
// that have already been set.
// https://github.com/motdotla/dotenv
require('dotenv').config({silent: true});

21
var chalk = require('chalk');
22
var fs = require('fs-extra');
23
var path = require('path');
24
var filesize = require('filesize');
25
var gzipSize = require('gzip-size').sync;
26
var webpack = require('webpack');
27
var config = require('../config/webpack.config.prod');
28
var paths = require('../config/paths');
29
var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
Elijah Manor's avatar
Elijah Manor committed
30
var recursive = require('recursive-readdir');
31
var stripAnsi = require('strip-ansi');
32

33
var useYarn = fs.existsSync(paths.yarnLockFile);
Ville Immonen's avatar
Ville Immonen committed
34

35
36
37
38
// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
  process.exit(1);
}
39

40
41
// Input: /User/dan/app/build/static/js/main.82be8.js
// Output: /static/js/main.js
Elijah Manor's avatar
Elijah Manor committed
42
function removeFileNameHash(fileName) {
Dan Abramov's avatar
Dan Abramov committed
43
44
45
  return fileName
    .replace(paths.appBuild, '')
    .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3);
Elijah Manor's avatar
Elijah Manor committed
46
}
47

48
49
// Input: 1024, 2048
// Output: "(+1 KB)"
Dan Abramov's avatar
Dan Abramov committed
50
function getDifferenceLabel(currentSize, previousSize) {
51
  var FIFTY_KILOBYTES = 1024 * 50;
Elijah Manor's avatar
Elijah Manor committed
52
  var difference = currentSize - previousSize;
53
54
  var fileSize = !Number.isNaN(difference) ? filesize(difference) : 0;
  if (difference >= FIFTY_KILOBYTES) {
Elijah Manor's avatar
Elijah Manor committed
55
    return chalk.red('+' + fileSize);
56
57
58
59
60
61
  } else if (difference < FIFTY_KILOBYTES && difference > 0) {
    return chalk.yellow('+' + fileSize);
  } else if (difference < 0) {
    return chalk.green(fileSize);
  } else {
    return '';
62
  }
Elijah Manor's avatar
Elijah Manor committed
63
}
64

Dan Abramov's avatar
Dan Abramov committed
65
66
// First, read the current file sizes in build directory.
// This lets us display how much they changed later.
Dan Abramov's avatar
Dan Abramov committed
67
recursive(paths.appBuild, (err, fileNames) => {
Dan Abramov's avatar
Dan Abramov committed
68
  var previousSizeMap = (fileNames || [])
Dan Abramov's avatar
Dan Abramov committed
69
    .filter(fileName => /\.(js|css)$/.test(fileName))
Elijah Manor's avatar
Elijah Manor committed
70
71
72
73
74
    .reduce((memo, fileName) => {
      var contents = fs.readFileSync(fileName);
      var key = removeFileNameHash(fileName);
      memo[key] = gzipSize(contents);
      return memo;
Dan Abramov's avatar
Dan Abramov committed
75
    }, {});
76

Elijah Manor's avatar
Elijah Manor committed
77
78
  // Remove all content but keep the directory so that
  // if you're in it, you don't end up in Trash
79
  fs.emptyDirSync(paths.appBuild);
Elijah Manor's avatar
Elijah Manor committed
80

Dan Abramov's avatar
Dan Abramov committed
81
  // Start the webpack build
Elijah Manor's avatar
Elijah Manor committed
82
  build(previousSizeMap);
83
84
85

  // Merge with the public folder
  copyPublicFolder();
Elijah Manor's avatar
Elijah Manor committed
86
});
87

88
// Print a detailed summary of build files.
Dan Abramov's avatar
Dan Abramov committed
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
function printFileSizes(stats, previousSizeMap) {
  var assets = stats.toJson().assets
    .filter(asset => /\.(js|css)$/.test(asset.name))
    .map(asset => {
      var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name);
      var size = gzipSize(fileContents);
      var previousSize = previousSizeMap[removeFileNameHash(asset.name)];
      var difference = getDifferenceLabel(size, previousSize);
      return {
        folder: path.join('build', path.dirname(asset.name)),
        name: path.basename(asset.name),
        size: size,
        sizeLabel: filesize(size) + (difference ? ' (' + difference + ')' : '')
      };
    });
  assets.sort((a, b) => b.size - a.size);
  var longestSizeLabelLength = Math.max.apply(null,
    assets.map(a => stripAnsi(a.sizeLabel).length)
  );
  assets.forEach(asset => {
    var sizeLabel = asset.sizeLabel;
    var sizeLength = stripAnsi(sizeLabel).length;
    if (sizeLength < longestSizeLabelLength) {
      var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength);
      sizeLabel += rightPadding;
    }
    console.log(
      '  ' + sizeLabel +
      '  ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name)
    );
  });
}

122
123
124
125
126
127
128
129
130
131
// Print out errors
function printErrors(summary, errors) {
  console.log(chalk.red(summary));
  console.log();
  errors.forEach(err => {
    console.log(err.message || err);
    console.log();
  });
}

132
// Create the production build and print the deployment instructions.
Elijah Manor's avatar
Elijah Manor committed
133
134
function build(previousSizeMap) {
  console.log('Creating an optimized production build...');
Dan Abramov's avatar
Dan Abramov committed
135
  webpack(config).run((err, stats) => {
Elijah Manor's avatar
Elijah Manor committed
136
    if (err) {
137
138
139
140
141
142
      printErrors('Failed to compile.', [err]);
      process.exit(1);
    }

    if (stats.compilation.errors.length) {
      printErrors('Failed to compile.', stats.compilation.errors);
Elijah Manor's avatar
Elijah Manor committed
143
      process.exit(1);
144
    }
145

146
    if (process.env.CI && stats.compilation.warnings.length) {
147
     printErrors('Failed to compile. When process.env.CI = true, warnings are treated as failures. Most CI servers set this automatically.', stats.compilation.warnings);
148
149
150
     process.exit(1);
   }

Elijah Manor's avatar
Elijah Manor committed
151
    console.log(chalk.green('Compiled successfully.'));
152
    console.log();
Elijah Manor's avatar
Elijah Manor committed
153
154

    console.log('File sizes after gzip:');
155
    console.log();
Dan Abramov's avatar
Dan Abramov committed
156
    printFileSizes(stats, previousSizeMap);
157
    console.log();
Elijah Manor's avatar
Elijah Manor committed
158
159

    var openCommand = process.platform === 'win32' ? 'start' : 'open';
160
161
    var appPackage  = require(paths.appPackageJson);
    var homepagePath = appPackage.homepage;
Dan Abramov's avatar
Dan Abramov committed
162
163
164
    var publicPath = config.output.publicPath;
    if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) {
      // "homepage": "http://user.github.io/project"
Dan Abramov's avatar
Dan Abramov committed
165
166
167
168
169
      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('To publish it at ' + chalk.green(homepagePath) + ', run:');
170
171
172
173
174
175
176
177
178
179
180
181
182
183
      // 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('// ...'));
184
185
        console.log('      ' + chalk.yellow('"predeploy"') + ': ' + chalk.yellow('"npm run build",'));
        console.log('      ' + chalk.yellow('"deploy"') + ': ' + chalk.yellow('"gh-pages -d build"'));
186
187
188
        console.log('    }');
        console.log();
        console.log('Then run:');
Ville Immonen's avatar
Ville Immonen committed
189
      }
190
      console.log();
Ville Immonen's avatar
Ville Immonen committed
191
      console.log('  ' + chalk.cyan(useYarn ? 'yarn' : 'npm') +  ' run deploy');
Elijah Manor's avatar
Elijah Manor committed
192
      console.log();
Dan Abramov's avatar
Dan Abramov committed
193
194
195
196
    } 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') + '.');
Elijah Manor's avatar
Elijah Manor committed
197
      console.log();
Dan Abramov's avatar
Dan Abramov committed
198
      console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
Elijah Manor's avatar
Elijah Manor committed
199
      console.log();
Dan Abramov's avatar
Dan Abramov committed
200
201
    } else {
      // no homepage or "homepage": "http://mywebsite.com"
Dan Abramov's avatar
Dan Abramov committed
202
203
204
205
      console.log('The project was built assuming it is hosted at the server root.');
      if (homepagePath) {
        // "homepage": "http://mywebsite.com"
        console.log('You can control this with the ' + chalk.green('homepage') + ' field in your '  + chalk.cyan('package.json') + '.');
Dan Abramov's avatar
Dan Abramov committed
206
        console.log();
Dan Abramov's avatar
Dan Abramov committed
207
208
209
210
211
212
      } else {
        // no homepage
        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(','));
Dan Abramov's avatar
Dan Abramov committed
213
        console.log();
Dan Abramov's avatar
Dan Abramov committed
214
      }
Dan Abramov's avatar
Dan Abramov committed
215
216
217
      console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.');
      console.log('You may also serve it locally with a static server:')
      console.log();
Ville Immonen's avatar
Ville Immonen committed
218
219
220
221
222
      if (useYarn) {
        console.log('  ' + chalk.cyan('yarn') +  ' global add pushstate-server');
      } else {
        console.log('  ' + chalk.cyan('npm') +  ' install -g pushstate-server');
      }
Dan Abramov's avatar
Dan Abramov committed
223
224
      console.log('  ' + chalk.cyan('pushstate-server') + ' build');
      console.log('  ' + chalk.cyan(openCommand) + ' http://localhost:9000');
Dan Abramov's avatar
Dan Abramov committed
225
      console.log();
Elijah Manor's avatar
Elijah Manor committed
226
227
228
    }
  });
}
229
230
231
232
233
234
235

function copyPublicFolder() {
  fs.copySync(paths.appPublic, paths.appBuild, {
    dereference: true,
    filter: file => file !== paths.appHtml
  });
}