@luminati-io/jake
Version:
JavaScript build tool, similar to Make or Rake
309 lines (267 loc) • 9.45 kB
JavaScript
/*
* Jake JavaScript build tool
* Copyright 2112 Matthew Eernisse (mde@fleegix.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
var fs = require('fs')
, path = require('path')
, exec = require('child_process').exec
, utils = require('utilities')
, currDir = process.cwd()
, FileList = require('filelist').FileList;
var PublishTask = function () {
var args = Array.prototype.slice.call(arguments).filter(function (item) {
return typeof item != 'undefined';
})
, arg
, opts = {}
, definition
, prereqs = []
, createDef = function (arg) {
return function () {
this.packageFiles.include(arg);
};
};
this.name = args.shift();
// Old API, just name + list of files
if (args.length == 1 && (Array.isArray(args[0]) || typeof args[0] == 'string')) {
definition = createDef(args.pop());
}
// Current API, name + [prereqs] + [opts] + definition
else {
while ((arg = args.pop())) {
// Definition func
if (typeof arg == 'function') {
definition = arg;
}
// Prereqs
else if (Array.isArray(arg) || typeof arg == 'string') {
prereqs = arg;
}
// Opts
else {
opts = arg;
}
}
}
this.prereqs = prereqs;
this.packageFiles = new FileList();
this.publishCmd = opts.publishCmd || 'npm publish %filename';
this.gitCmd = opts.gitCmd || 'git';
this.versionFiles = opts.versionFiles || ['package.json'];
this.scheduleDelay = 5000;
// Override utility funcs for testing
this._ensureRepoClean = function (stdout) {
if (stdout.length) {
fail(new Error('Git repository is not clean.'));
}
};
this._getCurrentBranch = function (stdout) {
return utils.string.trim(stdout);
};
if (typeof definition == 'function') {
definition.call(this);
}
this.define();
};
PublishTask.prototype = new (function () {
var _currentBranch = null;
var getPackage = function () {
var pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(),
'/package.json')).toString());
return pkg;
}
, getPackageVersionNumber = function () {
return getPackage().version;
};
this.define = function () {
var self = this;
namespace('publish', function () {
task('fetchTags', {async: true}, function () {
// Make sure local tags are up to date
var cmds = [
self.gitCmd + ' fetch --tags'
];
jake.exec(cmds, function () {
console.log('Fetched remote tags.');
complete();
});
});
task('getCurrentBranch', {async: true}, function () {
// Figure out what branch to push to
exec(self.gitCmd + ' symbolic-ref --short HEAD',
function (err, stdout, stderr) {
if (err) {
fail(err);
}
if (stderr) {
fail(new Error(stderr));
}
if (!stdout) {
fail(new Error('No current Git branch found'));
}
_currentBranch = self._getCurrentBranch(stdout);
console.log('On branch ' + _currentBranch);
complete();
});
});
task('ensureClean', {async: true}, function () {
// Only bump, push, and tag if the Git repo is clean
exec(self.gitCmd + ' status --porcelain --untracked-files=no',
function (err, stdout, stderr) {
if (err) {
fail(err);
}
if (stderr) {
fail(new Error(stderr));
}
// Throw if there's output
self._ensureRepoClean(stdout);
complete();
});
});
task('updateVersionFiles', function () {
var p
, pkg
, version
, arr
, patch;
// Grab the current version-string
p = path.join(process.cwd(), 'package.json');
pkg = getPackage();
version = pkg.version;
// Increment the patch-number for the version
arr = version.split('.');
patch = parseInt(arr.pop(), 10) + 1;
arr.push(patch);
version = arr.join('.');
// Update package.json or other files with the new version-info
self.versionFiles.forEach(function (file) {
var p = path.join(process.cwd(), file)
, data = JSON.parse(fs.readFileSync(p).toString());
data.version = version;
fs.writeFileSync(p, JSON.stringify(data, true, 2));
});
// Return the version string so that listeners for the 'complete' event
// for this task can use it (e.g., to update other files before pushing
// to Git)
return version;
});
task('pushVersion', ['ensureClean', 'updateVersionFiles'], {async: true},
function () {
var version = getPackageVersionNumber()
, message = 'Version ' + version
, cmds = [
self.gitCmd + ' commit -a -m "' + message + '"'
, self.gitCmd + ' push origin ' + _currentBranch
, self.gitCmd + ' tag -a v' + version + ' -m "' + message + '"'
, self.gitCmd + ' push --tags'
];
var execOpts = {};
if (process.platform == 'win32') {
// Windows won't like the quotes in our cmdline
execOpts.windowsVerbatimArguments = true;
}
jake.exec(cmds, function () {
var version = getPackageVersionNumber();
console.log('Bumped version number to v' + version + '.');
complete();
}, execOpts);
});
task('definePackage', function () {
var version = getPackageVersionNumber()
, t;
t = new jake.PackageTask(self.name, 'v' + version, self.prereqs, function () {
// Replace the PackageTask's FileList with the PublishTask's FileList
this.packageFiles = self.packageFiles;
this.needTarGz = true; // Default to tar.gz
// If any of the need<CompressionFormat> or archive opts are set
// proxy them to the PackageTask
for (var p in this) {
if (p.indexOf('need') == 0 || p.indexOf('archive') == 0) {
if (typeof self[p] != 'undefined') {
this[p] = self[p];
}
}
}
});
});
task('package', {async: true}, function () {
var definePack = jake.Task['publish'+jake.nsSep+'definePackage']
, pack = jake.Task.package
, version = getPackageVersionNumber();
// May have already been run
definePack.reenable(true);
definePack.addListener('complete', function () {
pack.addListener('complete', function () {
console.log('Created package for ' + self.name + ' v' + version);
complete();
});
pack.invoke();
});
definePack.invoke();
});
task('publish', {async: true}, function () {
var version = getPackageVersionNumber()
, filename
, cmd;
console.log('Publishing ' + self.name + ' v' + version);
if (typeof self.createPublishCommand == 'function') {
cmd = self.createPublishCommand(version);
}
else {
filename = 'pkg/' + self.name + '-v' + version + '.tar.gz';
cmd = self.publishCmd.replace(/%filename/gi, filename);
}
// Hackity hack -- NPM publish sometimes returns errror like:
// Error sending version data\nnpm ERR!
// Error: forbidden 0.2.4 is modified, should match modified time
setTimeout(function () {
jake.exec(cmd, function () {
console.log('BOOM! Published.');
complete();
}, {printStdout: true, printStderr: true});
}, self.scheduleDelay);
});
task('cleanup', {async: true}, function () {
var clobber = jake.Task.clobber;
clobber.reenable(true);
clobber.on('complete', function () {
console.log('Cleaned up package');
complete();
});
clobber.invoke();
});
});
var prefixNs = function (item) {
return 'publish' + jake.nsSep + item;
};
// Create aliases in the default namespace
desc('Version and release.');
task('publish', self.prereqs.concat(['version', 'release']
.map(prefixNs)));
desc('Increment the verison number.');
task('version', ['fetchTags', 'getCurrentBranch', 'pushVersion']
.map(prefixNs));
desc('Package, publish, and clean up.');
task('release', ['package', 'publish', 'cleanup']
.map(prefixNs));
// Invoke proactively so there will be a callable 'package' task
// which can be used apart from 'publish'
jake.Task['publish'+jake.nsSep+'definePackage'].invoke();
};
})();
jake.PublishTask = PublishTask;
exports.PublishTask = PublishTask;