Overview

Jake is the JavaScript build tool for NodeJS. Jake has been around since the very early days of Node, and is full featured and well tested.

It has capabilities similar to GNU Make, CMake, or Rake in the Ruby community.

Similar tools in the Node and JavaScript ecosystem are Grunt and Gulp.

Installation

Jake is primarily a CLI tool. If you want to install it globally, install it with the -g flag:

$ npm install -g jake

You'll likely need to use sudo to install, if you don't have permissions on /usr/local.

You can also install it locally as a dev dependency, like so:

$ npm install --save-dev jake

Or with yarn:

$ yarn add -D jake

You can then invoke it from inside your project via npx.

Embedded Jake

Jake is mainly meant to be used as a CLI tool, but you can also embed it in your own programs, as a building block for creating macros, generator scripts, or anything that requires tasks that run with prerequisites.

Basic usage

Run Jake from the command line, like so:

$ jake [options ...] [env variables ...] target

Jake accepts the following options:

-f / --jakefile FILE Use FILE as the Jakefile.
-C / --directory DIRECTORY Change to DIRECTORY before running tasks.
-q / --quiet Do not log built-in Jake messages to standard output.
-B / --always-make Unconditionally make all targets.
-t / -T / -ls / --tasks [PATTERN] Display the tasks (matching optional PATTERN) with descriptions, then exit.
-J / --jakelibdir JAKELIBDIR Auto-import any .jake files in JAKELIBDIR (default is 'jakelib').
-h / --help Display this help message.
-V / -v / --version Display the Jake version.

Jakefiles

Tasks for Jake to run are defined in Jakefiles. A Jakefile is just executable JavaScript. You can include whatever JavaScript you want in it.

When you run Jake, it will look for a Jakefile in the current diretory. Jake will recognize Jakefile, Jakefile.js, jakefile, or jakefile.js as a vaild name for a Jakefile.

If it doesn't find a Jakefile in the current directory, it will look its parent directory, and that one's parent directory, recursively, until it hits the root of the filesystem. (If it hasn't found a Jakefile then, it errors out.)

Sample Jakefile

Here's an example of a very simple Jakefile:

let { task, desc } = require('jake');

desc('This is the default task.');
task('default', function () {
  console.log('This is the default task.');
  console.log('Jake will run this task if you run `jake` with no task specified.');
});

desc('This is some other task. It depends on the default task');
task('otherTask', ['default'], function () {
  console.log('Some other task');
});

Showing tasks

Passing jake the -t or --tasks flag (or the aliases -T / -ls) will display the full list of tasks available in a Jakefile, along with their descriptions:

$ jake -t
jake default       # This is the default task.
jake asdf          # This is the asdf task.
jake concat.txt    # File task, concatenating two files together
jake failure       # Failing task.
jake lookup        # Jake task lookup by name.
jake foo:bar       # This the foo:bar task
jake foo:fonebone  # This the foo:fonebone task

Setting a value for -t/--tasks will filter the list by that value:

$ jake -t foo
jake foo:bar       # This the foo:bar task
jake foo:fonebone  # This the foo:fonebone task

The list displayed will be all tasks whose namespace/name contain the filter string.

API

There are two APIs for defining tasks: the original API, which uses a call to a task method to define one, and an experimental API which relies simply on exported functions to define tasks — but the basic building block of both APIs is the Task.

Tasks

Tasks are the basic building block of execution for Jake-based build processes. Tasks can have other tasks as prerequities, meaning those tasks have to run before the current task can run.

Jake is smart enough to know if particular prerequisites have already been run, so it doesn't run the same task twice. (There is an API for reenabling tasks, so you can run them multiple times if you need to.)

The task method

The task method defines tasks. It has one required argument, the task name, and two optional arguments.

task(name, [prerequisites], [action]);

The name argument is a String defining the name of the task, and prerequisites is an optional Array of the list of prerequisite tasks (i.e., their names as Strings) to run first. (For example, you may decide to make the "clean" and "lint" tasks prerequisites of the "build" task.)

The action is a Function defining the action to take for the task. The action is invoked with the Task object itself as the execution context (i.e, this inside the action function references the Task object).

Async Functions as Actions

As you'll see in the example below, Jake handles actions that are async functions (or any that return a Promise) without doing anything special. It will correctly wait until the async function or Promise resolves, before marking the task as complete, and moving to the next.

A Task is also an EventEmitter which emits the 'start' event when it begins to run, and the 'complete' event when it is finished.

This is one way to allow asynchronous task actions to be run from within other tasks via either invoke or execute, and ensures they will complete before the rest of the containing task executes. See the section "Running tasks from within other tasks," below.

The desc method

Use desc to add a string description of the task. If you add this description, the task will be visible when you run jake -t.

Private tasks

Tasks without a description (without a call to desc) are considered private. They will not show up when you run jake -t

Task examples

let { task, desc } = require('jake');

desc('This is the foo task.');
task('foo', function () {
  console.log('This is the foo task.');
});

desc('This is the bar task.');
task('foo', function () {
  console.log('This is the bar task.');
});

desc('This async task has prerequisites.');
task('hasPrereqs', ['foo', 'bar'], async function () {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('Ran some prereqs first.');
      resolve();
    }, 2000);
  });
});

