echo-fecs
Version:
Front End Code Style Suite
540 lines (444 loc) • 15 kB
JavaScript
/**
* @file 工具方法集
* @author chris<wfsr@foxmail.com>
*/
var fs = require('fs');
var path = require('path');
var util = require('util');
var chalk = require('chalk');
var mapStream = require('map-stream');
var Manis = require('manis');
var CONFIG_NAME = '.fecsrc';
var PACKAGE_NAME = 'package.json';
var JSON_YAML_REG = /(.+)\.(?:json|yml)$/i;
var rcloader;
var config = exports.config = (function (config) {
['css', 'less', 'html', 'js'].forEach(function (dir) {
readConfigs(path.join(__dirname, dir), config);
});
return config;
})({});
/**
* 获取指定路径下特定的文件配置
*
* @param {string} key 配置文件名
* @param {string} filepath 要搜索的起始路径
* @param {Object} defaults 默认配置
* @param {boolean} force 强制重新读取配置
* @return {Object} 合并后的搜索路径下所有的配置对象
*/
exports.getConfig = function (key, filepath, defaults, force) {
defaults = defaults || {};
// 未提供起始路径时,使用内置配置
if (!filepath) {
return config[key] || defaults;
}
var localRcloader = rcloader;
if (!localRcloader || force) {
localRcloader = new Manis({
files: [
CONFIG_NAME,
{name: PACKAGE_NAME, get: 'fecs'},
{
name: '.eslintrc',
get: function (eslint) {
return {eslint: eslint};
}
}
]
});
localRcloader.setDefault(config);
if (!rcloader) {
rcloader = localRcloader;
}
}
return localRcloader.from(filepath)[key] || defaults;
};
/**
* 从工作目录向上查找包含指定文件的路径
*
* @param {string} filename 要查找的文件名
* @param {?string=} start 开始查找的目录,默认为当前目录
* @return {(string|boolean)} 第一个能查找到文件的路径,否则返回 false
*/
function findPath(filename, start) {
start = path.resolve(start || './');
var root = path.resolve('/');
var filepath = path.join(start, filename);
var dir = start;
while (dir !== root) {
filepath = path.join(dir, filename);
if (fs.existsSync(filepath)) {
return dir;
}
dir = path.resolve(dir, '..');
}
return false;
}
/**
* 构建文件匹配的 golb pattern
*
* @param {Array.<string>=} dirs 目录
* @param {string=} extensions 扩展名
* @return {Array.<string>} 能查找到对应 dirs 目录下目标文件的对应的 glob pattern 数组
*/
exports.buildPattern = function (dirs, extensions) {
extensions = (extensions || 'js,css,less,html')
.replace(/\s+/g, '')
.replace(/\bhtml?\b/gi, 'htm,html')
.replace(/\bjs\b/gi, 'js,es,es6')
.replace(/^,|,\s*(?=,|$)/g, '');
var regExt = extensions.replace(/,/g, '|').replace(/(?=[^\w|])/g, '\\');
var validExt = new RegExp('\\.(' + regExt + ')$', 'i');
if (~extensions.indexOf(',')) {
extensions = '{' + extensions + '}';
}
var defaultPath = './';
var includeSpecials = true;
if (!dirs || !dirs.length) {
var cwd = findPath(CONFIG_NAME) || findPath(PACKAGE_NAME) || path.resolve(defaultPath);
process.chdir(cwd);
dirs = exports.getConfig('files', defaultPath, [defaultPath], true);
includeSpecials = false;
}
var specials = [];
var transform = function (dir) {
if (fs.existsSync(dir)) {
var stat = fs.statSync(dir);
if (dir !== defaultPath
&& (stat.isDirectory() || stat.isFile() && validExt.test(dir))
&& includeSpecials
) {
specials.push(dir);
}
return stat.isDirectory()
? path.join(dir, '/') + '**/*.' + extensions
: dir;
}
return dir;
};
var patterns = dirs.map(transform).filter(Boolean);
patterns.push('!**/{node_modules,bower_components}/**');
// HACK: CLI 中直接指定的文件或目录,可以不被 .fecsignore 忽略
patterns.specials = specials;
return patterns;
};
/**
* 读取指定目录下的 JSON 配置文件,以文件名的 camelCase 形式为 key 暴露
*
* @param {string} dir 目录路径
* @param {Object} out 调用模块的 exports 对象
*/
function readConfigs(dir, out) {
if (!fs.existsSync(dir)) {
return;
}
fs.readdirSync(dir).forEach(function (file) {
var match = file.match(JSON_YAML_REG);
if (match) {
var key = match[1].replace(/\-[a-z]/g, function (a) {
return a[1].toUpperCase();
});
var filepath = path.join(dir, file);
out[key] = Manis.loader(fs.readFileSync(filepath, 'utf-8'), filepath);
}
});
}
exports.readConfigs = readConfigs;
/**
* 为字符固定宽度(位数)
*
* @param {string} src 输入字符串
* @param {number=} width 需要固定的位数,默认为 3
* @param {string=} chr 不够时补齐的字符,默认为 1 个空格
* @return {string} 补齐后的字符串
*/
exports.fixWidth = function (src, width, chr) {
src = src + '';
chr = (chr || ' ')[0];
width = +width || 3;
var len = src.length;
if (len >= width) {
return src;
}
return new Array(width - len + 1).join(chr) + src;
};
/**
* 格式化字符串
*
* @param {string} pattern 字符串模式
* @param {...*} args 要替换的数据
* @return {string} 数据格式化后的字符串
*/
exports.format = function (pattern, args) {
return util.format.apply(null, arguments);
};
/**
* 从 stack 中解释错误所在位置
*
* @param {Object} info 错误的基本信息
* @param {Error} error 源错误对象
* @return {(Object | Error)} 解释成功后返回包含行列和文件信息的对象,否则返回源错误对象
*/
exports.parseError = function (info, error) {
info.origin = error;
info.message = error.message;
var matches = (error.stack || '').match(/\(?(.+?\.js):(\d+)(:(\d+))?\)?/);
if (!('lineNumber' in error) && matches) {
info.line = matches[2] | 0;
if (matches[4]) {
info.column = matches[4] | 0;
}
var errorMessages = matches.input.match(/^.*?([a-z]*Error:[^\r\n]+).*$/mi);
if (errorMessages) {
info.message = errorMessages[1];
}
info.message += '\(' + matches[1].replace(/^.+\/fecs\/(.+)$/, '~/$1') + '\).';
}
else {
info.line = error.line;
info.column = 'col' in error ? error.col : error.column;
}
info.code = error.code || info.code || '998';
info.rule = error.rule || info.rule || 'syntax';
return info;
};
/**
* 对象属性拷贝
*
* @param {Object} target 目标对象
* @param {...Object} source 源对象
* @return {Object} 返回目标对象
*/
exports.extend = function extend(target) {
function isObject(obj) {
var toString = Object.prototype.toString;
return toString.call(obj) === '[object Object]';
}
for (var i = 1; i < arguments.length; i++) {
var src = arguments[i];
if (src == null) {
continue;
}
for (var key in src) {
if (src.hasOwnProperty(key)) {
target[key] = isObject(src[key])
? extend(isObject(target[key]) ? target[key] : {}, src[key])
: src[key];
}
}
}
return target;
};
/**
* 混合对象
*
* @param {...Object} source 要混合的对象
* @return {Object} 混合后的对象
*/
exports.mix = function () {
var o = {};
var src = Array.prototype.slice.call(arguments);
return exports.extend.apply(this, [o].concat(src));
};
/**
* 构建用于文件名匹配的正则
*
* @param {string|RegExp} stringSplitByComma 逗号分隔的文件名列表或匹配的正则
* @return {RegExp} 构建后的正则
*/
exports.buildRegExp = function (stringSplitByComma) {
if (util.isRegExp(stringSplitByComma)) {
return stringSplitByComma;
}
if (!stringSplitByComma) {
return /.*/i;
}
var array = String(stringSplitByComma).replace(/[^a-z\d_,\s]/gi, '\\$&').split(/\s*,\s*/);
var reg = array.length === 1 ? array[0] : '(' + array.join('|') + ')';
return new RegExp('\\.' + reg + '$', 'i');
};
/**
* 为在控制台输出的字符着色
*
* @param {string} text 要着色的字符
* @param {string=} color chalk 合法的颜色名,非法名将导致不着色
* @return {string} 着色后的字符
*/
exports.colorize = function (text, color) {
if (color && chalk[color]) {
return chalk[color](text);
}
return text;
};
/**
* map-stream 化
*
* @param {Function} transform transform 操作
* @param {Function=} flush flush 操作
* @return {Transform} 转换流
*/
exports.mapStream = function (transform, flush) {
var stream = mapStream(transform);
if (typeof flush === 'function') {
stream.on('end', flush);
}
return stream;
};
/**
* List all variable in a given scope (from eslint-plugin-react)
*
* @param {Object} context The current rule context.
* @param {Array} name The name of the variable to search.
* @return {boolean} True if the variable was found, false if not.
*/
exports.variablesInScope = function (context) {
var scope = context.getScope();
var variables = scope.variables;
while (scope.type !== 'global') {
scope = scope.upper;
variables = variables.concat(scope.variables);
}
return variables;
};
/* eslint-disable max-len */
/**
* 匹配数组的静态方法
*
* @const
* @type {RegExp}
*/
const ARRAY_STATIC_PATTERN = /^Array[\s\r\n]*\.[\s\r\n]*(?:from|of|call|apply)\b/;
/**
* 匹配数组的实例方法
*
* @const
* @type {RegExp}
*/
const ARRAY_PROTO_PATTERN = /^Array[\s\r\n]*\.[\s\r\n]*prototype[\s\r\n]*\.[\s\r\n]*(?:concat|copyWithin|entries|filter|fill|keys|map|values|reverse|slice|sort|splice)\b/;
/**
* 匹配对象转数组的方法
*
* @const
* @type {RegExp}
*/
const OBJECT_ARRAY_PATTERN = /^Object[\s\r\n]*\.[\s\r\n]*(?:keys|values|entries)\b/;
/* eslint-enable max-len */
/**
* 匹配对象创建的静态方法
*
* @const
* @type {RegExp}
*/
const OBJECT_STATIC_PATTERN = /^Object[\s\r\n]*\.[\s\r\n]*(?:create|assign)\b/;
exports.isArrayNode = function (context) {
return function isArrayNode(node, name) {
switch (node.type) {
case 'Identifier':
if (node.parent) {
if (node.parent.type === 'RestElement') {
return true;
}
if (node.parent.type === 'ArrayPattern') {
return false;
}
}
var variable;
exports.variablesInScope(context).some(function (item) {
if (item.name === node.name) {
variable = item;
return true;
}
return false;
});
if (!variable) {
return false;
}
// variableName = variableName.slice(0)
if (variable.name === name) {
return true;
}
return variable.references.reduceRight(function (result, ref) {
var writeExpr = ref.writeExpr;
if (!result && writeExpr) {
// variableName = variableName
if (writeExpr.type === 'Identifier' && writeExpr.name === variable.name) {
return false;
}
result = isArrayNode(ref.writeExpr, variable.name);
}
return result;
}, false);
case 'ArrayExpression':
return true;
case 'NewExpression':
return String(node.callee.name).slice(-5) === 'Array';
case 'CallExpression':
var callee = node.callee;
var code = context.getSourceCode().getText(callee);
return String(callee.name).slice(-5) === 'Array'
|| code.match(ARRAY_STATIC_PATTERN)
|| code.match(ARRAY_PROTO_PATTERN)
|| code.match(OBJECT_ARRAY_PATTERN)
|| callee.type === 'MemberExpression'
&& isArrayNode(callee, name);
case 'MemberExpression':
return (node.property.name === 'prototype'
|| ARRAY_PROTO_PATTERN.test('Array.prototype.' + node.property.name))
&& isArrayNode(node.object, name);
}
return false;
};
};
exports.isObjectNode = function (context) {
var isArray = exports.isArrayNode(context);
return function isObjectNode(node, name) {
if (isArray(node)) {
return false;
}
var type = node.type;
switch (type) {
case 'Identifier':
if (node.parent && node.parent.type === 'Property') {
return node.parent.parent.type === 'RestProperty';
}
var variable;
exports.variablesInScope(context).some(function (item) {
if (item.name === node.name) {
variable = item;
return true;
}
return false;
});
if (!variable) {
return false;
}
// variableName = variableName
if (variable.name === name) {
return false;
}
return variable.references.reduceRight(function (result, ref) {
if (!result && ref.writeExpr) {
result = isObjectNode(ref.writeExpr, variable.name);
}
return result;
}, false);
case 'ObjectExpression':
return true;
case 'NewExpression':
return node.callee.name !== 'Array';
case 'CallExpression':
var callee = node.callee;
return callee.name === 'Object'
|| callee.type === 'MemberExpression'
&& isObjectNode(callee, name);
case 'MemberExpression':
var code = context.getSourceCode().getText(node);
return node.parent
&& node.parent.type !== 'MemberExpression'
&& node.property.name === 'prototype'
|| code.match(OBJECT_STATIC_PATTERN);
}
return false;
};
};