Phoenix Framework: the assets pipeline
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.
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
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)
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.
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.
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.
# 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.