@ghini/kit
Version:
js practical tools to assist efficient development
493 lines (486 loc) • 14.2 kB
JavaScript
export {
cs,
csm,
cdev,
cdebug,
cinfo,
cwarn,
clog,
cerror,
prompt,
style,
clear,
echo,
fresh,
};
/**
* error 错误处理
* log 日常输出
* warn 警告
* info 信息
* debug 调试
* dev(自定义) 开发环境输出,需要特别设置,默认不输出
*
* 普通输出console.log() (优先级100)
* 强化输出cs() (优先级50)
* 开发隐藏输出console.log.bind({info: -1})() (优先级10)
* 强化输出带opt指明() (优先级5)
*/
// 错误跟log不一样,往往不需要重复显示,但高频请求错误会导致如此,所以使用error_map限制频率
const error_map = new Map();
const MAX_ERRORS = 1000;
const TTL = 180000;
let timer;
function error_cache(args) {
// 从源头来判断是否阻断,往往error不会太长,所以不需要hash;如果有记录且未超时return,否则记录
const jerr = JSON.stringify(
args.map((arg) => (arg instanceof Error ? arg.message : arg))
);
const tmp = error_map.get(jerr);
const now = Date.now();
if (tmp) {
if (now - tmp.t < TTL) {
tmp.n++;
return 1;
}
tmp.t = now;
tmp.n++;
args.push(tmp.n);
} else {
// 容量控制,一般不太会达到
if (error_map.size >= MAX_ERRORS) {
const oldestKey = Array.from(error_map.entries()).sort(
(a, b) => a[1].t - b[1].t
)[0][0];
error_map.delete(oldestKey);
}
error_map.set(jerr, { t: now, n: 1 });
}
// 设置清理定时器,回头清理缓存
if (!timer) {
timer = setInterval(() => {
const now = Date.now();
error_map.forEach((v, k) => {
if (Date.now() - v.t > TTL) error_map.delete(k);
});
if (!error_map.size) {
clearInterval(timer);
timer = undefined;
}
}, TTL + 15000);
}
return 0;
}
const sep_file = process.platform == "win32" ? "file:///" : "file://"; //win32|linux|darwin
console.sm = csm; //对长内容能简短输出 smart simple small
console.dev = cdev.bind({ info: -1 }); //
const originalDebug = console.debug;
const originalInfo = console.info;
const originalWarn = console.warn;
const originalLog = console.log;
const originalError = console.error;
const reset = "\x1b[0m"; // 重置所有样式
const bold = "\x1b[1m"; // 加粗
const dim = "\x1b[2m"; // 暗淡
const underline = "\x1b[4m"; // 下划线
// const blink = "\x1b[5m"; // 闪烁 (windows支持不佳)
const reverse = "\x1b[7m"; // 反显
const hidden = "\x1b[8m"; // 隐藏
const hidcursor = "\x1b[?25l"; // 隐藏光标
const showcursor = "\x1b[?25h"; // 显示光标
// 前景色(字体颜色)
const black = "\x1b[30m"; // 黑色(灰色)
const red = "\x1b[31m"; // 红色
const green = "\x1b[32m"; // 绿色
const yellow = "\x1b[33m"; // 黄色
const blue = "\x1b[34m"; // 蓝色
const magenta = "\x1b[35m"; // 洋红色(暗红)
const cyan = "\x1b[36m"; // 青色(暗绿)
const white = "\x1b[37m"; // 白色
// 明亮的前景色(字体颜色)
const brightBlack = "\x1b[90m";
const brightRed = "\x1b[91m";
const brightGreen = "\x1b[92m";
const brightYellow = "\x1b[93m";
const brightBlue = "\x1b[94m";
const brightMagenta = "\x1b[95m";
const brightCyan = "\x1b[96m";
const brightWhite = "\x1b[97m";
// 背景色
const bgBlack = "\x1b[40m"; // 黑
const bgRed = "\x1b[41m"; // 红
const bgGreen = "\x1b[42m"; // 绿
const bgYellow = "\x1b[43m"; // 黄
const bgBlue = "\x1b[44m"; // 蓝
const bgMagenta = "\x1b[45m"; // 洋红
const bgCyan = "\x1b[46m"; // 青
const bgWhite = "\x1b[47m"; // 白
// 明亮的背景色
const bgBrightBlack = "\x1b[100m";
const bgBrightRed = "\x1b[101m";
const bgBrightGreen = "\x1b[102m";
const bgBrightYellow = "\x1b[103m";
const bgBrightBlue = "\x1b[104m";
const bgBrightMagenta = "\x1b[105m";
const bgBrightCyan = "\x1b[106m";
const bgBrightWhite = "\x1b[107m";
const style = {
reset,
bold,
dim,
underline,
// blink,
reverse,
hidden,
black,
red,
green,
yellow,
blue,
magenta,
cyan,
white,
brightBlack,
brightRed,
brightGreen,
brightYellow,
brightBlue,
brightMagenta,
brightCyan,
brightWhite,
bgBlack,
bgRed,
bgGreen,
bgYellow,
bgBlue,
bgMagenta,
bgCyan,
bgWhite,
bgBrightBlack,
bgBrightRed,
bgBrightGreen,
bgBrightYellow,
bgBrightBlue,
bgBrightMagenta,
bgBrightCyan,
bgBrightWhite,
};
const csconf = {
info: 6,
line: 4, // 0错误名, 1new Error, 2preStyle, 3clog, 4首次clog调用位置
xinfo: undefined,
xline: undefined,
};
/*XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX*/
// 数字会影响后面的样式,将其转换为string; 还可以将长对象适当收缩显示摘要
function arvg_final(arvg) {
return arvg.map((item) => {
if (typeof item === "number") item += "";
return item;
});
}
function arvg_final_sm(arvg) {
return arvg.map((item) => {
if (typeof item === "number") item += "";
else if (typeof item === "object") {
return JSON.stringify(
item,
(key, value) => {
if (typeof value === "string" && value.length > 400)
return value.slice(0, 200) + ` ... [TOTAL:${value.length}]`; // 压缩显示
return value;
},
2
);
}
if (item?.length > 200)
item = item.slice(0, 100) + "... total:" + item.length;
return item;
});
}
// 简短打印
function csm(...args) {
let pre = preStyle(this, `${reset}`);
if (!pre) return;
process.stdout.write(pre);
originalLog(...arvg_final_sm(args), `${reset}`);
}
function cdev(...args) {
let pre = preStyle(this, `${cyan}[dev] ${reset}${yellow}`);
if (!pre) return;
process.stdout.write(pre);
originalLog(...arvg_final(args), `${reset}`);
}
function cdebug(...args) {
let pre = preStyle(this, `${reset}${brightYellow}`);
if (!pre) return;
process.stdout.write(pre);
originalInfo(...arvg_final(args), `${reset}`);
}
function cinfo(...args) {
let pre = preStyle(this, `${reset}${bold}${brightWhite}`);
if (!pre) return;
process.stdout.write(pre);
originalInfo(...arvg_final(args), `${reset}`);
}
function cwarn(...args) {
let pre = preStyle(this, `${reset}${bold}${brightMagenta}`);
if (!pre) return;
process.stdout.write(pre);
originalWarn(...arvg_final(args), `${reset}`);
}
function clog(...args) {
let pre = preStyle(this, `${reset}`);
if (!pre) return;
process.stdout.write(pre);
originalLog(...arvg_final(args), `${reset}`);
}
function cerror(...args) {
if (error_cache(args)) return;
const mainstyle = `${reset}${red}`;
let pre = preStyle(this, mainstyle);
if (!pre) return;
process.stdout.write(pre);
originalError(
...args.map((item) => {
if (item instanceof Error) {
const stack = item.stack.split("\n");
return (
stack[0] +
" " +
underline +
// 带//的有文件路径
(stack.slice(1).find((item) => item.match("//")) || stack[1]).split(
"at "
)[1] +
reset +
mainstyle
);
} else if (typeof item === "number") {
return item + "";
}
return item;
}),
`${reset}`
);
}
/**
* 重写或扩展控制台输出方法,支持带时间戳和调用行号的 `console.log` 和 `console.error`。
* @param {number} [rewrite=2] - 是否重写全局 `console.log` 和 `console.error` 方法,重写等级,默认2。
* @returns {{ log: Function, error: Function }} - 返回扩展的日志方法:
* - `log(...args: any[]): void` 用于日志输出。
* - `error(...args: any[]): void` 用于错误输出。
*
* @example 实用用法
* cs() cs(3) 简单使用:一般性的都能显示
* cs(66) cs(88,5) 进阶使用:两者等效,大于10后看个位,可加入第二个参数调整line
* cs({xinfo:4,xline:5}) 可读性强的使用,与上效果一致
* bind绑定的xinfo xline为最高优先级,不可改
*/
function cs(config, n) {
if (config === null || (typeof config === "number" && config < 0)) {
// 复原
console.debug = originalDebug;
console.info = originalInfo;
console.warn = originalWarn;
console.log = originalLog;
console.error = originalError;
return;
} else if (typeof config === "object") {
config.info ? (csconf.info = config.info) : 0;
config.line ? (csconf.line = config.line) : 0;
config.xinfo ? (csconf.xinfo = config.xinfo) : 0;
config.xline ? (csconf.xline = config.xline) : 0;
} else if (typeof config === "number" && config >= 0) {
csconf.info = config;
csconf.line = n;
if (config > 10) {
csconf.xinfo = config % 10;
csconf.xline = n;
}
}
console.debug = cdebug;
console.info = cinfo;
console.warn = cwarn;
console.log = clog;
console.error = cerror;
}
cs(8); //几乎必备 直接默认运行吧 cs(88)才会显示dev
async function prompt(
promptText = "ENTER continue , CTRL+C exit: ",
validator = () => true,
option
) {
option = {
...{ loop: true, show: true },
...option,
};
let inputBuffer = "";
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding("utf8");
process.stdout.write(promptText);
return new Promise((resolve) => {
process.stdin.on("data", onData);
function onData(key) {
const char = key.toString();
const code = char.codePointAt(0);
if (
(code > 31 && code < 127) || // ASCII 可打印字符
(code > 0x4e00 && code < 0x9fff) || // 常用汉字
(code > 0x3000 && code < 0x303f) // 中文标点
) {
if (option.show) process.stdout.write(char);
inputBuffer += char;
}
switch (char) {
case "\r": // 回车
case "\n":
process.stdout.write("\n");
if (validator(inputBuffer)) {
close();
resolve(inputBuffer);
} else {
if (option.loop) {
inputBuffer = "";
process.stdout.write(promptText);
} else {
close();
resolve(false);
}
}
return;
case "\b": // 退格键
case "\x7f":
if (inputBuffer.length > 0) {
if (option.show) {
const charWidth = getCharWidth(inputBuffer.at(-1));
process.stdout.write("\b".repeat(charWidth));
process.stdout.write(" ".repeat(charWidth));
process.stdout.write("\b".repeat(charWidth));
}
inputBuffer = inputBuffer.slice(0, -1);
}
return;
case "\x17": // Ctrl + 退格
if (inputBuffer.length > 0) {
process.stdout.clearLine();
process.stdout.cursorTo(0);
process.stdout.write(promptText);
inputBuffer = "";
}
return;
case "\u0003": // Ctrl + C
process.stdout.write("\x1b[30m^C\n\x1b[0m");
close();
process.exit();
}
}
function close() {
process.stdin.setRawMode(false);
process.stdin.removeListener("data", onData);
process.stdin.pause();
}
function getCharWidth(char) {
const code = char.codePointAt(0);
if (
(code > 0x3000 && code < 0x303f) || // 中文标点
(code > 0x4e00 && code < 0x9fff)
) {
return 2;
}
return 1;
}
});
}
function getTimestamp() {
const now = new Date();
return `${(now.getMonth() + 1).toString().padStart(2, "0")}-${now
.getDate()
.toString()
.padStart(2, "0")} ${now.getHours().toString().padStart(2, "0")}:${now
.getMinutes()
.toString()
.padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}.${now
.getMilliseconds()
.toString()
.padStart(3, "0")}`;
}
function getLineInfo(i = 3) {
const arr = new Error().stack.split("\n");
let res = arr[i]?.split("(").at(-1).split(sep_file).at(-1);
if (res?.endsWith(")")) res = res.slice(0, -1);
// if (!res) console.warn('exceed:', arr);
return res;
}
// b用来对其自定义log,比如dev sm
function preStyle(opt, mainstyle) {
let pre;
if (opt == console) opt = undefined;
// 基本开启了cs就按default来,除非有效设置
// originalLog("opt:", opt, "csconf:", csconf);
// 0不适用,增加运算逻辑,使用0相当于没用
const info = opt?.xinfo || csconf.xinfo || opt?.info || csconf.info; //number else default
let line = opt?.xline || csconf.xline || opt?.line || csconf.line;
if (typeof line !== "number") line = 4;
// originalLog("final csconf", info, line);
switch (info) {
case -1:
return;
case 1:
pre = `${reset}`;
break;
case 2:
pre = `${black}[${getTimestamp()}]: ` + mainstyle;
break;
case 3:
pre = `${blue}${getLineInfo(line)}: ` + mainstyle;
break;
default:
pre =
`${black}[${getTimestamp()}] ${blue}${getLineInfo(line)}: ` + mainstyle;
}
return pre;
}
/** @param {number} [n=999] 清理的行数,默认999相当于(仅限当前窗口高度)全部清理 */
function clear(n = 999) {
process.stdout.write(`\x1b[${n}A\r`);
process.stdout.write("\x1b[J");
}
function fresh() {
// 不去做宽度计算,直接输出高度的空行,然后清屏来保留之前输出
process.stdout.write("\n".repeat(process.stdout.rows));
process.stdout.write(`\x1b[999A\r`);
process.stdout.write("\x1b[J");
}
// 自清除的帧渲染输出
const echo1 = {
show: "",
frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
intervalId: undefined,
stop: () => {
clearInterval(echo1.intervalId);
echo1.intervalId = undefined;
clear();
console.log(obj.show);
process.stdout.write(showcursor);
},
};
function echo(data) {
if (!echo1.intervalId) {
process.stdout.write(hidcursor);
fresh();
}
let frameIndex = 0;
echo1.show = data; //如果是对象,会保持引用
const frames = echo1.frames;
const length = frames.length;
echo1.intervalId = setInterval(() => {
const frame = frames[frameIndex % length];
clear();
process.stdout.write(cyan + bold + frame + reset + " ");
console.log(echo1.show);
frameIndex++;
}, 100);
return echo1;
}