From 81aae5978ffe05552dfe82742424bd4ec5c83616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 23 Jul 2025 17:36:52 -0300 Subject: [PATCH 01/21] feat: create logger module --- src/logger/__tests__/index.test.mjs | 19 ++ src/logger/__tests__/logger.test.mjs | 184 +++++++++++++++ .../__tests__/transports/console.test.mjs | 210 ++++++++++++++++++ .../__tests__/transports/github.test.mjs | 185 +++++++++++++++ src/logger/constants.mjs | 34 +++ src/logger/index.mjs | 35 +++ src/logger/logger.mjs | 124 +++++++++++ src/logger/transports/console.mjs | 46 ++++ src/logger/transports/github.mjs | 45 ++++ src/logger/types.d.ts | 25 +++ src/logger/utils/colors.mjs | 18 ++ src/logger/utils/time.mjs | 19 ++ 12 files changed, 944 insertions(+) create mode 100644 src/logger/__tests__/index.test.mjs create mode 100644 src/logger/__tests__/logger.test.mjs create mode 100644 src/logger/__tests__/transports/console.test.mjs create mode 100644 src/logger/__tests__/transports/github.test.mjs create mode 100644 src/logger/constants.mjs create mode 100644 src/logger/index.mjs create mode 100644 src/logger/logger.mjs create mode 100644 src/logger/transports/console.mjs create mode 100644 src/logger/transports/github.mjs create mode 100644 src/logger/types.d.ts create mode 100644 src/logger/utils/colors.mjs create mode 100644 src/logger/utils/time.mjs diff --git a/src/logger/__tests__/index.test.mjs b/src/logger/__tests__/index.test.mjs new file mode 100644 index 00000000..939022a4 --- /dev/null +++ b/src/logger/__tests__/index.test.mjs @@ -0,0 +1,19 @@ +import { ok, strictEqual } from 'node:assert'; +import { describe, it } from 'node:test'; + +import { Logger } from '../index.mjs'; + +describe('Logger singleton', () => { + it('should return the same instance on multiple calls', () => { + const logger1 = Logger.getInstance(); + const logger2 = Logger.getInstance(); + + strictEqual(logger1, logger2); + }); + + it('should create an instance of logger', () => { + const logger = Logger.getInstance(); + + ok(logger); + }); +}); diff --git a/src/logger/__tests__/logger.test.mjs b/src/logger/__tests__/logger.test.mjs new file mode 100644 index 00000000..aa12634a --- /dev/null +++ b/src/logger/__tests__/logger.test.mjs @@ -0,0 +1,184 @@ +import { deepStrictEqual, strictEqual } from 'node:assert'; +import { describe, it } from 'node:test'; + +import { LogLevel } from '../constants.mjs'; +import { createLogger } from '../logger.mjs'; + +/** + * @type {import('../types').Metadata} + */ +const metadata = { + file: { + path: 'test.md', + position: { + start: { line: 1 }, + end: { line: 1 }, + }, + }, +}; + +describe('createLogger', () => { + describe('DEBUG', () => { + it('should log DEBUG messages when logger level is set to DEBUG', t => { + t.mock.timers.enable({ apis: ['Date'] }); + + const transport = t.mock.fn(); + + const logger = createLogger(transport, LogLevel.debug); + + logger.debug('Hello, World!', metadata); + + strictEqual(transport.mock.callCount(), 1); + + const call = transport.mock.calls[0]; + deepStrictEqual(call.arguments, [ + { + level: LogLevel.debug, + message: 'Hello, World!', + metadata, + timestamp: 0, + }, + ]); + }); + + it('should filter DEBUG messages when logger level is set to INFO or higher', t => { + [LogLevel.info, LogLevel.warn, LogLevel.error, LogLevel.fatal].forEach( + loggerLevel => { + const transport = t.mock.fn(); + + const logger = createLogger(transport, loggerLevel); + + logger.debug('Hello, World!'); + + strictEqual(transport.mock.callCount(), 0); + } + ); + }); + }); + + describe('INFO', () => { + it('should log INFO messages when logger level is set to INFO or lower', t => { + t.mock.timers.enable({ apis: ['Date'] }); + [LogLevel.info, LogLevel.debug].forEach(loggerLevel => { + const transport = t.mock.fn(); + + const logger = createLogger(transport, loggerLevel); + + logger.info('Hello, World!', metadata); + + strictEqual(transport.mock.callCount(), 1); + + const call = transport.mock.calls[0]; + deepStrictEqual(call.arguments, [ + { + level: LogLevel.info, + message: 'Hello, World!', + metadata, + timestamp: 0, + }, + ]); + }); + }); + + it('should filter INFO messages when logger level is set to WARN or higher', t => { + [LogLevel.warn, LogLevel.error, LogLevel.fatal].forEach(loggerLevel => { + const transport = t.mock.fn(); + + const logger = createLogger(transport, loggerLevel); + + logger.info('Hello, World!'); + + strictEqual(transport.mock.callCount(), 0); + }); + }); + }); + + describe('WARN', () => { + it('should log WARN messages when logger level is set to WARN or lower', t => { + t.mock.timers.enable({ apis: ['Date'] }); + + [LogLevel.warn, LogLevel.info, LogLevel.debug].forEach(loggerLevel => { + const transport = t.mock.fn(); + + const logger = createLogger(transport, loggerLevel); + + logger.warn('Hello, World!', metadata); + + strictEqual(transport.mock.callCount(), 1); + + const call = transport.mock.calls[0]; + deepStrictEqual(call.arguments, [ + { + level: LogLevel.warn, + message: 'Hello, World!', + metadata, + timestamp: 0, + }, + ]); + }); + }); + + it('should filter WARN messages when logger level is set to ERROR or higher', t => { + [LogLevel.error, LogLevel.fatal].forEach(loggerLevel => { + const transport = t.mock.fn(); + + const logger = createLogger(transport, loggerLevel); + + logger.warn('Hello, World!'); + + strictEqual(transport.mock.callCount(), 0); + }); + }); + }); + + describe('ERROR', () => { + it('should log ERROR messages when logger level is set to ERROR or lower', t => { + t.mock.timers.enable({ apis: ['Date'] }); + + [LogLevel.error, LogLevel.warn, LogLevel.info, LogLevel.debug].forEach( + loggerLevel => { + const transport = t.mock.fn(); + + const logger = createLogger(transport, loggerLevel); + + logger.error('Hello, World!', metadata); + + strictEqual(transport.mock.callCount(), 1); + + const call = transport.mock.calls[0]; + deepStrictEqual(call.arguments, [ + { + level: LogLevel.error, + message: 'Hello, World!', + metadata, + timestamp: 0, + }, + ]); + } + ); + }); + + it('should filter ERROR messages when logger level is set to FATAL', t => { + const transport = t.mock.fn(); + + const logger = createLogger(transport, LogLevel.fatal); + + logger.warn('Hello, World!'); + + strictEqual(transport.mock.callCount(), 0); + }); + }); + + it('should filter all messages when minimum level is set above FATAL', t => { + const transport = t.mock.fn(); + + // silent logs + const logger = createLogger(transport, 100); + + Object.keys(LogLevel).forEach(level => { + logger[level]('Hello, World!'); + }); + + strictEqual(transport.mock.callCount(), 0); + }); +}); diff --git a/src/logger/__tests__/transports/console.test.mjs b/src/logger/__tests__/transports/console.test.mjs new file mode 100644 index 00000000..d7f4b070 --- /dev/null +++ b/src/logger/__tests__/transports/console.test.mjs @@ -0,0 +1,210 @@ +import { deepStrictEqual, strictEqual } from 'assert'; +import { describe, it } from 'node:test'; + +import { LogLevel } from '../../constants.mjs'; +import { console } from '../../transports/console.mjs'; + +describe('console', () => { + it('should print debug messages', t => { + t.mock.timers.enable({ apis: ['Date'] }); + + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + console({ + level: LogLevel.debug, + message: 'Test message', + timestamp: Date.now(), + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 4); + deepStrictEqual(callsArgs, [ + '[21:00:00.000]', + ' \x1B[34mDEBUG\x1B[39m', + ': Test message', + '\n', + ]); + }); + + it('should print info messages', t => { + t.mock.timers.enable({ apis: ['Date'] }); + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + console({ + level: LogLevel.info, + message: 'Test message', + timestamp: Date.now(), + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 4); + deepStrictEqual(callsArgs, [ + '[21:00:00.000]', + ' \x1B[32mINFO\x1B[39m', + ': Test message', + '\n', + ]); + }); + + it('should print error messages ', t => { + t.mock.timers.enable({ apis: ['Date'] }); + + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + console({ + level: LogLevel.error, + message: 'Test message', + timestamp: Date.now(), + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 4); + deepStrictEqual(callsArgs, [ + '[21:00:00.000]', + ' \x1B[35mERROR\x1B[39m', + ': Test message', + '\n', + ]); + }); + + it('should print fatal messages', t => { + t.mock.timers.enable({ apis: ['Date'] }); + + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + console({ + level: LogLevel.fatal, + message: 'Test message', + timestamp: Date.now(), + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 4); + deepStrictEqual(callsArgs, [ + '[21:00:00.000]', + ' \x1B[31mFATAL\x1B[39m', + ': Test message', + '\n', + ]); + }); + + it('should print messages with file', t => { + t.mock.timers.enable({ apis: ['Date'] }); + + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + console({ + level: LogLevel.info, + message: 'Test message', + metadata: { + file: { + path: 'test.md', + position: { + start: { line: 1 }, + end: { line: 1 }, + }, + }, + }, + timestamp: Date.now(), + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 6); + deepStrictEqual(callsArgs, [ + '[21:00:00.000]', + ' \x1B[32mINFO\x1B[39m', + ': Test message', + ' at test.md', + '(1:1)', + '\n', + ]); + }); + + it('should print child logger name', t => { + t.mock.timers.enable({ apis: ['Date'] }); + + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + console({ + level: LogLevel.info, + message: 'Test message', + timestamp: Date.now(), + module: 'child1', + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 5); + deepStrictEqual(callsArgs, [ + '[21:00:00.000]', + ' \x1B[32mINFO\x1B[39m', + ' (child1)', + ': Test message', + '\n', + ]); + }); + + it('should print without colors if FORCE_COLOR = 0', t => { + process.env.FORCE_COLOR = 0; + + t.mock.timers.enable({ apis: ['Date'] }); + + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + console({ + level: LogLevel.info, + message: 'Test message', + timestamp: Date.now(), + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 4); + deepStrictEqual(callsArgs, [ + '[21:00:00.000]', + ' INFO', + ': Test message', + '\n', + ]); + }); +}); diff --git a/src/logger/__tests__/transports/github.test.mjs b/src/logger/__tests__/transports/github.test.mjs new file mode 100644 index 00000000..33a7251a --- /dev/null +++ b/src/logger/__tests__/transports/github.test.mjs @@ -0,0 +1,185 @@ +import { deepStrictEqual, strictEqual } from 'assert'; +import { describe, it } from 'node:test'; + +import { LogLevel } from '../../constants.mjs'; +import { github } from '../../transports/github.mjs'; + +describe('github', () => { + it('should print debug messages', t => { + t.mock.timers.enable({ apis: ['Date'] }); + + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + github({ + level: LogLevel.debug, + message: 'Test message', + timestamp: Date.now(), + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 1); + deepStrictEqual(callsArgs, [ + '::notice ::[21:00:00.000] \x1B[34mDEBUG\x1B[39m: Test message\n', + ]); + }); + + it('should print info messages', t => { + t.mock.timers.enable({ apis: ['Date'] }); + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + github({ + level: LogLevel.info, + message: 'Test message', + timestamp: Date.now(), + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 1); + deepStrictEqual(callsArgs, [ + '::notice ::[21:00:00.000] \x1B[32mINFO\x1B[39m: Test message\n', + ]); + }); + + it('should print error messages ', t => { + t.mock.timers.enable({ apis: ['Date'] }); + + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + github({ + level: LogLevel.error, + message: 'Test message', + timestamp: Date.now(), + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 1); + deepStrictEqual(callsArgs, [ + '::notice ::[21:00:00.000] \x1B[35mERROR\x1B[39m: Test message\n', + ]); + }); + + it('should print fatal messages', t => { + t.mock.timers.enable({ apis: ['Date'] }); + + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + github({ + level: LogLevel.fatal, + message: 'Test message', + timestamp: Date.now(), + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 1); + deepStrictEqual(callsArgs, [ + '::notice ::[21:00:00.000] \x1B[31mFATAL\x1B[39m: Test message\n', + ]); + }); + + it('should print messages with file', t => { + t.mock.timers.enable({ apis: ['Date'] }); + + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + github({ + level: LogLevel.info, + message: 'Test message', + metadata: { + file: { + path: 'test.md', + position: { + start: { line: 1 }, + end: { line: 1 }, + }, + }, + }, + timestamp: Date.now(), + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 1); + deepStrictEqual(callsArgs, [ + '::notice file=test.md,line=1,endLine=1::[21:00:00.000] \x1B[32mINFO\x1B[39m: Test message\n', + ]); + }); + + it('should print child logger name', t => { + t.mock.timers.enable({ apis: ['Date'] }); + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + github({ + level: LogLevel.info, + message: 'Test message', + timestamp: Date.now(), + module: 'child1', + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 1); + deepStrictEqual(callsArgs, [ + '::notice ::[21:00:00.000] \x1B[32mINFO\x1B[39m (child1): Test message\n', + ]); + }); + + it('should print without colors if FORCE_COLOR = 0', t => { + process.env.FORCE_COLOR = 0; + + t.mock.timers.enable({ apis: ['Date'] }); + + const fn = t.mock.method(process.stdout, 'write'); + + // noop + fn.mock.mockImplementation(() => {}); + + github({ + level: LogLevel.info, + message: 'Test message', + timestamp: Date.now(), + }); + + const callsArgs = process.stdout.write.mock.calls.map( + call => call.arguments[0] + ); + + strictEqual(process.stdout.write.mock.callCount(), 1); + deepStrictEqual(callsArgs, [ + '::notice ::[21:00:00.000] INFO: Test message\n', + ]); + }); +}); diff --git a/src/logger/constants.mjs b/src/logger/constants.mjs new file mode 100644 index 00000000..07683124 --- /dev/null +++ b/src/logger/constants.mjs @@ -0,0 +1,34 @@ +'use strict'; + +/** + * Numeric log level definitions. + */ +export const LogLevel = { + debug: 10, + info: 20, + warn: 30, + error: 40, + fatal: 50, +}; + +/** + * Maps log level numbers to their string tags. + */ +export const levelTags = { + [LogLevel.debug]: 'DEBUG', + [LogLevel.info]: 'INFO', + [LogLevel.warn]: 'WARN', + [LogLevel.error]: 'ERROR', + [LogLevel.fatal]: 'FATAL', +}; + +/** + * Maps log level numbers to CLI color names. + */ +export const levelToColorMap = { + [LogLevel.debug]: 'blue', + [LogLevel.info]: 'green', + [LogLevel.warn]: 'yellow', + [LogLevel.error]: 'magenta', + [LogLevel.fatal]: 'red', +}; diff --git a/src/logger/index.mjs b/src/logger/index.mjs new file mode 100644 index 00000000..0ac4243e --- /dev/null +++ b/src/logger/index.mjs @@ -0,0 +1,35 @@ +'use strict'; + +import { createLogger } from './logger.mjs'; + +/** + * Singleton logger instance for consistent logging across the app. + */ +export const Logger = (function () { + let instance; + + /** + * Creates a new logger instance. + * + * @returns {Logger} + */ + const createInstance = () => { + return createLogger(); + }; + + /** + * Returns the singleton logger instance. + * + * @returns {Logger} + */ + const getInstance = () => { + if (!instance) { + instance = createInstance(); + } + return instance; + }; + + return { + getInstance, + }; +})(); diff --git a/src/logger/logger.mjs b/src/logger/logger.mjs new file mode 100644 index 00000000..bb6a95d2 --- /dev/null +++ b/src/logger/logger.mjs @@ -0,0 +1,124 @@ +'use strict'; + +import { LogLevel } from './constants.mjs'; + +/** + * Creates a logger instance with the specified transport, log level and an + * optional module name. + * + * @param {import('./types').Transport} transport - Function to handle log output. + * @param {number} [loggerLevel] - Minimum log level to output. + * @param {string} [module] - Optional module name for the logger. + */ +export const createLogger = ( + transport, + loggerLevel = LogLevel.warn, + module +) => { + /** + * Logs a message at the given level with optional metadata. + * + * @param {number} level - Log level for the message. + * @param {string} message - Message to log. + * @param {import('./types').Metadata} metadata - Additional metadata + */ + const log = (level, message, metadata = {}) => { + if (!shouldLog(level)) { + return; + } + + const timestamp = Date.now(); + + transport({ + level, + message, + timestamp, + metadata, + ...(module && { module }), + }); + }; + + /** + * Logs an info message. + * + * @param {string} message - Info message to log. + * @param {import('./types').Metadata} metadata - Additional metadata + * @returns {void} + */ + const info = (message, metadata = {}) => + log(LogLevel.info, message, metadata); + + /** + * Logs a warning message. + * + * @param {string} message - Warning message to log. + * @param {import('./types').Metadata} metadata - Additional metadata + * @returns {void} + */ + const warn = (message, metadata = {}) => + log(LogLevel.warn, message, metadata); + + /** + * Logs an error message or Error object. + * + * @param {string | Error} input - Error message or Error object to log. + * @param {import('./types').Metadata} metadata - Additional metadata + * @returns {void} + */ + const error = (input, metadata = {}) => { + const message = typeof input === 'string' ? input : input.message; + + log(LogLevel.error, message, metadata); + }; + + /** + * Logs a fatal error message or Error object. + * + * @param {string | Error} input - Fatal error message or Error object to log. + * @param {import('./types').Metadata} metadata - Additional metadata + * @returns {void} + */ + const fatal = (input, metadata = {}) => { + const message = typeof input === 'string' ? input : input.message; + + log(LogLevel.fatal, message, metadata); + }; + + /** + * Logs a debug message. + * + * @param {string} message - Debug message to log. + * @param {import('./types').Metadata} metadata - Additional metadata + * @returns {void} + */ + const debug = (message, metadata = {}) => + log(LogLevel.debug, message, metadata); + + /** + * Creates a child logger for a specific module. + * + * @param {string} module - Module name for the child logger. + * @returns {ReturnType} + */ + const child = module => createLogger(transport, loggerLevel, module); + + /** + * Checks if the given log level should be logged based on the current logger + * level. + * + * @param {number} level - Log level to check. + * @returns {boolean} + */ + const shouldLog = level => { + return level >= loggerLevel; + }; + + return { + info, + warn, + error, + fatal, + debug, + child, + }; +}; diff --git a/src/logger/transports/console.mjs b/src/logger/transports/console.mjs new file mode 100644 index 00000000..ca4c0bce --- /dev/null +++ b/src/logger/transports/console.mjs @@ -0,0 +1,46 @@ +'use strict'; + +import { prettifyLevel } from '../utils/colors.mjs'; +import { prettifyTimestamp } from '../utils/time.mjs'; + +/** + * Logs a formatted message to stdout for human-friendly CLI output. + * + * @param {import('../types').TransportContext} context + * @returns {void} + */ +export const console = ({ + level, + message, + timestamp, + metadata = {}, + module, +}) => { + const { file } = metadata; + + const time = prettifyTimestamp(timestamp); + + process.stdout.write(`[${time}]`); + + const prettyLevel = prettifyLevel(level); + + process.stdout.write(` ${prettyLevel}`); + + if (module) { + process.stdout.write(` (${module})`); + } + + process.stdout.write(`: ${message}`); + + if (file) { + process.stdout.write(` at ${file.path}`); + } + + if (file?.position) { + const position = `(${file.position.start.line}:${file.position.end.line})`; + + process.stdout.write(position); + } + + process.stdout.write('\n'); +}; diff --git a/src/logger/transports/github.mjs b/src/logger/transports/github.mjs new file mode 100644 index 00000000..845fdef3 --- /dev/null +++ b/src/logger/transports/github.mjs @@ -0,0 +1,45 @@ +'use strict'; + +import * as core from '@actions/core'; + +import { prettifyLevel } from '../utils/colors.mjs'; +import { prettifyTimestamp } from '../utils/time.mjs'; + +const actions = { + debug: core.debug, + info: core.notice, + warn: core.warning, + error: core.error, + fatal: core.error, +}; + +/** + * Logs messages to GitHub Actions with formatted output and file info with + * appropriate gh actions based on level. + * + * @param {import('../types').TransportContext} context + * @returns {void} + */ +export const github = ({ + level, + message, + timestamp, + metadata = {}, + module, +}) => { + const { file } = metadata; + + const time = prettifyTimestamp(timestamp); + + const prettyLevel = prettifyLevel(level); + + const logMessage = `[${time}] ${prettyLevel}${module ? ` (${module})` : ''}: ${message}`; + + const logFn = actions[level] || core.notice; + + logFn(logMessage, { + file: file?.path, + startLine: file?.position?.start.line, + endLine: file?.position?.end.line, + }); +}; diff --git a/src/logger/types.d.ts b/src/logger/types.d.ts new file mode 100644 index 00000000..19500b47 --- /dev/null +++ b/src/logger/types.d.ts @@ -0,0 +1,25 @@ +export type LogLevel = 'info' | 'warn' | 'error' | 'fatal' | 'trace' | 'debug'; + +export interface Position { + start: { line: number }; + end: { line: number }; +} + +export interface File { + path: string; + position?: Position; +} + +interface Metadata { + file?: File; +} + +interface TransportContext { + level: number; + message: string; + timestamp: number; + metadata?: Metadata; + module?: string; +} + +export type Transport = (context: TransportContext) => void; diff --git a/src/logger/utils/colors.mjs b/src/logger/utils/colors.mjs new file mode 100644 index 00000000..ebdcb165 --- /dev/null +++ b/src/logger/utils/colors.mjs @@ -0,0 +1,18 @@ +'use strict'; + +import { styleText } from 'node:util'; + +import { levelTags, levelToColorMap } from '../constants.mjs'; + +/** + * Returns a styled, uppercase log level tag for CLI output with color mapping + * for better readability. + * + * @param {number} level + * @returns {string} + */ +export const prettifyLevel = level => { + const tag = levelTags[level] ?? String(level); + + return styleText(levelToColorMap[level], tag.toUpperCase()); +}; diff --git a/src/logger/utils/time.mjs b/src/logger/utils/time.mjs new file mode 100644 index 00000000..ef24ddc9 --- /dev/null +++ b/src/logger/utils/time.mjs @@ -0,0 +1,19 @@ +'use strict'; + +/** + * Formats a Unix timestamp in milliseconds as a human-readable time string for + * CLI output. + * + * @param {number} timestamp + * @returns {string} + */ +export const prettifyTimestamp = timestamp => { + const date = new Date(timestamp); + + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const seconds = date.getSeconds().toString().padStart(2, '0'); + const milliseconds = date.getMilliseconds().toString().padStart(3, '0'); + + return `${hours}:${minutes}:${seconds}.${milliseconds}`; +}; From 1da6ac14114d06c6b030dd65651ef98e0bd4200c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 23 Jul 2025 17:39:42 -0300 Subject: [PATCH 02/21] fix: correct return type --- src/logger/index.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logger/index.mjs b/src/logger/index.mjs index 0ac4243e..43279d40 100644 --- a/src/logger/index.mjs +++ b/src/logger/index.mjs @@ -11,7 +11,7 @@ export const Logger = (function () { /** * Creates a new logger instance. * - * @returns {Logger} + * @returns {ReturnType} */ const createInstance = () => { return createLogger(); @@ -20,7 +20,7 @@ export const Logger = (function () { /** * Returns the singleton logger instance. * - * @returns {Logger} + * @returns {ReturnType} */ const getInstance = () => { if (!instance) { From 5c8cef53391f92938fac1d5a0b02a6ad50438682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 23 Jul 2025 17:41:40 -0300 Subject: [PATCH 03/21] feat: remove console calls --- bin/commands/interactive.mjs | 3 ++- bin/utils.mjs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/commands/interactive.mjs b/bin/commands/interactive.mjs index 3c855d00..2cd6b58a 100644 --- a/bin/commands/interactive.mjs +++ b/bin/commands/interactive.mjs @@ -13,6 +13,7 @@ import { } from '@clack/prompts'; import commands from './index.mjs'; +import { Logger } from '../../src/logger/index.mjs'; /** * Validates that a string is not empty. @@ -165,7 +166,7 @@ export default async function interactive() { const finalCommand = cmdParts.join(' '); - console.log(`\nGenerated command:\n${finalCommand}\n`); + Logger.getInstance().info(`\nGenerated command:\n${finalCommand}\n`); // Step 5: Confirm and execute the generated command if (await confirm({ message: 'Run now?', initialValue: true })) { diff --git a/bin/utils.mjs b/bin/utils.mjs index fdb3e70e..37a6f9b7 100644 --- a/bin/utils.mjs +++ b/bin/utils.mjs @@ -1,4 +1,5 @@ import createMarkdownLoader from '../src/loaders/markdown.mjs'; +import { Logger } from '../src/logger/index.mjs'; import createMarkdownParser from '../src/parsers/markdown.mjs'; /** @@ -42,7 +43,7 @@ export const errorWrap = try { return await fn(...args); } catch (err) { - console.error(err); + Logger.getInstance().error(err); process.exit(1); } }; From f5bf533bf3cf85491d1fda38d6de6f462ced139f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 23 Jul 2025 18:14:56 -0300 Subject: [PATCH 04/21] feat: replace linter reporters to transports --- src/logger/index.mjs | 40 ++++++++++++++++++++++++--------- src/logger/transports/index.mjs | 11 +++++++++ 2 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 src/logger/transports/index.mjs diff --git a/src/logger/index.mjs b/src/logger/index.mjs index 43279d40..0b9e1c4e 100644 --- a/src/logger/index.mjs +++ b/src/logger/index.mjs @@ -1,35 +1,55 @@ +// @ts-check + 'use strict'; import { createLogger } from './logger.mjs'; +import { transports } from './transports/index.mjs'; + +/** + * @typedef {ReturnType} LoggerInstance + */ /** * Singleton logger instance for consistent logging across the app. */ -export const Logger = (function () { +export const Logger = (() => { + /** + * @type {LoggerInstance} + */ let instance; /** - * Creates a new logger instance. * - * @returns {ReturnType} + * @param {string} [transportName] + * @returns {void} */ - const createInstance = () => { - return createLogger(); - }; + function init(transportName = 'console') { + const transport = transports[transportName]; + + if (!transport) { + throw new Error(`Transport '${transportName}' not found.`); + } + + instance = createLogger(transport); + } /** * Returns the singleton logger instance. * - * @returns {ReturnType} + * @returns {LoggerInstance} */ - const getInstance = () => { + function getInstance() { if (!instance) { - instance = createInstance(); + throw new Error( + 'Logger not initialized. Call Logger.init(transportName) first.' + ); } + return instance; - }; + } return { + init, getInstance, }; })(); diff --git a/src/logger/transports/index.mjs b/src/logger/transports/index.mjs new file mode 100644 index 00000000..eb4c7042 --- /dev/null +++ b/src/logger/transports/index.mjs @@ -0,0 +1,11 @@ +'use strict'; + +import { console } from './console.mjs'; +import { github } from './github.mjs'; + +export const transports = { + console, + github, +}; + +export const availableTransports = Object.keys(transports); From 40cf9f58b8e21c6b9aebf6827f49f86789c41f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 23 Jul 2025 18:15:18 -0300 Subject: [PATCH 05/21] feat: change default logger level --- src/logger/logger.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger/logger.mjs b/src/logger/logger.mjs index bb6a95d2..64f470ee 100644 --- a/src/logger/logger.mjs +++ b/src/logger/logger.mjs @@ -12,7 +12,7 @@ import { LogLevel } from './constants.mjs'; */ export const createLogger = ( transport, - loggerLevel = LogLevel.warn, + loggerLevel = LogLevel.info, module ) => { /** From ee2f8306ae876792bdbe80d968a7cc3f355cf0db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 23 Jul 2025 18:51:07 -0300 Subject: [PATCH 06/21] fix: init logger on list command --- .tool-versions | 1 + 1 file changed, 1 insertion(+) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..71c0000b --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 22.17.1 From b29a9a33a7131493ccfb98de7878923874389780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 23 Jul 2025 19:06:04 -0300 Subject: [PATCH 07/21] feat: add array logging --- src/logger/__tests__/logger.test.mjs | 32 ++++++++++++++++++ src/logger/logger.mjs | 49 ++++++++++++++++------------ src/logger/types.d.ts | 2 ++ 3 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/logger/__tests__/logger.test.mjs b/src/logger/__tests__/logger.test.mjs index aa12634a..9f4b245f 100644 --- a/src/logger/__tests__/logger.test.mjs +++ b/src/logger/__tests__/logger.test.mjs @@ -181,4 +181,36 @@ describe('createLogger', () => { strictEqual(transport.mock.callCount(), 0); }); + + it('should log all messages if message is a string array', t => { + const transport = t.mock.fn(); + + const logger = createLogger(transport, LogLevel.info); + + logger.info(['Hello, 1!', 'Hello, 2!', 'Hello, 3!']); + + strictEqual(transport.mock.callCount(), 3); + }); + + it.only('should log error message', t => { + t.mock.timers.enable({ apis: ['Date'] }); + + const transport = t.mock.fn(); + + const logger = createLogger(transport, LogLevel.error); + + logger.error(new Error('Hello, World!')); + + strictEqual(transport.mock.callCount(), 1); + + const call = transport.mock.calls[0]; + deepStrictEqual(call.arguments, [ + { + level: LogLevel.error, + message: 'Hello, World!', + metadata: {}, + timestamp: 0, + }, + ]); + }); }); diff --git a/src/logger/logger.mjs b/src/logger/logger.mjs index 64f470ee..9c67c87a 100644 --- a/src/logger/logger.mjs +++ b/src/logger/logger.mjs @@ -2,6 +2,11 @@ import { LogLevel } from './constants.mjs'; +/** + * @typedef {import('./types').Metadata} Metadata + * @typedef {import('./types').LogMessage} LogMessage + */ + /** * Creates a logger instance with the specified transport, log level and an * optional module name. @@ -19,19 +24,27 @@ export const createLogger = ( * Logs a message at the given level with optional metadata. * * @param {number} level - Log level for the message. - * @param {string} message - Message to log. - * @param {import('./types').Metadata} metadata - Additional metadata + * @param {LogMessage} message - Message to log. + * @param {Metadata} metadata - Additional metadata + * @returns {void} */ const log = (level, message, metadata = {}) => { if (!shouldLog(level)) { return; } + if (Array.isArray(message)) { + return message.forEach(msg => log(level, msg, metadata)); + } + const timestamp = Date.now(); + // Extract message string from Error object or use message as-is + const msg = message instanceof Error ? message.message : message; + transport({ level, - message, + message: msg, timestamp, metadata, ...(module && { module }), @@ -41,8 +54,8 @@ export const createLogger = ( /** * Logs an info message. * - * @param {string} message - Info message to log. - * @param {import('./types').Metadata} metadata - Additional metadata + * @param {LogMessage} message - Info message to log. + * @param {Metadata} metadata - Additional metadata * @returns {void} */ const info = (message, metadata = {}) => @@ -51,8 +64,8 @@ export const createLogger = ( /** * Logs a warning message. * - * @param {string} message - Warning message to log. - * @param {import('./types').Metadata} metadata - Additional metadata + * @param {LogMessage} message - Warning message to log. + * @param {Metadata} metadata - Additional metadata * @returns {void} */ const warn = (message, metadata = {}) => @@ -61,34 +74,28 @@ export const createLogger = ( /** * Logs an error message or Error object. * - * @param {string | Error} input - Error message or Error object to log. - * @param {import('./types').Metadata} metadata - Additional metadata + * @param {LogMessage} message - Error message or Error object to log. + * @param {Metadata} metadata - Additional metadata * @returns {void} */ - const error = (input, metadata = {}) => { - const message = typeof input === 'string' ? input : input.message; - + const error = (message, metadata = {}) => log(LogLevel.error, message, metadata); - }; /** * Logs a fatal error message or Error object. * - * @param {string | Error} input - Fatal error message or Error object to log. - * @param {import('./types').Metadata} metadata - Additional metadata + * @param {LogMessage} message - Fatal error message or Error object to log. + * @param {Metadata} metadata - Additional metadata * @returns {void} */ - const fatal = (input, metadata = {}) => { - const message = typeof input === 'string' ? input : input.message; - + const fatal = (message, metadata = {}) => log(LogLevel.fatal, message, metadata); - }; /** * Logs a debug message. * - * @param {string} message - Debug message to log. - * @param {import('./types').Metadata} metadata - Additional metadata + * @param {LogMessage} message - Debug message to log. + * @param {Metadata} metadata - Additional metadata * @returns {void} */ const debug = (message, metadata = {}) => diff --git a/src/logger/types.d.ts b/src/logger/types.d.ts index 19500b47..3e7fc9c1 100644 --- a/src/logger/types.d.ts +++ b/src/logger/types.d.ts @@ -1,5 +1,7 @@ export type LogLevel = 'info' | 'warn' | 'error' | 'fatal' | 'trace' | 'debug'; +export type LogMessage = string | Error | string[]; + export interface Position { start: { line: number }; end: { line: number }; From 327f0ae1ecee11a0ae052a6dedf0968f3f6d3e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 23 Jul 2025 19:08:51 -0300 Subject: [PATCH 08/21] test: fix logger init --- src/logger/__tests__/index.test.mjs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/logger/__tests__/index.test.mjs b/src/logger/__tests__/index.test.mjs index 939022a4..b18fa23b 100644 --- a/src/logger/__tests__/index.test.mjs +++ b/src/logger/__tests__/index.test.mjs @@ -1,19 +1,15 @@ -import { ok, strictEqual } from 'node:assert'; +import { strictEqual } from 'node:assert'; import { describe, it } from 'node:test'; import { Logger } from '../index.mjs'; describe('Logger singleton', () => { it('should return the same instance on multiple calls', () => { + Logger.init('console'); + const logger1 = Logger.getInstance(); const logger2 = Logger.getInstance(); strictEqual(logger1, logger2); }); - - it('should create an instance of logger', () => { - const logger = Logger.getInstance(); - - ok(logger); - }); }); From 7bee26b3e2552ddb460d080465860d531b95d0e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 23 Jul 2025 19:09:11 -0300 Subject: [PATCH 09/21] chore: remove .tool-versions --- .tool-versions | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index 71c0000b..00000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -nodejs 22.17.1 From fd3039ff52551d6a58d675012897e1838e23d7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 23 Jul 2025 19:11:50 -0300 Subject: [PATCH 10/21] fix: missing logger init on generate command --- src/logger/index.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/logger/index.mjs b/src/logger/index.mjs index 0b9e1c4e..62d69762 100644 --- a/src/logger/index.mjs +++ b/src/logger/index.mjs @@ -19,9 +19,10 @@ export const Logger = (() => { let instance; /** + * Inits the logger with the specified transport. * * @param {string} [transportName] - * @returns {void} + * @returns {LoggerInstance} */ function init(transportName = 'console') { const transport = transports[transportName]; @@ -31,6 +32,7 @@ export const Logger = (() => { } instance = createLogger(transport); + return instance; } /** From 4ed8c8917b52d602909d22a0675f2434213e1cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 23 Jul 2025 19:51:54 -0300 Subject: [PATCH 11/21] fix: github actions fn mapping --- src/logger/__tests__/transports/github.test.mjs | 6 +++--- src/logger/transports/github.mjs | 13 +++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/logger/__tests__/transports/github.test.mjs b/src/logger/__tests__/transports/github.test.mjs index 33a7251a..87134139 100644 --- a/src/logger/__tests__/transports/github.test.mjs +++ b/src/logger/__tests__/transports/github.test.mjs @@ -25,7 +25,7 @@ describe('github', () => { strictEqual(process.stdout.write.mock.callCount(), 1); deepStrictEqual(callsArgs, [ - '::notice ::[21:00:00.000] \x1B[34mDEBUG\x1B[39m: Test message\n', + '::debug::[21:00:00.000] \x1B[34mDEBUG\x1B[39m: Test message\n', ]); }); @@ -72,7 +72,7 @@ describe('github', () => { strictEqual(process.stdout.write.mock.callCount(), 1); deepStrictEqual(callsArgs, [ - '::notice ::[21:00:00.000] \x1B[35mERROR\x1B[39m: Test message\n', + '::error ::[21:00:00.000] \x1B[35mERROR\x1B[39m: Test message\n', ]); }); @@ -96,7 +96,7 @@ describe('github', () => { strictEqual(process.stdout.write.mock.callCount(), 1); deepStrictEqual(callsArgs, [ - '::notice ::[21:00:00.000] \x1B[31mFATAL\x1B[39m: Test message\n', + '::error ::[21:00:00.000] \x1B[31mFATAL\x1B[39m: Test message\n', ]); }); diff --git a/src/logger/transports/github.mjs b/src/logger/transports/github.mjs index 845fdef3..e8d65769 100644 --- a/src/logger/transports/github.mjs +++ b/src/logger/transports/github.mjs @@ -2,15 +2,16 @@ import * as core from '@actions/core'; +import { LogLevel } from '../constants.mjs'; import { prettifyLevel } from '../utils/colors.mjs'; import { prettifyTimestamp } from '../utils/time.mjs'; const actions = { - debug: core.debug, - info: core.notice, - warn: core.warning, - error: core.error, - fatal: core.error, + [LogLevel.debug]: core.debug, + [LogLevel.info]: core.notice, + [LogLevel.warn]: core.warning, + [LogLevel.error]: core.error, + [LogLevel.fatal]: core.error, }; /** @@ -35,7 +36,7 @@ export const github = ({ const logMessage = `[${time}] ${prettyLevel}${module ? ` (${module})` : ''}: ${message}`; - const logFn = actions[level] || core.notice; + const logFn = actions[level] ?? core.notice; logFn(logMessage, { file: file?.path, From 1bb3b008e82a71a4ede0ecdbfd5b3ad40f509d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 23 Jul 2025 19:57:12 -0300 Subject: [PATCH 12/21] fix: use utc timezone --- src/logger/__tests__/transports/console.test.mjs | 14 +++++++------- src/logger/__tests__/transports/github.test.mjs | 14 +++++++------- src/logger/utils/time.mjs | 12 ++++++------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/logger/__tests__/transports/console.test.mjs b/src/logger/__tests__/transports/console.test.mjs index d7f4b070..57700fc8 100644 --- a/src/logger/__tests__/transports/console.test.mjs +++ b/src/logger/__tests__/transports/console.test.mjs @@ -25,7 +25,7 @@ describe('console', () => { strictEqual(process.stdout.write.mock.callCount(), 4); deepStrictEqual(callsArgs, [ - '[21:00:00.000]', + '[00:00:00.000]', ' \x1B[34mDEBUG\x1B[39m', ': Test message', '\n', @@ -51,7 +51,7 @@ describe('console', () => { strictEqual(process.stdout.write.mock.callCount(), 4); deepStrictEqual(callsArgs, [ - '[21:00:00.000]', + '[00:00:00.000]', ' \x1B[32mINFO\x1B[39m', ': Test message', '\n', @@ -78,7 +78,7 @@ describe('console', () => { strictEqual(process.stdout.write.mock.callCount(), 4); deepStrictEqual(callsArgs, [ - '[21:00:00.000]', + '[00:00:00.000]', ' \x1B[35mERROR\x1B[39m', ': Test message', '\n', @@ -105,7 +105,7 @@ describe('console', () => { strictEqual(process.stdout.write.mock.callCount(), 4); deepStrictEqual(callsArgs, [ - '[21:00:00.000]', + '[00:00:00.000]', ' \x1B[31mFATAL\x1B[39m', ': Test message', '\n', @@ -141,7 +141,7 @@ describe('console', () => { strictEqual(process.stdout.write.mock.callCount(), 6); deepStrictEqual(callsArgs, [ - '[21:00:00.000]', + '[00:00:00.000]', ' \x1B[32mINFO\x1B[39m', ': Test message', ' at test.md', @@ -171,7 +171,7 @@ describe('console', () => { strictEqual(process.stdout.write.mock.callCount(), 5); deepStrictEqual(callsArgs, [ - '[21:00:00.000]', + '[00:00:00.000]', ' \x1B[32mINFO\x1B[39m', ' (child1)', ': Test message', @@ -201,7 +201,7 @@ describe('console', () => { strictEqual(process.stdout.write.mock.callCount(), 4); deepStrictEqual(callsArgs, [ - '[21:00:00.000]', + '[00:00:00.000]', ' INFO', ': Test message', '\n', diff --git a/src/logger/__tests__/transports/github.test.mjs b/src/logger/__tests__/transports/github.test.mjs index 87134139..bbbeda1e 100644 --- a/src/logger/__tests__/transports/github.test.mjs +++ b/src/logger/__tests__/transports/github.test.mjs @@ -25,7 +25,7 @@ describe('github', () => { strictEqual(process.stdout.write.mock.callCount(), 1); deepStrictEqual(callsArgs, [ - '::debug::[21:00:00.000] \x1B[34mDEBUG\x1B[39m: Test message\n', + '::debug::[00:00:00.000] \x1B[34mDEBUG\x1B[39m: Test message\n', ]); }); @@ -48,7 +48,7 @@ describe('github', () => { strictEqual(process.stdout.write.mock.callCount(), 1); deepStrictEqual(callsArgs, [ - '::notice ::[21:00:00.000] \x1B[32mINFO\x1B[39m: Test message\n', + '::notice ::[00:00:00.000] \x1B[32mINFO\x1B[39m: Test message\n', ]); }); @@ -72,7 +72,7 @@ describe('github', () => { strictEqual(process.stdout.write.mock.callCount(), 1); deepStrictEqual(callsArgs, [ - '::error ::[21:00:00.000] \x1B[35mERROR\x1B[39m: Test message\n', + '::error ::[00:00:00.000] \x1B[35mERROR\x1B[39m: Test message\n', ]); }); @@ -96,7 +96,7 @@ describe('github', () => { strictEqual(process.stdout.write.mock.callCount(), 1); deepStrictEqual(callsArgs, [ - '::error ::[21:00:00.000] \x1B[31mFATAL\x1B[39m: Test message\n', + '::error ::[00:00:00.000] \x1B[31mFATAL\x1B[39m: Test message\n', ]); }); @@ -129,7 +129,7 @@ describe('github', () => { strictEqual(process.stdout.write.mock.callCount(), 1); deepStrictEqual(callsArgs, [ - '::notice file=test.md,line=1,endLine=1::[21:00:00.000] \x1B[32mINFO\x1B[39m: Test message\n', + '::notice file=test.md,line=1,endLine=1::[00:00:00.000] \x1B[32mINFO\x1B[39m: Test message\n', ]); }); @@ -153,7 +153,7 @@ describe('github', () => { strictEqual(process.stdout.write.mock.callCount(), 1); deepStrictEqual(callsArgs, [ - '::notice ::[21:00:00.000] \x1B[32mINFO\x1B[39m (child1): Test message\n', + '::notice ::[00:00:00.000] \x1B[32mINFO\x1B[39m (child1): Test message\n', ]); }); @@ -179,7 +179,7 @@ describe('github', () => { strictEqual(process.stdout.write.mock.callCount(), 1); deepStrictEqual(callsArgs, [ - '::notice ::[21:00:00.000] INFO: Test message\n', + '::notice ::[00:00:00.000] INFO: Test message\n', ]); }); }); diff --git a/src/logger/utils/time.mjs b/src/logger/utils/time.mjs index ef24ddc9..dbbed853 100644 --- a/src/logger/utils/time.mjs +++ b/src/logger/utils/time.mjs @@ -1,8 +1,8 @@ 'use strict'; /** - * Formats a Unix timestamp in milliseconds as a human-readable time string for - * CLI output. + * Formats a Unix timestamp in milliseconds as a human-readable time string + * in UTC timezone for CLI output. * * @param {number} timestamp * @returns {string} @@ -10,10 +10,10 @@ export const prettifyTimestamp = timestamp => { const date = new Date(timestamp); - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - const seconds = date.getSeconds().toString().padStart(2, '0'); - const milliseconds = date.getMilliseconds().toString().padStart(3, '0'); + const hours = date.getUTCHours().toString().padStart(2, '0'); + const minutes = date.getUTCMinutes().toString().padStart(2, '0'); + const seconds = date.getUTCSeconds().toString().padStart(2, '0'); + const milliseconds = date.getUTCMilliseconds().toString().padStart(3, '0'); return `${hours}:${minutes}:${seconds}.${milliseconds}`; }; From 086b9510ed8fa0c1f0cfc1ec990b98bcbb4ce96a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 23 Jul 2025 19:58:00 -0300 Subject: [PATCH 13/21] test: remove it.only --- src/logger/__tests__/logger.test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger/__tests__/logger.test.mjs b/src/logger/__tests__/logger.test.mjs index 9f4b245f..7d66f812 100644 --- a/src/logger/__tests__/logger.test.mjs +++ b/src/logger/__tests__/logger.test.mjs @@ -192,7 +192,7 @@ describe('createLogger', () => { strictEqual(transport.mock.callCount(), 3); }); - it.only('should log error message', t => { + it('should log error message', t => { t.mock.timers.enable({ apis: ['Date'] }); const transport = t.mock.fn(); From a3a10b5cf098a8e89caa07732f9e61dfcad241bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Fri, 25 Jul 2025 17:38:39 -0300 Subject: [PATCH 14/21] refactor: use default exports --- src/logger/transports/console.mjs | 10 +++------- src/logger/transports/github.mjs | 10 +++------- src/logger/transports/index.mjs | 4 ++-- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/logger/transports/console.mjs b/src/logger/transports/console.mjs index ca4c0bce..325b2a3c 100644 --- a/src/logger/transports/console.mjs +++ b/src/logger/transports/console.mjs @@ -9,13 +9,7 @@ import { prettifyTimestamp } from '../utils/time.mjs'; * @param {import('../types').TransportContext} context * @returns {void} */ -export const console = ({ - level, - message, - timestamp, - metadata = {}, - module, -}) => { +const console = ({ level, message, timestamp, metadata = {}, module }) => { const { file } = metadata; const time = prettifyTimestamp(timestamp); @@ -44,3 +38,5 @@ export const console = ({ process.stdout.write('\n'); }; + +export default console; diff --git a/src/logger/transports/github.mjs b/src/logger/transports/github.mjs index e8d65769..1922be5e 100644 --- a/src/logger/transports/github.mjs +++ b/src/logger/transports/github.mjs @@ -21,13 +21,7 @@ const actions = { * @param {import('../types').TransportContext} context * @returns {void} */ -export const github = ({ - level, - message, - timestamp, - metadata = {}, - module, -}) => { +const github = ({ level, message, timestamp, metadata = {}, module }) => { const { file } = metadata; const time = prettifyTimestamp(timestamp); @@ -44,3 +38,5 @@ export const github = ({ endLine: file?.position?.end.line, }); }; + +export default github; diff --git a/src/logger/transports/index.mjs b/src/logger/transports/index.mjs index eb4c7042..1adcfd70 100644 --- a/src/logger/transports/index.mjs +++ b/src/logger/transports/index.mjs @@ -1,7 +1,7 @@ 'use strict'; -import { console } from './console.mjs'; -import { github } from './github.mjs'; +import console from './console.mjs'; +import github from './github.mjs'; export const transports = { console, From fc29c54bdf99d62f305f6b79dad52d5f1c970f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Fri, 25 Jul 2025 17:41:34 -0300 Subject: [PATCH 15/21] refactor: remove namespace gh import --- src/logger/transports/github.mjs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/logger/transports/github.mjs b/src/logger/transports/github.mjs index 1922be5e..8b5fb512 100644 --- a/src/logger/transports/github.mjs +++ b/src/logger/transports/github.mjs @@ -1,17 +1,17 @@ 'use strict'; -import * as core from '@actions/core'; +import { debug, notice, warning, error } from '@actions/core'; import { LogLevel } from '../constants.mjs'; import { prettifyLevel } from '../utils/colors.mjs'; import { prettifyTimestamp } from '../utils/time.mjs'; const actions = { - [LogLevel.debug]: core.debug, - [LogLevel.info]: core.notice, - [LogLevel.warn]: core.warning, - [LogLevel.error]: core.error, - [LogLevel.fatal]: core.error, + [LogLevel.debug]: debug, + [LogLevel.info]: notice, + [LogLevel.warn]: warning, + [LogLevel.error]: error, + [LogLevel.fatal]: error, }; /** @@ -30,7 +30,7 @@ const github = ({ level, message, timestamp, metadata = {}, module }) => { const logMessage = `[${time}] ${prettyLevel}${module ? ` (${module})` : ''}: ${message}`; - const logFn = actions[level] ?? core.notice; + const logFn = actions[level] ?? notice; logFn(logMessage, { file: file?.path, From 806803de11d05c9b8395ca06e7d3e5958c0aefc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Fri, 25 Jul 2025 17:42:38 -0300 Subject: [PATCH 16/21] refactor: remove @ts-check --- src/logger/index.mjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/logger/index.mjs b/src/logger/index.mjs index 62d69762..44249d72 100644 --- a/src/logger/index.mjs +++ b/src/logger/index.mjs @@ -1,5 +1,3 @@ -// @ts-check - 'use strict'; import { createLogger } from './logger.mjs'; From 1df5c546824ec61fa65c2f9c8a27877c1317240d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Fri, 25 Jul 2025 17:45:30 -0300 Subject: [PATCH 17/21] refactor: remove module check --- src/logger/__tests__/logger.test.mjs | 5 +++++ src/logger/logger.mjs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/logger/__tests__/logger.test.mjs b/src/logger/__tests__/logger.test.mjs index 7d66f812..51685af8 100644 --- a/src/logger/__tests__/logger.test.mjs +++ b/src/logger/__tests__/logger.test.mjs @@ -36,6 +36,7 @@ describe('createLogger', () => { level: LogLevel.debug, message: 'Hello, World!', metadata, + module: undefined, timestamp: 0, }, ]); @@ -74,6 +75,7 @@ describe('createLogger', () => { level: LogLevel.info, message: 'Hello, World!', metadata, + module: undefined, timestamp: 0, }, ]); @@ -112,6 +114,7 @@ describe('createLogger', () => { level: LogLevel.warn, message: 'Hello, World!', metadata, + module: undefined, timestamp: 0, }, ]); @@ -151,6 +154,7 @@ describe('createLogger', () => { level: LogLevel.error, message: 'Hello, World!', metadata, + module: undefined, timestamp: 0, }, ]); @@ -209,6 +213,7 @@ describe('createLogger', () => { level: LogLevel.error, message: 'Hello, World!', metadata: {}, + module: undefined, timestamp: 0, }, ]); diff --git a/src/logger/logger.mjs b/src/logger/logger.mjs index 9c67c87a..2277f4f7 100644 --- a/src/logger/logger.mjs +++ b/src/logger/logger.mjs @@ -47,7 +47,7 @@ export const createLogger = ( message: msg, timestamp, metadata, - ...(module && { module }), + module, }); }; From c9d92a543a69c4bd705e9a16661100400179f383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Fri, 25 Jul 2025 17:46:07 -0300 Subject: [PATCH 18/21] test: fix imports --- src/logger/__tests__/transports/console.test.mjs | 2 +- src/logger/__tests__/transports/github.test.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logger/__tests__/transports/console.test.mjs b/src/logger/__tests__/transports/console.test.mjs index 57700fc8..71d1268b 100644 --- a/src/logger/__tests__/transports/console.test.mjs +++ b/src/logger/__tests__/transports/console.test.mjs @@ -2,7 +2,7 @@ import { deepStrictEqual, strictEqual } from 'assert'; import { describe, it } from 'node:test'; import { LogLevel } from '../../constants.mjs'; -import { console } from '../../transports/console.mjs'; +import console from '../../transports/console.mjs'; describe('console', () => { it('should print debug messages', t => { diff --git a/src/logger/__tests__/transports/github.test.mjs b/src/logger/__tests__/transports/github.test.mjs index bbbeda1e..3ee156f8 100644 --- a/src/logger/__tests__/transports/github.test.mjs +++ b/src/logger/__tests__/transports/github.test.mjs @@ -2,7 +2,7 @@ import { deepStrictEqual, strictEqual } from 'assert'; import { describe, it } from 'node:test'; import { LogLevel } from '../../constants.mjs'; -import { github } from '../../transports/github.mjs'; +import github from '../../transports/github.mjs'; describe('github', () => { it('should print debug messages', t => { From 7830dc70248dc05456cc1049cffd0c9a085f49c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 18 Aug 2025 18:03:21 -0300 Subject: [PATCH 19/21] refactor: use datetimeformat --- src/logger/utils/time.mjs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/logger/utils/time.mjs b/src/logger/utils/time.mjs index dbbed853..a35291ab 100644 --- a/src/logger/utils/time.mjs +++ b/src/logger/utils/time.mjs @@ -10,10 +10,12 @@ export const prettifyTimestamp = timestamp => { const date = new Date(timestamp); - const hours = date.getUTCHours().toString().padStart(2, '0'); - const minutes = date.getUTCMinutes().toString().padStart(2, '0'); - const seconds = date.getUTCSeconds().toString().padStart(2, '0'); - const milliseconds = date.getUTCMilliseconds().toString().padStart(3, '0'); - - return `${hours}:${minutes}:${seconds}.${milliseconds}`; + return new Intl.DateTimeFormat('en-US', { + timeZone: 'UTC', + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 3, + }).format(date); }; From 2e7f020be8a854d9530f25f18e1a30b08a0c36a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Mon, 18 Aug 2025 18:26:54 -0300 Subject: [PATCH 20/21] refactor: add default logger --- src/logger/index.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/logger/index.mjs b/src/logger/index.mjs index 44249d72..beb53a03 100644 --- a/src/logger/index.mjs +++ b/src/logger/index.mjs @@ -53,3 +53,5 @@ export const Logger = (() => { getInstance, }; })(); + +export default Logger.init(); From c29dd1a3f27638a95c28d9cc7343f496e94fb5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Wed, 27 Aug 2025 18:28:47 -0300 Subject: [PATCH 21/21] refactor: delete singleton --- src/logger/__tests__/index.test.mjs | 15 -------- src/logger/index.mjs | 54 +++++++---------------------- 2 files changed, 12 insertions(+), 57 deletions(-) delete mode 100644 src/logger/__tests__/index.test.mjs diff --git a/src/logger/__tests__/index.test.mjs b/src/logger/__tests__/index.test.mjs deleted file mode 100644 index b18fa23b..00000000 --- a/src/logger/__tests__/index.test.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import { strictEqual } from 'node:assert'; -import { describe, it } from 'node:test'; - -import { Logger } from '../index.mjs'; - -describe('Logger singleton', () => { - it('should return the same instance on multiple calls', () => { - Logger.init('console'); - - const logger1 = Logger.getInstance(); - const logger2 = Logger.getInstance(); - - strictEqual(logger1, logger2); - }); -}); diff --git a/src/logger/index.mjs b/src/logger/index.mjs index beb53a03..4b2ac5b4 100644 --- a/src/logger/index.mjs +++ b/src/logger/index.mjs @@ -8,50 +8,20 @@ import { transports } from './transports/index.mjs'; */ /** - * Singleton logger instance for consistent logging across the app. + * Creates a new logger instance with the specified transport. + * + * @param {string} [transportName='console'] - Name of the transport to use. + * @returns {LoggerInstance} */ -export const Logger = (() => { - /** - * @type {LoggerInstance} - */ - let instance; +export const Logger = (transportName = 'console') => { + const transport = transports[transportName]; - /** - * Inits the logger with the specified transport. - * - * @param {string} [transportName] - * @returns {LoggerInstance} - */ - function init(transportName = 'console') { - const transport = transports[transportName]; - - if (!transport) { - throw new Error(`Transport '${transportName}' not found.`); - } - - instance = createLogger(transport); - return instance; - } - - /** - * Returns the singleton logger instance. - * - * @returns {LoggerInstance} - */ - function getInstance() { - if (!instance) { - throw new Error( - 'Logger not initialized. Call Logger.init(transportName) first.' - ); - } - - return instance; + if (!transport) { + throw new Error(`Transport '${transportName}' not found.`); } - return { - init, - getInstance, - }; -})(); + return createLogger(transport); +}; -export default Logger.init(); +// Default logger instance using console transport +export default Logger();