design-system-framework
Version:
Create and maintain web component libraries for websites
457 lines (359 loc) • 12.4 kB
JavaScript
var ComponentBase = require('./ComponentBase.js'),
_ = require('lodash'),
pathUtils = require('../lib/utils/path.js'),
path = require('path'),
mkdirp = require('mkdirp'),
fs = require('fs'),
async = require('async'),
glob = require('glob'),
gfile = require('gulp-file'),
gulp = require('gulp'),
through2 = require('through2'),
runSequence = require('run-sequence'),
chalk = require('chalk');
//var partialsRegex = /\{\{> ?([a-zA-Z\/\-_]+)/gm;
/**
* A directory containing HTML,CSS,JS
* @param {object} options
* * id {string} unique identifier
*/
function Component(options){
ComponentBase.call(this, options);
this.id = options.id;
this.resourcePaths = {};
this.cache = {};
this.partialRegistered = false;
this.missingPartial = false;
this.baseDependencies = [];
this.dependencyOf = []; // array of componentIds depending on this
this.cachedResourcePath = {};
this.variations = {
html: {},
css: {}
};
}
Component.prototype = Object.create(ComponentBase.prototype);
Component.prototype.build = function(callback) {
var self = this;
this._onBuildDone = callback;
async.series([
// override config with the component's own config.json
this.addLocalConfig.bind(this),
this.cacheResourcePathes.bind(this),
this.cacheHtml.bind(this),
this.cacheCss.bind(this),
// dependencies
this.resolveDependencies.bind(this),
function(cb){
self.loaded = true;
cb();
}
], function(){
if(self._onBuildDone){
self._onBuildDone.apply(self, arguments);
}
});
};
Component.prototype.rebuild = function(callback) {
var self = this;
this.log('rebuilding... '+this.id);
this.build(function(){
if(self.dependencyOf.length > 0){
var next = function(i){
if(!self.dependencyOf[i]){
callback();
return;
}
var dependentComponent = self.dsf.getComponent(self.dependencyOf[i]);
self.log('-> rebuild '+dependentComponent.id+' because it depends on '+self.id);
dependentComponent.rebuild(next.bind(null, i+1));
};
next(0);
}else{
callback();
}
});
};
Component.prototype.addLocalConfig = function(callback) {
var self = this;
this.getResourcePaths('config', function(err, paths){
if(paths.length===1){
self.dsf.util.file.readJSON(paths[0], function(err, localConfig){
if(err){
self.warning('WARNING: '+err);
callback();// fail silently
return;
}
_.merge(self.config, localConfig);
callback();
});
}else{
// no local config
callback();
}
});
};
Component.prototype.resolveDependencies = function(callback) {
var self = this,
dependencies = this.config.dependencies || [],
m,
re = /\{\{> ?([a-zA-Z\/\-_]+)/gm;
while ((m = re.exec(this.cache.html)) !== null) {
if (m.index === re.lastIndex) {
re.lastIndex++;
}
if(dependencies.indexOf(m[1]) === -1){
dependencies.push(m[1]);
}
}
if(this.isBaseCss){
this.dependencies = dependencies;
}else{
this.dependencies = this.baseDependencies.concat(dependencies);
}
if(this.dependencies.length > 0){
this.dsf.whenLoaded(this.dependencies, function(){
// mark dependencies as being a dependency of this
self.dependencies.forEach(function(dependencyId){
var dependency = self.dsf.getComponent(dependencyId);
dependency.addDependencyOf(self.id);
});
// and cache dependencies
self.cacheDependencies(callback);
});
}else{
callback();
}
};
Component.prototype.addDependencyOf = function(dependentComponentId) {
if(this.dependencyOf.indexOf(dependentComponentId) === -1){
this.dependencyOf.push(dependentComponentId);
}
};
Component.prototype.cacheDependencies = function(callback) {
var dependecyCss = '';
this.dependencies.forEach(function(dependencyId){
var dependency = this.dsf.getComponent(dependencyId);
if(dependency.isBaseCss){
return;
}
dependecyCss+='\n\n/* dependency: '+dependencyId+' */\n' + dependency.getCss(true) + '\n';
}, this);
this.cache.cssDependencies = dependecyCss;
//this.buildStandaloneCss();
callback();
};
Component.prototype.registerVariation = function(type, name, file) {
if(!this.variations[type]){
this.variations[type] = {};
}
this.variations[type][name] = file;
};
Component.prototype.hasVariations = function(type) {
return _.size(this.variations[type]) > 0;
};
/**
* Returns all existing files for the given resource type
* @param {string} type the type of resource (html, css, js, config)
* @param {Function} callback
* @return {void}
*/
Component.prototype.getResourcePaths = function(type, callback) {
var self = this;
glob(this.getGlobPath(type), function(err, files){
if(err) throw err;
self.cachedResourcePath[type] = files;
callback(null, files);
});
};
Component.prototype.getResourceHandler = function(type) {
var handler = this.dsf.getResourceHandler(type);
if(handler){
handler = handler.bind(null, this);
}
return handler;
};
Component.prototype.cacheResourcePathes = function(callback) {
var types = this.dsf.getResourceTypes();
async.map(types, this.getResourcePaths.bind(this), callback);
};
Component.prototype.cacheCss = function(cacheCssCallback) {
var self = this;
this.getResourcePaths('css',function(err, files){
if(files.length>0){
async.map(files, fs.readFile, function(err, files){
if(err) throw err;
async.reduce(files, '', function(memo, item, callback){
callback(null, memo + '\n' + item.toString());
}, function(err, css){
if(err) throw err;
self.cache.css = css;
cacheCssCallback();
});
});
}else{
cacheCssCallback();
}
});
};
Component.prototype.cacheHtml = function(callback) {
var self = this;
this.getResourcePaths('html',function(err, files){
if(files.length>0){
if(files.length>0){
if(self.hasVariations('html')){
self.cache.html = {};
self.cache.tpl = {};
async.mapSeries(Object.keys(self.variations.html), function(name, cb){
fs.readFile(self.variations.html[name], function(err, html){
html = html.toString();
self.cache.html[name] = html;
self.cache.tpl[name] = self.dsf.getHandlebars().compile(html);
self.dsf.getHandlebars().registerPartial(self.id + '/' + name, html);
// override previous "index"
if(name === 'index'){
self.dsf.getHandlebars().registerPartial(self.id, html);
}
cb();
});
}, callback);
}else{
// concat all
async.map(files, fs.readFile, function(err, files){
if(err) throw err;
async.reduce(files, '', function(memo, item, cb){
cb(null, memo + '\n' + item.toString());
}, function(err, html){
if(err) throw err;
self.cache.html = html;
self.cache.tpl = self.dsf.getHandlebars().compile(html);
self.dsf.getHandlebars().registerPartial(self.id , html);
callback();
});
});
}
}else{
callback();
}
}else{
callback();
}
});
};
// without base css
Component.prototype.getCss = function(withDependencies) {
// dependencies CSS after component CSS so user can't override dependencies
return (this.cache.css || '') + ((withDependencies && this.cache.cssDependencies) ? this.cache.cssDependencies : '');
};
Component.prototype.renderHtml = function(context, variation, callback) {
if(typeof variation === 'function'){
callback = variation;
variation = null;
}
if(!callback){
this.error('renderHtml called without callback');
return;
}
if(this.cache.tpl){
context = context || {};
if(this.config.vars){
context = _.merge({}, this.config.vars, context);
}
var tpl = this.cache.tpl;
if(typeof tpl === 'object'){
tpl = this.cache.tpl.index;
if(this.cache.tpl[variation]){
tpl = this.cache.tpl[variation];
}else{
this.error('variation '+variation+' does not exist');
}
}
var html = tpl(context);
this.process('html', html, callback);
}else{
callback(null, '');
}
};
Component.prototype.renderCss = function(callback) {
var self = this,
css = this.getCss(true);
if(arguments.length < 1){
this.error('renderCss called with not enough arguments');
return;
}
if(!this.isBaseCss){
this.dsf.getBaseCss(function(baseCss){
self.process('css', baseCss + css, callback);
});
}else{
self.process('css', css, callback);
}
};
Component.prototype.renderResource = function(type, callback) {
var handler = this.getResourceHandler(type);
handler(callback);
};
Component.prototype.process = function(type, str, callback) {
var self = this;
if(this.config.process && this.config.process[type] && typeof str === 'string' && str.trim()!==''){
var plugins = this.config.process[type];
var next = function(i){
if(plugins[i]){
gulpTask(self.id + '_' + plugins[i], str, require(plugins[i]), self.getDestPath(), function(err, out){
str = out;
next(i+1);
});
}else if(callback){
callback(null, str);
}
};
next(0);
}else if(callback){
callback(null, str);
}
};
function gulpTask(taskName, str, module, dest, callback){
gulp.task(taskName, function(){
return gfile(taskName, str, {src:true})
.pipe(module())
.pipe(gulp.dest(dest))
.pipe(through2.obj(function(file,enc,cb){
callback(null, file.contents.toString());
cb();
}));
});
//gulp.start([this.id + '.' + type]);
runSequence([taskName]);
}
Component.prototype.getDestPath = function() {
return path.join(__dirname, '../public/_built/'+this.id+'/');
};
Component.prototype.getDocument = function() {
if(!this.config.document){
return false;
}
return this.dsf.getHandlebars().compile(fs.readFileSync(path.join(this.absPath, this.config.document)).toString());
};
Component.prototype.toJson = function() {
var json = {
id: this.id,
resource: _.clone(this.cachedResourcePath)
};
return json;
};
function logId(){
return chalk.bgBlue(' '+this.id+' ');
}
Component.prototype.log = function(msg) {
msg = logId.call(this) + ' ' + msg;
this.dsf.log(msg);
};
Component.prototype.warning = function(msg) {
msg = logId.call(this) + ' ' + chalk.yellow(msg);
this.dsf.log(msg, true);
};
Component.prototype.error = function(msg) {
msg = logId.call(this) + ' ' + chalk.red(msg);
this.dsf.log(msg, true);
};
module.exports = Component;