enb
Version:
Faster BEM/BEViS assembler
450 lines (432 loc) • 16.5 kB
JavaScript
/**
* DepsResolver
* ============
*/
var inherit = require('inherit');
var vm = require('vm');
var Vow = require('vow');
var vowFs = require('../fs/async-fs');
var 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';
/**
* DepsResolver — класс, раскрывающий deps'ы.
* @name DepsResolver
*/
module.exports = inherit({
/**
* Конструктор.
* @param {Level[]} levels
*/
__constructor: function (levels) {
this.levels = levels;
this.declarations = [];
this.resolved = {};
this.declarationIndex = {};
},
/**
* Раскрывает шорткаты deps'а.
* @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.view) {
(dep.mods || (dep.mods = {})).view = dep.view;
}
if (dep.skin) {
(dep.mods || (dep.mods = {})).skin = dep.skin;
}
if (dep.elem) {
if (dep.mods) {
Object.keys(dep.mods).forEach(function (modName) {
var modVals = dep.mods[modName];
if (!Array.isArray(modVals)) {
modVals = [modVals];
}
res = res.concat(modVals.map(function (modVal) {
return { name: dep.block || blockName, elem: dep.elem, modName: modName, modVal: modVal };
}));
});
} else if (dep.mod) {
res.push({ name: dep.block || blockName, elem: dep.elem, modName: dep.mod, modVal: dep.val });
} else {
res.push({ name: dep.block || blockName, elem: dep.elem });
}
} else if (dep.mod || dep.mods || dep.elems) {
if (dep.mod) {
res.push({ name: dep.block || blockName, modName: dep.mod, modVal: dep.val });
}
Object.keys(dep.mods || {}).forEach(function (modName) {
var modVals = dep.mods[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;
}
},
/**
* Раскрывает шорткаты для списка deps'ов.
* @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);
}
},
/**
* Возвращает deps'ы для декларации (с помощью levels).
* @param {Object} decl
* @returns {{mustDeps: Array, shouldDeps: Array}}
*/
getDeps: function (decl) {
var _this = this;
var mustDecls;
var 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 = {};
var shouldDepIndex = {};
mustDepIndex[declKey(decl)] = true;
var mustDeps = [];
if (decl.modName) {
if (decl.elem) {
mustDecls = [
{ name: decl.name, elem: decl.elem }
];
if (decl.modVal) {
mustDecls.push({ name: decl.name, elem: decl.elem, modName: decl.modName });
}
} else {
mustDecls = [
{ name: decl.name }
];
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 vowFs.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.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);
var index;
var 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);
}
},
/**
* Добавляет декларацию блока в резолвер.
* @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
});
}
},
/**
* Добавляет декларацию элемента в резолвер.
* @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
});
}
},
/**
* Добавляет декларацию в резолвер.
* @param {Object} decl
* @returns {Promise}
*/
addDecl: function (decl) {
var _this = this;
var 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] = true;
decl.depCount++;
}).then(function () {
return _this.addDecls(deps.shouldDeps);
});
});
},
/**
* Добавляет набор деклараций.
* @param {Array} decls
* @returns {Promise}
* @param {Function} [preCallback]
*/
addDecls: function (decls, preCallback) {
var promise = Vow.fulfill();
var _this = this;
decls.forEach(function (decl) {
promise = promise.then(function () {
if (preCallback) {
preCallback(decl);
}
return _this.addDecl(decl);
});
});
return promise;
},
/**
* Упорядочивает deps'ы, возвращает в порядке зависимостей.
* @returns {Array}
*/
resolve: function () {
var items = this.declarations.slice(0);
var result = [];
var hasChanges = true;
var newItems;
while (hasChanges) {
newItems = [];
hasChanges = false;
for (var i = 0, l = items.length; i < l; i++) {
var decl = items[i];
if (decl.depCount === 0) {
hasChanges = true;
for (var j = 0; j < l; j++) {
var subDecl = items[j];
if (subDecl.deps[decl.key]) {
delete subDecl.deps[decl.key];
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 (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 : '') : '');
}