Exported function tasks (experimental)

Jake also supports an experimental API that will create tasks implicitly from any functions you export from your Jakefile. The name of the task will be the key you export the function with.

For example, with the following Jakefile:

function foo() {}
  async function bar() {}

  exports.foo = foo;
  exports.bar = bar

Running jake -t wiil yield the following output:

$ jake -t
jake foo (Exported function task)
jake bar (Exported function task)

The series method (experimental)

The series method allows you to chain together Jake tasks into single, larger tasks (similar to providing task prerequisites, with the original API).

This approach allows you to compose larger, more complex tasks from smaller, simpler ones, and could be considered an example of recursive composition.

Use the series method like this:

let { series } = require('jake');


function clean() {
  // Do pre-build cleanup here
};
async function lint() {
  // Shell out, do some linting
}

async function _build() {
  // This is only used internally -- always clean and lint
  // before building
}

exports.clean = clean;
exports.lint = lint;
exports.build = series(clean, lint, _build);

File tasks

Create a file task by using the file method.

File tasks create a file from one or more other files. With a file task, Jake will run the specified action if the file does not exist yet, or if it is older than the files specified by any prerequisite file tasks. File tasks are particularly useful for compiling something from a tree of source files.

You can also use ordinary tasks (or directory tasks, below) as prereqs for file tasks.

let { file, desc } = require('jake');

desc('This builds a compiled JS file for production.');
file('foo-production.js', ['bar', 'foo-bar.js', 'foo-baz.js'], function () {
  // Code to concat and compile goes here
});

Directory tasks

Create a directory task by using the directory method.

Directory-tasks create a directory for use with for file tasks. Jake checks for he existence of the directory, and only creates it if needed.

let {directory, desc } = require('jake');

desc('This task creates creates a directory named "bar"');
directory('bar');

A directory task can be used as a prerequisite for a file task, or when run from the command line.

Namespaces

Use namespace to create a namespace of tasks to perform. Call it with two arguments:

namespace(name, namespaceTasks);

Where name is the name of the namespace, and namespaceTasks is a function with calls inside it to task or desc defining all the tasks for that namespace.

Here's an example:

let { namespace, task, desk } = require('jake');

desc('This is the default task.');
task('default', function () {
  console.log('This is the default task.');
});

namespace('foo', function () {
  desc('This the foo:bar task');
  task('bar', function () {
    console.log('doing foo:bar task');
  });

  desc('This the foo:baz task');
  task('baz', ['default', 'foo:bar'], function () {
    console.log('doing foo:baz task');
  });
});

In this example, the foo:baz task depends on the default and foo:bar tasks.

Rules

When you add a filename as a prerequisite for a task, but there is not a file-task defined for it, Jake can create file-tasks on the fly from Rules.

Here's an example:

let { rule } = require('jake');
let exec = require('child_process').execSync;

rule('.o', '.c', function () {
  let cmd = 'cc ' + this.source + ' -c -o ' + this.name;
  exec(cmd);
});

This rule will take effect for any task-name that ends in '.o', but will require the existence of a prerequisite source file with the same name ending in '.c'.

For example, with this rule, if you reference a task 'foobarbaz.o' as a prerequisite somewhere in one of your Jake tasks, rather than complaining about this file not existing, or the lack of a task with that name, Jake will automatically create a FileTask for 'foobarbaz.o' with the action specified in the rule you've defined. (The usual action would be to create 'foobarbaz.o' from 'foobarbaz.c'). If 'foobarbaz.c' does not exist, it will recursively attempt synthesize a viable rule for it as well.

