echo/src/lib/char.ts

172 lines
4 KiB
TypeScript

import { basename } from "node:path";
import { format, inspect } from "node:util";
import { format as formatDate } from "date-fns-tz";
import { ansiColors, defaultLevelColor, logLevelValues } from "@lib/config";
import type {
LogLevel,
LogLevelValue,
LoggerConfig,
PatternContext,
} from "@types";
function getTimestamp(config: Required<LoggerConfig>): {
prettyTimestamp: string;
timestamp: string;
} {
const now = new Date();
if (config.timezone === "local") {
return {
prettyTimestamp: formatDate(now, config.dateFormat),
timestamp: now.toISOString(),
};
}
return {
prettyTimestamp: formatDate(now, config.dateFormat, {
timeZone: config.timezone,
}),
timestamp: now.toISOString(),
};
}
function getCallerInfo(config: Required<LoggerConfig>): {
id: string;
fileName: string;
line: string;
column: string;
timestamp: string;
prettyTimestamp: string;
} {
const id = generateShortId();
const timestampInfo = getTimestamp(config);
const fallback = {
id,
fileName: "unknown",
line: "0",
column: "0",
timestamp: timestampInfo.timestamp,
prettyTimestamp: timestampInfo.prettyTimestamp,
};
const stack = new Error().stack;
if (!stack) {
return fallback;
}
const lines = stack.split("\n");
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
const fileURLMatch = line.match(
/at\s+(?:.*\()?file:\/\/(.*):(\d+):(\d+)\)?/,
);
if (fileURLMatch) {
const fullPath = fileURLMatch[1];
const lineNumber = fileURLMatch[2];
const columnNumber = fileURLMatch[3];
return {
id: id,
fileName: basename(fullPath),
line: lineNumber,
column: columnNumber,
timestamp: timestampInfo.timestamp,
prettyTimestamp: timestampInfo.prettyTimestamp,
};
}
const rawMatch = line.match(/at\s+(?:.*\()?(.+):(\d+):(\d+)\)?/);
if (rawMatch) {
const fullPath = rawMatch[1];
const lineNumber = rawMatch[2];
const columnNumber = rawMatch[3];
if (
fullPath.includes("/logger/") ||
fullPath.includes("/src/index.ts") ||
fullPath.includes("/src/lib/")
) {
continue;
}
return {
id: id,
fileName: basename(fullPath),
line: lineNumber,
column: columnNumber,
timestamp: timestampInfo.timestamp,
prettyTimestamp: timestampInfo.prettyTimestamp,
};
}
}
return fallback;
}
function generateShortId(length = 8): string {
const chars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let id = "";
for (let i = 0; i < length; i++) {
const rand = Math.floor(Math.random() * chars.length);
id += chars[rand];
}
return id;
}
function replaceColorTokens(
input: string,
level: LogLevel,
config: Required<LoggerConfig>,
): string {
return input
.replace(/{color:(\w+)}/g, (_, colorKey) => {
if (!config.consoleColor) return "";
if (colorKey === "levelColor") {
const colorForLevel =
config.levelColor?.[level] ?? defaultLevelColor[level];
return ansiColors[colorForLevel ?? ""] ?? "";
}
return ansiColors[colorKey] ?? "";
})
.replace(/{reset}/g, config.consoleColor ? ansiColors.reset : "");
}
function parsePattern(ctx: PatternContext): string {
const { level, data, config } = ctx;
const { id, fileName, line, column, timestamp, prettyTimestamp } =
getCallerInfo(config);
const resolvedData: string =
config.prettyPrint && typeof data === "object" && data !== null
? inspect(data, {
depth: null,
colors: config.consoleColor,
breakLength: 1,
compact: false,
})
: format(data);
const numericLevel: LogLevelValue = logLevelValues[level];
const final = config.pattern
.replace(/{timestamp}/g, timestamp)
.replace(/{pretty-timestamp}/g, prettyTimestamp)
.replace(/{level-name}/g, level.toUpperCase())
.replace(/{level}/g, String(numericLevel))
.replace(/{file-name}/g, fileName)
.replace(/{line}/g, line)
.replace(/{data}/g, resolvedData)
.replace(/{id}/g, id)
.replace(/{column}/g, column);
return replaceColorTokens(final, level, config);
}
export { parsePattern, getCallerInfo, getTimestamp, generateShortId };