A tale of build tools

I recently started with Phoenix and tried to get on board with the whole brunch thing, but that faded quickly enough. I’m more back-end focused and don’t want to learn some obscure front-end thing I won’t use elsewhere. So, I decided to switch out brunch with webpack.

I’m using webpack 1 still, since it’s easier for me at the moment, but will switch to webpack 2 sometime in the future. Unless of course The Next Big Thing TM for front-end comes out before then.


Like most things Phoenix, this was actually pretty easy to switch out. I’m going to show you my personal setup I use on my own projects. I am not claiming for this to be The One True Way or anything remotely close to that.

First, let’s add some npm dependencies to package.json. This is what I use:

    "babel-core": "^6.24.0",
    "babel-loader": "^6.4.1",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-es2015": "^6.24.0",
    "copy-webpack-plugin": "^4.0.1",
    "cross-env": "^3.2.4",
    "css-loader": "^0.27.3",
    "eslint": "^3.18.0",
    "eslint-loader": "^1.6.3",
    "extract-text-webpack-plugin": "^1.0.1",
    "json-loader": "^0.5.4",
    "node-sass": "^4.5.0",
    "sass-loader": "^6.0.3",
    "style-loader": "^0.14.1",
    "webpack": "1"

Add a .babelrc file:

  "presets": [
  "plugins": [

Add an .eslintrc file (this is a snippet version of my rules, see full file here):

  "env": {
    "browser": true,
    "node": true

  "parserOptions": {
    "sourceType": "module"

  "ecmaFeatures": {
    "arrowFunctions": true,
    "destructuring": true,
    "classes": true,
    "defaultParams": true,
    "blockBindings": true,
    "modules": true,
    "objectLiteralComputedProperties": true,
    "objectLiteralShorthandMethods": true,
    "objectLiteralShorthandProperties": true,
    "restParams": true,
    "spread": true,
    "templateStrings": true

  "rules": {
    "accessor-pairs": 2,
    "array-bracket-spacing": 0,
    "block-scoped-var": 0,
    "brace-style": [2, "1tbs", { "allowSingleLine": true }],
    "camelcase": 0,
    "etc ..."

And then let’s add a webpack.config.js file:

var ExtractTextPlugin = require("extract-text-webpack-plugin");
var CopyWebpackPlugin = require("copy-webpack-plugin");

module.exports = {
  entry: ["./web/static/css/app.scss", "./web/static/js/app.js"],

  output: {
    path: "./priv/static",
    filename: "js/app.js"

  resolve: {
    modulesDirectories: [ "node_modules", __dirname + "/web/static/js" ]

  devtool: "source-map",

  module: {
    loaders: [{
      test: /\.json$/,
      loader: 'json'
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel!eslint"
    }, {
      test: /\.scss$/,
      loader: ExtractTextPlugin.extract(

  plugins: [
    new CopyWebpackPlugin([{ from: "./web/static/assets" }]),
    new ExtractTextPlugin("css/app.css")

After that we will tweak package.json scripts, change this:

"scripts": {
  "deploy": "brunch build --production",
  "watch": "brunch watch --stdin"

to this:

"scripts": {
  "deploy": "webpack -p",
  "watch": "webpack --watch-stdin --progress --color"

And finally, update your Phoenix config, config/dev.exs. Change:

watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
           cd: Path.expand("../", __DIR__)]]


watchers: [npm: ["run", "watch"]]


That wasn’t so bad! Now when you run npm watch or mix phoenix.server, you will be using webpack instead of brunch.

Going further

If you use Vue.js, then I’ve added a post Add Vue.js to your Phoenix project’s webpack 1 config