Phoenix Assets with Webpack

by Andrew Stewart
published October 22, 2015

Phoenix is a (fairly) new web framework for the Elixir programming language. It can be loosely compared to existing frameworks such as Ruby on Rails or Django.

By default, Phoenix uses the Brunch build tool to manage static assets - CSS, JavaScript, etc. Brunch is a good default choice - very simple to set up, customizable through plugins, and fast. It also supports ES6/ES2015 features through Babel by default.

However, Brunch is fairly inflexible, and some developers are fond of other asset build tools. Luckily for us, Phoenix makes it very easy to use a different asset build tool, or none at all.

For this example, we’ll create a new Phoenix app using the Webpack module bundler. Webpack is capable of handling many different file formats with loaders, but in this case we’ll focus on JS and CSS.

Initial Setup

To get started, let’s create a new Phoenix app. We’ll create this one without Brunch.

$ mix phoenix.new hello --no-brunch

When you get prompted to Fetch and install dependencies? [Yn], just hit enter. This will install our Elixir dependencies now rather than later.

With that done, let’s jump into the project:

$ cd hello

There’s some clean up we’ll need to get to first. We’ll be keeping our assets under web/static, compiling them into priv/static as needed. Unfortunately for us, if we tell Elixir we’re not using Brunch, it just puts all the compiled assets into priv/static by default.

Let’s remove the default-generated JS / CSS:

$ rm -rf priv/static/js priv/static/css

And we’ll move the rest of what’s there to a new assets folder in web/static:

$ mv priv/static web/static/assets

Static Assets

So now we have our static assets living in web/static/assets, but Phoenix expects to serve them from priv/static.

Let’s add a quick script to help us manage this (we’ll also make a new scripts directory to hold it):

$ mkdir scripts
./scripts/static
#!/bin/bash
rm -rf priv/static
mkdir priv/static
cp -R web/static/assets/* priv/static/

Simple enough, and it gets the job done. You can make the script executible with chmod:

$ chmod a+x ./scripts/static

Now, if we run this script at the root of our application, it will remove any generated assets, recreate priv/static, and copy over our static assets.

While we’re here, let’s also append an entry to our .gitignore to ignore generated assets:

./.gitignore
# [...]
/priv/static

And there we go. We’ve got a basic enough system in place to handle non-compiled static assets.

If desired, you could use a tool like watch to re-copy on changes, but we’re not too concerned for now.

Webpack

Now to the good stuff.

To get started, let’s build a package.json file:

./package.json
{
  "name": "hello",
  "private": true,
  "dependencies": {}
}

Not that much to see at the moment.

Next up, let’s install some npm packages. We’ll start with Webpack itself. We’ll also use the --save flag, which will auto-add entries to package.json.

$ npm install --save webpack

Next up, we’ll install the necessary dependencies to compile our JavaScript with Babel:

$ npm install --save babel-core babel-loader

Almost there. Now some loaders to compile stylesheets with SASS:

$ npm install --save style-loader css-loader sass-loader node-sass

Last but not least, we’ll install something a bit different - a plugin to let Webpack export our stylesheets as a separate file:

$ npm install --save extract-text-webpack-plugin

Whew.

Thankfully, we’re done installing stuff for now, let’s get to wiring it all up.

Similarly to Brunch’s brunch-config.js file, Webpack is configured via a webpack.config.js file.

Here’s an example one we’ll use - for more info, check out the Webpack docs.

./webpack.config.js
"use strict";

var path = require("path");

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

// helpers for writing path names
// e.g. join("web/static") => "/full/disk/path/to/hello/web/static"
function join(dest) { return path.resolve(__dirname, dest); }
function web(dest) { return join("web/static/" + dest); }

var config = module.exports = {
  // our app's entry points - for this example we'll use a single each for
  // css and js
  entry: [
    web("css/style.scss"),
    web("js/script.js")
  ],

  // where webpack should output our files
  output: {
    path: join("priv/static"),
    filename: "js/script.js"
  },

  // more information on how our modules are structured, and
  //
  // in this case, we'll define our loaders for JavaScript and CSS.
  // we use regexes to tell Webpack what files require special treatment, and
  // what patterns to exclude.
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel"
      }, {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract("style", "css!sass")
      }
    ]
  },

  // what plugins we'll be using - in this case, just our ExtractTextPlugin.
  // we'll also tell the plugin where the final CSS file should be generated
  // (relative to config.output.path)
  plugins: [
    new ExtractTextPlugin("css/style.css")
  ]
};

// if running webpack in production mode, minify files with uglifyjs
if (process.env.NODE_ENV === "production") {
  config.plugins.push(
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.UglifyJsPlugin({ minimize: true })
  );
}

Now that we’ve told Webpack how to build our assets, let’s actually give it something to build!

First, let’s create the directories where these assets will live:

$ mkdir -p web/static/js web/static/css

We’ll create basic CSS and JS files as we defined in our config:

./web/static/js/script.js
console.log("hello, world");
./web/static/css/style.scss
body {
  font-family: sans-serif;
  color: white;
  background: purple;
}

We could use ES2015 JavaScript and SCSS in these files, but let’s just get everything working for now.

Now comes the moment of truth - let’s try to build it all:

$ node ./node_modules/.bin/webpack
Hash: 10fddef56efce318c4be
Version: webpack 1.12.2
Time: 1030ms
        Asset      Size  Chunks             Chunk Names
js/script.js     1.7 kB       0  [emitted]  main
css/style.css  74 bytes       0  [emitted]  main
  [0] multi main 40 bytes {0} [built]
    + 5 hidden modules
Child extract-text-webpack-plugin:
        + 2 hidden modules

Hey, how about that!

Phoenix

Well, we’ve managed to go this far without really touching Phoenix at all - let’s fix that.

We can tell Phoenix to start Webpack at the same time as our development server. That way, Webpack can watch for changes and rebuild our JS/CSS without having to manually run the webpack command.

To do this, we’ll add modify the watchers entry in config/dev.exs:

./config/dev.exs
config :hello, Hello.Endpoint,
  # [...]
  watchers: [
    node: ["node_modules/.bin/webpack", "--watch", "--colors", "--progress"]
  ]

With that saved, Phoenix now knows to spin up Webpack in development.

Lastly, let’s update our app’s layout since we gave our new assets different names than the Phoenix defaults. Open up web/templates/layouts/app.html.eex and change the appropriate CSS/JS static_path calls:

Start It Up

And that’s it.

Now let’s get the whole thing running:

$ mix phoenix.server

Once your app finishes compiling, you should see Phoenix start serving via cowboy, and Webpack compile your assets for the first time.

Go visit http://localhost:4000, and you should see our beautiful new CSS:

If you check the console, you’ll also see a message from our test JavaScript.

And that’s it! Now we have a shiny new Phoenix app, ready to go, with an asset pipeline powered by Webpack.

If you want the phoenix.js file that we deleted earlier, you can find the ES2015 version in the Phoenix repo.

For some further reading, I recommend:

I hope this post’s been helpful - if you did found it useful in getting started, let me know! I’m very excited to see what people can build with Phoenix and Elixir in general.