topkat-utils
Version:
A comprehensive collection of TypeScript/JavaScript utility functions for common programming tasks. Includes validation, object manipulation, date handling, string formatting, and more. Zero dependencies, fully typed, and optimized for performance.
363 lines • 16.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.dim = exports.cliLoadingSpinner = exports.cliProgressBar = exports.C = exports.logger = void 0;
/* eslint-disable no-console */
/* eslint-disable no-control-regex */
//----------------------------------------
// LOGGER
//----------------------------------------
const config_1 = require("./config");
const isset_1 = require("./isset");
const remove_circular_json_stringify_1 = require("./remove-circular-json-stringify");
const clean_stack_trace_1 = require("./clean-stack-trace");
exports.logger = {
/** log is an OUTDATED alisas of info, appError is for warning */
log(str, level = 'info') {
const { preprocessLog } = (0, config_1.configFn)();
if (typeof preprocessLog === 'function')
str = preprocessLog(str) || str;
if ((0, isset_1.isset)(console[level]))
console[level](str);
else
console.log(str);
exports.logger.raw.push(str + `\n`);
exports.logger.raw = exports.logger.raw.slice(exports.logger.raw.length - (0, config_1.configFn)()?.nbOfLogsToKeep, exports.logger?.raw?.length);
},
/**
* @param {String[]|String} inputLogs
*/
toHtml(inputLogs = [...exports.logger.raw]) {
if (!Array.isArray(inputLogs))
inputLogs = [inputLogs];
const code2css = {
2: `opacity:.5`,
32: `color:#679933`,
31: `color:#A8383B`,
33: `color:#D7941C`,
35: `color:#C21949`,
36: `color:#128C6D`,
34: `color:#1B568B`, // blue
};
let htmlLogs = '';
inputLogs.join('<br/>').split('\x1b[').forEach((bit, i) => {
// the first doesn't have a preceding (may be undef)
if (bit) {
if (i !== 0) {
const [, code, R, G, B, content] = bit.match(/(^\d\d?)(?:;|m)(?:\d;(\d+);(\d+);(\d+)m)?(.*$)/) || [];
if (content) {
const style = !(0, isset_1.isset)(R) ? (0, isset_1.isset)(code) ? code2css[code] : undefined : `color:rgb(${R},${G},${B})`;
htmlLogs += (0, isset_1.isset)(style) ? `<span style='${style}'>${content.replace(/\n/g, '<br>')}</span>` : content.replace(/\n/g, '<br>');
}
}
else
htmlLogs += bit.replace(/\n/g, '<br>');
}
});
return `<div style='color:#ccc'>${htmlLogs}<br></div>`;
},
toText(inputLogs = [...exports.logger.raw]) {
const str = Array.isArray(inputLogs) ? inputLogs.join('\n') : inputLogs;
return str.replace(/\x1b\[.*?m/g, '');
},
raw: [],
json: [],
};
/**
// console colored output
// * console.log(C.green(C.dim('Hey bro !')))
// * or C.log() // will use color defined by themes
// * or C.line('MY TITLE', 53)
// * or C.gradientize(myLongString)
*/
exports.C = {
dim: str => exports.C.output(2, str),
green: str => exports.C.output(32, str),
red: str => exports.C.output(31, str),
yellow: str => exports.C.output(33, str),
grey: str => exports.C.output(2, str),
magenta: str => exports.C.output(35, str),
cyan: str => exports.C.output(36, str),
blue: str => exports.C.output(34, str),
primary: str => {
const primary = (0, config_1.configFn)()?.terminal?.theme?.primary;
return exports.C.rgb(...primary) + str + exports.C.reset;
},
reset: '\x1b[0m',
output: (code, str = '') => (0, config_1.configFn)()?.terminal?.noColor ? str : `\x1b[${code}m${str}\x1b[0m`,
// true RGB colors B-*
rgb: (r, g = 0, b = 0) => (0, config_1.configFn)()?.terminal?.noColor || !(0, isset_1.isset)(r, g, b) ? '' : `\x1b[38;2;${r};${g};${b}m`,
bg: (r, g, b) => (0, config_1.configFn)()?.terminal?.noColor || !(0, isset_1.isset)(r, g, b) ? '' : `${'\x1b['}48;2;${r};${g};${b}m`,
/** Output a line of title */
line(title = '', length = (0, config_1.configFn)()?.terminal?.theme?.pageWidth, clr = (0, config_1.configFn)()?.terminal?.theme?.primary, char = '=') {
this.log('\u00A0\n' + this.rgb(...clr) + (title + ' ').padEnd(length || 0, char) + this.reset + '\u00A0\n');
},
/** Eg: ['cell1', 'cell2', 'cell3'], [25, 15] will start cell2 at 25 and cell 3 at 25 + 15
* @param {Array} limits default divide the viewport
*/
cols(strings, limits = [], clr = (0, config_1.configFn)()?.terminal?.theme?.fontColor) {
if (!limits.length) {
const colWidth = Math.round((0, config_1.configFn)()?.terminal?.theme.pageWidth / strings.length);
limits = Array(strings.length - 1).fill(2).map((_, i) => colWidth * i + 1);
}
const str = strings.reduce((glob, str = '', i) => {
const realCharLength = str.toString().replace(/\x1b\[.*?m/, '').length;
const charLength = str.toString().length;
const realLimit = limits[i] + charLength - realCharLength;
return glob + str.toString().substring(0, realLimit || 999).padEnd(realLimit || 0, ' ');
}, '');
this.logClr(str, clr);
},
/** Console log alias */
log(...stringsCtxMayBeFirstParam) {
stringsCtxMayBeFirstParam.forEach(str => this.logClr(str));
},
logClr(str, clr = (0, config_1.configFn)()?.terminal?.theme?.fontColor) {
if (!(0, isset_1.isset)(str))
return;
str = (typeof clr !== 'undefined' ? this.rgb(...clr) : '') + str.toString().replace(/\n/g, '\n' + (typeof clr !== 'undefined' ? this.rgb(...clr) : ''));
exports.logger.log(str + this.reset, 'info');
},
info(...str) {
str.forEach((s, i) => {
if (i === 0)
this.logClr('ⓘ ' + s, (0, config_1.configFn)()?.terminal?.theme?.primary);
else
this.log(this.dimStrSplit(s));
});
this.log(' ');
},
success(...str) {
str.forEach((s, i) => {
if (i === 0)
this.log(this.green('✓ ' + s));
else
this.log(this.dimStrSplit(s));
});
},
/** First param **false** to avoid logging stack trace */
error: (...errors) => logErrPrivate('error', [255, 0, 0], ...errors),
/** First param **false** to avoid logging stack trace */
warning: (...str) => logErrPrivate('warn', [255, 122, 0], ...str),
customError: (color, ...str) => logErrPrivate('error', color, ...str),
customWarning: (color, ...str) => logErrPrivate('warn', color, ...str),
applicationError: (color, ...str) => logErrPrivate('warn', color, ...str),
warningLight: (_, ...str) => logErrPrivate('warn', [196, 120, 52], ...str),
dimStrSplit(...logs) {
const logsStr = [];
logs.filter(isset_1.isset).forEach(log => log.toString().split('\n').forEach(line => line && logsStr.push(this.dim(` ${line}`))));
return logsStr.join('\n');
},
notifShow() {
console.log('\n\u00A0');
this.notifications.forEach(fn => fn());
this.notifications = [];
console.log('\n\u00A0');
},
/** Keep in memory the logs to show when needed with C.notifShow()
* Ex: C.notification('info', str); */
notification(type, ...messages) {
this.notifications.push(() => {
if ((0, isset_1.isset)(this[type])) {
this[type](...messages);
}
else {
this.warning('Wrong param for C.notification');
}
});
},
notifications: [],
/** Gratientize lines of text (separated by \n) */
gradientize(str = '', rgb1 = (0, config_1.configFn)()?.terminal?.theme?.shade1, rgb2 = (0, config_1.configFn)()?.terminal?.theme?.shade2, bgRgb = (0, config_1.configFn)()?.terminal?.theme?.bgColor, paddingY = 2) {
const lines = str.split('\n');
const largestLine = lines.reduce((sum, line) => sum < line.length ? line.length : sum, 0);
const rgbParts = rgb1.map((val, i) => (val - rgb2[i]) / (lines.length));
const bg = bgRgb ? this.bg(bgRgb[0], bgRgb[1], bgRgb[2]) : '';
const padLine = bg + ' '.padEnd(largestLine, ' ') + '\x1b[0m\n';
console.log(padLine.repeat(paddingY) +
lines.reduce((s, line, i) => {
return s + bg + this.rgb(...(rgb1.map((val, i2) => Math.round(val - i * rgbParts[i2])))) + line.padEnd(largestLine, ' ') + '\x1b[0m\n';
}, '') +
padLine.repeat(paddingY));
},
debugModeLog(title, ...string) {
this.logClr('🐞 ' + title, (0, config_1.configFn)()?.terminal?.theme?.debugModeColor);
this.log(this.dimStrSplit(...string));
},
/** allow to clear the last lines written to console */
clearLastLines(n = 2) {
for (let i = 0; i < n; i++) {
process?.stdout?.moveCursor?.(0, -1); // Move up one line
process?.stdout?.clearLine?.(0);
}
process?.stdout?.cursorTo?.(0); // Move cursor to beginning of the current line
}
};
function logErrPrivate(level, color, ...errors) {
const { isProd } = (0, config_1.configFn)();
if (errors.length === 1 && typeof errors[0]?.log === 'function')
return errors[0].log();
let stackTrace = (new Error('')).stack || '';
const displayStack = errors[0] === false ? errors.shift() : true;
const symbol = level === 'error' ? '✘ ' : '⚠ ';
if (errors.length > 1 && !(0, isset_1.isset)(errors[0]))
errors.shift(); // remove first empty object
const getStringFromErr = (err, i) => {
if (!(0, isset_1.isset)(err))
return '';
else if (typeof err === 'string') {
if (i === 0)
return exports.C.rgb(...color) + symbol + err + exports.C.reset;
else
return err.split('\n').map(val => exports.C.dim(val)).join('\n');
}
else if (err instanceof Error) {
const { str, stackTrace: stkTrc } = stringifyInstanceOfError(err, level, color);
if (stkTrc)
stackTrace = stkTrc;
return str;
}
else if (typeof err === 'object') {
let msg = '';
msg += (0, remove_circular_json_stringify_1.removeCircularJSONstringify)(err, 2).split('\n').map(val => exports.C.dim(val)).join('\n') + '\n';
const { str, stackTrace: stkTrc } = stringifyExtraInfos(err.extraInfo || err, level, color);
if (stkTrc)
stackTrace = stkTrc;
msg += str;
return msg;
}
else
return '';
};
if (errors.length && errors[0]) {
const messages = errors.map((e, i) => {
if (typeof e?.log === 'function') {
e.log();
return '';
}
else
return getStringFromErr(e, i);
});
// Stack
if (displayStack) {
messages.push(isProd ? stackTrace : (0, clean_stack_trace_1.cleanStackTrace)(stackTrace) + '\n');
}
exports.logger.log(messages.join(''), level);
}
}
function stringifyInstanceOfError(err, type = 'error', color = [255, 0, 0], level = 0) {
if (level > 5)
return { str: '' };
let str = '';
let stackTrace;
const symbol = type === 'error' ? '✘ ' : '⚠ ';
const title = err.msg || err.message || err.id || (err.stack ? err.stack.split('\n')[0] : 'Error');
// Err mess
str += exports.C.rgb(...color) + symbol + title + exports.C.reset + '\n';
if (err.stack)
stackTrace = err.stack; // more relevant
// ExtraInfos
if ((0, isset_1.isset)(err.extraInfo)) {
const { str: str2, stackTrace: stkTrc } = stringifyExtraInfos(err.extraInfo, type, color, level++);
if (stkTrc)
stackTrace = stkTrc;
str += str2;
}
return { str, stackTrace };
}
function stringifyExtraInfos(extraInfoOriginal, type, color, level = 0) {
let stackTrace;
const originalError = [exports.C.dim(`ORIGINAL ERROR ${'-'.repeat(39)}\n`)];
if (extraInfoOriginal instanceof Error) { // case where error is passed directly to extraInfos
return stringifyInstanceOfError(extraInfoOriginal, type, color);
}
else {
const extraInfo = { ...extraInfoOriginal };
const extraInfos = [exports.C.dim(`EXTRA INFOS ${'-'.repeat(41)}\n`)];
if (typeof extraInfo === 'object' && Object.keys(extraInfo).length) {
for (const itemName in extraInfo) {
if (extraInfo[itemName] instanceof Error) {
const { str, stackTrace: stkTrc } = stringifyInstanceOfError(extraInfo[itemName], type, color, level++);
originalError.push(str);
stackTrace = stkTrc;
delete extraInfo[itemName];
}
}
}
if (Object.keys(extraInfo).length) {
extraInfos.push((0, remove_circular_json_stringify_1.removeCircularJSONstringify)(extraInfo, 2)
.replace(/(?:^\s*{(?:\n {2})?|}\s*$)/g, '')
.replace(/\n {2}/g, '\n')
.split('\n')
.map(val => exports.C.dim(val)).join('\n') + '\n');
}
return {
str: (extraInfos.length > 1 ? extraInfos.join('') : '') + (originalError.length > 1 ? originalError.join('') + '\n' : ''),
stackTrace
};
}
}
/**
* Call this at each steps of your progress and change the step value
* @param {Number} step Number of "char" to output
* @param {String} char Default: '.'
* @param {String} msg String before char. Final output will be `${str}${char.repeat(step)}`
*/
function cliProgressBar(step, char = '.', msg = `\x1b[2mⓘ Waiting response`) {
if (typeof process?.stdout?.clearLine === 'function') {
process.stdout.cursorTo(0);
process.stdout.write(`${msg}${char.repeat(step)}\x1b[0m`); // \x1b[0m == reset color
}
}
exports.cliProgressBar = cliProgressBar;
/** This allow an intuitive inline loading spinner with a check mark when loading as finished or a red cross for errors */
class cliLoadingSpinner {
/** Please use it like spinner.start('myStuff') then spinner.end()
* @param {String} type in: ['arrow', 'dots']
*/
frameRate;
animFrames;
activeProcess;
frameNb = 0;
progressMessage = '';
interval = -1;
constructor(type = 'dots', activeProcess = process) {
const anims = {
arrow: {
interval: 120,
frames: ['▹▹▹▹▹', '▸▹▹▹▹', '▹▸▹▹▹', '▹▹▸▹▹', '▹▹▹▸▹', '▹▹▹▹▸']
},
dots: {
interval: 80,
frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
}
};
this.frameRate = anims[type].interval;
this.animFrames = anims[type].frames;
this.activeProcess = activeProcess;
}
start(msg) {
this.frameNb = 0;
this.progressMessage = msg;
this.interval = setInterval(() => {
this.activeProcess?.stdout?.cursorTo?.(0); // Move cursor to beginning of the current line
const symbol = this.animFrames[this.frameNb++ % this.animFrames.length];
this.activeProcess.stdout.write(exports.C.primary(symbol) + ' ' + this.progressMessage);
}, this.frameRate);
}
end(error = false) {
clearInterval(this.interval);
this.activeProcess?.stdout?.cursorTo?.(0);
exports.C.log(error ? exports.C.red('✘ ' + this.progressMessage + '\n\n')
: '\x1b[32m✓ ' + this.progressMessage + '\n\n');
this.progressMessage = '';
}
error() {
return this.end(true);
}
}
exports.cliLoadingSpinner = cliLoadingSpinner;
function dim(str = '') {
return (0, config_1.configFn)()?.terminal?.noColor ? str : `\x1b[2m${str.toString().split('\n').join('\x1b[0m\n\x1b[2m')}\x1b[0m`;
}
exports.dim = dim;
//# sourceMappingURL=logger-utils.js.map