Skip to content

Instantly share code, notes, and snippets.

@enten
Last active November 22, 2016 11:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save enten/0b35f2566e8a79d0c056e5bf738b9c2c to your computer and use it in GitHub Desktop.
Save enten/0b35f2566e8a79d0c056e5bf738b9c2c to your computer and use it in GitHub Desktop.
Rewriting of react-universally/tools/webpack/configFactory.js with wcf
var wcf = require('wcf')
var {basename, resolve} = require('path')
// mocks >
var globSync = () => []
var happyPackPlugin = (x) => x
// < mocks
var f = wcf.create()
// We have to set this to be able to use these items when executing a
// server bundle. Otherwise strangeness happens, like __dirname resolving
// to '/'. There is no effect on our client bundle.
f._node({
__dirname: true,
__filename: true
})
// We want to be able to get nice stack traces when running our server
// bundle. To fully support this we'll also need to configure the
// `node-source-map-support` module to execute at the start of the server
// bundle. This module will allow the node to make use of the
// source maps.
// We also want to be able to link to the source in chrome dev tools
// whilst we are in development mode. :)
f.devtool('source-map')
// When in production client mode we don't want any source maps to
// decrease our payload sizes.
// This form has almost no cost.
f.web.prod.devtool('hidden-source-map')
// Anything listed in externals will not be included in our bundle.
//
// Don't allow the server to bundle the universal middleware bundle. We
// want the server to natively require it from the build dir.
f.server.externals(/\.\.[/\\]universalMiddleware/)
f.server.dev.externals(/development[/\\]universalDevMiddleware/)
// We don't want our node_modules to be bundled with our server package,
// prefering them to be resolved via native node module system. Therefore
// we use the `webpack-node-externals` library to help us generate an
// externals config that will ignore all node_modules.
f.node.externalsWith(() => require('webpack-node-externals')({
// NOTE: !!!
// However the node_modules may contain files that will rely on our
// webpack loaders in order to be used/resolved, for example CSS or
// SASS. For these cases please make sure that the file extensions
// are added to the below list. We have added the most common formats.
whitelist: [
/\.(eot|woff|woff2|ttf|otf)$/,
/\.(svg|png|jpg|jpeg|gif|ico)$/,
/\.(mp4|mp3|ogg|swf|webp)$/,
/\.(css|scss|sass|sss|less)$/
]
}))
// Define our entry chunks for our bundle.
f.web.dev.entry.index('react-hot-loader/patch')
f.web.dev.entry.indexWith(({envVars}) =>
`webpack-hot-middleware/client?reload=true&path=http://localhost:${envVars.CLIENT_DEVSERVER_PORT}/__webpack_hmr`)
// We are using polyfill.io instead of the very heavy babel-polyfill.
// Therefore we need to add the regenerator-runtime as the babel-polyfill
// included this, which polyfill.io doesn't include.
f.web.entry.index('regenerator-runtime/runtime')
f.entry.indexWith(({appRootPath, target}) =>
resolve(appRootPath, `./src/${target}/index.js`))
// The dir in which our bundle should be output.
f.output.pathWith(({appRootPath, target, envVars}) =>
resolve(appRootPath, envVars.BUNDLE_OUTPUT_PATH, `./${target}`))
// The filename format for our bundle's entries.
f.web.output.filename(
// We include a hash for client caching purposes. Including a unique
// has for every build will ensure browsers always fetch our newest
// bundle.
'[name]-[chunkhash].js',
// We want a determinable file name when running our server bundles,
// as we need to be able to target our server start file from our
// npm scripts. We don't care about caching on the server anyway.
// We also want our client development builds to have a determinable
// name for our hot reloading client bundle server.
'[name].js'
)
f.output.chunkFilename('[name]-[chunkhash].js')
// This is the web path under which our webpack bundled output should
// be considered as being served from.
f.dev.output.publicPathWith(
({envVars}) =>
// As we run a seperate server for our client and server bundles we
// need to use an absolute http path for our assets public path.
`http://localhost:${envVars.CLIENT_DEVSERVER_PORT}${envVars.CLIENT_BUNDLE_HTTP_PATH}`,
// Otherwise we expect our bundled output to be served from this path.
({envVars}) =>
envVars.CLIENT_BUNDLE_HTTP_PATH
)
// When in server mode we will output our bundle as a commonjs2 module.
f.node.output.libraryTarget('commonjs2', 'var')
// These extensions are tried when resolving a file.
f.resolve.extensions([
'.js',
'.jsx',
'.json'
])
f.pluginsWith((context, meta) => {
const CodeSplitPlugin = require('code-split-component/webpack')
return new CodeSplitPlugin({
// The code-split-component doesn't work nicely with hot module reloading,
// which we use in our development builds, so we will disable it (which
// ensures synchronously behaviour on the CodeSplit instances).
disabled: !f.prod.$hasSomeMeta(meta)
})
})
// 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.
f.web.pluginsWith(() => {
const WebpackMd5Hash = require('webpack-md5-hash')
return new WebpackMd5Hash
})
// The DefinePlugin is used by webpack to substitute any patterns that it
// finds within the code with the respective value assigned below.
//
// For example you may have the following in your code:
// if (process.env.NODE_ENV === 'development') {
// console.log('Foo');
// }
//
// If we assign the NODE_ENV variable in the DefinePlugin below a value
// of 'production' webpack will replace your code with the following:
// if ('production' === 'development') {
// console.log('Foo');
// }
//
// This is very useful as we are compiling/bundling our code and we would
// like our environment variables to persist within the code.
//
// At the same time please be careful with what environment variables you
// use in each respective bundle. For example, don't accidentally
// expose a database connection string within your client bundle src!
f.pluginsWith(({envVars}, meta) => {
const {DefinePlugin} = require('webpack')
const mode = f.prod.$hasSomeMeta(meta) ? 'production' : 'development'
const defines = {}
Object.keys(envVars)
.map((key) => [key, envVars[key]])
// NOTE: The NODE_ENV key is especially important for production
// builds as React relies on process.env.NODE_ENV for optimizations.
.concat([['NODE_ENV', mode]])
// Feel free to add any "dynamic" environment variables, to be
// created by this webpack script. Below I am adding a "IS_NODE"
// environment variable which will allow our code to know if it's
// being bundled for a node target.
.concat([['IS_NODE', f.node.$hasSomeMeta(meta)]])
// Now we will expose all of our environment variables to webpack
// so that it can make all the subtitutions for us.
// Note: ALL of these values will be given as string types, therefore
// you may need to do operations like the following within your src:
// const MY_NUMBER = parseInt(process.env.MY_NUMBER, 10);
// const MY_BOOL = process.env.MY_BOOL === 'true';
.forEach(([key, val]) => defines[`process.env.${key}`] = JSON.stringify(val))
return new DefinePlugin(defines)
})
f.web.pluginsWith(({appRootPath, envVars, target}) => {
const AssetsPlugin = require('assets-webpack-plugin')
return new AssetsPlugin({
// 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.
filename: envVars.BUNDLE_ASSETS_FILENAME,
path: resolve(appRootPath, envVars.BUNDLE_OUTPUT_PATH, `./${target}`),
})
})
// We don't want webpack errors to occur during development as it will
// kill our dev servers.
f.dev.pluginsWith(() => {
const {NoErrorsPlugin} = require('webpack')
return new NoErrorsPlugin
})
// We need this plugin to enable hot module reloading for our dev server.
f.web.dev.pluginsWith(() => {
const {HotModuleReplacementPlugin} = require('webpack')
return new HotModuleReplacementPlugin
})
// Adds options to all of our loaders.
f.web.prod.pluginsWith(() => {
const {LoaderOptionsPlugin} = require('webpack')
return new LoaderOptionsPlugin({
// Indicates to our loaders that they should minify their output
// if they have the capability to do so.
minimize: true,
// Indicates to our loaders that they should enter into debug mode
// should they support it.
debug: false,
})
})
// JS Minification.
f.web.prod.pluginsWith(() => {
const {UglifyJsPlugin} = require('webpack').optimize
return new UglifyJsPlugin({
// sourceMap: true,
compress: {
screw_ie8: true,
warnings: false,
},
mangle: {
screw_ie8: true,
},
output: {
comments: false,
screw_ie8: true,
},
})
})
// This is actually only useful when our deps are installed via npm2.
// In npm2 its possible to get duplicates of dependencies bundled
// given the nested module structure. npm3 is flat, so this doesn't
// occur.
f.web.prod.pluginsWith(() => {
const {DedupePlugin} = require('webpack').optimize
return new DedupePlugin
})
// This is a production client so we will extract our CSS into
// CSS files.
f.web.prod.pluginsWith(() => {
const ExtractTextPlugin = require('extract-text-webpack-plugin')
return new ExtractTextPlugin({
filename: '[name]-[chunkhash].css',
allChunks: true
})
})
// Service Worker.
// @see https://github.com/goldhand/sw-precache-webpack-plugin
// This plugin generates a service worker script which as configured below
// will precache all our generated client bundle assets as well as the
// index page for our application.
// This gives us aggressive caching as well as offline support.
// Don't worry about cache invalidation. As we are using the Md5HashPlugin
// for our assets, any time their contents change they will be given
// unique file names, which will cause the service worker to fetch them.
f.web.prod.pluginsWith(({appRootPath, appName, envVars, json}) => {
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin')
const clientBundleAssets = globSync(resolve(appRootPath, envVars.BUNDLE_OUTPUT_PATH, './client/*.js'))
const dynamicUrlToDependencies = globSync(resolve(appRootPath, './public/*'))
.reduce((acc, cur) => {
// We will precache our public asset, with it being invalidated
// any time our client bundle assets change.
acc[`/${basename(cur)}`] = clientBundleAssets // eslint-disable-line no-param-reassign,max-len
return acc;
},
{
// Our index.html page will be precatched and it will be
// invalidated and refetched any time our client bundle
// assets change.
'/': clientBundleAssets,
// Lets cache the call to the polyfill.io service too.
'https://cdn.polyfill.io/v2/polyfill.min.js': clientBundleAssets,
})
// Note: The default cache size is 2mb. This can be reconfigured:
// maximumFileSizeToCacheInBytes: 2097152,
const swPluginOpts = {
cacheId: `${appName}-sw`,
filepath: resolve(envVars.BUNDLE_OUTPUT_PATH, './serviceWorker/sw.js'),
dynamicUrlToDependencies
}
// When outputing a json stat file we want to silence the output.
if (!!json) {
swPluginOpts.verbose = false
swPluginOpts.logger = () => {}
}
return new SWPrecacheWebpackPlugin(swPluginOpts)
})
// HappyPack 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.
f.pluginsWith((context, meta) => {
// We will use babel to do all our JS processing.
const babelLoader = {
path: 'babel',
query: {
presets: [
// JSX
'react',
// All the latest JS goodies, except for ES6 modules which
// webpack has native support for and uses in the tree shaking
// process.
// TODO: When babel-preset-latest-minimal has stabilised use it
// for our node targets so that only the missing features for
// our respective node version will be transpiled.
['latest', { es2015: { modules: false } }],
],
plugins: [
// We are adding the experimental "object rest spread" syntax as
// it is super useful. There is a caviat with the plugin that
// requires us to include the destructuring plugin too.
'transform-object-rest-spread',
'transform-es2015-destructuring',
// The class properties plugin is really useful for react components.
'transform-class-properties',
// This plugin transpiles the code-split-component component
// instances, taking care of all the heavy boilerplate that we
// would have had to do ourselves to get code splitting w/SSR
// support working.
// @see https://github.com/ctrlplusb/code-split-component
[
'code-split-component/babel',
{
// The code-split-component doesn't work nicely with hot
// module reloading, which we use in our development builds,
// so we will disable it (which ensures synchronously
// behaviour on the CodeSplit instances).
disabled: !f.prod.$hasSomeMeta(meta),
// When a node target (i.e. a server rendering bundle) then
// we will set the role as being server which will ensure that
// our code split components are resolved synchronously.
role: f.web.$hasSomeMeta(meta) ? 'client' : 'server',
},
]
]
}
}
if (f.web.dev.$hasSomeMeta(meta)) {
babelLoader.query.plugins.unshift('react-hot-loader/babel')
}
return happyPackPlugin({
name: 'happypack-javascript',
loaders: [
babelLoader
]
})
})
// HappyPack 'css' instance for development client.
f.web.dev.pluginsWith(() => happyPackPlugin({
name: 'happypack-devclient-css',
// We will use a straight style & css loader along with source maps.
// This combo gives us a better development experience.
loaders: [
'style-loader',
{ path: 'css-loader', query: { sourceMap: true } },
],
}))
// Javascript
f.module.rulesWith(({appRootPath}) => ({
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: [resolve(appRootPath, './src')],
}))
// 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.
f.web.dev.module.rules({
test: /\.css$/,
loaders: ['happypack/loader?id=happypack-devclient-css'],
})
// For a production client build we use the ExtractTextPlugin which
// will extract our CSS into CSS files.
// The plugin needs to be registered within the plugins section too.
// Also, as we are using the ExtractTextPlugin we can't use happypack
// for this case.
f.web.prod.module.rulesWith(() => {
const ExtractTextPlugin = require('extract-text-webpack-plugin')
return {
test: /\.css$/,
loader: ExtractTextPlugin.extract({
fallbackLoader: 'style-loader',
loader: '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.
f.node.module.rules({
test: /\.css$/,
loaders: ['css-loader/locals'],
})
// JSON
f.module.rules({
test: /\.json$/,
loader: 'json-loader',
})
// Images and Fonts
f.module.rulesWith((context, meta) => ({
test: /\.(jpg|jpeg|png|gif|ico|eot|svg|ttf|woff|woff2|otf)$/,
loader: 'url-loader',
query: {
// Any file with a byte smaller than this will be "inlined" via
// a base64 representation.
limit: 10000,
// We only emit files when building a client bundle, for the server
// bundles this will just make sure any file imports will not fall
// over.
emitFile: f.web.$hasSomeMeta(meta)
}
}))
// -----
const context = {
appName: 'react-universally-app',
appRootPath: '.',
target: 'client',
json: false,
envVars: {
CLIENT_DEVSERVER_PORT: 7331,
BUNDLE_OUTPUT_PATH: './build',
CLIENT_BUNDLE_HTTP_PATH: '/client/',
BUNDLE_ASSETS_FILENAME: 'assets.json'
}
}
console.log('-------')
console.log('CLIENT:')
console.log('-------')
console.dir(
f.web.prod.$build(context)
// f.web.dev.$build(context)
, {depth: null, colors: true})
console.log()
console.log('-------')
console.log('NODE:')
console.log('-------')
console.dir(
f.node.prod.$build(context)
// f.node.dev.$build(context)
, {depth: null, colors: true})
console.log()
console.log('-------')
console.log('SERVER:')
console.log('-------')
console.dir(
f.server.prod.$build(context)
// f.server.dev.$build(context)
, {depth: null, colors: true})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment