UNPKG

bem

Version:
999 lines (819 loc) 27.5 kB
/* global toString: false */ 'use strict'; var ASSERT = require('assert'), Q = require('q'), QFS = require('q-fs'), PATH = require('./path'), FS = require('fs'), VM = require('vm'), UTIL = require('util'), ENV = require('./env'), MKDIRP = require('mkdirp'), _ = require('underscore'); exports.nodeVer = process.version.substr(1) .split('.') .map(function(v) { return parseInt(v, 10); }); exports.oldNode = exports.nodeVer[0] === 0 && exports.nodeVer[1] < 10; exports.chdirOptParse = function() { return this.opt() .name('dir').short('C').long('chdir') .title('change process working directory, cwd by default; to specify level use --level, -l option instead') .def(process.cwd()) .val(function(d) { return PATH.join(PATH.resolve(d), PATH.dirSep); }) .act(function(opts) { process.chdir(opts.dir); }) .end(); }; exports.techsOptParse = function() { return this .opt() .name('addTech').short('t').long('add-tech') .title('add tech') .arr() .end() .opt() .name('forceTech').short('T').long('force-tech') .title('use only specified tech') .arr() .end() .opt() .name('noTech').short('n').long('no-tech') .title('exclude tech') .arr() .end(); }; exports.levelOptParse = function() { var def = exports.findLevel(process.cwd()), rel = PATH.relative(process.cwd(), def); return this.opt() .name('level').short('l').long('level') .def(def) .title(['level directory path, default: ', !rel? '.' : rel, PATH.dirSep].join('')) .val(function(l) { return typeof l === 'string'? require('./level').createLevel(l) : l; }) .end(); }; exports.mergeTechs = function(level, opts) { // NOTE: если при создании блока/элемента/модификатора // указали --force-tech <name> или --no-tech, и в level.js // определена технология с таким именем/файлом на диске, // нужно использовать именно её var techs = opts.forceTech? {} : level.techs, optsTechs = []; opts.forceTech && optsTechs.push.apply(optsTechs, opts.forceTech); opts.addTech && optsTechs.push.apply(optsTechs, opts.addTech); optsTechs.forEach(function(t) { var tech = level.getTech(t), name = tech.getTechName(); techs[name] || (techs[name] = tech); }); opts.noTech && opts.noTech.forEach(function(t) { delete techs[level.getTech(t).getTechName()]; }); return techs; }; /** * Create symbolic link. * * If `force` is specified and is true, it will check * for `link` to exist and remove it in case it is * a symbolic link. * * Files and directories will be left untouched. * * @param {String} link Symbolic link name. * @param {String} target Symbolic link target. * @param {Boolean} [force] Force creating symplink in case it is already exist. * @return {Promise * Undefined} */ exports.symbolicLink = function(link, target, force) { return Q.resolve(force) .then(function(force) { if (!force) return; return QFS.statLink(link) .then(function(stat) { if (stat.isSymbolicLink()) { return QFS.remove(link); } }) .fail(function() {}); }) .then(function() { return QFS.symbolicLink(link, target); }); }; /** * Remove path (file or directory) but not recursively. * * @param {String} path Path to remove * @return {Promise * Undefined} */ exports.removePath = function(path) { return QFS.stat(path) .then(function(stat) { if (stat.isDirectory()) return QFS.removeDirectory(path); return QFS.remove(path); }); }; exports.write = function(path, content) { FS.writeFileSync(path, Array.isArray(content) ? content.join('') : content); }; exports.writeFile = function(path, content) { return Q.when(content, function(content) { return QFS.write(path, Array.isArray(content) ? content.join('') : content, { charset: 'utf8' }); }); }; exports.writeFileIfDiffers = function(path, content) { return QFS.exists(path) .then(function(exists) { if (!exists) return true; return exports.readFile(path) .then(function(current) { return current !== content; }); }) .then(function(rewrite) { if (rewrite) return exports.writeFile(path, content); }); }; exports.readFile = function(path) { return QFS.read(path, { charset: 'utf8' }); }; exports.readBinary = function(path) { return QFS.read(path, { charset: 'binary' }); }; /** * Read and parse declaration module-like file, * e.g. deps.js or bemdecl.js. * * @param {String} path Path to declaration file. * @return {Promise * Object} Declaration object. */ exports.readDecl = function(path) { return exports.readFile(path) .then(function(c) { var fn = VM.runInThisContext(declWrapper[0] + c + declWrapper[1], path), decl = {}, module = { exports: decl }; return fn(decl, require, module, PATH.resolve(path), PATH.resolve(PATH.dirname(path))); }); }; /** * Declaration modules content wrapper for `readDecl()`. * * @type {String[]} */ var declWrapper = ['(function(exports, require, module, __filename, __dirname) {', ';return module.exports;})']; /** * Read and parse JSON-JS file. * * @param {String} path Path to file to read. * @return {Promise * Object|Array} Data read from file. */ exports.readJsonJs = function(path) { return exports.readFile(path) .then(function(c) { return VM.runInThisContext(c, path); }); }; exports.mkdir = function(path) { try { FS.mkdirSync(path, '0777'); } catch(ignore) {} }; /** * Create directories. * * @return {String} First directory being created. */ exports.mkdirs = MKDIRP.sync; /** * Create directories. * * @return {Promise * String} First directory being created. */ exports.mkdirp = Q.nfbind(MKDIRP); exports.isExists = function(path) { var d = Q.defer(); PATH.exists(path, function(res) { d.resolve(res); }); return d.promise; }; exports.isFile = function(path) { try { return FS.statSync(path).isFile(); } catch(ignore) {} return false; }; /** @deprecated */ exports.isFileP = QFS.isFile.bind(QFS); exports.isDirectory = function(path) { try { return FS.statSync(path).isDirectory(); } catch(ignore) {} return false; }; exports.isLevel = function(path) { return exports.isDirectory(path) && exports.isFile(PATH.join(path, '.bem', 'level.js')); }; /** * Search for the nearest level recursivelt from the specified * directory to the filesystem root. * * @param {String} path Path to start search from. * @param {String[]|String|Undefined} [types] Level type to search. * @param {String} [startPath] * @return {String} Found level path or specified path if not found. */ exports.findLevel = function(path, types, startPath) { var createLevel = require('./level').createLevel; if (types && !Array.isArray(types)) types = [types]; startPath = startPath || path; // Check for level and level type if applicable if (exports.isLevel(path) && (!types || exports.containsAll(createLevel(path).getTypes(), types))) return path; // Check for fs root if (PATH.isRoot(path)) return startPath; return exports.findLevel(PATH.dirname(path), types, startPath); }; /** * Filter out non-existent paths. * * @param {String[]} paths Paths to filter * @return {Promise * String[]} Existent paths */ exports.filterPaths = function(paths) { var d = Q.defer(), res = [], total = paths.length, count = 0; paths.forEach(function(path, index) { PATH.exists(path, function(exists) { count++; res[index] = exists; if (count < total) return; d.resolve(paths.filter(function(path, index) { return res[index]; })); }); }); return d.promise; }; exports.fsWalkTree = function(root, fileCb, filterCb, ctx) { var files = FS.readdirSync(root); files.sort(); while (files.length > 0) { var path = PATH.join(root, files.shift()); if(filterCb && !filterCb.call(ctx, path)) continue; fileCb.call(ctx, path); if(exports.isDirectory(path)) exports.fsWalkTree(path, fileCb, filterCb, ctx); } }; exports.fsWalkTreeAsync = function(root, fileCb, filterCb, ctx) { return QFS.list(root) .then(function(files) { return Q.all(files.map(function(file) { var path = PATH.join(root, file); if (!(filterCb && !filterCb.call(ctx, path, file))) { //fileCb.call(ctx, path); return QFS.isDirectory(path) .then(function(isdir) { if (isdir) return exports.fsWalkTreeAsync(path, fileCb, filterCb, ctx); }); } })); }); }; exports.fsWalkTreeCb = function(root, fileCb) { var d = Q.defer(), results = {}; function done(err) { if (err) d.reject(err); d.resolve(results); } if (root[root.length-1] !== PATH.dirSep) root += PATH.dirSep; walk(root, '', done); function walk(path, relPath, cb) { FS.readdir(path, function(err, list) { if (err) return done(err); var pending = list.length; if (!pending) return cb(null, results); list.forEach(function(file) { var absPath = PATH.join(path, file); FS.stat(absPath, function(err, stat) { if (err) return done(err); if (file[0] !== '.') { if (stat && stat.isDirectory()) { walk(absPath, PATH.join(relPath, file), function() { if (!--pending) cb(null, results); }); return; } var f = PATH.join(relPath, file); process.nextTick(fileCb.bind(fileCb, { file: file, relPath: f, absPath: absPath, lastUpdated: stat.mtime.getTime() })); } if (!--pending) cb(null, results); }); }); }); } return d.promise; }; exports.getDirs = function(path) { try { return exports.isDirectory(path)? FS.readdirSync(path) .filter(function(d) { return !(/^\.svn$/.test(d)) && exports.isDirectory(PATH.join(path, d)); }) .sort() : []; } catch (e) { return []; } }; exports.getDirsAsync = function(path) { return QFS.list(path).then(function(items) { return Q.all(items.map(function(i) { return QFS.isDirectory(PATH.join(path, i)) .then(function(isDir){ return { name: i, dir: isDir }; } ); })) .then(function(items) { return items .filter(function(item) { return item.dir; }) .map(function(item) { return item.name; }); } ); }); }; exports.getFilesAsync = function(path) { return QFS.list(path).then(function(items) { return Q.all(items.map(function(i) { return QFS.isFile(PATH.join(path, i)) .then(function(isFile){ return { name: i, file: isFile }; } ); })) .then(function(items) { return items .filter(function(item) { return item.file; }) .map(function(item) { return item.name; }); } ); }); }; exports.getFiles = function(path) { try { return exports.isDirectory(path)? FS.readdirSync(path) .filter(function(f) { return exports.isFile(PATH.join(path, f)); }) .sort() : []; } catch (e) { return []; } }; exports.toUpperCaseFirst = function(str) { return str.charAt(0).toUpperCase() + str.slice(1); }; /* jshint -W098 */ exports.isEmptyObject = function(obj) { for(var i in obj) return false; return true; }; /* jshint +W098 */ var errRe = /^Cannot find module/; exports.isRequireError = function(e) { return errRe.test(e.message); }; exports.isPath = function(str) { return PATH.normalize(str).indexOf(PATH.dirSep) !== -1; }; exports.isRequireable = function(path) { try { require.resolve(path); return true; } catch (e) { if(! exports.isRequireError(e)) throw e; return false; } }; exports.arrayUnique = function(arr) { return arr.reduce(function(prev, cur) { if(prev.indexOf(cur) + 1) return prev; return prev.concat([cur]); }, []); }; exports.arrayReverse = function(arr) { return arr.reduceRight(function(prev, cur) { prev.push(cur); return prev; }, []); }; exports.getBemTechPath = function(name, opts) { opts = opts || {}; var bemLib = process.env.COVER? 'bem/lib-cov/' : 'bem/lib/', bemTechs = PATH.unixToOs(bemLib + 'techs'), path = PATH.join(bemTechs, name); if(exports.isRequireable(path + '.js')) return path + '.js'; if(exports.isRequireable(path)) return path; if (!opts.throwWhenUnresolved) return PATH.unixToOs(bemLib + 'tech'); throw new Error('Unable to resolve "' + name + '" tech'); }; exports.stripModuleExt = function(path) { var exts = Object.keys(require.extensions).map(function(v) { return v.replace(/^\./, ''); }); return path.replace(new RegExp('\\.(' + exts.join('|') + ')$'), ''); }; exports.getNodePaths = function() { return (process.env.NODE_PATH || '').split(PATH.pathSep); }; exports.mergeDecls = function mergeDecls(d1, d2) { var keys = {}; d1? d1.forEach(function(o) { keys[o.name || o] = o; }) : d1 = []; d2.forEach(function(o2) { var name = o2.name || o2; if (keys.hasOwnProperty(name)) { var o1 = keys[name]; o2.elems && (o1.elems = mergeDecls(o1.elems, o2.elems)); o2.mods && (o1.mods = mergeDecls(o1.mods, o2.mods)); o2.vals && (o1.vals = mergeDecls(o1.vals, o2.vals)); o2.techs && (o1.techs = mergeDecls(o1.techs, o2.techs)); } else { d1.push(o2); keys[name] = o2; } }); return d1; }; exports.declForEach = function(decl, cb) { var forItemWithMods = function(block, elem) { var item = elem || block, type = elem? 'elem' : 'block', args = elem? [block.name, elem.name] : [block.name]; // for block and element cb(type, args, item); // for each modifier item.mods && item.mods.forEach(function(mod) { // for modifier cb(type + '-mod', args.concat(mod.name), mod); // for each modifier value mod.vals && mod.vals.forEach(function(val, i) { if (!val.name) { val = { name: val }; mod.vals[i] = val; } cb(type + '-mod-val', args.concat(mod.name, val.name), val); }); }); }, forBlockDecl = function(block) { // for block forItemWithMods(block); // for each block element block.elems && block.elems.forEach(function(elem) { forItemWithMods(block, elem); }); }, forBlocksDecl = function(blocks) { // for each block in declaration blocks.forEach(forBlockDecl); }; decl.name && forBlockDecl(decl); decl.blocks && forBlocksDecl(decl.blocks); }; /** * Constructs BEM entity key from entity properties. * * @param {Object} item BEM entity object. * @param {String} item.block Block name. * @param {String} [item.elem] Element name. * @param {String} [item.mod] Modifier name. * @param {String} [item.val] Modifier value. * @return {String} */ exports.bemKey = function(item) { var key = ''; if (item.block) { key += item.block; item.elem && (key += '__' + item.elem); if (item.mod) { key += '_' + item.mod; item.val && (key += '_' + item.val); } } return key; }; /** * Constructs BEM entity full key from entity properties plus tech name. * * @param {Object} item BEM entity object. * @param {String} item.block Block name. * @param {String} [item.elem] Element name. * @param {String} [item.mod] Modifier name. * @param {String} [item.val] Modifier value. * @param {String} [item.tech] Tech name. * @return {String} */ exports.bemFullKey = function(item) { return exports.bemKey(item) + (item.tech? '.' + item.tech : ''); }; /** * Return BEM entity type by describing object. * * @param {Object} item BEM entity object. * @param {String} item.block Block name. * @param {String} [item.elem] Element name. * @param {String} [item.mod] Modifier name. * @param {String} [item.val] Modifier value. * @return {String} */ exports.bemType = function(item) { var type = item.elem? 'elem' : 'block'; if (item.mod) { type += '-mod'; item.val && (type += '-val'); } return type; }; var bemItemRe = '([^_.]+)', bemKeyRe = new RegExp('^' + bemItemRe + '(?:__' + bemItemRe + ')?(?:_' + bemItemRe + '(?:_' + bemItemRe + ')?)?' + '(?:\\.([^_]+))?$'); /** * Parse BEM-entity key into BEM-entity object. * * @param {String} key Key to parse. * @return {Object} BEM-entity object. */ exports.bemParseKey = function(key) { var m = bemKeyRe.exec(key), item = { block: m[1] }; m[2] && (item.elem = m[2]); m[3] && (item.mod = m[3]); m[4] && (item.val = m[4]); m[5] && (item.tech = m[5]); return item; }; /* jshint -W106 */ /** * Adopted from jquery's extend method. Under the terms of MIT License. * * http://code.jquery.com/jquery-1.4.2.js * * Modified by mscdex to use Array.isArray instead of the custom isArray method */ var extend = exports.extend = function() { // copy reference to target object var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options, name, src, copy; // Handle a deep copy situation if (typeof target === 'boolean') { deep = target; target = arguments[1] || {}; // skip the boolean and the target i = 2; } // Handle case when target is a string or something (possible in deep copy) if (typeof target !== 'object' && typeof target !== 'function') target = {}; var isPlainObject = function(obj) { // Must be an Object. // Because of IE, we also have to check the presence of the constructor property. // Make sure that DOM nodes and window objects don't pass through, as well if (!obj || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.setInterval) return false; var has_own_constructor = hasOwnProperty.call(obj, 'constructor'); var has_is_property_of_method = hasOwnProperty.call(obj.constructor.prototype, 'isPrototypeOf'); // Not own constructor property must be Object if (obj.constructor && !has_own_constructor && !has_is_property_of_method) return false; // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. var key, last_key; for (key in obj) last_key = key; return typeof last_key === 'undefined' || hasOwnProperty.call(obj, last_key); }; for (; i < length; i++) { // Only deal with non-null/undefined values if ((options = arguments[i]) !== null) { // Extend the base object for (name in options) { if (!options.hasOwnProperty(name)) continue; src = target[name]; copy = options[name]; // Prevent never-ending loop if (target === copy) continue; // Recurse if we're merging object literal values or arrays if (deep && copy && (isPlainObject(copy) || Array.isArray(copy))) { var clone = src && (isPlainObject(src) || Array.isArray(src)) ? src : Array.isArray(copy) ? [] : {}; // Never move original objects, clone them target[name] = extend(deep, clone, copy); // Don't bring in undefined values } else if (typeof copy !== 'undefined') target[name] = copy; } } } // Return the modified object return target; }; /* jshint +W106 */ exports.requireWrapper = function(wrappedRequire) { var func = function(module, noCache) { if (noCache) delete wrappedRequire.cache[wrappedRequire.resolve(module)]; return wrappedRequire(module); }; ['resolve', 'cache', 'extensions', 'registerExtension'].forEach(function(key) { func[key] = wrappedRequire[key]; }); return func; }; exports.removeFromArray = function(arr, o) { var i = arr.indexOf(o); return i >= 0 ? (arr.splice(i, 1), true) : false; }; /** * Return true if all of `needles` are found in `arr`. * * @param {Array} arr Array to search. * @param {String[]|String} needles Needles to search. * @return {Boolean} */ exports.containsAll = function(arr, needles) { Array.isArray(needles) || (needles = [needles]); return _.all(needles, function(i) { return _.contains(arr, i); }); }; var getNodePrefix = exports.getNodePrefix = function(level, item) { return PATH.join( PATH.relative(ENV.getEnv('root'), level.dir), level.getRelByObj(item)); }; exports.getNodeTechPath = function(level, item, tech) { return level.getPath(getNodePrefix(level, item), tech); }; exports.setEnv = function(opts) { ENV.setEnv('root', opts.root); ENV.setEnv('verbose', opts.verbose); ENV.setEnv('force', opts.force); }; exports.pad = exports.lpad = function(n, desiredLength, padWith) { n = '' + n; if (n.length < desiredLength) n = new Array(desiredLength - n.length + 1).join(padWith) + n; return n; }; /** * Implementation `rsplit` from Python. * * See http://docs.python.org/library/stdtypes.html#str.rsplit * * @param {String} string String to split * @param {String} [sep] Separator * @param {Number} [maxsplit] Max chunks * * @return {Array} */ exports.rsplit = function(string, sep, maxsplit) { var arr = string.split(sep || /s+/); return maxsplit ? [arr.slice(0, -maxsplit).join(sep)].concat(arr.slice(-maxsplit)) : arr; }; exports.snapshotArch = function(arch, filename) { ASSERT.ok(arch, 'argument is not an object'); ASSERT.ok(filename, 'string is expected'); ASSERT.ok(arch.toJson, 'object has no toJson method'); var path = PATH.dirname(filename); return QFS.exists(path) .then(function(exists) { if (!exists) return QFS.makeDirectory(path); }) .then(function() { return exports.writeFile(filename, arch.toJson()); }); }; exports.getDirsFiles = function(path, dirs, files) { return QFS.list(path).then(function(list) { return Q.all(list .map(function(i) { return QFS.isDirectory(PATH.join(path, i)) .then(function(isDir) { (isDir ? dirs : files).push(i); }); })); }); }; exports.getDirsFilesSync = function(path, dirs, files) { var items = FS.readdirSync(path); items.forEach(function(item) { if (item[0] === '.') return; var stat = FS.statSync(path + PATH.dirSep + item), file = { file: item, absPath: path + PATH.dirSep + item, lastUpdated: stat.mtime.getTime() }; if (stat.isDirectory()) dirs && dirs.push(file); else files && files.push(file); }); }; /** * Executes specified command with options. * * @param {String} cmd Command to execute. * @param {Object} options Options to `child_process.exec()` function. * @param {Boolean} resolveWithOutput Resolve returned promise with command output if true. * @return {Promise * String | Undefined} */ exports.exec = function(cmd, options, resolveWithOutput) { var cp = require('child_process').exec(cmd, options), d = Q.defer(), output = ''; cp.on('exit', function(code) { if (code === 0) return d.resolve(resolveWithOutput && output ? output : null); d.reject(new Error(UTIL.format('%s failed: %s', cmd, output))); }); cp.stderr.on('data', function(data) { output += data; }); cp.stdout.on('data', function(data) { output += data; }); return d.promise; }; var os = require('os'), hits = {}; /** * Output deprecate message only once based on the value of * the `methodName` argument. * * Based on the code of `deprecate` module: * https://github.com/brianc/node-deprecate * * @type {Function} * @param {String} methodName * @param {String} message */ var deprecate = exports.deprecate = function(methodName, message) { if (deprecate.silence) return; if (hits[methodName]) return; hits[methodName] = true; deprecate.stream.write(os.EOL); if (deprecate.color) { deprecate.stream.write(deprecate.color); } deprecate.stream.write('WARNING!' + os.EOL); for (var i = 0; i < arguments.length; i++) { deprecate.stream.write(arguments[i] + os.EOL); } if (deprecate.color) { deprecate.stream.write('\x1b[0m'); } deprecate.stream.write(os.EOL); }; deprecate.stream = process.stderr; deprecate.silence = !!process.env.BEM_NO_DEPRECATION; deprecate.color = '\x1b[31;1m';