diff --git a/packages/cli/src/lib/autorun/autorun-command.ts b/packages/cli/src/lib/autorun/autorun-command.ts index 0771c3237..0b3699504 100644 --- a/packages/cli/src/lib/autorun/autorun-command.ts +++ b/packages/cli/src/lib/autorun/autorun-command.ts @@ -1,4 +1,4 @@ -import { bold, gray } from 'ansis'; +import ansis from 'ansis'; import type { ArgumentsCamelCase, CommandModule } from 'yargs'; import { type CollectOptions, @@ -6,7 +6,7 @@ import { collectAndPersistReports, upload, } from '@code-pushup/core'; -import { ui } from '@code-pushup/utils'; +import { logger } from '@code-pushup/utils'; import { CLI_NAME } from '../constants.js'; import { collectSuccessfulLog, @@ -23,8 +23,8 @@ export function yargsAutorunCommandObject() { command, describe: 'Shortcut for running collect followed by upload', handler: async (args: ArgumentsCamelCase) => { - ui().logger.log(bold(CLI_NAME)); - ui().logger.info(gray(`Run ${command}...`)); + logger.info(ansis.bold(CLI_NAME)); + logger.debug(`Running ${ansis.bold(command)} command`); const options = args as unknown as AutorunOptions; // we need to ensure `json` is part of the formats as we want to upload @@ -51,7 +51,7 @@ export function yargsAutorunCommandObject() { uploadSuccessfulLog(report.url); } } else { - ui().logger.warning('Upload skipped because configuration is not set.'); + logger.warn('Upload skipped because configuration is not set.'); renderIntegratePortalHint(); } }, diff --git a/packages/cli/src/lib/collect/collect-command.ts b/packages/cli/src/lib/collect/collect-command.ts index 0f295d0fa..3baf3df9d 100644 --- a/packages/cli/src/lib/collect/collect-command.ts +++ b/packages/cli/src/lib/collect/collect-command.ts @@ -1,10 +1,10 @@ -import { bold, gray } from 'ansis'; +import ansis from 'ansis'; import type { ArgumentsCamelCase, CommandModule } from 'yargs'; import { type CollectAndPersistReportsOptions, collectAndPersistReports, } from '@code-pushup/core'; -import { link, ui } from '@code-pushup/utils'; +import { link, logger, ui } from '@code-pushup/utils'; import { CLI_NAME } from '../constants.js'; import { collectSuccessfulLog, @@ -18,8 +18,8 @@ export function yargsCollectCommandObject(): CommandModule { describe: 'Run Plugins and collect results', handler: async (args: ArgumentsCamelCase) => { const options = args as unknown as CollectAndPersistReportsOptions; - ui().logger.log(bold(CLI_NAME)); - ui().logger.info(gray(`Run ${command}...`)); + logger.info(ansis.bold(CLI_NAME)); + logger.debug(`Running ${ansis.bold(command)} command`); await collectAndPersistReports(options); collectSuccessfulLog(); @@ -40,12 +40,13 @@ export function yargsCollectCommandObject(): CommandModule { } export function renderUploadAutorunHint(): void { + // TODO: replace @poppinss/cliui ui() .sticker() - .add(bold.gray('💡 Visualize your reports')) + .add(ansis.bold.gray('💡 Visualize your reports')) .add('') .add( - `${gray('❯')} npx code-pushup upload - ${gray( + `${ansis.gray('❯')} npx code-pushup upload - ${ansis.gray( 'Run upload to upload the created report to the server', )}`, ) @@ -55,7 +56,7 @@ export function renderUploadAutorunHint(): void { )}`, ) .add( - `${gray('❯')} npx code-pushup autorun - ${gray('Run collect & upload')}`, + `${ansis.gray('❯')} npx code-pushup autorun - ${ansis.gray('Run collect & upload')}`, ) .add( ` ${link( diff --git a/packages/cli/src/lib/compare/compare-command.ts b/packages/cli/src/lib/compare/compare-command.ts index 3934d06f8..5e5fb72a2 100644 --- a/packages/cli/src/lib/compare/compare-command.ts +++ b/packages/cli/src/lib/compare/compare-command.ts @@ -1,8 +1,8 @@ -import { bold, gray } from 'ansis'; +import ansis from 'ansis'; import type { CommandModule } from 'yargs'; import { type CompareOptions, compareReportFiles } from '@code-pushup/core'; import type { PersistConfig, UploadConfig } from '@code-pushup/models'; -import { ui } from '@code-pushup/utils'; +import { logger } from '@code-pushup/utils'; import { CLI_NAME } from '../constants.js'; import { yargsCompareOptionsDefinition } from '../implementation/compare.options.js'; @@ -13,8 +13,8 @@ export function yargsCompareCommandObject() { describe: 'Compare 2 report files and create a diff file', builder: yargsCompareOptionsDefinition(), handler: async (args: unknown) => { - ui().logger.log(bold(CLI_NAME)); - ui().logger.info(gray(`Run ${command}...`)); + logger.info(ansis.bold(CLI_NAME)); + logger.debug(`Running ${ansis.bold(command)} command`); const options = args as CompareOptions & { persist: Required; @@ -28,9 +28,9 @@ export function yargsCompareCommandObject() { { before, after, label }, ); - ui().logger.info( + logger.info( `Reports diff written to ${outputPaths - .map(path => bold(path)) + .map(path => ansis.bold(path)) .join(' and ')}`, ); }, diff --git a/packages/cli/src/lib/compare/compare-command.unit.test.ts b/packages/cli/src/lib/compare/compare-command.unit.test.ts index 69d78cfc7..d0ccb5199 100644 --- a/packages/cli/src/lib/compare/compare-command.unit.test.ts +++ b/packages/cli/src/lib/compare/compare-command.unit.test.ts @@ -1,11 +1,11 @@ -import { bold } from 'ansis'; +import ansis from 'ansis'; import { compareReportFiles } from '@code-pushup/core'; import { DEFAULT_PERSIST_FILENAME, DEFAULT_PERSIST_FORMAT, DEFAULT_PERSIST_OUTPUT_DIR, } from '@code-pushup/models'; -import { ui } from '@code-pushup/utils'; +import { logger } from '@code-pushup/utils'; import { DEFAULT_CLI_CONFIGURATION } from '../../../mocks/constants.js'; import { yargsCli } from '../yargs-cli.js'; import { yargsCompareCommandObject } from './compare-command.js'; @@ -78,11 +78,10 @@ describe('compare-command', () => { commands: [yargsCompareCommandObject()], }).parseAsync(); - expect(ui()).toHaveLogged( - 'info', - `Reports diff written to ${bold( + expect(logger.info).toHaveBeenCalledWith( + `Reports diff written to ${ansis.bold( '.code-pushup/report-diff.json', - )} and ${bold('.code-pushup/report-diff.md')}`, + )} and ${ansis.bold('.code-pushup/report-diff.md')}`, ); }); }); diff --git a/packages/cli/src/lib/history/history-command.ts b/packages/cli/src/lib/history/history-command.ts index 1895ba80e..b53150a00 100644 --- a/packages/cli/src/lib/history/history-command.ts +++ b/packages/cli/src/lib/history/history-command.ts @@ -1,4 +1,4 @@ -import { bold, gray } from 'ansis'; +import ansis from 'ansis'; import type { CommandModule } from 'yargs'; import { type HistoryOptions, history } from '@code-pushup/core'; import { @@ -6,8 +6,8 @@ import { getCurrentBranchOrTag, getHashes, getSemverTags, + logger, safeCheckout, - ui, } from '@code-pushup/utils'; import { CLI_NAME } from '../constants.js'; import { yargsFilterOptionsDefinition } from '../implementation/filter.options.js'; @@ -17,8 +17,8 @@ import { normalizeHashOptions } from './utils.js'; const command = 'history'; async function handler(args: unknown) { - ui().logger.info(bold(CLI_NAME)); - ui().logger.info(gray(`Run ${command}`)); + logger.info(ansis.bold(CLI_NAME)); + logger.debug(`Running ${ansis.bold(command)} command`); const currentBranch = await getCurrentBranchOrTag(); const { targetBranch: rawTargetBranch, ...opt } = args as HistoryCliOptions & @@ -50,7 +50,7 @@ async function handler(args: unknown) { results.map(({ hash }) => hash), ); - ui().logger.log(`Reports: ${reports.length}`); + logger.info(`Reports: ${reports.length}`); } finally { // go back to initial branch await safeCheckout(currentBranch); diff --git a/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts b/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts index 55bfda28d..f1a1395f6 100644 --- a/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts +++ b/packages/cli/src/lib/implementation/filter.middleware.unit.test.ts @@ -1,5 +1,5 @@ import type { CategoryConfig, PluginConfig } from '@code-pushup/models'; -import { ui } from '@code-pushup/utils'; +import { logger } from '@code-pushup/utils'; import { filterMiddleware, filterSkippedCategories, @@ -315,14 +315,12 @@ describe('filterMiddleware', () => { ] as CategoryConfig[], }); - expect(ui()).toHaveNthLogged( + expect(logger.info).toHaveBeenNthCalledWith( 1, - 'info', 'The --skipPlugins argument removed the following categories: c1, c2.', ); - expect(ui()).toHaveNthLogged( + expect(logger.info).toHaveBeenNthCalledWith( 2, - 'info', 'The --onlyPlugins argument removed the following categories: c1, c2.', ); }); diff --git a/packages/cli/src/lib/implementation/global.utils.ts b/packages/cli/src/lib/implementation/global.utils.ts index 83d18beac..a70069bd1 100644 --- a/packages/cli/src/lib/implementation/global.utils.ts +++ b/packages/cli/src/lib/implementation/global.utils.ts @@ -1,5 +1,5 @@ import yargs from 'yargs'; -import { toArray, ui } from '@code-pushup/utils'; +import { logger, stringifyError, toArray } from '@code-pushup/utils'; import { OptionValidationError } from './validate-filter-options.utils.js'; export function filterKebabCaseKeys>( @@ -36,11 +36,11 @@ export function logErrorBeforeThrow any>( return await fn(...args); } catch (error) { if (error instanceof OptionValidationError) { - ui().logger.error(error.message); + logger.error(error.message); await new Promise(resolve => process.stdout.write('', resolve)); yargs().exit(1, error); } else { - console.error(error); + logger.error(stringifyError(error)); await new Promise(resolve => process.stdout.write('', resolve)); throw error; } diff --git a/packages/cli/src/lib/implementation/global.utils.unit.test.ts b/packages/cli/src/lib/implementation/global.utils.unit.test.ts index e24e97cf6..7d538096a 100644 --- a/packages/cli/src/lib/implementation/global.utils.unit.test.ts +++ b/packages/cli/src/lib/implementation/global.utils.unit.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { ui } from '@code-pushup/utils'; +import { logger } from '@code-pushup/utils'; import { filterKebabCaseKeys, logErrorBeforeThrow } from './global.utils.js'; import { OptionValidationError } from './validate-filter-options.utils.js'; @@ -64,7 +64,7 @@ describe('logErrorBeforeThrow', () => { } catch { /* suppress */ } - expect(ui()).toHaveLogged('error', 'Option validation failed'); + expect(logger.error).toHaveBeenCalledWith('Option validation failed'); }); it('should rethrow errors other than OptionValidationError', async () => { diff --git a/packages/cli/src/lib/implementation/logging.ts b/packages/cli/src/lib/implementation/logging.ts index be271721b..2191cbe51 100644 --- a/packages/cli/src/lib/implementation/logging.ts +++ b/packages/cli/src/lib/implementation/logging.ts @@ -1,31 +1,31 @@ -import { bold, gray } from 'ansis'; -import { link, ui } from '@code-pushup/utils'; +import ansis from 'ansis'; +import { link, logger, ui } from '@code-pushup/utils'; export function renderConfigureCategoriesHint(): void { - ui().logger.info( - gray( - `💡 Configure categories to see the scores in an overview table. See: ${link( - 'https://github.com/code-pushup/cli/blob/main/packages/cli/README.md', - )}`, - ), + logger.debug( + `💡 Configure categories to see the scores in an overview table. See: ${link( + 'https://github.com/code-pushup/cli/blob/main/packages/cli/README.md', + )}`, + { force: true }, ); } export function uploadSuccessfulLog(url: string): void { - ui().logger.success('Upload successful!'); - ui().logger.success(link(url)); + logger.info(ansis.green('Upload successful!')); + logger.info(link(url)); } export function collectSuccessfulLog(): void { - ui().logger.success('Collecting report successful!'); + logger.info(ansis.green('Collecting report successful!')); } export function renderIntegratePortalHint(): void { + // TODO: replace @poppinss/cliui ui() .sticker() - .add(bold.gray('💡 Integrate the portal')) + .add(ansis.bold.gray('💡 Integrate the portal')) .add('') .add( - `${gray('❯')} Upload a report to the server - ${gray( + `${ansis.gray('❯')} Upload a report to the server - ${ansis.gray( 'npx code-pushup upload', )}`, ) @@ -35,12 +35,12 @@ export function renderIntegratePortalHint(): void { )}`, ) .add( - `${gray('❯')} ${gray('Portal Integration')} - ${link( + `${ansis.gray('❯')} ${ansis.gray('Portal Integration')} - ${link( 'https://github.com/code-pushup/cli/blob/main/packages/cli/README.md#portal-integration', )}`, ) .add( - `${gray('❯')} ${gray('Upload Command')} - ${link( + `${ansis.gray('❯')} ${ansis.gray('Upload Command')} - ${link( 'https://github.com/code-pushup/cli/blob/main/packages/cli/README.md#portal-integration', )}`, ) diff --git a/packages/cli/src/lib/implementation/set-verbose.middleware.int.test.ts b/packages/cli/src/lib/implementation/set-verbose.middleware.int.test.ts deleted file mode 100644 index bfe0584a4..000000000 --- a/packages/cli/src/lib/implementation/set-verbose.middleware.int.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { isVerbose } from '@code-pushup/utils'; -import { setVerboseMiddleware } from './set-verbose.middleware.js'; - -describe('setVerboseMiddleware', () => { - it.each([ - [true, undefined, true], - [false, undefined, false], - [undefined, undefined, false], - [undefined, true, true], - [true, true, true], - [false, true, true], - [true, false, false], - [false, false, false], - ['True', undefined, true], - ['TRUE', undefined, true], - [0, undefined, false], - [undefined, 'true', true], - [true, 'False', false], - ])( - 'should set CP_VERBOSE based on env variable `%j` and cli argument `%j` and return `%j` from isVerbose() function', - (envValue, cliFlag, expected) => { - vi.stubEnv('CP_VERBOSE', `${envValue}`); - - setVerboseMiddleware({ verbose: cliFlag } as any); - expect(process.env['CP_VERBOSE']).toBe(`${expected}`); - expect(isVerbose()).toBe(expected); - }, - ); -}); diff --git a/packages/cli/src/lib/implementation/set-verbose.middleware.ts b/packages/cli/src/lib/implementation/set-verbose.middleware.ts index 8d77e1175..24291a994 100644 --- a/packages/cli/src/lib/implementation/set-verbose.middleware.ts +++ b/packages/cli/src/lib/implementation/set-verbose.middleware.ts @@ -1,36 +1,35 @@ import type { GlobalOptions } from '@code-pushup/core'; import type { CoreConfig } from '@code-pushup/models'; -import { coerceBooleanValue } from '@code-pushup/utils'; +import { coerceBooleanValue, logger } from '@code-pushup/utils'; import type { FilterOptions } from './filter.model.js'; import type { GeneralCliOptions } from './global.model'; /** * - * | CP_VERBOSE value | CLI `--verbose` flag | Effect | - * |------------------|-----------------------------|------------| - * | true | Not provided | enabled | - * | false | Not provided | disabled | - * | Not provided | Not provided | disabled | - * | Not provided | Explicitly set (true) | enabled | - * | true | Explicitly set (true) | enabled | - * | false | Explicitly set (true) | enabled | - * | true | Explicitly negated (false) | disabled | - * | false | Explicitly negated (false) | disabled | + * | CP_VERBOSE value | CLI `--verbose` flag | Effect | + * |------------------|----------------------|--------| + * | true | - | true | + * | false | - | false | + * | - | - | false | + * | - | true | true | + * | - | false | false | + * | true | true | true | + * | false | true | true | + * | true | false | false | + * | false | false | false | * * @param originalProcessArgs */ export function setVerboseMiddleware< T extends GeneralCliOptions & CoreConfig & FilterOptions & GlobalOptions, >(originalProcessArgs: T): T { - const envVerbose = coerceBooleanValue(process.env['CP_VERBOSE']); const cliVerbose = coerceBooleanValue(originalProcessArgs.verbose); - const verboseEffect = cliVerbose ?? envVerbose ?? false; - - // eslint-disable-next-line functional/immutable-data - process.env['CP_VERBOSE'] = `${verboseEffect}`; + if (cliVerbose != null) { + logger.setVerbose(cliVerbose); + } return { ...originalProcessArgs, - verbose: verboseEffect, + verbose: logger.isVerbose(), }; } diff --git a/packages/cli/src/lib/implementation/set-verbose.middleware.unit.test.ts b/packages/cli/src/lib/implementation/set-verbose.middleware.unit.test.ts index ada1f6cd9..26ce8845a 100644 --- a/packages/cli/src/lib/implementation/set-verbose.middleware.unit.test.ts +++ b/packages/cli/src/lib/implementation/set-verbose.middleware.unit.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it, vi } from 'vitest'; +import { describe, expect, it } from 'vitest'; +import { logger } from '@code-pushup/utils'; import { setVerboseMiddleware } from './set-verbose.middleware.js'; describe('setVerboseMiddleware', () => { @@ -7,6 +8,7 @@ describe('setVerboseMiddleware', () => { [false, undefined, false], [undefined, undefined, false], [undefined, true, true], + [undefined, false, false], [true, true, true], [false, true, true], [true, false, false], @@ -14,11 +16,13 @@ describe('setVerboseMiddleware', () => { ])( 'should set verbosity based on env variable `%j` and cli argument `%j` to perform verbose effect as `%j`', (envValue, cliFlag, expected) => { - vi.stubEnv('CP_VERBOSE', `${envValue}`); + logger.setVerbose(envValue ?? false); expect(setVerboseMiddleware({ verbose: cliFlag } as any).verbose).toBe( expected, ); + expect(process.env['CP_VERBOSE']).toBe(`${expected}`); + expect(logger.isVerbose()).toBe(expected); }, ); }); diff --git a/packages/cli/src/lib/implementation/validate-filter-options.utils.ts b/packages/cli/src/lib/implementation/validate-filter-options.utils.ts index cf3ef9e44..839d44406 100644 --- a/packages/cli/src/lib/implementation/validate-filter-options.utils.ts +++ b/packages/cli/src/lib/implementation/validate-filter-options.utils.ts @@ -3,8 +3,8 @@ import { capitalize, filterItemRefsBy, isVerbose, + logger, pluralize, - ui, } from '@code-pushup/utils'; import type { FilterOptionType, Filterables } from './filter.model.js'; @@ -49,12 +49,12 @@ export function validateFilterOption( ) { throw new OptionValidationError(message); } - ui().logger.warning(message); + logger.warn(message); } if (skippedValidItems.length > 0 && isVerbose()) { const item = getItemType(option, skippedValidItems.length); const prefix = skippedValidItems.length === 1 ? `a skipped` : `skipped`; - ui().logger.warning( + logger.warn( `The --${option} argument references ${prefix} ${item}: ${skippedValidItems.join(', ')}.`, ); } @@ -66,7 +66,7 @@ export function validateFilterOption( ).map(({ slug }) => slug); if (removedCategories.length > 0) { - ui().logger.info( + logger.info( `The --${option} argument removed the following categories: ${removedCategories.join( ', ', )}.`, @@ -84,7 +84,7 @@ export function validateSkippedCategories( ); if (skippedCategories.length > 0 && isVerbose()) { skippedCategories.forEach(category => { - ui().logger.info( + logger.info( `Category ${category.slug} was removed because all its refs were skipped. Affected refs: ${category.refs .map(ref => `${ref.slug} (${ref.type})`) .join(', ')}`, diff --git a/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts b/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts index ffeaa539b..98d3e01ff 100644 --- a/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts +++ b/packages/cli/src/lib/implementation/validate-filter-options.utils.unit.test.ts @@ -1,6 +1,6 @@ import { describe, expect } from 'vitest'; import type { CategoryConfig, PluginConfig } from '@code-pushup/models'; -import { ui } from '@code-pushup/utils'; +import { logger } from '@code-pushup/utils'; import type { FilterOptionType, Filterables } from './filter.model.js'; import { OptionValidationError, @@ -50,7 +50,7 @@ describe('validateFilterOption', () => { }, { itemsToFilter, skippedItems: [] }, ); - expect(ui()).toHaveLogged('warn', expected); + expect(logger.warn).toHaveBeenCalledWith(expected); }, ); @@ -93,7 +93,7 @@ describe('validateFilterOption', () => { }, { itemsToFilter, skippedItems: [] }, ); - expect(ui()).toHaveLogged('warn', expected); + expect(logger.warn).toHaveBeenCalledWith(expected); }, ); @@ -108,7 +108,8 @@ describe('validateFilterOption', () => { }, { itemsToFilter: ['p1'], skippedItems: [] }, ); - expect(ui()).not.toHaveLogs(); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); }); it('should log a category ignored as a result of plugin filtering', () => { @@ -129,9 +130,9 @@ describe('validateFilterOption', () => { }, { itemsToFilter: ['p1'], skippedItems: [] }, ); - expect(ui()).toHaveLoggedTimes(1); - expect(ui()).toHaveLogged( - 'info', + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.info).toHaveBeenCalledTimes(1); + expect(logger.info).toHaveBeenCalledWith( 'The --onlyPlugins argument removed the following categories: c1, c3.', ); }); @@ -223,14 +224,10 @@ describe('validateFilterOption', () => { { plugins, categories }, { itemsToFilter: ['p1'], skippedItems: ['p1'] }, ); - expect(ui()).toHaveNthLogged( - 1, - 'warn', + expect(logger.warn).toHaveBeenCalledWith( 'The --skipPlugins argument references a skipped plugin: p1.', ); - expect(ui()).toHaveNthLogged( - 2, - 'info', + expect(logger.info).toHaveBeenCalledWith( 'The --skipPlugins argument removed the following categories: c1.', ); }); @@ -462,16 +459,14 @@ describe('validateSkippedCategories', () => { refs: [{ type: 'audit', plugin: 'p2', slug: 'a1', weight: 1 }], }, ] as NonNullable); - expect(ui()).toHaveLogged( - 'info', + expect(logger.info).toHaveBeenCalledWith( 'Category c1 was removed because all its refs were skipped. Affected refs: g1 (group)', ); }); it('should not log anything when categories are not removed', () => { - const loggerSpy = vi.spyOn(ui().logger, 'info'); validateSkippedCategories(categories, categories); - expect(loggerSpy).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); }); it('should throw an error when no categories remain after filtering', () => { diff --git a/packages/cli/src/lib/merge-diffs/merge-diffs-command.ts b/packages/cli/src/lib/merge-diffs/merge-diffs-command.ts index 3a3a7308c..77dafae87 100644 --- a/packages/cli/src/lib/merge-diffs/merge-diffs-command.ts +++ b/packages/cli/src/lib/merge-diffs/merge-diffs-command.ts @@ -1,8 +1,8 @@ -import { bold, gray } from 'ansis'; +import ansis from 'ansis'; import type { CommandModule } from 'yargs'; import { mergeDiffs } from '@code-pushup/core'; import type { PersistConfig } from '@code-pushup/models'; -import { ui } from '@code-pushup/utils'; +import { logger } from '@code-pushup/utils'; import { CLI_NAME } from '../constants.js'; import type { MergeDiffsOptions } from '../implementation/merge-diffs.model.js'; import { yargsMergeDiffsOptionsDefinition } from '../implementation/merge-diffs.options.js'; @@ -14,8 +14,8 @@ export function yargsMergeDiffsCommandObject() { describe: 'Combine many report diffs into a single diff file', builder: yargsMergeDiffsOptionsDefinition(), handler: async (args: unknown) => { - ui().logger.log(bold(CLI_NAME)); - ui().logger.info(gray(`Run ${command}...`)); + logger.info(ansis.bold(CLI_NAME)); + logger.debug(`Running ${ansis.bold(command)} command`); const options = args as MergeDiffsOptions & { persist: Required; @@ -24,7 +24,7 @@ export function yargsMergeDiffsCommandObject() { const outputPath = await mergeDiffs(files, persist); - ui().logger.info(`Reports diff written to ${bold(outputPath)}`); + logger.info(`Reports diff written to ${ansis.bold(outputPath)}`); }, } satisfies CommandModule; } diff --git a/packages/cli/src/lib/merge-diffs/merge-diffs-command.unit.test.ts b/packages/cli/src/lib/merge-diffs/merge-diffs-command.unit.test.ts index 3313004c4..991538750 100644 --- a/packages/cli/src/lib/merge-diffs/merge-diffs-command.unit.test.ts +++ b/packages/cli/src/lib/merge-diffs/merge-diffs-command.unit.test.ts @@ -1,11 +1,11 @@ -import { bold } from 'ansis'; +import ansis from 'ansis'; import { mergeDiffs } from '@code-pushup/core'; import { DEFAULT_PERSIST_FILENAME, DEFAULT_PERSIST_FORMAT, DEFAULT_PERSIST_OUTPUT_DIR, } from '@code-pushup/models'; -import { ui } from '@code-pushup/utils'; +import { logger } from '@code-pushup/utils'; import { DEFAULT_CLI_CONFIGURATION } from '../../../mocks/constants.js'; import { yargsCli } from '../yargs-cli.js'; import { yargsMergeDiffsCommandObject } from './merge-diffs-command.js'; @@ -65,9 +65,8 @@ describe('merge-diffs-command', () => { }, ).parseAsync(); - expect(ui()).toHaveLogged( - 'info', - `Reports diff written to ${bold('.code-pushup/report-diff.md')}`, + expect(logger.info).toHaveBeenCalledWith( + `Reports diff written to ${ansis.bold('.code-pushup/report-diff.md')}`, ); }); }); diff --git a/packages/cli/src/lib/print-config/print-config-command.ts b/packages/cli/src/lib/print-config/print-config-command.ts index 25af05aaa..61854d3e6 100644 --- a/packages/cli/src/lib/print-config/print-config-command.ts +++ b/packages/cli/src/lib/print-config/print-config-command.ts @@ -1,8 +1,8 @@ -import { bold } from 'ansis'; +import ansis from 'ansis'; import { mkdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; import type { CommandModule } from 'yargs'; -import { ui } from '@code-pushup/utils'; +import { logger } from '@code-pushup/utils'; import { filterKebabCaseKeys } from '../implementation/global.utils.js'; import type { PrintConfigOptions } from '../implementation/print-config.model.js'; import { yargsPrintConfigOptionsDefinition } from '../implementation/print-config.options.js'; @@ -25,9 +25,9 @@ export function yargsPrintConfigCommandObject() { if (output) { await mkdir(path.dirname(output), { recursive: true }); await writeFile(output, content); - ui().logger.info(`Config printed to file ${bold(output)}`); + logger.info(`Config printed to file ${ansis.bold(output)}`); } else { - ui().logger.log(content); + logger.info(content); } }, } satisfies CommandModule; diff --git a/packages/cli/src/lib/print-config/print-config-command.unit.test.ts b/packages/cli/src/lib/print-config/print-config-command.unit.test.ts index 32c830846..83da94a1b 100644 --- a/packages/cli/src/lib/print-config/print-config-command.unit.test.ts +++ b/packages/cli/src/lib/print-config/print-config-command.unit.test.ts @@ -1,8 +1,9 @@ +import ansis from 'ansis'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { describe, expect, vi } from 'vitest'; import { MEMFS_VOLUME } from '@code-pushup/test-utils'; -import { ui } from '@code-pushup/utils'; +import { logger } from '@code-pushup/utils'; import { DEFAULT_CLI_CONFIGURATION } from '../../../mocks/constants.js'; import { yargsCli } from '../yargs-cli.js'; import { yargsPrintConfigCommandObject } from './print-config-command.js'; @@ -24,7 +25,9 @@ describe('print-config-command', () => { commands: [yargsPrintConfigCommandObject()], }).parseAsync(); - expect(ui()).toHaveLogged('log', expect.stringContaining('"plugins": [')); + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('"plugins": ['), + ); }); it('should write config to file if output option is given', async () => { @@ -37,11 +40,12 @@ describe('print-config-command', () => { await expect(readFile(outputPath, 'utf8')).resolves.toContain( '"plugins": [', ); - expect(ui()).not.toHaveLogged( - 'log', + expect(logger.info).not.toHaveBeenCalledWith( expect.stringContaining('"plugins": ['), ); - expect(ui()).toHaveLogged('info', `Config printed to file ${outputPath}`); + expect(logger.info).toHaveBeenCalledWith( + `Config printed to file ${ansis.bold(outputPath)}`, + ); }); it('should filter out meta arguments and kebab duplicates', async () => { @@ -50,15 +54,17 @@ describe('print-config-command', () => { commands: [yargsPrintConfigCommandObject()], }).parseAsync(); - expect(ui()).not.toHaveLogged('log', expect.stringContaining('"$0":')); - expect(ui()).not.toHaveLogged('log', expect.stringContaining('"_":')); + expect(logger.info).not.toHaveBeenCalledWith( + expect.stringContaining('"$0":'), + ); + expect(logger.info).not.toHaveBeenCalledWith( + expect.stringContaining('"_":'), + ); - expect(ui()).toHaveLogged( - 'log', + expect(logger.info).toHaveBeenCalledWith( expect.stringContaining('"outputDir": "destinationDir"'), ); - expect(ui()).not.toHaveLogged( - 'log', + expect(logger.info).not.toHaveBeenCalledWith( expect.stringContaining('"output-dir":'), ); }); diff --git a/packages/cli/src/lib/upload/upload-command.ts b/packages/cli/src/lib/upload/upload-command.ts index 23adad16a..8d4318cfa 100644 --- a/packages/cli/src/lib/upload/upload-command.ts +++ b/packages/cli/src/lib/upload/upload-command.ts @@ -1,7 +1,7 @@ -import { bold, gray } from 'ansis'; +import ansis from 'ansis'; import type { ArgumentsCamelCase, CommandModule } from 'yargs'; import { type UploadOptions, upload } from '@code-pushup/core'; -import { ui } from '@code-pushup/utils'; +import { logger } from '@code-pushup/utils'; import { CLI_NAME } from '../constants.js'; import { renderIntegratePortalHint, @@ -14,8 +14,8 @@ export function yargsUploadCommandObject() { command, describe: 'Upload report results to the portal', handler: async (args: ArgumentsCamelCase) => { - ui().logger.log(bold(CLI_NAME)); - ui().logger.info(gray(`Run ${command}...`)); + logger.info(ansis.bold(CLI_NAME)); + logger.debug(`Running ${ansis.bold(command)} command`); const options = args as unknown as UploadOptions; if (options.upload == null) { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4c873dbea..259602273 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -23,11 +23,7 @@ export { executePlugin, executePlugins, } from './lib/implementation/execute-plugin.js'; -export { - PersistDirError, - PersistError, - persistReport, -} from './lib/implementation/persist.js'; +export { persistReport } from './lib/implementation/persist.js'; export { autoloadRc, ConfigPathError, diff --git a/packages/core/src/lib/collect-and-persist.ts b/packages/core/src/lib/collect-and-persist.ts index eb7d266d9..d3db86792 100644 --- a/packages/core/src/lib/collect-and-persist.ts +++ b/packages/core/src/lib/collect-and-persist.ts @@ -6,11 +6,10 @@ import { validate, } from '@code-pushup/models'; import { - isVerbose, logStdoutSummary, + logger, scoreReport, sortReport, - ui, } from '@code-pushup/utils'; import { collect } from './implementation/collect.js'; import { @@ -31,14 +30,13 @@ export type CollectAndPersistReportsOptions = Pick< export async function collectAndPersistReports( options: CollectAndPersistReportsOptions, ): Promise { - const logger = ui().logger; const reportResult = await collect(options); const sortedScoredReport = sortReport(scoreReport(reportResult)); const { persist } = options; const { skipReports = false, ...persistOptions } = persist ?? {}; - if (skipReports === true) { + if (skipReports) { logger.info('Skipping saving reports as `persist.skipReports` is true'); } else { const persistResults = await persistReport( @@ -46,10 +44,7 @@ export async function collectAndPersistReports( sortedScoredReport, persistOptions, ); - - if (isVerbose()) { - logPersistedResults(persistResults); - } + logPersistedResults(persistResults); } // terminal output diff --git a/packages/core/src/lib/collect-and-persist.unit.test.ts b/packages/core/src/lib/collect-and-persist.unit.test.ts index 01eea84a2..0b95022ca 100644 --- a/packages/core/src/lib/collect-and-persist.unit.test.ts +++ b/packages/core/src/lib/collect-and-persist.unit.test.ts @@ -1,18 +1,15 @@ import { type MockInstance, describe } from 'vitest'; import { - ISO_STRING_REGEXP, MINIMAL_CONFIG_MOCK, MINIMAL_REPORT_MOCK, } from '@code-pushup/test-utils'; +import * as utils from '@code-pushup/utils'; import { type ScoredReport, - isVerbose, logStdoutSummary, scoreReport, sortReport, - ui, } from '@code-pushup/utils'; -import * as utils from '@code-pushup/utils'; import { type CollectAndPersistReportsOptions, collectAndPersistReports, @@ -46,88 +43,30 @@ describe('collectAndPersistReports', () => { logStdoutSpy.mockRestore(); }); - it('should call collect and persistReport with correct parameters in non-verbose mode', async () => { + it('should call collect and persistReport with correct parameters', async () => { const sortedScoredReport = sortReport(scoreReport(MINIMAL_REPORT_MOCK)); - expect(isVerbose()).toBeFalse(); - - const nonVerboseConfig: CollectAndPersistReportsOptions = { + const config: CollectAndPersistReportsOptions = { ...MINIMAL_CONFIG_MOCK, - persist: { - outputDir: 'output', - filename: 'report', - format: ['md'], - }, - cache: { - read: false, - write: false, - }, + persist: { outputDir: 'output', filename: 'report', format: ['md'] }, + cache: { read: false, write: false }, progress: false, }; - await collectAndPersistReports(nonVerboseConfig); + await collectAndPersistReports(config); - expect(collect).toHaveBeenCalledWith(nonVerboseConfig); + expect(collect).toHaveBeenCalledWith(config); expect(persistReport).toHaveBeenCalledWith< Parameters - >( - { - packageName: '@code-pushup/core', - version: '0.0.1', - date: expect.stringMatching(ISO_STRING_REGEXP), - duration: 666, - commit: expect.any(Object), - plugins: expect.any(Array), - }, - sortedScoredReport, - { - outputDir: 'output', - filename: 'report', - format: ['md'], - }, - ); - - expect(logStdoutSummary).toHaveBeenCalledWith(sortedScoredReport); - expect(logPersistedResults).not.toHaveBeenCalled(); - }); - - it('should call collect and persistReport with correct parameters in verbose mode', async () => { - const sortedScoredReport = sortReport(scoreReport(MINIMAL_REPORT_MOCK)); - - vi.stubEnv('CP_VERBOSE', 'true'); - - const verboseConfig: CollectAndPersistReportsOptions = { - ...MINIMAL_CONFIG_MOCK, - persist: { - outputDir: 'output', - filename: 'report', - format: ['md'], - }, - cache: { - read: false, - write: false, - }, - progress: false, - }; - await collectAndPersistReports(verboseConfig); - - expect(collect).toHaveBeenCalledWith(verboseConfig); - - expect(persistReport).toHaveBeenCalledWith( - MINIMAL_REPORT_MOCK, - sortedScoredReport, - verboseConfig.persist, - ); + >(MINIMAL_REPORT_MOCK, sortedScoredReport, config.persist); expect(logStdoutSummary).toHaveBeenCalledWith(sortedScoredReport); expect(logPersistedResults).toHaveBeenCalled(); }); - it('should call collect and not persistReport if skipReports options is true in verbose mode', async () => { + it('should call collect and not persistReport if skipReports options is true', async () => { const sortedScoredReport = sortReport(scoreReport(MINIMAL_REPORT_MOCK)); - vi.stubEnv('CP_VERBOSE', 'true'); - const verboseConfig: CollectAndPersistReportsOptions = { ...MINIMAL_CONFIG_MOCK, persist: { @@ -137,6 +76,7 @@ describe('collectAndPersistReports', () => { skipReports: true, }, progress: false, + cache: { read: false, write: false }, }; await collectAndPersistReports(verboseConfig); @@ -152,6 +92,8 @@ describe('collectAndPersistReports', () => { await collectAndPersistReports( MINIMAL_CONFIG_MOCK as CollectAndPersistReportsOptions, ); - expect(ui()).toHaveLogged('log', 'Made with ❤ by code-pushup.dev'); + expect(utils.logger.info).toHaveBeenCalledWith( + 'Made with ❤ by code-pushup.dev', + ); }); }); diff --git a/packages/core/src/lib/compare.ts b/packages/core/src/lib/compare.ts index 18f4c391c..6fffd2022 100644 --- a/packages/core/src/lib/compare.ts +++ b/packages/core/src/lib/compare.ts @@ -15,9 +15,9 @@ import { createReportPath, ensureDirectoryExists, generateMdReportsDiff, + logger, readJsonFile, scoreReport, - ui, } from '@code-pushup/utils'; import { type ReportsToCompare, @@ -154,9 +154,7 @@ async function fetchPortalComparisonLink( }); } catch (error) { if (error instanceof PortalOperationError) { - ui().logger.warning( - `Failed to fetch portal comparison link - ${error.message}`, - ); + logger.warn(`Failed to fetch portal comparison link - ${error.message}`); return undefined; } throw error; diff --git a/packages/core/src/lib/history.ts b/packages/core/src/lib/history.ts index 58bdf6ba9..257192c3c 100644 --- a/packages/core/src/lib/history.ts +++ b/packages/core/src/lib/history.ts @@ -4,7 +4,11 @@ import type { PersistConfig, UploadConfig, } from '@code-pushup/models'; -import { getCurrentBranchOrTag, safeCheckout, ui } from '@code-pushup/utils'; +import { + getCurrentBranchOrTag, + logger, + safeCheckout, +} from '@code-pushup/utils'; import { collectAndPersistReports } from './collect-and-persist.js'; import type { GlobalOptions } from './types.js'; import { upload } from './upload.js'; @@ -32,7 +36,7 @@ export async function history( const reports: string[] = []; // eslint-disable-next-line functional/no-loop-statements for (const commit of commits) { - ui().logger.info(`Collect ${commit}`); + logger.info(`Collecting for commit ${commit}`); await safeCheckout(commit, forceCleanStatus); const currentConfig: HistoryOptions = { @@ -51,14 +55,12 @@ export async function history( await collectAndPersistReports(currentConfig); if (skipUploads) { - ui().logger.info('Upload is skipped because skipUploads is set to true.'); + logger.info('Upload is skipped because skipUploads is set to true.'); } else { if (currentConfig.upload) { await upload(currentConfig); } else { - ui().logger.info( - 'Upload is skipped because upload config is undefined.', - ); + logger.info('Upload is skipped because upload config is undefined.'); } } diff --git a/packages/core/src/lib/implementation/execute-plugin.unit.test.ts b/packages/core/src/lib/implementation/execute-plugin.unit.test.ts index b536cd7bd..c6a5eac3c 100644 --- a/packages/core/src/lib/implementation/execute-plugin.unit.test.ts +++ b/packages/core/src/lib/implementation/execute-plugin.unit.test.ts @@ -1,6 +1,6 @@ -import { bold } from 'ansis'; +import ansis from 'ansis'; import { vol } from 'memfs'; -import { describe, expect, it, vi } from 'vitest'; +import { type MockInstance, describe, expect, it, vi } from 'vitest'; import { type AuditOutputs, DEFAULT_PERSIST_CONFIG, @@ -14,12 +14,23 @@ import { executePlugin, executePlugins } from './execute-plugin.js'; import * as runnerModule from './runner.js'; describe('executePlugin', () => { - beforeEach(() => { - vi.clearAllMocks(); + let readRunnerResultsSpy: MockInstance< + Parameters<(typeof runnerModule)['readRunnerResults']>, + ReturnType<(typeof runnerModule)['readRunnerResults']> + >; + let executePluginRunnerSpy: MockInstance< + Parameters<(typeof runnerModule)['executePluginRunner']>, + ReturnType<(typeof runnerModule)['executePluginRunner']> + >; + + beforeAll(() => { + readRunnerResultsSpy = vi.spyOn(runnerModule, 'readRunnerResults'); + executePluginRunnerSpy = vi.spyOn(runnerModule, 'executePluginRunner'); }); afterEach(() => { - vi.restoreAllMocks(); + readRunnerResultsSpy.mockRestore(); + executePluginRunnerSpy.mockRestore(); }); it('should execute a valid plugin config and pass runner params', async () => { @@ -251,9 +262,9 @@ describe('executePlugins', () => { { progress: false }, ), ).rejects.toThrow( - `Executing 1 plugin failed.\n\nError: - Plugin ${bold( + `Executing 1 plugin failed.\n\nError: - Plugin ${ansis.bold( title, - )} (${bold(slug)}) produced the following error:\n - Audit output is invalid`, + )} (${ansis.bold(slug)}) produced the following error:\n - Audit output is invalid`, ); }); @@ -366,13 +377,13 @@ describe('executePlugins', () => { { progress: false }, ), ).rejects.toThrow( - `Error: - Plugin ${bold('plg1')} (${bold( + `Error: - Plugin ${ansis.bold('plg1')} (${ansis.bold( 'plg1', - )}) produced the following error:\n - Audit metadata not present in plugin config. Missing slug: ${bold( + )}) produced the following error:\n - Audit metadata not present in plugin config. Missing slug: ${ansis.bold( 'missing-audit-slug-a', - )}\nError: - Plugin ${bold('plg2')} (${bold( + )}\nError: - Plugin ${ansis.bold('plg2')} (${ansis.bold( 'plg2', - )}) produced the following error:\n - Audit metadata not present in plugin config. Missing slug: ${bold( + )}) produced the following error:\n - Audit metadata not present in plugin config. Missing slug: ${ansis.bold( 'missing-audit-slug-b', )}`, ); diff --git a/packages/core/src/lib/implementation/persist.ts b/packages/core/src/lib/implementation/persist.ts index 340d61b96..36041fd37 100644 --- a/packages/core/src/lib/implementation/persist.ts +++ b/packages/core/src/lib/implementation/persist.ts @@ -1,3 +1,4 @@ +import ansis from 'ansis'; import { mkdir, stat, writeFile } from 'node:fs/promises'; import type { Format, PersistConfig, Report } from '@code-pushup/models'; import { @@ -8,21 +9,8 @@ import { generateMdReport, logMultipleFileResults, stringifyError, - ui, } from '@code-pushup/utils'; -export class PersistDirError extends Error { - constructor(outputDir: string) { - super(`outPath: ${outputDir} is no directory.`); - } -} - -export class PersistError extends Error { - constructor(reportPath: string) { - super(`fileName: ${reportPath} could not be saved.`); - } -} - export async function persistReport( report: Report, sortedScoredReport: ScoredReport, @@ -52,8 +40,9 @@ export async function persistReport( try { await mkdir(outputDir, { recursive: true }); } catch (error) { - ui().logger.warning(stringifyError(error)); - throw new PersistDirError(outputDir); + throw new Error( + `Failed to create output directory in ${ansis.bold(outputDir)} - ${stringifyError(error)}`, + ); } } @@ -68,15 +57,16 @@ export async function persistReport( ); } -async function persistResult(reportPath: string, content: string) { +function persistResult(reportPath: string, content: string) { return ( writeFile(reportPath, content) // return reportPath instead of void .then(() => stat(reportPath)) .then(stats => [reportPath, stats.size] as const) - .catch(error => { - ui().logger.warning((error as Error).toString()); - throw new PersistError(reportPath); + .catch((error: unknown) => { + throw new Error( + `Failed to persist report in ${ansis.bold(reportPath)} - ${stringifyError(error)}`, + ); }) ); } diff --git a/packages/core/src/lib/implementation/persist.unit.test.ts b/packages/core/src/lib/implementation/persist.unit.test.ts index f458f0bdc..bcb10158c 100644 --- a/packages/core/src/lib/implementation/persist.unit.test.ts +++ b/packages/core/src/lib/implementation/persist.unit.test.ts @@ -8,7 +8,7 @@ import { MINIMAL_REPORT_MOCK, REPORT_MOCK, } from '@code-pushup/test-utils'; -import { scoreReport, sortReport, ui } from '@code-pushup/utils'; +import { logger, scoreReport, sortReport } from '@code-pushup/utils'; import { logPersistedResults, persistReport } from './persist.js'; describe('persistReport', () => { @@ -92,27 +92,30 @@ describe('persistReport', () => { describe('logPersistedResults', () => { it('should log report sizes correctly`', () => { logPersistedResults([{ status: 'fulfilled', value: ['out.json', 10_000] }]); - expect(ui()).toHaveNthLogged( + expect(logger.debug).toHaveBeenNthCalledWith( 1, - 'success', expect.stringContaining('Generated reports successfully: '), ); - expect(ui()).toHaveNthLogged( + expect(logger.debug).toHaveBeenNthCalledWith( 2, - 'success', expect.stringContaining('9.77 kB'), ); - expect(ui()).toHaveNthLogged( + expect(logger.debug).toHaveBeenNthCalledWith( 2, - 'success', expect.stringContaining('out.json'), ); }); it('should log fails correctly`', () => { logPersistedResults([{ status: 'rejected', reason: 'fail' }]); - expect(ui()).toHaveNthLogged(1, 'warn', 'Generated reports failed: '); - expect(ui()).toHaveNthLogged(2, 'warn', expect.stringContaining('fail')); + expect(logger.warn).toHaveBeenNthCalledWith( + 1, + 'Generated reports failed: ', + ); + expect(logger.warn).toHaveBeenNthCalledWith( + 2, + expect.stringContaining('fail'), + ); }); it('should log report sizes and fails correctly`', () => { @@ -120,26 +123,25 @@ describe('logPersistedResults', () => { { status: 'fulfilled', value: ['out.json', 10_000] }, { status: 'rejected', reason: 'fail' }, ]); - expect(ui()).toHaveNthLogged( + expect(logger.debug).toHaveBeenNthCalledWith( 1, - 'success', 'Generated reports successfully: ', ); - expect(ui()).toHaveNthLogged( + expect(logger.debug).toHaveBeenNthCalledWith( 2, - 'success', expect.stringContaining('out.json'), ); - expect(ui()).toHaveNthLogged( + expect(logger.debug).toHaveBeenNthCalledWith( 2, - 'success', expect.stringContaining('9.77 kB'), ); - expect(ui()).toHaveNthLogged( - 3, - 'warn', + expect(logger.warn).toHaveBeenNthCalledWith( + 1, expect.stringContaining('Generated reports failed: '), ); - expect(ui()).toHaveNthLogged(3, 'warn', expect.stringContaining('fail')); + expect(logger.warn).toHaveBeenNthCalledWith( + 1, + expect.stringContaining('fail'), + ); }); }); diff --git a/packages/core/src/lib/load-portal-client.ts b/packages/core/src/lib/load-portal-client.ts index 4ee3bb0df..0f7a13e45 100644 --- a/packages/core/src/lib/load-portal-client.ts +++ b/packages/core/src/lib/load-portal-client.ts @@ -1,4 +1,4 @@ -import { stringifyError, ui } from '@code-pushup/utils'; +import { logger, stringifyError } from '@code-pushup/utils'; export async function loadPortalClient(): Promise< typeof import('@code-pushup/portal-client') | null @@ -6,10 +6,10 @@ export async function loadPortalClient(): Promise< try { return await import('@code-pushup/portal-client'); } catch (error) { - ui().logger.warning( + logger.warn( `Failed to import @code-pushup/portal-client - ${stringifyError(error)}`, ); - ui().logger.error( + logger.error( 'Optional peer dependency @code-pushup/portal-client is not available. Make sure it is installed to enable upload functionality.', ); return null; diff --git a/packages/core/src/lib/merge-diffs.ts b/packages/core/src/lib/merge-diffs.ts index 6470c059c..39935c2a0 100644 --- a/packages/core/src/lib/merge-diffs.ts +++ b/packages/core/src/lib/merge-diffs.ts @@ -6,9 +6,9 @@ import { generateMdReportsDiffForMonorepo, isPromiseFulfilledResult, isPromiseRejectedResult, + logger, readJsonFile, stringifyError, - ui, } from '@code-pushup/utils'; export async function mergeDiffs( @@ -32,9 +32,7 @@ export async function mergeDiffs( }), ); results.filter(isPromiseRejectedResult).forEach(({ reason }) => { - ui().logger.warning( - `Skipped invalid report diff - ${stringifyError(reason)}`, - ); + logger.warn(`Skipped invalid report diff - ${stringifyError(reason)}`); }); const diffs = results .filter(isPromiseFulfilledResult) diff --git a/packages/core/src/lib/merge-diffs.unit.test.ts b/packages/core/src/lib/merge-diffs.unit.test.ts index ef529e1dc..be5e31e4e 100644 --- a/packages/core/src/lib/merge-diffs.unit.test.ts +++ b/packages/core/src/lib/merge-diffs.unit.test.ts @@ -9,7 +9,7 @@ import { reportsDiffMock, reportsDiffUnchangedMock, } from '@code-pushup/test-utils'; -import { fileExists, ui } from '@code-pushup/utils'; +import { fileExists, logger } from '@code-pushup/utils'; import { mergeDiffs } from './merge-diffs.js'; describe('mergeDiffs', () => { @@ -24,6 +24,7 @@ describe('mergeDiffs', () => { outputDir: MEMFS_VOLUME, filename: 'report', format: ['json', 'md'], + skipReports: false, }; beforeEach(() => { @@ -63,16 +64,14 @@ describe('mergeDiffs', () => { ), ).resolves.toBe(path.join(MEMFS_VOLUME, 'report-diff.md')); - expect(ui()).toHaveNthLogged( + expect(logger.warn).toHaveBeenNthCalledWith( 1, - 'warn', expect.stringContaining( 'Skipped invalid report diff - Failed to read JSON file missing-report-diff.json', ), ); - expect(ui()).toHaveNthLogged( + expect(logger.warn).toHaveBeenNthCalledWith( 2, - 'warn', expect.stringContaining( 'Skipped invalid report diff - Invalid reports diff in invalid-report-diff.json', ), diff --git a/packages/utils/src/lib/env.ts b/packages/utils/src/lib/env.ts index 453d0f7ef..f170c7332 100644 --- a/packages/utils/src/lib/env.ts +++ b/packages/utils/src/lib/env.ts @@ -6,7 +6,6 @@ import { type RunnerArgs, formatSchema, } from '@code-pushup/models'; -import { ui } from './logging.js'; export function isCI() { return isEnvVarEnabled('CI'); @@ -23,12 +22,6 @@ export function isEnvVarEnabled(name: string): boolean { return value; } - if (process.env[name]) { - ui().logger.warning( - `Environment variable ${name} expected to be a boolean (true/false/1/0), but received value ${process.env[name]}. Treating it as disabled.`, - ); - } - return false; } diff --git a/packages/utils/src/lib/env.unit.test.ts b/packages/utils/src/lib/env.unit.test.ts index ac125a5f1..dfe7bb7ab 100644 --- a/packages/utils/src/lib/env.unit.test.ts +++ b/packages/utils/src/lib/env.unit.test.ts @@ -9,7 +9,6 @@ import { runnerArgsFromEnv, runnerArgsToEnv, } from './env.js'; -import { ui } from './logging.js'; describe('isEnvVarEnabled', () => { beforeEach(() => { @@ -40,15 +39,6 @@ describe('isEnvVarEnabled', () => { vi.stubEnv('CP_VERBOSE', '0'); expect(isEnvVarEnabled('CP_VERBOSE')).toBeFalse(); }); - - it('should log a warning for unexpected values', () => { - vi.stubEnv('CP_VERBOSE', 'unexpected'); - expect(isEnvVarEnabled('CP_VERBOSE')).toBeFalse(); - expect(ui()).toHaveLogged( - 'warn', - 'Environment variable CP_VERBOSE expected to be a boolean (true/false/1/0), but received value unexpected. Treating it as disabled.', - ); - }); }); describe('coerceBooleanValue', () => { diff --git a/packages/utils/src/lib/file-system.ts b/packages/utils/src/lib/file-system.ts index 72bb6acc6..d9c7deb2a 100644 --- a/packages/utils/src/lib/file-system.ts +++ b/packages/utils/src/lib/file-system.ts @@ -1,11 +1,11 @@ -import { bold, gray } from 'ansis'; +import ansis from 'ansis'; import { type Options, bundleRequire } from 'bundle-require'; import { mkdir, readFile, readdir, rm, stat } from 'node:fs/promises'; import path from 'node:path'; import type { Format, PersistConfig } from '@code-pushup/models'; import { formatBytes } from './formatting.js'; import { logMultipleResults } from './log-results.js'; -import { ui } from './logging.js'; +import { logger } from './logger.js'; export async function readTextFile(filePath: string): Promise { const buffer = await readFile(filePath); @@ -41,7 +41,7 @@ export async function ensureDirectoryExists(baseDir: string) { return; } catch (error) { const fsError = error as NodeJS.ErrnoException; - ui().logger.warning(fsError.message); + logger.warn(fsError.message); if (fsError.code !== 'EEXIST') { throw error; } @@ -63,11 +63,11 @@ export function logMultipleFileResults( ): void { const succeededTransform = (result: PromiseFulfilledResult) => { const [fileName, size] = result.value; - const formattedSize = size ? ` (${gray(formatBytes(size))})` : ''; - return `- ${bold(fileName)}${formattedSize}`; + const formattedSize = size ? ` (${ansis.gray(formatBytes(size))})` : ''; + return `- ${ansis.bold(fileName)}${formattedSize}`; }; const failedTransform = (result: PromiseRejectedResult) => - `- ${bold(result.reason as string)}`; + `- ${ansis.bold(`${result.reason}`)}`; logMultipleResults( fileResults, diff --git a/packages/utils/src/lib/git/git.commits-and-tags.ts b/packages/utils/src/lib/git/git.commits-and-tags.ts index f4bef08d6..abb7b0a07 100644 --- a/packages/utils/src/lib/git/git.commits-and-tags.ts +++ b/packages/utils/src/lib/git/git.commits-and-tags.ts @@ -1,7 +1,7 @@ import { type LogOptions as SimpleGitLogOptions, simpleGit } from 'simple-git'; import { type Commit, commitSchema, validate } from '@code-pushup/models'; import { stringifyError } from '../errors.js'; -import { ui } from '../logging.js'; +import { logger } from '../logger.js'; import { isSemver } from '../semver.js'; export async function getLatestCommit( @@ -15,7 +15,7 @@ export async function getLatestCommit( }); return validate(commitSchema, log.latest); } catch (error) { - ui().logger.error(stringifyError(error)); + logger.error(stringifyError(error)); return null; } } diff --git a/packages/utils/src/lib/git/git.ts b/packages/utils/src/lib/git/git.ts index f706129f0..86da81c3f 100644 --- a/packages/utils/src/lib/git/git.ts +++ b/packages/utils/src/lib/git/git.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { type StatusResult, simpleGit } from 'simple-git'; -import { ui } from '../logging.js'; +import { logger } from '../logger.js'; import { toUnixPath } from '../transform.js'; export function getGitRoot(git = simpleGit()): Promise { @@ -82,7 +82,7 @@ export async function safeCheckout( if (forceCleanStatus) { await git.raw(['reset', '--hard']); await git.clean(['f', 'd']); - ui().logger.info(`git status cleaned`); + logger.info(`git status cleaned`); } await guardAgainstLocalChanges(git); await git.checkout(branchOrHash); diff --git a/packages/utils/src/lib/log-results.ts b/packages/utils/src/lib/log-results.ts index 254072460..953ec1cbc 100644 --- a/packages/utils/src/lib/log-results.ts +++ b/packages/utils/src/lib/log-results.ts @@ -1,5 +1,5 @@ import { isPromiseFulfilledResult, isPromiseRejectedResult } from './guards.js'; -import { ui } from './logging.js'; +import { logger } from './logger.js'; export function logMultipleResults( results: PromiseSettledResult[], @@ -29,16 +29,16 @@ export function logMultipleResults( } export function logPromiseResults< - T extends PromiseFulfilledResult | PromiseRejectedResult, ->(results: T[], logMessage: string, getMsg: (result: T) => string): void { + T extends PromiseFulfilledResult[] | PromiseRejectedResult[], +>(results: T, logMessage: string, getMsg: (result: T[number]) => string): void { if (results.length > 0) { const log = results[0]?.status === 'fulfilled' - ? (m: string) => { - ui().logger.success(m); + ? (message: string) => { + logger.debug(message); } - : (m: string) => { - ui().logger.warning(m); + : (message: string) => { + logger.warn(message); }; log(logMessage); diff --git a/packages/utils/src/lib/log-results.unit.test.ts b/packages/utils/src/lib/log-results.unit.test.ts index e9203fbae..ad7086f31 100644 --- a/packages/utils/src/lib/log-results.unit.test.ts +++ b/packages/utils/src/lib/log-results.unit.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; import type { FileResult } from './file-system.js'; import { logMultipleResults, logPromiseResults } from './log-results.js'; -import { ui } from './logging.js'; +import { logger } from './logger.js'; describe('logMultipleResults', () => { const succeededCallbackMock = vi.fn(); @@ -67,12 +67,11 @@ describe('logPromiseResults', () => { 'Uploaded reports successfully:', (result): string => result.value.toString(), ); - expect(ui()).toHaveNthLogged( + expect(logger.debug).toHaveBeenNthCalledWith( 1, - 'success', 'Uploaded reports successfully:', ); - expect(ui()).toHaveNthLogged(2, 'success', 'out.json'); + expect(logger.debug).toHaveBeenNthCalledWith(2, 'out.json'); }); it('should log on fail', () => { @@ -81,7 +80,7 @@ describe('logPromiseResults', () => { 'Generated reports failed:', (result: { reason: string }) => result.reason, ); - expect(ui()).toHaveNthLogged(1, 'warn', 'Generated reports failed:'); - expect(ui()).toHaveNthLogged(2, 'warn', 'fail'); + expect(logger.warn).toHaveBeenNthCalledWith(1, 'Generated reports failed:'); + expect(logger.warn).toHaveBeenNthCalledWith(2, 'fail'); }); }); diff --git a/packages/utils/src/lib/logging.ts b/packages/utils/src/lib/logging.ts index acadcc073..d056198ed 100644 --- a/packages/utils/src/lib/logging.ts +++ b/packages/utils/src/lib/logging.ts @@ -1,6 +1,6 @@ import isaacs_cliui from '@isaacs/cliui'; import { cliui } from '@poppinss/cliui'; -import { underline } from 'ansis'; +import ansis from 'ansis'; import { TERMINAL_WIDTH } from './reports/constants.js'; // TODO: remove once logger is used everywhere @@ -56,5 +56,5 @@ export function logListItem(args: ArgumentsType) { } export function link(text: string) { - return underline.blueBright(text); + return ansis.underline.blueBright(text); } diff --git a/packages/utils/src/lib/reports/log-stdout-summary.int.test.ts b/packages/utils/src/lib/reports/log-stdout-summary.int.test.ts index b4d7c5fc4..754000739 100644 --- a/packages/utils/src/lib/reports/log-stdout-summary.int.test.ts +++ b/packages/utils/src/lib/reports/log-stdout-summary.int.test.ts @@ -1,25 +1,32 @@ import { beforeAll, describe, expect, vi } from 'vitest'; import { removeColorCodes, reportMock } from '@code-pushup/test-utils'; +import { logger } from '../logger.js'; import { ui } from '../logging.js'; import { logStdoutSummary } from './log-stdout-summary.js'; import { scoreReport } from './scoring.js'; import { sortReport } from './sorting.js'; describe('logStdoutSummary', () => { - let logs: string[]; + let stdout: string; beforeAll(() => { - logs = []; - // console.log is used inside the logger when in "normal" mode - vi.spyOn(console, 'log').mockImplementation(msg => { - logs = [...logs, msg]; + vi.mocked(logger.info).mockImplementation(message => { + stdout += `${message}\n`; + }); + vi.mocked(logger.newline).mockImplementation(() => { + stdout += '\n'; + }); + // console.log is used inside the @poppinss/cliui logger when in "normal" mode + vi.spyOn(console, 'log').mockImplementation(message => { + stdout += `${message}\n`; }); // we want to see table and sticker logs in the final style ("raw" don't show borders etc so we use `console.log` here) ui().switchMode('normal'); }); - afterEach(() => { - logs = []; + beforeEach(() => { + stdout = ''; + logger.setVerbose(false); }); afterAll(() => { @@ -29,10 +36,8 @@ describe('logStdoutSummary', () => { it('should contain all sections when using the fixture report', async () => { logStdoutSummary(sortReport(scoreReport(reportMock()))); - const output = logs.join('\n'); - - expect(output).toContain('Categories'); - await expect(removeColorCodes(output)).toMatchFileSnapshot( + expect(stdout).toContain('Categories'); + await expect(removeColorCodes(stdout)).toMatchFileSnapshot( '__snapshots__/report-stdout.txt', ); }); @@ -41,22 +46,19 @@ describe('logStdoutSummary', () => { logStdoutSummary( sortReport(scoreReport({ ...reportMock(), categories: undefined })), ); - const output = logs.join('\n'); - expect(output).not.toContain('Categories'); - await expect(removeColorCodes(output)).toMatchFileSnapshot( + expect(stdout).not.toContain('Categories'); + await expect(removeColorCodes(stdout)).toMatchFileSnapshot( '__snapshots__/report-stdout-no-categories.txt', ); }); it('should include all audits when verbose is true', async () => { - vi.stubEnv('CP_VERBOSE', 'true'); + logger.setVerbose(true); logStdoutSummary(sortReport(scoreReport(reportMock()))); - const output = logs.join('\n'); - - await expect(removeColorCodes(output)).toMatchFileSnapshot( + await expect(removeColorCodes(stdout)).toMatchFileSnapshot( '__snapshots__/report-stdout-verbose.txt', ); }); @@ -77,10 +79,8 @@ describe('logStdoutSummary', () => { logStdoutSummary(sortReport(scoreReport(reportWithPerfectScores))); - const output = logs.join('\n'); - - expect(output).toContain('All 47 audits have perfect scores'); - await expect(removeColorCodes(output)).toMatchFileSnapshot( + expect(stdout).toContain('All 47 audits have perfect scores'); + await expect(removeColorCodes(stdout)).toMatchFileSnapshot( '__snapshots__/report-stdout-all-perfect-scores.txt', ); }); diff --git a/packages/utils/src/lib/reports/log-stdout-summary.ts b/packages/utils/src/lib/reports/log-stdout-summary.ts index 0c50499bb..39425b3a8 100644 --- a/packages/utils/src/lib/reports/log-stdout-summary.ts +++ b/packages/utils/src/lib/reports/log-stdout-summary.ts @@ -1,6 +1,7 @@ -import { bold, cyan, cyanBright, green, red } from 'ansis'; +import cliui from '@isaacs/cliui'; +import ansis from 'ansis'; import type { AuditReport } from '@code-pushup/models'; -import { isVerbose } from '../env.js'; +import { logger } from '../logger.js'; import { ui } from '../logging.js'; import { CODE_PUSHUP_DOMAIN, @@ -16,34 +17,30 @@ import { scoreTargetIcon, } from './utils.js'; -function log(msg = ''): void { - ui().logger.log(msg); -} - export function logStdoutSummary(report: ScoredReport): void { const { plugins, categories, packageName, version } = report; - log(reportToHeaderSection({ packageName, version })); - log(); + logger.info(reportToHeaderSection({ packageName, version })); + logger.newline(); logPlugins(plugins); if (categories && categories.length > 0) { logCategories({ plugins, categories }); } - log(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`); - log(); + logger.info(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`); + logger.newline(); } function reportToHeaderSection({ packageName, version, }: Pick): string { - return `${bold(REPORT_HEADLINE_TEXT)} - ${packageName}@${version}`; + return `${ansis.bold(REPORT_HEADLINE_TEXT)} - ${packageName}@${version}`; } export function logPlugins(plugins: ScoredReport['plugins']): void { plugins.forEach(plugin => { const { title, audits } = plugin; const filteredAudits = - isVerbose() || audits.length === 1 + logger.isVerbose() || audits.length === 1 ? audits : audits.filter(({ score }) => score !== 1); const diff = audits.length - filteredAudits.length; @@ -57,21 +54,22 @@ export function logPlugins(plugins: ScoredReport['plugins']): void { : `... ${diff} audits with perfect scores omitted for brevity ...`; logRow(1, notice); } - log(); + logger.newline(); }); } function logAudits(pluginTitle: string, audits: AuditReport[]): void { - log(); - log(bold.magentaBright(`${pluginTitle} audits`)); - log(); + logger.newline(); + logger.info(ansis.bold.magentaBright(`${pluginTitle} audits`)); + logger.newline(); audits.forEach(({ score, title, displayValue, value }) => { logRow(score, title, displayValue || `${value}`); }); } function logRow(score: number, title: string, value?: string): void { - ui().row([ + const ui = cliui({ width: TERMINAL_WIDTH }); + ui.div( { text: applyScoreColor({ score, text: '●' }), width: 2, @@ -85,14 +83,15 @@ function logRow(score: number, title: string, value?: string): void { ...(value ? [ { - text: cyanBright(value), + text: ansis.cyanBright(value), // eslint-disable-next-line @typescript-eslint/no-magic-numbers width: 20, padding: [0, 0, 0, 0], }, ] : []), - ]); + ); + logger.info(ui.toString()); } export function logCategories({ @@ -106,12 +105,13 @@ export function logCategories({ `${binaryIconPrefix(score, scoreTarget)}${applyScoreColor({ score })}`, countCategoryAudits(refs, plugins), ]); + // TODO: replace @poppinss/cliui const table = ui().table(); // eslint-disable-next-line @typescript-eslint/no-magic-numbers table.columnWidths([TERMINAL_WIDTH - 9 - 10 - 4, 9, 10]); table.head( REPORT_RAW_OVERVIEW_TABLE_HEADERS.map((heading, idx) => ({ - content: cyan(heading), + content: ansis.cyan(heading), hAlign: hAlign(idx), })), ); @@ -124,10 +124,10 @@ export function logCategories({ ), ); - log(bold.magentaBright('Categories')); - log(); + logger.info(ansis.bold.magentaBright('Categories')); + logger.newline(); table.render(); - log(); + logger.newline(); } export function binaryIconPrefix( @@ -135,8 +135,8 @@ export function binaryIconPrefix( scoreTarget: number | undefined, ): string { return scoreTargetIcon(score, scoreTarget, { - passIcon: bold(green('✓')), - failIcon: bold(red('✗')), + passIcon: ansis.bold(ansis.green('✓')), + failIcon: ansis.bold(ansis.red('✗')), postfix: ' ', }); } diff --git a/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts b/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts index 302fa4328..58681e545 100644 --- a/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts +++ b/packages/utils/src/lib/reports/log-stdout-summary.unit.test.ts @@ -1,5 +1,6 @@ import { beforeAll, describe, expect, vi } from 'vitest'; import { removeColorCodes } from '@code-pushup/test-utils'; +import { logger } from '../logger.js'; import { ui } from '../logging.js'; import { binaryIconPrefix, @@ -162,25 +163,21 @@ describe('logCategories', () => { }); describe('logPlugins', () => { - let logs: string[]; + let stdout: string; beforeAll(() => { - logs = []; - vi.spyOn(console, 'log').mockImplementation(msg => { - logs = [...logs, msg]; + vi.mocked(logger.info).mockImplementation(message => { + stdout += `${message}\n`; }); - ui().switchMode('normal'); }); - afterEach(() => { - logs = []; - }); - - afterAll(() => { - ui().switchMode('raw'); + beforeEach(() => { + stdout = ''; }); it('should log only audits with scores other than 1 when verbose is false', () => { + logger.setVerbose(false); + logPlugins([ { title: 'Best Practices', @@ -191,14 +188,14 @@ describe('logPlugins', () => { ], }, ] as ScoredReport['plugins']); - const output = logs.join('\n'); - expect(output).toContain('Audit 1'); - expect(output).not.toContain('Audit 2'); - expect(output).toContain('audits with perfect scores omitted for brevity'); + + expect(stdout).toContain('Audit 1'); + expect(stdout).not.toContain('Audit 2'); + expect(stdout).toContain('audits with perfect scores omitted for brevity'); }); it('should log all audits when verbose is true', () => { - vi.stubEnv('CP_VERBOSE', 'true'); + logger.setVerbose(true); logPlugins([ { @@ -210,12 +207,14 @@ describe('logPlugins', () => { ], }, ] as ScoredReport['plugins']); - const output = logs.join('\n'); - expect(output).toContain('Audit 1'); - expect(output).toContain('Audit 2'); + + expect(stdout).toContain('Audit 1'); + expect(stdout).toContain('Audit 2'); }); it('should indicate all audits have perfect scores', () => { + logger.setVerbose(false); + logPlugins([ { title: 'Best Practices', @@ -226,11 +225,13 @@ describe('logPlugins', () => { ], }, ] as ScoredReport['plugins']); - const output = logs.join('\n'); - expect(output).toContain('All 2 audits have perfect scores'); + + expect(stdout).toContain('All 2 audits have perfect scores'); }); it('should log original audits when verbose is false and no audits have perfect scores', () => { + logger.setVerbose(false); + logPlugins([ { title: 'Best Practices', @@ -241,12 +242,14 @@ describe('logPlugins', () => { ], }, ] as ScoredReport['plugins']); - const output = logs.join('\n'); - expect(output).toContain('Audit 1'); - expect(output).toContain('Audit 2'); + + expect(stdout).toContain('Audit 1'); + expect(stdout).toContain('Audit 2'); }); it('should not truncate a perfect audit in non-verbose mode when it is the only audit available', () => { + logger.setVerbose(false); + logPlugins([ { title: 'Best Practices', @@ -254,8 +257,8 @@ describe('logPlugins', () => { audits: [{ title: 'Audit 1', score: 1, value: 100 }], }, ] as ScoredReport['plugins']); - const output = logs.join('\n'); - expect(output).toContain('Audit 1'); + + expect(stdout).toContain('Audit 1'); }); });