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
JavaScript
;
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;