Skip to content

Instantly share code, notes, and snippets.

@mschipperheyn
Created March 2, 2017 15:53
Show Gist options
  • Save mschipperheyn/b34091863e96cdd50e38eb9210119bdc to your computer and use it in GitHub Desktop.
Save mschipperheyn/b34091863e96cdd50e38eb9210119bdc to your computer and use it in GitHub Desktop.
configFactory.js
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 isOptimize = optimize;
const isDev = !isOptimize;
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 ifOptimize = ifElse(isOptimize);
const ifNode = ifElse(isNode);
const ifClient = ifElse(isClient);
const ifDevClient = ifElse(isDev && isClient);
const ifOptimizeClient = ifElse(isOptimize && isClient);
console.log(`==> Creating ${isOptimize ? '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([
// Required to support hot reloading of our client.
ifDevClient(() => `webpack-hot-middleware/client?reload=true&path=http://${config('host')}:${config('clientDevServerPort')}/__webpack_hmr`),
// 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'),
// 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: ifOptimizeClient(
// 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: ifOptimizeClient(
// 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: mergeDeep(
{
modernizr$: path.resolve(appRootDir.get(), './.modernizrrc'),
},
// For our optimised builds we will alias to the optimised versions
// of React and ReactDOM.
ifOptimize({
react$: path.resolve(
appRootDir.get(), './node_modules/react/dist/react.min.js',
),
'react-dom$': path.resolve(
appRootDir.get(), './node_modules/react-dom/dist/react-dom.min.js',
),
'react-dom/server$': path.resolve(
appRootDir.get(), './node_modules/react-dom/dist/react-dom-server.min.js',
),
}),
),
},
// 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',
// We want react bundled with our node bundles for the optimised
// builds as we are going to resolve to the optmised versions
// of react via the webpack alias configuration.
ifOptimize('react'),
ifOptimize('react-dom'),
ifOptimize('react-dom/server'),
])
// 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 special 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.
//
// The DefinePlugin is used by webpack to substitute any code
// that matches the property keys of the object you provide it below with
// the given value that has been assigned to each respective property.
//
// 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: You may be used to having to do NODE_ENV = production here to
// get optimized React/ReactDOM builds. Almost every blog and example
// will tell you to do this. I have decided against this model as it
// often confused me when I was passing custom NODE_ENV values
// such as "staging" / "test" to my scripts. Therefore to avoid any
// confusion we instead use the webpack alias feature to target the
// pre-optimised dist versions of React/ReactDOM when required.
new webpack.DefinePlugin({
// Is this the "client" bundle?
'process.env.BUILD_FLAG_IS_CLIENT': JSON.stringify(isClient),
// Is this the "server" bundle?
'process.env.BUILD_FLAG_IS_SERVER': JSON.stringify(isServer),
// Is this a node bundle?
'process.env.BUILD_FLAG_IS_NODE': JSON.stringify(isNode),
// Is this a development build?
'process.env.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.
ifOptimizeClient(
() => 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.
ifOptimizeClient(
() => 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.
ifOptimizeClient(
() => 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-0',
// 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(['latest', { es2015: { modules: false } }]),
// For a node bundle we use the awesome babel-preset-env which
// acts like babel-preset-latest in that it supports the latest
// ratified ES201X syntax, however, it will only transpile what
// is necessary for a target environment. We have configured it
// to target our current node version. This is cool because
// recent node versions have extensive support for ES201X syntax.
// Also, we have disabled modules transpilation as webpack will
// take care of that for us ensuring tree shaking takes place.
// NOTE: Make sure you use the same node version for development
// and production.
ifNode(['env', { targets: { node: true }, modules: false }]),
].filter(x => x != null),
plugins: [
'transform-class-properties',
'syntax-decorators',
'transform-decorators-legacy',
// 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.
ifOptimize('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.
ifOptimize('transform-react-constant-elements'),
].filter(x => x != null),
env: {
production: {
presets: [
ifOptimize('react-optimize'),
].filter(x => x != null)
}
},
},
buildOptions,
),
}],
}),
// HappyPack 'css' instance for development client.
ifDevClient(
() => happyPackPlugin({
name: 'happypack-devclient-css',
loaders: [
'style-loader',
{
path: 'css-loader?modules=1&localIdentName=[hash:base64:5]',
// 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),
),
ifOptimizeClient(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|scss)$/,
},
// 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.
ifOptimizeClient(() => ({
loader: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'sass-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 (isOptimize && isClient) {
webpackConfig = withServiceWorker(webpackConfig, bundleConfig);
}
// Apply the configuration middleware.
return config('plugins.webpackConfig')(webpackConfig, buildOptions);
}
{
"name": "react-universally",
"version": "13.0.0",
"description": "A starter kit for universal react applications.",
"main": "build/server/index.js",
"engines": {
"node": ">=6"
},
"scripts": {
"analyze:client": "babel-node internal/scripts/analyze --client",
"analyze:server": "babel-node internal/scripts/analyze --server",
"build": "babel-node internal/scripts/build --optimize",
"build:dev": "babel-node internal/scripts/build",
"clean": "babel-node internal/scripts/clean",
"deploy": "babel-node internal/scripts/deploy",
"develop": "babel-node internal/development",
"lint": "eslint client server shared config internal",
"preinstall": "node internal/scripts/preinstall",
"start": "node build/server",
"test": "jest",
"test:coverage": "jest --coverage",
"postinstall": "yarn run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ctrlplusb/react-universally.git"
},
"keywords": [
"react",
"boilerplate",
"starter kit",
"universal",
"javascript",
"express",
"webpack"
],
"contributors": [
{
"name": "Alin Porumb",
"url": "https://github.com/alinporumb"
},
{
"name": "Benjamin Kniffler",
"url": "https://github.com/bkniffler"
},
{
"name": "Carson Perrotti",
"url": "https://github.com/carsonperrotti"
},
{
"name": "Christian Glombek",
"url": "https://github.com/LorbusChris"
},
{
"name": "Christoph Werner",
"url": "https://github.com/codepunkt"
},
{
"name": "David Edmondson",
"url": "https://github.com/threehams"
},
{
"name": "Dion Dirza",
"url": "https://github.com/diondirza"
},
{
"name": "Evgeny Boxer",
"url": "https://github.com/evgenyboxer"
},
{
"name": "Joe Kohlmann",
"url": "https://github.com/kohlmannj"
},
{
"name": "Lucian Lature",
"url": "https://github.com/lucianlature"
},
{
"name": "Steven Enten",
"url": "https://github.com/enten"
},
{
"name": "Sean Matheson",
"url": "https://github.com/ctrlplusb"
},
{
"name": "Steven Truesdell",
"url": "https://github.com/strues"
},
{
"name": "Tom",
"url": "https://github.com/datoml"
}
],
"license": "MIT",
"bugs": {
"url": "https://github.com/ctrlplusb/react-universally/issues"
},
"homepage": "https://github.com/ctrlplusb/react-universally#readme",
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx}"
],
"snapshotSerializers": [
"<rootDir>/node_modules/enzyme-to-json/serializer"
],
"testPathIgnorePatterns": [
"<rootDir>/(build|internal|node_modules|flow-typed|public)/"
]
},
"dependencies": {
"app-root-dir": "1.0.2",
"classnames": "^2.2.5",
"colors": "1.1.2",
"compression": "1.6.2",
"dotenv": "4.0.0",
"express": "4.14.1",
"flexboxgrid": "^6.3.1",
"helmet": "3.4.0",
"hpp": "0.2.1",
"material-ui": "^0.17.0",
"modernizr": "3.3.1",
"normalize.css": "5.0.0",
"offline-plugin": "4.6.1",
"react": "15.4.2",
"react-async-component": "0.2.2",
"react-dom": "15.4.2",
"react-flexbox-grid": "^0.10.2",
"react-helmet": "4.0.0",
"react-intl": "^2.2.3",
"react-intl-redux": "^0.4.1",
"react-redux": "^5.0.3",
"react-router-dom": "4.0.0-beta.6",
"react-waypoint": "^5.1.0",
"redux": "^3.6.0",
"redux-form": "^6.5.0",
"redux-thunk": "^2.2.0",
"serialize-javascript": "1.3.0",
"styled-components": "2.0.0-5",
"uuid": "3.0.1"
},
"devDependencies": {
"assets-webpack-plugin": "3.5.1",
"babel-cli": "6.23.0",
"babel-core": "6.23.1",
"babel-eslint": "7.1.1",
"babel-jest": "18.0.0",
"babel-loader": "6.3.2",
"babel-plugin-syntax-decorators": "^6.13.0",
"babel-plugin-transform-class-properties": "^6.23.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-react-constant-elements": "6.23.0",
"babel-plugin-transform-react-inline-elements": "6.22.0",
"babel-plugin-transform-react-jsx-self": "6.22.0",
"babel-plugin-transform-react-jsx-source": "6.22.0",
"babel-polyfill": "6.23.0",
"babel-preset-env": "1.1.8",
"babel-preset-es2015": "^6.22.0",
"babel-preset-latest": "6.22.0",
"babel-preset-react": "6.23.0",
"babel-preset-stage-0": "^6.22.0",
"babel-preset-stage-3": "6.22.0",
"babel-template": "6.23.0",
"chokidar": "1.6.1",
"css-loader": "0.26.1",
"enzyme": "2.7.1",
"enzyme-to-json": "1.4.5",
"eslint": "3.15.0",
"eslint-config-airbnb": "14.1.0",
"eslint-plugin-flowtype": "2.30.0",
"eslint-plugin-import": "2.2.0",
"eslint-plugin-jsx-a11y": "4.0.0",
"eslint-plugin-react": "6.10.0",
"extract-text-webpack-plugin": "2.0.0-rc.3",
"file-loader": "0.10.0",
"glob": "7.1.1",
"happypack": "3.0.2",
"html-webpack-plugin": "2.28.0",
"jest": "18.1.0",
"md5": "2.2.1",
"modernizr-loader": "1.0.1",
"node-notifier": "5.0.2",
"node-sass": "^4.5.0",
"react-addons-test-utils": "15.4.2",
"regenerator-runtime": "0.10.3",
"rimraf": "2.6.0",
"sass-loader": "^6.0.2",
"semver": "5.3.0",
"source-map-support": "0.4.11",
"style-loader": "0.13.1",
"webpack": "2.2.1",
"webpack-bundle-analyzer": "2.3.0",
"webpack-dev-middleware": "1.10.1",
"webpack-hot-middleware": "2.17.0",
"webpack-md5-hash": "0.0.5",
"webpack-node-externals": "1.5.4"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment