UNPKG

mockm

Version:

Analog interface server, painless parallel development of front and back ends.

1,848 lines (1,515 loc) 52.8 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault"); var _find = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/find")); var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise")); var _trim = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/trim")); var _map = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/map")); var _stringify = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/json/stringify")); var _now = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/date/now")); var _setTimeout2 = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/set-timeout")); var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes")); var _indexOf = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/index-of")); var _slice = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/slice")); var _forEach = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/for-each")); var _keys = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/object/keys")); var _reduce = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/reduce")); var _repeat = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/repeat")); var _url = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/url")); var _sort = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/sort")); var _findIndex = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/find-index")); var _setInterval2 = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/set-interval")); var _lastIndexOf = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/last-index-of")); var _reverse = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/reverse")); var _isArray = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/array/is-array")); var _filter = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/filter")); var _padStart = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/pad-start")); const { print } = require(`./log.js`); function tool() { // 与业务没有相关性, 可以脱离业务使用的工具函数 function npm() { // npm 相关 /** * 获取本地 package 版本号 * @param {string} name packageName * @param {object} param1 选项 * @param {string} param1.packagePath node_modules 路径 */ function getLocalVersion(name, { packagePath } = {}) { // 从本地获取版本号 const hasFile = tool().file.hasFile; const pathList = [...require.main.paths, `${require(`path`).parse(process.execPath).dir}/node_modules`, `${require(`path`).parse(process.execPath).dir}/../lib/node_modules`]; packagePath = packagePath || (0, _find.default)(pathList).call(pathList, path => hasFile(`${path}/${name}/package.json`)); if (packagePath) { return require(`${packagePath}/${name}/package.json`).version; // 从 package 中获取版本 } } /** * 从 npmjs 中获取版本号 * @param {*} name packageName 名称 */ function getServerVersion(name) { // 从 npmjs 中获取版本号 return new _promise.default((resolve, reject) => { const https = require(`https`); https.get(`https://registry.npmjs.org/-/package/${name}/dist-tags`, res => { let data = ``; res.on(`data`, chunk => { data += chunk; }); res.on(`end`, () => { const latest = JSON.parse(data || `{}`).latest; // 获取最新版本 resolve(latest); }); }).on(`error`, err => { reject(err.message); }); }); } /** * 从 npmjs 检查依赖版本 * @param {*} name 要检查更新的依赖名称 * @param {object} param1 参数 * @param {string} param1.version 指定版本 * @param {array} param1.packagePath 指定路径 */ async function checkUpdate(name, { version, packagePath } = {}) { const getLocalVersionRes = version || getLocalVersion(name, { packagePath }); const getServerVersionRes = await getServerVersion(name).catch(err => console.log(err)); return { local: getLocalVersionRes, server: getServerVersionRes }; } /** * 获取当前 npm 配置文件中使用的注册表地址, 而不是环境变量中的地址 */ async function getNpmRegistry() { const cp = require(`child_process`); const url = fn().tryFn(() => { var _context; return (0, _trim.default)(_context = cp.execSync(`npm config get registry`, { env: { /** * 设置为空, 即可避免使用当前环境变量中的值而是使用 npm 配置文件中的值 * 为了避免通过 yarn 启动时, 获取到的值是 registry.yarnpkg.com * 但我们需要的其实是需要 npm 本身配置的值, 例如通过 nrm use 来切换的配置 */ NPM_CONFIG_REGISTRY: undefined } }).toString()).call(_context); }); return url; } return { getNpmRegistry, getLocalVersion, getServerVersion, checkUpdate }; } function control() { // 流程控制 /** * 同步执行异步函数, 由于是把函数源码抽到单独的 js 文件中运行, 所以有一些限制 * - 如果依赖 babel 可能导致 toString 后的代码没有相关 polyfill * - 入参和出参需要可序列化(json), 不会输出出参之外的其他信息 * - 函数内不要有外部依赖(例如) * @param fn 要运行的函数 * @return {function} 接收原参数, 返回 {res, err} */ function asyncTosync(fn) { const { createNewFile, filesCreateOrRemove } = file(); return (...args) => { const { writeFileSync, readFileSync } = require(`fs`); const fnStr = fn.toString(); const tempDir = (__dirname || require(`os`).tmpdir()).replace(/\\/g, `/`); const fileObj = { fnFile: createNewFile(tempDir, `fn.js`), resFile: createNewFile(tempDir, `res.log`), errFile: createNewFile(tempDir, `err.log`) }; filesCreateOrRemove(fileObj, `create`); let res = ``; let err = ``; try { const argsString = (0, _map.default)(args).call(args, arg => (0, _stringify.default)(arg)).join(`, `); const codeString = ` const { writeFileSync } = require('fs') const fn = ${fnStr} new Promise(() => { fn(${argsString}) .then((output = '') => { writeFileSync("${fileObj.resFile}", String(output), 'utf8') }) .catch((error = '') => { writeFileSync("${fileObj.errFile}", String(error), 'utf8') }) .finally(() => { process.exit() }) } ) `; writeFileSync(fileObj.fnFile, codeString, `utf8`); require(`child_process`).execSync(`"${process.execPath}" ${fileObj.fnFile}`); res = readFileSync(fileObj.resFile, `utf8`); res = res ? JSON.parse(res) : undefined; err = readFileSync(fileObj.errFile, `utf8`); err = err ? JSON.parse(err) : undefined; } catch (error) { console.log(`error`, error); } filesCreateOrRemove(fileObj, `remove`); return { res, err }; }; } /** * 以 Promise 方式等待条件成立 * @param {*} condition 条件函数, 返回 true 时才陈立 * @param {*} ms 检测条件的实时间隔毫秒 * @param {*} timeout 超时 */ function awaitTrue({ condition, ms = 250, timeout = 5e3 }) { return new _promise.default(async (resolve, reject) => { let timeStart = (0, _now.default)(); let res = await condition(); while (res !== true) { res = await condition(); if ((0, _now.default)() - timeStart > timeout) { // 超时 reject(false); } await sleep(ms); } resolve(res); }); } /** * 异步等待 sleep * @param {*} ms 毫秒 */ function sleep(ms = 1e3) { // 异步 sleep return new _promise.default(resolve => (0, _setTimeout2.default)(resolve, ms)); } return { asyncTosync, awaitTrue, sleep }; } function cache() { // 缓存处理程序 function delRequireCache(filePath) { delete require.cache[require.resolve(filePath)]; } return { delRequireCache }; } function hasPackage(name, cfg = {}) { // 是还存在某个包 return Boolean(npm().getLocalVersion(name)); } /** * 自动安装依赖 * // ? todo 当使用 yarn mm remote 或 npm run mm remote 的包管理器启动程序时, 看不到实时输出效果, * 需要使用 mm remote 这种直接调用可执行文件的方式才能实时输出, 不知道为什么 * 注意, 假设安装 a 依赖后, a 依赖会被存储到 dependencies 中, 建议保留它, 因为可能是对等依赖 * 例如 joi-to-swagger 依赖 joi, 这要求在父项目的 dependencies 中显式存在 joi 并已安装 * @param {*} param0 * @returns */ async function installPackage({ cwd, env, pkg, attempt = 3, requireName }) { const registryUrl = await npm().getNpmRegistry(); const { MOCKM_REGISTRY } = process.env; const useUrl = registryUrl || MOCKM_REGISTRY || `https://registry.npmmirror.com/`; process.env.NPM_CONFIG_REGISTRY = useUrl; cwd = cwd.replace(/\\/g, `/`); // 注意: 修改为 npm 时某些依赖会无法安装, 需要使用 cnpm 成功率较高 const { manager } = require(`whatnpm`); let installEr = manager(cwd).er || `npm`; if (hasPackage(installEr) === false) { // 如果安装器不存在, 则退出, 注意: 可能遇到安装器判断错误的情况. print(cli.colors.red(`Please install mockm with npm and try again`)); process.exit(); } // 不再使用 --registry 参数, 因为某些管理器要求此值与 lock 中的值一致 // 不再使用 npx , 因为它在新版本需要交互式确认 const cmd = { npm: `npm add ${pkg}`, pnpm: `pnpm add ${pkg}`, // pnpm 其实不支持 --registry 参数 cnpm: `cnpm i ${pkg}`, // cnpm 其实不支持 add 参数 yarn: `yarn add ${pkg}` }[installEr]; const cd = require(`os`).type() === `Windows_NT` ? `cd /d` : `cd`; const tips = tool().string.removeLeft(` initializing: ${pkg}... ${tool().cli.colors.yellow(`If the automatic installation fails, you can try running the following commands manually:`)} ${tool().cli.getFullLine()} ${cd} "${cwd}" ${cmd} ${cd} "${process.cwd()}" ${tool().cli.getFullLine()} `); print(tips); let attemptNum = attempt; // 重试次数 do { const cp = require(`child_process`); cp.execSync(cmd, { stdio: `inherit`, // 实时转发子进程的输出到当前控制台中 cwd, env: { ...process.env, ...env, NPM_CONFIG_REGISTRY: useUrl } }); if (attemptNum < attempt) { print(`number of retries: ${attempt - attemptNum}/${attempt - 1}`); } attemptNum = attemptNum - 1; } while (hasPackage(requireName) === false && attemptNum > 0); const hasPackageRes = hasPackage(requireName); print(tool().cli.colors[[`red`, `green`][Number(hasPackageRes)]](`Initialize ${pkg} ${[`failed`, `successfully`][Number(hasPackageRes)]}`)); return hasPackageRes; } function generate() { // 生成器 /** * 如果某个依赖不存在, 则安装它 * @param {*} pkg 要安装的依赖, 与 npm i 后面的参数一致 * @param {object} param1 配置 * @param {boolean} param1.getRequire 是否安装完成后进行 require * @param {boolean} param1.requireName require 时使用的名称, 默认为自动解析, 例如当使用 url 安装时程序是无法知道真实名称的, 需要指定 * @param {object} param1.env 安装时的环境变量 * @param {string} param1.msg 依赖不存在时提示的消息 */ async function initPackge(pkg, { getRequire = true, requireName, env = {}, msg } = {}) { try { const path = require(`path`); const mainPath = path.join(__dirname, `../`); // 主程序目录 const packageJson = require(`${mainPath}/package.json`); let pkgVersion = ``; if ((0, _includes.default)(pkg).call(pkg, `://`) === false) { var _context2; let nameEndsAt = pkg[0] === `@` ? (0, _indexOf.default)(_context2 = (0, _slice.default)(pkg).call(pkg, 1)).call(_context2, `@`) + 1 : (0, _indexOf.default)(pkg).call(pkg, `@`); pkgVersion = nameEndsAt > 0 ? (0, _slice.default)(pkg).call(pkg, nameEndsAt + 1) : ``; if (pkgVersion === ``) { // 若未指定版本时, 从已声明的依赖中选择版本 pkgVersion = pkgVersion || (packageJson.pluginDependencies || {})[pkg] || (packageJson.optionalDependencies || {})[pkg] || packageJson.dependencies[pkg] || ``; pkg = pkgVersion ? `${pkg}@${pkgVersion}` : pkg; } } const requireNameNew = requireName || (pkgVersion ? pkg.replace(`@${pkgVersion}`, ``) : pkg); const hasPackageRes = hasPackage(requireNameNew); if (hasPackageRes === false) { // 如果依赖不存在, 则安装它 msg && console.log(msg); await installPackage({ cwd: mainPath, env, pkg, requireName: requireNameNew }); } if (getRequire) { cache().delRequireCache(requireNameNew); return require(requireNameNew); } } catch (err) { console.log(`err`, err); } } function nextId() { // 获取全局自增 id global.id = (global.id || 0) + (0, _now.default)() + 1; return global.id; } return { initPackge, nextId }; } function hex() { // 进制转换 /** * 字节可读化 * @param {*} size * @returns */ function getSize(size) { const sizes = [` Bytes`, ` KB`, ` MB`, ` GB`, ` TB`, ` PB`, ` EB`, ` ZB`, ` YB`]; for (let i = 1; i < sizes.length; i++) { if (size < Math.pow(1024, i)) return Math.round(size / Math.pow(1024, i - 1) * 100) / 100 + sizes[i - 1]; } return size; } function string10to62(number) { // 10 进制转 62 进制, 用来压缩数字长度 const chars = `0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ`.split(``); const radix = chars.length; const arr = []; let qutient = +number; do { const mod = qutient % radix; qutient = (qutient - mod) / radix; arr.unshift(chars[mod]); } while (qutient); return arr.join(``); } function string62to10(str) { // 62 进制转 10 进制 str = String(str); const chars = `0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ`; const radix = chars.length; const len = str.length; let origin_number = 0; let i = 0; while (i < len) { origin_number += Math.pow(radix, i++) * (0, _indexOf.default)(chars).call(chars, str.charAt(len - i) || 0); } return origin_number; } return { getSize, string10to62, string62to10 }; } function cli() { // 命令行相关处理程序 /** * 处理命令行上传入的路径参数, 如果是相对路径, 则相对于运行命令的目录, 而不是相对于书写 require() 方法文件的目录 * @param {*} pathStr 路径 */ function handlePathArg(pathStr) { if (pathStr === undefined) { return undefined; } const path = require(`path`); let newPathStr = path.isAbsolute(pathStr) ? pathStr : `${process.cwd()}/${pathStr}`; // 如果是相对路径, 则相对于运行命令的位置 newPathStr = path.normalize(newPathStr); // 转换为跨平台的路径 return newPathStr; } /** * 自定义控制台颜色 * https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color * nodejs 内置颜色: https://nodejs.org/api/util.html#util_foreground_colors */ function colors() { var _context3; const util = require(`util`); function colorize(color, text) { const codes = util.inspect.colors[color]; return `\x1b[${codes[0]}m${text}\x1b[${codes[1]}m`; } let returnValue = {}; (0, _forEach.default)(_context3 = (0, _keys.default)(util.inspect.colors)).call(_context3, color => { returnValue[color] = text => colorize(color, text); }); const colorTable = new Proxy(returnValue, { get(obj, prop) { // 在没有对应的具名颜色函数时, 返回空函数作为兼容处理 const res = obj[prop] ? obj[prop] : arg => arg; return res; } }); // 取消下行注释, 查看所有的颜色和名字: // Object.keys(returnValue).forEach((color) => console.log(returnValue[color](color))) return colorTable; } /** * 以 Promise 方式运行 spawn * @param {*} cmd 主程序 * @param {*} args 程序参数数组 * @param {*} opts spawn 选项 */ function spawn(cmd, args, opts) { opts = { stdio: `inherit`, ...opts }; opts.shell = opts.shell || process.platform === `win32`; return new _promise.default((resolve, reject) => { const cp = require(`child_process`); const child = cp.spawn(cmd, args, opts); let stdout = ``; let stderr = ``; child.stdout && child.stdout.on(`data`, d => { stdout += d; }); child.stderr && child.stderr.on(`data`, d => { stderr += d; }); child.on(`error`, reject); child.on(`close`, code => { resolve({ code, stdout, stderr }); }); }); } function parseArgv(arr) { var _context4, _context5; // 解析命令行参数 return (0, _reduce.default)(_context4 = arr || (0, _slice.default)(_context5 = process.argv).call(_context5, 2)).call(_context4, (acc, arg) => { let [k, ...v] = arg.split(`=`); v = v.join(`=`); // 把带有 = 的值合并为字符串 acc[k] = v === `` // 没有值时, 则表示为 true ? true : /^(true|false)$/.test(v) // 转换指明的 true/false ? v === `true` : /[\d|.]+/.test(v) ? isNaN(Number(v)) ? v : Number(v) // 如果转换为数字失败, 则使用原始字符 : v; return acc; }, {}); } /** * 从 curl 命令中解析 request 库的 options 参数 * @param {string} cmd // curl/bash 命令 */ async function getOptions(cmd) { const curlconverter = await tool().generate.initPackge(`curlconverter`); const requestStr = curlconverter.toNodeRequest(cmd); let optionStr = requestStr.match(/^var request = require[\s\S].*;([\s\S]*)^function callback([\s\S]*)/m)[1]; // 只取出 options 相关的代码 let options = {}; try { const { NodeVM } = require(`vm2`); const vm = new NodeVM(); options = vm.run(`${optionStr}\nmodule.exports = options`, `vm.js`) || ``; // 避免 node v10.12.0 出现 options.uri is a required argument options.url = options.url || options.uri; options.uri = options.url || options.uri; } catch (err) { console.log(`err`, err); } return options; } /** * 以单行形式输出文本到终端 * @param {string} str 要输出的字符 */ function onlyLine(str) { var _context6; const columns = process.stdout.columns; // 终端字符宽度 str = str.length > columns ? str : (0, _repeat.default)(_context6 = ` `).call(_context6, columns).replace(new RegExp(`.{${str.length}}`), str); // 以空格补齐整行终端, 避免其他字符侵入本行 process.stdout.write(`\r${str}`); } /** * 获取与终端大小相同的字符串 * @param {string} str 要输出的字符 * @returns {string} */ function getFullLine(str = `=`) { const size = (process.stdout.columns || 80) - 1; // 给换行符让位 return (0, _repeat.default)(str).call(str, size); } return { handlePathArg, getFullLine, onlyLine, spawn, parseArgv, getOptions, colors: colors() }; } function url() { // url 处理程序 /** * 根据 pathname 返回最匹配的 url * @param {*} param0.urlList url 列表 * @param {*} param0.pathname pathname */ function findLikeUrl({ urlList, pathname }) { const apiSplitList = new _url.default(`http://127.0.0.1${pathname}`).pathname.split(`/`); const lvList = (0, _map.default)(urlList).call(urlList, openApiItem => { const openApiSplitList = new _url.default(openApiItem).pathname.split(`/`); const lv = (0, _reduce.default)(apiSplitList).call(apiSplitList, (acc, apiSplitListItem, apiSplitListItemIndex) => { return acc + (apiSplitListItem === openApiSplitList[apiSplitListItemIndex] ? 1 : 0); }, 0); return lv; }); function findMaxIndex(arr) { var _context7; // 查找数组中最大的数的索引 const sortRes = (0, _sort.default)(_context7 = [...arr]).call(_context7, (a, b) => a - b); return (0, _findIndex.default)(arr).call(arr, item => item === sortRes[arr.length - 1]); } const maxIndex = findMaxIndex(lvList); return urlList[maxIndex]; } function fullApi2Obj(api) { let [, method, url] = api.match(/(\S+)\s+(.*)/) || [undefined, `*`, (0, _trim.default)(api).call(api)]; if (method === `*`) { method = `all`; } if (url === undefined || url === `/`) { url = `/`; // 注意不能是 *, 否则使用 use 时会出问题 /** 这可能与 https://github.com/expressjs/express/issues/2495 有关, 需要使用 / 或 {0,} 代替 * 和 / 不一样 假设有两个 use 中间件, 都使用了静态文件, 第一个 path 为 * , 第二个为 /test * 的情况: /test/a.html 在第一个不存在时, 并不会进入 /test / 的情况: 不存在时会进入 /test */ } const { path } = tool().httpClient.getClientUrlAndPath(url); return { path, method, url }; } function parseUrlArgToObjList(urlArg) { // 转换 url 参数为对象数组 const querystring = require(`querystring`); const obj = querystring.parse(urlArg); // <= urlArg const res = []; if (typeof obj.action === `string`) { res.push(obj); } else { var _context8; (0, _forEach.default)(_context8 = obj.action).call(_context8, (item, index) => { res.push({ action: obj.action[index], arg: obj.arg[index] }); }); } return res; } return { findLikeUrl, fullApi2Obj, parseUrlArgToObjList }; } function file() { // 文件相关 /** * 递归复制 * @param {*} from * @param {*} to */ function copyFolderSync(from, to) { var _context9; const fs = require(`fs`); const path = require(`path`); hasFile(to) === false && fs.mkdirSync(to); (0, _forEach.default)(_context9 = fs.readdirSync(from)).call(_context9, element => { if (fs.lstatSync(path.join(from, element)).isFile()) { hasFile(path.join(to, element)) === false && fs.copyFileSync(path.join(from, element), path.join(to, element)); } else { copyFolderSync(path.join(from, element), path.join(to, element)); } }); } /** * 创建或删除一组文件 * @param objOrArr {object|number} 要操作的内容 * @param action {stirng} 操作方式 create remove */ function filesCreateOrRemove(objOrArr, action) { var _context10; const { writeFileSync, unlinkSync } = require(`fs`); (0, _forEach.default)(_context10 = (0, _keys.default)(objOrArr)).call(_context10, key => { const name = objOrArr[key]; if (action === `create`) { writeFileSync(name, ``, `utf8`); } if (action === `remove`) { unlinkSync(name); } }); } function fileChange(file, cb) { const chokidar = require(`chokidar`); tool().type.isEmpty(file) === false && chokidar.watch(file, { ignored: `**/node_modules/**`, usePolling: true }).on(`change`, files => { cb(files); }); } /** * 根据 dirName 和 fileName 返回一个当前目录不存在的文件名 * @param dirName 目录 * @param fileName 名称 * @return {stirng} 例 `${dirName}/temp_${Date.now()}.${fileName}` */ function createNewFile(dirName, fileName) { const newFile = `${dirName}/temp_${(0, _now.default)()}.${fileName}`; return require(`fs`).existsSync(newFile) === true ? createNewFile(dirName, fileName) : newFile; } /** * 检查 nodmeon 是否可以修改文件后重启应用(某些设备可以检测到修改, 但应用并没有重启) * @param {number} timeout 多少毫秒后超时返回 false, 默认 3000 * @returns {boolean} */ function checkChangeRestart(timeout = 3000) { return new _promise.default((resolve, reject) => { const fs = require(`fs`); const os = require(`os`); const path = require(`path`); const nodemon = require(`nodemon`); const jsFile = path.normalize(`${os.tmpdir()}/${(0, _now.default)()}.js`); const tag = (0, _now.default)(); const fnStr = (arg => { console.log(arg); (0, _setInterval2.default)(() => {}, 2 * 1000); }).toString(); fs.writeFileSync(jsFile, `(${fnStr})('')`); (0, _setTimeout2.default)(() => { fs.writeFileSync(jsFile, `(${fnStr})(${tag})`); }, 500); nodemon({ ignoreRoot: [], exec: `node ${jsFile}`, watch: [jsFile], stdout: false }).on(`readable`, function (arg) { this.stdout.on(`data`, data => { var _context11; let log = (0, _trim.default)(_context11 = String(data)).call(_context11); if (log && (0, _includes.default)(log).call(log, tag)) { end(true); } }); }); (0, _setTimeout2.default)(() => { end(false); }, timeout); function end(isOk) { resolve(isOk); nodemon.emit(`quit`); fs.existsSync(jsFile) && fs.unlinkSync(jsFile); } }); } const fileStoreInitValueMap = {}; function fileStore(storePath, initValue = {}) { // 存取需要持久化存储的数据 initValue = fileStoreInitValueMap[storePath] = fileStoreInitValueMap[storePath] || initValue; const fs = require(`fs`); const { o2s, deepSet, deepGet } = obj(); let initEd = false; const init = () => { if (initEd === false) { if (isFileEmpty(storePath)) { fs.writeFileSync(storePath, o2s(initValue)); } else { // 避免后期添加的键由于存在文件而没有正常初始化 const store = JSON.parse(fs.readFileSync(storePath, `utf-8`)); fs.writeFileSync(storePath, o2s({ ...initValue, ...store })); } } initEd = true; }; let store = () => { return isFileEmpty(storePath) ? JSON.parse((0, _stringify.default)(initValue)) : JSON.parse(fs.readFileSync(storePath, `utf-8`)); }; return { set(key, val) { init(); const newStore = store(); deepSet(newStore, key, val); fs.writeFileSync(storePath, o2s(newStore)); return store; }, get(key) { const newStore = store(); return deepGet(newStore, key); }, updateApiCount() { const apiCountOld = this.get(`apiCount`) || 0; this.set(`apiCount`, apiCountOld + 1); return this.get(`apiCount`); } }; } function hasFile(filePath) { // 判断文件或目录是否存在 const fs = require(`fs`); return fs.existsSync(filePath); } /** * 获取文件 md5 - 同步版 * @param {*} pathOrBuffer 文件路径或 buffer * @param {*} type 如果是文件路径时, type 为 path * @returns */ function getFileMd5(pathOrBuffer, type) { const fs = require(`fs`); const buffer = type === `path` ? fs.readFileSync(pathOrBuffer) : pathOrBuffer; const crypto = require(`crypto`); const md5 = crypto.createHash(`md5`).update(buffer).digest(`hex`); return md5; } /** * 获取远程 url 的 * @param {string} url http url * @returns {object} {data, ext} binary 和后缀名 */ function getFile(url) { const [, tag = ``, username, password] = url.match(/:\/\/((.+):(.+)@)/) || []; url = url.replace(tag, ``); return new _promise.default((resolve, reject) => { var _context12; const http = require((0, _trim.default)(_context12 = url.replace(/(:\/\/.*)/, ``)).call(_context12)); // 获取 http 或 https http.get(url, { auth: username ? `${username}:${password}` : undefined }, res => { const { statusCode } = res; if (statusCode !== 200) { reject(statusCode); } let data = ``; res.setEncoding(`binary`); res.on(`data`, chunk => { data += chunk; }); res.on(`end`, () => { const mime = require(`mime`); const ext = mime.getExtension(res.headers[`content-type`]) || ``; resolve({ data, ext }); }); }).on(`error`, e => { reject(e.message); }); }); } /** * 根据 url 获取文件的存储路径以及文件名, 避免特殊字符 * @param {string} url http 地址 * @returns {object} {pathname, fileName} fileName 是 query 参数生成的名字 */ function getFilePath(url) { const filenamify = require(`filenamify`); const { pathname, search = `` } = new _url.default(url); const fileName = filenamify(search, { maxLength: 255, replacement: `_` }); return { pathname, fileName }; } /** * 备份一个 http url 对应的文件 * @param {string} baseDir 要备份于什么目录之下 * @param {string} fileUrl 文件 url * @param {function} format 备份前格式化数据 */ async function backUrl(baseDir = __dirname, fileUrl, format) { var _context13; let { data: fileData, ext: fileExt } = (await getFile(fileUrl).catch(err => { console.log(err); })) || {}; if (fileData === undefined) { return false; } if (fileData && format) { fileData = format(fileData); } const { pathname, fileName } = getFilePath(fileUrl); const fs = require(`fs`); const dir = `${baseDir}/${pathname}`; fs.mkdirSync(dir, { recursive: true }); // 从符合备份文件名规则的所有文件中找到最新备份的那个文件名和时间, 获取文件的 md5 与请求的 md5 做比较 const getMax = (0, _reduce.default)(_context13 = fs.readdirSync(dir)).call(_context13, (acc, curFileName) => { const re = new RegExp(`${fileName}_(\\d{4}-\\d{2}-\\d{2} \\d{2}-\\d{2}-\\d{2})\\.`); const [, tag = ``] = curFileName.match(re) || []; const curTime = Number(tag.replace(/\D/g, ``)); return { maxTime: acc.maxTime < curTime ? curTime : acc.maxTime, curFileName: acc.maxTime < curTime ? curFileName : acc.curFileName }; }, { maxTime: 0, curFileName: `` }); const newName = time().dateFormat(`YYYY-MM-DD hh-mm-ss`, new Date()); if (getMax.maxTime) { const oldMd5 = getFileMd5(fs.readFileSync(`${dir}/${getMax.curFileName}`)); const tempFile = `${dir}/temp`; saveFile(tempFile, fileData); const newMd5 = getFileMd5(fs.readFileSync(tempFile)); fs.unlinkSync(tempFile); if (oldMd5 !== newMd5) { saveFile(`${dir}/${fileName}_${newName}.${fileExt}`, fileData); } } else { saveFile(`${dir}/${fileName}_${newName}.${fileExt}`, fileData); } } /** * 获取 http url 备份文件 * @param {string} baseDir 备份于什么目录之下 * @param {string} fileUrl 文件 url * @return {string} 最新的文件 */ async function getBackUrl(baseDir = __dirname, fileUrl) { var _context14; const { pathname, fileName } = getFilePath(fileUrl); const fs = require(`fs`); const dir = `${baseDir}/${pathname}`; if (hasFile(dir) === false) { return undefined; } const getMax = (0, _reduce.default)(_context14 = fs.readdirSync(dir)).call(_context14, (acc, curFileName) => { const re = new RegExp(`${fileName}_(\\d{4}-\\d{2}-\\d{2} \\d{2}-\\d{2}-\\d{2})\\.`); const [, tag = ``] = curFileName.match(re) || []; const curTime = Number(tag.replace(/\D/g, ``)); return { maxTime: acc.maxTime < curTime ? curTime : acc.maxTime, curFileName: acc.maxTime < curTime ? curFileName : acc.curFileName }; }, { maxTime: 0, curFileName: `` }); if (getMax.maxTime) { return `${dir}/${getMax.curFileName}`; } else { return undefined; } } /** * 保存文件 * @param {string} filePath 文件的路径 * @param {binary} bin 二进制内容 */ function saveFile(filePath, bin) { const fs = require(`fs`); fs.writeFileSync(filePath, bin, { encoding: `binary` }); } function getMd5(path) { // 获取文件 md5 return new _promise.default((resolve, reject) => { const fs = require(`fs`); const crypto = require(`crypto`); const md5sum = crypto.createHash(`md5`); const stream = fs.createReadStream(path); stream.on(`data`, chunk => { md5sum.update(chunk); }); stream.on(`end`, () => { const md5 = md5sum.digest(`hex`).toUpperCase(); resolve(md5); }); }); } function getMd5Sync(path) { // 获取文件 md5 const fs = require(`fs`); const buffer = fs.readFileSync(path); return tool().string.getMd5(buffer); } function isFileEmpty(file) { var _context15; // 判断文件是否存或为空 const fs = require(`fs`); return hasFile(file) === false || (0, _trim.default)(_context15 = fs.readFileSync(file, `utf-8`)).call(_context15) === ``; } /** * 当文件不存在或内容为空时创建文件, 自动创建目录 * arg[0].filePath 文件地址 * arg[0].str 文件内容 * arg[0].force 是否强行覆盖 * * @return boolean 是否创建 */ function createFile({ filePath, str = ``, force = false, msg = `` } = {}) { const path = require(`path`); const fs = require(`fs`); const dir = path.parse(filePath).dir; dir && fs.existsSync(dir) === false && fs.mkdirSync(dir, { recursive: true }); let res = false; if (force) { res = (fs.writeFileSync(filePath, str), true); } else { res = isFileEmpty(filePath) && (fs.writeFileSync(filePath, str), true); } return res; } return { fileChange, createFile, copyFolderSync, filesCreateOrRemove, createNewFile, checkChangeRestart, getBackUrl, backUrl, fileStore, getMd5, getMd5Sync, isFileEmpty, hasFile }; } function httpClient() { function getClientUrlAndPath(originalUrl) { // 获取从客户端访问的 url 以及 path // 当重定向路由(mock api)时, req.originalUrl 和 req.url 不一致, req.originalUrl 为浏览器中访问的 url, 应该基于这个 url 获取 path return { url: originalUrl, path: new _url.default(originalUrl, `http://127.0.0.1`).pathname }; } function getClientIp(req) { var _context16; // 获取客户端 IP const { deepGet } = obj(); let ip = (0, _trim.default)(_context16 = deepGet(req, `headers.x-forwarded-for`, ``).split(`,`).pop()).call(_context16) || // 判断是否有反向代理 IP req.ip || deepGet(req, `connection.remoteAddress`) || // 判断 connection 的远程 IP deepGet(req, `socket.remoteAddress`) || // 判断后端的 socket 的 IP deepGet(req, `connection.socket.remoteAddress`) || ``; if ((0, _includes.default)(ip).call(ip, `,`)) { ip = ip.split(`,`)[0]; } ip = ip.substr((0, _lastIndexOf.default)(ip).call(ip, `:`) + 1, ip.length); // ::ffff:xxx => xxx ip = { /** 当使用以下请求方式时, 获取到的 ip 为 ::1 - js fetch(`http://localhost:9090/ip`); - cli http :9000/ip */ "1": `127.0.0.1` }[ip] || ip; return ip; } /** * 判断请求是 http 还是 https * https://stackoverflow.com/questions/10348906/how-to-know-if-a-request-is-http-or-https-in-node-js * @param {*} req * @returns */ function getProtocol(req) { let proto = req.connection.encrypted ? 'https' : 'http'; // only do this if you trust the proxy proto = req.headers['x-forwarded-proto'] || proto; return proto.split(/\s*,\s*/)[0]; } return { getProtocol, getClientUrlAndPath, getClientIp }; } function fn() { // 函数处理工具 function emptyFn(f) { // 把函数的参数 {}, [], null 转为默认值 return (...a) => { return f(...(0, _map.default)(a).call(a, v => { return type().isEmpty(v) ? undefined : v; })); }; } /** * 让函数运行不报错 */ function tryFn(fn, ...arg) { try { return fn(...arg); } catch (error) { return undefined; } } return { tryFn, emptyFn }; } function obj() { // 对象处理工具 // 深度排序对象 function sortObj(obj, newObj = {}) { var _context17, _context18; (0, _forEach.default)(_context17 = (0, _sort.default)(_context18 = (0, _keys.default)(obj)).call(_context18)).call(_context17, key => { if (typeof obj[key] === `object`) { newObj[key] = type().isType(obj[key], `array`) ? [] : {}; newObj[key] = sortObj(obj[key], newObj[key]); } else { newObj[key] = obj[key]; } }); return newObj; } /** * 排序对象的 key, 仅操作第一层 * @param {*} obj 要排序的对象 * @param {*} opt.firstLong 是否长 key 在前, 默认 false * @returns */ function sortKey(obj, { firstLong = false } = {}) { var _context19; const newObj = {}; let keys = (0, _sort.default)(_context19 = (0, _keys.default)(obj)).call(_context19); if (firstLong) { keys = (0, _reverse.default)(keys).call(keys); } (0, _forEach.default)(keys).call(keys, key => newObj[key] = obj[key]); return newObj; } function flatObj(value, currentKey) { var _context20; // 展开对象 let result = {}; (0, _forEach.default)(_context20 = (0, _keys.default)(value)).call(_context20, key => { const tempKey = currentKey ? `${currentKey}.${key}` : key; if (typeof value[key] !== `object`) { result[tempKey] = value[key]; } else { result = { ...result, ...flatObj(value[key], tempKey) }; } }); return result; } function deepGet(object, keys = [], defaultValue) { var _context21; // 深层获取对象值 let res = (0, _reduce.default)(_context21 = !(0, _isArray.default)(keys) ? keys.replace(/\[/g, `.`).replace(/\]/g, ``).split(`.`) : keys).call(_context21, (o, k) => (o || {})[k], object); return res !== undefined ? res : defaultValue; } function deepMergeObject(originObj, adventObj) { var _context22; // 深层合并对象 const flatObjRes = flatObj(adventObj); (0, _forEach.default)(_context22 = (0, _keys.default)(flatObjRes)).call(_context22, key => { deepSet(originObj, key, flatObjRes[key]); }); return originObj; } function deepSet(object, keys, val) { // 深层设置对象值 keys = (0, _isArray.default)(keys) ? keys : keys.replace(/\[/g, `.`).replace(/\]/g, ``).split(`.`); if (keys.length > 1) { object[keys[0]] = object[keys[0]] || {}; deepSet(object[keys[0]], (0, _slice.default)(keys).call(keys, 1), val); return object; } object[keys[0]] = val; return object; } function removeEmpty(obj) { var _context23; // 删除对象中为空值的键 obj = { ...obj }; (0, _forEach.default)(_context23 = (0, _keys.default)(obj)).call(_context23, key => { if (type().isEmpty(obj[key])) { delete obj[key]; } }); return obj; } function o2s(o) { // 对象转字符串 return (0, _stringify.default)(o, null, 2); } return { sortKey, sortObj, npm, deepMergeObject, flatObj, deepGet, deepSet, removeEmpty, o2s }; } function os() { // 系统工具 /** * * @param {string} param0 动作, set, remove * @param {object} param1.ip 对应的 ip, 默认 127.0.0.1 * @param {object} param1.hostname 对应的 hostname */ async function sysHost(action, { ip = `127.0.0.1`, hostname }) { const hostile = await tool().generate.initPackge(`hostile`); return new _promise.default((resolve, reject) => { hostile[action](ip, hostname, err => { err ? reject(err) : resolve(true); }); }); } /** * 程序退出前清理工具 * @param {object} param0.hostname 恢复 host 文件修改 */ function clearProcess({ hostname } = {}) { function killProcess(...arg) { if (arg[0] !== `SIGINT`) { process.send({ type: 'process:msg', data: { action: 'err-exit', data: [] } }); } else { process.exit(); } hostname ? sysHost(`remove`, { hostname }).finally(() => {}) : () => {}; } process.on(`SIGTERM`, killProcess); process.on(`SIGINT`, killProcess); process.on(`uncaughtException`, killProcess); process.on(`unhandledRejection`, killProcess); } function portIsOk(port) { // 判断端口是否可用 if (typeof port === `object`) { // 判断多个端口 return _promise.default.all((0, _map.default)(port).call(port, item => portIsOk(item))); } return new _promise.default(resolve => { const net = require(`net`); const server = net.createServer().listen(port); server.on(`listening`, () => server.close(resolve(true))); server.on(`error`, () => resolve(port)); }); } function getOsIp() { // 获取系统 ip let gatewayIp; try { const gateway = require('default-gateway').v4.sync(); gatewayIp = require('address').ip(gateway && gateway.interface); // 获取默认IP } catch (error) { var _context24, _context25; // 获取默认网关错误 const obj = require(`os`).networkInterfaces(); const ipObj = (0, _filter.default)(_context24 = (0, _reduce.default)(_context25 = (0, _keys.default)(obj)).call(_context25, (res, cur, index) => { return [...res, ...obj[cur]]; }, [])).call(_context24, item => item.family === `IPv4`)[0] || {}; gatewayIp = ipObj.address; } return gatewayIp || `127.0.0.1`; } return { clearProcess, sysHost, getOsIp, portIsOk }; } function type() { // 类型处理工具 function isEmpty(value) { // 判断空值 return value === null || value === undefined // 避免存在 key 但未定义值 || value === `` || typeof value === `object` && (value.length === 0 || (0, _keys.default)(value).length === 0); } function isType(data, type = undefined) { // 判断数据是否为 type, 或返回 type const dataType = Object.prototype.toString.call(data).match(/\s(.+)]/)[1].toLowerCase(); return type ? dataType === type.toLowerCase() : dataType; } return { isEmpty, isType }; } function time() { /** * 时间格式化 * @param {string} fmt 格式 * @param {Date} date 时间对象 */ function dateFormat(fmt, date) { let ret; const opt = { 'Y+': date.getFullYear().toString(), // 年 'M+': (date.getMonth() + 1).toString(), // 月 'D+': date.getDate().toString(), // 日 'h+': date.getHours().toString(), // 时 'm+': date.getMinutes().toString(), // 分 's+': date.getSeconds().toString() // 秒 // 有其他格式化字符需求可以继续添加,必须转化成字符串 }; for (let k in opt) { ret = new RegExp(`(${k})`).exec(fmt); if (ret) { var _context26; fmt = fmt.replace(ret[1], ret[1].length == 1 ? opt[k] : (0, _padStart.default)(_context26 = opt[k]).call(_context26, ret[1].length, `0`)); } } return fmt; } return { dateFormat }; } function string() { // 字符串处理 /** * 获取字符串的 md5 * @param {*} str 字符串 */ function getMd5(str) { const crypto = require(`crypto`); const md5 = crypto.createHash(`md5`); return md5.update(str).digest(`hex`); } /** * 驼峰转下划线 * @param {*} str */ function toLine(str) { str = str.replace(str[0], str[0].toLowerCase()); return str.replace(/([A-Z])/g, `_$1`).toLowerCase(); } /** * 转换字符为小驼峰, 支持 `空格 - _` * @param {string} str 要处理的字符 */ function toLittleHump(str) { str = toLine(str); // 先转一下, 避免本来是驼峰转换后不是驼峰了 let arr = str.split(` `).join(`-`).split(`-`).join(`_`).split(`_`); for (let i = 1; i < arr.length; i++) { arr[i] = arr[i].charAt(0).toUpperCase() + arr[i].substring(1); } arr[0] = arr[0].toLowerCase(); // 此行为小驼峰 return arr.join(``); } function removeLeft(str) { var _context27, _context28; const lines = str.split(`\n`); // 获取应该删除的空白符数量 const minSpaceNum = (0, _sort.default)(_context27 = (0, _map.default)(_context28 = (0, _filter.default)(lines).call(lines, item => (0, _trim.default)(item).call(item))).call(_context28, item => item.match(/(^\s+)?/)[0].length)).call(_context27, (a, b) => a - b)[0]; // 删除空白符 const newStr = (0, _map.default)(lines).call(lines, item => (0, _slice.default)(item).call(item, minSpaceNum)).join(`\n`); return newStr; } return { getMd5, toLittleHump, toLine, removeLeft }; } function array() { // 数组处理 /** * 排序数组 * @param {array} arr 要排序的数组 * @param {object} param1 * @param {string} param1.key 要比较的 key * @param {boolean} param1.asc 是否升序, 默认是 */ function sort(arr, { key, asc } = {}) { asc = asc === undefined ? true : asc; (0, _sort.default)(arr).call(arr, (a, b) => { const val1 = key ? a[key] : a; const val2 = key ? b[key] : b; return asc ? val1 - val2 : val2 - val1; }); return arr; } /** * 转换数组为树形结构 * @param {array} data 包含 id, pid 的数组 * @param {function} childrenFn // 如果有 children 时, 可以接收 parent */ function toTree(data, childrenFn = () => {}) { let result = []; if (!(0, _isArray.default)(data)) { return result; } (0, _forEach.default)(data).call(data, item => { delete item.children; }); let map = {}; (0, _forEach.default)(data).call(data, item => { map[item.id] = item; }); (0, _forEach.default)(data).call(data, item => { let parent = map[item.pid]; if (parent) { childrenFn && childrenFn(parent); (parent.children || (parent.children = [])).push(item); } else { result.push(item); } }); return result; } /** * 根据字段值中的 - 标志转为树形结构 ``` js const arr = ` A -A.1 B -B.1 -B.2 --B.2.1 --B.2.2 C D E `.trim().split(`\n`).filter(item => item.trim()).map(item => ({key: item})) console.log(`res`, arrToTree(arr, {key: `key`, tag: `-`})) ``` * @param {array} arr 数组 * @param {object} param1 * @param {string} param1.key 对应的 key * @param {string} param1.tag 对应的 tag */ function arrToTree(arr, { key = `key`, tag = `-` } = {}) { let pid = undefined; // pid let oldTag = undefined; // 标志 const res = (0, _map.default)(arr).call(arr, (item, index) => { let [, newTag, res] = item[key].match(new RegExp(`^(${tag}*)(.*)`)); if (newTag === ``) { // 不存标志时 pid 为 root pid = `root`; } else if (newTag !== oldTag) { // 存在标志但标志标志不同, 则更新 pid pid = index - 1; } oldTag = newTag; return { ...item, [key]: res, // 删除 tag 标志的结果 id: index, pid }; }); return toTree(res, parent => { parent.type = `object`; }); } return { sort, arrToTree, toTree }; } return { array: array(), string: string(), npm: npm(), control: control(), cache: cache(), generate: generate(), url: url(), file: file(), cli: cli(), hex: hex(), httpClient: httpClient(), fn: fn(), obj: obj(), os: os(), type: type(), time: time() }; } module.exports = tool();