@@ -2,114 +2,202 @@ import { Writable } from 'stream';
2
2
import * as util from 'util' ;
3
3
import * as chalk from 'chalk' ;
4
4
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
+ }
9
15
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 ;
17
26
}
18
27
28
+ const { stdout, stderr } = process ;
29
+
30
+ // Corking mechanism
19
31
let CORK_COUNTER = 0 ;
20
32
const logBuffer : [ Writable , string ] [ ] = [ ] ;
21
33
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
+ } ;
25
42
26
- function corkLogging ( ) {
27
- CORK_COUNTER += 1 ;
28
- }
43
+ // Stream selection
44
+ let CI = false ;
29
45
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 ;
35
56
}
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 ;
36
77
}
37
78
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
+ }
40
87
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
+ }
45
97
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
+ }
47
116
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 ] ) {
52
133
return ;
53
134
}
54
135
55
- realStream . write ( str + '\n' ) ;
56
- } ;
136
+ // Format the message
137
+ let finalMessage = entry . message ;
57
138
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 } ` ;
64
142
}
65
- }
66
143
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
+ }
75
148
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 ) ;
78
152
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 ) ;
82
155
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
+ }
86
161
87
- export function increaseVerbosity ( ) {
88
- logLevel += 1 ;
162
+ // Write to stream
163
+ stream . write ( finalMessage + '\n' ) ;
89
164
}
90
165
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
+ } ) ;
104
190
105
191
/**
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
112
196
*/
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