Regex patterns

You can use regular expressions to match file extensions as well:

let { rule } = require('jake');
let exec = require('child_process').execSync;

rule(/\.o$/, '.c', function () {
  let cmd = 'cc ' + this.source + ' -c -o ' + this.name;
  exec(cmd);
});

Source files from functions

You can also use a function to calculate the name of the desired source file to use, instead of assuming simple suffix substitution:

let { rule } = require('jake');

// Match .less.css or .scss.css and run appropriate preprocessor
let getSourceFilename = function (name) {
  // Strip off the extension for the filename
  return name.replace(/\.css$/, '');
};

rule(/\.\w{2,4}\.css$/, getSourceFilename, {async: true}, function () {
  // Get appropriate preprocessor for this.source, e.g., foo.less
  // Generate a file with filename of this.name, e.g., foo.less.css
});

Advanced usage

Concurrent tasks

Jake can run the prerequisites of a task concurrently.

This only makes sense when the tasks are asynchronous and do not block the event loop — for example, executing shell commands. You can enable and limit the number of simultaneous tasks with the concurrency task option. Note: you can't necessarily know order in which concurrent tasks will finish.

The folowing example uses setTimout to finish two concurrent tasks out of order:

task('A', function() {
  console.log('Started A');
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('Finished A');
      resolve();
    }, 500);
  });
});
task('B', function() {
  console.log('Started B');
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('Finished B');
      resolve();
    }, 250);
  });

});
task('concurrent', ['A','B'], {concurrency: 2}, function() {
  console.log('Finished concurrent tasks');
});

Passing parameters

Parameters can be passed on the jake CLI in two ways: task-specific arguments, and environment variables.

Task arguments

To pass arguments to a Jake task, enclose them in square braces, separated by commas, after the name of the task on the command-line. For example, with the following Jakefile:

let { task, desc } = require('jake');

desc('This is an awesome task.');
task('awesome', function (a, b, c) {
  console.log(a, b, c);
});

You could run jake like this:

$ jake awesome[hoge,piyo,fuga]

And you'd get the following output:

hoge piyo fuga

Note that you cannot uses spaces between the commas separating the parameters.

For zsh users

You will need to escape the brackets or wrap in single quotes like this to pass parameters:

$ jake 'awesome[foo,bar,baz]'

Another solution is to deactivate permanently file-globbing for the jake command. You can do this by adding this line to your .zshrc file:

alias jake="noglob jake"

Environment variables

Any parameters passed after the Jake task that contain an equals sign (=) will be added to process.env.

With the following Jakefile:

let { task, desc } = require('jake');

desc('This is an awesome task.');
task('awesome', function (a, b, c) {
  console.log(a, b, c);
  console.log(process.env.qux, process.env.frang);
});

You could run jake like this:

jake awesome[hoge,piyo,fuga] qux=zoobie frang=asdf

And you'd get the following output:

hoge piyo fuga
zoobie asdf

No args = default task

Running the jake command with no arguments at all runs the default task.

Programmatic tasks

Jake supports the ability to run a task from within another task via the invoke and execute methods.

The invoke method will run the desired task, along with its prerequisites:

let { task, desc, Task } = require('jake');

desc('Calls the foo:bar task and its prerequisites.');
task('invokeFooBar', function () {
  // Calls foo:bar and its prereqs
  Task['foo:bar'].invoke();
});

The invoke method will only run the task once, even if you call it repeatedly.

let { task, desc, Task } = require('jake');

desc('Calls the foo:bar task and its prerequisites.');
task('invokeFooBarOnce', function () {
  let t = Task['foo:bar'];
  // Calls foo:bar and its prereqs
  t.invoke();
  // No-op
  t.invoke();
});

The execute method runs the desired task without its prerequisites:

let { task, desc, Task } = require('jake');

desc('Calls the foo:bar task without its prerequisites.');
task('executeFooBar', function () {
  // Calls foo:bar without its prereqs
  Task['foo:baz'].execute();
});

Calling execute repeatedly will run the desired task repeatedly.

let { task, desc, Task } = require('jake');

