diff --git a/webpack.config.js b/webpack.config.js index 12f95757..53db1dd1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,8 +1,7 @@ const fs = require('fs'); const path = require('path'); const webpack = require('webpack'); -// let ExtractTextPlugin = require('extract-text-webpack-plugin'); -let CleanWebpackPlugin = require('clean-webpack-plugin'); +const CleanWebpackPlugin = require('clean-webpack-plugin'); const ProgressBarPlugin = require('progress-bar-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); @@ -27,7 +26,6 @@ module.exports = function(_, env) { return { mode: isProd ? 'production' : 'development', entry: path.join(__dirname, 'config/client-boot.js'), - // entry: './src/index', output: { filename: isProd ? '[name].[chunkhash:5].js' : '[name].js', chunkFilename: '[name].chunk.[chunkhash:5].js', @@ -43,6 +41,7 @@ module.exports = function(_, env) { }, resolveLoader: { alias: { + // async-component-loader returns a wrapper component that waits for the import to load before rendering: async: path.join(__dirname, 'config/async-component-loader') } }, @@ -50,19 +49,22 @@ module.exports = function(_, env) { rules: [ { test: /\.tsx?$/, + // Ensure typescript is compiled prior to Babel running: enforce: 'pre', - // loader: 'awesome-typescript-loader', loader: 'ts-loader', - exclude: nodeModules + // Don't transpile anything in node_modules: + exclude: nodeModules, }, { test: /\.(tsx?|jsx?)$/, loader: 'babel-loader', + // Don't respect any Babel RC files found on the filesystem: options: Object.assign(readJson('.babelrc'), { babelrc: false }) }, { test: /\.(scss|sass)$/, loader: 'sass-loader', + // SCSS gets preprocessed, then treated like any other CSS: enforce: 'pre', options: { sourceMap: true, @@ -71,12 +73,14 @@ module.exports = function(_, env) { }, { test: /\.(scss|sass|css)$/, + // Only enable CSS Modules within `src/{components,routes}/*` include: componentStyleDirs, use: [ + // In production, CSS is extracted to files on disk. In development, it's inlined into JS: isProd ? MiniCssExtractPlugin.loader : 'style-loader', { - // loader: 'typings-for-css-modules-loader?modules&localIdentName=[local]__[hash:base64:5]&importLoaders=1'+(isProd ? '&sourceMap' : '') - // loader: 'css-loader', + // This is a fork of css-loader that auto-generates .d.ts files for CSS module imports. + // The result is a definition file with the exported String classname mappings. loader: 'typings-for-css-modules-loader', options: { modules: true, @@ -89,21 +93,10 @@ module.exports = function(_, env) { } } ] - // loader: ExtractTextPlugin.extract({ - // fallback: 'style-loader', - // use: [{ - // loader: 'css-loader', - // options: { - // modules: true, - // localIdentName: '[local]__[hash:base64:5]', - // importLoaders: 1, - // sourceMap: isProd - // } - // }] - // }) }, { test: /\.(scss|sass|css)$/, + // Process non-modular CSS everywhere *except* `src/{components,routes}/*` exclude: componentStyleDirs, use: [ isProd ? MiniCssExtractPlugin.loader : 'style-loader', @@ -115,20 +108,11 @@ module.exports = function(_, env) { } } ] - // loader: ExtractTextPlugin.extract({ - // fallback: 'style-loader', - // use: [{ - // loader: 'css-loader', - // options: { - // importLoaders: 1, - // sourceMap: isProd - // } - // }] - // }) } ] }, plugins: [ + // Pretty progressbar showing build progress: new ProgressBarPlugin({ format: '\u001b[90m\u001b[44mBuild\u001b[49m\u001b[39m [:bar] \u001b[32m\u001b[1m:percent\u001b[22m\u001b[39m (:elapseds) \u001b[2m:msg\u001b[22m', renderThrottle: 100, @@ -136,25 +120,42 @@ module.exports = function(_, env) { clear: true }), - isProd && new CleanWebpackPlugin(['build']), - isProd && new webpack.optimize.SplitChunksPlugin({}), - isProd && new MiniCssExtractPlugin({}), - // new ExtractTextPlugin({ - // filename: isProd ? 'style.[contenthash:5].css' : 'style.css', - // disable: !isProd, - // allChunks: true - // }), + // Remove old files before outputting a production build: + isProd && new CleanWebpackPlugin([ + 'assets', + '**/*.{css,js,json,html}' + ], { + root: path.join(__dirname, 'build'), + beforeEmit: true + }), - // fixes infinite loop in typings-for-css-modules-loader: + // Automatically split code into async chunks. + // See: https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366 + isProd && new webpack.optimize.SplitChunksPlugin({}), + + // In production, extract all CSS to produce files on disk, even for + // lazy-loaded CSS chunks. CSS for async chunks is loaded on-demand. + // This is a modern Webpack 4 replacement for ExtractTextPlugin. + // See: https://github.com/webpack-contrib/mini-css-extract-plugin + // See also: https://twitter.com/wsokra/status/970253245733113856 + isProd && new MiniCssExtractPlugin({ + chunkFilename: '[name].chunk.[contenthash:5].css' + }), + + // These plugins fix infinite loop in typings-for-css-modules-loader. + // See: https://github.com/Jimdo/typings-for-css-modules-loader/issues/35 new webpack.WatchIgnorePlugin([ /(c|sc|sa)ss\.d\.ts$/ ]), new WatchTimestampsPlugin([ /(c|sc|sa)ss\.d\.ts$/ ]), + + // For now we're not doing SSR. new HtmlWebpackPlugin({ filename: path.join(__dirname, 'build/index.html'), template: '!!ejs-loader!src/index.html', + // template: '!!'+path.join(__dirname, 'config/prerender-loader')+'!src/index.html', minify: isProd && { collapseWhitespace: true, removeScriptTypeAttributes: true, @@ -163,58 +164,87 @@ module.exports = function(_, env) { removeComments: true }, manifest: readJson('./src/manifest.json'), - - /** @todo Finish implementing prerendering similar to that of Preact CLI. */ - prerender() { - return '
'; - // require('babel-register')({ ignore: false }); - // return require('./config/prerender')(); - }, inject: true, compile: true }), + + // Inject for resources isProd && new PreloadWebpackPlugin(), + + // Inline constants during build, so they can be folded by UglifyJS. new webpack.DefinePlugin({ + // We set node.process=false later in this config. + // Here we make sure if (process && process.foo) still works: process: '{}' }), + + // Babel embeds helpful error messages into transpiled classes that we don't need in production. + // Here we replace the constructor and message with a static throw, leaving the message to be DCE'd. + // This is useful since it shows the message in SourceMapped code when debugging. isProd && new ReplacePlugin({ include: /babel-helper$/, patterns: [{ - regex: /throw\s+(new\s+)?(Type|Reference)?Error\s*\(/g, - value: s => `return;${Array(s.length - 7).join(' ')}(` + regex: /throw\s+(?:new\s+)?((?:Type|Reference)?Error)\s*\(/g, + value: (s, type) => `throw 'babel error'; (` }] }), + + // Copying files via Webpack allows them to be served dynamically by `webpack serve` new CopyPlugin([ { from: 'src/manifest.json', to: 'manifest.json' }, { from: 'src/assets', to: 'assets' } ]), + // For production builds, output module size analysis to build/report.html isProd && new BundleAnalyzerPlugin({ analyzerMode: 'static', defaultSizes: 'gzip', openAnalyzer: false + }), + + // Generate a ServiceWorker using Workbox. + isProd && new WorkboxPlugin.GenerateSW({ + swDest: 'sw.js', + clientsClaim: true, + skipWaiting: true, + // allow for offline client-side routing: + navigateFallback: '/', + navigateFallbackBlacklist: [/\.[a-z0-9]+$/i] }) ].filter(Boolean), + // Turn off various NodeJS environment polyfills Webpack adds to bundles. + // They're supposed to be added only when used, but the heuristic is loose + // (eg: existence of a variable called setImmedaite in any scope) node: { console: false, + // Keep global, it's just an alias of window and used by many third party modules: global: true, + // Turn off process to avoid bundling a nextTick implementation: process: false, + // Inline __filename and __dirname values: __filename: 'mock', __dirname: 'mock', + // Never embed a portable implementation of Node's Buffer module: Buffer: false, + // Never embed a setImmediate implementation: setImmediate: false }, devServer: { + // Any unmatched request paths will serve static files from src/*: contentBase: path.join(__dirname, 'src'), inline: true, hot: true, + // Request paths not ending in a file extension serve index.html: historyApiFallback: true, + // Don't output server address info to console on startup: noInfo: true, - // quiet: true, + // Suppress forwarding of Webpack logs to the browser console: clientLogLevel: 'none', + // Supress the extensive stats normally printed after a dev build (since sizes are mostly useless): stats: 'minimal', + // Don't embed an error overlay ("redbox") into the client bundle: overlay: false } };