bem
Version:
678 lines (589 loc) • 23.5 kB
JavaScript
'use strict';
var Q = require('q'),
QFS = require('q-fs'),
bemUtil = require('./../util'),
PATH = require('./../path'),
INHERIT = require('inherit'),
LOGGER = require('./../logger'),
Tech = exports.Tech = INHERIT(/** @lends Tech.prototype */ {
/**
* Construct an instance of Tech.
*
* @class Tech base class
* @constructs
* @private
* @param {String} name Tech name.
* @param {String} path Tech module absolute path.
*/
__constructor: function(name, path) {
bemUtil.deprecate('Tech modules API V1 is not recommended to use, because it is slow.',
'Please use tech modules API V2, it makes your build process faster!');
this.techName = name;
this.techPath = path;
},
/**
* Set context to use in tech module.
*
* @public
* @param {Context} ctx Context instance object.
*/
setContext: function(ctx) {
this.context = ctx;
return this;
},
/**
* Return context.
*
* @public
* @returns {Context} Context instance object.
*/
getContext: function() {
return this.context;
},
/**
* Implementation of 'bem create block | elem | mod' commands.
*
* @param {Object} item BEM entity object.
* @param {Object} item.block BEM entity block name.
* @param {Object} item.elem BEM entity elem name.
* @param {Object} item.mod BEM entity modifier name.
* @param {Object} item.val BEM entity modifier value.
* @param {Level} level Level instance object.
* @param {Object} opts Additional options.
* @returns {Promise * Undefined}
*/
createByDecl: function(item, level, opts) {
opts = opts || {};
var prefix = level.getByObj(item),
vars = {
opts: opts,
BlockName: item.block,
Prefix: prefix
};
if (item.elem) vars.ElemName = item.elem;
if (item.mod) vars.ModName = item.mod;
if (item.val) vars.ModVal = item.val;
return this.create(prefix, vars, opts.force);
},
/**
* Implementation of 'bem create block | elem | mod' commands.
*
* @public
* @param {String} prefix Path prefix of object to create.
* @param {Object} vars Variables to use in template.
* @param {Boolean} force Force creation flag.
* @returns {Promise * Undefined}
*/
create: function(prefix, vars, force) {
return this.storeCreateResults(prefix, this.getCreateResults(prefix, vars), force);
},
/**
* Return create result for one tech suffix.
*
* @protected
* @param {String} path Full path of object to create.
* @param {String} suffix Suffix of object to create.
* @param {Object} vars Variables to use in template.
* @returns {Promise * String}
*/
getCreateResult: function(path, suffix, vars) {
return Q.resolve('');
},
/**
* Return create result for all tech suffixes.
*
* @protected
* @param {String} prefix Oath prefix of object to create.
* @param {Object} vars Variables to use in template.
* @returns {Promise * Object}
*/
getCreateResults: function(prefix, vars) {
var _this = this,
res = {};
return Q
.all(this.getCreateSuffixes().map(function(suffix) {
return Q.when(_this.getCreateResult(_this.getPath(prefix, suffix), suffix, vars))
.then(function(r) {
res[suffix] = r;
});
}))
.then(function() {
return res;
});
},
/**
* Store result of object creation for one suffix.
*
* @protected
* @param {String} path Full path of object to create.
* @param {String} suffix Suffix of object to create.
* @param {Object} res Result of object creation.
* @param {Boolean} force Force creation flag.
* @returns {Promise * Undefined}
*/
storeCreateResult: function(path, suffix, res, force) {
return QFS.exists(path).then(function(exists) {
if(exists && !force) {
return Q.reject("Already exists '" + path + "'");
}
// TODO: replace by promisy equivalent
bemUtil.mkdirs(PATH.dirname(path));
return bemUtil.writeFile(path, res);
});
},
/**
* Store results of object creation.
*
* @protected
* @param {String} prefix Path prefix of object to create.
* @param {Object} res Result of object creation.
* @param {Boolean} force Force creation flag.
* @returns {Promise * Undefined}
*/
storeCreateResults: function(prefix, res, force) {
var _this = this;
return Q.when(res, function(res) {
return Q.all(_this.getCreateSuffixes().map(function(suffix) {
return _this.storeCreateResult(_this.getPath(prefix, suffix), suffix, res[suffix], force);
})).get(0);
});
},
/**
* Read and return content of object identified by specified
* prefix for specified suffix.
*
* @protected
* @param {String} path Full path of object to read.
* @param {String} suffix Suffix of object to read.
* @returns {Promise * String}
*/
readContent: function(path, suffix) {
return QFS.exists(path).then(function(exists) {
if(!exists) return '';
return bemUtil.readFile(path);
});
},
/**
* Read and return content of object identified by specified prefix.
*
* @param {String} prefix Path prefix of object to read content of.
* @returns {Promise * Object}
*/
readAllContent: function(prefix) {
var _this = this,
res = {};
return Q
.all(this.getCreateSuffixes().map(function(suffix) {
return _this.readContent(_this.getPath(prefix, suffix), suffix)
.then(function(r) {
res[suffix] = r;
});
}))
.then(function() {
return res;
});
},
/**
* Implementation of 'bem build' command.
*
* @public
* @param {Object} decl BEM entities declaration.
* @param {Level[]} levels Array of levels.
* @param {String} output Path prefix of output.
* @returns {Promise * Undefined}
*/
buildByDecl: function(decl, levels, output) {
return this.build(this.getBuildPrefixes(this.transformBuildDecl(decl), levels),
PATH.dirname(output) + PATH.dirSep, PATH.basename(output));
},
/**
* Return transformed build declaration.
*
* @param {Promise * (Object|Array)} decl Initial declaration.
* @returns {Promise * (Object|Array)} Promise of transformed declaration.
*/
transformBuildDecl: function(decl) {
return Q.resolve(decl);
},
/**
* Return build prefixes for BEM entities declaration and levels.
*
* @param {Promise * (Object|Array)} decl Decraration of BEM entities to build.
* @param {Level[]} levels levels
* @returns {Promise * String[]} Prefixes of all BEM entities to build from.
*/
getBuildPrefixes: function(decl, levels) {
var prefixes = [],
eachLevel = function(getter, args) {
// for each level
levels.forEach(function (level) {
// collect file frefix
prefixes.push(level.get(getter, args));
});
},
forItemWithMods = function(block, elem) {
var item = elem || block,
type = elem? 'elem' : 'block',
args = elem? [block.name, elem.name] : [block.name];
// for block or elem
eachLevel(type, args);
// for each modifier
item.mods && item.mods.forEach(function (mod) {
// for modifier
eachLevel(type + '-mod', args.concat(mod.name));
// for each modifier value
mod.vals && mod.vals.forEach(function (val) {
eachLevel(type + '-mod-val', args.concat(mod.name, val.name || val));
});
});
},
forBlockDecl = function (block) {
// for block
forItemWithMods(block);
// for each elem in block
block.elems && block.elems.forEach(function (elem) {
forItemWithMods(block, elem);
});
},
forBlocksDecl = function (blocks) {
// for each block in declaration
blocks.forEach(forBlockDecl);
},
forDepsDecl = function (deps) {
deps.forEach(function (dep) {
if(dep.block) {
var getter = 'block',
args = [dep.block];
if(dep.elem) {
getter = 'elem';
args.push(dep.elem);
}
if(dep.mod) {
getter += '-mod';
args.push(dep.mod);
if(dep.val) {
getter += '-val';
args.push(dep.val);
}
}
eachLevel(getter, args);
}
});
};
return Q.when(decl, function(decl) {
decl.name && forBlockDecl(decl);
decl.blocks && forBlocksDecl(decl.blocks);
decl.deps && forDepsDecl(decl.deps);
return prefixes;
});
},
/**
* Implementation of 'bem build' command.
*
* @public
* @param {Promise * String[]} prefixes Prefixes of BEM entities to build from.
* @param {String} outputDir Dir to output result to.
* @param {String} outputName Prefix of output.
* @returns {Promise * Undefined}
*/
build: function(prefixes, outputDir, outputName) {
return this.storeBuildResults(
PATH.resolve(outputDir, outputName),
this.getBuildResults(prefixes, outputDir, outputName));
},
/**
* Filter prefixes with attached suffixes and return
* array of paths to aggregate during build process.
*
* @protected
* @param {Promise * String[]} prefixes Prefixes to filter.
* @param {String[]} suffixes Suffixes to append.
* @returns {Promise * String[]} Filtered paths.
*/
filterPrefixes: function(prefixes, suffixes) {
// Possible values: promised, callback, sync
var strategy = process.env.BEM_IO_STRATEGY_FILTER_PREFIXES || process.env.BEM_IO_STRATEGY,
_this = this;
['promised', 'callback', 'sync'].indexOf(strategy) !== -1 || (strategy = 'callback');
LOGGER.fverbose('Using %s strategy in Tech.filterPrefixes()', strategy);
return Q.when(prefixes, function(prefixes) {
if (strategy === 'promised') {
// Promised file existence check
return (function() {
var paths = [],
res = [],
counter = 0;
prefixes.forEach(function(prefix) {
suffixes.forEach(function(suffix) {
var path = _this.getPath(prefix, suffix);
res[counter++] = QFS.exists(path);
paths.push(path);
});
});
return Q.shallow(res)
.then(function(res) {
return paths.filter(function(path, index) {
return res[index];
});
});
})();
}
else if (strategy === 'callback') {
// Async file existence check
return (function() {
var paths = [];
prefixes.forEach(function(prefix) {
suffixes.forEach(function(suffix) {
paths.push(_this.getPath(prefix, suffix));
});
});
return bemUtil.filterPaths(paths);
})();
}
// Sync file existence check to get some ~100% build boost
// See https://github.com/bem/bem-tools/pull/156
return (function() {
var paths = [];
prefixes.forEach(function(prefix) {
suffixes.forEach(function(suffix) {
var path = _this.getPath(prefix, suffix);
if (PATH.existsSync(path)) paths.push(path);
});
});
return paths;
})();
});
},
/**
* Return build result chunk.
*
* @protected
* @param {String} relPath Relative path to source object.
* @param {String} path Path to source object.
* @param {String} suffix Suffix of source object.
* @returns {String} Build result chunk.
*/
getBuildResultChunk: function(relPath, path, suffix) {
return relPath + '\n';
},
/**
* Build and return result of build of specified prefixes
* for specified suffix.
*
* @protected
* @param {Promise * String[]} prefixes Prefixes to build from.
* @param {String} suffix Suffix to build result for.
* @param {String} outputDir Output dir name for build result.
* @param {String} outputName Output name of build result.
* @returns {Promise * String} Promise for build result.
*/
getBuildResult: function(prefixes, suffix, outputDir, outputName) {
var _this = this;
return Q.when(this.filterPrefixes(prefixes, [suffix]), function(paths) {
return Q.all(paths.map(function(path) {
return _this.getBuildResultChunk(
PATH.relative(outputDir, path), path, suffix);
}));
});
},
/**
* Build and return result of build of specified prefixes.
*
* @protected
* @param {Promise * String[]} prefixes Prefixes to build from.
* @param {String} outputDir Output dir name for build result.
* @param {String} outputName Output name of build result.
* @returns {Promise * Object} Promise for build results object.
*/
getBuildResults: function(prefixes, outputDir, outputName) {
var _this = this,
res = {};
return Q
.all(this.getBuildSuffixes().map(function(suffix) {
return _this.getBuildResult(prefixes, suffix, outputDir, outputName)
.then(function(r) {
res[suffix] = r;
});
}))
.then(function() {
return res;
});
},
/**
* Store result of build for specified suffix.
*
* @protected
* @param {String} path Path of object to store.
* @param {String} suffix Suffix of object to store.
* @param {String} res Result of build for specified suffix.
* @returns {Promise * Undefined}
*/
storeBuildResult: function(path, suffix, res) {
return bemUtil.writeFile(path, res);
},
/**
* Store results of build.
*
* @protected
* @param {String} prefix Prefix of object to build.
* @param {Promise * String} res Result of build.
* @return {Promise * Undefined}
*/
storeBuildResults: function(prefix, res) {
var _this = this;
return Q.when(res, function(res) {
return Q.all(_this.getBuildSuffixes().map(function(suffix) {
return _this.storeBuildResult(_this.getPath(prefix, suffix), suffix, res[suffix]);
})).get(0);
});
},
/**
* Return true if suffix mathes one of tech suffixes.
*
* @public
* @param {String} suffix Suffix to match.
* @returns {Boolean}
*/
matchSuffix: function(suffix) {
(suffix.substr(0, 1) === '.') && (suffix = suffix.substr(1));
return this.getSuffixes().indexOf(suffix) >= 0;
},
/**
* Return all tech suffixes.
*
* @public
* @returns {String[]}
*/
getSuffixes: function() {
return [this.getTechName()];
},
/**
* Return tech suffixes to use in process of bem create.
*
* @return {String[]}
*/
getCreateSuffixes: function() {
return this.getSuffixes();
},
/**
* Return tech suffixes to use in process of bem build.
*
* @return {String[]}
*/
getBuildSuffixes: function() {
return this.getSuffixes();
},
/**
* Return path by prefix and suffix.
*
* @public
* @param {String} prefix
* @param {String} suffix
* @returns {String}
*/
getPath: function(prefix, suffix) {
suffix = suffix || this.getTechName();
return [prefix, suffix].join('.');
},
/**
* Return all paths by prefix.
*
* @public
* @param {String|String[]} prefixes
* @param {String|String[]} suffixes
* @returns {String[]}
*/
getPaths: function(prefixes, suffixes) {
prefixes = Array.isArray(prefixes)? prefixes : [prefixes];
suffixes = (!Array.isArray(suffixes) && suffixes) ?
[suffixes] :
(suffixes || this.getSuffixes());
var _this = this,
paths = [];
prefixes.forEach(function(p) {
suffixes.forEach(function(s) {
paths.push(_this.getPath(p, s));
});
});
return paths;
},
/**
* Return tech name.
*
* @public
* @returns {String}
*/
getTechName: function() {
if(this.techName) return this.techName;
return bemUtil.stripModuleExt(PATH.basename(this.getTechPath()));
},
/**
* Return tech module absolute path.
*
* @public
* @returns {String}
*/
getTechPath: function() {
return this.techPath;
},
/**
* Return tech module relative path.
*
* @public
* @param {String} from Path to calculate relative path from.
* @returns {String}
*/
getTechRelativePath: function(from) {
from = PATH.join(from || '.', PATH.dirSep);
var absPath = this.getTechPath(),
techPath = PATH.relative(PATH.join(__dirname, PATH.unixToOs('../../../')), absPath),
testDotRe = new RegExp('^[\\.' + PATH.dirSepRe + ']'),
testLibRe = new RegExp('^.*?' + PATH.dirSepRe + 'lib'),
replaceRe = new RegExp('^.*?' + PATH.dirSepRe);
// tech from 'bem' module
if(!testDotRe.test(techPath) && testLibRe.test(techPath)) {
techPath = techPath.replace(replaceRe, PATH.unixToOs('bem/'));
} else {
// look for tech into node_modules and NODE_PATH env variable
var shortestPath = PATH.relative(from, absPath);
shortestPath = shortestPath.split(PATH.dirSep);
module.paths.concat(bemUtil.getNodePaths()).forEach(function(reqPath) {
var relPath = PATH.relative(PATH.join(reqPath, PATH.dirSep), absPath);
if(!/^\./.test(relPath)) {
relPath = relPath.split(PATH.dirSep);
if(relPath.length < shortestPath.length) {
shortestPath = relPath;
}
}
});
techPath = PATH.join.apply(null, shortestPath);
// NOTE: could not replace to PATH.join('.', techPath), because of
// '.' will be stripped
if(!/^\./.test(techPath)) techPath = '.' + PATH.dirSep + techPath;
}
// NOTE: default tech, need to return empty path for it
if(techPath === bemUtil.getBemTechPath('default')) return '';
return techPath;
},
/**
* Return array of tech name dependencies.
*
* @public
* @return {String[]}
*/
getDependencies: function() {
return [];
}
}, {
/**
* Set context to use in all tech modules.
*
* @static
* @public
* @param {Context} ctx Context instance object.
*/
setContext: function(ctx) {
Tech.prototype.context = ctx;
require('./../legacy-tech').Tech.setContext(ctx);
}
});