diff --git a/bin/roadhog.js b/bin/roadhog.js index 8cefc57..d9298f8 100755 --- a/bin/roadhog.js +++ b/bin/roadhog.js @@ -33,6 +33,7 @@ switch (script) { console.log(require('../package.json').version); break; case 'build': + case 'buildDll': case 'server': case 'test': require('atool-monitor').emit(); diff --git a/package.json b/package.json index f24da37..85a24be 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,8 @@ "json-loader": "^0.5.4", "less": "^2.7.1", "less-loader": "^2.2.3", + "lodash.pullall": "^4.2.0", + "lodash.uniq": "^4.5.0", "mocha": "^3.2.0", "parse-json-pretty": "^0.1.0", "postcss": "^5.2.6", diff --git a/src/buildDll.js b/src/buildDll.js new file mode 100644 index 0000000..d7daeab --- /dev/null +++ b/src/buildDll.js @@ -0,0 +1,176 @@ +import chalk from 'chalk'; +import fs from 'fs-extra'; +import path from 'path'; +import filesize from 'filesize'; +import { sync as gzipSize } from 'gzip-size'; +import webpack from 'webpack'; +import recursive from 'recursive-readdir'; +import stripAnsi from 'strip-ansi'; +import getPaths from './config/paths'; +import getConfig from './utils/getConfig'; +import applyWebpackConfig, { warnIfExists } from './utils/applyWebpackConfig'; + +process.env.NODE_ENV = process.env.NODE_ENV || 'production'; + +const argv = require('yargs') + .usage('Usage: roadhog buildDll [options]') + .help('h') + .argv; + +let rcConfig; +let outputPath; +let appBuild; +let config; + +export function build(argv) { + const paths = getPaths(argv.cwd); + + try { + rcConfig = getConfig(process.env.NODE_ENV, argv.cwd); + } catch (e) { + console.log(chalk.red('Failed to parse .roadhogrc config.')); + console.log(); + console.log(e.message); + process.exit(1); + } + + if (!rcConfig.dllPlugin) { + console.log(chalk.red('dllPlugin config not found in .roadhogrc')); + process.exit(1); + } + + appBuild = paths.dllNodeModule; + config = applyWebpackConfig( + require('./config/webpack.config.dll')(argv, rcConfig, paths), + process.env.NODE_ENV, + ); + + return new Promise((resolve) => { + // First, read the current file sizes in build directory. + // This lets us display how much they changed later. + recursive(appBuild, (err, fileNames) => { + const previousSizeMap = (fileNames || []) + .filter(fileName => /\.(js|css)$/.test(fileName)) + .reduce((memo, fileName) => { + const contents = fs.readFileSync(fileName); + const key = removeFileNameHash(fileName); + memo[key] = gzipSize(contents); + return memo; + }, {}); + + // Remove all content but keep the directory so that + // if you're in it, you don't end up in Trash + fs.emptyDirSync(appBuild); + + // Start the webpack build + realBuild(previousSizeMap, resolve, argv); + }); + }); +} + +// Input: /User/dan/app/build/static/js/main.82be8.js +// Output: /static/js/main.js +function removeFileNameHash(fileName) { + return fileName + .replace(appBuild, '') + .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3); +} + +// Input: 1024, 2048 +// Output: "(+1 KB)" +function getDifferenceLabel(currentSize, previousSize) { + const FIFTY_KILOBYTES = 1024 * 50; + const difference = currentSize - previousSize; + const fileSize = !Number.isNaN(difference) ? filesize(difference) : 0; + if (difference >= FIFTY_KILOBYTES) { + return chalk.red(`+${fileSize}`); + } else if (difference < FIFTY_KILOBYTES && difference > 0) { + return chalk.yellow(`+${fileSize}`); + } else if (difference < 0) { + return chalk.green(fileSize); + } else { + return ''; + } +} + +// Print a detailed summary of build files. +function printFileSizes(stats, previousSizeMap) { + const assets = stats.toJson().assets + .filter(asset => /\.(js|css)$/.test(asset.name)) + .map((asset) => { + const fileContents = fs.readFileSync(`${appBuild}/${asset.name}`); + const size = gzipSize(fileContents); + const previousSize = previousSizeMap[removeFileNameHash(asset.name)]; + const difference = getDifferenceLabel(size, previousSize); + return { + folder: path.join(appBuild, path.dirname(asset.name)), + name: path.basename(asset.name), + size, + sizeLabel: filesize(size) + (difference ? ` (${difference})` : ''), + }; + }); + assets.sort((a, b) => b.size - a.size); + const longestSizeLabelLength = Math.max.apply( + null, + assets.map(a => stripAnsi(a.sizeLabel).length), + ); + assets.forEach((asset) => { + let sizeLabel = asset.sizeLabel; + const sizeLength = stripAnsi(sizeLabel).length; + if (sizeLength < longestSizeLabelLength) { + const rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength); + sizeLabel += rightPadding; + } + console.log( + ` ${sizeLabel} ${chalk.dim(asset.folder + path.sep)}${chalk.cyan(asset.name)}`, + ); + }); +} + +// Print out errors +function printErrors(summary, errors) { + console.log(chalk.red(summary)); + console.log(); + errors.forEach((err) => { + console.log(err.message || err); + console.log(); + }); +} + +function doneHandler(previousSizeMap, argv, resolve, err, stats) { + if (err) { + printErrors('Failed to compile.', [err]); + process.exit(1); + } + + if (stats.compilation.errors.length) { + printErrors('Failed to compile.', stats.compilation.errors); + process.exit(1); + } + + warnIfExists(); + + console.log(chalk.green('Compiled successfully.')); + console.log(); + + console.log('File sizes after gzip:'); + console.log(); + printFileSizes(stats, previousSizeMap); + console.log(); + + resolve(); +} + +// Create the production build and print the deployment instructions. +function realBuild(previousSizeMap, resolve, argv) { + console.log('Creating dll bundle...'); + + const compiler = webpack(config); + const done = doneHandler.bind(null, previousSizeMap, argv, resolve); + compiler.run(done); +} + +// Run. +if (require.main === module) { + build({ ...argv, cwd: process.cwd() }); +} diff --git a/src/config/paths.js b/src/config/paths.js index d283afa..c6d1a27 100644 --- a/src/config/paths.js +++ b/src/config/paths.js @@ -19,6 +19,8 @@ export default function getPaths(cwd) { appSrc: resolveApp('src'), appNodeModules: resolveApp('node_modules'), ownNodeModules: resolveOwn('../../node_modules'), + dllNodeModule: resolveApp('node_modules/roadhog-dlls'), + dllManifest: resolveApp('node_modules/roadhog-dlls/roadhog.json'), resolveApp, appDirectory, }; diff --git a/src/config/webpack.config.dev.js b/src/config/webpack.config.dev.js index c995365..9469a4f 100644 --- a/src/config/webpack.config.dev.js +++ b/src/config/webpack.config.dev.js @@ -5,6 +5,7 @@ import webpack from 'webpack'; import fs from 'fs'; import WatchMissingNodeModulesPlugin from 'react-dev-utils/WatchMissingNodeModulesPlugin'; import SystemBellWebpackPlugin from 'system-bell-webpack-plugin'; +import { join } from 'path'; import getPaths from './paths'; import getEntry from '../utils/getEntry'; import getTheme from '../utils/getTheme'; @@ -30,6 +31,19 @@ export default function (config, cwd) { const theme = JSON.stringify(getTheme(process.cwd(), config)); const paths = getPaths(cwd); + const dllPlugins = config.dllPlugin ? [ + new webpack.DllReferencePlugin({ + context: paths.appSrc, + manifest: require(paths.dllManifest), // eslint-disable-line + }), + new CopyWebpackPlugin([ + { + from: join(paths.dllNodeModule, 'roadhog.dll.js'), + to: join(paths.appBuild, 'roadhog.dll.js'), + }, + ]), + ] : []; + const finalWebpackConfig = { devtool: 'cheap-module-source-map', entry: getEntry(config, paths.appDirectory), @@ -147,6 +161,8 @@ export default function (config, cwd) { new WatchMissingNodeModulesPlugin(paths.appNodeModules), new SystemBellWebpackPlugin(), ].concat( + dllPlugins + ).concat( !fs.existsSync(paths.appPublic) ? [] : new CopyWebpackPlugin([ { diff --git a/src/config/webpack.config.dll.js b/src/config/webpack.config.dll.js new file mode 100644 index 0000000..2d66302 --- /dev/null +++ b/src/config/webpack.config.dll.js @@ -0,0 +1,37 @@ +import webpack from 'webpack'; +import { join } from 'path'; + +import pullAll from 'lodash.pullall'; +import uniq from 'lodash.uniq'; + +export default function (argv, rcConfig, paths) { + const appBuild = paths.dllNodeModule; + const pkg = require(join(paths.appDirectory, 'package.json')); // disable-eslint-line + + const { include, exclude } = rcConfig.dllPlugin || {}; + + const dependencyNames = Object.keys(pkg.dependencies); + const includeDependencies = uniq(dependencyNames.concat(include || [])); + + return { + entry: { + roadhog: pullAll(includeDependencies, exclude), + }, + output: { + path: appBuild, + filename: '[name].dll.js', + library: '[name]', + }, + plugins: [ + new webpack.DllPlugin({ + path: join(appBuild, '[name].json'), + name: '[name]', + context: paths.appSrc, + }), + ], + resolve: { + root: paths.appDirectory, + modulesDirectories: ['node_modules'], + }, + }; +} diff --git a/src/runServer.js b/src/runServer.js index e511173..7bc463c 100644 --- a/src/runServer.js +++ b/src/runServer.js @@ -1,4 +1,5 @@ import detect from 'detect-port'; +import fs from 'fs'; import clearConsole from 'react-dev-utils/clearConsole'; import getProcessForPort from 'react-dev-utils/getProcessForPort'; import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages'; @@ -211,6 +212,12 @@ function run(port) { function init() { readRcConfig(); + + if (rcConfig.dllPlugin && !fs.existsSync(paths.dllManifest)) { + console.log(chalk.red('Failed to start the server, since you have enabled dllPlugin, but have not run `roadhog buildDll` before `roadhog server`.')); + process.exit(1); + } + readWebpackConfig(); detect(DEFAULT_PORT).then((port) => { if (port === DEFAULT_PORT) {