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 is able to construct the entire graph, from beginning to end:
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);
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);
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!
There are a number of Javascript build tools. Which is good. I like choice. But how does Fez compare to something like Broccoli?
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 ?
Nah, I’d prefer Gulp.
there’s a more easier tool called gulp. it’s a taskrunner too and it’s more easy than grunt
People here recommend Gulp, but unlike Fez, Gulp has one major problem: as pointed out by https://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.
gulpjs is best
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).
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?
Between this, gulp and grunt, this is the true underdog.
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.
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.
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.
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”
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?.