/**
* `GlaceJS` utils.
*
* @module glace-utils
*/
var fs = require("fs");
var path = require("path");
var readline = require("readline");
var util = require("util");
var colors = require("colors");
var espree = require("espree");
var highlight = require("cli-highlight").highlight;
var _ = require("lodash");
var yargs = require("yargs").help(" "); // disable default `--help` capture
module.exports.__findProcess = require("find-process");
var fse = require("fs-extra");
/**
* Clears empty folders recursive.
*
* @function
* @arg {string} folder - Path to root folder.
*/
var clearEmptyFolders = module.exports.clearEmptyFolders = folder => {
var files = fs.readdirSync(folder);
for (var fileName of files) {
var filePath = path.join(folder, fileName);
if (fs.statSync(filePath).isDirectory()) {
clearEmptyFolders(filePath);
}
}
if (!_.isEmpty(files)) {
files = fs.readdirSync(folder);
}
if (_.isEmpty(files)) {
fs.rmdirSync(folder);
}
};
/**
* Makes delay (sleep) during code execution.
*
* @function
* @arg {number} timeout - Time to sleep, ms.
* @arg {boolean} [blocking=false] - Flag whether sleep should be
* block code execution.
* @return {Promise<void>} If sleep isn't blocking.
* @return {undefined} If sleep is blocking.
*/
module.exports.sleep = (timeout, blocking) => {
blocking = !!blocking;
if (blocking) {
(ms => {
ms += new Date().getTime();
while (new Date() < ms) { /* nothing */ }
})(timeout);
} else {
return new Promise(resolve => {
setTimeout(resolve, timeout);
});
}
};
/**
* Composes file path from segments. If folder of file is absent, it will
* be created.
*
* @function
* @arg {...string} paths - A sequence of paths or path segments.
* @return {string} Composed path.
*/
module.exports.mkpath = function () {
var result = path.resolve.apply(path, arguments);
var dirname = path.dirname(result);
fse.mkdirsSync(dirname);
return result;
};
/**
* Helper to generate request key for storage.
*
* @function
* @arg {Request} req - Client request.
* @return {string} Request key according to its method, host, url.
*/
module.exports.getReqKey = req => req.method + "_" + req.headers.host + req.url;
/**
* Sorts files by date in folder.
*
* @function
* @arg {string} dir - Path to directory.
* @arg {object} [opts] - Options.
* @arg {boolean} [opts.desc=false] - Flag to reverse order.
* @return {string[]} Sequence of files sorted by date
*/
module.exports.filesByDate = (dir, opts) => {
opts = opts || {};
opts.desc = opts.desc || false;
var filesList = fs
.readdirSync(dir)
.filter(filename => {
var filePath = path.resolve(dir, filename);
return !fs.statSync(filePath).isDirectory();
})
.map(filename => {
var filePath = path.resolve(dir, filename);
return { path: filePath,
time: fs.statSync(filePath).mtime.getTime() };
})
.sort((a, b) => a.time - b.time)
.map(el => el.path);
if (opts.desc) filesList.reverse();
return filesList;
};
/**
* Files sorted by order.
*
* @function
* @arg {string} dir - Path to directory.
* @arg {object} [opts] - Options.
* @arg {boolean} [opts.desc=false] - Flag to reverse order.
* @return {string[]} Sequence of files sorted by order.
*/
module.exports.filesByOrder = (dir, opts) => {
opts = opts || {};
opts.desc = opts.desc || false;
var filesList = fs
.readdirSync(dir)
.filter(filename => {
var filePath = path.resolve(dir, filename);
return !fs.statSync(filePath).isDirectory();
})
.map(filename => {
return { path: path.resolve(dir, filename),
number: parseInt(_.split(filename, "-", 1)[0]) || 0 };
})
.sort((a, b) => a.number - b.number)
.map(el => el.path);
if (opts.desc) filesList.reverse();
return filesList;
};
/**
* Gets subfolders of directory.
*
* @function
* @arg {string} dir - Path to directory.
* @arg {object} [opts] - Options.
* @arg {boolean} [opts.nameOnly=false] - Gets only folder names. By default,
* full paths.
* @return {string[]} Sequence of results.
*/
module.exports.subFolders = (dir, opts) => {
opts = opts || {};
opts.nameOnly = opts.nameOnly || false;
if (!fs.existsSync(dir)) return [];
var dirsList = fs
.readdirSync(dir)
.filter(filename => {
var filePath = path.resolve(dir, filename);
return fs.statSync(filePath).isDirectory();
});
if (!opts.nameOnly) {
dirsList = dirsList.map(name => path.resolve(dir, name));
}
return dirsList;
};
/**
* Returns function which switches message color.
*
* @function
* @arg {object} [opts] - Options.
* @arg {string} [opts.c1=magenta] - Color #1.
* @arg {string} [opts.c2=cyan] - Color #2.
* @return {function} Function to switch color of passed text in terminal.
*/
const switchColor = module.exports.switchColor = opts => {
opts = opts || {};
var c1 = opts.c1 || "magenta";
var c2 = opts.c2 || "cyan";
var trigger = true;
return function () {
var msg = Array.from(arguments).join(" ");
msg = msg[trigger ? c1 : c2].bold;
trigger = !trigger;
return msg;
};
};
/**
* Exits process with error printing.
*
* @function
* @arg {string} source - Source of fatal error.
* @return {function} Function with takes error to print and exits process.
*/
module.exports.exit = source => err => {
console.log(source + ":", err);
process.exit(1);
};
/**
* @prop {string} cwd - Current work directory.
*/
module.exports.cwd = process.cwd();
module.exports.loadJson = require("./lib/loadJson");
module.exports.config = require("./lib/config");
module.exports.logger = require("./lib/logger");
/**
* Wraps function inside other functions.
*
* @function
* @arg {function[]} wrappers - List of functions which will wrap target.
* @arg {function} target - Target function which will be wrapped.
* @return {function} Wrapping function.
*/
module.exports.wrap = (wrappers, target) => {
_.clone(wrappers).reverse().forEach(wrapper => {
target = (target => () => wrapper(target))(target);
});
return target;
};
/**
* Helper to kill processes by name.
*
* @async
* @function
* @arg {string} procName - Process name or chunk of name.
* @return {Promise<void>}
*/
module.exports.killProcs = procName => {
var logger = I.logger;
logger.debug(`Looking for ${procName} processes to kill...`);
return I.__findProcess("name", procName).then(procList => {
return procList.forEach(proc => {
if ([process.pid, process.ppid].includes(+proc.pid)) return;
logger.debug(`Killing ${procName} with PID ${proc.pid}...`);
try {
process.kill(proc.pid, "SIGTERM");
logger.debug("Process is killed");
} catch (e) {
if (e.message !== "kill ESRCH") throw e;
logger.error(`Can't kill ${procName} with PID ${proc.pid} because it doesn't exist`);
}
});
});
};
/**
* Help
*
* @function
* @arg {function} [d] - Function to manage describe message: join, colorize, etc.
* @return {yargs} Preconfigured yargs.
*/
module.exports.help = d => {
d = d || switchColor();
return yargs
.options({
"config [path]": {
alias: "c",
describe: d("Path to JSON file with CLI arguments.",
"Default is 'cwd/config.json' (if it exists)."),
type: "string",
group: "Arguments:",
},
"stdout-log": {
describe: d("Print log messages to stdout."),
type: "boolean",
group: "Log:",
},
"log [path]": {
describe: d("Path to log file. Default is 'cwd/glace.log'."),
type: "string",
group: "Log:",
},
"log-level [level]": {
describe: d("Log level. Supported values are 'error', 'warn',",
"'info', 'verbose', 'debug', 'silly'. Default is 'debug'."),
type: "string",
group: "Log:",
},
})
.help("h")
.alias("h", "help");
};
/**
* Defines whether object is located on screen or no.
*
* @function
* @arg {object} obj - Object which may be on screen.
* @arg {object} screen - Screen object.
* @arg {object} [opts] - Options.
* @arg {boolean} [opts.fully=false] - Flag to check full presence on screen.
* @return {boolean} `true` if it is on screen, `false` otherwise.
*/
module.exports.isInScreen = (obj, screen, opts) => {
opts = I.coalesce(opts, {});
var fully = I.coalesce(opts.fully, false);
if (fully) {
return ((obj.x >= screen.x) &&
(obj.y >= screen.y) &&
(obj.x + obj.width <= screen.x + screen.width) &&
(obj.y + obj.height <= screen.y + screen.height));
} else {
return !((obj.x >= screen.x + screen.width) ||
(obj.y >= screen.y + screen.height) ||
(obj.x + obj.width <= screen.x) ||
(obj.y + obj.height <= screen.y));
}
};
/**
* Gets object position on screen.
*
* @function
* @arg {object} obj - Object which should be on screen.
* @arg {object} screen - Screen object.
* @return {object} Object position on screen.
* @throws {Error} If object isn't located on screen.
*/
module.exports.objOnScreenPos = (obj, screen) => {
if (!I.isInScreen(obj, screen)) {
throw new Error(
`Object { x: ${obj.x}, y: ${obj.y}, width: ${obj.width}, ` +
`height: ${obj.height} } isn't on screen { x: ${screen.x}, ` +
`y: ${screen.y}, width: ${screen.width}, height: ${screen.height} }`);
}
var res = _.clone(obj);
if (res.x < screen.x) res.x = screen.x;
if (res.y < screen.y) res.y = screen.y;
if (res.x + res.width > screen.x + screen.width) {
res.width = screen.x + screen.width - res.x;
}
if (res.y + res.height > screen.y + screen.height) {
res.height = screen.y + screen.height - res.y;
}
return res;
};
/**
* Transforms string to kebab case. Replace all symbols, except numbers,
* chars and dots with dashes.
*
* @function
* @arg {string} str - String to transform.
* @return {string} Transformed string.
*/
module.exports.toKebab = str => {
return str
.trim()
.toLowerCase()
.replace(/[^A-Za-z0-9_.]+/g, "-")
.replace(/-\./g, ".")
.replace(/-_/g, "_")
.replace(/-$/g, "")
.replace(/^-/g, "");
};
/**
* Waits for predicate returns truly value.
*
* @async
* @function
* @arg {function} predicate - Function which should return truly value during
* timeout.
* @arg {object} [opts] - Options.
* @arg {number} [opts.timeout=1] - Time to wait for predicate result, sec.
* @arg {number} [opts.polling=0.1] - Time to poll predicate result, sec.
* @return {Promise<boolean>} `false` if predicate didn't return truly value
* during expected time.
* @return {Promise<object>} Predicate truly value.
*/
module.exports.waitFor = async (predicate, opts) => {
opts = I.coalesce(opts, {});
var timeout = I.coalesce(opts.timeout, 1) * 1000;
var polling = I.coalesce(opts.polling, 0.1) * 1000;
var limit = new Date().getTime() + timeout;
while(limit > new Date().getTime()) {
var result = await predicate();
if (result) return result;
await I.sleep(polling);
}
return false;
};
/**
* Waits during a time that predicate returns truly value.
*
* @async
* @function
* @arg {function} predicate - Function which should return truly value during
* timeout.
* @arg {object} [opts] - Options.
* @arg {number} [opts.timeout=1] - Time to wait predicate result, sec.
* @arg {number} [opts.polling=0.1] - Time to poll predicate result, sec.
* @return {Promise<boolean>} `false` if predicate didn't return truly value
* during expected time.
* @return {Promise<object>} Predicate truly value.
*/
module.exports.waitDuring = async (predicate, opts) => {
opts = I.coalesce(opts, {});
var timeout = I.coalesce(opts.timeout, 1) * 1000;
var polling = I.coalesce(opts.polling, 0.1) * 1000;
var limit = new Date().getTime() + timeout;
while(limit > new Date().getTime()) {
var result = await predicate();
if (!result) return false;
await I.sleep(polling);
}
return result;
};
var complete = line => {
line = colors.strip(line);
var tokens = line.split(/[^A-Za-z0-9._$]+/).filter(i => i);
if (!tokens.length) return [[], line];
var targetToken = tokens[tokens.length - 1];
var namespace = global;
var filterPrefix = targetToken;
var targetObject;
if (targetToken.includes(".")) {
targetObject = targetToken.split(".");
filterPrefix = targetObject.pop();
targetObject = targetObject.join(".");
if (!targetObject) return [[], targetToken];
try {
namespace = eval(targetObject);
} catch (e) {
return [[], targetToken];
}
}
try {
var completions = [];
for (var key in namespace) {
completions.push(key);
}
completions = _.union(
completions,
Object.getOwnPropertyNames(namespace),
Object.getOwnPropertyNames(Object.getPrototypeOf(namespace))
).sort()
.filter(i => i.startsWith(filterPrefix))
.filter(i => /^(\w|\$)+$/.test(i))
.filter(i => /^\D/.test(i));
} catch (e) {
return [[], targetToken];
}
if (targetObject) {
completions = completions.map(i => targetObject + "." + i);
}
return [completions, targetToken];
};
/**
* Interactive debugger with syntax highlighting and autocomplete.
*
* <img src="./debug_example.gif" title="Debug example" />
*
* @async
* @function
* @arg {string} [helpMessage] - Help message.
* @return {Promise}
*/
module.exports.debug = async function (helpMessage) {
const defaultHelp = "In interactive mode you can execute any nodejs code.\n" +
"Also next commands are available:\n";
helpMessage = helpMessage || defaultHelp;
helpMessage += "- h, help - show interactive mode help;\n" +
"- go - continue code execution;\n" +
"- exit - finish current nodejs process;";
console.log("interactive mode".yellow);
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: complete,
});
var ttyWrite = rl._ttyWrite;
rl._ttyWrite = function (s, key) {
if (this.cursor <= this.line.length) {
this.line = colors.strip(this.line);
if (this.cursor > this.line.length) {
this._moveCursor(+Infinity);
}
}
ttyWrite.call(this, s, key);
if (this.cursor < this.line.length) {
this.line = colors.strip(this.line);
if (this.cursor > this.line.length) {
this._moveCursor(+Infinity);
}
} else {
this.line = highlight(colors.strip(this.line), { language: "js" });
this._moveCursor(+Infinity);
}
if (key.name !== "return") {
this._refreshLine();
}
};
var origGlobals = {};
var isFinished = false;
while (!isFinished) {
isFinished = await new Promise(resolve => {
rl.question("> ".red, answer => {
answer = colors.strip(answer);
if (answer === "exit") {
console.log("emergency exit".red);
process.exit(1);
}
if (answer === "go") {
console.log("continue execution".green);
resolve(true);
return;
}
if (["help", "h"].includes(answer)) {
console.log(helpMessage);
resolve(false);
return;
}
var ast, varName;
try {
ast = espree.parse(answer, { ecmaVersion: 9 });
varName = ast.body[0].expression.left.name;
} catch (e) {
try {
varName = ast.body[0].declarations[0].id.name;
} catch (e) { /* nothing */ }
}
Promise
.resolve()
.then(() => {
var result = eval(answer);
if (varName) {
if (!Object.prototype.hasOwnProperty.call(origGlobals, varName)) {
origGlobals[varName] = global[varName];
}
global[varName] = eval(varName);
}
return result;
})
.then(result => console.log(util.format(result).yellow))
.catch(e => console.log(util.format(e).red))
.then(() => resolve(false));
});
});
}
for (var [k, v] of Object.entries(origGlobals)) {
global[k] = v;
}
};
/**
* Activates docstring support for js functions.
*
* @function
*/
module.exports.docString = () => {
if (Object.prototype.hasOwnProperty.call(Function.prototype, "__doc__")) return;
require("docstring");
Function.prototype.bond = function (ctx) {
var result = this.bind(ctx);
Object.defineProperty(result, "__doc__", {
value: this.__doc__,
writable: false,
});
return result;
};
};
/**
* `Glace` fixtures factory.
*
* Provides easy way to make a fixture with hooks related with shared context.
*
* @function
* @arg {object} [opts] - Options.
* @arg {function} [opts.before] - Callback of `before` hook.
* @arg {function} [opts.after] - Callback of `after` hook.
* @arg {function} [opts.beforeChunk] - Callback of `beforeChunk` hook.
* @arg {function} [opts.afterChunk] - Callback of `afterChunk` hook.
* @return {function} - Fixture.
*/
module.exports.makeFixture = (opts = {}) => {
return func => {
const ctx = {};
if (opts.before) before(opts.before(ctx));
if (opts.beforeChunk) beforeChunk(opts.beforeChunk(ctx));
func();
if (opts.afterChunk) afterChunk(opts.afterChunk(ctx));
if (opts.after) after(opts.after(ctx));
};
};
Object.assign(exports, require("./lib/small"));
module.exports.download = require("./lib/download");
module.exports.Pool = require("./lib/pool");
const I = module.exports;