A strong Webpack configuration (part 1)

You will find the code of this article on this repository.

Problem

Webpack is a very complete and complex tool, thus configuring a maintenable and strong development environment can be a real hell.

Requirements

  • Production ready builds. I need to be able to generate minified and optimized files ready to run in production.
  • Development builds, with live reload, and fast complilation.
  • Unit test builds. I need to run tests with KarmaJs, so I need to provide KarmaJs a Webpack configuration.
  • I need to be able to create some serverside logic for API mocking.

The standard webpack.config.js file shall be available at the root of the project with the correct configuration.

Obviously, all this configuration have to be easily maintenable and we must avoid redundance between them.

Solution

Configurations

We need to build 3 differents configuration, but we will do that in a dynamic way so common pieces will be shared between all the configrations (remember : never write twice the same code)

For that we will use a nice npm plugin called webpack-config. This plugin allow easy and smart configuration merging.

Quick overview of webpack-config api :

More on the official page

new WebpackConfig() creates a blank base configuration

.merge({some config}) smartly merge an existing (or a blank) configuration with a new one. The new one override the existing.

.extend(relative path to existing config) allow us to extend an existing configuration.

Let's start by creating a global configuration file, whish will contain all configuration items shared by all elements :

// file : /config/webpack-base.config.js

const webpack = require('webpack')  
const ExtractTextPlugin = require('extract-text-webpack-plugin')  
const WebpackConfig = require('webpack-config')  
const path = require('path')

module.exports = new WebpackConfig().merge({  
  entry: {
    app: ['./src/app/app.js']
  },
  output: {
    path: path.resolve(__dirname, '../build'),
    filename: 'app/app.js',
    library: 'App',
    libraryTarget: 'umd' // others: var, this, commonjs, commonjs2, amd, umd
  },
  plugins: [
    new webpack.NoErrorsPlugin(),
    new ExtractTextPlugin('app.css', { allChunks: true })
  ],
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loaders: ['babel']
      },
      {
        test: /\.html$/,
        loader: 'html'
      },
      {
        test: /\.(ico)|(jpg)|(png)|(jpeg)|(gif)$/,
        loaders: ['file-loader', 'image-webpack?optimizationLevel=7&interlaced=false']
      },
      {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract('style-loader', 'css-loader?minimize&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]')
      }
    ]
  }
})

Let's have some explainations:

new WebpackConfig() creates a valid blank configuration. Then we can merge this configuration with our configration. The .merge method allows us to smartly merge 2 configurations.

We define here some shared informations, like the entries and output of the app and the common loaders (babel, html, images and css).

Now, in separates files we will create final configurations for production and development environment.

// file : /config/webpack-production.config.js

const webpack = require('webpack')  
const WebpackConfig = require('webpack-config')

module.exports = new WebpackConfig().extend('./config/webpack-base.config.js').merge({  
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        'NODE_ENV': JSON.stringify('production')
      }
    }),
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.UglifyJsPlugin(),
    new webpack.optimize.OccurenceOrderPlugin()
  ]
})

Here, with the .extend method we tell webpack-config to extend an existing configuration. The key is the file path relative to the project root. Then we can merge this configuration with some special production features like minification and environment variables.

// file : /config/webpack-dev.config.js

const WebpackConfig = require('webpack-config')

module.exports = new WebpackConfig().extend('./config/webpack-base.config.js').merge({  
  debug: true,
  output: {
    publicPath: '/assets/'
  },
  devServer: {
    contentBase: './server'
  }
})

In the dev config we define some options for the live-server.

The webpack-karma config is kind of special, so we will not extend the base config.

// file : /config/webpack-karma.config.js
const WebpackConfig = require('webpack-config')

module.exports = new WebpackConfig().merge({  
  debug: true,
  devtool: 'inline-source-map',

  module: {
    loaders: [{
      test: /\.js$/,
      exclude: /node_modules/,
      loaders: ['babel']
    }]
  }
})

Single entry point

By convention, webpack uses the configuration defined in /webpack.config.js.

If we want to be able to run the > webpack command without messing up with some configuration we have to create this file, as a dynamic entry point for our configurations.

// file : /webpack.config.js

const WebpackConfig = require('webpack-config')

const TARGET = process.env.npm_lifecycle_event

var webpackConfig

swith (TARGET) {  
  case 'start':
    webpackConfig = './config/webpack-dev.config.js'
    break
  case 'test'
    webpackConfig = './config/webpack-test.config.js'
    break
  default:
    webpackConfig = './config/webpack-production.config.js'
    break
}

module.exports = new WebpackConfig().extend(webpackConfig)

Here I choosed to use the npm_lifecycle_event environment variable to dynamicly define the base configuration. If the variable is not provided webpack will use the production configuration.

This forces us to use npm to trigger task, but I think this is a good practice.

Here the scripts value of the package.json

"scripts": {
    "test": "karma start",
    "build": "webpack",
    "start": "node server/index.js"
  }

Summary

We end up with a clear and flexible file structure :

  • config
    • webpack-base.config.js
    • webpack-base.dev.js
    • webpack-production.dev.js
    • webpack-test.dev.js
  • package.json
  • webpack.config.js

To build the app, we just have to run > webpack in the command line, and that's it !

Thanks to webpack-config plugin and npm power, we end up with a flexible and maintenable webpack configuration. I the next chapter we will see how use this configuration to serve our application in a dev server !


Part 2 : Lift up a webpack-dev-server (Comming soon)