UNPKG

fock-logger

Version:

Simple logger for your pet-project

453 lines (452 loc) 21.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Logger = exports.DEFAULT_EXECUTE_DATA = exports.loggersNames = void 0; const configurator_1 = __importDefault(require("../config/configurator")); const { config } = new configurator_1.default(); const color_1 = require("../utils/color"); const loggers_names_1 = __importDefault(require("../data/loggers.names")); const file_logger_1 = __importDefault(require("./file.logger")); exports.loggersNames = new loggers_names_1.default(config.logging); var Keys; (function (Keys) { Keys["ctrl_backspace"] = "\u0017"; Keys["ctrl_c"] = "\u0003"; Keys["backspace"] = ""; Keys["backspaceTwo"] = "\b"; Keys["enter"] = "\r"; })(Keys || (Keys = {})); const CLEAR_KEYS = [ Keys.backspace, Keys.backspaceTwo ]; exports.DEFAULT_EXECUTE_DATA = { level: "info", end: "\n", join: " ", sign: true, hideSymbol: "*", hideInput: false, overwriteListeners: false, }; class Logger { name; out; input; _file_logger; _config; _execute_data; constructor(name, data = { ...exports.DEFAULT_EXECUTE_DATA, }, out = process.stdout, input = process.stdin) { this.name = name; this.out = out; this.input = input; this._config = { ...config, ...exports.loggersNames.GetNames()[name], ...data, name, }; this._execute_data = { ...exports.DEFAULT_EXECUTE_DATA, color: this._config.colors[1], write: this._config.logging, level: (data.level || this._config.defaultLevel), }; this._file_logger = new file_logger_1.default(this._config.dir, this._config, config.logging); exports.loggersNames.SetNames({ [this._config.name]: { name: this._config.name, colors: this._config.colors, }, }); } /** * Главный метод, который используется для вывода информации в терминал * и последующей её записи в файл * @param text Ваш текст или другая информация, которая будет переведена в `JSON` формат (если Вы закинули массив) * или отобразится как `Error` если элемент будет соответствовать `Error` * * @param data это конфигурация Вашего логгера, которые Вы можете менять, смотря, что Вы хотите * * ## Значения * * - `color` — Цвет текста после имени логгера (`this.colors[1]` по умолчанию) * - `level` — Уровень логирования (определяет, нужно ли выводить информацию или просто её записать в `.log` файл) (`this._config.defaultLevel` по умолчанию) * - `sign` — Булево значение, говорит, нужно ли подписывать данный лог или нет (`true` по умолчанию) * - `write` — Булево значение, говорит, нужно ли записывать данный лог в файл или нет (`true` по умолчанию) * - `end` — Конец строки для логгера (`\n` по умолчанию) * - `join` — Соединение для значения массиов (`" "` по умолчанию) * * @returns значения `colored` и `base`, где первый — цветное отображение Ваших данных, а `base` — обычное, в Вашем виде данных */ execute(text, data = {}) { return this.log(text, data, "execute"); } /** * Выводит ошибки в терминал и обрабатывает их, также запись в `.log`-файл * * @param text Ваши ошибки * * @param data это конфигурация Вашего логгера, которые Вы можете менять, смотря, что Вы хотите * * ## Значения * * - `color` — Цвет текста после имени логгера (`this.colors[1]` по умолчанию) * - `level` — Уровень логирования (определяет, нужно ли выводить информацию или просто её записать в `.log` файл) (`this._config.defaultLevel` по умолчанию) * - `sign` — Булево значение, говорит, нужно ли подписывать данный лог или нет (`true` по умолчанию) * - `write` — Булево значение, говорит, нужно ли записывать данный лог в файл или нет (`true` по умолчанию) * - `end` — Конец строки для логгера (`\n` по умолчанию) * - `join` — Соединение для значения массиов (`" "` по умолчанию) * * @returns значения `colored` и `base`, где первый — цветное отображение Ваших данных, а `base` — обычное, в Вашем виде данных */ error(text, data = {}) { return this.log(text, data, "error"); } cleanupInput(listeners) { if (listeners) { listeners.forEach((listener) => this.input.removeAllListeners(listener)); } else { this.input.removeAllListeners(); } this.input.setRawMode(false); this.input.pause(); } /** * Читает текст, который введёт пользователь в терминал. Читает все символы последовательно * * @param text То, что Вы хотите вывести перед тем, как пользователь начнёт ввод * * @param data это конфигурация Вашего логгера, которые Вы можете менять, смотря, что Вы хотите * * ## Значения * * - `color` — Цвет текста после имени логгера (`this.colors[1]` по умолчанию) * - `level` — Уровень логирования (определяет, нужно ли выводить информацию или просто её записать в `.log` файл) (`this._config.defaultLevel` по умолчанию) * - `sign` — Булево значение, говорит, нужно ли подписывать данный лог или нет (`true` по умолчанию) * - `write` — Булево значение, говорит, нужно ли записывать данный лог в файл или нет (`true` по умолчанию) * - `end` — Конец строки для логгера (`\n` по умолчанию) * - `join` — Соединение для значения массиов (`" "` по умолчанию) * - `listeners` — Ваши прослушиватели, если хотите больше гибкости (`{}`) * - `overwriteListeners` — Булево значение, говорит, нужно ли перезаписывать дефолтные прослушиватели логгера (`false`) * - `hideInput` — Булево значение, говорит, нужно ли прятать текст в терминале (ввод превратится в `****`) (`false`) * * @returns текст, что ввёл пользователь */ readRaw(text, data = {}) { const configuration = this.resolveData(data, this._execute_data); return new Promise((resolve, reject) => { this.input.setRawMode(true); this.input.resume(); this.input.setEncoding("utf8"); const listeners = (configuration?.listeners || (() => undefined))(this.input); listeners?.onStart?.(); this.execute(text, configuration); let globalData = ""; const onData = (buffer) => { listeners?.onData?.(buffer); const key = buffer.toString("utf8"); if (key === Keys.ctrl_c) { cleanup(); return reject(new Error("User interrupted with Ctrl+C")); } if (key === Keys.ctrl_backspace) { this.clearChars(globalData.length, this.out); return (globalData = ""); } if (CLEAR_KEYS.includes(key)) { if (globalData.length === 0) { return; } this.clearChars(1, this.out); return (globalData = globalData.slice(0, -1)); } if (Keys.enter === key) { cleanup(); this.out.write("\n"); return resolve(globalData); } globalData += key; this.out.write(data.hideInput ? configuration.hideSymbol : key); }; const onError = (err) => { listeners?.onError?.(err); cleanup(); reject(err); }; const onEnd = () => { cleanup(); reject(new Error("Stream ended without data")); }; const cleanup = () => { this.input.removeListener("data", onData); this.input.removeListener("error", onError); this.input.removeListener("end", onEnd); this.input.setRawMode(false); this.input.pause(); if (listeners?.onData) { this.input.removeListener("data", listeners.onData); } if (listeners?.onError) { this.input.removeListener("error", listeners.onError); } if (listeners?.onEnd) { this.input.removeListener("end", listeners.onEnd); } }; if (data.overwriteListeners) { this.input.on("data", listeners?.onData || onData); this.input.on("error", listeners?.onError || onError); this.input.on("end", listeners?.onEnd || onEnd); } else { this.input.on("data", onData); this.input.on("error", onError); this.input.on("end", onEnd); } }); } /** * Читает текст, который введёт пользователь в терминал. Читает всё целиком * * @param text То, что Вы хотите вывести перед тем, как пользователь начнёт ввод * * @param data это конфигурация Вашего логгера, которые Вы можете менять, смотря, что Вы хотите * * ## Значения * * - `color` — Цвет текста после имени логгера (`this.colors[1]` по умолчанию) * - `level` — Уровень логирования (определяет, нужно ли выводить информацию или просто её записать в `.log` файл) (`this._config.defaultLevel` по умолчанию) * - `sign` — Булево значение, говорит, нужно ли подписывать данный лог или нет (`true` по умолчанию) * - `write` — Булево значение, говорит, нужно ли записывать данный лог в файл или нет (`true` по умолчанию) * - `end` — Конец строки для логгера (`\n` по умолчанию) * - `join` — Соединение для значения массиов (`" "` по умолчанию) * - `listeners` — Ваши прослушиватели, если хотите больше гибкости (`{}`) * * @returns текст, что ввёл пользователь */ read(text, data = {}) { const configuration = this.resolveData(data, this._execute_data); return new Promise((resolve, reject) => { this.input.resume(); this.input.setEncoding("utf8"); const listeners = (configuration?.listeners || (() => undefined))(this.input); listeners?.onStart?.(); const cleanup = () => { this.input.removeListener("readable", onReadable); this.input.removeListener("error", onError); this.input.removeListener("end", onEnd); if (listeners?.onData) { this.input.removeListener("data", listeners.onData); } this.input.pause(); }; this.execute(text, configuration); const onReadable = () => { try { listeners?.onReadable?.(); const userInput = this.input.read(); if (!userInput) { return reject(new Error("No user input resolved")); } cleanup(); const input = userInput.replace(/\r?\n$/, ""); this._file_logger.execute("User: " + input); return resolve(input); } catch (error) { cleanup(); return reject(error); } }; const onError = (err) => { listeners?.onError?.(err); cleanup(); return reject(err); }; const onEnd = () => { listeners?.onEnd?.(); cleanup(); return reject(new Error("Stream ended without data")); }; this.input.on("readable", onReadable); this.input.on("error", onError); this.input.on("end", onEnd); if (listeners?.onData) { this.input.on("data", listeners.onData); } }); } /** * Изменяет последнюю линию в терминале * * @param text То, что Вы хотите вывести перед тем, как пользователь начнёт ввод * @param data это конфигурация Вашего логгера, которые Вы можете менять, смотря, что Вы хотите * * ## Значения * * - `color` — Цвет текста после имени логгера (`this.colors[1]` по умолчанию) * - `level` — Уровень логирования (определяет, нужно ли выводить информацию или просто её записать в `.log` файл) (`this._config.defaultLevel` по умолчанию) * - `sign` — Булево значение, говорит, нужно ли подписывать данный лог или нет (`true` по умолчанию) * - `write` — Булево значение, говорит, нужно ли записывать данный лог в файл или нет (`true` по умолчанию) * - `end` — Конец строки для логгера (`\n` по умолчанию) * - `join` — Соединение для значения массиов (`" "` по умолчанию) * - `ignoreLineBreakerError` — Булево значение, говорит, игнорировать ли ошибки о `\n` (`false` по умолчанию) * * @returns тот же метод для замены предыдущего текста * * @example * ```ts * const logger = new Logger(); * * const datas = [ * "|", * "/", * "—", * "\\", * ]; * * let i = 1; * * const changeLine = logger.changeLine(datas[i-1], { end: "" }); * * setInterval(() => { * if (i>datas.length-1) i = 0; * * changeLine(datas[i++]); * }, 200); * ``` */ changeLine(text, data = {}) { const configuration = this.resolveData(data, this._execute_data); const errorCaptched = !configuration.ignoreLineBreakerError && configuration.end.includes("\n"); if (errorCaptched) { throw new Error("Can not resolve line breaker"); } this.execute(text, configuration); return (t, d = {}) => { this.out.cursorTo(-1); this.out.clearLine(0); return this.changeLine(t, { ...data, ...d }); }; } executeLogFile(text, colored) { if (typeof text === "string") { this._file_logger.execute(`${this.name}: ${text}`); } else { this._file_logger.execute(`${this.name}:`); for (const msg of colored) { this._file_logger.execute(msg[1]); } } } errorLogFile(text) { this._file_logger.error(text); } log(text, data, type) { const configuration = this.resolveData(data, this._execute_data); const { colored, base } = this.resolveText(Array.isArray(text) ? text : [text], configuration.color); const name = (0, color_1.color)(this.name, this._config.colors[0]) + ":"; const date = `[${new Date().toISOString()}]`; const { prefix, join, suffix } = this.resolveAffix({ ...configuration, date, name, }); const configLevel = this._config.levels[config.level]; const messageLevel = this._config.levels[configuration.level]; if (configLevel === undefined || messageLevel === undefined) { throw new Error(`Invalid log level. Config level: ${config.level}, Message level: ${configuration.level}`); } const isLevelEqualsOrLess = configLevel <= messageLevel; if (isLevelEqualsOrLess) { // Используем уже окрашенный текст без дополнительного окрашивания this.out.write(prefix + colored.join(join) + suffix); } const logEnabled = (config.logging && this._file_logger) || configuration.write; if (logEnabled) { this.logFileService({ type, text, colored }); } return { colored, base, }; } clearChars(length, stdout) { const clear = new Array(length).fill("\b").join(""); const space = new Array(length).fill(" ").join(""); stdout.write(clear + space + clear); } logFileService({ type, text, colored, }) { const suffix = "LogFile"; const prefix = type; const name = `${prefix}${suffix}`; this[name](text, colored); } get write() { return this._file_logger.execute; } get colors() { return this._config.colors; } resolveData(data, defaultData) { return { ...defaultData, ...data }; } resolveText(text, color) { const out = typeof text === "string" ? [text] : text; const finalColor = color || this._config.colors[1]; const seen = new WeakSet(); const output = out.map((text) => { const processedText = typeof text !== "string" ? text instanceof Error ? text.stack || text.message : JSON.stringify(text, (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular]'; } seen.add(value); } return value; }, 4) : text; // Добавляем цвет только если он определен return finalColor ? (0, color_1.color)(processedText, finalColor) : processedText; }); return { colored: output, base: out, }; } resolveAffix({ date, name, end, join, sign, }) { const prefix = this.resolveLogPrefix({ date, dateEnabled: this._config.date, sign: name, signEnabled: sign !== false, }); const resolvedJoin = this.resolveExecuteData("join", join); const suffix = this.resolveExecuteData("end", end); return { prefix, suffix, join: resolvedJoin, }; } resolveExecuteData(key, data) { return data !== undefined ? data : exports.DEFAULT_EXECUTE_DATA[key]; } resolveLogPrefix({ date, dateEnabled, sign, signEnabled, }) { const datePrefix = dateEnabled ? date + " " : ""; const signPrefix = signEnabled ? sign + " " : ""; return datePrefix + signPrefix; } } exports.Logger = Logger; exports.default = Logger;