From c33672085287d54a7eca1b1b66d7751113d6cc11 Mon Sep 17 00:00:00 2001 From: kaedeek Date: Fri, 24 Apr 2026 20:44:01 +0900 Subject: [PATCH] feat: introduce plugin-based notifier system and refactor core --- VersaLog/core/index.ts | 298 ++++++++++++++++++++++++++++ VersaLog/index.ts | 402 -------------------------------------- VersaLog/plugin/notify.ts | 11 ++ package.json | 31 +-- tsconfig.json | 3 +- 5 files changed, 319 insertions(+), 426 deletions(-) create mode 100644 VersaLog/core/index.ts delete mode 100644 VersaLog/index.ts create mode 100644 VersaLog/plugin/notify.ts diff --git a/VersaLog/core/index.ts b/VersaLog/core/index.ts new file mode 100644 index 0000000..08a1af3 --- /dev/null +++ b/VersaLog/core/index.ts @@ -0,0 +1,298 @@ +type LogMode = "simple" | "simple2" | "detailed" | "file"; +type LogLevel = "INFO" | "ERROR" | "WARNING" | "DEBUG" | "CRITICAL"; + +import { format } from "date-fns"; +import * as fs from "fs"; +import * as path from "path"; + +const COLORS: Record = { + INFO: "\x1b[32m", + ERROR: "\x1b[31m", + WARNING: "\x1b[33m", + DEBUG: "\x1b[36m", + CRITICAL: "\x1b[35m", +}; + +const SYMBOLS: Record = { + INFO: "[+]", + ERROR: "[-]", + WARNING: "[!]", + DEBUG: "[D]", + CRITICAL: "[C]", +}; + +const RESET = "\x1b[0m"; + +class Versalog { + public mode: LogMode; + public tag: string[]; + public show_file: boolean; + public show_tag: boolean; + public notice: boolean; + public all_save: boolean; + public save_levels: LogLevel[]; + public silent: boolean; + + private notifyHandler?: (title: string, message: string) => void; + + private log_queue: Array<{ log_text: string; level: string }> = []; + private is_worker_running: boolean = false; + private catch_exceptions: boolean; + private last_cleanup_date: string | null = null; + + constructor( + mode: LogMode = "simple", + show_file: boolean = false, + show_tag: boolean = false, + tag: string | string[] = "", + enable_all: boolean = false, + notice: boolean = false, + all_save: boolean = false, + save_levels: LogLevel[] | null = null, + silent: boolean = false, + catch_exceptions: boolean = false, + ) { + if (enable_all) { + show_file = true; + show_tag = true; + notice = true; + all_save = true; + } + + const valid_modes: LogMode[] = ["simple", "simple2", "detailed", "file"]; + if (!valid_modes.includes(mode)) { + throw new Error(`Invalid mode '${mode}'`); + } + + this.mode = mode; + this.show_file = show_file; + this.show_tag = show_tag; + this.tag = Array.isArray(tag) ? tag : tag ? [tag] : []; + + this.notice = notice; + this.all_save = all_save; + + const valid_levels: LogLevel[] = [ + "INFO", + "ERROR", + "WARNING", + "DEBUG", + "CRITICAL", + ]; + + this.save_levels = save_levels ?? [...valid_levels]; + + if ( + !Array.isArray(this.save_levels) || + !this.save_levels.every((l) => valid_levels.includes(l)) + ) { + throw new Error(`Invalid save_levels`); + } + + this.silent = silent; + this.catch_exceptions = catch_exceptions; + + if (this.catch_exceptions) { + process.on("uncaughtException", (err) => { + this.critical(`Unhandled exception:\n${err.stack || err}`); + }); + process.on("unhandledRejection", (reason: any) => { + this.critical(`Unhandled rejection:\n${reason?.stack || reason}`); + }); + } + } + + public setNotifier(fn: (title: string, message: string) => void) { + this.notifyHandler = fn; + } + + private GetTime(): string { + return format(new Date(), "yyyy-MM-dd HH:mm:ss"); + } + + private GetCaller(): string { + const stack = new Error().stack?.split("\n"); + const callerLine = stack && stack[3] ? stack[3] : stack?.[2] ?? ""; + const match = callerLine.match(/\(([^)]+)\)/); + if (!match) return ""; + const parts = match[1].split(":"); + return `${parts[0].split("/").pop()}:${parts[1]}`; + } + + private save_log(log_text: string, level: string): void { + if (!this.save_levels.includes(level as LogLevel)) return; + + this.log_queue.push({ log_text, level }); + this.run_worker(); + + const today = format(new Date(), "yyyy-MM-dd"); + if (this.last_cleanup_date !== today) { + this.cleanup_old_logs(7); + this.last_cleanup_date = today; + } + } + + private run_worker() { + if (this.is_worker_running) return; + this.is_worker_running = true; + + const processQueue = () => { + if (this.log_queue.length === 0) { + this.is_worker_running = false; + return; + } + + const { log_text } = this.log_queue.shift()!; + const logDir = path.join(process.cwd(), "log"); + + if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true }); + + const logFile = path.join( + logDir, + `${format(new Date(), "yyyy-MM-dd")}.log` + ); + + fs.appendFile(logFile, log_text + "\n", () => { + setImmediate(processQueue); + }); + }; + + setImmediate(processQueue); + } + + private cleanup_old_logs(days: number = 7): void { + const logDir = path.join(process.cwd(), "log"); + if (!fs.existsSync(logDir)) return; + + const now = Date.now(); + + for (const filename of fs.readdirSync(logDir)) { + if (!filename.endsWith(".log")) continue; + + const filepath = path.join(logDir, filename); + const stat = fs.statSync(filepath); + + const diffDays = + (now - stat.mtime.getTime()) / (1000 * 60 * 60 * 24); + + if (diffDays >= days) { + try { + fs.unlinkSync(filepath); + } catch {} + } + } + } + + private Log(msg: string, tye: LogLevel, tag?: string | string[]): void { + const color = COLORS[tye]; + const symbol = SYMBOLS[tye]; + + const tags: string[] = []; + if (this.show_tag) { + tags.push(...this.tag); + if (tag) { + if (Array.isArray(tag)) tags.push(...tag); + else tags.push(tag); + } + } + + const tagStr = tags.length ? tags.map(t => `[${t}]`).join("") : ""; + + const caller = + this.show_file || this.mode === "file" ? this.GetCaller() : ""; + + if (this.notice && (tye === "ERROR" || tye === "CRITICAL")) { + this.notifyHandler?.(`${tye} Log notice`, msg); + } + + let formatted = ""; + let plain = ""; + + if (this.mode === "simple") { + formatted = + (this.show_file ? `[${caller}]` : "") + + tagStr + + `${color}${symbol}${RESET} ${msg}`; + plain = + (this.show_file ? `[${caller}]` : "") + + tagStr + + `${symbol} ${msg}`; + } else if (this.mode === "simple2") { + const time = this.GetTime(); + formatted = `[${time}]${caller ? ` [${caller}]` : ""}${tagStr}${color}${symbol}${RESET} ${msg}`; + plain = `[${time}]${caller ? ` [${caller}]` : ""}${tagStr}${symbol} ${msg}`; + } else if (this.mode === "file") { + formatted = `[${caller}]${tagStr}${color}[${tye}]${RESET} ${msg}`; + plain = `[${caller}]${tagStr}[${tye}] ${msg}`; + } else { + const time = this.GetTime(); + formatted = `[${time}]${color}[${tye}]${RESET}${tagStr}${ + this.show_file ? `[${caller}]` : "" + } : ${msg}`; + plain = `[${time}][${tye}]${tagStr}${ + this.show_file ? `[${caller}]` : "" + } : ${msg}`; + } + + if (!this.silent) { + process.stdout.write(formatted + "\n"); // 🚀 高速化 + } + + if (this.all_save && this.save_levels.includes(tye)) { + this.save_log(plain, tye); + } + } + + public info(msg: string, tag?: string | string[]) { + this.Log(msg, "INFO", tag); + } + + public error(msg: string, tag?: string | string[]) { + this.Log(msg, "ERROR", tag); + } + + public warning(msg: string, tag?: string | string[]) { + this.Log(msg, "WARNING", tag); + } + + public debug(msg: string, tag?: string | string[]) { + this.Log(msg, "DEBUG", tag); + } + + public critical(msg: string, tag?: string | string[]) { + this.Log(msg, "CRITICAL", tag); + } + + public board(): void { + process.stdout.write("=".repeat(45) + "\n"); + } + + public progress(title: string, current: number, total: number, tag?: string | string[]) { + const percent = total ? Math.floor((current / total) * 100) : 0; + this.Log(`${title} : ${percent}% (${current}/${total})`, "INFO", tag); + } + + public step(title: string, step: number, total: number, tag?: string | string[]) { + this.Log(`[STEP ${step}/${total}] ${title}`, "INFO", tag); + } + + public async timer( + title: string, + fn: () => Promise | void, + tag?: string | string[] + ): Promise { + const start = Date.now(); + + this.Log(`${title} : start`, "INFO", tag); + + try { + await fn(); + } finally { + const elapsed = (Date.now() - start) / 1000; + this.Log(`${title} : done (${elapsed.toFixed(2)}s)`, "INFO", tag); + } + } +} + +export default Versalog; +export { Versalog }; \ No newline at end of file diff --git a/VersaLog/index.ts b/VersaLog/index.ts deleted file mode 100644 index c33a453..0000000 --- a/VersaLog/index.ts +++ /dev/null @@ -1,402 +0,0 @@ -type LogMode = "simple" | "simple2" | "detailed" | "file"; -type LogLevel = "INFO" | "ERROR" | "WARNING" | "DEBUG" | "CRITICAL"; -import { format } from "date-fns"; -const notifier = require("node-notifier"); -import * as fs from "fs"; -import * as path from "path"; - -const COLORS: Record = { - INFO: "\x1b[32m", - ERROR: "\x1b[31m", - WARNING: "\x1b[33m", - DEBUG: "\x1b[36m", - CRITICAL: "\x1b[35m", -}; - -const SYMBOLS: Record = { - INFO: "[+]", - ERROR: "[-]", - WARNING: "[!]", - DEBUG: "[D]", - CRITICAL: "[C]", -}; - -const RESET = "\x1b[0m"; - -class Versalog { - public mode: LogMode; - public tag: string | string[]; - public show_file: boolean; - public show_tag: boolean; - public notice: boolean; - public all_save: boolean; - public save_levels: LogLevel[]; - public silent: boolean; - private log_queue: Array<{ log_text: string; level: string }> = []; - private is_worker_running: boolean = false; - private catch_exceptions: boolean; - private last_cleanup_date: string | null = null; - - constructor( - mode: LogMode = "simple", - show_file: boolean = false, - show_tag: boolean = false, - tag: string | string[] = "", - enable_all: boolean = false, - notice: boolean = false, - all_save: boolean = false, - save_levels: LogLevel[] | null = null, - silent: boolean = false, - catch_exceptions: boolean = false, - ) { - if (enable_all) { - show_file = true; - show_tag = true; - notice = true; - all_save = true; - } - - const valid_modes: LogMode[] = ["simple", "simple2", "detailed", "file"]; - if (!valid_modes.includes(mode)) { - throw new Error( - `Invalid mode '${mode}'. Valid modes are: ${valid_modes.join(", ")}`, - ); - } - - this.mode = mode; - this.show_file = show_file; - this.show_tag = show_tag; - if (Array.isArray(tag)) { - this.tag = tag; - } else if (typeof tag === "string") { - this.tag = tag ? [tag] : []; - } else { - this.tag = []; - } - this.notice = notice; - this.all_save = all_save; - const valid_save_levels: LogLevel[] = [ - "INFO", - "ERROR", - "WARNING", - "DEBUG", - "CRITICAL", - ]; - this.save_levels = save_levels ?? [...valid_save_levels]; - if ( - !Array.isArray(this.save_levels) || - !this.save_levels.every((l) => valid_save_levels.includes(l)) - ) { - throw new Error( - `Invalid save_levels specified. Valid levels are: ${valid_save_levels.join(", ")}`, - ); - } - this.silent = silent; - this.catch_exceptions = catch_exceptions; - if (this.catch_exceptions) { - process.on("uncaughtException", (err) => { - this.critical(`Unhandled exception:\n${err.stack || err}`); - }); - process.on("unhandledRejection", (reason: any) => { - this.critical(`Unhandled rejection:\n${reason?.stack || reason}`); - }); - } - } - - public setConfig(options: { - mode?: LogMode; - show_file?: boolean; - show_tag?: boolean; - tag?: string | string[]; - enable_all?: boolean; - notice?: boolean; - all_save?: boolean; - save_levels?: LogLevel[]; - silent?: boolean; - catch_exceptions?: boolean; - }): void { - if (options.enable_all) { - this.show_file = true; - this.show_tag = true; - this.notice = true; - this.all_save = true; - } - - if (options.mode !== undefined) { - this.mode = options.mode; - } - if (options.show_file !== undefined) this.show_file = options.show_file; - if (options.show_tag !== undefined) this.show_tag = options.show_tag; - if (options.tag !== undefined) { - if (Array.isArray(options.tag)) { - this.tag = options.tag; - } else if (typeof options.tag === "string") { - this.tag = options.tag ? [options.tag] : []; - } else { - this.tag = []; - } - } - if (options.notice !== undefined) this.notice = options.notice; - if (options.all_save !== undefined) this.all_save = options.all_save; - if (options.save_levels !== undefined) { - const valid_save_levels: LogLevel[] = [ - "INFO", - "ERROR", - "WARNING", - "DEBUG", - "CRITICAL", - ]; - if ( - !Array.isArray(options.save_levels) || - !options.save_levels.every((l) => valid_save_levels.includes(l)) - ) { - throw new Error( - `Invalid save_levels specified. Valid levels are: ${valid_save_levels.join(", ")}`, - ); - } - this.save_levels = options.save_levels; - } - if (options.silent !== undefined) this.silent = options.silent; - if (options.catch_exceptions !== undefined) { - this.catch_exceptions = options.catch_exceptions; - if (this.catch_exceptions) { - process.on("uncaughtException", (err) => { - this.critical(`Unhandled exception:\n${err.stack || err}`); - }); - process.on("unhandledRejection", (reason: any) => { - this.critical(`Unhandled rejection:\n${reason?.stack || reason}`); - }); - } - } - } - - private GetTime(): string { - return format(new Date(), "yyyy-MM-dd HH:mm:ss"); - } - - private GetCaller(): string { - const err = new Error(); - const stack = err.stack?.split("\n"); - const callerLine = stack && stack[3] ? stack[3] : (stack?.[2] ?? ""); - const match = callerLine.match(/\(([^)]+)\)/); - if (match && match[1]) { - const parts = match[1].split(":"); - const file = parts[0].split("/").pop(); - const line = parts[1]; - return `${file}:${line}`; - } - return ""; - } - - private save_log(log_text: string, level: string): void { - if (!this.save_levels.includes(level as LogLevel)) return; - this.log_queue.push({ log_text, level }); - this.run_worker(); - - const today = format(new Date(), "yyyy-MM-dd"); - if (this.last_cleanup_date !== today) { - this.cleanup_old_logs(7); - this.last_cleanup_date = today; - } - } - - private run_worker() { - if (this.is_worker_running) return; - this.is_worker_running = true; - const processQueue = () => { - if (this.log_queue.length === 0) { - this.is_worker_running = false; - return; - } - const { log_text, level } = this.log_queue.shift()!; - const logDir = path.join(process.cwd(), "log"); - if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true }); - const dateStr = format(new Date(), "yyyy-MM-dd"); - const logFile = path.join(logDir, `${dateStr}.log`); - fs.appendFile(logFile, log_text + "\n", { encoding: "utf-8" }, () => { - setImmediate(processQueue); - }); - }; - setImmediate(processQueue); - } - - private cleanup_old_logs(days: number = 7): void { - const logDir = path.join(process.cwd(), "log"); - if (!fs.existsSync(logDir)) return; - - const now = new Date(); - fs.readdirSync(logDir).forEach((filename) => { - if (!filename.endsWith(".log")) return; - const filepath = path.join(logDir, filename); - - let fileDate: Date; - try { - fileDate = new Date(filename.replace(".log", "")); - if (isNaN(fileDate.getTime())) throw new Error(); - } catch { - fileDate = new Date(fs.statSync(filepath).mtime); - } - - const diffDays = - (now.getTime() - fileDate.getTime()) / (1000 * 60 * 60 * 24); - if (diffDays >= days) { - try { - fs.unlinkSync(filepath); - if (!this.silent) this.info(`[LOG CLEANUP] removed: ${filepath}`); - } catch (e) { - if (!this.silent) - this.warning( - `[LOG CLEANUP WARNING] ${filepath} cannot be removed: ${e}`, - ); - } - } - }); - } - - private Log(msg: string, tye: string, tag?: string | string[]): void { - const color = COLORS[tye] ?? ""; - const symbol = SYMBOLS[tye] ?? "[?]"; - const types = tye.toUpperCase(); - - const tags: string[] = []; - if (this.show_tag) { - if (Array.isArray(this.tag)) { - tags.push(...this.tag); - } else if (this.tag) { - tags.push(this.tag); - } - if (tag) { - if (Array.isArray(tag)) tags.push(...tag); - else tags.push(tag); - } - } - const tagStr = tags.length > 0 ? tags.map(t => `[${t}]`).join("") : ""; - const caller = - this.show_file || this.mode === "file" ? this.GetCaller() : ""; - - let formatted = ""; - let plain = ""; - - if (this.notice && (types === "ERROR" || types === "CRITICAL")) { - notifier.notify({ - title: `${types} Log notice`, - message: msg, - appName: "VersaLog", - }); - } - - if (this.mode === "simple") { - formatted = ""; - if (this.show_file) { - formatted += `[${caller}]`; - } - formatted += tagStr; - formatted += `${color}${symbol}${RESET} ${msg}`; - plain = - (this.show_file ? `[${caller}]` : "") + - tagStr + - `${symbol} ${msg}`; - } else if (this.mode === "simple2") { - const time = this.GetTime(); - if (this.show_file) { - formatted = `[${time}]`; - if (caller) formatted += ` [${caller}]`; - formatted += tagStr; - formatted += `${color}${symbol}${RESET} ${msg}`; - plain = - `[${time}]` + - (caller ? ` [${caller}]` : "") + - tagStr + - `${symbol} ${msg}`; - } else { - formatted = `[${time}] ${color}${symbol}${RESET} ${msg}`; - plain = `[${time}] ${symbol} ${msg}`; - } - } else if (this.mode === "file") { - formatted = `[${caller}]`; - formatted += tagStr; - formatted += `${color}[${types}]${RESET} ${msg}`; - plain = - `[${caller}]` + tagStr + `[${types}] ${msg}`; - } else { - const time = this.GetTime(); - formatted = `[${time}]${color}[${types}]${RESET}`; - plain = `[${time}][${types}]`; - if (tagStr) { - formatted += tagStr; - plain += tagStr; - } - if (this.show_file) { - formatted += `[${caller}]`; - plain += `[${caller}]`; - } - formatted += ` : ${msg}`; - plain += ` : ${msg}`; - } - - if (!this.silent) { - console.log(formatted); - } - if (this.all_save && this.save_levels.includes(types as LogLevel)) { - this.save_log(plain, types); - } - } - - public info(msg: string, tag?: string | string[]): void { - this.Log(msg, "INFO", tag); - } - - public error(msg: string, tag?: string | string[]): void { - this.Log(msg, "ERROR", tag); - } - - public warning(msg: string, tag?: string | string[]): void { - this.Log(msg, "WARNING", tag); - } - - public debug(msg: string, tag?: string | string[]): void { - this.Log(msg, "DEBUG", tag); - } - - public critical(msg: string, tag?: string | string[]): void { - this.Log(msg, "CRITICAL", tag); - } - - public board(): void { - console.log("=".repeat(45)); - } - - public progress(title: string, current: number, total: number, tag?: string | string[]): void { - const percent = total ? Math.floor((current / total) * 100) : 0; - const msg = `${title} : ${percent}% (${current}/${total})`; - this.Log(msg, "INFO", tag); - } - - public step(title: string, step: number, total: number, tag?: string | string[]): void { - const msg = `[STEP ${step}/${total}] ${title}`; - this.Log(msg, "INFO", tag); - } - - public async timer( - title: string, - fn: () => Promise | void, - tag?: string | string[] - ): Promise { - const start = Date.now(); - - this.Log(`${title} : start`, "INFO", tag); - - try { - await fn(); - } finally { - const elapsed = (Date.now() - start) / 1000; - this.Log( - `${title} : done (${elapsed.toFixed(2)}s)`, - "INFO", - tag - ); - } - } - -} -export = Versalog; diff --git a/VersaLog/plugin/notify.ts b/VersaLog/plugin/notify.ts new file mode 100644 index 0000000..c3aed57 --- /dev/null +++ b/VersaLog/plugin/notify.ts @@ -0,0 +1,11 @@ +import notifier from "node-notifier"; + +export const notifyPlugin = (log: any) => { + log.setNotifier((title: string, message: string) => { + notifier.notify({ + title, + message, + appName: "VersaLog", + }); + }); +}; \ No newline at end of file diff --git a/package.json b/package.json index 8a8cc16..a97a7ed 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,19 @@ { "name": "versalog", - "version": "2.0.0", + "version": "2.1.0", "description": "A simple and customizable logger for Node.js", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "dist/core/index.js", + "types": "dist/core/index.d.ts", "author": "kayu0514", "license": "MIT", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint .", - "format": "prettier --write .", "build": "tsc" }, - "keywords": [], + "dependencies": { + "date-fns": "^4.1.0" + }, "devDependencies": { - "@eslint/js": "^9.36.0", "@types/node": "^24.0.10", - "@types/node-notifier": "^8.0.5", - "eslint": "^9.36.0", - "globals": "^16.4.0", - "jiti": "^2.6.0", - "prettier": "^3.6.2", - "typescript": "^5.8.3", - "typescript-eslint": "^8.45.0" - }, - "dependencies": { - "date-fns": "^4.1.0", - "luxon": "^3.6.1", - "node-notifier": "^10.0.1", - "pretty-format": "^30.0.2", - "versalog": "^1.0.0" + "typescript": "^5.8.3" } -} +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d51ce2e..cec4300 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "rootDir": "./VersaLog", "outDir": "./dist", "declaration": true, "module": "commonjs", @@ -9,4 +10,4 @@ "lib": ["es2020"] }, "include": ["VersaLog/**/*"] -} +} \ No newline at end of file