@@ -2,114 +2,202 @@ import { Writable } from 'stream';
22import * as util from 'util' ;
33import * as chalk from 'chalk' ;
44
5- type StyleFn = ( str : string ) => string ;
6- const { stdout, stderr } = process ;
7-
8- type WritableFactory = ( ) => Writable ;
5+ /**
6+ * Available log levels in order of increasing verbosity.
7+ */
8+ export enum LogLevel {
9+ ERROR = 'error' ,
10+ WARN = 'warn' ,
11+ INFO = 'info' ,
12+ DEBUG = 'debug' ,
13+ TRACE = 'trace' ,
14+ }
915
10- export async function withCorkedLogging < A > ( block : ( ) => Promise < A > ) : Promise < A > {
11- corkLogging ( ) ;
12- try {
13- return await block ( ) ;
14- } finally {
15- uncorkLogging ( ) ;
16- }
16+ /**
17+ * Configuration options for a log entry.
18+ */
19+ export interface LogEntry {
20+ level : LogLevel ;
21+ message : string ;
22+ timestamp ?: boolean ;
23+ prefix ?: string ;
24+ style ?: ( ( str : string ) => string ) ;
25+ forceStdout ?: boolean ;
1726}
1827
28+ const { stdout, stderr } = process ;
29+
30+ // Corking mechanism
1931let CORK_COUNTER = 0 ;
2032const logBuffer : [ Writable , string ] [ ] = [ ] ;
2133
22- function corked ( ) {
23- return CORK_COUNTER !== 0 ;
24- }
34+ // Style mappings
35+ const styleMap : Record < LogLevel , ( str : string ) => string > = {
36+ [ LogLevel . ERROR ] : chalk . red ,
37+ [ LogLevel . WARN ] : chalk . yellow ,
38+ [ LogLevel . INFO ] : chalk . white ,
39+ [ LogLevel . DEBUG ] : chalk . gray ,
40+ [ LogLevel . TRACE ] : chalk . gray ,
41+ } ;
2542
26- function corkLogging ( ) {
27- CORK_COUNTER += 1 ;
28- }
43+ // Stream selection
44+ let CI = false ;
2945
30- function uncorkLogging ( ) {
31- CORK_COUNTER -= 1 ;
32- if ( ! corked ( ) ) {
33- logBuffer . forEach ( ( [ stream , str ] ) => stream . write ( str + '\n' ) ) ;
34- logBuffer . splice ( 0 ) ;
46+ /**
47+ * Determines which output stream to use based on log level and configuration.
48+ * @param level - The log level to determine stream for
49+ * @param forceStdout - Whether to force stdout regardless of level
50+ * @returns The appropriate Writable stream
51+ */
52+ const getStream = ( level : LogLevel , forceStdout ?: boolean ) : Writable => {
53+ // Special case - data() calls should always go to stdout
54+ if ( forceStdout ) {
55+ return stdout ;
3556 }
57+ if ( level === LogLevel . ERROR ) return stderr ;
58+ return CI ? stdout : stderr ;
59+ } ;
60+
61+ const levelPriority : Record < LogLevel , number > = {
62+ [ LogLevel . ERROR ] : 0 ,
63+ [ LogLevel . WARN ] : 1 ,
64+ [ LogLevel . INFO ] : 2 ,
65+ [ LogLevel . DEBUG ] : 3 ,
66+ [ LogLevel . TRACE ] : 4 ,
67+ } ;
68+
69+ let currentLogLevel : LogLevel = LogLevel . INFO ;
70+
71+ /**
72+ * Sets the current log level. Messages with a lower priority level will be filtered out.
73+ * @param level - The new log level to set
74+ */
75+ export function setLogLevel ( level : LogLevel ) {
76+ currentLogLevel = level ;
3677}
3778
38- const logger = ( stream : Writable | WritableFactory , styles ?: StyleFn [ ] , timestamp ?: boolean ) => ( fmt : string , ...args : unknown [ ] ) => {
39- const ts = timestamp ? `[${ formatTime ( new Date ( ) ) } ] ` : '' ;
79+ /**
80+ * Sets whether the logger is running in CI mode.
81+ * In CI mode, all non-error output goes to stdout instead of stderr.
82+ * @param newCI - Whether CI mode should be enabled
83+ */
84+ export function setCI ( newCI : boolean ) {
85+ CI = newCI ;
86+ }
4087
41- let str = ts + util . format ( fmt , ...args ) ;
42- if ( styles && styles . length ) {
43- str = styles . reduce ( ( a , style ) => style ( a ) , str ) ;
44- }
88+ /**
89+ * Formats a date object into a timestamp string (HH:MM:SS).
90+ * @param d - Date object to format
91+ * @returns Formatted time string
92+ */
93+ function formatTime ( d : Date ) : string {
94+ const pad = ( n : number ) : string => n . toString ( ) . padStart ( 2 , '0' ) ;
95+ return `${ pad ( d . getHours ( ) ) } :${ pad ( d . getMinutes ( ) ) } :${ pad ( d . getSeconds ( ) ) } ` ;
96+ }
4597
46- const realStream = typeof stream === 'function' ? stream ( ) : stream ;
98+ /**
99+ * Executes a block of code with corked logging. All log messages during execution
100+ * are buffered and only written after the block completes.
101+ * @param block - Async function to execute with corked logging
102+ * @returns Promise that resolves with the block's return value
103+ */
104+ export async function withCorkedLogging < T > ( block : ( ) => Promise < T > ) : Promise < T > {
105+ CORK_COUNTER ++ ;
106+ try {
107+ return await block ( ) ;
108+ } finally {
109+ CORK_COUNTER -- ;
110+ if ( CORK_COUNTER === 0 ) {
111+ logBuffer . forEach ( ( [ stream , str ] ) => stream . write ( str + '\n' ) ) ;
112+ logBuffer . splice ( 0 ) ;
113+ }
114+ }
115+ }
47116
48- // Logger is currently corked, so we store the message to be printed
49- // later when we are uncorked.
50- if ( corked ( ) ) {
51- logBuffer . push ( [ realStream , str ] ) ;
117+ /**
118+ * Core logging function that handles all log output.
119+ * @param entry - LogEntry object or log level
120+ * @param fmt - Format string (when using with log level)
121+ * @param args - Format arguments (when using with log level)
122+ */
123+ export function log ( entry : LogEntry ) : void ;
124+ export function log ( level : LogLevel , fmt : string , ...args : unknown [ ] ) : void ;
125+ export function log ( levelOrEntry : LogLevel | LogEntry , fmt ?: string , ...args : unknown [ ] ) : void {
126+ // Normalize input
127+ const entry : LogEntry = typeof levelOrEntry === 'string'
128+ ? { level : levelOrEntry as LogLevel , message : util . format ( fmt ! , ...args ) }
129+ : levelOrEntry ;
130+
131+ // Check if we should log this level
132+ if ( levelPriority [ entry . level ] > levelPriority [ currentLogLevel ] ) {
52133 return ;
53134 }
54135
55- realStream . write ( str + '\n' ) ;
56- } ;
136+ // Format the message
137+ let finalMessage = entry . message ;
57138
58- function formatTime ( d : Date ) {
59- return `${ lpad ( d . getHours ( ) , 2 ) } :${ lpad ( d . getMinutes ( ) , 2 ) } :${ lpad ( d . getSeconds ( ) , 2 ) } ` ;
60-
61- function lpad ( x : any , w : number ) {
62- const s = `${ x } ` ;
63- return '0' . repeat ( Math . max ( w - s . length , 0 ) ) + s ;
139+ // Add timestamp first if requested
140+ if ( entry . timestamp ) {
141+ finalMessage = `[${ formatTime ( new Date ( ) ) } ] ${ finalMessage } ` ;
64142 }
65- }
66143
67- export enum LogLevel {
68- /** Not verbose at all */
69- DEFAULT = 0 ,
70- /** Pretty verbose */
71- DEBUG = 1 ,
72- /** Extremely verbose */
73- TRACE = 2 ,
74- }
144+ // Add prefix AFTER timestamp
145+ if ( entry . prefix ) {
146+ finalMessage = `${ entry . prefix } ${ finalMessage } ` ;
147+ }
75148
76- export let logLevel = LogLevel . DEFAULT ;
77- export let CI = false ;
149+ // Apply custom style if provided, otherwise use level-based style
150+ const style = entry . style || styleMap [ entry . level ] ;
151+ finalMessage = style ( finalMessage ) ;
78152
79- export function setLogLevel ( newLogLevel : LogLevel ) {
80- logLevel = newLogLevel ;
81- }
153+ // Get appropriate stream - pass through forceStdout flag
154+ const stream = getStream ( entry . level , entry . forceStdout ) ;
82155
83- export function setCI ( newCI : boolean ) {
84- CI = newCI ;
85- }
156+ // Handle corking
157+ if ( CORK_COUNTER > 0 ) {
158+ logBuffer . push ( [ stream , finalMessage ] ) ;
159+ return ;
160+ }
86161
87- export function increaseVerbosity ( ) {
88- logLevel += 1 ;
162+ // Write to stream
163+ stream . write ( finalMessage + '\n' ) ;
89164}
90165
91- const stream = ( ) => CI ? stdout : stderr ;
92- const _debug = logger ( stream , [ chalk . gray ] , true ) ;
93-
94- export const trace = ( fmt : string , ...args : unknown [ ] ) => logLevel >= LogLevel . TRACE && _debug ( fmt , ...args ) ;
95- export const debug = ( fmt : string , ...args : unknown [ ] ) => logLevel >= LogLevel . DEBUG && _debug ( fmt , ...args ) ;
96- export const error = logger ( stderr , [ chalk . red ] ) ;
97- export const warning = logger ( stream , [ chalk . yellow ] ) ;
98- export const success = logger ( stream , [ chalk . green ] ) ;
99- export const highlight = logger ( stream , [ chalk . bold ] ) ;
100- export const print = logger ( stream ) ;
101- export const data = logger ( stdout ) ;
102-
103- export type LoggerFunction = ( fmt : string , ...args : unknown [ ] ) => void ;
166+ // Convenience logging methods
167+ export const error = ( fmt : string , ...args : unknown [ ] ) => log ( LogLevel . ERROR , fmt , ...args ) ;
168+ export const warning = ( fmt : string , ...args : unknown [ ] ) => log ( LogLevel . WARN , fmt , ...args ) ;
169+ export const info = ( fmt : string , ...args : unknown [ ] ) => log ( LogLevel . INFO , fmt , ...args ) ;
170+ export const print = ( fmt : string , ...args : unknown [ ] ) => log ( LogLevel . INFO , fmt , ...args ) ;
171+ export const data = ( fmt : string , ...args : unknown [ ] ) => log ( {
172+ level : LogLevel . INFO ,
173+ message : util . format ( fmt , ...args ) ,
174+ forceStdout : true ,
175+ } ) ;
176+ export const debug = ( fmt : string , ...args : unknown [ ] ) => log ( LogLevel . DEBUG , fmt , ...args ) ;
177+ export const trace = ( fmt : string , ...args : unknown [ ] ) => log ( LogLevel . TRACE , fmt , ...args ) ;
178+
179+ export const success = ( fmt : string , ...args : unknown [ ] ) => log ( {
180+ level : LogLevel . INFO ,
181+ message : util . format ( fmt , ...args ) ,
182+ style : chalk . green ,
183+ } ) ;
184+
185+ export const highlight = ( fmt : string , ...args : unknown [ ] ) => log ( {
186+ level : LogLevel . INFO ,
187+ message : util . format ( fmt , ...args ) ,
188+ style : chalk . bold ,
189+ } ) ;
104190
105191/**
106- * Create a logger output that features a constant prefix string.
107- *
108- * @param prefixString the prefix string to be appended before any log entry.
109- * @param fn the logger function to be used (typically one of the other functions in this module)
110- *
111- * @returns a new LoggerFunction.
192+ * Creates a logging function that prepends a prefix to all messages.
193+ * @param prefixString - String to prepend to all messages
194+ * @param level - Log level to use (defaults to INFO)
195+ * @returns Logging function that accepts format string and arguments
112196 */
113- export function prefix ( prefixString : string , fn : LoggerFunction ) : LoggerFunction {
114- return ( fmt : string , ...args : any [ ] ) => fn ( `%s ${ fmt } ` , prefixString , ...args ) ;
115- }
197+ export function prefix ( prefixString : string , level : LogLevel = LogLevel . INFO ) : ( fmt : string , ...args : unknown [ ] ) => void {
198+ return ( fmt : string , ...args : unknown [ ] ) => log ( {
199+ level,
200+ message : util . format ( fmt , ...args ) ,
201+ prefix : prefixString ,
202+ } ) ;
203+ }
0 commit comments