UNPKG

fis3

Version:
1,581 lines (1,474 loc) 41.9 kB
'use strict'; var minimatch = require('minimatch'); var lodash = require('lodash'); var fs = require('fs'), pth = require('path'), crypto = require('crypto'), Url = require('url'), _exists = fs.existsSync || pth.existsSync, toString = Object.prototype.toString, iconv, tar; var IS_WIN = process.platform.indexOf('win') === 0; /* * 后缀类型HASH * @type {Array} */ var TEXT_FILE_EXTS = [ 'css', 'tpl', 'js', 'php', 'txt', 'json', 'xml', 'htm', 'text', 'xhtml', 'html', 'md', 'conf', 'po', 'config', 'tmpl', 'coffee', 'less', 'sass', 'jsp', 'scss', 'pcss', 'manifest', 'bak', 'asp', 'tmp', 'haml', 'jade', 'aspx', 'ashx', 'java', 'py', 'c', 'cpp', 'h', 'cshtml', 'asax', 'master', 'ascx', 'cs', 'ftl', 'vm', 'ejs', 'styl', 'jsx', 'handlebars', 'shtml', 'ts', 'tsx', 'yml', 'sh', 'es', 'es6', 'es7', 'map' ], IMAGE_FILE_EXTS = [ 'svg', 'tif', 'tiff', 'wbmp', 'png', 'bmp', 'fax', 'gif', 'ico', 'jfif', 'jpe', 'jpeg', 'jpg', 'woff', 'cur', 'webp', 'swf', 'ttf', 'eot', 'woff2' ], MIME_MAP = { //text 'css': 'text/css', 'tpl': 'text/html', 'js': 'text/javascript', 'jsx': 'text/javascript', 'ts': 'text/javascript', 'tsx': 'text/javascript', 'es': 'text/javascript', 'es6': 'text/javascript', 'es7': 'text/javascript', 'php': 'text/html', 'asp': 'text/html', 'jsp': 'text/jsp', 'txt': 'text/plain', 'json': 'application/json', 'xml': 'text/xml', 'htm': 'text/html', 'text': 'text/plain', 'md': 'text/plain', 'xhtml': 'text/html', 'html': 'text/html', 'conf': 'text/plain', 'po': 'text/plain', 'config': 'text/plain', 'coffee': 'text/javascript', 'less': 'text/css', 'sass': 'text/css', 'scss': 'text/css', 'styl': 'text/css', 'pcss': 'text/css', 'manifest': 'text/cache-manifest', //image 'svg': 'image/svg+xml', 'tif': 'image/tiff', 'tiff': 'image/tiff', 'wbmp': 'image/vnd.wap.wbmp', 'webp': 'image/webp', 'png': 'image/png', 'bmp': 'image/bmp', 'fax': 'image/fax', 'gif': 'image/gif', 'ico': 'image/x-icon', 'jfif': 'image/jpeg', 'jpg': 'image/jpeg', 'jpe': 'image/jpeg', 'jpeg': 'image/jpeg', 'eot': 'application/vnd.ms-fontobject', 'woff': 'application/font-woff', 'ttf': 'application/octet-stream', 'cur': 'application/octet-stream' }; function getIconv() { if (!iconv) { iconv = require('iconv-lite'); } return iconv; } function getTar() { if (!tar) { tar = require('tar'); } return tar; } /** * fis 中工具类操作集合。{@link https://lodash.com/ lodash} 中所有方法都挂载在此名字空间下面。 * @param {String} path * @return {String} * @example * /a/b//c\d/ -> /a/b/c/d * @namespace fis.util */ var _ = module.exports = function(path) { var type = typeof path; if (arguments.length > 1) { path = Array.prototype.join.call(arguments, '/'); } else if (type === 'string') { //do nothing for quickly determining. } else if (type === 'object') { path = Array.prototype.join.call(path, '/'); } else if (type === 'undefined') { path = ''; } if (path) { path = pth.normalize(path.replace(/[\/\\]+/g, '/')).replace(/\\/g, '/'); if (path !== '/') { path = path.replace(/\/$/, ''); } } return path; }; // 将lodash内部方法的引用挂载到utils上,方便使用 lodash.assign(_, lodash); _.is = function(source, type) { return toString.call(source) === '[object ' + type + ']'; }; /** * 对象枚举元素遍历,若merge为true则进行_.assign(obj, callback),若为false则回调元素的key value index * @param {Object} obj 源对象 * @param {Function|Object} callback 回调函数|目标对象 * @param {Boolean} merge 是否为对象赋值模式 * @memberOf fis.util * @name map * @function */ _.map = function(obj, callback, merge) { var index = 0; for (var key in obj) { if (obj.hasOwnProperty(key)) { if (merge) { callback[key] = obj[key]; } else if (callback(key, obj[key], index++)) { break; } } } }; /** * 固定长度字符前后缀填补方法(fillZero) * @param {String} str 初始字符串 * @param {Number} len 固定长度 * @param {String} fill 填补的缀 * @param {Boolean} pre 前缀还是后缀 * @return {String} 填补后的字符串 * @memberOf fis.util * @name pad * @function */ _.pad = function(str, len, fill, pre) { if (str.length < len) { fill = (new Array(len)).join(fill || ' '); if (pre) { str = (fill + str).substr(-len); } else { str = (str + fill).substring(0, len); } } return str; }; /** * 将target合并到source上,新值为undefiend一样会覆盖掉原有数据 * @param {Object} source 源对象 * @param {Object} target 目标对象 * @return {Object} 合并后的对象 * @memberOf fis.util * @name merge * @function */ _.merge = function(source, target) { if (_.is(source, 'Object') && _.is(target, 'Object')) { _.map(target, function(key, value) { source[key] = _.merge(source[key], value); }); } else { source = target; } return source; }; /** * clone一个变量 * @param {any} source 变量 * @return {any} clone值 * @memberOf fis.util * @name clone * @function */ /*_.clone = function(source) { var ret; switch (toString.call(source)) { case '[object Object]': ret = {}; _.map(source, function(k, v) { ret[k] = _.clone(v); }); break; case '[object Array]': ret = []; source.forEach(function(ele) { ret.push(_.clone(ele)); }); break; default: ret = source; } return ret; };*/ /** * 正则串编码转义 * @param {String} str 正则串 * @return {String} 普通字符串 * @memberOf fis.util * @name escapeReg * @function */ _.escapeReg = function(str) { return str.replace(/[\.\\\+\*\?\[\^\]\$\(\){}=!<>\|:\/]/g, '\\$&'); }; /** * shell命令编码转义 * @param {String} 命令 * @memberOf fis.util * @name escapeShellCmd * @function */ _.escapeShellCmd = function(str) { return str.replace(/ /g, '"$&"'); }; /** * shell编码转义 * @param {String} 命令 * @memberOf fis.util * @name escapeShellArg * @function */ _.escapeShellArg = function(cmd) { return '"' + cmd + '"'; }; /** * 提取字符串中的引号和一对引号包围的内容 * @param {String} str 待处理字符串 * @param {String} quotes 初始引号可选范围,缺省为[',"] * @return {Object} { * origin: 源字符串 * rest: 引号包围的文字内容 * quote: 引号类型 * } * @memberOf fis.util * @name stringQuote * @function */ _.stringQuote = function(str, quotes) { var info = { origin: str, rest: str = str.trim(), quote: '' }; if (str) { quotes = quotes || '\'"'; var strLen = str.length - 1; for (var i = 0, len = quotes.length; i < len; i++) { var c = quotes[i]; if (str[0] === c && str[strLen] === c) { info.quote = c; info.rest = str.substring(1, strLen); break; } } } return info; }; /** * 匹配文件后缀所属MimeType类型 * @param {String} ext 文件后缀 * @return {String} MimeType类型 * @memberOf fis.util * @name getMimeType * @function */ _.getMimeType = function(ext) { if (ext[0] === '.') { ext = ext.substring(1); } return MIME_MAP[ext] || 'application/x-' + ext; }; /** * 判断文件是否存在。 * @param {String} filepath 文件路径。 * @memberOf fis.util * @name exist * @function */ _.exists = _exists; _.fs = fs; /** * 返回path的绝对路径,若path不存在则返回false * @param {String} path 路径 * @return {String} 绝对路径 * @memberOf fis.util * @name realpath * @function */ _.realpath = function(path) { if (path && _exists(path)) { path = fs.realpathSync(path); if (IS_WIN) { path = path.replace(/\\/g, '/'); } if (path !== '/') { path = path.replace(/\/$/, ''); } return path; } else { return false; } }; /** * 多功能path处理 * @param {String} path 路径 * @return {String} 处理后的路径 * @memberOf fis.util * @name realpathSafe * @function */ _.realpathSafe = function(path) { return _.realpath(path) || _(path); }; /** * 判断路径是否为绝对路径 * @param {String} path 路径 * @return {Boolean} true为是 * @memberOf fis.util * @name isAbsolute * @function */ // _.isAbsolute = function(path) { // if (!IS_WIN && path && path[0] === '~') { // return true; // } // return pth.isAbsolute ? pth.isAbsolute(path) : pth.resolve(path) === pth.normalize(path); // }; _.isAbsolute = function(path) { if (IS_WIN) { return /^[a-z]:/i.test(path); } else { if (path === '/') { return true; } else { var split = path.split('/'); if (split[0] === '~') { return true; } else if (split[0] === '' && split[1]) { return _.isDir('/' + split[1] + '/' + split[2]); } else { return false; } } } }; /** * 是否为一个文件 * @param {String} path 路径 * @return {Boolean} true为是 * @memberOf fis.util * @name isFile * @function */ _.isFile = function(path) { return _exists(path) && fs.statSync(path).isFile(); }; /** * 是否为文件夹 * @param {String} path 路径 * @return {Boolean} true为是 * @memberOf fis.util * @name isDir * @function */ _.isDir = function(path) { return _exists(path) && fs.statSync(path).isDirectory(); }; /** * 获取路径最近修改时间 * @param {String} path 路径 * @return {Date} 时间(GMT+0800) * @memberOf fis.util * @name mtime * @function */ _.mtime = function(path) { var time = 0; if (_exists(path)) { time = fs.statSync(path).mtime; } return time; }; /** * 修改文件时间戳 * @param {String} path 路径 * @param {Date|Number} mtime 时间戳 * @memberOf fis.util * @name touch * @function */ _.touch = function(path, mtime) { if (!_exists(path)) { _.write(path, ''); } if (mtime instanceof Date) { //do nothing for quickly determining. } else if (typeof mtime === 'number') { var time = new Date(); time.setTime(mtime); mtime = time; } else { fis.log.error('invalid argument [mtime]'); } fs.utimesSync(path, mtime, mtime); }; /** * 是否为windows系统 * @return {Boolean} * @memberOf fis.util * @name isWin * @function */ _.isWin = function() { return IS_WIN; }; /* * 生成对应文件类型判断的正则 * @param {String} type 文件类型 * @return {RegExp} 对应判断的正则表达式 */ function getFileTypeReg(type) { var map = [], ext = fis.config.get('project.fileType.' + type); if (type === 'text') { map = TEXT_FILE_EXTS; } else if (type === 'image') { map = IMAGE_FILE_EXTS; } else { fis.log.error('invalid file type [%s]', type); } if (ext && ext.length) { if (typeof ext === 'string') { ext = ext.split(/\s*,\s*/); } map = map.concat(ext); } map = map.join('|'); return new RegExp('\\.(?:' + map + ')$', 'i'); } /** * 是否为配置中的text文件类型 * @param {String} path 路径 * @return {Boolean} * @memberOf fis.util * @name isTextFile * @function */ _.isTextFile = function(path) { return getFileTypeReg('text').test(path || ''); }; /** * 是否为配置中的image文件类型 * @param {String} path 路径 * @return {Boolean} * @memberOf fis.util * @name isImageFile * @function */ _.isImageFile = function(path) { return getFileTypeReg('image').test(path || ''); }; /** * 按位数生成md5串 * @param {String|Buffer} data 数据源 * @param {Number} len 长度 * @return {String} md5串 * @memberOf fis.util * @name md5 * @function */ _.md5 = function(data, len) { var md5sum = crypto.createHash('md5'), encoding = typeof data === 'string' ? 'utf8' : 'binary'; md5sum.update(data, encoding); len = len || fis.config.get('project.md5Length', 7); return md5sum.digest('hex').substring(0, len); }; /** * 生成base64串 * @param {String|Buffer|Array} data 数据源 * @return {String} base64串 * @memberOf fis.util * @name base64 * @function */ _.base64 = function(data) { if (data instanceof Buffer) { //do nothing for quickly determining. } else if (data instanceof Array) { data = new Buffer(data); } else { //convert to string. data = new Buffer(String(data || '')); } return data.toString('base64'); }; /** * 递归创建文件夹 * @param {String} path 路径 * @param {Number} mode 创建模式 * @memberOf fis.util * @name mkdir * @function */ _.mkdir = function(path, mode) { if (typeof mode === 'undefined') { //511 === 0777 mode = 511 & (~process.umask()); } if (_exists(path)) return; path.split('/').reduce(function(prev, next) { if (prev && !_exists(prev)) { fs.mkdirSync(prev, mode); } return prev + '/' + next; }); if (!_exists(path)) { fs.mkdirSync(path, mode); } }; /** * 字符串编码转换 * @param {String|Number|Array|Buffer} str 待处理的字符串 * @param {String} encoding 编码格式 * @return {String} 编码转换后的字符串 * @memberOf fis.util * @name toEncoding * @function */ _.toEncoding = function(str, encoding) { return getIconv().toEncoding(String(str), encoding); }; /** * 判断Buffer是否为utf8 * @param {Buffer} bytes 待检数据 * @return {Boolean} true为utf8 * @memberOf fis.util * @name isUtf8 * @function */ _.isUtf8 = function(bytes) { var i = 0; while (i < bytes.length) { if (( // ASCII 0x00 <= bytes[i] && bytes[i] <= 0x7F )) { i += 1; continue; } if (( // non-overlong 2-byte (0xC2 <= bytes[i] && bytes[i] <= 0xDF) && (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) )) { i += 2; continue; } if ( ( // excluding overlongs bytes[i] == 0xE0 && (0xA0 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) ) || ( // straight 3-byte ((0xE1 <= bytes[i] && bytes[i] <= 0xEC) || bytes[i] == 0xEE || bytes[i] == 0xEF) && (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) ) || ( // excluding surrogates bytes[i] == 0xED && (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x9F) && (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) ) ) { i += 3; continue; } if ( ( // planes 1-3 bytes[i] == 0xF0 && (0x90 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) && (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF) ) || ( // planes 4-15 (0xF1 <= bytes[i] && bytes[i] <= 0xF3) && (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0xBF) && (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) && (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF) ) || ( // plane 16 bytes[i] == 0xF4 && (0x80 <= bytes[i + 1] && bytes[i + 1] <= 0x8F) && (0x80 <= bytes[i + 2] && bytes[i + 2] <= 0xBF) && (0x80 <= bytes[i + 3] && bytes[i + 3] <= 0xBF) ) ) { i += 4; continue; } return false; } return true; }; /** * 处理Buffer编码方式 * @param {Buffer} buffer 待读取的Buffer * @return {String} 判断若为utf8可识别的编码则去掉bom返回utf8编码后的String,若不为utf8可识别编码则返回gbk编码后的String * @memberOf fis.util * @name readBuffer * @function */ _.readBuffer = function(buffer) { if (_.isUtf8(buffer)) { buffer = buffer.toString('utf8'); if (buffer.charCodeAt(0) === 0xFEFF) { buffer = buffer.substring(1); } } else { buffer = getIconv().decode(buffer, 'gbk'); } return buffer; }; /** * 读取文件内容 * @param {String} path 路径 * @param {Boolean} convert 是否使用text方式转换文件内容的编码 @see readBuffer * @return {String} 文件内容 * @memberOf fis.util * @name read * @function */ _.read = function(path, convert) { var content = false; if (_exists(path)) { content = fs.readFileSync(path); if (convert || _.isTextFile(path)) { content = _.readBuffer(content); } } else { fis.log.error('unable to read file[%s]: No such file or directory.', path); } return content; }; /** * 写文件,若路径不存在则创建 * @param {String} path 路径 * @param {String} data 写入内容 * @param {String} charset 编码方式 * @param {Boolean} append 是否为追加模式 * @memberOf fis.util * @name write * @function */ _.write = function(path, data, charset, append) { if (!_exists(path)) { _.mkdir(_.pathinfo(path).dirname); } if (charset) { data = getIconv().encode(data, charset); } if (append) { fs.appendFileSync(path, data, null); } else { fs.writeFileSync(path, data, null); } }; /** * str过滤处理,判断include中匹配str为true,exclude中不匹配str为true * @param {String} str 待处理的字符串 * @param {Array} include include匹配规则 * @param {Array} exclude exclude匹配规则 * @return {Boolean} 是否匹配 * @memberOf fis.util * @name filter * @function */ _.filter = function(str, include, exclude) { // pattern处理,若不为正则则调用glob处理生成正则 function normalize(pattern) { var type = toString.call(pattern); switch (type) { case '[object String]': return _.glob(pattern); case '[object RegExp]': return pattern; default: fis.log.error('invalid regexp [%s].', pattern); } } // 判断str是否符合patterns中的匹配规则 function match(str, patterns) { var matched = false; if (!_.is(patterns, 'Array')) { patterns = [patterns]; } patterns.every(function(pattern) { if (!pattern) { return true; } matched = matched || str.search(normalize(pattern)) > -1; return !matched; }); return matched; } var isInclude, isExclude; if (include) { isInclude = match(str, include); } else { isInclude = true; } if (exclude) { isExclude = match(str, exclude); } return isInclude && !isExclude; }; /** * 若rPath为文件夹,夹遍历目录下符合include和exclude规则的全部文件;若rPath为文件,直接匹配该文件路径是否符合include和exclude规则 * @param {String} rPath 要查找的目录 * @param {Array} include 包含匹配正则集合,可传null * @param {Array} exclude 排除匹配正则集合,可传null * @param {String} root 根目录 * @return {Array} 符合规则的文件路径的集合 * @memberOf fis.util * @name find * @function */ _.find = function(rPath, include, exclude, root) { var list = [], path = _.realpath(rPath), filterPath = root ? path.substring(root.length) : path; if (path) { var stat = fs.statSync(path); if (stat.isDirectory() && (include || _.filter(filterPath, include, exclude))) { fs.readdirSync(path).forEach(function(p) { if (p[0] != '.') { list = list.concat(_.find(path + '/' + p, include, exclude, root)); } }); } else if (stat.isFile() && _.filter(filterPath, include, exclude)) { list.push(path); } } else { fis.log.error('unable to find [%s]: No such file or directory.', rPath); } return list.sort(); }; /** * 删除指定目录下面的文件。 * @memberOf fis.util * @name del * @function */ _.del = function(rPath, include, exclude) { var removedAll = true; var path; if (rPath && _.exists(rPath)) { var stat = fs.lstatSync(rPath); var isFile = stat.isFile() || stat.isSymbolicLink(); if (stat.isSymbolicLink()) { path = rPath; } else { path = _.realpath(rPath); } if (/^(?:\w:)?\/$/.test(path)) { fis.log.error('unable to delete directory [%s].', rPath); } if (stat.isDirectory()) { fs.readdirSync(path).forEach(function(name) { if (name != '.' && name != '..') { removedAll = _.del(path + '/' + name, include, exclude) && removedAll; } }); if (removedAll) { fs.rmdirSync(path); } } else if (isFile && _.filter(path, include, exclude)) { fs.unlinkSync(path); } else { removedAll = false; } } else { //fis.log.error('unable to delete [' + rPath + ']: No such file or directory.'); } return removedAll; }; /** * 复制符合include和exclude规则的文件到目标目录,若rSource为文件夹则递归其下属每个文件 * @param {String} rSource 源路径 * @param {String} target 目标路径 * @param {Array} include 包含匹配规则 * @param {Array} exclude 排除匹配规则 * @param {Boolean} uncover 是否覆盖 * @param {Boolean} move 是否移动 * @memberOf fis.util * @name copy * @function */ _.copy = function(rSource, target, include, exclude, uncover, move) { var removedAll = true, source = _.realpath(rSource); target = _(target); if (source) { var stat = fs.statSync(source); if (stat.isDirectory()) { fs.readdirSync(source).forEach(function(name) { if (name != '.' && name != '..') { removedAll = _.copy( source + '/' + name, target + '/' + name, include, exclude, uncover, move ) && removedAll; } }); if (move && removedAll) { fs.rmdirSync(source); } } else if (stat.isFile() && _.filter(source, include, exclude)) { if (uncover && _exists(target)) { //uncover removedAll = false; } else { _.write(target, fs.readFileSync(source, null)); if (move) { fs.unlinkSync(source); } } } else { removedAll = false; } } else { fis.log.error('unable to copy [%s]: No such file or directory.', rSource); } return removedAll; }; /** * path处理 * @param {String} str 待处理的路径 * @return {Object} * @example * str = /a.b.c/f.php?kw=%B2%E5%BB%AD#addfhubqwek * { * origin: '/a.b.c/f.php?kw=%B2%E5%BB%AD#addfhubqwek', * rest: '/a.b.c/f', * hash: '#addfhubqwek', * query: '?kw=%B2%E5%BB%AD', * fullname: '/a.b.c/f.php', * dirname: '/a.b.c', * ext: '.php', * filename: 'f', * basename: 'f.php' * } * @memberOf fis.util * @name ext * @function */ _.ext = function(str) { var info = _.query(str), pos; str = info.fullname = info.rest; if ((pos = str.lastIndexOf('/')) > -1) { if (pos === 0) { info.rest = info.dirname = '/'; } else { info.dirname = str.substring(0, pos); info.rest = info.dirname + '/'; } str = str.substring(pos + 1); } else { info.rest = info.dirname = ''; } if ((pos = str.lastIndexOf('.')) > -1) { info.ext = str.substring(pos).toLowerCase(); info.filename = str.substring(0, pos); info.basename = info.filename + info.ext; } else { info.basename = info.filename = str; info.ext = ''; } info.rest += info.filename; return info; }; /** * path处理,提取path中rest部分(?之前)、query部分(?#之间)、hash部分(#之后) * @param {String} str 待处理的url * @return {Object} { * origin: 原始串 * rest: path部分(?之前) * query: query部分(?#之间) * hash: hash部分(#之后) * } * @memberOf fis.util * @name query * @function */ _.query = function(str) { var rest = str, pos = rest.indexOf('#'), hash = '', query = ''; if (pos > -1) { hash = rest.substring(pos); rest = rest.substring(0, pos); } pos = rest.indexOf('?'); if (pos > -1) { query = rest.substring(pos); rest = rest.substring(0, pos); } rest = rest.replace(/\\/g, '/'); if (rest !== '/') { // 排除由于.造成路径查找时因filename为""而产生bug,以及隐藏文件问题 rest = rest.replace(/\/\.?$/, ''); } return { origin: str, rest: rest, hash: hash, query: query }; }; /** * 生成路径信息 * @param {String|Array} path 路径,可使用多参数传递:pathinfo('a', 'b', 'c') * @return {Object} @see ext() * @memberOf fis.util * @name pathinfo * @function */ _.pathinfo = function(path) { //can not use _() method directly for the case _.pathinfo('a/'). var type = typeof path; if (arguments.length > 1) { path = Array.prototype.join.call(arguments, '/'); } else if (type === 'string') { //do nothing for quickly determining. } else if (type === 'object') { path = Array.prototype.join.call(path, '/'); } return _.ext(path); }; /** * 驼峰写法转换 * @param {String} str 待转换字符串 * @return {String} 转换后的字符串 * @memberOf fis.util * @name camelcase * @function */ _.camelcase = function(str) { var ret = ''; if (str) { str.split(/[-_ ]+/).forEach(function(ele) { ret += ele[0].toUpperCase() + ele.substring(1); }); } else { ret = str; } return ret; }; /** * 加载处理fis模块下的全部插件,如fis3-plugin-* * @param {String} type 模块名 * @param {Function} callback 回调 * @param {Object} def 模块获取的默认值 @see fis.config.get def * @memberOf fis.util * @name pipe * @function */ _.pipe = function(type, callback, def) { var processors = fis.media().get('modules.' + type, def); if (processors) { // 兼容处理[]、String、'String1, String2, ...'的配置写法 if ('string' === typeof processors) { processors = processors.trim().split(/\s*,\s*/); } else if (!Array.isArray(processors)) { processors = [processors]; } // 过滤掉同名的插件, 没必要重复操作。 processors = processors.filter(function(item, idx, arr) { item = item.__name || item; return idx === _.findIndex(arr, function(target) { target = target.__name || target; return target === item; }); }); // 若type为多层级(ex: 'a.b'),获取fis.config中groups[defaultGroup]['modules']下一层配置项的名称 type = type.split('.')[0]; processors.forEach(function(obj, index) { var processor = obj.__name || obj; var key; if (typeof processor === 'string') { key = type + '.' + processor; processor = fis.require(type, processor); } else { key = type + '.' + index; } if (typeof processor === 'function') { var settings = {}; _.assign(settings, processor.defaultOptions || processor.options || {}); _.assign(settings, fis.media().get('settings.' + key, {})); typeof obj === 'object' && _.assign(settings, obj); // 删除隐藏配置 delete settings.__name; delete settings.__plugin; delete settings.__pos; delete settings.__isPlugin; callback(processor, settings, key, type); } else { fis.log.warning('invalid processor [modules.' + key + ']'); } }); } }; /** * url解析函数,规则类似require('url').parse * @param {String} url 待解析的url * @param {Object} opt 解析配置参数 { host|hostname, port, path, method, agent } * @return {Object} { protocol, host, port, path, method, agent } * @memberOf fis.util * @name parseUrl * @function */ _.parseUrl = function(url, opt) { opt = opt || {}; url = Url.parse(url); var ssl = url.protocol === 'https:'; opt.host = opt.host || opt.hostname || ((ssl || url.protocol === 'http:') ? url.hostname : 'localhost'); opt.port = opt.port || (url.port || (ssl ? 443 : 80)); opt.path = opt.path || (url.pathname + (url.search ? url.search : '')); opt.method = opt.method || 'GET'; opt.agent = opt.agent || false; return opt; }; /** * 下载功能实现 * @param {String} url 下载的url * @param {Function} callback 回调 * @param {String} extract 压缩提取路径 * @param {Object} opt 配置 * @memberOf fis.util * @name download * @function */ _.download = function(url, callback, extract, opt) { opt = _.parseUrl(url, opt || {}); var http = opt.protocol === 'https:' ? require('https') : require('http'), name = _.md5(url, 8) + _.ext(url).ext, tmp = fis.project.getTempPath('downloads', name), data = opt.data; delete opt.data; _.write(tmp, ''); var writer = fs.createWriteStream(tmp), http_err_handler = function(err) { writer.destroy(); fs.unlinkSync(tmp); var msg = typeof err === 'object' ? err.message : err; if (callback) { callback(msg); } else { fis.log.error('request error [%s]', msg); } }, req = http.request(opt, function(res) { var status = res.statusCode; res .on('data', function(chunk) { writer.write(chunk); }) .on('end', function() { if (status >= 200 && status < 300 || status === 304) { if (extract) { fs .createReadStream(tmp) .pipe(getTar().Extract({ path: extract })) .on('error', function(err) { if (callback) { callback(err); } else { fis.log.error('extract tar file [%s] fail, error [%s]', tmp, err); } }) .on('end', function() { if (callback && (typeof callback(null, tmp, res) === 'undefined')) { fs.unlinkSync(tmp); } }); } else if (callback && (typeof callback(null, tmp, res) === 'undefined')) { fs.unlinkSync(tmp); } } else { http_err_handler(status); } }) .on('error', http_err_handler); }); req.on('error', http_err_handler); if (data) { req.write(data); } req.end(); }; /** * 遵从RFC规范的文件上传功能实现 * @param {String} url 上传的url * @param {Object} opt 配置 * @param {Object} data 要上传的formdata,可传null * @param {String} content 上传文件的内容 * @param {String} subpath 上传文件的文件名 * @param {Function} callback 上传后的回调 * @memberOf fis.util * @name upload * @function */ _.upload = function(url, opt, data, content, subpath, callback) { if (typeof content === 'string') { content = new Buffer(content, 'utf8'); } else if (!(content instanceof Buffer)) { fis.log.error('unable to upload content [%s]', (typeof content)); } opt = opt || {}; data = data || {}; var endl = '\r\n'; var boundary = '-----np' + Math.random(); var collect = []; _.map(data, function(key, value) { collect.push('--' + boundary + endl); collect.push('Content-Disposition: form-data; name="' + key + '"' + endl); collect.push(endl); collect.push(value + endl); }); collect.push('--' + boundary + endl); collect.push('Content-Disposition: form-data; name="' + (opt.uploadField || "file") + '"; filename="' + subpath + '"' + endl); collect.push(endl); collect.push(content); collect.push(endl); collect.push('--' + boundary + '--' + endl); var length = 0; collect.forEach(function(ele) { if (typeof ele === 'string') { length += new Buffer(ele).length; } else { length += ele.length; } }); opt.method = opt.method || 'POST'; opt.headers = _.assign({ 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': length }, opt.headers || {}); opt = _.parseUrl(url, opt); var http = opt.protocol === 'https:' ? require('https') : require('http'); var req = http.request(opt, function(res) { var status = res.statusCode; var body = ''; res .on('data', function(chunk) { body += chunk; }) .on('end', function() { if (status >= 200 && status < 300 || status === 304) { callback(null, body); } else { callback(status); } }) .on('error', function(err) { callback(err.message || err); }); }); req.on('error', function(err) { callback(err.message || err); }); collect.forEach(function(d) { req.write(d); }); req.end(); }; /** * 读取fis组件安装 * @param {String} name 组件名称 * @param {String} version 版本标识 * @param {Object} opt 安装配置 { remote, extract, before, error, done, } * @memberOf fis.util * @name install * @function */ _.install = function(name, version, opt) { version = version === '*' ? 'latest' : (version || 'latest'); var remote = opt.remote.replace(/^\/$/, ''); var url = remote + '/' + name + '/' + version + '.tar'; var extract = opt.extract || process.cwd(); if (opt.before) { opt.before(name, version); } _.download(url, function(err) { if (err) { if (opt.error) { opt.error(err); } else { fis.log.error('unable to download component [%s@%s] from [%s], error [%s].', name, version, url, err); } } else { if (opt.done) { opt.done(name, version); } process.stdout.write('install [' + name + '@' + version + ']\n'); var pkg = _(extract, 'package.json'); if (_.isFile(pkg)) { var info = _.readJSON(pkg); fs.unlinkSync(pkg); _.map(info.dependencies || {}, function(depName, depVersion) { _.install(depName, depVersion, opt); }); } } }, extract); }; /** * 读取JSON文件 * @param {String} path 路径 * @return {Object} JSON文件内容JSON.parse后得到的对象 * @memberOf fis.util * @name readJson * @function */ _.readJSON = function(path) { var json = _.read(path), result = {}; try { result = JSON.parse(json); } catch (e) { fis.log.error('parse json file[%s] fail, error [%s]', path, e.message); } return result; }; /** * 模拟linux glob文法实现,但()为捕获匹配模式 * @param {String} pattern 符合fis匹配文法的正则串 * @param {String} str 待匹配的字符串 * @param {Object} options 匹配设置参数 @see minimatch.makeRe * @return {Boolean|RegExp} 若str参数为String则返回str是否可被pattern匹配 * 若str参数不为String,则返回正则表达式 * @memberOf fis.util * @name glob * @function */ _.glob = function(pattern, str, options) { var regex; // 推荐使用 ::text 和 ::image // text 和 image 后续也许不会再支持。 if (~['::text', '::image', 'text', 'image'].indexOf(pattern)) { regex = getFileTypeReg(pattern.replace(/^::/, '')); // 当以下用法时,$0 应该是拿到文件路径的全部,而不是只有后缀,所以需要修改 regex。 // fis.match('::image', { // url: '$0' // }) // regex = new RegExp( '^.*' + regex.source, regex.ignoreCase ? 'i' : ''); } else { // 由于minimatch提供的glob支持中()语法不符合fis glob的需求,因此只针对()单独处理 var hasBracket = ~pattern.indexOf('('); // 当用户配置 *.js 这种写法的时候,需要让其命中所有所有目录下面的。 if (/^(\(*?)(?!\:|\/|\(|\*\*)(.*)$/.test(pattern)) { pattern = '**/' + pattern; } var special = /^(\(+?)\*\*/.test(pattern); // support special global star // 保留原来的 **/ 和 /** 用法,只扩展 **.ext 这种用法。 pattern = pattern.replace(/\*\*(?!\/|$)/g, '\uFFF0gs\uFFF1'); if (hasBracket) { if (special) { pattern = pattern.replace(/\(/g, '\uFFF0/').replace(/\)/g, '/\uFFF1'); } else { pattern = pattern.replace(/\(/g, '\uFFF0').replace(/\)/g, '\uFFF1'); } } regex = minimatch.makeRe(pattern, options || { matchBase: true, // nocase: true }); pattern = regex.source; pattern = pattern.replace(/\uFFF0gs\uFFF1/g, '(?!\\.)(?=.).*'); if (hasBracket) { if (special) { pattern = pattern.replace(/\uFFF0\\\//g, '(').replace(/\\\/\uFFF1/g, ')'); } else { pattern = pattern.replace(/\uFFF0/g, '(').replace(/\uFFF1/g, ')'); } } regex = new RegExp(pattern, regex.ignoreCase ? 'i' : ''); } if (typeof str === 'string') { return regex.test(str); } return regex; }; /** * 调起nohup命令 * @param {String} cmd 执行的语句 * @param {Object} options 配置参数,可传可不传 @see [child_process.exec options](https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback) * @param {Function} callback nohup执行完毕的回调函数 * @memberOf fis.util * @name nohup * @function */ _.nohup = function(cmd, options, callback) { if (typeof options === 'function') { callback = options; options = null; } var exec = require('child_process').exec; if (IS_WIN) { var cmdEscape = cmd.replace(/"/g, '""'), file = fis.project.getTempPath('nohup-' + _.md5(cmd) + '.vbs'), script = ''; script += 'Dim shell\n'; script += 'Set shell = Wscript.CreateObject("WScript.Shell")\n'; script += 'ret = shell.Run("cmd.exe /c start /b ' + cmdEscape + '", 0, TRUE)\n'; script += 'WScript.StdOut.Write(ret)\n'; script += 'Set shell = NoThing'; _.write(file, script); return exec('cscript.exe /nologo "' + file + '"', options, function(error, stdout) { if (stdout != '0') { fis.log.error('exec command [%s] fail.', cmd); } fs.unlinkSync(file); if (typeof callback === 'function') { callback(); } }); } else { return exec('nohup ' + cmd + ' > /dev/null 2>&1 &', options, function(error, stdout) { if (error !== null) { fis.log.error('exec command [%s] fail, stdout [%s].', cmd, stdout); } if (typeof callback === 'function') { callback(); } }); } }; /** * 判断对象是否为null,[],{},0 * @param {Object} obj 待测对象 * @return {Boolean} 是否为空 * @memberOf fis.util * @name isEmpty * @function */ _.isEmpty = function(obj) { if (obj == null) return true; if (_.is(obj, 'Array')) return obj.length == 0; for (var key in obj) { if (obj.hasOwnProperty(key)) { return false; } } return true }; /** * 将 matches 规则应用到某个对象上面。 * * @param {String} path 路径。用来与 match 规则匹配 * @param {Array} matches 规则数组 * @param {Array} allowed 可以用来过滤掉不关心的字段。 * @memberOf fis.util * @name applyMatches * @function */ _.applyMatches = function(path, matches, allowed) { var obj = {}; matches.forEach(function(item, index) { var properties = item.properties || {}; var keys = Object.keys(properties); if (!keys.length) { return; } var m; var set = item.reg; if (!Array.isArray(set)) { set = [set]; } set.every(function(reg) { reg.lastIndex = 0; // reset if (m = reg.exec(path)) { return false; } return true; }); var match = !!m; if (match !== item.negate) { // 当用 negate 模式时,排除命中特殊选择器 if (item.negate && ~path.indexOf(':')) { return; } m = m || { 0: path }; keys.forEach(function(key) { // 如果指定了允许的属性名,走白名单规则规则。 if (allowed && !~allowed.indexOf(key)) { return; } var value = typeof properties[key] === 'object' ? fis.util.cloneDeep(properties[key]) : properties[key]; // 将 property 中 $1 $2... 替换为本来的值 if (typeof value === 'string') { value = value.replace(/\$(\d+|&)/g, function(_, k) { k = k === '&' ? 0 : k; return m[k] || ''; }); } else if (typeof(value) === 'function' && ~[ 'release', 'url', 'relative', 'moduleId' ].indexOf(key)) { value = value.call(null, m, path); } // 记录是命中的 match 的位置。 obj['__' + key + 'Index'] = index; // 调整 plugin 顺序 if (value && value.__plugin && value.__pos) { if (!obj[key]) { obj[key] = value; } else { if (!Array.isArray(obj[key])) { obj[key] = [obj[key]]; } var pos = value.__pos; pos = pos === 'prepend' ? 0 : (pos === 'append' ? obj[key].length : (parseInt(pos) || 0)); obj[key].splice(pos, 0, value); } } else if (_.isPlainObject(value) && _.isPlainObject(obj[key]) && !value.__isPlugin && !obj[key].__isPlugin) { fis.util.assign(obj[key], value); } else { obj[key] = value; } }); } }); return obj; };