diff --git a/e2e/cli-e2e/tests/collect.spec.ts b/e2e/cli-e2e/tests/collect.spec.ts index ade008dda..3ab682e42 100644 --- a/e2e/cli-e2e/tests/collect.spec.ts +++ b/e2e/cli-e2e/tests/collect.spec.ts @@ -6,8 +6,7 @@ import { cleanFolderPutGitKeep } from '../mocks/fs.mock'; describe('CLI collect', () => { const exampleCategoryTitle = 'Code style'; - const exampleAuditTitle = - 'Require `const` declarations for variables that are never reassigned after declared'; + const exampleAuditTitle = 'Require `const` declarations for variables'; const omitVariableData = ({ date, diff --git a/package-lock.json b/package-lock.json index a6aaccf42..e07fc8a2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@swc/helpers": "~0.5.0", "bundle-require": "^4.0.1", "chalk": "^5.3.0", + "cli-table3": "^0.6.3", "cliui": "^8.0.1", "multi-progress-bars": "^5.0.3", "simple-git": "^3.20.0", @@ -2075,6 +2076,15 @@ "tslib": "^2.3.0" } }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@commitlint/cli": { "version": "17.7.1", "dev": true, @@ -8303,6 +8313,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-table3": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", + "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, "node_modules/cli-width": { "version": "3.0.0", "dev": true, diff --git a/package.json b/package.json index d93356fc4..ec9455077 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@swc/helpers": "~0.5.0", "bundle-require": "^4.0.1", "chalk": "^5.3.0", + "cli-table3": "^0.6.3", "cliui": "^8.0.1", "multi-progress-bars": "^5.0.3", "simple-git": "^3.20.0", diff --git a/packages/core/src/lib/implementation/persist.spec.ts b/packages/core/src/lib/implementation/persist.spec.ts index 8c0f09436..c2efa8ef8 100644 --- a/packages/core/src/lib/implementation/persist.spec.ts +++ b/packages/core/src/lib/implementation/persist.spec.ts @@ -12,6 +12,7 @@ import { import { CODE_PUSHUP_DOMAIN, FOOTER_PREFIX, + NEW_LINE, README_LINK, } from '@code-pushup/utils'; import { mockConsole, unmockConsole } from '../../../test'; @@ -76,7 +77,9 @@ describe('persistReport', () => { it('should stdout as format by default`', async () => { await persistReport(dummyReport, dummyConfig); - expect(logs).toContain(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`); + expect(logs.join(NEW_LINE)).toContain( + `${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`, + ); expect(() => readReport('json')).not.toThrow(); expect(() => readReport('md')).toThrow('no such file or directory'); @@ -89,7 +92,9 @@ describe('persistReport', () => { ...dummyConfig, persist, }); - expect(logs).toContain(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`); + expect(logs.join(NEW_LINE)).toContain( + `${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`, + ); expect(() => readReport('json')).not.toThrow('no such file or directory'); expect(() => readReport('md')).toThrow('no such file or directory'); @@ -143,7 +148,9 @@ describe('persistReport', () => { `${FOOTER_PREFIX} [Code PushUp](${README_LINK})`, ); - expect(logs).toContain(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`); + expect(logs.join(NEW_LINE)).toContain( + `${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`, + ); }); it('should persist some formats`', async () => { @@ -162,7 +169,9 @@ describe('persistReport', () => { `${FOOTER_PREFIX} [Code PushUp](${README_LINK})`, ); - expect(logs).toContain(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`); + expect(logs.join(NEW_LINE)).toMatch( + `${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`, + ); }); // @TODO: should throw PersistDirError diff --git a/packages/core/src/lib/implementation/persist.ts b/packages/core/src/lib/implementation/persist.ts index 4150ccf38..bfbc44c8c 100644 --- a/packages/core/src/lib/implementation/persist.ts +++ b/packages/core/src/lib/implementation/persist.ts @@ -35,7 +35,7 @@ export async function persistReport( let scoredReport; if (format.includes('stdout')) { scoredReport = scoreReport(report); - reportToStdout(scoredReport); + console.log(reportToStdout(scoredReport)); } // collect physical format outputs diff --git a/packages/utils/package.json b/packages/utils/package.json index 9697c3f4f..4aecd26bf 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -7,6 +7,7 @@ "chalk": "^5.3.0", "cliui": "^8.0.1", "simple-git": "^3.20.0", - "multi-progress-bars": "^5.0.3" + "multi-progress-bars": "^5.0.3", + "cli-table3": "^0.6.3" } } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index fb228198e..ae501640a 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -42,3 +42,4 @@ export { distinct, slugify, } from './lib/transformation'; +export { NEW_LINE } from './lib/md'; diff --git a/packages/utils/src/lib/__snapshots__/report-to-md.spec.ts.snap b/packages/utils/src/lib/__snapshots__/report-to-md.spec.ts.snap index 75db1f8d0..33486e46b 100644 --- a/packages/utils/src/lib/__snapshots__/report-to-md.spec.ts.snap +++ b/packages/utils/src/lib/__snapshots__/report-to-md.spec.ts.snap @@ -436,5 +436,5 @@ The following plugins were run: |ESLint|47|\`0.1.0\`|368 ms| |Lighthouse|5|\`0.1.0\`|1.23 s| -Made with ❤️ by [Code PushUp](https://github.com/flowup/quality-metrics-cli#readme)" +Made with ❤ by [Code PushUp](https://github.com/flowup/quality-metrics-cli#readme)" `; diff --git a/packages/utils/src/lib/__snapshots__/report-to-stdout.spec.ts.snap b/packages/utils/src/lib/__snapshots__/report-to-stdout.spec.ts.snap index 4461b7377..f58c460cb 100644 --- a/packages/utils/src/lib/__snapshots__/report-to-stdout.spec.ts.snap +++ b/packages/utils/src/lib/__snapshots__/report-to-stdout.spec.ts.snap @@ -1,28 +1,90 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`report-to-stdout > should contain all sections when using the fixture report 1`] = ` -"Code PushUp Report - @code-pushup/core@0.1.0 ---- -Package Name: @code-pushup/core -Version: 0.1.0 -Commit: feat(cli): add logic for markdown report - 7eba125ad5643c2f90cb21389fc3442d786f43f9 -Date: today -Duration: 42ms -Plugins: 1 -Audits: 1 ---- +"Code PushUp Report - @code-pushup/core@0.0.1 - 🏷 Category ⭐ Score 🛡 Audits - Category 1 0 1/1 +ESLint audits +● Disallow assignment operators in conditional expressions 0 +● Disallow reassigning \`const\` variables 0 +● Disallow the use of \`debugger\` 0 +● Disallow invalid regular expression strings in \`RegExp\` construct 0 + ors +● Disallow the use of undeclared variables unless mentioned in \`/*g 0 + lobal */\` comments +● Disallow loops with a body that allows only one iteration 0 +● Disallow negating the left operand of relational operators 0 +● Disallow use of optional chaining in contexts where the \`undefine 0 + d\` value is not allowed +● Disallow unused variables 1 warning +● Require calls to \`isNaN()\` when checking for \`NaN\` 0 +● Enforce comparing \`typeof\` expressions against valid strings 0 +● Require braces around arrow function bodies 1 warning +● Enforce camelcase naming convention 0 +● Enforce consistent brace style for all control statements 0 +● Require the use of \`===\` and \`!==\` 1 warning +● Enforce a maximum number of lines of code in a function 1 warning +● Enforce a maximum number of lines per file 0 +● Disallow variable declarations from shadowing variables declared 3 warnings + in the outer scope +● Require \`let\` or \`const\` instead of \`var\` 0 +● Require or disallow method and property shorthand syntax for obje 3 warnings + ct literals +● Require using arrow functions for callbacks 0 +● Require \`const\` declarations for variables that are never reassig 1 warning + ned after declared +● Disallow using Object.assign with an object literal as the first 0 + argument and prefer the use of object spread instead +● Require or disallow \\"Yoda\\" conditions 0 +● Disallow missing \`key\` props in iterators/collection literals 1 warning +● Disallow missing props validation in a React component definition 6 warnings +● Disallow missing React when using JSX 0 +● enforces the Rules of Hooks 0 +● verifies the list of dependencies for Hooks like useEffect and si 2 warnings + milar +● Disallow missing displayName in a React component definition 0 +● Disallow comments from being inserted as text nodes 0 +● Disallow duplicate properties in JSX 0 +● Disallow \`target=\\"_blank\\"\` attribute without \`rel=\\"noreferrer\\"\` 0 +● Disallow undeclared variables in JSX 0 +● Disallow React to be incorrectly marked as unused 0 +● Disallow variables used in JSX to be incorrectly marked as unused 0 +● Disallow passing of children as props 0 +● Disallow when a DOM element is using both children and dangerousl 0 + ySetInnerHTML +● Disallow usage of deprecated methods 0 +● Disallow direct mutation of this.state 0 +● Disallow usage of findDOMNode 0 +● Disallow usage of isMounted 0 +● Disallow usage of the return value of ReactDOM.render 0 +● Disallow using string references 0 +● Disallow unescaped HTML entities from appearing in markup 0 +● Disallow usage of unknown DOM property 0 +● Enforce ES5 or ES6 class for returning value in render function 0 -Category 1 0 -- Audit Title (1) - audit description - http://www.my-docs.dev +Lighthouse audits +● First Contentful Paint 1.2 s +● Largest Contentful Paint 1.5 s +● Total Blocking Time 0 ms +● Cumulative Layout Shift 0 +● Speed Index 1.2 s -Made with ❤️ by code-pushup.dev" + +Categories + +┌────────────────┬───────┬────────┐ +│ Category │ Score │ Audits │ +├────────────────┼───────┼────────┤ +│ Performance │ 92 │ 6 │ +├────────────────┼───────┼────────┤ +│ Bug prevention │ 68 │ 16 │ +├────────────────┼───────┼────────┤ +│ Code style │ 54 │ 13 │ +└────────────────┴───────┴────────┘ + +Made with ❤ by code-pushup.dev +" `; diff --git a/packages/utils/src/lib/report-to-stdout.spec.ts b/packages/utils/src/lib/report-to-stdout.spec.ts index 54881e3dd..b01f8fd58 100644 --- a/packages/utils/src/lib/report-to-stdout.spec.ts +++ b/packages/utils/src/lib/report-to-stdout.spec.ts @@ -1,23 +1,14 @@ -import { afterEach, beforeEach, describe } from 'vitest'; -import { minimalReport } from '@code-pushup/models/testing'; -import { mockConsole, unmockConsole } from '../../test'; +import { describe } from 'vitest'; +import { report } from '@code-pushup/models/testing'; import { reportToStdout } from './report-to-stdout'; import { scoreReport } from './scoring'; -let logs: string[]; - describe('report-to-stdout', () => { - beforeEach(async () => { - logs = []; - mockConsole(msg => logs.push(msg)); - }); - afterEach(() => { - unmockConsole(); - }); - it('should contain all sections when using the fixture report', () => { - reportToStdout(scoreReport(minimalReport('tmp'))); - const logOutput = logs.join('\n'); - expect(logOutput).toMatchSnapshot(); + const logOutput = reportToStdout(scoreReport(report())); + // logOutput.replace(/\u001B\[\d+m/g, '') removes all color codes from the output + // for snapshot readability + // eslint-disable-next-line no-control-regex + expect(logOutput.replace(/\u001B\[\d+m/g, '')).toMatchSnapshot(); }); }); diff --git a/packages/utils/src/lib/report-to-stdout.ts b/packages/utils/src/lib/report-to-stdout.ts index e108e947f..c6644c789 100644 --- a/packages/utils/src/lib/report-to-stdout.ts +++ b/packages/utils/src/lib/report-to-stdout.ts @@ -1,113 +1,113 @@ import chalk from 'chalk'; +import Table from 'cli-table3'; import cliui from 'cliui'; import { NEW_LINE } from './md'; import { CODE_PUSHUP_DOMAIN, FOOTER_PREFIX, - countWeightedRefs, + countCategoryAudits, + formatReportScore, reportHeadlineText, - reportOverviewTableHeaders, + reportRawOverviewTableHeaders, } from './report'; import { ScoredReport } from './scoring'; -const ui = cliui({ width: 60 }); // @TODO check display width - -export function reportToStdout(report: ScoredReport): void { - reportToHeaderSection(report); - reportToMetaSection(report); - console.log(NEW_LINE); // @TODO just use '' and \n does only work in markdown - reportToOverviewSection(report); - console.log(NEW_LINE); - reportToDetailSection(report); - console.log(NEW_LINE); - console.log(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`); +function addLine(line = ''): string { + return line + NEW_LINE; } -function reportToHeaderSection(report: ScoredReport): void { +export function reportToStdout(report: ScoredReport): string { + let output = ''; + + output += addLine(reportToHeaderSection(report)); + output += addLine(); + output += addLine(reportToDetailSection(report)); + output += addLine(reportToOverviewSection(report)); + output += addLine(`${FOOTER_PREFIX} ${CODE_PUSHUP_DOMAIN}`); + + return output; +} + +function reportToHeaderSection(report: ScoredReport): string { const { packageName, version } = report; - console.log(`${chalk.bold(reportHeadlineText)} - ${packageName}@${version}`); + return `${chalk.bold(reportHeadlineText)} - ${packageName}@${version}`; } -function reportToMetaSection(report: ScoredReport): void { - const { date, duration, version, packageName, plugins } = report; - const _print = (text: string) => console.log(chalk.italic(chalk.gray(text))); +function reportToOverviewSection({ + categories, + plugins, +}: ScoredReport): string { + let output = addLine(chalk.magentaBright.bold('Categories')); + output += addLine(); - _print(`---`); - _print(`Package Name: ${packageName}`); - _print(`Version: ${version}`); - _print( - `Commit: feat(cli): add logic for markdown report - 7eba125ad5643c2f90cb21389fc3442d786f43f9`, - ); - _print(`Date: ${date}`); - _print(`Duration: ${duration}ms`); - _print(`Plugins: ${plugins?.length}`); - _print( - `Audits: ${plugins?.reduce((sum, { audits }) => sum + audits.length, 0)}`, + const table = new Table({ + head: reportRawOverviewTableHeaders, + colAligns: ['left', 'right', 'right'], + style: { + head: ['cyan'], + }, + }); + + table.push( + ...categories.map(({ title, score, refs }) => [ + title, + withColor({ score }), + countCategoryAudits(refs, plugins), + ]), ); - _print(`---`); + + output += addLine(table.toString()); + + return output; } -function reportToOverviewSection(report: ScoredReport): void { - const base = { - width: 20, - padding: [0, 1, 0, 1], - }; - - // table header - ui.div(...reportOverviewTableHeaders.map(text => ({ text, ...base }))); - - // table content - report.categories.forEach(({ title, refs, score }) => { - const audits = `${refs.length.toString()}/${countWeightedRefs(refs)}`; - - ui.div( - { - text: `${title}`, - ...base, - }, - { - text: score.toString(), - ...base, - }, - { - text: audits, - ...base, - }, - ); +function reportToDetailSection(report: ScoredReport): string { + const { plugins } = report; + + let output = ''; + + plugins.forEach(({ title, audits }) => { + output += addLine(); + output += addLine(chalk.magentaBright.bold(`${title} audits`)); + output += addLine(); + + const ui = cliui({ width: 80 }); + + audits.forEach(({ score, title, displayValue, value }) => { + ui.div( + { + text: withColor({ score, text: '●' }), + width: 2, + padding: [0, 1, 0, 0], + }, + { + text: title, + padding: [0, 3, 0, 0], + }, + { + text: chalk.cyanBright(displayValue || `${value}`), + width: 10, + padding: [0, 0, 0, 0], + }, + ); + }); + + output += addLine(ui.toString()); + output += addLine(); }); - console.log(ui.toString()); + return output; } -function reportToDetailSection(report: ScoredReport): void { - const { categories, plugins } = report; - - categories.forEach(category => { - const { title, refs, score } = category; - - console.log(chalk.bold(`${title} ${score}`)); - - refs.forEach( - ({ slug: auditSlugInCategoryRefs, weight, plugin: pluginSlug }) => { - const audit = plugins - .find(({ slug }) => slug === pluginSlug) - ?.audits.find( - ({ slug: auditSlugInPluginAudits }) => - auditSlugInPluginAudits === auditSlugInCategoryRefs, - ); - - if (audit) { - let content = `${audit.description}` + NEW_LINE; - if (audit.docsUrl) { - content += ` ${audit.docsUrl} ${NEW_LINE}`; - } - console.log(`- ${audit.title} (${weight})`); - console.log(` ${content}`); - } else { - // this should never happen - throw new Error(`No audit found for ${auditSlugInCategoryRefs}`); - } - }, - ); - }); +function withColor({ score, text }: { score: number; text?: string }) { + let str = text ?? formatReportScore(score); + const style = text ? chalk : chalk.bold; + if (score < 0.5) { + str = style.red(str); + } else if (score < 0.9) { + str = style.yellow(str); + } else { + str = style.green(str); + } + return str; } diff --git a/packages/utils/src/lib/report.ts b/packages/utils/src/lib/report.ts index 78c15bbd8..94f50d1fb 100644 --- a/packages/utils/src/lib/report.ts +++ b/packages/utils/src/lib/report.ts @@ -15,7 +15,7 @@ import { import { ScoredReport } from './scoring'; import { pluralize } from './transformation'; -export const FOOTER_PREFIX = 'Made with ❤️ by'; +export const FOOTER_PREFIX = 'Made with ❤ by'; // replace ❤️ with ❤, because of ❤️ has output issues export const CODE_PUSHUP_DOMAIN = 'code-pushup.dev'; export const README_LINK = 'https://github.com/flowup/quality-metrics-cli#readme'; @@ -25,6 +25,7 @@ export const reportOverviewTableHeaders = [ '⭐ Score', '🛡 Audits', ]; +export const reportRawOverviewTableHeaders = ['Category', 'Score', 'Audits']; export const reportMetaTableHeaders: string[] = [ 'Commit', 'Version',