desc('Calls the foo:bar task without its prerequisites.');
task('executeFooBarRepeatedly', function () {
  // Calls foo:bar without its prereqs
  let t = Task['foo:bar'];
  t.execute();
  // Can keep running this over and over
  t.execute();
  t.execute();
});

If you want to run the task and its prerequisites more than once, you can use invoke with the reenable method.

let { task, desc, Task } = require('jake');

desc('Calls the foo:bar task and its prerequisites.');
  task('invokeFooBar', function () {
  let t = Task['foo:bar'];
  // Calls foo:bar and its prereqs
  t.invoke();
  // Does nothing
  t.invoke();
  // Only re-runs foo:bar, but not its prerequisites
  t.reenable();
  t.invoke();
});

The reenable method takes a single Boolean arg, a 'deep' flag, which reenables the task's prerequisites if set to true.

let { task, desc, Task } = require('jake');

desc('Calls the foo:bar task and its prerequisites.');
task('invokeFooBar', function () {
  let t = Task['foo:bar'];
  // Calls foo:bar and its prereqs
  t.invoke();
  // Does nothing
  t.invoke();
  // Re-runs foo:bar and all of its prerequisites
  t.reenable(true);
  t.invoke();
});

It's easy to pass params on to a sub-task run via invoke or execute:

let { task, desc, Task } = require('jake');

desc('Passes params on to other tasks.');
task('passParams', function (...args) {
  // Calls foo:bar, passing along current args
  Task['foo:bar'].invoke(...args);
});

Evented tasks

Tasks are EventEmitters. The Jake runner does a pretty good job of managing task asynchrony, but in some cases, particularly when running tasks programmatically using invoke, you may want to listen specifically for a particular task's events.

Following are the events a task may emit:

start Emitted before the task's action is run. (Will also emit for tasks that have no action defined.) (No value)
skip Emitted when a task is specified as a prereq for another task, but it has already been run. (No value)
error Emitted when there is an error running a task's action. (This also applies to rejected Promises in async actions.) If there is no listener for the error event on the Jake task, there will be a global Jake failure, and the program will exit. The thrown error
complete Emitted when a task successfully completes. The value returned by the task

let { task, desc, Task } = require('jake');

desc('Calls the async foo:baz task and its prerequisites.');
task('invokeFooBaz', async function () {
  let t = Task['foo:baz'];
  return new Promise((resolve, reject) => {
    t.addListener('complete', () => {
      console.log('Finished executing foo:baz');
      // Maybe run some other code
      // ...
      resolve();
    });
    // Kick off foo:baz
    t.invoke();
  });
});

If you want to handle the errors in a task in some specific way, you can set a listener for the 'error' event, like so:

let { task, namespace, Task } = require('jake');

namespace('vronk', function () {
  task('groo', function () {
    var t = Task['vronk:zong'];
    t.addListener('error', function (e) {
      console.log(e.message);
    });
    t.invoke();
  });

  task('zong', function () {
    throw new Error('OMFGZONG');
  });
});

Unhandled task errors

If no specific listener is set for the error event, errors are handled by Jake's built-in error handling — and your program will exit with a non-zero exit code.

Aborting tasks

Any unhandled error in a task will abort the current task, and cause your program to exit with a non-zero exit code.

Alternatively you can use the fail method, which allows you to specify a custom message to be printed to stderr. You may also specify a number to use as the exit code.

let { task, fail } = require('jake');

task('failTaskQuestionCustomStatus', function () {
  fail('Yikes, something went wrong', 42);
});

If you run this task, the process will die with an exit code of 42, and the specified message will get printed to stderr.

Modularizing

Jake will automatically look for files with a .js extension in a 'jakelib' directory in your project, and load them (using require) after loading your main jakefile.js. (The directory name can be overridden using the -J/--jakelibdir command-line option.)

This allows you to break your tasks up over multiple files -- a good way to do it is one namespace per file: e.g., a zardoz namespace full of tasks in 'jakelib/zardox.js'.

Note that these .js files in the jakelib each run in their own module-context, so they don't have access to each others' data. However, the Jake API methods, and the task-hierarchy are globally available, so you can use tasks in any file as prerequisites for tasks in any other, just as if everything were in a single file.

And of course environment variables set on the command line are likewise also naturally available to code in all those files, in process.env.

