Last active
November 22, 2016 11:23
-
-
Save enten/0b35f2566e8a79d0c056e5bf738b9c2c to your computer and use it in GitHub Desktop.
Rewriting of react-universally/tools/webpack/configFactory.js with wcf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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