Using Grunt? Consider Fez

by Brian Rinaldi on February 24, 2014

The modern web is always changing, and this article is more than two years old.

By Isaac Wagner

In the world of JavaScript tooling, Grunt is king. Grunt is a task runner, meaning that a build is defined as a series of tasks which run one after another. Tasks may include file concatenation, application deployment, source linting, etc. Grunt isn’t unique as a task runner – you may have heard of Java’s Ant, or Ruby’s Rake.

Developers have another option besides using a task runner: a file-based build tool. File-based build tools are extremely common; Make is arguably the most well known and frequently used file-based build tool, but there are many others. Rather than defining a sequence of tasks to run one after the other, like a task runner, with a file-based build tool a developer defines builds as transformational relationships between files. For example, instead of having a single task that searches a directory and turns every CoffeeScript file into JavaScript, we define an operation for each CoffeeScript file which translates it into a new JavaScript file. This difference may seem inconsequential at first glance, but it becomes incredibly important when defining how a tool will behave. In fact, if we take a step back it becomes apparent that Grunt isn’t a build tool at all. Rather, it simply plays host to build steps, among any number of other non-build tasks.

Enter Fez. Fez approaches the problem from a different perspective: that of a build tool, not a task runner. Not only does Fez have a built-in understanding of what it means to build a project, but it also has an escape hatch, discussed below, to allow for more traditional “ordered tasks.” Fez tries to bring together the best of both worlds: the intelligence of Make and the flexibility of Grunt.

What is Fez?

Fez is a file-based build tool loosely based on tup and written in JavaScript. Fez takes the idea of a JavaScript build tool with pluggable operations from Grunt, but goes in a different direction. Builds are defined as sets of rules with three components: the input(s), the output, and the operation. An operation is just a function. Unlike Grunt, Fez doesn’t do any magic plugin loading. You simply require the operation function and pass it as an argument to a rule. Here is why this simple rule-based build definition system is awesome: Fez is able to glob the file system for an initial set of inputs, then construct the entire dependency graph from that set of inputs and the set of rules.

See Also:  Where did Vue.js come from?

For example, say we had a build spec like this:

*.less → %f.css
*.css → %f.min.css
*.min.css → dist.min.css

This would be written in JavaScript as:

exports.default = function(spec) {
  spec.with("*.less").each(function(file) {
    //
    spec.rule(file, file.patsubst("%.less", "%.css"), less());
  });

  spec.with("*.css).not("*.min.css").each(function(file) {
    spec.rule(file, file.patsubst("%.css, "%.min.css), cssmin());
  });

  spec.with("*.min.css).all(function(files) {
    spec.rule(files, "dist.min.css", concat());
  });
};

(The file object’s patsubst has the same semantics as make’s patsubst)

And we have a few starting nodes we found on the file system:

fez_ex1

Fez is able to construct the entire graph, from beginning to end:

fez_ex2

Now all we have to do is traverse the graph with a topological sort. We can introduce parallelization of operations where appropriate by executing them in child processes and waiting on convergence points. Fez, like make, compares timestamps of inputs and outputs during graph traversal, allowing Fez to only do the work which needs to be done. The build graph even improves cleaning: any node with one or more inputs is a generated node, and can besafely removed.

Idempotence

One of the coolest features of Fez is its inherent idempotence.

idempotence (uncountable): (mathematics, computing) A quality of an action such that repetitions of the action have no further effect on outcome – being idempotent.

What this means in the context of Fez is that you can run your build script as many times as you want, and unless any input files have changed, no work will be done. As a consequence, there is no reason to have a file watching system in Fez. You can just (on a Unix system) run watch node fez.js.

Escape Hatch

Sometimes you will want to just escape the whole rule-based system and do things imperatively. This is simple: spec.do defines a node in the build graph in which you can perform whatever computation, file manipulation, etc. that you want. It’s usually a good idea to put an imperative node (or stage, as it’s called in Fez) in its own target and chain your targets with spec.use, but you can certainly put the node right in the build graph along with every other node (created from rules or otherwise) using spec.after(...).do. Here’s an example:

exports.default = function (spec) {
  var x = 0;

  // This stage will start alongside the jshint rules. The actual order
  // they will execute is undefined.
  spec.do(function() {
    console.log(x++)
  });

  // Save a handle to the stage.
  var stage = spec.with("*.js").each(function (file) {
    spec.rule(file, jshint());
  });

  // After all operations within the stage are completed,
  // then peform this task.
  spec.after(stage).do(function() {
      console.log(x++);
  });
};

Examples

For good measure, here are some example builds with animated GIFs illustrating their graphs:

See Also:  Why you should limit JavaScript — and how to do it

In this example we illustrate a simple LESS project. The main.less file has an @import "mobile.less"; statement. This is an example of a secondary input which is dependent on the file’s contents. The file main.less won’t be read until Fez is sure that no other operations are going to generate main.less (which they aren’t). In some situations though, a file with secondary inputs may be a generated file, and the file won’t be read until it has been recreated this time through the build.

var fez = require("fez"),
    less = require("fez-less"),
    clean = require("fez-clean-css"),
    concat = require("fez-concat");

exports.build = function(spec) {
  spec.with("main.less").one(function(file) {
    spec.rule(file, less.imports(file), file.patsubst("%.less", "css/%.css"), less());
  });

  spec.with("dist/*.min.css").all(function(files) {
    spec.rule(files, "dist.min.css", concat());
  });

  spec.with("css/*.css").each(function(file) {
    spec.rule(file, file.patsubst("css/%.css", "dist/%.min.css"), clean());
  });
};

exports.default = exports.build;

fez(module);

fez_ex3

This example illustrates the flexibility of with(...).all(...) which is able to combine any number of source (non-generated) files and generated files. Like in the previous example, there are many node pairs which have not one, but two edges between them. The first edge is created between a stage’s input and any operation nodes within the stage. The second edge represents the link between a rule’s primary input and its operation node. The double edges are not computationally necessary, but are useful to show when visualizing Fez’s behavior.

var fez = require("fez");

exports.build = function(spec) {
  spec.with("a").each(function(file) {
    spec.rule(file, "b", function nop1() {});
  });

  spec.with("b").each(function(file) {
    spec.rule(file, "c", function nop2() {});
  });

  spec.with(["a", "b", "c"]).all(function(files) {
    spec.rule(files, "d", function nop3() {});
  });
};

exports.default = exports.build;

fez(module);

fez_ex4

Help us out

The API is still pretty raw. Fez can be used in real projects (and is being used in a few real projects!), but I am announcing a “request for feedback.” In true open source fashion, it would be invaluable to have developers help iron out warts from Fez’s API as early as possible. The best way is to just use Fez in a project and let us know about your experiences. Was it simple enough? Did you find it to be an improvement over your existing build tool? Did you find yourself wishing you never touched Fez? Tell us why! Join us on the GitHub issue tracker, and come hang out in #fez on Freenode and share your thoughts!