Utility tasks

Jake ships with a number of built-in utility tasks that make the repetitive work of building and releasing easier.

PackageTask

When you create a PackageTask, it programmatically creates a set of tasks for packaging up your project for distribution. Here's an example:

let { packageTask } = require('jake');

packageTask('fonebone', 'v0.1.2112', function () {
  var fileList = [
    'jakefile.js'
  , 'README.md'
  , 'package.json'
  , 'lib/*'
  , 'bin/*'
  , 'tests/*'
  ];
  this.packageFiles.include(fileList);
  this.needTarGz = true;
  this.needTarBz2 = true;
});

This will automatically create a 'package' task that will assemble the specified files in 'pkg/fonebone-v0.1.2112,' and compress them according to the specified options. After running jake package, you'll have the following in pkg/:

fonebone-v0.1.2112
fonebone-v0.1.2112.tar.bz2
fonebone-v0.1.2112.tar.gz 

PackageTask also creates a 'clobber' task that removes the pkg/ directory.

  • name {String} The name of the project
  • version {String} The project version-string
  • prereqs {Array} Tasks to run before packaging
  • packageDir {String='pkg'} The directory-name to use for packaging the software
  • packageFiles {jake.FileList} The list of files and directories to include in the package-archive
  • needTar {Boolean=false} If set to true, uses the tar utility to create a gzip .tgz archive of the package
  • needTarGz {Boolean=false} If set to true, uses the tar utility to create a gzip .tar.gz archive of the package
  • needTarBz2 If set to true, uses the tar utility to create a bzip2 .bz2 archive of the package
  • needJar {Boolean=false} If set to true, uses the jar utility to create a .jar archive of the package
  • needZip {Boolean=false} If set to true, uses the zip utility to create a .zip archive of the package
  • manifestFile {String=null} Can be set to point the jar utility at a manifest file to use in a .jar archive. If unset, one will be automatically created by the jar utility. This path should be relative to the root of the package directory (this.packageDir above, likely 'pkg')
  • tarCommand {String='tar'} The shell-command to use for creating tar archives.
  • jarCommand {String='jar'} The shell-command to use for creating jar archives.
  • zipCommand {String='zip'} The shell-command to use for creating zip archives.
  • archiveNoBaseDir {Boolean=false} Simple option for performing the archive on the contents of the directory instead of the directory itself
  • archiveChangeDir {String=null} Equivalent to the '-C' command for the tar and jar commands. ("Change to this directory before adding files.")
  • archiveContentDir {String=null} Specifies the files and directories to include in the package-archive. If unset, this will default to the main package directory -- i.e., name + version.

FileList

Jake's FileList takes a list of glob-patterns and file-names, and lazy-creates a list of files to include. Instead of immediately searching the filesystem to find the files, a FileList holds the pattern until it is actually used.

When any of the normal JavaScript Array methods (or the toArray method) are called on the FileList, the pending patterns are resolved into an actual list of file-names.

To build the list of files, use FileList's include and exclude methods:

let list = new jake.FileList();
list.include('foo/*.txt');
list.include(['bar/*.txt', 'README.md']);
list.include('Makefile', 'package.json');
list.exclude('foo/zoobie.txt');
list.exclude(/foo\/src.*.txt/);
console.log(list.toArray());

The include method can be called either with an array of items, or multiple single parameters. Items can be either glob-patterns, or individual file-names.

The exclude method will prevent files from being included in the list. These files must resolve to actual files on the filesystem. It can be called either with an array of items, or multiple single parameters. Items can be glob-patterns, individual file-names, string-representations of regular-expressions, or regular-expression literals.

PublishTask

The PublishTask will automatically create a publish task which performs the following steps:

  1. Bump the version number in your package.json
  2. Commit change in git, push it to GitHub
  3. Create a git tag for the version
  4. Push the tag to GitHub
  5. Package the new version of your project
  6. Publish it to NPM
  7. Clean up the package

If you want to publish to a private NPM repository or some other type of repo, you can specify a custom publishing command:

let { publishTask } = require('jake');

publishTask('zerb', function () {
  this.packageFiles.include([
  , 'index.js'
  , 'package.json'
    ]);

  // Publishes using the gemfury cli
  // `%filename` will be replaced with the package filename
  this.publishCmd = 'fury push %filename';
});