diff --git a/README.md b/README.md index 7efd9eb..6357652 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Common utils used by Rambler team - [@rambler-tech/cookie-storage](packages/cookie-storage) - [@rambler-tech/local-storage](packages/local-storage) - [@rambler-tech/session-storage](packages/session-storage) +- [@rambler-tech/lhci-report](packages/lhci-report) ## Contributing diff --git a/package.json b/package.json index 1d554ab..84e9c3d 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@rambler-tech/eslint-config": "^0.10.4", "@rambler-tech/licenselint-config": "^0.0.2", "@rambler-tech/prettier-config": "^0.1.0", - "@rambler-tech/ts-config": "^0.1.0", + "@rambler-tech/ts-config": "^0.1.1", "@rambler-tech/typedoc-config": "^0.3.0", "@size-limit/preset-small-lib": "^9.0.0", "@types/jest": "^29.5.5", diff --git a/packages/lhci-report/README.md b/packages/lhci-report/README.md new file mode 100644 index 0000000..0d74b65 --- /dev/null +++ b/packages/lhci-report/README.md @@ -0,0 +1,32 @@ +# Lighthouse CI report + +Print Lighthouse CI report to CLI + +## Install + +``` +npm install -D eslint @rambler-tech/lhci-report +``` + +or + +``` +yarn add -D eslint @rambler-tech/lhci-report +``` + +## Usage + +```sh +@rambler-tech/lhci-report + +Lighthouse report + +URL: https://www.rambler.ru/ +Number of runs: 3 + +performance 0.93 0.94 0.94 +accessibility 0.87 0.87 0.87 +best-practices 0.52 0.56 0.56 +seo 0.92 0.92 0.92 +pwa 0.71 0.71 0.71 +``` diff --git a/packages/lhci-report/cli.ts b/packages/lhci-report/cli.ts new file mode 100644 index 0000000..a3e980e --- /dev/null +++ b/packages/lhci-report/cli.ts @@ -0,0 +1,24 @@ +#!/usr/bin/env node +import * as path from 'path' +import * as fs from 'fs/promises' +import {formatReport} from './format' +import {getReport, type Manifest} from './report' + +async function print(dir: string) { + const manifestFile = path.resolve(dir, 'manifest.json') + // eslint-disable-next-line security/detect-non-literal-fs-filename + const manifestJson = await fs.readFile(manifestFile, 'utf8') + const manifest: Manifest = JSON.parse(manifestJson) + const report = getReport(manifest) + + // eslint-disable-next-line no-console + console.log(formatReport(report)) +} + +const [, , reportDir] = process.argv + +if (!reportDir) { + throw new Error('report dir not exists') +} + +print(path.resolve(process.cwd(), reportDir)) diff --git a/packages/lhci-report/format.test.ts b/packages/lhci-report/format.test.ts new file mode 100644 index 0000000..c126e6e --- /dev/null +++ b/packages/lhci-report/format.test.ts @@ -0,0 +1,30 @@ +import {green, red, yellow} from 'chalk' +import {formatReport} from './format' + +test('format the report', () => { + const mock = { + 'http://example.com': { + performance: [1, 0.9], + accessibility: [0.5, 0.6], + 'best-practices': [0.6, 0.5], + seo: [0.2, 0.3], + pwa: [0.1, 0.4] + } + } + + const report = formatReport(mock) + + expect(report).toContain('URL: http://example.com') + expect(report).toContain('Number of runs: 2') + expect(report).toContain( + `performance ${green('1.00')} ${green('0.90')}` + ) + expect(report).toContain( + `accessibility ${yellow('0.50')} ${yellow('0.60')}` + ) + expect(report).toContain( + `best-practices ${yellow('0.60')} ${yellow('0.50')}` + ) + expect(report).toContain(`seo ${red('0.20')} ${red('0.30')}`) + expect(report).toContain(`pwa ${red('0.10')} ${red('0.40')}`) +}) diff --git a/packages/lhci-report/format.ts b/packages/lhci-report/format.ts new file mode 100644 index 0000000..8c9f932 --- /dev/null +++ b/packages/lhci-report/format.ts @@ -0,0 +1,43 @@ +import {green, red, yellow, type Chalk} from 'chalk' +import type {Report} from './report' + +const METRIC_NAME_LENGTH = 18 + +const CRITICAL_TRESHOLD = 0.5 +const WARN_TRESHOLD = 0.9 + +const TRESHOLDS: [number, Chalk][] = [ + [CRITICAL_TRESHOLD, red], + [WARN_TRESHOLD, yellow], + [Infinity, green] +] + +function formatMetricValue(value: number) { + const [, color]: any = TRESHOLDS.find(([treshold]) => value < treshold) + + return color(value.toFixed(2)) +} + +export function formatReport(report: Report) { + let output = '\nLighthouse report\n' + + Object.entries(report).forEach(([url, metrics]) => { + output += '\n' + output += `URL: ${url}` + output += '\n' + output += `Number of runs: ${metrics.performance.length}` + output += '\n\n' + + Object.entries(metrics).forEach(([metric, values]) => { + const row = [ + metric.padEnd(METRIC_NAME_LENGTH, ' '), + ...values.map(formatMetricValue) + ] + + output += row.join(' ') + output += '\n' + }) + }) + + return output +} diff --git a/packages/lhci-report/package.json b/packages/lhci-report/package.json new file mode 100644 index 0000000..3b5fc4c --- /dev/null +++ b/packages/lhci-report/package.json @@ -0,0 +1,18 @@ +{ + "name": "@rambler-tech/lhci-report", + "version": "0.0.0", + "cli": "dist/cli.js", + "types": "dist/cli.d.ts", + "license": "MIT", + "sideEffects": false, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@types/chalk": "^0.4.31", + "@types/node": "^20.11.30" + }, + "dependencies": { + "chalk": "^4" + } +} diff --git a/packages/lhci-report/report.test.ts b/packages/lhci-report/report.test.ts new file mode 100644 index 0000000..d4dddc0 --- /dev/null +++ b/packages/lhci-report/report.test.ts @@ -0,0 +1,38 @@ +import {getReport} from './report' + +test('get the report', () => { + const mock = [ + { + url: 'http://example.com', + summary: { + performance: 1, + acccessibility: 0.5, + 'best-practices': 0.6, + seo: 0.2, + pwa: 0.1 + } + }, + { + url: 'http://example.com', + summary: { + performance: 0.9, + acccessibility: 0.6, + 'best-practices': 0.5, + seo: 0.3, + pwa: 0.4 + } + } + ] + + const report = getReport(mock) + + expect(report).toEqual({ + 'http://example.com': { + performance: [1, 0.9], + acccessibility: [0.5, 0.6], + 'best-practices': [0.6, 0.5], + seo: [0.2, 0.3], + pwa: [0.1, 0.4] + } + }) +}) diff --git a/packages/lhci-report/report.ts b/packages/lhci-report/report.ts new file mode 100644 index 0000000..efe151c --- /dev/null +++ b/packages/lhci-report/report.ts @@ -0,0 +1,18 @@ +export type Manifest = { + url: string + summary: Record +}[] + +export type Report = Record> + +export function getReport(manifest: Manifest) { + return manifest.reduce((acc, {url, summary}) => { + Object.entries(summary).forEach(([metric, value]) => { + acc[url] ??= {} + acc[url][metric] ??= [] + acc[url][metric].push(value) + }) + + return acc + }, {} as Report) +} diff --git a/packages/lhci-report/tsconfig.json b/packages/lhci-report/tsconfig.json new file mode 120000 index 0000000..238bf1b --- /dev/null +++ b/packages/lhci-report/tsconfig.json @@ -0,0 +1 @@ +../../tsconfig.package.json \ No newline at end of file diff --git a/tsconfig.package.json b/tsconfig.package.json index c9bbdd7..b81d8e5 100644 --- a/tsconfig.package.json +++ b/tsconfig.package.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "module": "commonjs", "baseUrl": ".", "outDir": "dist" }, diff --git a/yarn.lock b/yarn.lock index b3706fa..698671b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1507,10 +1507,10 @@ resolved "https://registry.yarnpkg.com/@rambler-tech/prettier-config/-/prettier-config-0.1.0.tgz#e80dd5b7ea7f01f3fa30e86d785a68f0656882ff" integrity sha512-WyJcCAPDe7nsClWdBz3eOguhJJhhZRGjXQYWvB1JPbDpljxmiEwLD9IU1TcLvy/Smt9eq9Y4jh1sIupkKxrfmQ== -"@rambler-tech/ts-config@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@rambler-tech/ts-config/-/ts-config-0.1.0.tgz#fd44b9c3af4b1f9b2aeff2bcefe6a21b5befc067" - integrity sha512-bePQ83MwWsAKgKntDZL9iCMCTo7YEyUFEHDryyEhyIq9nDkAe4G7uhEqQTqntAdQ9fx4n+tDaSagtA+sSk1Viw== +"@rambler-tech/ts-config@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@rambler-tech/ts-config/-/ts-config-0.1.1.tgz#02ae41b7909923b341391d8411c708abff2a00c4" + integrity sha512-gQiGU7x3wjdclv9rxgelAlmBNeRJBpxg+nXjmmUzspIOBp4f7u6cOmxKFYlmBJMpk9CyvpHlTG+YFIZ8t6W9Jw== "@rambler-tech/typedoc-config@^0.3.0": version "0.3.0" @@ -1680,6 +1680,11 @@ dependencies: "@babel/types" "^7.20.7" +"@types/chalk@^0.4.31": + version "0.4.31" + resolved "https://registry.yarnpkg.com/@types/chalk/-/chalk-0.4.31.tgz#a31d74241a6b1edbb973cf36d97a2896834a51f9" + integrity sha512-nF0fisEPYMIyfrFgabFimsz9Lnuu9MwkNrrlATm2E4E46afKDyeelT+8bXfw1VSc7sLBxMxRgT7PxTC2JcqN4Q== + "@types/graceful-fs@^4.1.3": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.7.tgz#30443a2e64fd51113bc3e2ba0914d47109695e2a" @@ -1758,6 +1763,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.7.tgz#74d323a93f1391a63477b27b9aec56669c98b2ab" integrity sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g== +"@types/node@^20.11.30": + version "20.11.30" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f" + integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw== + dependencies: + undici-types "~5.26.4" + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -8041,7 +8053,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8115,7 +8136,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8577,6 +8605,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unique-filename@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea" @@ -8848,7 +8881,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -8866,6 +8899,15 @@ wrap-ansi@^6.0.1: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"