From 542beb82a4d06fa44adaaf4003b7c226b7fda5a9 Mon Sep 17 00:00:00 2001 From: creations Date: Sat, 24 May 2025 13:00:32 -0400 Subject: [PATCH] fix build problems, add all actual log funcs, move types --- .gitignore | 5 ++ package.json | 27 +++++++-- src/index.ts | 62 ++++++++++++++++++-- src/lib/char.ts | 143 ++++++++++++++++++++++++++++++++++++++++++++-- src/lib/config.ts | 50 ++++++++++++++-- tsconfig.json | 6 +- types/index.d.ts | 32 ----------- types/index.ts | 33 +++++++++++ 8 files changed, 305 insertions(+), 53 deletions(-) create mode 100644 .gitignore delete mode 100644 types/index.d.ts create mode 100644 types/index.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1a820e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/node_modules +bun.lock +.vscode +logger.json +dist diff --git a/package.json b/package.json index ec7703b..4299d67 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,28 @@ "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", + "typesVersions": { + "*": { + "lib/char": ["./dist/lib/char.d.ts"], + "lib/config": ["./dist/lib/config.d.ts"] + } + }, "exports": { ".": { "import": "./dist/index.js", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" + "require": "./dist/index.js" + }, + "./lib/char": { + "import": "./dist/lib/char.js", + "require": "./dist/lib/char.js" + }, + "./lib/config": { + "import": "./dist/lib/config.js", + "require": "./dist/lib/config.js" } }, "scripts": { + "build": "rm -rf dist && tsup src/index.ts --dts --out-dir dist --format esm,cjs", "lint": "bunx biome check", "lint:fix": "bunx biome check --fix", "cleanup": "rm -rf logs node_modules bun.lock" @@ -22,11 +36,16 @@ "license": "BSD-3-Clause", "devDependencies": { "@biomejs/biome": "^1.9.4", - "@types/bun": "^1.2.13" + "@types/bun": "^1.2.13", + "tsup": "^8.5.0", + "typescript": "^5.8.3" }, "files": ["dist", "README.md", "LICENSE"], "repository": { "type": "git", - "url": "https://git.creations.works/creations/logger.git" + "url": "https://git.creations.works/atums/echo" + }, + "dependencies": { + "date-fns-tz": "^3.2.0" } } diff --git a/src/index.ts b/src/index.ts index 1ba12c1..c43d134 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,20 +7,27 @@ import { statSync, } from "node:fs"; import { resolve } from "node:path"; -import { defaultConfig, loadEnvConfig, loadLoggerConfig } from "@lib/config"; +import { parsePattern } from "@lib/char"; +import { + defaultConfig, + loadEnvConfig, + loadLoggerConfig, + logLevelValues, +} from "@lib/config"; +import type { LogLevel, LoggerConfig } from "@types"; -export class Echo { +class Echo { private readonly directory: string; private readonly config: Required; - constructor(configOrPath?: string | LoggerConfig) { + constructor(config?: string | LoggerConfig) { const fileConfig: LoggerConfig = - typeof configOrPath === "string" - ? loadLoggerConfig(configOrPath) + typeof config === "string" + ? loadLoggerConfig(config) : loadLoggerConfig(); const overrideConfig: LoggerConfig = - typeof configOrPath === "object" ? configOrPath : {}; + typeof config === "object" ? config : {}; const envConfig: LoggerConfig = loadEnvConfig(); @@ -58,4 +65,47 @@ export class Echo { public getConfig(): Required { return this.config; } + + private log(level: LogLevel, data: unknown): void { + if ( + this.config.silent || + logLevelValues[this.config.level] > logLevelValues[level] + ) + return; + + const line = parsePattern({ level, data, config: this.config }); + + if (this.config.console) { + console[level === "error" ? "error" : level === "warn" ? "warn" : "log"]( + line, + ); + } + } + + public debug(data: unknown): void { + this.log("debug", data); + } + + public info(data: unknown): void { + this.log("info", data); + } + + public warn(data: unknown): void { + this.log("warn", data); + } + + public error(data: unknown): void { + this.log("error", data); + } + + public fatal(data: unknown): void { + this.log("fatal", data); + } + + public trace(data: unknown): void { + this.log("trace", data); + } } + +const echo = new Echo(); +export { echo, Echo }; diff --git a/src/lib/char.ts b/src/lib/char.ts index f6062ba..504e3bc 100644 --- a/src/lib/char.ts +++ b/src/lib/char.ts @@ -1,8 +1,139 @@ -function timestampToReadable(timestamp?: number): string { - const date: Date = - timestamp && !Number.isNaN(timestamp) ? new Date(timestamp) : new Date(); - if (Number.isNaN(date.getTime())) return "Invalid Date"; - return date.toISOString().replace("T", " ").replace("Z", ""); +import { basename } from "node:path"; +import { format } from "node:util"; +import { format as formatDate } from "date-fns-tz"; + +import { ansiColors, logLevelValues } from "@lib/config"; +import type { + LogLevel, + LogLevelValue, + LoggerConfig, + PatternContext, +} from "@types"; + +function getTimestamp(config: Required): string { + const now = new Date(); + + if (config.timezone === "local") { + return formatDate(now, config.dateFormat); + } + + return formatDate(now, config.dateFormat, { + timeZone: config.timezone, + }); } -export { timestampToReadable }; +function getCallerInfo(config: Required): { + fileName: string; + line: string; + column: string; + timestamp: string; +} { + const fallback = { + fileName: "unknown", + line: "0", + timestamp: getTimestamp(config), + column: "0", + }; + + 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 { + fileName: basename(fullPath), + line: lineNumber, + column: columnNumber, + timestamp: getTimestamp(config), + }; + } + + 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 { + fileName: basename(fullPath), + line: lineNumber, + column: columnNumber, + timestamp: getTimestamp(config), + }; + } + } + + 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, +): string { + return input + .replace(/{color:(\w+)}/g, (_, colorKey) => { + if (colorKey === "levelColor") { + const colorForLevel = config.levelColor?.[level]; + return ansiColors[colorForLevel ?? ""] ?? ""; + } + return ansiColors[colorKey] ?? ""; + }) + .replace(/{reset}/g, ansiColors.reset); +} + +function parsePattern(ctx: PatternContext): string { + const { level, data, config } = ctx; + + const { fileName, line, column, timestamp } = getCallerInfo(config); + const resolvedData: string = format(data); + const numericLevel: LogLevelValue = logLevelValues[level]; + + const final: string = config.pattern + .replace(/{timestamp}/g, timestamp) + .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, generateShortId()) + .replace(/{column}/g, column); + + return config.consoleColor ? replaceColorTokens(final, level, config) : final; +} + +export { parsePattern }; diff --git a/src/lib/config.ts b/src/lib/config.ts index ebe28c3..3b19195 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,5 +1,31 @@ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; +import type { LogLevel, LoggerConfig } from "@types"; + +const logLevelValues = { + trace: 10, + debug: 20, + info: 30, + warn: 40, + error: 50, + fatal: 60, + silent: 70, +}; + +const ansiColors: Record = { + reset: "\x1b[0m", + dim: "\x1b[2m", + bright: "\x1b[1m", + black: "\x1b[30m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", + white: "\x1b[37m", + gray: "\x1b[90m", +}; const defaultConfig: Required = { directory: "logs", @@ -13,12 +39,22 @@ const defaultConfig: Required = { console: true, consoleColor: true, - dateFormat: "YYYY-MM-DD HH:mm:ss", - timezone: "UTC", + dateFormat: "yyyy-MM-dd HH:mm:ss.SSS", + timezone: "local", silent: false, - pattern: "{timestamp} [{level-name}] ({file-name}:{line}){message}", + pattern: + "{color:gray}{timestamp}{reset} {color:levelColor}[{level-name}]{reset} {color:gray}({reset}{file-name}:{line}:{column}{color:gray}){reset} {data}", + + levelColor: { + trace: "cyan", + debug: "blue", + info: "green", + warn: "yellow", + error: "red", + fatal: "red", + }, }; function loadLoggerConfig(configPath = "logger.json"): LoggerConfig { @@ -56,4 +92,10 @@ function loadEnvConfig(): LoggerConfig { return config; } -export { defaultConfig, loadLoggerConfig, loadEnvConfig }; +export { + defaultConfig, + loadLoggerConfig, + loadEnvConfig, + logLevelValues, + ansiColors, +}; diff --git a/tsconfig.json b/tsconfig.json index 0ae7837..231a4a8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "baseUrl": "./", "paths": { "@/*": ["src/*"], + "@types": ["types/index.ts"], "@types/*": ["types/*"], "@lib/*": ["src/lib/*"] }, @@ -15,7 +16,10 @@ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, - "noEmit": true, + "noEmit": false, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist", "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index dcd32e4..0000000 --- a/types/index.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -const LogLevelValue = { - trace: 10, - debug: 20, - info: 30, - warn: 40, - error: 50, - fatal: 60, - silent: 70, -} as const; - -type LogLevelValue = typeof LogLevelValue[keyof typeof LogLevelValue]; -type LogLevel = keyof typeof LogLevelValue; - -type LoggerConfig = { - directory?: string; - level?: LogLevel; - disableFile?: boolean; - - rotate?: boolean; - maxSizeMB?: number; - maxFiles?: number; - - console?: boolean; - consoleColor?: boolean; - - dateFormat?: string; - timezone?: string; - - silent?: boolean; - - pattern?: string; -}; diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 0000000..03095c2 --- /dev/null +++ b/types/index.ts @@ -0,0 +1,33 @@ +import { ansiColors, logLevelValues } from "@lib/config"; + +type LogLevelValue = typeof logLevelValues[keyof typeof logLevelValues]; +type LogLevel = keyof typeof logLevelValues; + +type LoggerConfig = { + directory?: string; + level?: LogLevel; + disableFile?: boolean; + + rotate?: boolean; + maxSizeMB?: number; + maxFiles?: number; + + console?: boolean; + consoleColor?: boolean; + + dateFormat?: string; + timezone?: string; + + silent?: boolean; + + pattern?: string; + levelColor?: Partial>; +}; + +interface PatternContext { + level: LogLevel; + data: unknown; + config: Required; +} + +export type { LogLevel, LogLevelValue, LoggerConfig, PatternContext };