UNPKG

spectra-log

Version:

SpectraLog enables you to apply various colors to your log messages, enhancing readability and making your logs visually dynamic.

691 lines (548 loc) 19.7 kB
'use strict'; var colors = require('ansi-colors'); // > DIR | /config/colorManager const defineColor = (code) => { const colorFn = (text) => `\u001b[38;5;${code}m${text}\u001b[0m`; colorFn.colorCode = code; return colorFn; }; colors.red = defineColor(196); colors.green = defineColor(82); colors.blue = defineColor(33); colors.brightCyan = defineColor(116); colors.cyan = defineColor(51); colors.muteCyan = defineColor(67); colors.white = defineColor(255); colors.gray = defineColor(245); colors.dim = defineColor(240); colors.orange = defineColor(202); colors.pink = defineColor(213); colors.purple = defineColor(135); colors.violet = defineColor(129); colors.teal = defineColor(37); colors.brightYellow = defineColor(226); colors.brightGreen = defineColor(118); colors.brightRed = defineColor(196); colors.brightBlue = defineColor(75); colors.brown = defineColor(130); colors.gold = defineColor(220); colors.lime = defineColor(154); colors.silver = defineColor(250); colors.maroon = defineColor(88); const addStyleMethod = (colorFn, styleName, styleCode) => { colorFn[styleName] = (text) => { return `\u001b[38;5;${colorFn.colorCode}m${styleCode}${text}\u001b[0m`; }; }; Object.keys(colors).forEach((color) => { if (typeof colors[color] === "function" && colors[color].colorCode) { addStyleMethod(colors[color], "bold", "\u001b[1m"); addStyleMethod(colors[color], "dim", "\u001b[2m"); addStyleMethod(colors[color], "italic", "\u001b[3m"); addStyleMethod(colors[color], "underline", "\u001b[4m"); } }); // > DIR | /core/colorize.js const colorizeString = (message) => { // 입력값을 안전하게 문자열로 변환 let processedMessage; if (typeof message === 'object' && message !== null) { processedMessage = JSON.stringify(message, null, 2); } else if (message === null || message === undefined) { processedMessage = String(message); } else { processedMessage = String(message); } const regex = /\{\{\s*(?:(\w+)\s*:\s*)?(\w+)\s*:\s*([^\}]+?)\s*\}\}/g; return processedMessage.replace(regex, (match, style, color, text) => { style = style?.toLowerCase(); const colorFn = colors[color] || colors.dim; if (style && typeof colorFn[style] === "function") { return colorFn[style](text); } return colorFn(text); }); }; // > DIR | /util/stripAnsi.js // --- < stripAnsi > --- const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, ""); // > DIR | /util/sleep.js // --- < sleep 비동기 함수 > --- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // > DIR | /config/constants.js // --- < smoothPrint, interval 등 기본 설정 값 > --- let smoothPrint = false; let interval = 5; let processLevel = 2; let isProcessing = false; let displayStandby = false; const messageQueue = []; const getIsProcessing = () => isProcessing; const setIsProcessing = (value) => { isProcessing = value; }; const getSmoothPrint = () => smoothPrint; const setSmoothPrint = (value) => { smoothPrint = value; }; const getProcessLevel = () => processLevel; const setProcessLevel = (value) => { processLevel = value; }; const getPrintSpeed = () => interval; const setPrintSpeed = (value) => { interval = value; }; const getDisplayStandby = () => displayStandby; const setDisplayStandby = (value) => { displayStandby = value; }; // > DIR | /util/stripAnsi.js const fullWidthRegex = /[\u1100-\u115F\u2329\u232A\u2E80-\u303E\u3040-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE6F\uFF01-\uFF60\uFFE0-\uFFE6]|[\u{1F300}-\u{1F64F}]|[\u{1F900}-\u{1F9FF}]/u; const stringWidth = (str) => { const clean = stripAnsi(str); let width = 0; for (const char of [...clean]) { width += fullWidthRegex.test(char) ? 2 : 1; } return width; }; // > DIR | /core/printer.js const parseAnsiAndText = (str) => { const parts = []; let i = 0; let textBuffer = ""; while (i < str.length) { const ansiMatch = str.slice(i).match(/^\u001b\[[0-9;]*m/); if (ansiMatch) { // If we have text buffered, add it first if (textBuffer) { parts.push({ type: "text", content: textBuffer }); textBuffer = ""; } // Add the ANSI code parts.push({ type: "ansi", content: ansiMatch[0] }); i += ansiMatch[0].length; } else { // Add to text buffer textBuffer += str[i]; i++; } } // Add any remaining text if (textBuffer) { parts.push({ type: "text", content: textBuffer }); } return parts; }; const splitTextIntoChunks = (text, maxWidth) => { if (!text) return []; const parts = parseAnsiAndText(text); const chunks = []; let currentChunk = ""; let currentWidth = 0; let activeAnsiCodes = []; for (const part of parts) { if (part.type === "ansi") { // Always add ANSI codes directly currentChunk += part.content; // Track active codes for next chunk if (part.content === "\u001b[0m") { activeAnsiCodes = []; } else { activeAnsiCodes.push(part.content); } } else { let remaining = part.content; while (remaining) { // Calculate how many characters fit let fit = 0; let fitWidth = 0; for (let i = 0; i < remaining.length; i++) { const charWidth = stringWidth(remaining[i]); if (currentWidth + fitWidth + charWidth <= maxWidth) { fit++; fitWidth += charWidth; } else { break; } } if (fit === 0 && currentWidth > 0) { // Can't fit any more characters, start a new chunk chunks.push(currentChunk); currentChunk = activeAnsiCodes.join(""); currentWidth = 0; // Don't advance in the text } else { // Add what fits const textToAdd = remaining.slice(0, fit || 1); currentChunk += textToAdd; currentWidth += fitWidth || stringWidth(textToAdd); remaining = remaining.slice(fit || 1); // If we have remaining text, start a new chunk if (remaining && currentWidth > 0) { chunks.push(currentChunk); currentChunk = activeAnsiCodes.join(""); currentWidth = 0; } } } } } // Add the last chunk if there is one if (currentChunk) { chunks.push(currentChunk); } return chunks; }; const printLineSmooth = async (line, currentPrefix, terminalWidth) => { const prefixWidth = stripAnsi(currentPrefix).length; const availWidth = terminalWidth - prefixWidth; // Split the line into properly sized chunks const chunks = splitTextIntoChunks(line, availWidth); // If no chunks, just print the prefix if (chunks.length === 0) { process.stdout.write(`\r${currentPrefix}\n`); return; } // Print each chunk for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { const chunk = chunks[chunkIndex]; const prefix = chunkIndex === 0 ? currentPrefix : " ".repeat(36) + " | "; process.stdout.write(`\r${prefix}`); // Print the chunk character by character const parts = parseAnsiAndText(chunk); for (const part of parts) { if (part.type === "ansi") { // ANSI codes print instantly process.stdout.write(part.content); } else { // Text prints with delay for (const char of part.content) { process.stdout.write(char); await sleep(getPrintSpeed()); } } } process.stdout.write("\n"); } }; const printSmooth = async (prefix, lines) => { const terminalWidth = Math.floor(process.stdout.columns * 0.9) || 80; // 데이터 타입 검증 및 변환 let processedLines; if (Array.isArray(lines)) { processedLines = lines.map(line => typeof line === 'string' ? line : JSON.stringify(line, null, 2) ); } else if (typeof lines === 'string') { processedLines = [lines]; } else { processedLines = [JSON.stringify(lines, null, 2)]; } for (let i = 0; i < processedLines.length; i++) { const linePrefix = i === 0 ? prefix : " ".repeat(36) + " | "; await printLineSmooth(processedLines[i], linePrefix, terminalWidth); } }; // > DIR | /config/levelTypes.js const LEVEL_TYPES = { FATAL: { levelLabel: "FATAL", color: colors.red.bold }, ERROR: { levelLabel: "ERROR", color: colors.orange.bold }, INFO: { levelLabel: "INFO", color: colors.yellow.bold }, DEBUG: { levelLabel: "DEBUG", color: colors.brightCyan.bold }, TRACE: { levelLabel: "TRACE", color: colors.muteCyan.bold }, default: { levelLabel: "NOTLVL", color: colors.red.bold }, }; // > DIR | /config/httpTypes.js const HTTP_MESSAGE_TYPES = { 100: { httpLabel: "CONTINUE", color: colors.dim }, 101: { httpLabel: "SWITCHING", color: colors.dim }, 200: { httpLabel: "OK", color: colors.green }, 201: { httpLabel: "CREATED", color: colors.green }, 202: { httpLabel: "ACCEPTED", color: colors.cyan }, 204: { httpLabel: "NO-CONTENT", color: colors.gray }, 301: { httpLabel: "MOVED", color: colors.yellow }, 302: { httpLabel: "FOUND", color: colors.yellow }, 304: { httpLabel: "NOT-MODIFIED", color: colors.gray }, 400: { httpLabel: "BAD-REQUEST", color: colors.orange }, 401: { httpLabel: "UNAUTHZED", color: colors.orange }, 402: { httpLabel: "PAY-REQUEST", color: colors.orange }, 403: { httpLabel: "FORBIDDEN", color: colors.red }, 404: { httpLabel: "NOT-FOUND", color: colors.red }, 405: { httpLabel: "NO-METHOD", color: colors.orange }, 408: { httpLabel: "TIMEOUT", color: colors.orange }, 409: { httpLabel: "CONFLICT", color: colors.orange }, 410: { httpLabel: "GONE", color: colors.orange }, 429: { httpLabel: "TOO-MANY", color: colors.orange }, 500: { httpLabel: "SERVER-ERROR", color: colors.red }, 502: { httpLabel: "BAD-GATEWAY", color: colors.red }, 503: { httpLabel: "SERVER-NAVAL", color: colors.red }, 504: { httpLabel: "GW-TIMEOUT", color: colors.red }, 600: { httpLabel: "SERVER-START", color: colors.yellow }, default: { httpLabel: "UNKNOWN", color: colors.dim }, }; // > DIR | /util/time.js // --- < Time formatter > --- const getFormattedTime = (timestamp) => { const time = new Date(timestamp); return `${String(time.getHours()).padStart(2, "0")}:${String( time.getMinutes() ).padStart(2, "0")}:${String(time.getSeconds()).padStart(2, "0")}`; }; // > DIR | /core/formatter.js const getPrefix = (type, level, timestamp) => { const { levelLabel, color: levelColor } = LEVEL_TYPES[level] || LEVEL_TYPES.default; const { httpLabel, color: typeColor } = HTTP_MESSAGE_TYPES[type] || HTTP_MESSAGE_TYPES.default; const shortLabel = levelLabel.substring(0, 2); const fullLabel = shortLabel + levelLabel.substring(2); return `[ ${levelColor(fullLabel.padEnd(6))} | ${typeColor( httpLabel.padEnd(12) )} | ${getFormattedTime(timestamp)} ] | `; }; const formatMultiline = ( lines, prefix, maxWidth = Math.floor(process.stdout.columns * 0.9) || 80 ) => { const visualPrefixLength = stripAnsi(prefix).length; const linePad = " ".repeat(36) + " | "; const formatted = []; lines.forEach((line, i) => { const availWidth = maxWidth - (i === 0 ? visualPrefixLength : stripAnsi(linePad).length); // If the line is empty, just add the prefix if (!line) { formatted.push(`${i === 0 ? prefix : linePad}`); return; } const ansiLength = line.length - stripAnsi(line).length; let chunks = splitIntoVisualChunks(line, availWidth + ansiLength); // If no chunks were created (e.g., only ANSI codes), treat as a single chunk if (chunks.length === 0) { chunks = [line]; } chunks.forEach((chunk, chunkIndex) => { const linePrefix = i === 0 && chunkIndex === 0 ? prefix : linePad; formatted.push(`${linePrefix}${chunk}`); }); }); return formatted.join("\n"); }; const splitIntoVisualChunks = (text, maxWidth) => { // If text is empty, return an empty array if (!text) return []; // Extract and track all ANSI codes and their positions const ansiCodesMap = new Map(); // Map to store ANSI codes by position const visibleText = stripAnsi(text); // Text with all ANSI codes removed let originalIndex = 0; let strippedIndex = 0; let activeAnsiCodes = []; while (originalIndex < text.length) { const ansiMatch = text.slice(originalIndex).match(/^\x1B\[[0-9;]*m/); if (ansiMatch) { const ansiCode = ansiMatch[0]; // Store the ANSI code at the current visible text position if (!ansiCodesMap.has(strippedIndex)) { ansiCodesMap.set(strippedIndex, []); } ansiCodesMap.get(strippedIndex).push(ansiCode); // Track active codes for line wrapping if (ansiCode === "\u001b[0m") { activeAnsiCodes = []; } else { activeAnsiCodes.push(ansiCode); } originalIndex += ansiCode.length; } else { originalIndex++; strippedIndex++; } } // Now split the visible text based on width const chunks = []; let start = 0; while (start < visibleText.length) { let visibleWidth = 0; let end = start; // Find how many characters we can fit while (end < visibleText.length && visibleWidth < maxWidth) { const charWidth = stringWidth(visibleText[end]); if (visibleWidth + charWidth <= maxWidth) { visibleWidth += charWidth; end++; } else { break; } } // If we couldn't fit anything, force at least one character if (end === start && start < visibleText.length) { end = start + 1; } // Build the chunk with all ANSI codes in their proper positions let chunk = ""; let activeCodesForNextChunk = []; for (let i = start; i <= end; i++) { // Insert any ANSI codes that belong at this position if (ansiCodesMap.has(i)) { const codes = ansiCodesMap.get(i); for (const code of codes) { chunk += code; // Track active codes for next chunk if (code === "\u001b[0m") { activeCodesForNextChunk = []; } else { activeCodesForNextChunk.push(code); } } } // Add the character if we're not at the end boundary if (i < end && i < visibleText.length) { chunk += visibleText[i]; } } chunks.push(chunk); start = end; // Apply active ANSI codes to the beginning of the next chunk activeAnsiCodes = [...activeCodesForNextChunk]; } // If we have no chunks (e.g., pure ANSI string), make sure we add it if (chunks.length === 0 && text) { chunks.push(text); } return chunks; }; // > DIR | /util/debugLevel.js // --- < Debug level > --- const getDebugLevel = (levelLabel) => { switch (levelLabel) { case "MUTE": return { level: -1, color: "dim" }; case "FATAL": return { level: 0, color: "red" }; case "ERROR": return { level: 1, color: "orange" }; case "INFO": return { level: 2, color: "yellow" }; case "DEBUG": return { level: 3, color: "brightCyan" }; case "TRACE": return { level: 4, color: "muteCyan" }; default: return { level: 5, color: "dim" }; } }; // > DIR | /core/queueProcessor.js let isStandbyActive = false; const printMessage = async (message, type, level, timestamp) => { const prefix = getPrefix(type, level, timestamp); const str = typeof message === "object" ? JSON.stringify(message, null, 2) || "[Unserializable Object]" : String(message); const lines = str.split("\n"); if (getSmoothPrint() && lines.length > 0) { await printSmooth(prefix, lines); } else { process.stdout.write(`\r${formatMultiline(lines, prefix)}\n`); } }; const processQueue = async () => { if (getIsProcessing() || messageQueue.length === 0) return; stopStandbyLog(); setIsProcessing(true); const item = messageQueue.shift(); if (getProcessLevel() >= getDebugLevel(item.level)) ; const { message, type, level, timestamp } = item; await printMessage(message, type, level, timestamp); setIsProcessing(false); messageQueue.length === 0 ? startStandbyLog() : processQueue(); }; const startStandbyLog = () => { if (!getDisplayStandby() || isStandbyActive) return; isStandbyActive = true; (async function standbyLoop() { while (isStandbyActive) { const prefix = getPrefix(0, "INFO", Date.now()).replace( /\[.*?\]/, `[ ${colors.yellow.bold("STBY")} | - | ${getFormattedTime( Date.now() )} ]` ); process.stdout.write(`\r${prefix}`); await sleep(1000); } })(); }; const stopStandbyLog = () => { isStandbyActive = false; }; // > DIR | /index.js const log = (message, type = 200, level = "INFO", option = {}) => { const { urgent = false, force = false } = option; if (force || getProcessLevel() >= getDebugLevel(level).level) { message = colorizeString(message); if (!urgent) messageQueue.push({ message, type, level, timestamp: Date.now() }); else messageQueue.unshift({ message, type, level, timestamp: Date.now() }); processQueue(); } }; log.setDebugLevel = (level, options = {}) => { const { silent = false } = options; const temp = getDebugLevel(level); setProcessLevel(temp.level); silentHandler( silent, `{{ bold : yellow : Debug level }} has been changed to {{ bold : ${temp.color} : ${level} }}.` ); }; log.setPrintSpeed = (delay, options = {}) => { const { silent = false } = options; setPrintSpeed(delay); silentHandler( silent, `{{ bold : yellow : Smooth process level }} has been set to {{ bold : green : ${delay}ms Per Character }}.` ); }; log.setSmoothPrint = (value, options = {}) => { const { silent = false } = options; setSmoothPrint(value); silentHandler( silent, `{{ bold : yellow : Smooth print }} mode has been {{ bold : ${value ? "green : ACTIVATED" : "red : DEACTIVATED"} }}.` ); }; log.setDisplayStandby = (value, options = {}) => { setDisplayStandby(value); deprecationHandler( `{{ bold : yellow : Stand by }} mode has been {{ bold : ${value ? "green : ACTIVATED" : "red : DEACTIVATED"} }}`, "setDisplayStandby()", "setDisplayStandBy()" ); processQueue(); }; log.setDisplayStandBy = (value, options = {}) => { const { silent = false } = options; setDisplayStandby(value); silentHandler( silent, `{{ bold : yellow : Stand by }} mode has been {{ bold : ${value ? "green : ACTIVATED" : "red : DEACTIVATED"} }}.` ); }; const silentHandler = (silent, message) => { if (!silent) { log(message, 202, "INFO", { force: true }); processQueue(); } }; const deprecationHandler = (message, before, after) => { log( `${message} \n[{{ bold : red : DEPRECATION WARNING }}] {{ bold : yellow : ${before} }} is deprecated. Use {{ bold : green : ${after} }} instead.\nIt will be removed at next Major update.\n` ); }; module.exports = log;