@luminati-io/jake
Version:
JavaScript build tool, similar to Make or Rake
328 lines (289 loc) • 8.36 kB
JavaScript
var path = require('path')
, fs = require('fs')
, Task = require('./task/task').Task
, Matcher
, rule = {}
, Rule;
// Define a helper object with some utility functions
Matcher = new (function () {
// Split a task to two parts, name space and task name.
// For example, given 'foo:bin/a%.c', return an object with
// - 'ns' : foo
// - 'name' : bin/a%.c
this.split = function(task) {
var parts = task.split(jake.nsSep)
, name = parts.pop()
, ns = this.resolveNS( parts );
return {
'name' : name,
'ns' : ns
};
};
// Return the namespace based on an array of names.
// For example, given ['foo', 'baz' ], return the namespace
//
// default -> foo -> baz
//
// where default is the global root namespace
// and -> means child namespace.
this.resolveNS = function(parts) {
var ns = jake.defaultNamespace;
for(var i = 0, l = parts.length; ns && i < l; i++) {
ns = ns.childNamespaces[parts[i]];
}
return ns;
};
// Given a pattern p, say 'foo:bin/a%.c'
// Return an object with
// - 'ns' : foo
// - 'dir' : bin
// - 'prefix' : a
// - 'suffix' : .c
this.resolve = function(p) {
var task = this.split(p),
name = task.name,
ns = task.ns;
var split = path.basename(name).split('%');
return {
ns: ns
, dir: path.dirname(name)
, prefix: split[0]
, suffix: split[1]
};
};
// Test whether string a is a suffix of string b
this.stringEndWith = function (a,b) {
var l;
return (l = b.lastIndexOf(a)) == -1 ? false : l + a.length == b.length;
};
// Replace the suffix a of the string s with b.
// Note that, it is assumed a is a suffix of s.
this.stringReplaceSuffix = function (s, a, b) {
return s.slice(0,s.lastIndexOf(a)) + b;
};
// Test wether the a prerequisite matchs the pattern.
// The arg 'pattern' does not have namespace as prefix.
// For example, the following tests are true
//
// pattern | name
// bin/%.o | bin/main.o
// bin/%.o | foo:bin/main.o
//
// The following tests are false (trivally)
//
// pattern | name
// bin/%.o | foobin/main.o
// bin/%.o | bin/main.oo
this.match = function(pattern, name) {
var p
, task
, ns
, obj
, filename;
if (pattern instanceof RegExp) {
return pattern.test(name);
}
else if (pattern.indexOf('%') == -1) {
// No Pattern. No Folder. No Namespace.
// A Simple Suffix Rule. Just test suffix
return this.stringEndWith(pattern, name);
}
else {
// Resolve the dir, prefix and suffix of pattern
p = this.resolve(pattern);
// Resolve the namespace and task-name
task = this.split(name);
name = task.name;
ns = task.ns;
// Set the objective as the task-name
obj = name;
// Namespace is already matched.
// Check dir
if (path.dirname(obj) != p.dir) {
return false;
}
filename = path.basename(obj);
// Check file name length
if ((p.prefix.length + p.suffix.length + 1) > filename.length) {
// Length does not match.
return false;
}
// Check prefix
if (filename.indexOf(p.prefix) !== 0) {
return false;
}
// Check suffix
if (!this.stringEndWith(p.suffix, filename)) {
return false;
}
// OK. Find a match.
return true;
}
};
// Generate the source based on
// - name name for the synthesized task
// - pattern pattern for the objective
// - source pattern for the source
//
// Return the source with properties
// - dep the prerequisite of source
// (with the namespace)
//
// - file the file name of source
// (without the namespace)
//
// For example, given
//
// - name foo:bin/main.o
// - pattern bin/%.o
// - source src/%.c
//
// return 'foo:src/main.c',
//
this.getSource = function(name, pattern, source) {
var dep
, pat
, match
, file
, src;
// Regex pattern -- use to look up the extension
if (pattern instanceof RegExp) {
match = pattern.exec(name);
if (match) {
if (typeof source == 'function') {
src = source(name);
}
else {
src = this.stringReplaceSuffix(name, match[0], source);
}
}
}
// Assume string
else {
// Simple string suffix replacement
if (pattern.indexOf('%') == -1) {
if (typeof source == 'function') {
src = source(name);
}
else {
src = this.stringReplaceSuffix(name, pattern, source);
}
}
// Percent-based substitution
else {
pat = pattern.replace('%', '(.*?)');
pat = new RegExp(pat);
match = pat.exec(name);
if (match) {
if (typeof source == 'function') {
src = source(name);
}
else {
file = match[1];
file = source.replace('%', file);
dep = match[0];
src = name.replace(dep, file);
}
}
}
}
return src;
};
})();
Rule = function (opts) {
this.pattern = opts.pattern;
this.source = opts.source;
this.prereqs = opts.prereqs;
this.action = opts.action;
this.opts = opts.opts;
this.desc = opts.desc;
this.ns = opts.ns;
};
Rule.prototype = new (function () {
// Create a file task based on this rule for the specified
// task-name
// ======
// FIXME: Right now this just throws away any passed-in args
// for the synthsized task (taskArgs param)
// ======
this.createTask = function (fullName, level) {
var self = this
, pattern
, source
, action
, opts
, prereqs
, parts
, valid
, src
, tNs
, createdTask
, name = Task.getBaseTaskName(fullName)
, nsPath = Task.getBaseNamespacePath(fullName)
, ns = this.ns.resolveNamespace(nsPath);
pattern = this.pattern;
source = this.source;
if (typeof source == 'string') {
src = Matcher.getSource(name, pattern, source);
}
else {
src = source(name);
}
// TODO: Write a utility function that appends a
// taskname to a namespace path
src = nsPath.split(jake.nsSep).filter(function (item) {
return !!item;
}).concat(src).join(jake.nsSep);
// Generate the prerequisite for the matching task.
// It is the original prerequisites plus the prerequisite
// representing source file, i.e.,
//
// rule( '%.o', '%.c', ['some.h'] ...
//
// If the objective is main.o, then new task should be
//
// file( 'main.o', ['main.c', 'some.h' ] ...
prereqs = this.prereqs.slice(); // Get a copy to work with
prereqs.unshift(src);
// Prereq should be:
// 1. an existing task
// 2. an existing file on disk
// 3. a valid rule (i.e., not at too deep a level)
valid = prereqs.some(function (p) {
var ns = self.ns;
return ns.resolveTask(p) ||
fs.existsSync(Task.getBaseTaskName(p)) ||
jake.attemptRule(p, ns, level + 1);
});
// If any of the prereqs aren't valid, the rule isn't valid
if (!valid) {
return null;
}
// Otherwise, hunky-dory, finish creating the task for the rule
else {
// Create the action for the task
action = function () {
var task = this;
self.action.apply(task);
};
opts = this.opts;
// Insert the file task into Jake
//
// Since createTask function stores the task as a child task
// of currentNamespace. Here we temporariliy switch the namespace.
// FIXME: Should allow optional ns passed in instead of this hack
tNs = jake.currentNamespace;
jake.currentNamespace = ns;
createdTask = jake.createTask('file', name, prereqs, action, opts);
createdTask.source = src.split(jake.nsSep).pop();
jake.currentNamespace = tNs;
return createdTask;
}
};
this.match = function (name) {
return Matcher.match(this.pattern, name);
};
})();
rule.Rule = Rule;
rule.Matcher = Matcher;
module.exports = rule;