react-universally
Version:
A starter kit for universal react applications.
520 lines (483 loc) • 22 kB
JavaScript
import appRootDir from 'app-root-dir';
import AssetsPlugin from 'assets-webpack-plugin';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import nodeExternals from 'webpack-node-externals';
import path from 'path';
import webpack from 'webpack';
import WebpackMd5Hash from 'webpack-md5-hash';
import { happyPackPlugin } from '../utils';
import { ifElse } from '../../shared/utils/logic';
import { mergeDeep } from '../../shared/utils/objects';
import { removeNil } from '../../shared/utils/arrays';
import withServiceWorker from './withServiceWorker';
import config from '../../config';
/**
* Generates a webpack configuration for the target configuration.
*
* This function has been configured to support one "client/web" bundle, and any
* number of additional "node" bundles (e.g. our "server"). You can define
* additional node bundles by editing the project confuguration.
*
* @param {Object} buildOptions - The build options.
* @param {target} buildOptions.target - The bundle target (e.g 'clinet' || 'server').
* @param {target} buildOptions.optimize - Build an optimised version of the bundle?
*
* @return {Object} The webpack configuration.
*/
export default function webpackConfigFactory(buildOptions) {
const { target, optimize = false } = buildOptions;
const isProd = optimize;
const isDev = !isProd;
const isClient = target === 'client';
const isServer = target === 'server';
const isNode = !isClient;
// Preconfigure some ifElse helper instnaces. See the util docs for more
// information on how this util works.
const ifDev = ifElse(isDev);
const ifProd = ifElse(isProd);
const ifNode = ifElse(isNode);
const ifClient = ifElse(isClient);
const ifDevClient = ifElse(isDev && isClient);
const ifProdClient = ifElse(isProd && isClient);
console.log(
`==> Creating ${isProd ? 'an optimised' : 'a development'} bundle configuration for the "${target}"`,
);
const bundleConfig = isServer || isClient
? // This is either our "server" or "client" bundle.
config(['bundles', target])
: // Otherwise it must be an additional node bundle.
config(['additionalNodeBundles', target]);
if (!bundleConfig) {
throw new Error('No bundle configuration exists for target:', target);
}
let webpackConfig = {
// Define our entry chunks for our bundle.
entry: {
// We name our entry files "index" as it makes it easier for us to
// import bundle output files (e.g. `import server from './build/server';`)
index: removeNil([
// We are using polyfill.io instead of the very heavy babel-polyfill.
// Therefore we need to add the regenerator-runtime as polyfill.io
// doesn't support this.
ifClient('regenerator-runtime/runtime'),
// Extends hot reloading with the ability to hot path React Components.
// This should always be at the top of your entries list. Only put
// polyfills above it.
ifDevClient('react-hot-loader/patch'),
// Required to support hot reloading of our client.
ifDevClient(
() =>
`webpack-hot-middleware/client?reload=true&path=http://${config('host')}:${config('clientDevServerPort')}/__webpack_hmr`,
),
// The source entry file for the bundle.
path.resolve(appRootDir.get(), bundleConfig.srcEntryFile),
]),
},
// Bundle output configuration.
output: {
// The dir in which our bundle should be output.
path: path.resolve(appRootDir.get(), bundleConfig.outputPath),
// The filename format for our bundle's entries.
filename: ifProdClient(
// For our production client bundles we include a hash in the filename.
// That way we won't hit any browser caching issues when our bundle
// output changes.
// Note: as we are using the WebpackMd5Hash plugin, the hashes will
// only change when the file contents change. This means we can
// set very aggressive caching strategies on our bundle output.
'[name]-[chunkhash].js',
// For any other bundle (typically a server/node) bundle we want a
// determinable output name to allow for easier importing/execution
// of the bundle by our scripts.
'[name].js',
),
// The name format for any additional chunks produced for the bundle.
chunkFilename: '[name]-[chunkhash].js',
// When targetting node we will output our bundle as a commonjs2 module.
libraryTarget: ifNode('commonjs2', 'var'),
// This is the web path under which our webpack bundled client should
// be considered as being served from.
publicPath: ifDev(
// As we run a seperate development server for our client and server
// bundles we need to use an absolute http path for the public path.
`http://${config('host')}:${config('clientDevServerPort')}${config('bundles.client.webPath')}`,
// Otherwise we expect our bundled client to be served from this path.
bundleConfig.webPath,
),
},
target: isClient
? // Only our client bundle will target the web as a runtime.
'web'
: // Any other bundle must be targetting node as a runtime.
'node',
// Ensure that webpack polyfills the following node features for use
// within any bundles that are targetting node as a runtime. This will be
// ignored otherwise.
node: {
__dirname: true,
__filename: true,
},
// Source map settings.
devtool: ifElse(
// Include source maps for ANY node bundle so that we can support
// nice stack traces for errors (the source maps get consumed by
// the `node-source-map-support` module to allow for this).
isNode ||
// Always include source maps for any development build.
isDev ||
// Allow for the following flag to force source maps even for production
// builds.
config('includeSourceMapsForOptimisedClientBundle'),
)(
// Produces an external source map (lives next to bundle output files).
'source-map',
// Produces no source map.
'hidden-source-map',
),
// Performance budget feature.
// This enables checking of the output bundle size, which will result in
// warnings/errors if the bundle sizes are too large.
// We only want this enabled for our production client. Please
// see the webpack docs on how you can configure this to your own needs:
// https://webpack.js.org/configuration/performance/
performance: ifProdClient(
// Enable webpack's performance hints for production client builds.
{ hints: 'warning' },
// Else we have to set a value of "false" if we don't want the feature.
false,
),
resolve: {
// These extensions are tried when resolving a file.
extensions: config('bundleSrcTypes').map(ext => `.${ext}`),
// This is required for the modernizr-loader
// @see https://github.com/peerigon/modernizr-loader
alias: {
modernizr$: path.resolve(appRootDir.get(), './.modernizrrc'),
},
},
// We don't want our node_modules to be bundled with any bundle that is
// targetting the node environment, prefering them to be resolved via
// native node module system. Therefore we use the `webpack-node-externals`
// library to help us generate an externals configuration that will
// ignore all the node_modules.
externals: removeNil([
ifNode(() =>
nodeExternals(
// Some of our node_modules may contain files that depend on our
// webpack loaders, e.g. CSS or SASS.
// For these cases please make sure that the file extensions are
// registered within the following configuration setting.
{
whitelist: removeNil([
// We always want the source-map-support included in
// our node target bundles.
'source-map-support/register',
])
// And any items that have been whitelisted in the config need
// to be included in the bundling process too.
.concat(config('nodeExternalsFileTypeWhitelist') || []),
},
)),
]),
plugins: removeNil([
// This grants us source map support, which combined with our webpack
// source maps will give us nice stack traces for our node executed
// bundles.
// We use the BannerPlugin to make sure all of our chunks will get the
// source maps support installed.
ifNode(
() =>
new webpack.BannerPlugin({
banner: 'require("source-map-support").install();',
raw: true,
entryOnly: false,
}),
),
// We use this so that our generated [chunkhash]'s are only different if
// the content for our respective chunks have changed. This optimises
// our long term browser caching strategy for our client bundle, avoiding
// cases where browsers end up having to download all the client chunks
// even though 1 or 2 may have only changed.
ifClient(() => new WebpackMd5Hash()),
// These are process.env flags that you can use in your code in order to
// have advanced control over what is included/excluded in your bundles.
// For example you may only want certain parts of your code to be
// included/ran under certain conditions.
//
// Any process.env.X values that are matched will be code substituted for
// the associated values below.
//
// For example you may have the following in your code:
// if (process.env.BUILD_FLAG_IS_CLIENT === 'true') {
// console.log('Foo');
// }
//
// If the BUILD_FLAG_IS_CLIENT was assigned a value of `false` the above
// code would be converted to the following by the webpack bundling
// process:
// if ('false' === 'true') {
// console.log('Foo');
// }
//
// When your bundle is built using the UglifyJsPlugin unreachable code
// blocks like in the example above will be removed from the bundle
// final output. This is helpful for extreme cases where you want to
// ensure that code is only included/executed on specific targets, or for
// doing debugging.
//
// NOTE: We are stringifying the values to keep them in line with the
// expected type of a typical process.env member (i.e. string).
// @see https://github.com/ctrlplusb/react-universally/issues/395
new webpack.EnvironmentPlugin({
// It is really important to use NODE_ENV=production in order to use
// optimised versions of some node_modules, such as React.
NODE_ENV: isProd ? 'production' : 'development',
// Is this the "client" bundle?
BUILD_FLAG_IS_CLIENT: JSON.stringify(isClient),
// Is this the "server" bundle?
BUILD_FLAG_IS_SERVER: JSON.stringify(isServer),
// Is this a node bundle?
BUILD_FLAG_IS_NODE: JSON.stringify(isNode),
// Is this a development build?
BUILD_FLAG_IS_DEV: JSON.stringify(isDev),
}),
// Generates a JSON file containing a map of all the output files for
// our webpack bundle. A necessisty for our server rendering process
// as we need to interogate these files in order to know what JS/CSS
// we need to inject into our HTML. We only need to know the assets for
// our client bundle.
ifClient(
() =>
new AssetsPlugin({
filename: config('bundleAssetsFileName'),
path: path.resolve(appRootDir.get(), bundleConfig.outputPath),
}),
),
// We don't want webpack errors to occur during development as it will
// kill our dev servers.
ifDev(() => new webpack.NoEmitOnErrorsPlugin()),
// We need this plugin to enable hot reloading of our client.
ifDevClient(() => new webpack.HotModuleReplacementPlugin()),
// For our production client we need to make sure we pass the required
// configuration to ensure that the output is minimized/optimized.
ifProdClient(
() =>
new webpack.LoaderOptionsPlugin({
minimize: true,
}),
),
// For our production client we need to make sure we pass the required
// configuration to ensure that the output is minimized/optimized.
ifProdClient(
() =>
new webpack.optimize.UglifyJsPlugin({
sourceMap: config('includeSourceMapsForOptimisedClientBundle'),
compress: {
screw_ie8: true,
warnings: false,
},
mangle: {
screw_ie8: true,
},
output: {
comments: false,
screw_ie8: true,
},
}),
),
// For the production build of the client we need to extract the CSS into
// CSS files.
ifProdClient(
() =>
new ExtractTextPlugin({
filename: '[name]-[chunkhash].css',
allChunks: true,
}),
),
// -----------------------------------------------------------------------
// START: HAPPY PACK PLUGINS
//
// @see https://github.com/amireh/happypack/
//
// HappyPack allows us to use threads to execute our loaders. This means
// that we can get parallel execution of our loaders, significantly
// improving build and recompile times.
//
// This may not be an issue for you whilst your project is small, but
// the compile times can be signficant when the project scales. A lengthy
// compile time can significantly impare your development experience.
// Therefore we employ HappyPack to do threaded execution of our
// "heavy-weight" loaders.
// HappyPack 'javascript' instance.
happyPackPlugin({
name: 'happypack-javascript',
// We will use babel to do all our JS processing.
loaders: [
{
path: 'babel-loader',
// We will create a babel config and pass it through the plugin
// defined in the project configuration, allowing additional
// items to be added.
query: config('plugins.babelConfig')(
// Our "standard" babel config.
{
// We need to ensure that we do this otherwise the babelrc will
// get interpretted and for the current configuration this will mean
// that it will kill our webpack treeshaking feature as the modules
// transpilation has not been disabled within in.
babelrc: false,
presets: [
// JSX
'react',
// Stage 3 javascript syntax.
// "Candidate: complete spec and initial browser implementations."
// Add anything lower than stage 3 at your own risk. :)
'stage-3',
// For our client bundles we transpile all the latest ratified
// ES201X code into ES5, safe for browsers. We exclude module
// transilation as webpack takes care of this for us, doing
// tree shaking in the process.
ifClient(['env', { es2015: { modules: false } }]),
// For a node bundle we use the specific target against
// babel-preset-env so that only the unsupported features of
// our target node version gets transpiled.
ifNode(['env', { targets: { node: true } }]),
].filter(x => x != null),
plugins: [
// Required to support react hot loader.
ifDevClient('react-hot-loader/babel'),
// This decorates our components with __self prop to JSX elements,
// which React will use to generate some runtime warnings.
ifDev('transform-react-jsx-self'),
// Adding this will give us the path to our components in the
// react dev tools.
ifDev('transform-react-jsx-source'),
// Replaces the React.createElement function with one that is
// more optimized for production.
// NOTE: Symbol needs to be polyfilled. Ensure this feature
// is enabled in the polyfill.io configuration.
ifProd('transform-react-inline-elements'),
// Hoists element creation to the top level for subtrees that
// are fully static, which reduces call to React.createElement
// and the resulting allocations. More importantly, it tells
// React that the subtree hasn’t changed so React can completely
// skip it when reconciling.
ifProd('transform-react-constant-elements'),
].filter(x => x != null),
},
buildOptions,
),
},
],
}),
// HappyPack 'css' instance for development client.
ifDevClient(() =>
happyPackPlugin({
name: 'happypack-devclient-css',
loaders: [
'style-loader',
{
path: 'css-loader',
// Include sourcemaps for dev experience++.
query: { sourceMap: true },
},
],
})),
// END: HAPPY PACK PLUGINS
// -----------------------------------------------------------------------
]),
module: {
rules: removeNil([
// JAVASCRIPT
{
test: /\.jsx?$/,
// We will defer all our js processing to the happypack plugin
// named "happypack-javascript".
// See the respective plugin within the plugins section for full
// details on what loader is being implemented.
loader: 'happypack/loader?id=happypack-javascript',
include: removeNil([
...bundleConfig.srcPaths.map(srcPath => path.resolve(appRootDir.get(), srcPath)),
ifProdClient(path.resolve(appRootDir.get(), 'src/html')),
]),
},
// CSS
// This is bound to our server/client bundles as we only expect to be
// serving the client bundle as a Single Page Application through the
// server.
ifElse(isClient || isServer)(
mergeDeep(
{
test: /\.css$/,
},
// For development clients we will defer all our css processing to the
// happypack plugin named "happypack-devclient-css".
// See the respective plugin within the plugins section for full
// details on what loader is being implemented.
ifDevClient({
loaders: ['happypack/loader?id=happypack-devclient-css'],
}),
// For a production client build we use the ExtractTextPlugin which
// will extract our CSS into CSS files. We don't use happypack here
// as there are some edge cases where it fails when used within
// an ExtractTextPlugin instance.
// Note: The ExtractTextPlugin needs to be registered within the
// plugins section too.
ifProdClient(() => ({
loader: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader'],
}),
})),
// When targetting the server we use the "/locals" version of the
// css loader, as we don't need any css files for the server.
ifNode({
loaders: ['css-loader/locals'],
}),
),
),
// ASSETS (Images/Fonts/etc)
// This is bound to our server/client bundles as we only expect to be
// serving the client bundle as a Single Page Application through the
// server.
ifElse(isClient || isServer)(() => ({
test: new RegExp(`\\.(${config('bundleAssetTypes').join('|')})$`, 'i'),
loader: 'file-loader',
query: {
// What is the web path that the client bundle will be served from?
// The same value has to be used for both the client and the
// server bundles in order to ensure that SSR paths match the
// paths used on the client.
publicPath: isDev
? // When running in dev mode the client bundle runs on a
// seperate port so we need to put an absolute path here.
`http://${config('host')}:${config('clientDevServerPort')}${config('bundles.client.webPath')}`
: // Otherwise we just use the configured web path for the client.
config('bundles.client.webPath'),
// We only emit files when building a web bundle, for the server
// bundle we only care about the file loader being able to create
// the correct asset URLs.
emitFile: isClient,
},
})),
// MODERNIZR
// This allows you to do feature detection.
// @see https://modernizr.com/docs
// @see https://github.com/peerigon/modernizr-loader
ifClient({
test: /\.modernizrrc.js$/,
loader: 'modernizr-loader',
}),
ifClient({
test: /\.modernizrrc(\.json)?$/,
loader: 'modernizr-loader!json-loader',
}),
]),
},
};
if (isProd && isClient) {
webpackConfig = withServiceWorker(webpackConfig, bundleConfig);
}
// Apply the configuration middleware.
return config('plugins.webpackConfig')(webpackConfig, buildOptions);
}