Remix’s Tech Stack: Webpack

This is the first in a series about Remix’s tech stack. Over the last few months we’ve worked on setting up our ideal stack, and we’re proud of what we’ve built. Now it’s time to share it.

We’ll start with our front-end build tools. We wanted to use the latest Javascript features without worrying about older browsers (Babel), confidence when changing CSS by scoping classes (CSS Modules), immediate feedback when changing code (React Hot Loader), and so on. All of this is easy to set up when using Webpack, which is why we switched to that. In this article we’ll look at how we’ve set this up.

Webpack config

There are a few ways we want Webpack to behave, based on the context. The first is dev-server vs static build.

  • When developing we use the webpack-dev-server, which caches files in memory and serves them from a web server, which in turn saves time. It also supports hot module loading.
  • When building on the Continuous Integration, or CI, server (we use CircleCI), or when deploying using Heroku, we want to generate a static build, so we can host them statically in production. That way we can set proper caching headers.

We also use both minified and non-minified versions of our builds, depending on whether it’s in production or still in development.

  • In production, we want a gzipped and minified build, so it downloads and parses faster. We also enable all React optimisations, so it runs faster. Sometimes we also want to do this when developing locally, when measuring performance.
  • Usually though, we want to disable minifying when developing. This way development is faster, as minifying is an additional step. We also want to get all of React’s warnings when developing.

To set this context, we use two environment variables, which we use like this in our package.json:

"scripts": {
"build": "webpack",
"build-min": "WEBPACK_MINIFY=true npm run build",
"dev-server": "WEBPACK_DEV_SERVER=true webpack-dev-server",
"dev-server-min": "WEBPACK_MINIFY=true npm run dev-server"
},

In our webpack.config.js we first set up some common stuff:

const config = {
plugins: [ /* Some plugins here. */ ],
module: {
loaders: [ /* Some loaders here. */ ],
},
entry: {
build: ['client/build'], // Our main entry point.
tests: ['client/tests'], // Jasmine unit tests.
happo: ['client/happo'], // Happo screenshot tests.
},
resolve: {
alias: {
client: path.resolve('./client'),
},
},
};

Nothing too exciting here. All our code is in the client directory, plus node_modules for libraries (which Webpack finds by default). We alias the client directory, so we can easily refer to it. We have three entry points, one for our app (client/build.js) and two for tests (which we’ll get into in a later article).

We then look at if we’re minifying or not:

if (process.env.WEBPACK_MINIFY) {
config.plugins.push(new webpack.DefinePlugin({
'process.env': {
// Disable React warnings and assertions.
'NODE_ENV': JSON.stringify('production'),
},
'__DEV__': false, // For internal use.
}));
config.plugins.push(new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false },
}));
} else {
// Only use source maps when not minifying.
config.devtool = 'eval-source-map';
config.plugins.push(new webpack.DefinePlugin({
'__DEV__': true,
}));
}

If we’re minifying, we want to set NODE_ENV to “production”, as React uses this to strip away all sorts of assertions and warnings, making it faster. When not minifying we enable source maps.

Then we have setup specific for when using the dev-server:

if (process.env.WEBPACK_DEV_SERVER) {
// Development configuration, assumes this is loaded with
// webpack-dev-server, running on port 8080 (default).
// Hot loading for build.js.
config.devServer = { noInfo: true, host: '0.0.0.0', hot: true};
config.entry.build.unshift(
'webpack-dev-server/client?http://localhost:8080');
config.entry.build.unshift('webpack/hot/dev-server');
config.plugins.push(new webpack.HotModuleReplacementPlugin());
config.plugins.push(new webpack.NoErrorsPlugin());
// React components hot loading.
config.module.loaders.unshift({
test: /\.js$/,
include: path.resolve(__dirname, 'client/components'),
loader: 'react-hot',
});
// Expose Jasmine test page as index page on http://localhost:8080
config.plugins.push(new JasmineWebpackPlugin({
htmlOptions: {
chunks: ['tests'],
filename: 'index.html',
},
}));
config.output = {
publicPath: 'http://localhost:8080/',
filename: '[name].js',
// In case this is run without webpack-dev-server.
path: 'public/client',
};
}
  • We initialise the dev-server with noInfo, making it less verbose, and bind it to 0.0.0.0 so you can access it from other machines on the network (useful for debugging).
  • We enable Hot Module Replacement, per instructions here. We also enable the React Hot Loader, but only on actual React components (client/components).
  • Then we host the Jasmine index page on the same port, so you can just navigate there to run the tests.
  • Finally we set config.output to something simple, so you can easily view what is generated at http://localhost:8080/build.js. In case we run this without the dev-server (which should typically not happen), we write to where other static files are written.

If we’re not running the dev-server, we’re writing to disk:

else {
// Static configuration, outputs to public/client.
// For use with Heroku/CircleCI.
if (process.env.WEBPACK_MINIFY) {
// Gzip.
config.plugins.push(new CompressionPlugin());

// Generate stats.html.
config.plugins.push(new StatsPlugin('stats.json'));
config.plugins.push(new Visualizer());
}
config.output = {
path: 'public/client',
publicPath: '/client/',
// Unique filenames (for caching).
filename: '[id].[name].[chunkhash].js',
};
}
  • When minifying, we gzip all files (which Rails static asset hosting automatically uses), and we generate a file usage visualisation, which we serve on our internal development pages.
  • We also generate unique filenames for each build, so we can serve them with infinite caching headers.
