Using Webpack with Phoenix and Elixir

Updated October 13, 2016 for Phoenix 1.2.1 and webpack 1.13

Phoenix, by default, uses Brunch for a build tool – and for most folks, it’ll work, but I’ve grown fond of webpack because of features like hot module replacement and the ease of configuring loaders/processors in the build process. In my opinion, Elixir and Phoenix is the best for choice for server side architecture currently, so, naturally these should be paired with the best client side tools.

By the end of this guide, you’ll have wepback running at parity with the default Brunch configuration that Phoenix comes with. Here’s an example repo.

Initializing the Phoenix Application

Let’s begin with a new Phoenix application, we won’t be using the --no-brunch flag. --no-brunch assumes that you’ll be managing your frontend assets statically and changes the directory structure slightly. It’s easier to remove Brunch rather than recreate the directory structure manually.

When you’re asked to install dependencies, say no.

mix phoenix.new webpack_integration

Next, we’ll remove the Brunch specific configuration:

rm brunch-config.js

Then remove the Brunch packages from the dependencies object in package.json. When that’s done, package.json should look like this:

{
  "repository": {},
  "license": "MIT",
  "scripts": {
    "deploy": "brunch build --production",
    "watch": "brunch watch --stdin"
  },
  "dependencies": {
    "phoenix": "file:deps/phoenix",
    "phoenix_html": "file:deps/phoenix_html"
  },
  "devDependencies": {}
}

There are still scripts which mention brunch, we’ll get to those later on.

Adding Webpack

Installing webpack may cause npm to output warnings because of missing fields in package.json, like the name, etc. For our purposes, these fields are not important. That said, if you do want a fresh package.json, run npm init to generate an empty version of it.

To install webpack, run:

npm install --save-dev webpack

And then add a basic webpack config. By default webpack looks for webpack.config.js in the root directory of the project, so create the file with the following contents:

module.exports = {
  entry: "./web/static/js/app.js",
  output: {
    path: "./priv/static/js",
    filename: "app.js",
  },
};

This configuration tells webpack to look for web/static/js/app.js and compile it to priv/static/js/app.js.

To run webpack, we’ll add an entry to the scripts section of package.json. This will allow us to run webpack on its own, with our preferred command line options by running npm start at the command line.

{
  "scripts": {
    "watch": "webpack --watch-stdin --progress --color"
  }
}

Now we’ll tell Phoenix to run webpack as a watcher while running the development server. Edit the watchers option in config/dev.exs to look like this:

config :webpack_integration, WebpackIntegration.Endpoint,
  # leave other settings and change the `watchers` option.
  watchers: [npm: ["run", "watch"]]

Notes For Windows Users: npm doesn’t play very well with Windows so rather than using npm scripts to run webpack, we run it via node.js directly. Thanks to Keith and Kanmii for pointing this out.

Change your watchers options in config/dev.exs to:

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

Start up your server again with mix phoenix.server and we’ll test that webpack is working..

Start by editing web/static/js/app.js. Make sure that the entire file is commented out – we don’t have any ES2015 compilation working yet, so anything that relies on this syntax will cause a compilation issue with webpack.

At this point, the only thing we’ll want to run is:

// Ensure that this import is commented out for now.
// import "phoenix_html"

alert("webpack compiled me.");

Now you can install the dependencies, set up the database and run the server.

mix deps.get
mix ecto.create
mix phoenix.server

Now, when you open http://localhost:4000 you’ll see the alert, but we don’t have any JS or CSS processing set up. We’ll fix that next.

Adding Babel for JavaScript

Out of the box, webpack doesn’t compile CSS, or JavaScript written with ES2015 syntax for you, so we’ll have to add the required loaders. Let’s get started with Babel for JavaScript compilation.

Install babel, and the babel loader for webpack:

npm install babel-loader babel-core babel-preset-es2015 --save-dev

and add the following after the output options in webpack.config.js:

module.exports = {
  // entry and output options...

  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel",
        query: {
          presets: ["es2015"],
        },
      },
    ],
  },
};

This rules state that any file ending in .js that is required within the application will be run through Babel. Once it is in place, restart the server, and you’ll have support for all the great JavaScript features that Babel has to offer.

