Phoenix Framework: the assets pipeline

Updates

From the time I wrote part 1
of this short series, Atom has gained a new Elixir plugin based on Samuel Tonini’s Alchemist Server.
From the Emacs plugin, it inherits all the most notable features such as
autocomplete, jump to definition/documentation for the function/module under
the cursor, quote/unquote code and interactive macro expansion.
A feature reference along with some screenshots can be found at the atom-elixir page.
It also looks pretty good.

The assets pipeline

Assets pipelines are one of the most important features in modern web frameworks.
When working on this task, Phoenix developers have proven that they value
pragmatism over purity and have chosen to base their implementation on Brunch, a Node.js build tool that takes care of everything
related to assets management.
This choice has probably saved man-years of work, that would have inevitably delayed the release of a fully working pipeline system.
A very common counter argument is that this adds node as a dependency, but I
think it’s a negligible inconvenient, node is most probably already present on
the majority of developers machines.

Brunch installation is just a npm install away and it runs automatically when
you create a new Phoenix project

$ mix phoenix.new brunch_demo
* creating brunch_demo/config/config.exs
* creating brunch_demo/config/dev.exs
...
Fetch and install dependencies? [Yn] y
* running mix deps.get
* running npm install && node node_modules/brunch/bin/brunch build

As you can guess, the local brunch is installed in node_modules/brunch/bin/brunch.
You can install it globally with the usual -g flag: npm install -g brunch.
Phoenix automatically runs Brunch when assets change.
If you launch mix phoenix.server and make some changes to web/static/js/app.js you can see that Brunch is working in the background.

[info] Running BrunchDemo.Endpoint with Cowboy using http on port 4000
28 Apr 13:31:56 - info: compiled 5 files into 2 files, copied 3 in 2.2 sec
28 Apr 13:33:08 - info: compiled app.js and 3 cached files into app.js in 118ms

Defaults

By default Brunch watch for changes in css and js folders inside web/static and automatically recompile and package them for HTTP serving.
It is configured to work with ES6 and transpile it to ES5 through
Babel, so you can start using ES6 today, without hassles.

Javascript files are automatically wrapped into a module, that needs to be required
before being used.
Every file inside web/static/js will be converted and loaded on demand.

// web/static/js/app.js
export var App = {
  run: function(){
    console.log("Hello from Phoenix!")
  }
}
<!-- web/templates/layout/app.html.eex -->
<script>require("web/static/js/app").App.run()</script>

<!-- shows "Hello from Phoenix!" in the browser's console -->

If you have legacy code that won’t work if modularized or doesn’t need modularization,
you can put it in web/static/vendor and it will be copied as it is.
This is also the easier way to create global variables

// web/static/vendor/globals.js
global_variable = "this is global";

// open up the console and type global_variable
// can you guess the result?

Any of this default can be changed in brunch-config.js.
For example this is the part that ignores the module wrapping for the vendor folder.

plugins: {
  babel: {
    // Do not use ES6 compiler in vendor code
    ignore: [/web/static/vendor/]
  }
},

Last but not least, Phoenix automatically loads

  • Brunch’s bootstrapper code which provides module management and require() logic
  • Phoenix Channels JavaScript client (deps/phoenix/web/static/js/phoenix.js)
  • Some code from Phoenix.HTML (deps/phoenixhtml/web/static/js/phoenixhtml.js)

Plugins

Brunch is configured to load a numbers of plugin, specifically

  • javascript-brunch: enables processing of Javascript files
  • babel-brunch: the ES6 transpiler
  • uglify-js-brunch: Javascript minifier
  • css-brunch: enables processing of CSS files
  • clean-css-brunch: CSS minifier

Plugins are installed through npm, for example if you wanna use Coffeescript in
your application you can simply npm install --save coffee-script-brunch and
your coffee files will be automatically picked up and processed.

Tool belt

Manually copying files or libraries inside the vendor folder is not exactly the
best way to handle external dependencies.
One of the features of Brunch is that it allows the developers to take advantage of the Node ecosystem.
Brunch works seamlessly with Bower, which IMHO is the simplest way to handle
third-party frontend dependencies.
Do you need underscore?
Just bower install --save underscore, (restart the server if the files are not being compiled automatically) and type _ in the console

function _(obj) {
  if (obj instanceof _) return obj;
  if (!(this instanceof _)) return new _(obj);
  this._wrapped = obj;
}

One problem I’ve found is that not every folder provided by the packages is being copied (or concatenated) in the output folder (usually priv/static unless you changed it).
I was working with an old version of materialize and the font was not being loaded.
The solution was pretty easy, just open up lib/<app_name>/endpoint.ex and look for this line

plug Plug.Static,
  only: ~w(css fonts images js favicon.ico robots.txt)

In my case materialize was using font as a folder name, but it wasn’t whitelisted
in the Static Plug configuration. I added font to the list and it fixed the issue.

There’s more than a way

One thing that the Phoenix framework does and does really well is not being
strongly opinionated.
Brunch is just the default assets manager, but it’s really simple to use another one, just change this line in dev.ex

watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin"]]

There’s an example in the Pheonix documentation on how to use Phoenix with webpack, I’m gonna go further, I’ll show you how to write the skeleton of an assets manager and get it started by Phoenix.
Create a new file in lib/watcher.exs (exs means Exlixir script) and paste this code inside it

# lib/watcher.exs

defmodule Watcher do

  def start do
    IO.puts "* Start monitoring #{Path.absname("web/static")}"
    IO.inspect System.argv
    IO.inspect System.get_env
  end

end

Watcher.start

and then change the configuration this way

watchers: [mix: ["run", "lib/watcher.exs", "random input"]]

and start the Phoenix server

mix phoenix.server

[info] Running BrunchDemo.Endpoint with Cowboy using http on port 4000
* Start monitoring <app_path>/web/static
["random input"]
%{"CLICOLOR" => "1", "PROMPT_COMMAND" => "_update_ps1; update_terminal_cwd",
  "_system_arch" => "x86_64",
  "DISPLAY" => "/private/tmp/com.apple.launchd.4Zqdr48hnr/org.macosforge.xquartz:0",
...

Ok, it works but it’s not really useful, we’re gonna add a new feature, that watches
for file changes inside the web/static folder and logs to the screen.

First we’re gonna need to install a filesystem watcher component, there is one
written in Erlang that we can import directly from Github.

Add this line to mix.exs

# mix.exs
defp deps do
  # ...
   {:fs, github: "synrc/fs", override: true}]
   # override tells the Elixir compiler that this package overrides the default
   # :fs Erlang module
end

fetch the library with mix deps.get
and then update the watcher.exs code

# lib/watcher.exs
defmodule Watcher do

  def start do
    IO.puts "* Start monitoring #{Path.absname("web/static")}"
    IO.inspect System.argv
    IO.inspect System.get_env
    # starts the listener
    :fs.start_link(:watcher, Path.absname("web/static"))
    :fs.subscribe(:watcher)
    loop
  end

  def loop do
    receive do
      {_watcher_process, {:fs, :file_event}, {path, flags}} ->
        # logs events to the screen
        IO.puts("* #{path} -> #{Enum.join flags, ", "}")
    end
    loop
  end
end

Watcher.start

Start the server again, change some file inside web/static and enjoy the results.

# save app.js
* <app_path>/web/static/js/app.js -> inodemetamod, modified

# save a new file
* <app_path>/web/static/js/app2.js -> created, modified, finderinfomod, xattrmod

# delete a file
* <app_path>/web/static/js/app2.js -> renamed

That’s it for now, next time we’ll talk about long running processes.

Leave a Reply

wpDiscuz