bem
Version:
560 lines (428 loc) • 15.9 kB
JavaScript
'use strict';
var Q = require('q'),
QFS = require('q-fs'),
CP = require('child_process'),
PATH = require('../path'),
URL = require('url'),
UTIL = require('util'),
U = require('../util'),
LOGGER = require('../logger'),
registry = require('../nodesregistry'),
Node = require('./node').NodeName,
LibraryNodeName = exports.LibraryNodeName = 'LibraryNode',
SCM_VALIDITY_TIMEOUT = 60000; // 60 secs
/* jshint -W106 */
exports.__defineGetter__(LibraryNodeName, function() {
return registry.getNodeClass(LibraryNodeName);
});
/* jshint +W106 */
registry.decl(LibraryNodeName, Node, /** @lends LibraryNode.prototype */ {
nodeType: 2,
/**
* LibraryNode instance constructor.
*
* @class LibraryNode
* @constructs
* @param {Object} o Node options.
* @param {String} o.root Project root path.
* @param {String} o.target Library path.
*/
__constructor: function(o) {
this.__base(o);
this.root = o.root;
this.target = o.target;
this.npmPackages = o.npmPackages === undefined? ['package.json']: o.npmPackages;
},
/**
* Get absolute path to library.
*
* @return {String}
*/
getPath: function() {
return PATH.resolve(this.root, this.target);
},
getLibraryContent: function() {
// stub method
},
installLibraryDependencies: function() {
if (this.npmPackages === false) {
LOGGER.finfo('npmPacakges config variable is set to false, skip installing npm dependencies for the %s library', this.target);
return;
}
var _this = this;
return Q.all(this.npmPackages
.map(function(pkg) {
pkg = PATH.join(this.getPath(), pkg);
var opts = { cwd: PATH.dirname(pkg) },
npm = process.env.NPM || 'npm',
npmEnv = process.env.NPM_ENV || 'production';
return QFS.exists(pkg)
.then(function(exists) {
if (!exists) {
LOGGER.finfo('%s file does not exist, skip installing npm dependencies for the %s library', pkg, _this.target);
return;
}
var cmd;
// simple update process
if (!_this.ctx.force) {
LOGGER.finfo('Installing dependencies for %s library (npm install)', _this.target);
cmd = npm + ' install --' + npmEnv;
_this.log(cmd);
return U.exec(cmd, opts);
}
LOGGER.finfo('Installing dependencies for %s library (npm prune && npm update)', _this.target);
cmd = npm + ' cache clean';
_this.log(cmd);
return U.exec(cmd, opts)
.then(function() {
var cmd = npm + ' prune';
_this.log(cmd);
return U.exec(cmd, opts);
})
.then(function() {
var cmd = npm + ' update';
_this.log(cmd);
return U.exec(cmd, opts);
});
});
}, this));
},
make: function() {
var _this = this;
return Q.when(this.getLibraryContent())
.then(function() {
return _this.installLibraryDependencies();
});
}
}, {
createId: function(o) {
return o.target;
}
});
var SymlinkLibraryNodeName = exports.SymlinkLibraryNodeName = 'SymlinkLibraryNode';
/* jshint -W106 */
exports.__defineGetter__(SymlinkLibraryNodeName, function() {
return registry.getNodeClass(SymlinkLibraryNodeName);
});
/* jshint +W106 */
registry.decl(SymlinkLibraryNodeName, LibraryNodeName, /** @lends SymlinkLibraryNode.prototype */ {
/**
* SymlinkLibraryNode instance constructor.
*
* @class SymlinkLibraryNode
* @constructs
* @param {Object} o Node options.
* @param {String} o.root Project root path.
* @param {String} o.target Library path.
* @param {String} o.relative Symlink path.
*/
__constructor: function(o) {
this.__base(o);
this.relative = o.relative;
},
/**
* Check validity of node.
*
* @return {Boolean}
*/
isValid: function() {
return false;
},
/**
* Make node.
*
* @return {Promise * Undefined}
*/
getLibraryContent: function() {
var _this = this;
return QFS.statLink(this.getPath())
.fail(function() {
return false;
})
.then(function(stat) {
if (!stat) return;
/* jshint -W109 */
if (!stat.isSymbolicLink()) {
return Q.reject(UTIL.format("SymlinkLibraryNode: Path '%s' is exists and is not a symbolic link",
_this.getPath()));
}
/* jshint +W109 */
return QFS.remove(_this.getPath());
})
.then(function() {
var parent = PATH.dirname(_this.getPath());
return QFS.exists(parent)
.then(function(exists) {
/* jshint -W109 */
if(!exists) {
LOGGER.verbose("SymlinkLibraryNode: Creating parent directory for target '%s'",
_this.getPath());
return QFS.makeTree(parent);
}
/* jshint +W109 */
});
})
.then(function() {
return QFS.symbolicLink(_this.getPath(), _this.relative);
});
}
});
var ScmLibraryNodeName = exports.ScmLibraryNodeName = 'ScmLibraryNode';
/* jshint -W106 */
exports.__defineGetter__(ScmLibraryNodeName, function() {
return registry.getNodeClass(ScmLibraryNodeName);
});
/* jshint +W106 */
registry.decl(ScmLibraryNodeName, LibraryNodeName, /** @lends ScmLibraryNode.prototype */ {
/**
* ScmLibraryNode instance constructor.
*
* @class ScmLibraryNode
* @constructs
* @param {Object} o Node options.
* @param {String} o.root Project root path.
* @param {String} o.target Library path.
* @param {String} o.url Repository URL.
* @param {String[]} [o.paths=['']] Paths to checkout.
*/
__constructor: function(o) {
this.__base(o);
this.url = o.url;
this.paths = [''];
this.timeout = typeof o.timeout !== 'undefined'? Number(o.timeout) : SCM_VALIDITY_TIMEOUT;
},
/**
* Check validity of node.
*
* @return {Promise * Boolean}
*/
isValid: function() {
return Q.resolve(this.lastRunTime && this.timeout && (Date.now() - this.lastRunTime <= this.timeout));
},
/* jshint -W098 */
/**
* Stub for getInitialCheckoutCmd(), throws.
*
* @param {String} url Repository url.
* @param {String} target Repository checkout path.
* @throws {Error}
*/
getInitialCheckoutCmd: function(url, target) {
throw new Error('getInitialCheckoutCmd() not implemented!');
},
/* jshint +W098 */
/* jshint -W098 */
/**
* Stub for getUpdateCmd(), throws.
*
* @param {String} url Repository url.
* @param {String} target Repository checkout path.
* @throws {Error}
*/
getUpdateCmd: function(url, target) {
throw new Error('getInitialCheckoutCmd() not implemented!');
},
/* jshint +W098 */
/**
* Checkput or update single repository path.
*
* @param {String} path Repository path to checkout / update.
* @return {Promise * Undefined}
*/
updatePath: function(path) {
var _this = this,
target = PATH.resolve(this.root, this.target, path),
repo = joinUrlPath(this.url, path);
return QFS.exists(target)
.then(function(exists) {
var cmd = exists?
_this.getUpdateCmd(repo, target) :
_this.getInitialCheckoutCmd(repo, target),
d = Q.defer();
_this.log(cmd);
CP.exec(cmd, function(err, stdout, stderr) {
stdout && _this.log(stdout);
stderr && _this.log(stderr);
if (err) return d.reject(err);
_this.lastRunTime = Date.now();
d.resolve();
});
return d.promise;
});
},
/**
* Make node.
*
* @return {Promise * Undefined}
*/
getLibraryContent: function() {
return Q.all(this.paths.map(function(path) {
return this.updatePath(path);
}, this));
}
});
var GitLibraryNodeName = exports.GitLibraryNodeName = 'GitLibraryNode';
/* jshint -W106 */
exports.__defineGetter__(GitLibraryNodeName, function() {
return registry.getNodeClass(GitLibraryNodeName);
});
/* jshint +W106 */
registry.decl(GitLibraryNodeName, ScmLibraryNodeName, /** @lends GitLibraryNode.prototype */ {
/**
* GitLibraryNode instance constructor.
*
* @class GitLibraryNode
* @constructs
* @param {Object} o Node options.
* @param {String} o.root Project root path.
* @param {String} o.target Library path.
* @param {String} o.url Repository URL.
* @param {String[]} [o.paths=['']] Paths to checkout.
* @param {String} [o.treeish] Treeish (commit hash or tag) to checkout.
* @param {String} [o.branch='master'] Branch to checkout.
*/
__constructor: function(o) {
this.__base(o);
this.treeish = o.treeish;
this.branch = o.branch || 'master';
},
getInitialCheckoutCmd: function(url, target) {
return UTIL.format('git clone --progress %s %s && cd %s && git checkout %s', url, target, target, this.treeish || this.branch);
},
getUpdateCmd: function(url, target) {
var onBranch = !this.treeish;
return UTIL.format('cd %s && git fetch origin %s git checkout %s %s', target,
onBranch? ('&& git branch ' + this.branch + '; '): ' && ',
this.treeish || this.branch,
onBranch? '&& git rebase origin/' + this.branch: '');
},
isValid: function() {
var _this = this,
path = PATH.resolve(this.root, this.target);
return QFS.exists(path)
.then(function(exists) {
return exists && _this.getInfo(path)
.then(function(info) {
var head = info.HEAD,
wanted = _this.treeish || _this.branch;
LOGGER.verbose('HEAD is at ' + head + ' wanted is ' + wanted);
// head is at configured hash
if (wanted === head) return true;
var resolves = 'refs/heads/' + wanted;
if (info[resolves] !== head) resolves = 'refs/tags/' + wanted;
if (info[resolves] !== head) resolves = '';
if (resolves) LOGGER.fverbose('%s resolves to %s', head, resolves);
else LOGGER.fverbose('%s does not resolve to %s', head, wanted);
return !!resolves;
});
});
},
getInfo: function(path) {
var _this = this,
cmd = UTIL.format('cd %s && git show-ref --head', path),
d = Q.defer();
this.log(cmd);
CP.exec(cmd, function(err, stdout, stderr) {
stderr && _this.log(stderr);
if (err) return d.reject(err);
d.resolve(_this._parseInfo(stdout));
});
return d.promise;
},
_parseInfo: function(stdout) {
var refs = {};
stdout.split('\n').forEach(function(line) {
var ref = line.split(' ');
refs[ref[1]] = ref[0];
});
return refs;
}
});
var SvnLibraryNodeName = exports.SvnLibraryNodeName = 'SvnLibraryNode';
/* jshint -W106 */
exports.__defineGetter__(SvnLibraryNodeName, function() {
return registry.getNodeClass(SvnLibraryNodeName);
});
/* jshint +W106 */
registry.decl(SvnLibraryNodeName, ScmLibraryNodeName, /** @lends SvnLibraryNode.prototype */ {
/**
* SvnLibraryNode instance constructor.
*
* @class SvnLibraryNode
* @constructs
* @param {Object} o Node options.
* @param {String} o.root Project root path.
* @param {String} o.target Library path.
* @param {String} o.url Repository URL.
* @param {String[]} [o.paths=['']] Paths to checkout.
* @param {String} [o.revision='HEAD'] Revision to checkout.
*/
__constructor: function(o) {
this.__base(o);
this.paths = Array.isArray(o.paths)? o.paths : [o.paths || ''];
this.revision = o.revision || 'HEAD';
},
/**
* Check validity of node.
*
* Use output of `svn info` to check revision property.
* If revision is the same as in config on all paths then
* return promised true.
*
* @return {Promise * Boolean}
*/
isValid: function() {
var _this = this,
base = this.__base();
if (this.revision === 'HEAD') return base;
return Q.all(this.paths.map(function(path) {
return QFS.exists(PATH.resolve(_this.root, _this.target, path))
.then(function(exists) {
return exists && _this.getInfo(path)
.then(function(info) {
return String(info.revision) === String(_this.revision);
});
});
}))
.then(function(checks) {
return checks.reduce(function(cur, prev) {
return cur && prev;
}, true) || base;
});
},
getInitialCheckoutCmd: function(url, target) {
return UTIL.format('svn co --non-interactive -q -r %s %s %s', this.revision, url, target);
},
getUpdateCmd: function(url, target) {
return UTIL.format('svn up --non-interactive -q -r %s %s', this.revision, target);
},
getInfo: function(path) {
var _this = this,
cmd = UTIL.format('svn info --non-interactive %s', PATH.resolve(this.root, this.target, path)),
d = Q.defer();
this.log(cmd);
CP.exec(cmd, function(err, stdout, stderr) {
stdout && _this.log(stdout);
stderr && _this.log(stderr);
if (err) return d.reject(err);
d.resolve(_this._parseInfo(stdout));
});
return d.promise;
},
_parseInfo: function(text) {
var sep = ': ',
info = {};
text.split('\n').forEach(function(line) {
if (!line) return;
var i = line.indexOf(sep),
key = line.substr(0, i).toLowerCase().replace(/\s+/, '-').replace(/^\s+|\s+$/, '');
info[key] = line.substr(i + sep.length).replace(/^\s+|\s+$/, '');
});
return info;
}
});
function joinUrlPath(url, part) {
var p = URL.parse(url);
p.pathname = PATH.joinPosix(p.pathname, part);
return URL.format(p);
}