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.
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
});
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.
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:
- Bump the version number in your package.json
- Commit change in git, push it to GitHub
- Create a git tag for the version
- Push the tag to GitHub
- Package the new version of your project
- Publish it to NPM
- 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';
});