The file usage visualisation helps us debug increases in the bundle size

Finally we speed up deploys a bit by leaving out tests when deploying on Heroku:

// Don't build tests when deploying on Heroku.
if (process.env.HEROKU_APP_ID) {
delete config.entry.tests;
delete config.entry.happo;
}

Serving from Rails

Let’s now look at the plugins part of our Webpack config. It looks like this:

plugins: [
new CircularDependencyPlugin(),
new ManifestPlugin(),
],

The first one is to prevent circular dependencies, which can be a pain to debug in Webpack. The second one generates a manifest.json file, which looks something like this:

{
"build.js": "0.build.fc79868b1fdedc95cd1f.js",
"happo.js": "1.happo.d44f048f66291e0e73ca.js",
"tests.js": "2.tests.305167cc0309faddc140.js"
}

We use this in our Rails application to serve the right assets. For this we have created a helper file, assets_helper.rb:

# Adds `webpack_include_tag`.
module AssetsHelper
def webpack_include_tag(filename)
if Rails.application.config.use_webpack_dev_server
# Assumes that Webpack is configured with
# config.output.filename = '[name].js'.
return javascript_include_tag(root_url(port: 8080) + filename)
end
webpack_filename = webpack_manifest[filename]
if webpack_filename
javascript_include_tag("/client/#{webpack_filename}")
else
raise ArgumentError, "Webpack file not found: #{filename}"
end
end
private def webpack_manifest
@webpack_manifest ||= JSON.load(Rails.root.join(
'public', 'client', 'manifest.json'))
end
end

This allows us to call webpack_include_tag(‘build.js’) in templates, and have it use the filename including the hash. Now we can tell the browser to cache these files forever, as they will have a different filename if they ever change.

Note that when use_webpack_dev_server is enabled, we point to the dev-server. We set this variable to true in development, to false in staging and production, and in test.rb we set:

config.use_webpack_dev_server = !ENV['CI']

Preprocessing using Loaders

Finally, we have a bunch of Webpack loaders. This is what that looks like in the Webpack config:

module: {
loaders: [
{
test: /\.js$/,
include: path.resolve('./client'),
loader: 'babel',
query: {
cacheDirectory: '.babel-cache',
// For code coverage.
plugins: (!process.env.WEBPACK_DEV_SERVER &&
!process.env.WEBPACK_MINIFY) ? ['istanbul'] : [],
},
},
{
test: /\.less$/,
include: path.resolve('./client'),
loaders: [
// Inject into HTML (bundles it in JS).
'style',
// Resolves url() and :local().
'css?localIdentName=[path][name]--[local]--[hash:base64:10]',
// Autoprefixer (see below at `postcss()`).
'postcss-loader',
// LESS preprocessor.
'less',
],
},
{
test: /\.(jpe?g|png|gif)$/i,
include: path.resolve('./client'),
loaders: [
// Inline small images, otherwise create file.
'url?limit=10000',
// Minify images.
'img?progressive=true',
],
},
{
test: /\.(geo)?json$/,
include: [
path.resolve('./client'),
path.resolve('./spec'), // Shared fixtures.
],
loader: 'json',
},
{
test: /\.svg$/,
loader: 'raw',
},
],
},
postcss() {
return [autoprefixer];
},
  • First, the Babel loader. This allows us to use the latest Javascript features without having to worry about browser support (in conjunction with babel-polyfill). We use Istanbul to track code coverage for tests, and we set an explicit cache path so the CI can keep this cache between builds. This is what that looks like for CircleCI, in circle.yml:
dependencies:
cache_directories:
- ".babel-cache"
  • Next is CSS. We use LESS for preprocessing, although we’re thinking of switching to PostCSS, which uses future CSS standards. We already use PostCSS for auto-prefixing. We also use CSS Modules by setting css?localIdentName, which lets you scope CSS classes to only the file that uses them.
  • Images are being inlined if they are a small file, otherwise they get loaded separately. They are also minified.
  • JSON and SVG are straightforward. We import SVG as text, which we use with an <Svg> component that looks like this:
// Use a tool like https://jakearchibald.github.io/svgomg/
// to slim down the SVG, and then manually
// remove width/height/fill/stroke.
const Svg = React.createClass({
propTypes: {
height: React.PropTypes.number,
offset: React.PropTypes.number,
svg: React.PropTypes.string.isRequired,
width: React.PropTypes.number.isRequired,
},
render() {
return (
<div
className={styles.root}
dangerouslySetInnerHTML={{ __html: this.props.svg }}
style={{
height: this.props.height || this.props.width,
width: this.props.width,
top: this.props.offset,
}}
/>
);
},
});
export default Svg;

Which then gets used like this:

<Svg svg={require('./pencil.svg')} width={14} offset={2} />

Conclusion

This is just a small part of our stack, but it took a while to get right. After all, it’s the small things that make a difference, such as being able to run a minified version with all React optimisations in development, or persisting Babel’s cache between CI runs, or not building tests when deploying.

Hopefully this is useful to get started with Webpack, or to tune your existing setup. Keep an eye out for next editions in this series, in which we’ll talk about our components library, unit testing, screenshot testing, deploying, keeping the CI fast, and more!

Written by

Materialistic minimalist. Optimistic realist. Creative copycat. Rationalises believing in paradoxes.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store