Setting load paths

With webpack, using import or require expects an explicit path, relative to the file you’re working in. For example, if you have the following files:

app.js
components/filePicker.js

To require filePicker from within app.js it’d look like this:

// Brunch
import filePicker from "components/filePicker";

// Webpack
import filePicker from "./components/filePicker";

This is not better or worse, just different. Since we’re aiming for complete compatibility with the Brunch configuration we’ll have to add a configuration option to tell it to look in web/static/js for a module. While we’re at it, we’ll also tell webpack to look in the node_modules directory for any packages we install through npm.

Add the following webpack.config.js:

module.exports = {
  // Leave the entry, output, and module options we set previously

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

If you’re upgrading from an older version of Phoenix, you may have to add the phoenix_html and phoenix packages statically in package.json. If you don’t already have an entry for them in your dependencies, we’ll use npm to manage the phoenix_html and phoenix module dependencies. The JS packages are already included in the Elixir packages installed using hex, so we need to bring them into our node_modules directory with npm.

# If you've already got the Phoenix dependencies in package.json:
npm install

# If you need to move them into package.json:
npm install file:deps/phoenix_html file:deps/phoenix --save

and now we can import the modules normally:

import "phoenix_html";
import { Socket } from "phoenix";

Restart the Phoenix server, and you should have the Phoenix JavaScript modules included in our compiled file.

CSS and Webpack

If you’ve used webpack before, you’ve probably seen CSS being required from within the individual components of your application. Instead of generating separate CSS files, webpack will inline any CSS required when loading the page. Since we’re aiming for the exact functionality that Brunch provided, we’ll have to separate out the CSS from our JavaScript bundle. Implementing our CSS compilation this way is slightly different from the way it is normally demonstrated in webpack tutorials.

We need both the style and css loaders to actually parse and compile css files to their correct location. On top of this, we need the extract-text-webpack-plugin to pull the CSS out of our bundle and output it to its own file.

npm install css-loader style-loader extract-text-webpack-plugin --save-dev

Now we’ll add an additional entry point for webpack pointing to the app.css file, redefine the output path to account for CSS and JS locations, add the style and css loaders, and configure the ExtractText plugin to output the individual CSS file. Here’s what webpack.config.js file should look like when you’re done:

var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
  entry: ["./web/static/css/app.css", "./web/static/js/app.js"],
  output: {
    path: "./priv/static",
    filename: "js/app.js",
  },

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

  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel",
        include: __dirname,
        query: {
          presets: ["es2015"],
        },
      },
      {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract("style", "css"),
      },
    ],
  },

  plugins: [new ExtractTextPlugin("css/app.css")],
};

With this configuration, webpack will grab the web/static/css/app.css file, parse the CSS and move it to priv/static/css/app.css. Restart your Phoenix server to see the stylesheets working. The only thing missing now are the static assets.

Handling Static Assets

Last but not least, we need to move our static assets so that they’re accessible. By default, Phoenix stores static assets in web/static/assets and moves them to priv/static, so web/static/assets/favicon.ico will be moved to priv/static/favicon.ico.

To do this, install the copy-webpack-plugin:

npm install --save-dev copy-webpack-plugin

and add its configuration to the plugins array in webpack.config.js:

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

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

There is one gotcha to this approach – assets are not automatically copied when added to web/static/assets. With this configuration, you are required to restart webpack when a file is added to this directory.

Building for Production

The last step in this process is to tell elixir webpack to build production ready assets, this is simple. From the command line, run:

webpack -p

I’ve added this as a script in package.json like so:

{
  "scripts": {
    "deploy": "webpack -p"
  }
}

And that’s it. We’re all done! Run npm run deploy to build your assets during your deploy process and everything will work the same way that it did with Brunch.

If you get stuck, have a look at my example repo. The important files are package.json, webpack.config.js, and dev/config.exs.

Good luck!

This guide was inspired by Manuel Kallenbach’s guide Automatically Building Your Phoenix Assets with Webpack. This article provides a 1-to-1 mapping of webpack to the default Brunch setup that comes with Phoenix.