@@ -11,6 +11,11 @@ type CiPlatform = 'GitHub Actions' | 'GitLab CI/CD';
1111
1212const GROUP_COLOR_ENV_VAR_NAME = 'CP_LOGGER_GROUP_COLOR' ;
1313
14+ /**
15+ * Rich logging implementation for Code PushUp CLI, plugins, etc.
16+ *
17+ * Use {@link logger} singleton.
18+ */
1419export class Logger {
1520 #isVerbose = isEnvVarEnabled ( 'CP_VERBOSE' ) ;
1621 #isCI = isEnvVarEnabled ( 'CI' ) ;
@@ -56,37 +61,181 @@ export class Logger {
5661 process . exit ( os . platform ( ) === 'win32' ? 2 : 130 ) ;
5762 } ;
5863
64+ /**
65+ * Logs an error to the console (red).
66+ *
67+ * Automatically adapts to logger state if called within {@link task}, {@link group}, etc.
68+ *
69+ * @example
70+ * logger.error('Config file is invalid');
71+ *
72+ * @param message Error text
73+ */
5974 error ( message : string ) : void {
6075 this . #log( message , 'red' ) ;
6176 }
6277
78+ /**
79+ * Logs a warning to the console (yellow).
80+ *
81+ * Automatically adapts to logger state if called within {@link task}, {@link group}, etc.
82+ *
83+ * @example
84+ * logger.warn('Skipping invalid audits');
85+ *
86+ * @param message Warning text
87+ */
6388 warn ( message : string ) : void {
6489 this . #log( message , 'yellow' ) ;
6590 }
6691
92+ /**
93+ * Logs an informational message to the console (unstyled).
94+ *
95+ * Automatically adapts to logger state if called within {@link task}, {@link group}, etc.
96+ *
97+ * @example
98+ * logger.info('Code PushUp CLI v0.80.2');
99+ *
100+ * @param message Info text
101+ */
67102 info ( message : string ) : void {
68103 this . #log( message ) ;
69104 }
70105
106+ /**
107+ * Logs a debug message to the console (gray), but **only if verbose** flag is set (see {@link isVerbose}).
108+ *
109+ * Automatically adapts to logger state if called within {@link task}, {@link group}, etc.
110+ *
111+ * @example
112+ * logger.debug('Running ESLint version 9.16.0');
113+ *
114+ * @param message Debug text
115+ */
71116 debug ( message : string ) : void {
72117 if ( this . #isVerbose) {
73118 this . #log( message , 'gray' ) ;
74119 }
75120 }
76121
122+ /**
123+ * Print a blank line to the console, used to separate logs for readability.
124+ *
125+ * Automatically adapts to logger state if called within {@link task}, {@link group}, etc.
126+ *
127+ * @example
128+ * logger.newline();
129+ */
77130 newline ( ) : void {
78131 this . #log( '' ) ;
79132 }
80133
134+ /**
135+ * Is verbose flag set?
136+ *
137+ * Verbosity is configured by {@link setVerbose} call or `CP_VERBOSE` environment variable.
138+ *
139+ * @example
140+ * if (logger.isVerbose()) {
141+ * // ...
142+ * }
143+ */
81144 isVerbose ( ) : boolean {
82145 return this . #isVerbose;
83146 }
84147
148+ /**
149+ * Sets verbose flag for this logger.
150+ *
151+ * Also sets the `CP_VERBOSE` environment variable.
152+ * This means any future {@link Logger} instantiations (including child processes) will use the same verbosity level.
153+ *
154+ * @example
155+ * logger.setVerbose(process.argv.includes('--verbose'));
156+ *
157+ * @param isVerbose Verbosity level
158+ */
85159 setVerbose ( isVerbose : boolean ) : void {
86160 process . env [ 'CP_VERBOSE' ] = `${ isVerbose } ` ;
87161 this . #isVerbose = isVerbose ;
88162 }
89163
164+ /**
165+ * Animates asynchronous work using a spinner.
166+ *
167+ * Basic logs are supported within the worker function, they will be printed with indentation once the spinner completes.
168+ *
169+ * In CI environments, the spinner animation is disabled, and inner logs are printed immediately.
170+ *
171+ * Spinners may be nested within a {@link group} call, in which case line symbols are used instead of dots, as well the group's color.
172+ *
173+ * The task's duration is included in the logged output as a suffix.
174+ *
175+ * Listens for `SIGINT` event in order to cancel and restore spinner before exiting.
176+ *
177+ * Concurrent or nested spinners are not supported, nor can groups be nested in spinners.
178+ *
179+ * @example
180+ * await logger.task('Uploading report to portal', async () => {
181+ * // ...
182+ * return 'Uploaded report to portal';
183+ * });
184+ *
185+ * @param title Display text used as pending message.
186+ * @param worker Asynchronous implementation. Returned promise determines spinner status and final message. Support for inner logs has some limitations (described above).
187+ */
188+ task ( title : string , worker : ( ) => Promise < string > ) : Promise < void > {
189+ return this . #spinner( worker , {
190+ pending : title ,
191+ success : value => value ,
192+ failure : error => `${ title } → ${ ansis . red ( `${ error } ` ) } ` ,
193+ } ) ;
194+ }
195+
196+ /**
197+ * Similar to {@link task}, but spinner texts are formatted as shell commands.
198+ *
199+ * A `$`-prefix is added. Its color indicates the status (blue=pending, green=success, red=failure).
200+ *
201+ * @example
202+ * await logger.command('npx eslint . --format=json', async () => {
203+ * // ...
204+ * });
205+ *
206+ * @param bin Command string with arguments.
207+ * @param worker Asynchronous execution of the command (not implemented by the logger).
208+ */
209+ command ( bin : string , worker : ( ) => Promise < void > ) : Promise < void > {
210+ return this . #spinner( worker , {
211+ pending : `${ ansis . blue ( '$' ) } ${ bin } ` ,
212+ success : ( ) => `${ ansis . green ( '$' ) } ${ bin } ` ,
213+ failure : ( ) => `${ ansis . red ( '$' ) } ${ bin } ` ,
214+ } ) ;
215+ }
216+
217+ /**
218+ * Groups many logs into a visually distinct section.
219+ *
220+ * Groups alternate prefix colors between cyan and magenta.
221+ *
222+ * The group's total duration is included in the logged output.
223+ *
224+ * Nested groups are not supported.
225+ *
226+ * @example
227+ * await logger.group('Running plugin "ESLint"', async () => {
228+ * logger.debug('ESLint version is 9.16.0');
229+ * await logger.command('npx eslint . --format=json', () => {
230+ * // ...
231+ * })
232+ * logger.info('Found 42 lint errors.');
233+ * return 'Completed "ESLint" plugin execution';
234+ * });
235+ *
236+ * @param title Display title for the group.
237+ * @param worker Asynchronous implementation. Returned promise determines group status and ending message. Inner logs are attached to the group.
238+ */
90239 async group ( title : string , worker : ( ) => Promise < string > ) : Promise < void > {
91240 if ( this . #groupColor) {
92241 throw new Error (
@@ -197,22 +346,6 @@ export class Logger {
197346 }
198347 }
199348
200- task ( title : string , worker : ( ) => Promise < string > ) : Promise < void > {
201- return this . #spinner( worker , {
202- pending : title ,
203- success : value => value ,
204- failure : error => `${ title } → ${ ansis . red ( `${ error } ` ) } ` ,
205- } ) ;
206- }
207-
208- command ( bin : string , worker : ( ) => Promise < void > ) : Promise < void > {
209- return this . #spinner( worker , {
210- pending : `${ ansis . blue ( '$' ) } ${ bin } ` ,
211- success : ( ) => `${ ansis . green ( '$' ) } ${ bin } ` ,
212- failure : ( ) => `${ ansis . red ( '$' ) } ${ bin } ` ,
213- } ) ;
214- }
215-
216349 async #spinner< T > (
217350 worker : ( ) => Promise < T > ,
218351 messages : {
@@ -331,4 +464,12 @@ export class Logger {
331464 }
332465}
333466
467+ /**
468+ * Shared {@link Logger} instance.
469+ *
470+ * @example
471+ * import { logger } from '@code-pushup/utils';
472+ *
473+ * logger.info('Made with ❤️ by Code PushUp');
474+ */
334475export const logger = new Logger ( ) ;
0 commit comments