UNPKG

bem

Version:
671 lines (585 loc) 23 kB
'use strict'; var Q = require('q'), FS = require('fs'), QFS = require('q-fs'), bemUtil = require('./../util'), _ = require('underscore'), PATH = require('./../path'), INHERIT = require('inherit'), LOGGER = require('./../logger'), Tech = exports.Tech = INHERIT(/** @lends Tech.prototype */ { API_VER: 2, /** * 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) { 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. * @param {Object} opts Custom opts. * @returns {Promise * Undefined} */ buildByDecl: function(decl, levels, output, opts) { return this.storeBuildResults( output, this.getBuildResults(this.transformBuildDecl(decl), levels, output, opts)); }, /** * 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 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[]} path Files to build from. * @param {String} suffix Suffix to build result for. * @param {String} output Output prefix of build result. * @param {Object} opts Options. * @returns {Promise * String} Promise for build result. */ getBuildResult: function(files, suffix, output, opts) { var _this = this; return Q.all(files.map(function(file) { return _this.getBuildResultChunk( PATH.relative(PATH.dirname(output)+PATH.dirSep, file.absPath), file.absPath, file.suffix); })); }, /** * Build and return result of build of specified prefixes. * * @protected * @param {Promise * Object} decl Declaration to build from. * @param {Object[]} levels Array of levels to use. * @param {String} output Output prefix of build result. * @param {Object} opts Custom options * @returns {Promise * Object} Promise for build results object. */ getBuildResults: function(decl, levels, output, opts) { var _this = this, res = {}, files = this.getBuildPaths(decl, levels); return files.then(function(files) { return Q.all(_this.getBuildSuffixes() .map(function(destSuffix) { var filteredFiles = files[destSuffix] || [], file = _this.getPath(output, destSuffix); return _this.validate(file, filteredFiles, opts) .then(function(valid) { LOGGER.fverbose('file %s is %s', file, valid?'valid':'not valid'); if (!valid) { _this.saveLastUsedData(file, {buildFiles: filteredFiles}); return _this.getBuildResult(filteredFiles, destSuffix, output, opts) .then(function(r) { res[destSuffix] = r; }); } }); })) .then(function() { return res; }); }); }, /** * Determines is file up to date or not by comparing the list of files to use for build * with such saved list during previous build. Will return false if opts.force is defined. * @param {String} file File which is being built * @param {Object[]} files The list of files which will be used. * @param {Object} opts Custom options. * @return {Promise * boolean} */ validate: function(file, files, opts) { if (opts.force) return Q.resolve(false); var _this = this; return Q.all([_this.getLastUsedData(file), QFS.exists(file)]) .spread(function(prevFiles, exists) { prevFiles = prevFiles.buildFiles; if (prevFiles && prevFiles.length === files.length) return _this.sameFiles(files, prevFiles) && exists; else return false; }); }, /** * Compares two lists of files. * @param {Object[]} now * @param {Object[]} old * @return {Boolean} */ sameFiles: function(now, old) { for(var i = 0; i < now.length; i++) { var n = now[i], o = old[i]; if (n.absPath !== o.absPath || n.lastUpdated !== o.lastUpdated) { return false; } } return true; }, /** * Loads appropriate meta file for the file which is being built. * @param {String} file File path which is being built. * @return {Object} Object created from the meta file using deserialization. */ getLastUsedData: function(file) { var root = this.context.opts.root || ''; file = PATH.join( root, '.bem', 'cache', PATH.relative(root, file) + '~' + this.getTechName() + '.meta.js'); return bemUtil .readFile(file) .fail(function() { // meta file does not exist probably return '{}'; }) .then(function(content) { try { return JSON.parse(content); } catch (err) { LOGGER.fwarn('meta file %s failed to parse. It will be regenerated.', file); return {}; } }); }, saveLastUsedData: function(file, data) { var root = this.context.opts.root || ''; file = PATH.join( root, '.bem', 'cache', PATH.relative(root, file) + '~' + this.getTechName() + '.meta.js'); return Q.when(bemUtil.mkdirs(PATH.dirname(file))) .then(function() { return bemUtil.writeFile(file, JSON.stringify(data)); }); }, getBuildPaths: function(decl, levels) { var _this = this, res = {}, suffixesMap = this.getSuffixesMap(); return Q.when(decl, function(decl) { return Q .all(levels.map(function(level) { return level.scanFiles(); })) .then(function() { LOGGER.time('getBuildPaths'); if (decl.deps) for(var d = 0; d < decl.deps.length; d++) { var dep = decl.deps[d]; for(var l = 0; l < levels.length; l++) { var level = levels[l], files = level.getFileByObjIfExists(dep, _this); if (files) { for(var i = 0; i < files.length; i++) { var file = files[i], buildSuffixes = suffixesMap[file.suffix[0] === '.'?file.suffix.substr(1):file.suffix]; if (buildSuffixes) { for(var bs = 0; bs < buildSuffixes.length; bs++) { var buildSuffix = buildSuffixes[bs]; (res[buildSuffix] || (res[buildSuffix] = [])).push(file); } } } } } } LOGGER.timeEndLevel('silly', 'getBuildPaths'); 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(Object.keys(res).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; }, _suffixes: null, /** * Return all tech suffixes. * * @public * @returns {String[]} */ getSuffixes: function() { if (this._suffixes) return this._suffixes; var res = [], map = this.getBuildSuffixesMap(); Object.keys(map).forEach(function(bs) { res = res.concat(map[bs]); }, this); this._suffixes = _.uniq(res); return this._suffixes; }, /** * 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 Object.keys(this.getBuildSuffixesMap()); }, getBuildSuffixesMap: function() { var res = {}; res[this.getTechName()] = [this.getTechName()]; return res; }, getSuffixesMap: function() { var buildMap = this.getBuildSuffixesMap(), srcMap = {}; Object.keys(buildMap).forEach(function(buildSuffix) { buildMap[buildSuffix].forEach(function(srcSuffix) { (srcMap[srcSuffix] || (srcMap[srcSuffix] = [])).push(buildSuffix); }, this); }, this); return srcMap; }, /** * 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); } });