diff --git a/.jscsrc b/.jscsrc index 98d818c..407724d 100644 --- a/.jscsrc +++ b/.jscsrc @@ -18,7 +18,6 @@ "disallowSpacesInNamedFunctionExpression": { "beforeOpeningRoundBrace": true }, - "requireMultipleVarDecl": true, "requireBlocksOnNewline": 1, "disallowPaddingNewlinesInBlocks": true, "disallowSpacesInsideArrayBrackets": "nested", @@ -27,7 +26,6 @@ "disallowQuotedKeysInObjects": "allButReserved", "disallowSpaceAfterObjectKeys": true, "requireCommaBeforeLineBreak": true, - "requireOperatorBeforeLineBreak": true, "disallowSpaceAfterPrefixUnaryOperators": true, "disallowSpaceBeforePostfixUnaryOperators": true, "requireSpaceBeforeBinaryOperators": true, diff --git a/.jshintrc b/.jshintrc index 31717cd..dd2eb65 100644 --- a/.jshintrc +++ b/.jshintrc @@ -13,7 +13,9 @@ "unused": true, "node": true, + "esversion": 6, "expr": true, - "sub": true + "sub": true, + "laxbreak": true } diff --git a/.travis.yml b/.travis.yml index 937f8ab..ff72bef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,6 @@ language: node_js matrix: include: - - node_js: "0.10" - - node_js: "0.12" - node_js: "4" env: COVERALLS=1 - node_js: "5" diff --git a/exlib/deps-old.js b/exlib/deps-old.js index 41e5f8f..d84a2a5 100644 --- a/exlib/deps-old.js +++ b/exlib/deps-old.js @@ -5,10 +5,14 @@ * Заимствованный. */ -var inherit = require('inherit'); -var vowFs = require('enb/lib/fs/async-fs'); var vm = require('vm'); -var Vow = require('vow'); + +var enb = require('enb'); +var vow = require('vow'); +var inherit = require('inherit'); +var stringifyEntity = require('bem-naming').stringify; + +var vowFs = enb.asyncFs || require('enb/lib/fs/async-fs'); var MustDeps = require('./must-deps'); module.exports.OldDeps = (function () { @@ -185,13 +189,13 @@ module.exports.OldDeps = (function () { var depsCount1 = this.getCount(); var depsCount2; - return Vow.when(this.expandOnceByFS()) + return vow.when(this.expandOnceByFS()) .then(function again(newDeps) { depsCount2 = newDeps.getCount(); if (depsCount1 !== depsCount2) { depsCount1 = depsCount2; - return Vow.when(newDeps.expandOnceByFS(), again); + return vow.when(newDeps.expandOnceByFS(), again); } return newDeps.clone(_this); @@ -228,7 +232,7 @@ module.exports.OldDeps = (function () { return newDeps; }); } else { - return Vow.fulfill(newDeps); + return vow.fulfill(newDeps); } }, @@ -243,20 +247,29 @@ module.exports.OldDeps = (function () { var _this = this; var tech = this.tech; - var files = tech.levels.getFilesByDecl(item.item.block, item.item.elem, item.item.mod, item.item.val) + var dep = item.item; + var entity = { block: dep.block }; + + dep.elem && (entity.elem = dep.elem); + dep.mod && (entity.modName = dep.mod); + dep.val && (entity.modVal = dep.val); + + var files = tech.levels.getFilesByEntity(entity) .filter(function (file) { - return file.suffix === 'deps.js'; + return file.tech === 'deps.js'; }); - var promise = Vow.fulfill(); + var promise = vow.fulfill(); files.forEach(function (file) { + var filename = file.path; + promise = promise.then(function () { - return vowFs.read(file.fullname, 'utf8').then(function (content) { + return vowFs.read(filename, 'utf8').then(function (content) { try { - _this.parse(vm.runInThisContext(content, file.fullname), item); + _this.parse(vm.runInThisContext(content, filename), item); } catch (e) { - throw new Error('Syntax error in file "' + file.fullname + '": ' + e.message); + throw new Error('Syntax error in file "' + filename + '": ' + e.message); } }); }); diff --git a/lib/deps/deps-resolver.js b/lib/deps/deps-resolver.js index 7f8a9e4..bd0c9f7 100644 --- a/lib/deps/deps-resolver.js +++ b/lib/deps/deps-resolver.js @@ -18,8 +18,8 @@ module.exports = inherit(/** @lends DepsResolver.prototype */{ * * @param {Level[]} levels */ - __constructor: function (levels) { - this.levels = levels; + __constructor: function (introspection) { + this.introspection = introspection; this.declarations = []; this.resolved = {}; this.declarationIndex = {}; @@ -34,7 +34,6 @@ module.exports = inherit(/** @lends DepsResolver.prototype */{ * @returns {Array} */ normalizeDep: function (dep, blockName, elemName) { - var levels = this.levels; if (typeof dep === 'string') { return [{ name: dep }]; } else { @@ -92,9 +91,7 @@ module.exports = inherit(/** @lends DepsResolver.prototype */{ } 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]; } @@ -175,15 +172,20 @@ module.exports = inherit(/** @lends DepsResolver.prototype */{ 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'; + files, + introspection = this.introspection, + entity = { block: decl.name }; + + decl.elem && (entity.elem = decl.elem); + decl.modName && (entity.modName = decl.modName); + decl.modVal && (entity.modVal = decl.modVal); + + files = introspection.getFilesByEntity(entity).filter(function (file) { + var suffix = file.tech; + + return suffix === 'deps.js' || suffix === 'deps.yaml'; }); + var mustDepIndex = {}, shouldDepIndex = {}; mustDepIndex[declKey(decl)] = true; @@ -213,13 +215,17 @@ module.exports = inherit(/** @lends DepsResolver.prototype */{ var shouldDeps = []; function keepWorking(file) { - return vfs.read(file.fullname, 'utf8').then(function (depContent) { - if (file.suffix === 'deps.js') { + var filename = file.path; + + return vfs.read(filename, 'utf8').then(function (depContent) { + var suffix = file.tech; + + if (suffix === 'deps.js') { var depData; try { depData = vm.runInThisContext(depContent); } catch (e) { - throw new Error('Syntax error in file "' + file.fullname + '": ' + e.message); + throw new Error('Syntax error in file "' + filename + '": ' + e.message); } depData = Array.isArray(depData) ? depData : [depData]; depData.forEach(function (dep) { @@ -258,13 +264,13 @@ module.exports = inherit(/** @lends DepsResolver.prototype */{ } } }); - } else if (file.suffix === 'deps.yaml') { + } else if (suffix === 'deps.yaml') { var depYamlStructure = yaml.safeLoad(depContent, { - filename: file.fullname, + filename: filename, strict: true }); if (!Array.isArray(depYamlStructure)) { - throw new Error('Invalid yaml deps structure at: ' + file.fullname); + throw new Error('Invalid yaml deps structure at: ' + filename); } _this.normalizeDeps(depYamlStructure, decl.name, decl.elem).forEach(function (nd) { var key = declKey(nd), @@ -291,7 +297,7 @@ module.exports = inherit(/** @lends DepsResolver.prototype */{ } }).fail(function (err) { if (err instanceof DepsError) { - err.message += ' in file "' + file.fullname + '"'; + err.message += ' in file "' + filename + '"'; } throw err; }); diff --git a/lib/introspection.js b/lib/introspection.js new file mode 100644 index 0000000..13edd66 --- /dev/null +++ b/lib/introspection.js @@ -0,0 +1,54 @@ +'use strict'; + +const stringifyEntity = require('bem-naming').stringify; +const parseEntity = require('bem-naming').parse; + +module.exports = class Introspection { + constructor (levels, introspections) { + this._levels = levels; + this._introspections = introspections; + } + getLevels () { + return this._levels; + } + getEntities () { + const entities = []; + + this._introspections.forEach(introspection => { + Object.keys(introspection).forEach(id => { + const entity = parseEntity(id); + + entities.push(entity); + }); + }); + + return entities; + } + getFilesByEntity (entity) { + const id = stringifyEntity(entity); + + let files = []; + this._introspections.forEach(introspection => { + files = files.concat(introspection[id] || []); + }); + + return files; + } + getFilesByTechs (techs) { + const res = []; + + this._introspections.forEach(introspection => { + Object.keys(introspection).forEach(id => { + const files = introspection[id]; + + files.forEach(file => { + if (techs.indexOf(file.tech) !== -1) { + res.push(file); + } + }); + }); + }); + + return res; + } +}; diff --git a/lib/levels/level.js b/lib/levels/level.js deleted file mode 100644 index d8bf4be..0000000 --- a/lib/levels/level.js +++ /dev/null @@ -1,219 +0,0 @@ -var inherit = require('inherit'), - fs = require('fs'), - vow = require('vow'), - path = require('path'); - -module.exports = inherit(/** @lends Level.prototype */{ - /** - * @constructs Level - * @classdesc Model of level. - * - * @param {String} levelPath — path to level. - */ - __constructor: function (levelPath) { - this._path = levelPath; - this.blocks = {}; - this._loadDeferred = vow.defer(); - }, - - /** - * Load level from cache. - */ - loadFromCache: function (data) { - this.blocks = data; - this._loadDeferred.resolve(this); - }, - - /** - * Returns blocks info. - * - * @returns {Object} - */ - getBlocks: function () { - return this.blocks; - }, - - /** - * Checks whether there is a block with the specified name. - * - * @param {String} blockName - * @returns {Boolean} - */ - hasBlock: function (blockName) { - return this.blocks[blockName]; - }, - - /** - * Returns absolute path to level. - * - * @returns {String} - */ - getPath: function () { - return this._path; - }, - - /** - * Processes file, adds it to `blocks` filed. - * - * @param {String} filename - * @param {Object} stat node.js Stat - * @param {String} parentElementName - * @param {String} elementName - * @param {String} modName - * @private - */ - _processFile: function (filename, stat, parentElementName, elementName, modName) { - var requiredBaseNameWithoutExt = (parentElementName ? parentElementName + '__' : '') + - elementName + (modName ? '_' + modName : ''), - baseName = filename.split(path.sep).slice(-1)[0], - baseNameParts = baseName.split('.'), - baseNameWithoutExt = stat.isDirectory() ? - baseNameParts.slice(0, baseNameParts.length - 1).join('.') : - baseNameParts[0], - rl = requiredBaseNameWithoutExt.length, - modVal, - processFile = baseNameWithoutExt.indexOf(requiredBaseNameWithoutExt) === 0 && ( - modName ? - (rl === baseNameWithoutExt.length) || baseNameWithoutExt.charAt(rl) === '_' : - baseNameWithoutExt === requiredBaseNameWithoutExt - ); - if (processFile) { - var suffix = stat.isDirectory() ? baseNameParts.pop() : baseNameParts.slice(1).join('.'), - fileInfo = { - name: baseName, - fullname: filename, - suffix: suffix, - mtime: stat.mtime.getTime(), - isDirectory: stat.isDirectory() - }; - if (fileInfo.isDirectory) { - fileInfo.files = filterFiles(fs.readdirSync(filename)).map(function (subFilename) { - var subFullname = path.join(filename, subFilename), - subStat = fs.statSync(subFullname); - return { - name: subFilename, - fullname: subFullname, - suffix: subFilename.split('.').slice(1).join('.'), - mtime: subStat.mtime.getTime(), - isDirectory: subStat.isDirectory() - }; - }); - } - var blockName = parentElementName || elementName, - block = this.blocks[blockName] || (this.blocks[blockName] = { - name: blockName, - files: [], - dirs: [], - elements: {}, - mods: {} - }), - destElement; - if (parentElementName) { - destElement = block.elements[elementName] || (block.elements[elementName] = { - name: elementName, - files: [], - dirs: [], - mods: {} - }); - } else { - destElement = block; - } - var collectionKey = fileInfo.isDirectory ? 'dirs' : 'files'; - if (modName) { - if (!modVal) { - if (rl !== baseNameWithoutExt.length) { - modVal = baseNameWithoutExt.substr(rl + 1); - } else { - modVal = '*'; - } - } - var mod = destElement.mods[modName] || (destElement.mods[modName] = {}), - modValueFiles = (mod[modVal] || (mod[modVal] = { files: [], dirs: [] }))[collectionKey]; - modValueFiles.push(fileInfo); - } else { - destElement[collectionKey].push(fileInfo); - } - } - }, - - /** - * Loads files and directories in modifier directory. - * - * @param {String} parentElementName - * @param {String} elementName - * @param {String} modName - * @param {String} modDirPath - * @private - */ - _loadMod: function (parentElementName, elementName, modName, modDirPath) { - var _this = this; - filterFiles(fs.readdirSync(modDirPath)).forEach(function (filename) { - var fullname = path.join(modDirPath, filename), - stat = fs.statSync(fullname); - _this._processFile(fullname, stat, parentElementName, elementName, modName); - }); - }, - - /** - * Loads files and directories in element or block directory (if `parentElementName` is not specifed). - * - * @param {String} parentElementName - * @param {String} elementName - * @param {String} elementDirPath - * @param {String} containsElements - * @private - */ - _loadElement: function (parentElementName, elementName, elementDirPath, containsElements) { - var _this = this, - requiredBaseNameWithoutExt = (parentElementName ? parentElementName + '__' : '') + elementName; - filterFiles(fs.readdirSync(elementDirPath)).forEach(function (filename) { - var fullname = path.join(elementDirPath, filename), - stat = fs.statSync(fullname); - if (stat.isDirectory()) { - if (containsElements && filename.substr(0, 2) === '__') { - _this._loadElement(elementName, filename.substr(2), fullname, false); - } else if (filename.charAt(0) === '_') { - _this._loadMod(parentElementName, elementName, filename.substr(1), fullname); - } else if (filename.indexOf('.') !== -1 && filename.indexOf(requiredBaseNameWithoutExt + '.') === 0) { - _this._processFile(fullname, stat, parentElementName, elementName); - } else if (containsElements) { - _this._loadElement(elementName, filename, fullname, false); - } - } else if (stat.isFile()) { - _this._processFile(fullname, stat, parentElementName, elementName); - } - }); - }, - - /** - * Loads a level. - * - * The structure of blocks, elements and modifiers stored in `blocks` filed. - */ - load: function () { - var deferred = this._loadDeferred, - promise = deferred.promise(); - if (promise.isFulfilled()) { - return promise; - } - - var _this = this, - levelPath = this._path; - - filterFiles(fs.readdirSync(levelPath)).forEach(function (blockDir) { - var blockDirPath = path.join(levelPath, blockDir); - if (fs.statSync(blockDirPath).isDirectory()) { - _this._loadElement(null, blockDir, blockDirPath, true); - } - }); - deferred.resolve(this); - - return promise; - } -}); - -function filterFiles(filenames) { - return filenames.filter(function (filename) { - return filename.charAt(0) !== '.'; - }); -} diff --git a/lib/levels/levels.js b/lib/levels/levels.js deleted file mode 100644 index 55ffcc7..0000000 --- a/lib/levels/levels.js +++ /dev/null @@ -1,215 +0,0 @@ -var inherit = require('inherit'); - -module.exports = inherit(/** @lends Levels.prototype */{ - /** - * @constructs Levels - * @classdesc Works with list of levels. - * @see {@link Level} - * - * @param {Level[]} items — levels instances. - */ - __constructor: function (items) { - this.items = items; - }, - - /** - * Returns info about blocks. - * - * @param {String} blockName - * @returns {Object[]} - */ - getBlocks: function (blockName) { - var block, - blocks = []; - for (var i = 0, l = this.items.length; i < l; i++) { - block = this.items[i].blocks[blockName]; - if (block) { - blocks.push(block); - } - } - return blocks; - }, - - /** - * Returns info about elements. - * - * @param {String} blockName - * @param {String} elemName - * @returns {Object[]} - */ - getElems: function (blockName, elemName) { - var block, - elements = []; - for (var i = 0, l = this.items.length; i < l; i++) { - block = this.items[i].blocks[blockName]; - if (block && block.elements[elemName]) { - elements.push(block.elements[elemName]); - } - } - return elements; - }, - - /** - * Returns files and directories for block or modifier of block. - * - * @param {String} blockName - * @param {String} modName - * @param {String} modVal - * @returns {{files: Object[], dirs: Object[]}} - */ - getBlockEntities: function (blockName, modName, modVal) { - var block, - files = [], - dirs = [], - blocks = this.getBlocks(blockName); - - modVal || (modVal = '*'); - - for (var i = 0, l = blocks.length; i < l; i++) { - block = blocks[i]; - if (modName) { - var mods = block.mods[modName], - res = mods && (mods[modVal] || mods['*']); - - if (res) { - files = files.concat(res.files); - dirs = dirs.concat(res.dirs); - } - } else { - files = files.concat(block.files); - dirs = dirs.concat(block.dirs); - } - } - return { files: files, dirs: dirs }; - }, - - /** - * Returns files and directories for element or modifier of element. - * - * @param {String} blockName - * @param {String} elemName - * @param {String} modName - * @param {String} modVal - * @returns {{files: Object[], dirs: Object[]}} - */ - getElemEntities: function (blockName, elemName, modName, modVal) { - var elem, - files = [], - dirs = [], - elems = this.getElems(blockName, elemName); - - modVal || (modVal = '*'); - - for (var i = 0, l = elems.length; i < l; i++) { - elem = elems[i]; - if (modName) { - var mods = elem.mods[modName], - res = mods && (mods[modVal] || mods['*']); - - if (res) { - files = files.concat(res.files); - dirs = dirs.concat(res.dirs); - } - } else { - files = files.concat(elem.files); - dirs = dirs.concat(elem.dirs); - } - } - return { files: files, dirs: dirs }; - }, - - /** - * Returns files for block or modifier of block. - * - * @param {String} blockName - * @param {String} modName - * @param {String} modVal - * @returns {Object[]} - */ - getBlockFiles: function (blockName, modName, modVal) { - return this.getBlockEntities(blockName, modName, modVal).files; - }, - - /** - * Returns files for element or modifier of element. - * - * @param {String} blockName - * @param {String} elemName - * @param {String} modName - * @param {String} modVal - * @returns {Object[]} - */ - getElemFiles: function (blockName, elemName, modName, modVal) { - return this.getElemEntities(blockName, elemName, modName, modVal).files; - }, - - /** - * Returns files by declaration. - * - * @param {String} blockName - * @param {String} elemName - * @param {String} modName - * @param {String} modVal - * @returns {Object[]} - */ - getFilesByDecl: function (blockName, elemName, modName, modVal) { - if (elemName) { - return this.getElemFiles(blockName, elemName, modName, modVal); - } else { - return this.getBlockFiles(blockName, modName, modVal); - } - }, - - /** - * Returns files by suffix. - * - * @param {String} suffix - * @returns {Object[]} - */ - getFilesBySuffix: function (suffix) { - var files = []; - this.items.forEach(function (level) { - var blocks = level.blocks; - Object.keys(blocks).forEach(function (blockName) { - files = files.concat(getFilesInElementBySuffix(blocks[blockName], suffix)); - }); - }); - return files; - }, - - /** - * Returns value of block modifier. - * - * @param {String} blockName - * @param {String} modName - * @returns {String[]} - */ - getModValues: function (blockName, modName) { - var modVals = []; - this.items.forEach(function (level) { - var blockInfo = level.blocks[blockName]; - if (blockInfo && blockInfo.mods && blockInfo.mods[modName]) { - modVals = modVals.concat(Object.keys(blockInfo.mods[modName])); - } - }); - return modVals; - } -}); - -function getFilesInElementBySuffix(element, suffix) { - var files = element.files.filter(function (f) { return f.suffix === suffix; }), - mods = element.mods, - elements = element.elements; - Object.keys(mods).forEach(function (modName) { - var mod = mods[modName]; - Object.keys(mod).forEach(function (modVal) { - files = files.concat(mod[modVal].files.filter(function (f) { return f.suffix === suffix; })); - }); - }); - if (elements) { - Object.keys(elements).forEach(function (elemName) { - files = files.concat(getFilesInElementBySuffix(elements[elemName], suffix)); - }); - } - return files; -} diff --git a/package.json b/package.json index 3300f1c..e9d6526 100644 --- a/package.json +++ b/package.json @@ -37,18 +37,20 @@ "Marat Dulin " ], "engines": { - "node": ">= 0.10.0" + "node": ">= 4" }, "peerDependencies": { "enb": ">=0.15.0 <2.0.0" }, "dependencies": { "bem-naming": "1.0.1", + "bem-walk": "1.0.0-1", "clear-require": "1.0.1", "enb-async-require": "1.0.1", "enb-require-or-eval": "1.0.2", "inherit": "2.2.3", "js-yaml": "3.5.2", + "lodash": "4.11.1", "vow": "0.4.12" }, "devDependencies": { @@ -57,7 +59,6 @@ "istanbul": "0.4.2", "jscs": "2.8.0", "jshint": "2.9.1", - "lodash": "4.0.0", "matcha": "0.6.1", "mocha": "2.3.4", "mock-enb": "0.3.2", diff --git a/techs/deps-old.js b/techs/deps-old.js index dd5e308..8b8c356 100644 --- a/techs/deps-old.js +++ b/techs/deps-old.js @@ -1,10 +1,13 @@ -var inherit = require('inherit'), - vow = require('vow'), +var vow = require('vow'), + inherit = require('inherit'), enb = require('enb'), - vfs = enb.asyncFS || require('enb/lib/fs/async-fs'), - BaseTech = enb.BaseTech || require('enb/lib/tech/base-tech'), asyncRequire = require('enb-async-require'), clearRequire = require('clear-require'), + + vfs = enb.asyncFS || require('enb/lib/fs/async-fs'), + FileList = enb.FileList, + BaseTech = enb.BaseTech || require('enb/lib/tech/base-tech'), + OldDeps = require('../exlib/deps-old').OldDeps, deps = require('../lib/deps/deps'); @@ -73,8 +76,9 @@ module.exports = inherit(BaseTech, { strictMode = this._strict; return node.requireSources([this._levelsTarget, this._declFile]) - .spread(function (levels, sourceDeps) { - var depFiles = levels.getFilesBySuffix('deps.js'); + .spread(function (introspection, sourceDeps) { + var depFiles = introspection.getFilesByTechs(['deps.js', 'deps.yaml']) + .map(file => FileList.getFileInfo(file.path)); if (cache.needRebuildFile('deps-file', targetFilename) || cache.needRebuildFile('decl-file', declFilename) || @@ -82,7 +86,7 @@ module.exports = inherit(BaseTech, { ) { return requireSourceDeps(sourceDeps, declFilename) .then(function (sourceDeps) { - return (new OldDeps(sourceDeps, strictMode).expandByFS({ levels: levels })) + return (new OldDeps(sourceDeps, strictMode).expandByFS({ levels: introspection })) .then(function (resolvedDeps) { var resultDeps = resolvedDeps.getDeps(), loopPaths = resolvedDeps.getLoops().mustDeps.map(function (loop) { diff --git a/techs/deps.js b/techs/deps.js index b2e5dd5..9684108 100644 --- a/techs/deps.js +++ b/techs/deps.js @@ -1,10 +1,12 @@ var inherit = require('inherit'), vow = require('vow'), enb = require('enb'), - vfs = enb.asyncFS || require('enb/lib/fs/async-fs'), - BaseTech = enb.BaseTech || require('enb/lib/tech/base-tech'), asyncRequire = require('enb-async-require'), clearRequire = require('clear-require'), + + FileList = enb.FileList, + vfs = enb.asyncFS || require('enb/lib/fs/async-fs'), + BaseTech = enb.BaseTech || require('enb/lib/tech/base-tech'), DepsResolver = require('../lib/deps/deps-resolver'), deps = require('../lib/deps/deps'); @@ -66,8 +68,9 @@ module.exports = inherit(BaseTech, { declFilename = this.node.resolvePath(this._declFile); return this.node.requireSources([this._levelsTarget, this._declFile]) - .spread(function (levels, sourceDeps) { - var depFiles = levels.getFilesBySuffix('deps.js').concat(levels.getFilesBySuffix('deps.yaml')); + .spread(function (introspection, sourceDeps) { + var depFiles = introspection.getFilesByTechs(['deps.js', 'deps.yaml']) + .map(file => FileList.getFileInfo(file.path)); if (cache.needRebuildFile('deps-file', targetFilename) || cache.needRebuildFile('decl-file', declFilename) || @@ -75,7 +78,7 @@ module.exports = inherit(BaseTech, { ) { return requireSourceDeps(sourceDeps, declFilename) .then(function (sourceDeps) { - var resolver = new DepsResolver(levels), + var resolver = new DepsResolver(introspection), decls = resolver.normalizeDeps(sourceDeps); return resolver.addDecls(decls) diff --git a/techs/files.js b/techs/files.js index adcc64e..7cb2cbd 100644 --- a/techs/files.js +++ b/techs/files.js @@ -1,10 +1,15 @@ -var inherit = require('inherit'), +var path = require('path'), + + inherit = require('inherit'), vow = require('vow'), - deps = require('../lib/deps/deps'), enb = require('enb'), - BaseTech = enb.BaseTech || require('enb/lib/tech/base-tech'), asyncRequire = require('enb-async-require'), clearRequire = require('clear-require'), + stringifyEntity = require('bem-naming').stringify, + + deps = require('../lib/deps/deps'), + vfs = enb.asyncFs, + BaseTech = enb.BaseTech || require('enb/lib/tech/base-tech'), FileList = enb.FileList || require('enb/lib/file-list'); /** @@ -77,61 +82,45 @@ module.exports = inherit(BaseTech, { dirsTarget = this._dirsTarget; return this.node.requireSources([this._depsFile, this._levelsTarget]) - .spread(function (data, levels) { + .spread(function (data, introspection) { return requireSourceDeps(data, depsFilename) .then(function (sourceDeps) { var fileList = new FileList(), dirList = new FileList(), - levelPaths = levels.items.map(function (level) { - return level.getPath(); - }), - hash = {}; - - for (var i = 0, l = sourceDeps.length; i < l; i++) { - var dep = sourceDeps[i], - entities; - - if (dep.elem) { - entities = levels.getElemEntities(dep.block, dep.elem, dep.mod, dep.val); - } else { - entities = levels.getBlockEntities(dep.block, dep.mod, dep.val); - } - - slice(entities.files.filter(filter)).forEach(fileList.addFiles.bind(fileList)); - slice(entities.dirs.filter(filter)).forEach(dirList.addFiles.bind(dirList)); - } - - function slice(files) { - var slices = levelPaths.map(function () { return []; }), - uniqs = {}; - - files.forEach(function iterate(file) { - var filename = file.fullname; - - levelPaths.forEach(function (levelPath, index) { - if (!uniqs[filename] && filename.indexOf(levelPath) === 0) { - slices[index].push(file); - uniqs[filename] = true; - } - }); - }); + uniqs = {}; - return slices; - } + return vow.all(sourceDeps.map(function (entity) { + var id = stringifyEntity(entity); - function filter(file) { - var filename = file.fullname; + if (uniqs[id]) { return []; } + uniqs[id] = true; - if (hash[filename]) { - return false; - } + var slices = introspection._introspections.map(function (levelIntrospection) { + return levelIntrospection[id] || []; + }); - hash[filename] = true; - return true; - } + return vow.all(slices.map(function (slice) { + return vow.all(slice.map(getFileInfo)); + })); + })) + .then(function (res) { + res.forEach(function (slices) { + slices.forEach(function (slice) { + var files = []; + var dirs = []; + + slice.forEach(function (file) { + file.isDirectory ? dirs.push(file) : files.push(file); + }); + + fileList.addFiles(files); + dirList.addFiles(dirs); + }); + }); - _this.node.resolveTarget(filesTarget, fileList); - _this.node.resolveTarget(dirsTarget, dirList); + _this.node.resolveTarget(filesTarget, fileList); + _this.node.resolveTarget(dirsTarget, dirList); + }); }); }); }, @@ -139,6 +128,41 @@ module.exports = inherit(BaseTech, { clean: function () {} }); +/** + * Returns info about file for ENB FileList. + * + * @param {object} file - info about file. + * @returns {promise} + */ +function getFileInfo(file) { + var filename = file.path; + + return vfs.stat(filename) + .then(function (stats) { + var isDirectory = stats.isDirectory(); + var info = { + fullname: filename, + name: path.basename(filename), + suffix: file.tech, + mtime: stats.mtime, + isDirectory: isDirectory + }; + + if (!isDirectory) { + return info; + } + + return vfs.listDir(filename) + .then(function (basenames) { + info.files = basenames.map(function (basename) { + return FileList.getFileInfo(path.join(filename, basename)); + }); + + return info; + }); + }); +} + function requireSourceDeps(data, filename) { return (data ? vow.resolve(data) : ( clearRequire(filename), @@ -150,5 +174,16 @@ function requireSourceDeps(data, filename) { } return Array.isArray(sourceDeps) ? sourceDeps : sourceDeps.deps; + }) + .then(function (sourceDeps) { + return sourceDeps.map(function (dep) { + var entity = { block: dep.block }; + + dep.elem && (entity.elem = dep.elem); + dep.mod && (entity.modName = dep.mod); + dep.val && (entity.modVal = dep.val); + + return entity; + }); }); } diff --git a/techs/levels-to-bemdecl.js b/techs/levels-to-bemdecl.js index cc3663b..1d89ce3 100644 --- a/techs/levels-to-bemdecl.js +++ b/techs/levels-to-bemdecl.js @@ -1,10 +1,11 @@ var inherit = require('inherit'), enb = require('enb'), + asyncRequire = require('enb-async-require'), + clearRequire = require('clear-require'), + vfs = enb.asyncFS || require('enb/lib/fs/async-fs'), BaseTech = enb.BaseTech || require('enb/lib/tech/base-tech'), - deps = require('../lib/deps/deps'), - asyncRequire = require('enb-async-require'), - clearRequire = require('clear-require'); + deps = require('../lib/deps/deps'); /** * @class LevelsToBemdeclTech @@ -57,32 +58,28 @@ module.exports = inherit(BaseTech, { bemdeclFormat = this._bemdeclFormat, cache = node.getNodeCache(target); - return node.requireSources([this._source]).spread(function (levels) { + return node.requireSources([this._source]).spread(function (introspection) { if (cache.needRebuildFile('bemdecl-file', bemdeclFilename)) { - var resDeps = [], - decl = [], - data, - str; + var resDeps = introspection.getEntities().map(function (entity) { + var dep = { + block: entity.block + }; - levels.items.forEach(function (level) { - Object.keys(level.blocks).forEach(function (name) { - var block = level.blocks[name]; + entity.elem && (dep.elem = entity.elem); + entity.modName && (dep.mod = entity.modName); + entity.modVal && (dep.val = entity.modVal); - resDeps.push({ - block: name - }); - - processMods(resDeps, name, block.mods); - processElems(resDeps, name, block.elements); - }); - }); + return dep; + }), + data, + str; if (bemdeclFormat === 'deps') { - decl = resDeps; - data = { deps: decl }; - str = 'exports.deps = ' + JSON.stringify(decl, null, 4) + ';\n'; + data = { deps: resDeps }; + str = 'exports.deps = ' + JSON.stringify(resDeps, null, 4) + ';\n'; } else { - decl = deps.toBemdecl(resDeps); + var decl = deps.toBemdecl(resDeps); + data = { blocks: decl }; str = 'exports.blocks = ' + JSON.stringify(decl, null, 4) + ';\n'; } @@ -105,36 +102,3 @@ module.exports = inherit(BaseTech, { }); } }); - -function processElems(deps, block, elems) { - if (elems) { - Object.keys(elems).forEach(function (elemName) { - deps.push({ - block: block, - elem: elemName - }); - - processMods(deps, block, elems[elemName].mods, elemName); - }); - } -} - -function processMods(deps, block, mods, elem) { - if (mods) { - Object.keys(mods).forEach(function (modName) { - var vals = Object.keys(mods[modName]); - - vals.forEach(function (val) { - var dep = { - block: block, - mod: modName, - val: val === '*' ? true : val - }; - - elem && (dep.elem = elem); - - deps.push(dep); - }); - }); - } -} diff --git a/techs/levels.js b/techs/levels.js index cc8a813..9f393fe 100644 --- a/techs/levels.js +++ b/techs/levels.js @@ -1,11 +1,16 @@ -var path = require('path'), - inherit = require('inherit'), - vow = require('vow'), - enb = require('enb'), - vfs = enb.asyncFS || require('enb/lib/fs/async-fs'), - BaseTech = enb.BaseTech || require('enb/lib/tech/base-tech'), - Level = require('../lib/levels/level'), - Levels = require('../lib/levels/levels'); +'use strict'; + +const path = require('path'); + +const vow = require('vow'); +const enb = require('enb'); +const walk = require('bem-walk'); +const stringifyEntity = require('bem-naming').stringify; +const uniqBy = require('lodash').uniqBy; + +const vfs = enb.asyncFS || require('enb/lib/fs/async-fs'); +const buildFlow = enb.buildFlow || require('enb/lib/build-flow'); +const Introspection = require('../lib/introspection'); /** * @class LevelsTech @@ -40,85 +45,144 @@ var path = require('path'), * }]); * }; */ -module.exports = inherit(BaseTech, { - getName: function () { - return 'levels'; - }, - - init: function () { - this.__base.apply(this, arguments); - this._levelConfig = this.getRequiredOption('levels'); - this._sublevelDirectories = this.getOption('sublevelDirectories', ['blocks']); - this._target = this.node.unmaskTargetName(this.getOption('target', '?.levels')); - }, - - getTargets: function () { - return [this._target]; - }, - - build: function () { - var _this = this, - root = this.node.getRootDir(), - target = this._target, - levelList = [], - levelsToCache = [], - levelsIndex = {}, - cache = this.node.getNodeCache(target), - levelConfig = _this._levelConfig; - - for (var i = 0, l = levelConfig.length; i < l; i++) { - var levelInfo = levelConfig[i]; - - levelInfo = typeof levelInfo === 'object' ? levelInfo : { path: levelInfo }; - - var levelPath = path.resolve(root, levelInfo.path), - levelKey = 'level:' + levelPath; - if (levelsIndex[levelPath]) { - continue; - } - levelsIndex[levelPath] = true; - if (!this.node.buildState[levelKey]) { - var level = new Level(levelPath); - if (levelInfo.check === false) { - var blocks = cache.get(levelPath); - if (blocks) { - level.loadFromCache(blocks); - } else { - levelsToCache.push(level); - } - } - this.node.buildState[levelKey] = level; - } - levelList.push(this.node.buildState[levelKey]); - } +module.exports = buildFlow.create() + .name('levels') + .target('target', '?.levels') + .defineRequiredOption('levels') + .defineOption('sublevelDirectories', ['blocks']) + .saver(function () {}) + .needRebuild(function () { + return true; + }) + .builder(function () { + const target = this._target; + const node = this.node; + const scan = this.scanLevel.bind(this); - return vfs.listDir(path.join(_this.node.getRootDir(), _this.node.getPath())) - .then(function (listDir) { - return _this._sublevelDirectories.filter(function (path) { - return listDir.indexOf(path) !== -1; - }); - }) - .then(function (sublevels) { - return vow.all(sublevels.map(function (path) { - var sublevelPath = _this.node.resolvePath(path); - if (!levelsIndex[sublevelPath]) { - levelsIndex[sublevelPath] = true; - levelList.push(new Level(sublevelPath)); - } - })); + return this.getLevels() + .then(function (levels) { + return vow.all(levels.map(scan)) + .then(function (introspections) { + return [levels, introspections]; + }); }) - .then(function () { - return vow.all(levelList.map(function (level) { - return level.load(); - })) - .then(function () { - levelsToCache.forEach(function (level) { - cache.set(level.getPath(), level.getBlocks()); + .spread(function (levels, introspections) { + const levelPaths = levels.map(function (level) { return level.path; }); + + node.resolveTarget(target, new Introspection(levelPaths, introspections)); + }, this); + }) + .methods({ + /** + * Returns levels for current bundle. + * + * @returns {{path: string, check: boolean}[]} + */ + getLevels: function () { + const sourceLevels = this._initLevels(this._levels); + + return this._findSublevels() + .then(function (sublevels) { + return uniqBy(sourceLevels.concat(sublevels), 'path'); + }); + }, + /** + * Scans specified level. + * + * If level has `check: false` option and was scanned previously then introspection will be taken from cache. + * + * If the level of need for several bundles then it will be scanned only once. + * + * @param {{path: string, check: boolean}[]} level + * @returns {promise} + */ + scanLevel: function (level) { + const node = this.node; + const cache = node.getNodeCache(this._target); + const state = node.buildState; + const scan = this._forceScanLevel.bind(this); + const levelpath = level.path; + const key = 'level:' + levelpath; + + let promise = state[key]; + if (promise) { return promise; } + + if (level.check === false) { + var data = cache.get(key); + + promise = data ? vow.resolve(data) + : scan(levelpath) + .then(function (introspection) { + cache.set(key, introspection); + + return introspection; }); - _this.node.resolveTarget(target, new Levels(levelList)); - }); + } else { + promise = scan(levelpath); + } + + state[key] = promise; + + return promise; + }, + /** + * Processes the `levels` option. + * + * @returns {{path: string, check: boolean}[]} + */ + _initLevels: function () { + const root = this.node.getRootDir(); + + return this._levels.map(function (level) { + const levelpath = typeof level === 'object' ? level.path : level; + + return { + path: path.resolve(root, levelpath), + check: level.hasOwnProperty('check') ? level.check : true + }; }); - }, + }, + /** + * Finds special levels for current bundle. + * + * @returns {{path: string, check: boolean}[]} + */ + _findSublevels: function () { + const dir = path.join(this.node.getDir()); + const patterns = this._sublevelDirectories; + + return vfs.listDir(dir) + .then(function (basenames) { + return basenames + .filter(function (basename) { + return patterns.indexOf(basename) !== -1; + }) + .map(function (basename) { + return { path: path.join(dir, basename), check: false }; + }); + }); + }, + /** + * Scans specified level. + * + * The cache of current node will be ignored and scan happen again. + * + * @param {string} levelpath - path to level. + * @returns {promise} + */ + _forceScanLevel: function (levelpath) { + return new vow.Promise(function (resolve, reject) { + const data = {}; - clean: function () {} -}); + walk([levelpath]) + .on('data', function (file) { + const id = stringifyEntity(file.entity); + + (data[id] || (data[id] = [])).push(file); + }) + .on('error', reject) + .on('end', function () { resolve(data); }); + }); + } + }) + .createTech(); diff --git a/test/techs/deps-by-tech-to-bemdecl.test.js b/test/techs/deps-by-tech-to-bemdecl.test.js index 465c747..0e5bfdf 100644 --- a/test/techs/deps-by-tech-to-bemdecl.test.js +++ b/test/techs/deps-by-tech-to-bemdecl.test.js @@ -11,7 +11,7 @@ var path = require('path'), depsTech = techs.deps, depsByTechToBemdecl = techs.depsByTechToBemdecl; -describe('techs: deps', function () { +describe('techs: deps-by-tech-to-bemdecl', function () { afterEach(function () { mockFs.restore(); }); diff --git a/test/techs/levels-to-bemdecl.test.js b/test/techs/levels-to-bemdecl.test.js index 1446970..8bdeb44 100644 --- a/test/techs/levels-to-bemdecl.test.js +++ b/test/techs/levels-to-bemdecl.test.js @@ -49,7 +49,6 @@ describe('techs: levels-to-bemdecl', function () { } }, bemdecl = [ - { name: 'block' }, { name: 'block', mods: [{ name: 'bool-mod', vals: [{ name: true }] }] } ]; @@ -67,7 +66,6 @@ describe('techs: levels-to-bemdecl', function () { } }, bemdecl = [ - { name: 'block' }, { name: 'block', mods: [{ name: 'mod-name', vals: [{ name: 'mod-val' }] }] } ]; @@ -85,7 +83,6 @@ describe('techs: levels-to-bemdecl', function () { } }, bemdecl = [ - { name: 'block' }, { name: 'block', elems: [{ name: 'elem-name' }] } ]; @@ -105,8 +102,6 @@ describe('techs: levels-to-bemdecl', function () { } }, bemdecl = [ - { name: 'block' }, - { name: 'block', elems: [{ name: 'elem-name' }] }, { name: 'block', elems: [ { name: 'elem-name', mods: [{ name: 'bool-mod', vals: [{ name: true }] }] } ] } @@ -128,8 +123,6 @@ describe('techs: levels-to-bemdecl', function () { } }, bemdecl = [ - { name: 'block' }, - { name: 'block', elems: [{ name: 'elem-name' }] }, { name: 'block', elems: [{ name: 'elem-name', mods: [{ name: 'mod-name', vals: [{ name: 'mod-val' }] }] diff --git a/test/techs/levels.test.js b/test/techs/levels.test.js index b3b6f6d..db5f19e 100644 --- a/test/techs/levels.test.js +++ b/test/techs/levels.test.js @@ -1,7 +1,6 @@ var path = require('path'), mockFs = require('mock-fs'), - naming = require('bem-naming'), TestNode = require('mock-enb/lib/mock-node'), Tech = require('../utils/techs').levels; @@ -19,9 +18,21 @@ describe('techs: levels', function () { } } }, - files = ['blocks/block/block.ext']; + expected = { + block: [ + { + entity: { block: 'block' }, + tech: 'ext', + path: path.resolve('./blocks/block/block.ext'), + level: path.resolve('./blocks') + } + ] + }; - return hasFiles(scheme, files); + return assert(scheme, { + levels: [path.resolve('blocks')], + introspection: expected + }); }); it('must detect block dir in level', function () { @@ -32,9 +43,21 @@ describe('techs: levels', function () { } } }, - dirs = ['blocks/block/block.ext']; + expected = { + block: [ + { + entity: { block: 'block' }, + tech: 'ext', + path: path.resolve('./blocks/block/block.ext'), + level: path.resolve('./blocks') + } + ] + }; - return hasDirs(scheme, dirs); + return assert(scheme, { + levels: [path.resolve('blocks')], + introspection: expected + }); }); it('must detect boolean mod file of block in level', function () { @@ -47,9 +70,21 @@ describe('techs: levels', function () { } } }, - files = ['blocks/block/_bool-mod/block_bool-mod.ext']; + expected = { + 'block_bool-mod': [ + { + entity: { block: 'block', modName: 'bool-mod', modVal: true }, + tech: 'ext', + path: path.resolve('./blocks/block/_bool-mod/block_bool-mod.ext'), + level: path.resolve('./blocks') + } + ] + }; - return hasFiles(scheme, files); + return assert(scheme, { + levels: [path.resolve('blocks')], + introspection: expected + }); }); it('must detect boolean mod dir of block in level', function () { @@ -62,9 +97,21 @@ describe('techs: levels', function () { } } }, - dirs = ['blocks/block/_bool-mod/block_bool-mod.ext']; + expected = { + 'block_bool-mod': [ + { + entity: { block: 'block', modName: 'bool-mod', modVal: true }, + tech: 'ext', + path: path.resolve('./blocks/block/_bool-mod/block_bool-mod.ext'), + level: path.resolve('./blocks') + } + ] + }; - return hasDirs(scheme, dirs); + return assert(scheme, { + levels: [path.resolve('blocks')], + introspection: expected + }); }); it('must detect mod file of block in level', function () { @@ -77,9 +124,21 @@ describe('techs: levels', function () { } } }, - files = ['blocks/block/_mod-name/block_mod-name_mod-val.ext']; + expected = { + 'block_mod-name_mod-val': [ + { + entity: { block: 'block', modName: 'mod-name', modVal: 'mod-val' }, + tech: 'ext', + path: path.resolve('./blocks/block/_mod-name/block_mod-name_mod-val.ext'), + level: path.resolve('./blocks') + } + ] + }; - return hasFiles(scheme, files); + return assert(scheme, { + levels: [path.resolve('blocks')], + introspection: expected + }); }); it('must detect mod dir of block in level', function () { @@ -92,9 +151,21 @@ describe('techs: levels', function () { } } }, - dirs = ['blocks/block/_mod-name/block_mod-name_mod-val.ext']; + expected = { + 'block_mod-name_mod-val': [ + { + entity: { block: 'block', modName: 'mod-name', modVal: 'mod-val' }, + tech: 'ext', + path: path.resolve('./blocks/block/_mod-name/block_mod-name_mod-val.ext'), + level: path.resolve('./blocks') + } + ] + }; - return hasDirs(scheme, dirs); + return assert(scheme, { + levels: [path.resolve('blocks')], + introspection: expected + }); }); it('must detect elem file of block in level', function () { @@ -107,9 +178,21 @@ describe('techs: levels', function () { } } }, - files = ['blocks/block/__elem-name/block__elem-name.ext']; + expected = { + 'block__elem-name': [ + { + entity: { block: 'block', elem: 'elem-name' }, + tech: 'ext', + path: path.resolve('./blocks/block/__elem-name/block__elem-name.ext'), + level: path.resolve('./blocks') + } + ] + }; - return hasFiles(scheme, files); + return assert(scheme, { + levels: [path.resolve('blocks')], + introspection: expected + }); }); it('must detect elem dir of block in level', function () { @@ -122,9 +205,21 @@ describe('techs: levels', function () { } } }, - dirs = ['blocks/block/__elem-name/block__elem-name.ext']; + expected = { + 'block__elem-name': [ + { + entity: { block: 'block', elem: 'elem-name' }, + tech: 'ext', + path: path.resolve('./blocks/block/__elem-name/block__elem-name.ext'), + level: path.resolve('./blocks') + } + ] + }; - return hasDirs(scheme, dirs); + return assert(scheme, { + levels: [path.resolve('blocks')], + introspection: expected + }); }); it('must detect boolean mod file of elem in level', function () { @@ -139,9 +234,21 @@ describe('techs: levels', function () { } } }, - files = ['blocks/block/__elem-name/_bool-mod/block__elem-name_bool-mod.ext']; + expected = { + 'block__elem-name_bool-mod': [ + { + entity: { block: 'block', elem: 'elem-name', modName: 'bool-mod', modVal: true }, + tech: 'ext', + path: path.resolve('./blocks/block/__elem-name/_bool-mod/block__elem-name_bool-mod.ext'), + level: path.resolve('./blocks') + } + ] + }; - return hasFiles(scheme, files); + return assert(scheme, { + levels: [path.resolve('blocks')], + introspection: expected + }); }); it('must detect boolean mod dir of elem in level', function () { @@ -156,9 +263,21 @@ describe('techs: levels', function () { } } }, - dirs = ['blocks/block/__elem-name/_bool-mod/block__elem-name_bool-mod.ext']; + expected = { + 'block__elem-name_bool-mod': [ + { + entity: { block: 'block', elem: 'elem-name', modName: 'bool-mod', modVal: true }, + tech: 'ext', + path: path.resolve('./blocks/block/__elem-name/_bool-mod/block__elem-name_bool-mod.ext'), + level: path.resolve('./blocks') + } + ] + }; - return hasDirs(scheme, dirs); + return assert(scheme, { + levels: [path.resolve('blocks')], + introspection: expected + }); }); it('must detect mod file of elem in level', function () { @@ -173,9 +292,23 @@ describe('techs: levels', function () { } } }, - files = ['blocks/block/__elem-name/_mod-name/block__elem-name_mod-name_mod-val.ext']; + expected = { + 'block__elem-name_mod-name_mod-val': [ + { + entity: { block: 'block', elem: 'elem-name', modName: 'mod-name', modVal: 'mod-val' }, + tech: 'ext', + path: path.resolve( + './blocks/block/__elem-name/_mod-name/block__elem-name_mod-name_mod-val.ext' + ), + level: path.resolve('./blocks') + } + ] + }; - return hasFiles(scheme, files); + return assert(scheme, { + levels: [path.resolve('blocks')], + introspection: expected + }); }); it('must detect mod dir of elem in level', function () { @@ -190,9 +323,23 @@ describe('techs: levels', function () { } } }, - dirs = ['blocks/block/__elem-name/_mod-name/block__elem-name_mod-name_mod-val.ext']; + expected = { + 'block__elem-name_mod-name_mod-val': [ + { + entity: { block: 'block', elem: 'elem-name', modName: 'mod-name', modVal: 'mod-val' }, + tech: 'ext', + path: path.resolve( + './blocks/block/__elem-name/_mod-name/block__elem-name_mod-name_mod-val.ext' + ), + level: path.resolve('./blocks') + } + ] + }; - return hasDirs(scheme, dirs); + return assert(scheme, { + levels: [path.resolve('blocks')], + introspection: expected + }); }); it('must detect block files in levels', function () { @@ -208,12 +355,27 @@ describe('techs: levels', function () { } } }, - files = [ - 'level-1/block/block.ext', - 'level-2/block/block.ext' - ]; + expected = { + block: [ + { + entity: { block: 'block' }, + tech: 'ext', + path: path.resolve('./level-1/block/block.ext'), + level: path.resolve('./level-1') + }, + { + entity: { block: 'block' }, + tech: 'ext', + path: path.resolve('./level-2/block/block.ext'), + level: path.resolve('./level-2') + } + ] + }; - return hasFiles(scheme, files); + return assert(scheme, { + levels: [path.resolve('level-1'), path.resolve('level-2')], + introspection: expected + }); }); it('must detect block dirs in levels', function () { @@ -229,38 +391,31 @@ describe('techs: levels', function () { } } }, - dirs = [ - 'level-1/block/block.ext', - 'level-2/block/block.ext' - ]; - - return hasDirs(scheme, dirs); - }); - - it('must handle full paths', function () { - mockFs({ - blocks: { - block: { - 'block.ext': '' - } - }, - bundle: {} - }); - - var bundle = new TestNode('bundle'), - levelDirname = path.resolve('blocks'); - - return bundle.runTech(Tech, { levels: [levelDirname] }) - .then(function (levels) { - var files = getEntityFiles(levels, 'block', 'files'); + expected = { + block: [ + { + entity: { block: 'block' }, + tech: 'ext', + path: path.resolve('./level-1/block/block.ext'), + level: path.resolve('./level-1') + }, + { + entity: { block: 'block' }, + tech: 'ext', + path: path.resolve('./level-2/block/block.ext'), + level: path.resolve('./level-2') + } + ] + }; - files[0].name.must.be('block.ext'); - files.must.have.length(1); + return assert(scheme, { + levels: [path.resolve('level-1'), path.resolve('level-2')], + introspection: expected }); }); }); -function getLevels(fsScheme) { +function assert(fsScheme, expected) { var levels = Object.keys(fsScheme), bundle; @@ -269,37 +424,31 @@ function getLevels(fsScheme) { bundle = new TestNode('bundle'); - return bundle.runTech(Tech, { levels: levels }); -} - -function getEntityFiles(levels, entity, filetype) { - var notation = naming.parse(entity); - - if (notation.elem) { - return levels.getElemEntities(notation.block, notation.elem, notation.modName, notation.modVal)[filetype]; - } else { - return levels.getBlockEntities(notation.block, notation.modName, notation.modVal)[filetype]; - } -} + return bundle.runTech(Tech, { levels: levels }) + .then(function (data) { + var actual = { + levels: data._levels, + introspection: mergeIntrospections(data._introspections) + }; -function has(fsScheme, filenames, filetype) { - return getLevels(fsScheme) - .then(function (levels) { - filenames.forEach(function (filename, i) { - var basename = path.basename(filename).split('.')[0], - fullname = path.resolve(filename), - files = getEntityFiles(levels, basename, filetype); - - files.must.have.length(filenames.length); - files[i].fullname.must.be(fullname); - }); + actual.must.eql(expected); }); } -function hasFiles(fsScheme, filenames) { - return has(fsScheme, filenames, 'files'); -} +/** + * Merges introspection from several levels. + * + * @param {object[]} introspections + * @returns {object} + */ +function mergeIntrospections(introspections) { + var result = {}; + + introspections.forEach(function (introspection) { + Object.keys(introspection).forEach(function (id) { + result[id] = (result[id] || []).concat(introspection[id] || []); + }); + }); -function hasDirs(fsScheme, filenames) { - return has(fsScheme, filenames, 'dirs'); + return result; }