nv-pack
Version:
Build tool for packing static distribution packages. Part of the node-front project.
443 lines (392 loc) • 9.49 kB
JavaScript
/**
* Module dependencies.
*/
var program = require('commander')
, sprintf = require('sprintf').sprintf
, utils = require('../utils')
, async = require('async')
, path = require('path')
, mime = require('mime')
, _ = require('lodash')
, fs = require('fs');
// short cut for the utils.log
var log = utils.log;
// short cut for the utils.falog
var falog = utils.falog;
// curent working directory
var cwd = process.cwd();
var cfg;
/**
* Loading config
*/
try {
if (program.config) {
cfg = require(program.config);
} else {
cfg = require(path.join(cwd, '/config'));
}
} catch (e) {
console.error(" error: cannot find config file");
process.exit(1);
}
/*
*
*/
module.exports = function(scope, cb) {
async.waterfall([
function(cb) {
getPagesToPack(program.args, cfg.PAGES_ROOT, function(err, pages) {
if (err) return cb(err);
scope.pages = pages;
cb();
});
},
function(cb) {
buildPagesDist(scope.pages, scope.tmpDir, function(err, assetsMap) {
if (err) return cb(err);
scope.assetsMap = assetsMap;
cb();
});
}
], cb);
};
/**
* Returns names list of pages to pack basing on the existing
* pages and pages names passed through the first argument, returns
* error of at least one of passed pages is not exist. Addes "cmn"
* page to the list if particular pages are not passed.
*
* @param cb function
*/
function getPagesToPack(pages, rootPath, cb) {
getExistingPages(rootPath, function(err, existing) {
if (err) return cb(err);
if (pages.length) {
_.each(pages, function(page) {
if (!~existing.indexOf(page)) {
cb(new Error('cannot find page ' + page));
return false;
}
});
} else {
pages = existing.concat('cmn');
}
cb(null, pages);
});
}
/**
* Looks through the pages directory and
* returns names list of existing pages
*
* @param cb function
*/
function getExistingPages(rootPath, cb) {
async.waterfall([
function(cb) {
fs.readdir(rootPath, cb);
},
function(files, cb) {
var pages = [];
async.forEach(files, function(file, cb) {
fs.stat(path.join(rootPath, file), function(err, stat) {
if (err) return cb(err);
if (stat.isDirectory()) {
pages.push(file);
}
cb();
});
}, function(err) {
if (err) return cb(err);
cb(null, pages);
});
}
], cb);
}
/*
*
*/
function buildPagesDist(pages, dir, cb) {
var map = {};
var groups = {
css : ['base', 'ie7', 'ie8', 'ie9'],
js : ['base', 'ie7', 'ie8', 'ie9']
};
async.forEachSeries(pages, function(page, cb) {
buildPageDist(page, groups, dir, function(err, files) {
map[page] = files;
cb(err);
});
}, function(err) {
if (err) return cb(err);
cb(null, map);
});
}
/*
*
*/
function buildPageDist(page, groups, dir, cb) {
log(' ...packing %s:', page);
var res = {css: {}, js: {}};
async.series([
function(cb) {
async.forEachSeries(groups.css, function(group, cb) {
log(' • css %s', group);
buildCssDist(page, group, dir, function(err, files) {
if (err) return cb(err);
if (files.length) res.css[group] = files;
cb();
});
}, cb);
},
function(cb) {
async.forEachSeries(groups.js, function(group, cb) {
log(' • js %s', group);
buildJSDist(page, group, dir, function(err, files) {
if (err) return cb(err);
if (files.length) res.js[group] = files;
cb();
});
}, cb);
}
], function(err) {
if (err) return cb(err);
cb(null, res);
});
}
/*
*
*/
function buildCssDist(page, group, dir, cb) {
var srcs = getSources(page)
, csses = srcs['css_' + group]
, lesses = srcs['less_' + group];
if ((!csses || !csses.length) &&
(!lesses || !lesses.length)) {
log(' \033[90m✘ nothing to build here\033[39m');
return cb(null, []);
}
var dist = path.join(dir, sprintf('%s.%s.css', page, group));
async.waterfall([
function(cb) {
collectCss(csses, dist, cb);
},
function(stat, cb) {
stat && falog('concatted *.css files\t\t\t', stat);
compileLess(lesses, srcs.includes, dist, cb);
},
function(stat, cb) {
stat && falog('compiled *.less files\t\t\t', stat);
buildSprites(dist, cb);
},
function(stat, cb) {
stat && falog('built sprites with Tailor\t\t\t', stat);
optimizeCSS(dist, cb);
},
function(stat, cb) {
stat && falog('optimized with CSSO\t\t\t', stat);
embedBase64(dist, cb);
},
function(stat, cb) {
stat && falog('embedded images and asset hosts\t\t', stat);
cb(null, dist);
}
], cb);
}
/*
*
*/
function collectCss(files, dist, cb) {
if (!files || !files.length) {
return cb(null, null);
}
files = _.map(files, function(file) {
return cfg.STATIC_ROOT + '/' + file;
});
async.waterfall([
function(cb) {
utils.concatFiles(files, cb);
},
function(css, cb) {
utils.appendFile(dist, css, cb);
}
], cb);
}
/*
*
*/
function compileLess(lesses, inclds, dist, cb) {
if (!lesses || !lesses.length)
return cb(null, null);
var dir = cfg.STATIC_ROOT + '/';
var files = [];
files = files.concat(inclds || []);
files = files.concat(lesses);
var less = require('less');
async.waterfall([
function(cb) {
// Cancat all less files before rendering and patch
// all the image urls to be relative to the static dir
async.concatSeries(files, function(file, cb) {
utils.readFile(dir + file, function(err, cont) {
if (err) return cb(err);
if (cfg.LESS_RELATIVE_URLS) {
var sub = 'background-image: url("';
var rep = 'background-image: url("' + path.dirname(file) + '/';
cb(null, cont.split(sub).join(rep));
} else {
cb(null, cont);
}
});
}, cb);
},
function(cont, cb) {
less.render(cont.join(''), cb);
},
function(css, cb) {
utils.appendFile(dist, css, cb);
}
], cb);
}
/*
*
*/
function optimizeCSS(file, cb) {
var ccso = require('csso');
async.waterfall([
function(cb) {
utils.readFile(file, cb);
},
function(cont, cb) {
utils.writeFile(file, ccso.justDoIt(cont), cb);
}
], cb);
}
/*
*
*/
function buildSprites(filePath, cb) {
try {
var tailor = require('tailor');
} catch (err) {
return cb(null, null);
}
tailor([filePath], {
outDirPath: path.dirname(filePath)
, rootDirPath: cfg.STATIC_ROOT
}, function(err, files) {
if (err) return cb(err);
if (files[0].length == 1) {
return cb(null, null);
}
fs.stat(filePath, cb);
});
}
/*
*
*/
function embedBase64(file, cb) {
utils.readFile(file, function(err, css) {
if (err) return cb(err);
var rex = /url\(([^\)]+)\)?embed/g;
var urls = css.match(rex) || [];
urls = urls.map(function(url) {
return url.replace('url("', '');
});
async.forEachSeries(urls, function(url, cb) {
var file;
file = url.replace('?embed', '');
file = path.join(cfg.STATIC_ROOT, file);
fs.readFile(file, function(err, cont) {
if (err) return cb(err);
var type = mime.lookup(file);
var base64 = new Buffer(cont).toString('base64');
css = css.split(url);
css = css.join('data:' + type + ';base64,' + base64);
cb();
});
}, function(err) {
if (err) return cb(err);
utils.writeFile(file, css, cb);
});
});
}
/*
*
*/
function buildJSDist(page, group, dir, cb) {
var srcs = getSources(page);
var jses = srcs['js_' + group];
if (!jses || !jses.length) {
log(' \033[90m✘ nothing to build here\033[39m');
return cb(null, []);
}
var dist = path.join(dir, sprintf('%s.%s.js', page, group));
collectJS(jses, dist, function(err, stat) {
if (err) return cb(err);
falog('concatted *.js files\t\t\t', stat);
if (program.readable) {
return cb(null, dist);
}
compressJS(dist, function() {
falog('compressed with UglifyJS\t\t\t', stat);
cb(null, dist);
});
});
}
/*
*
*/
function collectJS(files, dist, cb) {
files = _.map(files, function(file) {
return cfg.STATIC_ROOT + '/' + file;
});
async.waterfall([
function(cb) {
utils.concatFiles(files, cb);
},
function(css, cb) {
utils.appendFile(dist, css, cb);
}
], cb);
}
/*
*
*/
function compressJS(file, cb) {
var jsp = require("uglify-js").parser;
var pro = require("uglify-js").uglify;
async.waterfall([
function(cb) {
utils.readFile(file, cb);
},
function(cont, cb) {
var ast = jsp.parse(cont);
ast = pro.ast_mangle(ast);
ast = pro.ast_squeeze(ast);
utils.writeFile(file, pro.gen_code(ast), cb);
}
], cb);
}
/*
*
*/
function getSources(name) {
if (name == 'cmn') {
return require(getCmnSrcsPath());
} else {
return _.extend({}, require(getPageSrcsPath(name)),
_.pick(require(getCmnSrcsPath()), 'includes'));
}
}
/*
*
*/
function getPageSrcsPath(name) {
return sprintf('%s/%2$s/%2$s.json', cfg.PAGES_ROOT, name);
}
/*
*
*/
function getCmnSrcsPath() {
return sprintf('%s/cmn.json', cfg.PAGES_ROOT);
}