bem
Version:
788 lines (658 loc) • 21.9 kB
JavaScript
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');
exports.nodeVer = process.version.substr(1)
.split('.')
.map(function(v) {
return parseInt(v);
});
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) {}
};
exports.mkdirs = function(path) {
if (PATH.existsSync(path)) return;
exports.mkdirs(PATH.dirname(path));
exports.mkdir(path);
};
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;
};
exports.isFileP = function(path) {
return Q.when(QW.execute(FS.stat, path),
function(stat) {
return stat.isFile();
},
function(err) {
return false;
});
};
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'));
};
exports.findLevel = function(path, startPath) {
startPath = startPath || path;
if (exports.isLevel(path)) return path;
if (PATH.isRoot(path)) return startPath;
return exports.findLevel(PATH.dirname(path), 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.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);
};
exports.isEmptyObject = function(obj) {
for(var i in obj) return false;
return true;
};
exports.isRequireError = function(e) {
return /^Cannot find module/.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) {
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;
return PATH.unixToOs(bemLib + 'tech.js');
};
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;
};
/**
* 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;
};
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
};
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);
});
}));
});
};
/**
* 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;
};