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.

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:

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!

14 comments"

  1. visualjeff says:

    There are a number of Javascript build tools. Which is good. I like choice. But how does Fez compare to something like Broccoli?

    1. Or Gulp?
      One of the things I like about gulp is that they are actively checking plugins people are making and providing feedback about which ones have been developed “properly” and which one a script kiddie threw together and chucked on npm. Are you doing something similar ?

  2. Juan says:

    Nah, I’d prefer Gulp.

  3. Widy says:

    there’s a more easier tool called gulp. it’s a taskrunner too and it’s more easy than grunt

  4. Popcorn says:

    People here recommend Gulp, but unlike Fez, Gulp has one major problem: as pointed out by http://www.solitr.com/blog/2014/02/broccoli-first-release/ (section 5), Gulp plugins have trouble with files that include/import other files — the Sass plugin, for example, abuses the Gulp’s piping system and reads the needed files directly from the filesystem, breaking the watch() plugin and preventing from any form of incremental rebuilding.
    I like that Fez addresses this issue in a pretty cool way (less.imports(file) adds all files imported by file to the tree, if I understand it correctly). It just needs a better documentation (from official docs: “[…] chain your targets with spec.use (see below for an example of this) […]”, no example below given) and more tasks (that’s just matter of popularity and time).
    Also, it would be pretty cool if someone wrote an adapter of existing tasks for Grunt or Gulp to be able to use them in Fez — I don’t know if this is really possible, but it would be really awesome.

  5. pipwiz says:

    gulpjs is best

  6. atmin says:

    Sold on the approach, will try Fez at some time.

    Current user of Grunt, works fine for me, but a bit dissatisfied with verbosity and lately speed (which can be fixed by introducing even more verbosity), so always curious on the alternatives (Gulp and Broccoli also look fine, but not convinced).

  7. It looks very interesting!
    One questing, how did you make this build-graph processing visualisations? Are there any build-in tools for that in fez or may be some external tools?

  8. adeeb says:

    Between this, gulp and grunt, this is the true underdog.

  9. Phil Thompson says:

    It would help other readers if you could actually comment on why you think one better than the other.

    Or just reframe from commenting and stick with the tool you know. Thanks.

    1. remotesynth says:

      I thought it was pretty clear that a) they are different tools – people call Grunt a build tool when it is a task runner; and b) the benefit to Fez is that it only reprocesses the files it needs to during a build, making the build process more efficient and faster.

  10. Thomas Koch says:

    Even if it’s possible to run fez every second, I’d prefer a build tool that watches my file system and rebuilds a few microseconds after I’ve saved a file so that everything is ready when I’ve switched from my editor to the browser.

    1. You need any special module in fez for this: “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”

  11. Bob Myers says:

    And what exactly is so wrong with GNU make, that people are running around re-inventing the wheel, without even realizing in most cases that they are doing so?.

Leave a Reply

Your email address will not be published. Required fields are marked *

© 2016 Modern Web & our authors. All rights reserved.