enb-bem-techs-2x
Version:
BEM methodology for ENB
500 lines (476 loc) • 18.3 kB
JavaScript
var inherit = require('inherit'),
vm = require('vm'),
vow = require('vow'),
vfs = require('enb/lib/fs/async-fs'),
yaml = require('js-yaml');
function DepsError(message) {
this.message = message;
Error.captureStackTrace(this, DepsError);
}
DepsError.prototype = Object.create(Error.prototype);
DepsError.prototype.name = 'Deps error';
module.exports = inherit(/** @lends DepsResolver.prototype */{
/**
* @constructs DepsResolver
* @classdesc Dependencies resolver.
*
* @param {Level[]} levels
*/
__constructor: function (levels) {
this.levels = levels;
this.declarations = [];
this.resolved = {};
this.declarationIndex = {};
},
/**
* Normalizes shortcut to verbose array.
*
* @param {String|Object} dep
* @param {String} blockName
* @param {String} elemName
* @returns {Array}
*/
normalizeDep: function (dep, blockName, elemName) {
var levels = this.levels;
if (typeof dep === 'string') {
return [{ name: dep }];
} else {
var res = [];
if (!dep || !(dep instanceof Object)) {
throw new DepsError('Deps shoud be instance of Object or String');
}
if (dep.elem) {
(Array.isArray(dep.elem) ? dep.elem : [dep.elem]).forEach(function (elem) {
if (dep.mods) {
var elemModObj = {};
if (Array.isArray(dep.mods)) {
dep.mods.forEach(function (modName) {
elemModObj[modName] = true;
});
} else {
elemModObj = dep.mods || {};
}
Object.keys(elemModObj).forEach(function (modName) {
var modVals = elemModObj[modName];
if (!Array.isArray(modVals)) {
modVals = [modVals];
}
res = res.concat(modVals.map(function (modVal) {
return { name: dep.block || blockName, elem: elem, modName: modName, modVal: modVal };
}));
});
} else if (dep.mod) {
res.push({ name: dep.block || blockName, elem: elem, modName: dep.mod, modVal: dep.val });
} else {
res.push({ name: dep.block || blockName, elem: elem });
}
});
} else if (dep.mod || dep.mods || dep.elems) {
if (dep.mod) {
if (elemName) {
res.push(
{ name: dep.block || blockName, elem: elemName, modName: dep.mod },
{ name: dep.block || blockName, elem: elemName, modName: dep.mod, modVal: dep.val }
);
} else {
res.push(
{ name: dep.block || blockName, modName: dep.mod },
{ name: dep.block || blockName, modName: dep.mod, modVal: dep.val }
);
}
}
var modObj = {};
if (Array.isArray(dep.mods)) {
dep.mods.forEach(function (modName) {
modObj[modName] = true;
});
} else {
modObj = dep.mods || {};
}
Object.keys(modObj).forEach(function (modName) {
var modVals = modObj[modName];
if (modVals === '*') {
modVals = levels.getModValues(dep.block || blockName, modName);
}
if (!Array.isArray(modVals)) {
modVals = [modVals];
}
res = res.concat(modVals.map(function (modVal) {
if (elemName && !dep.block && !dep.elem) {
return { name: dep.block || blockName, elem: elemName, modName: modName, modVal: modVal };
} else {
return { name: dep.block || blockName, modName: modName, modVal: modVal };
}
}));
});
if (dep.elems) {
res.push({ name: dep.block || blockName });
var elems = dep.elems || [];
if (!Array.isArray(elems)) {
elems = [elems];
}
elems.forEach(function (elem) {
if (typeof elem === 'object') {
res.push({ name: dep.block || blockName, elem: elem.elem });
Object.keys(elem.mods || {}).forEach(function (modName) {
var modVals = elem.mods[modName];
if (!Array.isArray(modVals)) {
modVals = [modVals];
}
res = res.concat(modVals.map(function (modVal) {
return {
name: dep.block || blockName,
elem: elem.elem,
modName: modName,
modVal: modVal
};
}));
});
} else {
res.push({ name: dep.block || blockName, elem: elem });
}
});
}
} else {
res = [{ name: dep.block || blockName }];
}
if (dep.required) {
res.forEach(function (subDep) {
subDep.required = true;
});
}
return res;
}
},
/**
* Normalizes declaration with shortcuts to verbose array.
*
* @param {String|Object|Array} deps
* @param {String} [blockName]
* @param {String} [elemName]
* @returns {Array}
*/
normalizeDeps: function (deps, blockName, elemName) {
if (Array.isArray(deps)) {
var result = [];
for (var i = 0, l = deps.length; i < l; i++) {
result = result.concat(this.normalizeDep(deps[i], blockName, elemName));
}
return result;
} else {
return this.normalizeDep(deps, blockName, elemName);
}
},
/**
* Returns dependencies by {@link Levels}.
*
* @param {Object} decl
* @returns {{mustDeps: Array, shouldDeps: Array}}
*/
getDeps: function (decl) {
var _this = this,
mustDecls,
files;
if (decl.elem) {
files = this.levels.getElemFiles(decl.name, decl.elem, decl.modName, decl.modVal);
} else {
files = this.levels.getBlockFiles(decl.name, decl.modName, decl.modVal);
}
files = files.filter(function (file) {
return file.suffix === 'deps.js' || file.suffix === 'deps.yaml';
});
var mustDepIndex = {},
shouldDepIndex = {};
mustDepIndex[declKey(decl)] = true;
var mustDeps = [];
if (decl.modName) {
if (decl.elem) {
mustDecls = [
{ name: decl.name, elem: decl.elem, fake: true }
];
if (decl.modVal) {
mustDecls.push({ name: decl.name, elem: decl.elem, modName: decl.modName });
}
} else {
mustDecls = [
{ name: decl.name, fake: true }
];
if (decl.modVal) {
mustDecls.push({ name: decl.name, modName: decl.modName });
}
}
mustDecls.forEach(function (mustDecl) {
mustDecl.key = declKey(mustDecl);
mustDepIndex[mustDecl.key] = true;
mustDeps.push(mustDecl);
});
}
var shouldDeps = [];
function keepWorking(file) {
return vfs.read(file.fullname, 'utf8').then(function (depContent) {
if (file.suffix === 'deps.js') {
var depData;
try {
depData = vm.runInThisContext(depContent);
} catch (e) {
throw new Error('Syntax error in file "' + file.fullname + '": ' + e.message);
}
depData = Array.isArray(depData) ? depData : [depData];
depData.forEach(function (dep) {
if (!dep) {
return;
}
if (!dep.tech) {
if (dep.mustDeps) {
_this.normalizeDeps(dep.mustDeps, decl.name, decl.elem).forEach(function (nd) {
var key = declKey(nd);
if (!mustDepIndex[key]) {
mustDepIndex[key] = true;
nd.key = key;
mustDeps.push(nd);
}
});
}
if (dep.shouldDeps) {
_this.normalizeDeps(dep.shouldDeps, decl.name, decl.elem).forEach(function (nd) {
var key = declKey(nd);
if (!shouldDepIndex[key]) {
shouldDepIndex[key] = true;
nd.key = key;
shouldDeps.push(nd);
}
});
}
if (dep.noDeps) {
_this.normalizeDeps(dep.noDeps, decl.name, decl.elem).forEach(function (nd) {
var key = declKey(nd);
nd.key = key;
removeFromDeps(nd, mustDepIndex, mustDeps);
removeFromDeps(nd, shouldDepIndex, shouldDeps);
});
}
}
});
} else if (file.suffix === 'deps.yaml') {
var depYamlStructure = yaml.safeLoad(depContent, {
filename: file.fullname,
strict: true
});
if (!Array.isArray(depYamlStructure)) {
throw new Error('Invalid yaml deps structure at: ' + file.fullname);
}
_this.normalizeDeps(depYamlStructure, decl.name, decl.elem).forEach(function (nd) {
var key = declKey(nd),
index,
depList;
if (nd.required) {
index = mustDepIndex;
depList = mustDeps;
} else {
index = shouldDepIndex;
depList = shouldDeps;
}
if (!index[key]) {
index[key] = true;
nd.key = key;
depList.push(nd);
}
});
}
if (files.length > 0) {
return keepWorking(files.shift());
} else {
return null;
}
}).fail(function (err) {
if (err instanceof DepsError) {
err.message += ' in file "' + file.fullname + '"';
}
throw err;
});
}
function removeFromDeps(decl, index, list) {
if (index[decl.key]) {
for (var i = 0, l = list.length; i < l; i++) {
if (list[i].key === decl.key) {
return list.splice(i, 1);
}
}
} else {
index[decl.key] = true;
}
return null;
}
var result = { mustDeps: mustDeps, shouldDeps: shouldDeps };
if (files.length > 0) {
return keepWorking(files.shift()).then(function () {
return result;
});
} else {
return vow.fulfill(result);
}
},
/**
* Adds block to resolver.
*
* @param {String} blockName
* @param {String} modName
* @param {String} modVal
* @returns {Promise}
*/
addBlock: function (blockName, modName, modVal) {
if (modName) {
this.addDecl({
name: blockName,
modName: modName,
modVal: modVal
});
} else {
this.addDecl({
name: blockName
});
}
},
/**
* Adds element to resolver.
*
* @param {String} blockName
* @param {String} elemName
* @param {String} modName
* @param {String} modVal
* @returns {Promise}
*/
addElem: function (blockName, elemName, modName, modVal) {
if (modName) {
return this.addDecl({
name: blockName,
elem: elemName,
modName: modName,
modVal: modVal
});
} else {
return this.addDecl({
name: blockName,
elem: elemName
});
}
},
/**
* Adds declaration to resolver.
*
* @param {Object} decl
* @returns {Promise}
*/
addDecl: function (decl) {
var _this = this,
key = declKey(decl);
if (this.declarationIndex[key]) {
return null;
}
this.declarations.push(decl);
this.declarationIndex[key] = decl;
return this.getDeps(decl).then(function (deps) {
decl.key = key;
decl.deps = {};
decl.depCount = 0;
return _this.addDecls(deps.mustDeps, function (dep) {
decl.deps[dep.key] = dep.fake ? 'fake' : true;
decl.depCount++;
}).then(function () {
return _this.addDecls(deps.shouldDeps);
});
});
},
/**
* Adds declarations to resolver.
*
* @param {Array} decls
* @returns {Promise}
* @param {Function} [preCallback]
*/
addDecls: function (decls, preCallback) {
var promise = vow.fulfill(),
_this = this;
decls.forEach(function (decl) {
promise = promise.then(function () {
if (preCallback) {
preCallback(decl);
}
return _this.addDecl(decl);
});
});
return promise;
},
/**
* Supplements declaration of BEM entities. Returns them in dependencies order.
*
* @returns {Array}
*/
resolve: function () {
var items = this.declarations.slice(0),
result = [],
hasChanges = true,
newItems;
while (hasChanges) {
var i, j, l;
newItems = [];
hasChanges = false;
for (i = 0, l = items.length; i < l; i++) {
var decl = items[i];
if (decl.depCount === 0) {
hasChanges = true;
for (j = 0; j < l; j++) {
var subDecl = items[j],
subDeclKey = decl.key;
if (subDecl.deps[subDeclKey]) {
delete subDecl.deps[subDeclKey];
subDecl.depCount--;
}
}
var item = {
block: decl.name
};
if (decl.elem) {
item.elem = decl.elem;
}
if (decl.modName) {
item.mod = decl.modName;
if (decl.hasOwnProperty('modVal')) {
item.val = decl.modVal;
}
}
result.push(item);
} else {
newItems.push(decl);
}
}
items = newItems;
if (!hasChanges && items.length) {
for (i = 0, l = items.length; i < l; i++) {
var curDecl = items[i],
curDeps = curDecl.deps,
declKeys = Object.keys(curDeps);
for (j = 0; j < declKeys.length; j++) {
var declKey = declKeys[j];
if (curDeps[declKey] === 'fake') {
delete curDeps[declKey];
curDecl.depCount--;
hasChanges = true;
}
}
}
}
}
if (items.length) {
var errorMessage = items.map(function (item) {
return item.key + ' <- ' + Object.keys(item.deps).join(', ');
});
throw Error('Unresolved deps: \n' + errorMessage.join('\n'));
}
return result;
}
});
function declKey(decl) {
return decl.name + (decl.elem ? '__' + decl.elem : '') +
(decl.modName ? '_' + decl.modName + (decl.modVal ? '_' + decl.modVal : '') : '');
}