diff --git a/package-lock.json b/package-lock.json index d46135e5a..444e316ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "dependencies": { "bundle-require": "^4.0.1", "chalk": "^5.3.0", + "cliui": "^8.0.1", "yargs": "^17.7.2", "zod": "^3.22.1" }, @@ -51,6 +52,7 @@ } }, "dist/packages/cli": { + "name": "@quality-metrics/cli", "version": "0.0.1", "dependencies": { "@quality-metrics/models": "^0.0.1", @@ -65,12 +67,14 @@ } }, "dist/packages/models": { + "name": "@quality-metrics/models", "version": "0.0.1", "dependencies": { "zod": "^3.22.1" } }, "dist/packages/plugin-eslint": { + "name": "@quality-metrics/eslint-plugin", "version": "0.0.1", "dependencies": { "@quality-metrics/models": ">=0.0.1", @@ -78,6 +82,7 @@ } }, "dist/packages/plugin-lighthouse": { + "name": "@quality-metrics/lighthouse-plugin", "version": "0.0.1", "dependencies": { "@quality-metrics/models": ">=0.0.1", @@ -85,9 +90,12 @@ } }, "dist/packages/utils": { + "name": "@quality-metrics/utils", "version": "0.0.1", "dependencies": { - "@quality-metrics/models": "^0.0.1" + "@quality-metrics/models": "^0.0.1", + "chalk": "^5.3.0", + "cliui": "^8.0.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -181,29 +189,29 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", - "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", + "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.19.tgz", - "integrity": "sha512-Q8Yj5X4LHVYTbLCKVz0//2D2aDmHF4xzCdEttYvKOnWvErGsa6geHXD6w46x64n5tP69VfeH+IfSrdyH3MLhwA==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.20.tgz", + "integrity": "sha512-Y6jd1ahLubuYweD/zJH+vvOY141v4f9igNQAQ+MBgq9JlHS2iTsZKn1aMsb3vGccZsXI16VzTBw52Xx0DWmtnA==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.13", "@babel/generator": "^7.22.15", "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.22.19", + "@babel/helper-module-transforms": "^7.22.20", "@babel/helpers": "^7.22.15", "@babel/parser": "^7.22.16", "@babel/template": "^7.22.15", - "@babel/traverse": "^7.22.19", + "@babel/traverse": "^7.22.20", "@babel/types": "^7.22.19", "convert-source-map": "^1.7.0", "debug": "^4.1.0", @@ -367,9 +375,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" @@ -425,16 +433,16 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.19.tgz", - "integrity": "sha512-m6h1cJvn+OJ+R3jOHp30faq5xKJ7VbjwDj5RGgHuRlU9hrMeKsGC+JpihkR5w1g7IfseCPPtZ0r7/hB4UKaYlA==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz", + "integrity": "sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.19" + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -465,14 +473,14 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.17", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.17.tgz", - "integrity": "sha512-bxH77R5gjH3Nkde6/LuncQoLaP16THYPscurp1S8z7S9ZgezCyV3G8Hc+TZiCmY8pz4fp8CvKSgtJMW0FkLAxA==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.17" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -482,13 +490,13 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", - "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { @@ -544,9 +552,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.19.tgz", - "integrity": "sha512-Tinq7ybnEPFFXhlYOYFiSjespWQk0dq2dRNAiMdRTOYQzEGqnnNyrTxPYHP5r6wGjlF1rFgABdDV0g8EwD6Qbg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -562,14 +570,14 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.17", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.17.tgz", - "integrity": "sha512-nAhoheCMlrqU41tAojw9GpVEKDlTS8r3lzFmF0lP52LwblCPbuFSO7nGIZoIcoU5NIm1ABrna0cJExE4Ay6l2Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", "dev": true, "dependencies": { "@babel/helper-function-name": "^7.22.5", "@babel/template": "^7.22.15", - "@babel/types": "^7.22.17" + "@babel/types": "^7.22.19" }, "engines": { "node": ">=6.9.0" @@ -590,12 +598,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.13.tgz", - "integrity": "sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, @@ -1863,12 +1871,12 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.15.tgz", - "integrity": "sha512-tZFHr54GBkHk6hQuVA8w4Fmq+MSPsfvMG0vPnOYyTnJpyfMqybL8/MbNCPRT9zc2KBO2pe4tq15g6Uno4Jpoag==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.20.tgz", + "integrity": "sha512-11MY04gGC4kSzlPHRfvVkNAZhUxOvm7DCJ37hPDnUENwe06npjIRAfInEMTGSb4LZK5ZgDFkv5hw0lGebHeTyg==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", + "@babel/compat-data": "^7.22.20", "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-validator-option": "^7.22.15", @@ -1942,7 +1950,7 @@ "@babel/plugin-transform-unicode-regex": "^7.22.5", "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", "@babel/preset-modules": "0.1.6-no-external-plugins", - "@babel/types": "^7.22.15", + "@babel/types": "^7.22.19", "babel-plugin-polyfill-corejs2": "^0.4.5", "babel-plugin-polyfill-corejs3": "^0.8.3", "babel-plugin-polyfill-regenerator": "^0.5.2", @@ -2031,14 +2039,14 @@ } }, "node_modules/@babel/traverse": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.19.tgz", - "integrity": "sha512-ZCcpVPK64krfdScRbpxF6xA5fz7IOsfMwx1tcACvCzt6JY+0aHkBk7eIU8FRDSZRU5Zei6Z4JfgAxN1bqXGECg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.20.tgz", + "integrity": "sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.22.13", "@babel/generator": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", @@ -4505,19 +4513,6 @@ "node": ">=16.3.0" } }, - "node_modules/@puppeteer/browsers/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@puppeteer/browsers/node_modules/yargs": { "version": "17.7.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", @@ -6586,6 +6581,17 @@ "node": ">=10.12.0" } }, + "node_modules/c8/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "node_modules/c8/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6898,14 +6904,16 @@ } }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/clone": { @@ -8834,9 +8842,9 @@ } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, "node_modules/follow-redirects": { "version": "1.15.2", @@ -12518,6 +12526,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/nx/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "node_modules/nx/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -15665,19 +15684,6 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", diff --git a/package.json b/package.json index e2e3f3852..14f866c15 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "bundle-require": "^4.0.1", "chalk": "^5.3.0", + "cliui": "^8.0.1", "yargs": "^17.7.2", "zod": "^3.22.1" }, diff --git a/packages/cli/src/lib/collect/command-object.spec.ts b/packages/cli/src/lib/collect/command-object.spec.ts index d62dceef5..228fb069a 100644 --- a/packages/cli/src/lib/collect/command-object.spec.ts +++ b/packages/cli/src/lib/collect/command-object.spec.ts @@ -3,11 +3,16 @@ import { CollectOptions } from '@quality-metrics/utils'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { yargsCli } from '../cli'; -import { getDirname } from '../implementation/utils'; +import { getDirname, logErrorBeforeThrow } from '../implementation/utils'; import { middlewares } from '../middlewares'; import { yargsGlobalOptionsDefinition } from '../options'; import { yargsCollectCommandObject } from './command-object'; +const command = { + ...yargsCollectCommandObject(), + handler: logErrorBeforeThrow(yargsCollectCommandObject().handler), +}; + const outputPath = 'collect-command-object.json'; const dummyConfig: CoreConfig = { persist: { outputPath }, @@ -18,12 +23,10 @@ const dummyConfig: CoreConfig = { describe('collect-command-object', () => { it('should parse arguments correctly', async () => { const args = ['collect', '--verbose', '--configPath', '']; - const cli = yargsCli([], { options: yargsGlobalOptionsDefinition() }) + const cli = yargsCli(args, { options: yargsGlobalOptionsDefinition() }) .config(dummyConfig) - .command(yargsCollectCommandObject()); - const parsedArgv = (await cli.parseAsync( - args, - )) as unknown as CollectOptions; + .command(command); + const parsedArgv = (await cli.argv) as unknown as CollectOptions; const { persist } = parsedArgv; const { outputPath: outPath } = persist; expect(outPath).toBe(outputPath); @@ -44,7 +47,7 @@ describe('collect-command-object', () => { ]; await yargsCli([], { middlewares }) .config(dummyConfig) - .command(yargsCollectCommandObject()) + .command(command) .parseAsync(args); const report = JSON.parse(readFileSync(outputPath).toString()) as Report; expect(report.plugins[0]?.meta.slug).toBe('collect-command-object'); diff --git a/packages/cli/src/lib/collect/command-object.ts b/packages/cli/src/lib/collect/command-object.ts index f843fa94f..977541e8b 100644 --- a/packages/cli/src/lib/collect/command-object.ts +++ b/packages/cli/src/lib/collect/command-object.ts @@ -1,13 +1,17 @@ -import { collect, CollectOptions } from '@quality-metrics/utils'; +import { collect, CollectOptions, persistReport } from '@quality-metrics/utils'; import { writeFile } from 'fs/promises'; import { CommandModule } from 'yargs'; export function yargsCollectCommandObject() { - const handler = async (args: CollectOptions): Promise => { - const collectOutput = await collect(args); + const handler = async ( + config: CollectOptions & { format: string }, + ): Promise => { + const report = await collect(config); - const { persist } = args; - await writeFile(persist.outputPath, JSON.stringify(collectOutput, null, 2)); + const { persist } = config; + await persistReport(report, config); + + await writeFile(persist.outputPath, JSON.stringify(report, null, 2)); }; return { diff --git a/packages/cli/src/lib/implementation/config-middleware.spec.ts b/packages/cli/src/lib/implementation/config-middleware.spec.ts index af4d322cf..d8834ecab 100644 --- a/packages/cli/src/lib/implementation/config-middleware.spec.ts +++ b/packages/cli/src/lib/implementation/config-middleware.spec.ts @@ -12,7 +12,6 @@ const configPath = (ext: string) => describe('applyConfigMiddleware', () => { it('should load valid .mjs config', async () => { const configPathMjs = configPath('mjs'); - console.log('configPathMjs: ', configPathMjs); const config = await configMiddleware({ configPath: configPathMjs }); expect(config.configPath).toContain('.mjs'); expect(config.persist.outputPath).toContain('mjs-'); diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 88dfe884b..726d61968 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -1,28 +1,24 @@ export { CategoryConfig, categoryConfigSchema } from './lib/category-config'; +export { GlobalCliArgs, globalCliArgsSchema } from './lib/global-cli-options'; export { CoreConfig, coreConfigSchema, refineCoreConfig, unrefinedCoreConfigSchema, } from './lib/core-config'; -export { GlobalCliArgs, globalCliArgsSchema } from './lib/global-cli-options'; export { PersistConfig, persistConfigSchema } from './lib/persist-config'; +export { UploadConfig, uploadConfigSchema } from './lib/upload-config'; export { AuditGroup, AuditMetadata, - Issue, PluginConfig, - RunnerOutput, + PluginOutput, auditGroupSchema, auditMetadataSchema, - issueSchema, pluginConfigSchema, - runnerOutputSchema, + Issue, + PluginRunnerOutput, + issueSchema, + pluginRunnerOutputSchema, } from './lib/plugin-config'; -export { - PluginOutput, - PluginReport, - Report, - runnerOutputAuditRefsPresentInPluginConfigs, -} from './lib/report'; -export { UploadConfig, uploadConfigSchema } from './lib/upload-config'; +export { PluginReport, AuditReport, Report, reportSchema } from './lib/report'; diff --git a/packages/models/src/lib/category-config.spec.ts b/packages/models/src/lib/category-config.spec.ts index 68574535f..e56a8f9b0 100644 --- a/packages/models/src/lib/category-config.spec.ts +++ b/packages/models/src/lib/category-config.spec.ts @@ -5,29 +5,24 @@ import { mockCategory } from '../../test'; describe('categoryConfigSchema', () => { it('should parse if configuration with audit refs is valid', () => { const cfg = mockCategory({ - auditRefOrGroupRef: [ - { type: 'audit', plugin: 'test', slug: 'a' }, - { type: 'audit', plugin: 'test', slug: 'b' }, - ], + pluginSlug: 'test', + auditSlug: 'a', }); expect(() => categoryConfigSchema.parse(cfg)).not.toThrow(); }); it('should parse if configuration with group refs is valid', () => { const cfg = mockCategory({ - auditRefOrGroupRef: [{ type: 'group', plugin: 'es-lint', slug: 'base' }], + pluginSlug: 'test', + groupSlug: 'g', }); expect(() => categoryConfigSchema.parse(cfg)).not.toThrow(); }); it('should throw if duplicate refs to audits or groups in metrics are given', () => { - const duplicatedSlug = { - type: 'audit' as const, - plugin: 'test', - slug: 'a', - }; + const duplicatedSlug = 'a'; const cfg = mockCategory({ - auditRefOrGroupRef: [duplicatedSlug, duplicatedSlug], + auditSlug: [duplicatedSlug, duplicatedSlug], }); expect(() => categoryConfigSchema.parse(cfg)).toThrow( 'the following audit or group refs are duplicates', diff --git a/packages/models/src/lib/category-config.ts b/packages/models/src/lib/category-config.ts index 6edcbe349..abd0454ad 100644 --- a/packages/models/src/lib/category-config.ts +++ b/packages/models/src/lib/category-config.ts @@ -6,7 +6,7 @@ import { } from './implementation/schemas'; import { errorItems, hasDuplicateStrings } from './implementation/utils'; -type RefsList = { +type _RefsList = { type?: string; slug?: string; plugin?: string; @@ -21,7 +21,7 @@ export const categoryConfigSchema = scorableSchema( z.object({ type: z.enum(['audit', 'group'], { description: - 'Discrimant for reference kind, affects where `slug` is looked up', + 'Discriminant for reference kind, affects where `slug` is looked up', }), plugin: slugSchema( 'Plugin slug (plugin should contain referenced audit or group)', @@ -35,13 +35,13 @@ export const categoryConfigSchema = scorableSchema( export type CategoryConfig = z.infer; // helper for validator: categories have unique refs to audits or groups -export function duplicateRefsInCategoryMetricsErrorMsg(metrics: RefsList) { +export function duplicateRefsInCategoryMetricsErrorMsg(metrics: _RefsList) { const duplicateRefs = getDuplicateRefsInCategoryMetrics(metrics); return `In the categories, the following audit or group refs are duplicates: ${errorItems( duplicateRefs, )}`; } -function getDuplicateRefsInCategoryMetrics(metrics: RefsList) { +function getDuplicateRefsInCategoryMetrics(metrics: _RefsList) { return hasDuplicateStrings( metrics.map(({ slug, type, plugin }) => `${type} :: ${plugin} / ${slug}`), ); diff --git a/packages/models/src/lib/core-config.spec.ts b/packages/models/src/lib/core-config.spec.ts index 4112055e9..ba1a80e52 100644 --- a/packages/models/src/lib/core-config.spec.ts +++ b/packages/models/src/lib/core-config.spec.ts @@ -15,14 +15,12 @@ import { mockCategory, mockConfig, mockPluginConfig } from '../../test'; describe('CoreConfig', () => { it('should parse if configuration is valid', () => { const cfg = mockConfig({ pluginSlug: 'test', auditSlug: ['a', 'b'] }); - cfg.categories.push( + cfg.categories = [ mockCategory({ - auditRefOrGroupRef: [ - { type: 'audit', plugin: 'test', slug: 'a' }, - { type: 'audit', plugin: 'test', slug: 'b' }, - ], + pluginSlug: 'test', + auditSlug: ['a', 'b'], }), - ); + ]; expect(() => coreConfigSchema.parse(cfg)).not.toThrow(); }); @@ -34,10 +32,9 @@ describe('CoreConfig', () => { ]; cfg.categories = [ mockCategory({ - auditRefOrGroupRef: [ - { plugin: 'plg', slug: 'lcp', type: 'audit' }, - { plugin: 'plg', slug: 'perf', type: 'group' }, - ], + pluginSlug, + groupSlug: ['perf'], + auditSlug: ['lcp'], }), ]; // In the categories, the following plugin refs do not exist in the provided plugins: test#group:group-slug @@ -50,11 +47,11 @@ describe('CoreConfig', () => { cfg.categories.push( mockCategory({ categorySlug: 'test', - auditRefOrGroupRef: [{ type: 'audit', plugin: 'test', slug: 'a' }], + auditSlug: ['a'], }), mockCategory({ categorySlug: 'test', - auditRefOrGroupRef: [{ type: 'audit', plugin: 'test', slug: 'b' }], + auditSlug: ['b'], }), ); expect(() => coreConfigSchema.parse(cfg)).toThrow( @@ -64,20 +61,15 @@ describe('CoreConfig', () => { it('should throw if ref in a category does not exist in audits', () => { const cfg = mockConfig({ pluginSlug: 'test', auditSlug: ['a', 'b'] }); - cfg.categories.push( + cfg.categories = [ mockCategory({ - categorySlug: 'test', - auditRefOrGroupRef: [ - { - type: 'audit', - plugin: 'missing-plugin-slug-in-category', - slug: 'auditref', - }, - ], + pluginSlug: 'test', + categorySlug: 'test-category', + auditSlug: ['auditref'], }), - ); + ]; expect(() => coreConfigSchema.parse(cfg)).toThrow( - `In the categories, the following plugin refs do not exist in the provided plugins: missing-plugin-slug-in-category/auditref`, + `In the categories, the following plugin refs do not exist in the provided plugins: test/auditref`, ); }); @@ -87,20 +79,16 @@ describe('CoreConfig', () => { auditSlug: ['a', 'b'], groupSlug: 'a', }); - cfg.categories.push( + + cfg.categories = [ mockCategory({ + pluginSlug: 'test', categorySlug: 'test-slug', - auditRefOrGroupRef: [ - { - type: 'group', - plugin: 'missing-plugin-slug-in-category', - slug: 'groupref', - }, - ], + groupSlug: ['groupref'], }), - ); + ]; expect(() => coreConfigSchema.parse(cfg)).toThrow( - `In the categories, the following plugin refs do not exist in the provided plugins: missing-plugin-slug-in-category/groupref (group)`, + `In the categories, the following plugin refs do not exist in the provided plugins: test#groupref (group)`, ); }); }); diff --git a/packages/models/src/lib/core-config.ts b/packages/models/src/lib/core-config.ts index 8b7df1a9c..c6f250555 100644 --- a/packages/models/src/lib/core-config.ts +++ b/packages/models/src/lib/core-config.ts @@ -1,33 +1,14 @@ import { Schema, z } from 'zod'; -import { CategoryConfig, categoryConfigSchema } from './category-config'; import { errorItems, hasDuplicateStrings, hasMissingStrings, } from './implementation/utils'; -import { persistConfigSchema } from './persist-config'; +import { CategoryConfig, categoryConfigSchema } from './category-config'; import { pluginConfigSchema } from './plugin-config'; +import { persistConfigSchema } from './persist-config'; import { uploadConfigSchema } from './upload-config'; -/** - * Define Zod schema for the CoreConfig type - * - * @example - * - * // Example data for the CoreConfig type - * const data = { - * // ... populate with example data ... - * }; - * - * // Validate the data against the schema - * const validationResult = coreConfigSchema.safeParse(data); - * - * if (validationResult.success) { - * console.log('Valid plugin config:', validationResult.data); - * } else { - * console.error('Invalid plugin config:', validationResult.error); - * } - */ export const unrefinedCoreConfigSchema = z.object({ plugins: z.array(pluginConfigSchema, { description: @@ -102,11 +83,11 @@ function getMissingRefsForCategories(coreCfg: CoreConfig) { const groupRefsFromCategory = coreCfg.categories.flatMap(({ refs }) => refs .filter(({ type }) => type === 'group') - .map(({ plugin, slug }) => `${plugin}/${slug} (group)`), + .map(({ plugin, slug }) => `${plugin}#${slug} (group)`), ); const groupRefsFromPlugins = coreCfg.plugins.flatMap(({ groups, meta }) => { return Array.isArray(groups) - ? groups.map(({ slug }) => `${meta.slug}/${slug} (group)`) + ? groups.map(({ slug }) => `${meta.slug}#${slug} (group)`) : []; }); const missingGroupRefs = hasMissingStrings( diff --git a/packages/models/src/lib/global-cli-options.ts b/packages/models/src/lib/global-cli-options.ts index a44dd5f74..f07083aa6 100644 --- a/packages/models/src/lib/global-cli-options.ts +++ b/packages/models/src/lib/global-cli-options.ts @@ -1,26 +1,6 @@ import { z } from 'zod'; import { generalFilePathSchema } from './implementation/schemas'; -/** - * Define Zod schema for the GlobalCliArgs type - * - * @example - * - * // Example data for the GlobalCliArgs type - * const args = { - * // ... - * }; - * - * // Validate the data against the schema - * const validationResult = globalCliArgsSchema.safeParse(args); - * - * if (validationResult.success) { - * console.log('Valid config:', validationResult.data); - * } else { - * console.error('Invalid config:', validationResult.error); - * } - * - */ export const globalCliArgsSchema = z.object({ interactive: z .boolean({ diff --git a/packages/models/src/lib/implementation/schemas.ts b/packages/models/src/lib/implementation/schemas.ts index e34918e7b..61806f9ac 100644 --- a/packages/models/src/lib/implementation/schemas.ts +++ b/packages/models/src/lib/implementation/schemas.ts @@ -101,7 +101,7 @@ export function packageVersionSchema(options?: { const versionSchema = z.string({ description: versionDescription }); return z.object( { - package: optional ? packageSchema.optional() : packageSchema, + packageName: optional ? packageSchema.optional() : packageSchema, version: optional ? versionSchema.optional() : versionSchema, }, { description: 'NPM package name and version of a published package' }, diff --git a/packages/models/src/lib/persist-config.ts b/packages/models/src/lib/persist-config.ts index cfb895cef..32523b5c6 100644 --- a/packages/models/src/lib/persist-config.ts +++ b/packages/models/src/lib/persist-config.ts @@ -1,28 +1,12 @@ import { z } from 'zod'; import { generalFilePathSchema } from './implementation/schemas'; -/** - * Define Zod schema for the PersistConfig type - * - * @example - * - * // Example data for the type - * const data = { - * // ... - * }; - * - * // Validate the data against the schema - * const validationResult = persistConfigSchema.safeParse(data); - * - * if (validationResult.success) { - * console.log('Valid persist config:', validationResult.data); - * } else { - * console.error('Invalid persist config:', validationResult.error); - * } - * - */ export const persistConfigSchema = z.object({ outputPath: generalFilePathSchema('Artifacts folder'), + format: z + .array(z.enum(['json', 'stdout', 'md'])) + .default(['stdout']) + .optional(), }); export type PersistConfig = z.infer; diff --git a/packages/models/src/lib/plugin-config.spec.ts b/packages/models/src/lib/plugin-config.spec.ts index feb08bdd9..bb19eced3 100644 --- a/packages/models/src/lib/plugin-config.spec.ts +++ b/packages/models/src/lib/plugin-config.spec.ts @@ -7,7 +7,7 @@ import { import { auditGroupSchema, pluginConfigSchema, - runnerOutputSchema, + pluginRunnerOutputSchema, } from './plugin-config'; describe('pluginConfigSchema', () => { @@ -96,19 +96,19 @@ describe('pluginConfigSchema', () => { describe('runnerOutputSchema', () => { it('should pass if output audits are valid', () => { const out = mockRunnerOutput(); - expect(() => runnerOutputSchema.parse(out)).not.toThrow(); + expect(() => pluginRunnerOutputSchema.parse(out)).not.toThrow(); }); it('should throw if slugs of audits are invalid', () => { const out = mockRunnerOutput({ auditSlug: '-invalid-audit-slug' }); - expect(() => runnerOutputSchema.parse(out)).toThrow( + expect(() => pluginRunnerOutputSchema.parse(out)).toThrow( 'slug has to follow the pattern', ); }); it('should throw if slugs of audits are duplicated', () => { const out = mockRunnerOutput({ auditSlug: ['a', 'a'] }); - expect(() => runnerOutputSchema.parse(out)).toThrow( + expect(() => pluginRunnerOutputSchema.parse(out)).toThrow( 'In plugin audits the slugs are not unique', ); }); diff --git a/packages/models/src/lib/plugin-config.ts b/packages/models/src/lib/plugin-config.ts index f07ef22cc..b0ca4dc1b 100644 --- a/packages/models/src/lib/plugin-config.ts +++ b/packages/models/src/lib/plugin-config.ts @@ -18,7 +18,6 @@ import { hasMissingStrings, } from './implementation/utils'; -// Define Zod schema for the PluginMetadata type export const pluginMetadataSchema = packageVersionSchema({ optional: true, }).merge( @@ -41,7 +40,6 @@ export const pluginMetadataSchema = packageVersionSchema({ ), ); -// Define Zod schema for the RunnerConfig type const runnerConfigSchema = z.object( { command: z.string({ @@ -55,12 +53,11 @@ const runnerConfigSchema = z.object( }, ); -// Define Zod schema for the AuditMetadata type export const auditMetadataSchema = z.object( { slug: slugSchema('ID (unique within plugin)'), title: titleSchema('Descriptive name'), - description: descriptionSchema('Description (Markdown)'), + description: descriptionSchema('Description (markdown)'), docsUrl: docsUrlSchema('Link to documentation (rationale)'), }, { description: 'List of scorable metrics for the given plugin' }, @@ -68,7 +65,6 @@ export const auditMetadataSchema = z.object( export type AuditMetadata = z.infer; -// Define Zod schema for the `Group` type export const auditGroupSchema = scorableSchema( 'An audit group aggregates a set of audits into a single score which can be referenced from a category. ' + 'E.g. the group slug "performance" groups audits and can be referenced in a category as "[plugin-slug]#group:[group-slug]")', @@ -79,28 +75,8 @@ export const auditGroupSchema = scorableSchema( getDuplicateRefsInGroups, duplicateRefsInGroupsErrorMsg, ); - export type AuditGroup = z.infer; -/** - * Define Zod schema for the PluginConfig type - * - * @example - * - * // Example data for the PluginConfig type - * const data = { - * // ... - * }; - * - * // Validate the data against the schema - * const validationResult = pluginConfigSchema.safeParse(data); - * - * if (validationResult.success) { - * console.log('Valid plugin config:', validationResult.data); - * } else { - * console.error('Invalid plugin config:', validationResult.error); - * } - */ export const pluginConfigSchema = z .object({ meta: pluginMetadataSchema, @@ -138,6 +114,42 @@ export const pluginConfigSchema = z export type PluginConfig = z.infer; +// helper for validator: group refs are unique +function duplicateSlugsInGroupsErrorMsg(groups: AuditGroup[] | undefined) { + const duplicateRefs = getDuplicateSlugsInGroups(groups); + return `In groups the slugs are not unique: ${errorItems(duplicateRefs)}`; +} +function getDuplicateSlugsInGroups(groups: AuditGroup[] | undefined) { + return Array.isArray(groups) + ? hasDuplicateStrings(groups.map(({ slug }) => slug)) + : false; +} + +type _PluginCfg = { + audits?: AuditMetadata[]; + groups?: AuditGroup[]; +}; +// helper for validator: every listed group ref points to an audit within the plugin +function missingRefsFromGroupsErrorMsg(pluginCfg: _PluginCfg) { + const missingRefs = getMissingRefsFromGroups(pluginCfg); + return `In the groups, the following audit ref's needs to point to a audit in this plugin config: ${errorItems( + missingRefs, + )}`; +} +function getMissingRefsFromGroups(pluginCfg: _PluginCfg) { + if (pluginCfg?.groups?.length && pluginCfg?.audits?.length) { + const groups = pluginCfg?.groups || []; + const audits = pluginCfg?.audits || []; + return hasMissingStrings( + groups.flatMap(({ refs: audits }) => audits.map(({ slug: ref }) => ref)), + audits.map(({ slug }) => slug), + ); + } + return false; +} + +// ======= + const sourceFileLocationSchema = z.object( { file: unixFilePathSchema('Relative path to source file in Git repo'), @@ -156,25 +168,19 @@ const sourceFileLocationSchema = z.object( { description: 'Source file location' }, ); -/** - * Define Zod schema for the Issue type. - */ export const issueSchema = z.object( { message: z.string({ description: 'Descriptive error message' }).max(128), severity: z.enum(['info', 'warning', 'error'], { description: 'Severity level', }), - // "Reference to source code" source: sourceFileLocationSchema.optional(), }, { description: 'Issue information' }, ); export type Issue = z.infer; -/** - * Define Zod schema for the Audit type. - */ -export const auditResultSchema = z.object( + +export const auditOutputSchema = z.object( { slug: slugSchema('References audit metadata'), displayValue: z @@ -199,16 +205,17 @@ export const auditResultSchema = z.object( }, { description: 'Audit information' }, ); +export type AuditOutput = z.infer; -export type AuditResult = z.infer; - -/** - * Define Zod schema for the RunnerOutput type. - */ -export const runnerOutputSchema = z.object( +export type PluginOutput = PluginRunnerOutput & { + slug: string; + date: string; + duration: number; +}; +export const pluginRunnerOutputSchema = z.object( { audits: z - .array(auditResultSchema, { description: 'List of audits' }) + .array(auditOutputSchema, { description: 'List of audits' }) // audit slugs are unique .refine( audits => !getDuplicateSlugsInAudits(audits), @@ -217,65 +224,30 @@ export const runnerOutputSchema = z.object( }, { description: 'JSON formatted output emitted by the runner.' }, ); -export type RunnerOutput = z.infer; +export type PluginRunnerOutput = z.infer; // helper for validator: audit slugs are unique -function duplicateSlugsInAuditsErrorMsg(audits: AuditResult[]) { +function duplicateSlugsInAuditsErrorMsg(audits: AuditOutput[]) { const duplicateRefs = getDuplicateSlugsInAudits(audits); return `In plugin audits the slugs are not unique: ${errorItems( duplicateRefs, )}`; } -function getDuplicateSlugsInAudits(audits: AuditResult[]) { +function getDuplicateSlugsInAudits(audits: AuditOutput[]) { return hasDuplicateStrings(audits.map(({ slug }) => slug)); } -// helper for validator: group refs are unique -function duplicateSlugsInGroupsErrorMsg(groups: AuditGroup[] | undefined) { - const duplicateRefs = getDuplicateSlugsInGroups(groups); - return `In groups the slugs are not unique: ${errorItems(duplicateRefs)}`; -} -function getDuplicateSlugsInGroups(groups: AuditGroup[] | undefined) { - return Array.isArray(groups) - ? hasDuplicateStrings(groups.map(({ slug }) => slug)) - : false; -} - -type RefsList = { slug?: string }[]; +type _RefsList = { slug?: string }[]; // helper for validator: group refs are unique -function duplicateRefsInGroupsErrorMsg(groupAudits: RefsList) { +function duplicateRefsInGroupsErrorMsg(groupAudits: _RefsList) { const duplicateRefs = getDuplicateRefsInGroups(groupAudits); return `In plugin groups the audit refs are not unique: ${errorItems( duplicateRefs, )}`; } -function getDuplicateRefsInGroups(groupAudits: RefsList) { +function getDuplicateRefsInGroups(groupAudits: _RefsList) { return hasDuplicateStrings( groupAudits.map(({ slug: ref }) => ref).filter(exists), ); } -type PluginCfg = { - audits?: AuditMetadata[]; - groups?: AuditGroup[]; -}; - -// helper for validator: every listed group ref points to an audit within the plugin -function missingRefsFromGroupsErrorMsg(pluginCfg: PluginCfg) { - const missingRefs = getMissingRefsFromGroups(pluginCfg); - return `In the groups, the following audit ref's needs to point to a audit in this plugin config: ${errorItems( - missingRefs, - )}`; -} - -function getMissingRefsFromGroups(pluginCfg: PluginCfg) { - if (pluginCfg?.groups?.length && pluginCfg?.audits?.length) { - const groups = pluginCfg?.groups || []; - const audits = pluginCfg?.audits || []; - return hasMissingStrings( - groups.flatMap(({ refs: audits }) => audits.map(({ slug: ref }) => ref)), - audits.map(({ slug }) => slug), - ); - } - return false; -} diff --git a/packages/models/src/lib/report.spec.ts b/packages/models/src/lib/report.spec.ts index 70b6c7d3f..cc3374ced 100644 --- a/packages/models/src/lib/report.spec.ts +++ b/packages/models/src/lib/report.spec.ts @@ -4,18 +4,24 @@ import { runnerOutputAuditRefsPresentInPluginConfigs } from './report'; describe('RunnerOutput', () => { it('should pass if output audits are valid', () => { - const plugin = mockPluginConfig({ pluginSlug: 'test', auditSlug: ['a'] }); - const out = mockRunnerOutput({ auditSlug: 'test#a' }); - expect(runnerOutputAuditRefsPresentInPluginConfigs(out, plugin)).toBe( - false, - ); + const pluginCfg = mockPluginConfig({ + pluginSlug: 'test', + auditSlug: ['a'], + }); + const runnerOutput = mockRunnerOutput({ auditSlug: 'test#a' }); + expect( + runnerOutputAuditRefsPresentInPluginConfigs(runnerOutput, pluginCfg), + ).toBe(false); }); it('should throw if output audits are not in config', () => { - const plugin = mockPluginConfig({ pluginSlug: 'test', auditSlug: ['a'] }); - const out = mockRunnerOutput({ auditSlug: 'test#b' }); - expect(runnerOutputAuditRefsPresentInPluginConfigs(out, plugin)).toEqual([ - 'test#b', - ]); + const pluginCfg = mockPluginConfig({ + pluginSlug: 'test', + auditSlug: ['a'], + }); + const runnerOutput = mockRunnerOutput({ auditSlug: 'test#b' }); + expect( + runnerOutputAuditRefsPresentInPluginConfigs(runnerOutput, pluginCfg), + ).toEqual(['test#b']); }); }); diff --git a/packages/models/src/lib/report.ts b/packages/models/src/lib/report.ts index b8924d6cc..037a42517 100644 --- a/packages/models/src/lib/report.ts +++ b/packages/models/src/lib/report.ts @@ -2,20 +2,14 @@ import { z } from 'zod'; import { hasMissingStrings } from './implementation/utils'; import { PluginConfig, - RunnerOutput, + PluginRunnerOutput, auditMetadataSchema, - auditResultSchema, + auditOutputSchema, pluginMetadataSchema, } from './plugin-config'; import { packageVersionSchema } from './implementation/schemas'; -export type PluginOutput = RunnerOutput & { - slug: string; - date: string; - duration: number; -}; - -export const auditReportSchema = auditMetadataSchema.merge(auditResultSchema); +export const auditReportSchema = auditMetadataSchema.merge(auditOutputSchema); export type AuditReport = z.infer; export const pluginReportSchema = z.object({ @@ -47,7 +41,7 @@ export type Report = z.infer; * */ export function runnerOutputAuditRefsPresentInPluginConfigs( - out: RunnerOutput, + out: PluginRunnerOutput, cfg: PluginConfig, ): string[] | false { const outRefs = out.audits.map(({ slug }) => slug); diff --git a/packages/models/src/lib/upload-config.ts b/packages/models/src/lib/upload-config.ts index 0ed13ab7d..c088e00d8 100644 --- a/packages/models/src/lib/upload-config.ts +++ b/packages/models/src/lib/upload-config.ts @@ -1,26 +1,6 @@ import { z } from 'zod'; import { urlSchema } from './implementation/schemas'; -/** - * Define Zod schema for the UploadConfig type - * - * @example - * - * // Example data for the UploadConfig type - * const data = { - * // ... - * }; - * - * // Validate the data against the schema - * const validationResult = uploadConfigSchema.safeParse(data); - * - * if (validationResult.success) { - * console.log('Valid upload config:', validationResult.data); - * } else { - * console.error('Invalid upload config:', validationResult.error); - * } - * - */ export const uploadConfigSchema = z.object({ server: urlSchema('URL of deployed portal API'), apiKey: z.string({ diff --git a/packages/models/test/helpers.mock.ts b/packages/models/test/helpers.mock.ts deleted file mode 100644 index 786955122..000000000 --- a/packages/models/test/helpers.mock.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { CategoryConfig } from '../src/lib/category-config'; -import { CoreConfig } from '../src/lib/core-config'; -import { PersistConfig } from '../src/lib/persist-config'; -import { - AuditGroup, - AuditMetadata, - PluginConfig, - RunnerOutput, -} from '../src/lib/plugin-config'; -import { UploadConfig } from '../src/lib/upload-config'; - -export function mockConfig(opt?: { - pluginSlug?: string | string[]; - auditSlug?: string | string[]; - groupSlug?: string | string[]; -}): CoreConfig { - let { pluginSlug, auditSlug, groupSlug } = opt || {}; - pluginSlug = pluginSlug || 'mock-plugin-slug'; - auditSlug = auditSlug || 'mock-audit-slug'; - groupSlug = groupSlug || 'mock-group-slug'; - return { - persist: { outputPath: 'command-object-config-out.json' }, - plugins: Array.isArray(pluginSlug) - ? pluginSlug.map(p => mockPluginConfig({ pluginSlug: p, auditSlug })) - : [mockPluginConfig({ pluginSlug, auditSlug, groupSlug })], - categories: [], - }; -} - -export function mockPluginConfig(opt?: { - pluginSlug?: string; - auditSlug?: string | string[]; - groupSlug?: string | string[]; - outputPath?: string; -}): PluginConfig { - const { groupSlug } = opt || {}; - let { pluginSlug, auditSlug, outputPath } = opt || {}; - pluginSlug = pluginSlug || 'mock-plugin-slug'; - auditSlug = auditSlug || 'mock-audit-slug'; - const addGroups = groupSlug !== undefined; - outputPath = outputPath || 'out-execute-plugin.json'; - - const audits = Array.isArray(auditSlug) - ? auditSlug.map(slug => mockAuditConfig({ auditSlug: slug })) - : [mockAuditConfig({ auditSlug })]; - - let groups: AuditGroup[] = []; - if (addGroups) { - groups = Array.isArray(groupSlug) - ? groupSlug.map(slug => mockGroupConfig({ groupSlug: slug })) - : [mockGroupConfig({ groupSlug, auditSlug })]; - } - - return { - audits, - groups, - runner: { - command: 'bash', - args: [ - '-c', - `echo '${JSON.stringify({ - audits: audits.map(({ slug }, idx) => ({ - slug: `${slug}`, - value: parseFloat('0.' + idx), - })), - } satisfies RunnerOutput)}' > ${outputPath}`, - ], - outputPath: outputPath, - }, - meta: { - slug: pluginSlug, - name: 'execute plugin', - }, - }; -} - -export function mockAuditConfig(opt?: { auditSlug?: string }): AuditMetadata { - let { auditSlug } = opt || {}; - auditSlug = auditSlug || 'mock-audit-slug'; - - return { - slug: auditSlug, - title: 'audit title', - description: 'audit description', - docsUrl: 'http://www.my-docs.dev', - }; -} - -export function mockAuditRef(opt?: { - pluginSlug?: string; - auditSlug?: string; -}): CategoryConfig['refs'][0] { - const { auditSlug = 'mock-audit-slug', pluginSlug = 'mock-plugin-slug' } = - opt || {}; - - return { - type: 'audit', - slug: auditSlug, - plugin: pluginSlug, - weight: 0, - }; -} - -export function mockGroupConfig(opt?: { - groupSlug?: string; - auditSlug?: string | string[]; -}): AuditGroup { - let { groupSlug, auditSlug } = opt || {}; - groupSlug = groupSlug || 'mock-group-slug'; - auditSlug = auditSlug || 'mock-audit-slug'; - const refs = Array.isArray(auditSlug) - ? auditSlug.map(slug => ({ slug, weight: 0 })) - : [{ slug: auditSlug, weight: 0 }]; - return { - slug: groupSlug, - title: 'group title', - description: 'group description', - refs, - }; -} - -type WeightlessRef = Omit; - -export function mockCategory(opt?: { - categorySlug?: string; - auditRefOrGroupRef?: WeightlessRef | WeightlessRef[]; -}): CategoryConfig { - let { auditRefOrGroupRef, categorySlug } = opt || {}; - categorySlug = categorySlug || 'mock-category-slug'; - auditRefOrGroupRef = auditRefOrGroupRef || mockAuditRef(); - - const refs = ( - Array.isArray(auditRefOrGroupRef) - ? auditRefOrGroupRef - : [auditRefOrGroupRef] - ).map(ref => ({ ...ref, weight: 0 })); - - return { - slug: categorySlug, - title: 'Mock category title', - description: 'mock description', - refs, - }; -} - -export function mockUploadConfig(opt?: Partial): UploadConfig { - return { - apiKey: 'm0ck-API-k3y', - server: 'http://test.server.io', - ...opt, - }; -} -export function mockPersistConfig(opt?: Partial): PersistConfig { - return { - outputPath: 'mock-output-path.json', - ...opt, - }; -} - -export function mockRunnerOutput(opt?: { - auditSlug: string | string[]; -}): RunnerOutput { - let { auditSlug } = opt || {}; - auditSlug = auditSlug || 'mock-audit-output-slug'; - const audits = Array.isArray(auditSlug) - ? auditSlug.map((slug, idx) => ({ - slug, - value: idx, - displayValue: '', - score: 0, - })) - : [ - { - slug: auditSlug, - value: 12, - displayValue: '', - score: 0, - }, - ]; - - return { - audits, - }; -} diff --git a/packages/models/test/index.ts b/packages/models/test/index.ts index c50d87c2b..ef568049b 100644 --- a/packages/models/test/index.ts +++ b/packages/models/test/index.ts @@ -1 +1,4 @@ -export * from './helpers.mock'; +export * from './schema.mock'; +export * from './test-data/config-and-report-dummy.mock'; +export * from './test-data/config-and-report-nx-validators.mock'; +export * from './test-data/config-and-report-lighthouse.mock'; diff --git a/packages/models/test/schema.mock.spec.ts b/packages/models/test/schema.mock.spec.ts new file mode 100644 index 000000000..783623df9 --- /dev/null +++ b/packages/models/test/schema.mock.spec.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest'; +import { coreConfigSchema, pluginConfigSchema, reportSchema } from '../src'; +import { + dummyConfig, + dummyReport, +} from './test-data/config-and-report-dummy.mock'; +import { + nxValidatorsOnlyConfig, + nxValidatorsOnlyReport, + nxValidatorsPlugin, +} from './test-data/config-and-report-nx-validators.mock'; +import { + lighthouseConfig, + lighthousePlugin, + lighthouseReport, +} from './test-data/config-and-report-lighthouse.mock'; + +// @NOTICE ATM the data structure changes a lot so this test is a temporarily check to see if the dummy data are correct +describe('dummy data', () => { + it('is valid', () => { + expect(() => coreConfigSchema.parse(dummyConfig)).not.toThrow(); + expect(() => reportSchema.parse(dummyReport)).not.toThrow(); + }); +}); + +describe('Nx Validators data', () => { + it('is valid', () => { + expect(() => pluginConfigSchema.parse(nxValidatorsPlugin())).not.toThrow(); + expect(() => coreConfigSchema.parse(nxValidatorsOnlyConfig)).not.toThrow(); + expect(() => reportSchema.parse(nxValidatorsOnlyReport)).not.toThrow(); + }); +}); + +describe('lighthouse data', () => { + it('is valid', () => { + expect(() => pluginConfigSchema.parse(lighthousePlugin())).not.toThrow(); + expect(() => coreConfigSchema.parse(lighthouseConfig)).not.toThrow(); + expect(() => reportSchema.parse(lighthouseReport)).not.toThrow(); + }); +}); diff --git a/packages/models/test/schema.mock.ts b/packages/models/test/schema.mock.ts new file mode 100644 index 000000000..f8fc0444c --- /dev/null +++ b/packages/models/test/schema.mock.ts @@ -0,0 +1,284 @@ +import { + AuditGroup, + AuditMetadata, + CategoryConfig, + CoreConfig, + PersistConfig, + PluginConfig, + PluginReport, + Report, + PluginRunnerOutput, + AuditReport, + UploadConfig, +} from '../src/index'; + +const __pluginSlug__ = 'mock-plugin-slug'; +const __auditSlug__ = 'mock-audit-slug'; +const __groupSlug__ = 'mock-group-slug'; +const __categorySlug__ = 'mock-category-slug'; +const __outputPath__ = 'out-execute-plugin.json'; +const randWeight = () => Math.floor(Math.random() * 10); +const randDuration = () => Math.floor(Math.random() * 1000); + +export function mockPluginConfig(opt?: { + pluginSlug?: string; + auditSlug?: string | string[]; + groupSlug?: string | string[]; +}): PluginConfig { + const { groupSlug } = opt || {}; + let { pluginSlug, auditSlug } = opt || {}; + pluginSlug = pluginSlug || __pluginSlug__; + auditSlug = auditSlug || __auditSlug__; + const addGroups = groupSlug !== undefined; + const outputPath = __outputPath__; + + const audits = Array.isArray(auditSlug) + ? auditSlug.map(slug => mockAuditConfig({ auditSlug: slug })) + : [mockAuditConfig({ auditSlug: auditSlug })]; + + let groups: AuditGroup[] = []; + if (addGroups) { + groups = Array.isArray(groupSlug) + ? groupSlug.map(slug => mockGroupConfig({ groupSlug: slug })) + : [mockGroupConfig({ groupSlug, auditSlug })]; + } + + return { + audits, + groups, + runner: { + command: 'bash', + args: [ + '-c', + `echo '${JSON.stringify({ + audits: audits.map(({ slug }, idx) => ({ + slug: `${slug}`, + label: '', + value: idx, + score: parseFloat('0.' + idx), + })), + } satisfies PluginRunnerOutput)}' > ${outputPath}`, + ], + outputPath: outputPath, + }, + meta: { + slug: pluginSlug, + name: 'execute plugin', + }, + }; +} + +export function mockAuditConfig(opt?: { auditSlug?: string }): AuditMetadata { + let { auditSlug } = opt || {}; + auditSlug = auditSlug || __auditSlug__; + + return { + slug: auditSlug, + title: auditSlug + ' title', + description: 'audit description', + docsUrl: 'http://www.my-docs.dev', + }; +} + +export function mockPersistConfig(opt?: Partial): PersistConfig { + let { outputPath, format } = opt || {}; + outputPath = outputPath || __outputPath__; + format = format || []; + return { + outputPath, + format, + }; +} + +export function mockGroupConfig(opt?: { + groupSlug?: string; + auditSlug?: string | string[]; +}): AuditGroup { + let { groupSlug, auditSlug } = opt || {}; + groupSlug = groupSlug || __groupSlug__; + auditSlug = auditSlug || __auditSlug__; + return { + slug: groupSlug, + title: 'group title', + description: 'group description', + refs: Array.isArray(auditSlug) + ? auditSlug.map(slug => ({ + slug, + weight: randWeight(), + })) + : [ + { + slug: auditSlug, + weight: randWeight(), + }, + ], + }; +} + +export function mockCategory(opt?: { + categorySlug?: string; + pluginSlug?: string; + auditSlug?: string | string[]; + groupSlug?: string | string[]; +}): CategoryConfig { + let { categorySlug, auditSlug, pluginSlug, groupSlug } = opt || {}; + const addAudits = !!auditSlug; + auditSlug = auditSlug || __auditSlug__; + const addGroups = !!groupSlug; + groupSlug = groupSlug || __groupSlug__; + pluginSlug = pluginSlug || __pluginSlug__; + categorySlug = categorySlug || __categorySlug__; + + const categoryAuditRefs: CategoryConfig['refs'] = addAudits + ? Array.isArray(auditSlug) + ? auditSlug.map(slug => ({ + slug, + type: 'audit' as const, + weight: randWeight(), + plugin: pluginSlug + '', + })) + : [ + { + slug: auditSlug, + type: 'audit' as const, + weight: randWeight(), + plugin: pluginSlug + '', + }, + ] + : []; + const categoryGroupRefs: CategoryConfig['refs'] = addGroups + ? Array.isArray(groupSlug) + ? groupSlug.map(slug => ({ + slug, + type: 'group', + weight: randWeight(), + plugin: pluginSlug + '', + })) + : [ + { + slug: groupSlug, + type: 'group', + weight: randWeight(), + plugin: pluginSlug + '', + }, + ] + : []; + + return { + slug: categorySlug, + title: `${categorySlug + .split('-') + .map(word => word.slice(0, 1).toUpperCase() + word.slice(1)) + .join(' ')}`, + description: `This is the category description of ${categorySlug}. Enjoy dummy text and data to the full.`, + refs: categoryAuditRefs.concat(categoryGroupRefs), + }; +} + +export function mockReport(opt?: { + auditSlug?: string | string[]; + pluginSlug?: string; +}): Report { + let { auditSlug, pluginSlug } = opt || {}; + auditSlug = auditSlug || __auditSlug__; + pluginSlug = pluginSlug || __pluginSlug__; + return { + packageName: 'mock-package', + version: '0.0.0', + date: new Date().toDateString(), + duration: randDuration(), + plugins: [mockPluginReport({ auditSlug, pluginSlug })], + }; +} + +export function mockPluginReport(opt?: { + pluginSlug: string; + auditSlug: string | string[]; +}): PluginReport { + let { auditSlug, pluginSlug } = opt || {}; + auditSlug = auditSlug || __auditSlug__; + pluginSlug = pluginSlug || __pluginSlug__; + return { + date: new Date().toDateString(), + duration: randDuration(), + meta: { + slug: pluginSlug, + docsUrl: `http://plugin.io/docs/${pluginSlug}`, + name: 'Mock plugin Name', + icon: 'socket', + }, + audits: Array.isArray(auditSlug) + ? auditSlug.map(a => mockAuditReport({ auditSlug: a })) + : [mockAuditReport({ auditSlug })], + }; +} + +export function mockAuditReport(opt?: { auditSlug: string }): AuditReport { + let { auditSlug } = opt || {}; + auditSlug = auditSlug || __auditSlug__; + return { + slug: auditSlug, + displayValue: 'mocked value', + value: Math.floor(Math.random() * 100), + score: Math.round(Math.random()), + title: auditSlug, + }; +} + +export function mockConfig(opt?: { + outputPath?: string; + categorySlug?: string | string[]; + pluginSlug?: string | string[]; + auditSlug?: string | string[]; + groupSlug?: string | string[]; +}): CoreConfig { + const { outputPath, pluginSlug, auditSlug, groupSlug, categorySlug } = + opt || {}; + return { + persist: mockPersistConfig({ outputPath }), + plugins: Array.isArray(pluginSlug) + ? pluginSlug.map(slug => + mockPluginConfig({ pluginSlug: slug, auditSlug, groupSlug }), + ) + : [mockPluginConfig({ pluginSlug, auditSlug, groupSlug })], + categories: Array.isArray(categorySlug) + ? categorySlug.map(slug => + mockCategory({ categorySlug: slug, auditSlug, groupSlug }), + ) + : [mockCategory({ categorySlug, auditSlug, groupSlug })], + }; +} + +export function mockUploadConfig(opt?: Partial): UploadConfig { + return { + apiKey: 'm0ck-API-k3y', + server: 'http://test.server.io', + ...opt, + }; +} + +export function mockRunnerOutput(opt?: { + auditSlug: string | string[]; +}): PluginRunnerOutput { + let { auditSlug } = opt || {}; + auditSlug = auditSlug || 'mock-audit-output-slug'; + const audits = Array.isArray(auditSlug) + ? auditSlug.map((slug, idx) => ({ + slug, + value: idx, + displayValue: '', + score: 0, + })) + : [ + { + slug: auditSlug, + value: 12, + displayValue: '', + score: 0, + }, + ]; + + return { + audits, + }; +} diff --git a/packages/models/test/test-data/config-and-report-dummy.mock.ts b/packages/models/test/test-data/config-and-report-dummy.mock.ts new file mode 100644 index 000000000..be6edf0f3 --- /dev/null +++ b/packages/models/test/test-data/config-and-report-dummy.mock.ts @@ -0,0 +1,41 @@ +import { + mockCategory, + mockConfig, + mockPluginConfig, + mockReport, +} from '../schema.mock'; + +const pluginSlug = ['plg-0', 'plg-1', 'plg-2']; +const auditSlug0 = ['0a', '0b', '0c', '0d']; +const auditSlug1 = ['1a', '1b', '1c']; +const auditSlug2 = ['2a', '2b', '2c', '2d', '2e']; + +export const dummyConfig = mockConfig({ outputPath: 'out' }); +dummyConfig.plugins = [ + mockPluginConfig({ pluginSlug: pluginSlug[0], auditSlug: auditSlug0 }), + mockPluginConfig({ pluginSlug: pluginSlug[1], auditSlug: auditSlug1 }), + mockPluginConfig({ pluginSlug: pluginSlug[2], auditSlug: auditSlug2 }), +]; + +dummyConfig.categories = [ + mockCategory({ + pluginSlug: pluginSlug[0], + categorySlug: 'performance', + auditSlug: auditSlug0, + }) /*, + mockCategory({ + pluginSlug: pluginSlug[1], + categorySlug: 'a11y', + auditSlug: auditSlug1, + }), + mockCategory({ + pluginSlug: pluginSlug[2], + categorySlug: 'seo', + auditSlug: auditSlug2, + }),*/, +]; + +export const dummyReport = mockReport({ + pluginSlug: pluginSlug[0], + auditSlug: auditSlug0, +}); diff --git a/packages/models/test/test-data/config-and-report-lighthouse.mock.ts b/packages/models/test/test-data/config-and-report-lighthouse.mock.ts new file mode 100644 index 000000000..e44324bbd --- /dev/null +++ b/packages/models/test/test-data/config-and-report-lighthouse.mock.ts @@ -0,0 +1,1954 @@ +import { PluginConfig } from '@quality-metrics/models'; +import { mockConfig, mockReport } from '../schema.mock'; + +export const lighthousePlugin: () => Required = () => ({ + runner: { + command: 'bun', + args: ['--help'], + outputPath: 'lighthouse-runner-output.json', + }, + meta: { + slug: 'lighthouse', + name: 'lighthouse', + docsUrl: `https://github.com/GoogleChrome/lighthouse/tree/main/README.md`, + }, + groups: [], + audits: [ + { + slug: 'is-on-https', + title: 'Uses HTTPS', + description: + "All sites should be protected with HTTPS, even ones that don't handle sensitive data. This includes avoiding [mixed content](https://developers.google.com/web/fundamentals/security/prevent-mixed-content/what-is-mixed-content), where some resources are loaded over HTTP despite the initial request being served over HTTPS. HTTPS prevents intruders from tampering with or passively listening in on the communications between your app and your users, and is a prerequisite for HTTP/2 and many new web platform APIs. [Learn more](https://web.dev/is-on-https/).", + label: 'Uses HTTPS', + }, + { + slug: 'service-worker', + title: + 'Does not register a service worker that controls page and `start_url`', + description: + 'The service worker is the technology that enables your app to use many Progressive Web App features, such as offline, add to homescreen, and push notifications. [Learn more](https://web.dev/service-worker/).', + label: + 'Does not register a service worker that controls page and `start_url`', + }, + { + slug: 'viewport', + title: + 'Has a `` tag with `width` or `initial-scale`', + description: + 'A `` not only optimizes your app for mobile screen sizes, but also prevents [a 300 millisecond delay to user input](https://developers.google.com/web/updates/2013/12/300ms-tap-delay-gone-away). [Learn more](https://web.dev/viewport/).', + label: + 'Has a `` tag with `width` or `initial-scale`', + }, + { + slug: 'first-contentful-paint', + title: 'First Contentful Paint', + description: + 'First Contentful Paint marks the time at which the first text or image is painted. [Learn more](https://web.dev/first-contentful-paint/).', + label: 'First Contentful Paint', + }, + { + slug: 'largest-contentful-paint', + title: 'Largest Contentful Paint', + description: + 'Largest Contentful Paint marks the time at which the largest text or image is painted. [Learn more](https://web.dev/lighthouse-largest-contentful-paint/)', + label: 'Largest Contentful Paint', + }, + { + slug: 'first-meaningful-paint', + title: 'First Meaningful Paint', + description: + 'First Meaningful Paint measures when the primary content of a page is visible. [Learn more](https://web.dev/first-meaningful-paint/).', + label: 'First Meaningful Paint', + }, + { + slug: 'speed-index', + title: 'Speed Index', + description: + 'Speed Index shows how quickly the contents of a page are visibly populated. [Learn more](https://web.dev/speed-index/).', + label: 'Speed Index', + }, + { + slug: 'screenshot-thumbnails', + title: 'Screenshot Thumbnails', + description: 'This is what the load of your site looked like.', + label: 'Screenshot Thumbnails', + }, + { + slug: 'final-screenshot', + title: 'Final Screenshot', + description: 'The last screenshot captured of the pageload.', + label: 'Final Screenshot', + }, + { + slug: 'total-blocking-time', + title: 'Total Blocking Time', + description: + 'Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds. [Learn more](https://web.dev/lighthouse-total-blocking-time/).', + label: 'Total Blocking Time', + }, + { + slug: 'max-potential-fid', + title: 'Max Potential First Input Delay', + description: + 'The maximum potential First Input Delay that your users could experience is the duration of the longest task. [Learn more](https://web.dev/lighthouse-max-potential-fid/).', + label: 'Max Potential First Input Delay', + }, + { + slug: 'cumulative-layout-shift', + title: 'Cumulative Layout Shift', + description: + 'Cumulative Layout Shift measures the movement of visible elements within the viewport. [Learn more](https://web.dev/cls/).', + label: 'Cumulative Layout Shift', + }, + { + slug: 'errors-in-console', + title: 'Browser errors were logged to the console', + description: + 'Errors logged to the console indicate unresolved problems. They can come from network request failures and other browser concerns. [Learn more](https://web.dev/errors-in-console/)', + label: 'Browser errors were logged to the console', + }, + { + slug: 'server-response-time', + title: 'Initial server response time was short', + description: + 'Keep the server response time for the main document short because all other requests depend on it. [Learn more](https://web.dev/time-to-first-byte/).', + label: 'Initial server response time was short', + }, + { + slug: 'interactive', + title: 'Time to Interactive', + description: + 'Time to interactive is the amount of time it takes for the page to become fully interactive. [Learn more](https://web.dev/interactive/).', + label: 'Time to Interactive', + }, + { + slug: 'user-timings', + title: 'User Timing marks and measures', + description: + "Consider instrumenting your app with the User Timing API to measure your app's real-world performance during key user experiences. [Learn more](https://web.dev/user-timings/).", + label: 'User Timing marks and measures', + }, + { + slug: 'critical-request-chains', + title: 'Avoid chaining critical requests', + description: + 'The Critical Request Chains below show you what resources are loaded with a high priority. Consider reducing the length of chains, reducing the download size of resources, or deferring the download of unnecessary resources to improve page load. [Learn more](https://web.dev/critical-request-chains/).', + label: 'Avoid chaining critical requests', + }, + { + slug: 'redirects', + title: 'Avoid multiple page redirects', + description: + 'Redirects introduce additional delays before the page can be loaded. [Learn more](https://web.dev/redirects/).', + label: 'Avoid multiple page redirects', + }, + { + slug: 'installable-manifest', + title: + 'Web app manifest or service worker do not meet the installability requirements', + description: + 'Service worker is the technology that enables your app to use many Progressive Web App features, such as offline, add to homescreen, and push notifications. With proper service worker and manifest implementations, browsers can proactively prompt users to add your app to their homescreen, which can lead to higher engagement. [Learn more](https://web.dev/installable-manifest/).', + label: + 'Web app manifest or service worker do not meet the installability requirements', + }, + { + slug: 'apple-touch-icon', + title: 'Does not provide a valid `apple-touch-icon`', + description: + 'For ideal appearance on iOS when users add a progressive web app to the home screen, define an `apple-touch-icon`. It must point to a non-transparent 192px (or 180px) square PNG. [Learn More](https://web.dev/apple-touch-icon/).', + label: 'Does not provide a valid `apple-touch-icon`', + }, + { + slug: 'splash-screen', + title: 'Is not configured for a custom splash screen', + description: + 'A themed splash screen ensures a high-quality experience when users launch your app from their homescreens. [Learn more](https://web.dev/splash-screen/).', + label: 'Is not configured for a custom splash screen', + }, + { + slug: 'themed-omnibox', + title: 'Does not set a theme color for the address bar.', + description: + 'The browser address bar can be themed to match your site. [Learn more](https://web.dev/themed-omnibox/).', + label: 'Does not set a theme color for the address bar.', + }, + { + slug: 'maskable-icon', + title: "Manifest doesn't have a maskable icon", + description: + 'A maskable icon ensures that the image fills the entire shape without being letterboxed when installing the app on a device. [Learn more](https://web.dev/maskable-icon-audit/).', + label: "Manifest doesn't have a maskable icon", + }, + { + slug: 'content-width', + title: 'Content is sized correctly for the viewport', + description: + "If the width of your app's content doesn't match the width of the viewport, your app might not be optimized for mobile screens. [Learn more](https://web.dev/content-width/).", + label: 'Content is sized correctly for the viewport', + }, + { + slug: 'image-aspect-ratio', + title: 'Displays images with correct aspect ratio', + description: + 'Image display dimensions should match natural aspect ratio. [Learn more](https://web.dev/image-aspect-ratio/).', + label: 'Displays images with correct aspect ratio', + }, + { + slug: 'image-size-responsive', + title: 'Serves images with appropriate resolution', + description: + 'Image natural dimensions should be proportional to the display size and the pixel ratio to maximize image clarity. [Learn more](https://web.dev/serve-responsive-images/).', + label: 'Serves images with appropriate resolution', + }, + { + slug: 'preload-fonts', + title: 'Fonts with `font-display: optional` are preloaded', + description: + 'Preload `optional` fonts so first-time visitors may use them. [Learn more](https://web.dev/preload-optional-fonts/)', + label: 'Fonts with `font-display: optional` are preloaded', + }, + { + slug: 'deprecations', + title: 'Avoids deprecated APIs', + description: + 'Deprecated APIs will eventually be removed from the browser. [Learn more](https://web.dev/deprecations/).', + label: 'Avoids deprecated APIs', + }, + { + slug: 'mainthread-work-breakdown', + title: 'Minimizes main-thread work', + description: + 'Consider reducing the time spent parsing, compiling and executing JS. You may find delivering smaller JS payloads helps with this. [Learn more](https://web.dev/mainthread-work-breakdown/)', + label: 'Minimizes main-thread work', + }, + { + slug: 'bootup-time', + title: 'JavaScript execution time', + description: + 'Consider reducing the time spent parsing, compiling, and executing JS. You may find delivering smaller JS payloads helps with this. [Learn more](https://web.dev/bootup-time/).', + label: 'JavaScript execution time', + }, + { + slug: 'uses-rel-preload', + title: 'Preload key requests', + description: + 'Consider using `` to prioritize fetching resources that are currently requested later in page load. [Learn more](https://web.dev/uses-rel-preload/).', + label: 'Preload key requests', + }, + { + slug: 'uses-rel-preconnect', + title: 'Preconnect to required origins', + description: + 'Consider adding `preconnect` or `dns-prefetch` resource hints to establish early connections to important third-party origins. [Learn more](https://web.dev/uses-rel-preconnect/).', + label: 'Preconnect to required origins', + }, + { + slug: 'font-display', + title: 'All text remains visible during webfont loads', + description: + 'Leverage the font-display CSS feature to ensure text is user-visible while webfonts are loading. [Learn more](https://web.dev/font-display/).', + label: 'All text remains visible during webfont loads', + }, + { + slug: 'diagnostics', + title: 'Diagnostics', + description: 'Collection of useful page vitals.', + label: 'Diagnostics', + }, + { + slug: 'network-requests', + title: 'Network Requests', + description: + 'Lists the network requests that were made during page load.', + label: 'Network Requests', + }, + { + slug: 'network-rtt', + title: 'Network Round Trip Times', + description: + "Network round trip times (RTT) have a large impact on performance. If the RTT to an origin is high, it's an indication that servers closer to the user could improve performance. [Learn more](https://hpbn.co/primer-on-latency-and-bandwidth/).", + label: 'Network Round Trip Times', + }, + { + slug: 'network-server-latency', + title: 'Server Backend Latencies', + description: + "Server latencies can impact web performance. If the server latency of an origin is high, it's an indication the server is overloaded or has poor backend performance. [Learn more](https://hpbn.co/primer-on-web-performance/#analyzing-the-resource-waterfall).", + label: 'Server Backend Latencies', + }, + { + slug: 'main-thread-tasks', + title: 'Tasks', + description: + 'Lists the toplevel main thread tasks that executed during page load.', + label: 'Tasks', + }, + { + slug: 'metrics', + title: 'Metrics', + description: 'Collects all available metrics.', + label: 'Metrics', + }, + { + slug: 'performance-budget', + title: 'Performance budget', + description: + 'Keep the quantity and size of network requests under the targets set by the provided performance budget. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/budgets).', + label: 'Performance budget', + }, + { + slug: 'timing-budget', + title: 'Timing budget', + description: + 'Set a timing budget to help you keep an eye on the performance of your site. Performant sites load fast and respond to user input events quickly. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/budgets).', + label: 'Timing budget', + }, + { + slug: 'resource-summary', + title: 'Keep request counts low and transfer sizes small', + description: + 'To set budgets for the quantity and size of page resources, add a budget.json file. [Learn more](https://web.dev/use-lighthouse-for-performance-budgets/).', + label: 'Keep request counts low and transfer sizes small', + }, + { + slug: 'third-party-summary', + title: 'Minimize third-party usage', + description: + 'Third-party code can significantly impact load performance. Limit the number of redundant third-party providers and try to load third-party code after your page has primarily finished loading. [Learn more](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/loading-third-party-javascript/).', + label: 'Minimize third-party usage', + }, + { + slug: 'third-party-facades', + title: 'Lazy load third-party resources with facades', + description: + 'Some third-party embeds can be lazy loaded. Consider replacing them with a facade until they are required. [Learn more](https://web.dev/third-party-facades/).', + label: 'Lazy load third-party resources with facades', + }, + { + slug: 'largest-contentful-paint-element', + title: 'Largest Contentful Paint element', + description: + 'This is the largest contentful element painted within the viewport. [Learn More](https://web.dev/lighthouse-largest-contentful-paint/)', + label: 'Largest Contentful Paint element', + }, + { + slug: 'lcp-lazy-loaded', + title: 'Largest Contentful Paint image was not lazily loaded', + description: + 'Above-the-fold images that are lazily loaded render later in the page lifecycle, which can delay the largest contentful paint. [Learn more](https://web.dev/lcp-lazy-loading/).', + label: 'Largest Contentful Paint image was not lazily loaded', + }, + { + slug: 'layout-shift-elements', + title: 'Avoid large layout shifts', + description: 'These DOM elements contribute most to the CLS of the page.', + label: 'Avoid large layout shifts', + }, + { + slug: 'long-tasks', + title: 'Avoid long main-thread tasks', + description: + 'Lists the longest tasks on the main thread, useful for identifying worst contributors to input delay. [Learn more](https://web.dev/long-tasks-devtools/)', + label: 'Avoid long main-thread tasks', + }, + { + slug: 'no-unload-listeners', + title: 'Avoids `unload` event listeners', + description: + 'The `unload` event does not fire reliably and listening for it can prevent browser optimizations like the Back-Forward Cache. Use `pagehide` or `visibilitychange` events instead. [Learn more](https://web.dev/bfcache/#never-use-the-unload-event)', + label: 'Avoids `unload` event listeners', + }, + { + slug: 'non-composited-animations', + title: 'Avoid non-composited animations', + description: + 'Animations which are not composited can be janky and increase CLS. [Learn more](https://web.dev/non-composited-animations)', + label: 'Avoid non-composited animations', + }, + { + slug: 'unsized-images', + title: 'Image elements have explicit `width` and `height`', + description: + 'Set an explicit width and height on image elements to reduce layout shifts and improve CLS. [Learn more](https://web.dev/optimize-cls/#images-without-dimensions)', + label: 'Image elements have explicit `width` and `height`', + }, + { + slug: 'valid-source-maps', + title: 'Page has valid source maps', + description: + 'Source maps translate minified code to the original source code. This helps developers debug in production. In addition, Lighthouse is able to provide further insights. Consider deploying source maps to take advantage of these benefits. [Learn more](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps).', + label: 'Page has valid source maps', + }, + { + slug: 'preload-lcp-image', + title: 'Preload Largest Contentful Paint image', + description: + 'Preload the image used by the LCP element in order to improve your LCP time. [Learn more](https://web.dev/optimize-lcp/#preload-important-resources).', + label: 'Preload Largest Contentful Paint image', + }, + { + slug: 'csp-xss', + title: 'Ensure CSP is effective against XSS attacks', + description: + 'A strong Content Security Policy (CSP) significantly reduces the risk of cross-site scripting (XSS) attacks. [Learn more](https://web.dev/csp-xss/)', + label: 'Ensure CSP is effective against XSS attacks', + }, + { + slug: 'full-page-screenshot', + title: 'Full-page screenshot', + description: 'A full-height screenshot of the final rendered page', + label: 'Full-page screenshot', + }, + { + slug: 'script-treemap-data', + title: 'Script Treemap Data', + description: 'Used for treemap app', + label: 'Script Treemap Data', + }, + { + slug: 'pwa-cross-browser', + title: 'Site works cross-browser', + description: + 'To reach the most number of users, sites should work across every major browser. [Learn more](https://web.dev/pwa-cross-browser/).', + label: 'Site works cross-browser', + }, + { + slug: 'pwa-page-transitions', + title: "Page transitions don't feel like they block on the network", + description: + "Transitions should feel snappy as you tap around, even on a slow network. This experience is key to a user's perception of performance. [Learn more](https://web.dev/pwa-page-transitions/).", + label: "Page transitions don't feel like they block on the network", + }, + { + slug: 'pwa-each-page-has-url', + title: 'Each page has a URL', + description: + 'Ensure individual pages are deep linkable via URL and that URLs are unique for the purpose of shareability on social media. [Learn more](https://web.dev/pwa-each-page-has-url/).', + label: 'Each page has a URL', + }, + { + slug: 'accesskeys', + title: '`[accesskey]` values are unique', + description: + 'Access keys let users quickly focus a part of the page. For proper navigation, each access key must be unique. [Learn more](https://web.dev/accesskeys/).', + label: '`[accesskey]` values are unique', + }, + { + slug: 'aria-allowed-attr', + title: '`[aria-*]` attributes match their roles', + description: + 'Each ARIA `role` supports a specific subset of `aria-*` attributes. Mismatching these invalidates the `aria-*` attributes. [Learn more](https://web.dev/aria-allowed-attr/).', + label: '`[aria-*]` attributes match their roles', + }, + { + slug: 'aria-command-name', + title: '`button`, `link`, and `menuitem` elements have accessible names', + description: + "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn more](https://web.dev/aria-name/).", + label: '`button`, `link`, and `menuitem` elements have accessible names', + }, + { + slug: 'aria-hidden-body', + title: '`[aria-hidden="true"]` is not present on the document ``', + description: + 'Assistive technologies, like screen readers, work inconsistently when `aria-hidden="true"` is set on the document ``. [Learn more](https://web.dev/aria-hidden-body/).', + label: '`[aria-hidden="true"]` is not present on the document ``', + }, + { + slug: 'aria-hidden-focus', + title: + '`[aria-hidden="true"]` elements do not contain focusable descendents', + description: + 'Focusable descendents within an `[aria-hidden="true"]` element prevent those interactive elements from being available to users of assistive technologies like screen readers. [Learn more](https://web.dev/aria-hidden-focus/).', + label: + '`[aria-hidden="true"]` elements do not contain focusable descendents', + }, + { + slug: 'aria-input-field-name', + title: 'ARIA input fields have accessible names', + description: + "When an input field doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn more](https://web.dev/aria-name/).", + label: 'ARIA input fields have accessible names', + }, + { + slug: 'aria-meter-name', + title: 'ARIA `meter` elements have accessible names', + description: + "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn more](https://web.dev/aria-name/).", + label: 'ARIA `meter` elements have accessible names', + }, + { + slug: 'aria-progressbar-name', + title: 'ARIA `progressbar` elements have accessible names', + description: + "When a `progressbar` element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn more](https://web.dev/aria-name/).", + label: 'ARIA `progressbar` elements have accessible names', + }, + { + slug: 'aria-required-attr', + title: '`[role]`s have all required `[aria-*]` attributes', + description: + 'Some ARIA roles have required attributes that describe the state of the element to screen readers. [Learn more](https://web.dev/aria-required-attr/).', + label: '`[role]`s have all required `[aria-*]` attributes', + }, + { + slug: 'aria-required-children', + title: + 'Elements with an ARIA `[role]` that require children to contain a specific `[role]` have all required children.', + description: + 'Some ARIA parent roles must contain specific child roles to perform their intended accessibility functions. [Learn more](https://web.dev/aria-required-children/).', + label: + 'Elements with an ARIA `[role]` that require children to contain a specific `[role]` have all required children.', + }, + { + slug: 'aria-required-parent', + title: '`[role]`s are contained by their required parent element', + description: + 'Some ARIA child roles must be contained by specific parent roles to properly perform their intended accessibility functions. [Learn more](https://web.dev/aria-required-parent/).', + label: '`[role]`s are contained by their required parent element', + }, + { + slug: 'aria-roles', + title: '`[role]` values are valid', + description: + 'ARIA roles must have valid values in order to perform their intended accessibility functions. [Learn more](https://web.dev/aria-roles/).', + label: '`[role]` values are valid', + }, + { + slug: 'aria-toggle-field-name', + title: 'ARIA toggle fields have accessible names', + description: + "When a toggle field doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn more](https://web.dev/aria-name/).", + label: 'ARIA toggle fields have accessible names', + }, + { + slug: 'aria-tooltip-name', + title: 'ARIA `tooltip` elements have accessible names', + description: + "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn more](https://web.dev/aria-name/).", + label: 'ARIA `tooltip` elements have accessible names', + }, + { + slug: 'aria-treeitem-name', + title: 'ARIA `treeitem` elements have accessible names', + description: + "When an element doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn more](https://web.dev/aria-name/).", + label: 'ARIA `treeitem` elements have accessible names', + }, + { + slug: 'aria-valid-attr-value', + title: '`[aria-*]` attributes have valid values', + description: + "Assistive technologies, like screen readers, can't interpret ARIA attributes with invalid values. [Learn more](https://web.dev/aria-valid-attr-value/).", + label: '`[aria-*]` attributes have valid values', + }, + { + slug: 'aria-valid-attr', + title: '`[aria-*]` attributes are valid and not misspelled', + description: + "Assistive technologies, like screen readers, can't interpret ARIA attributes with invalid names. [Learn more](https://web.dev/aria-valid-attr/).", + label: '`[aria-*]` attributes are valid and not misspelled', + }, + { + slug: 'button-name', + title: 'Buttons have an accessible name', + description: + 'When a button doesn\'t have an accessible name, screen readers announce it as "button", making it unusable for users who rely on screen readers. [Learn more](https://web.dev/button-name/).', + label: 'Buttons have an accessible name', + }, + { + slug: 'bypass', + title: 'The page contains a heading, skip link, or landmark region', + description: + 'Adding ways to bypass repetitive content lets keyboard users navigate the page more efficiently. [Learn more](https://web.dev/bypass/).', + label: 'The page contains a heading, skip link, or landmark region', + }, + { + slug: 'color-contrast', + title: + 'Background and foreground colors have a sufficient contrast ratio', + description: + 'Low-contrast text is difficult or impossible for many users to read. [Learn more](https://web.dev/color-contrast/).', + label: + 'Background and foreground colors have a sufficient contrast ratio', + }, + { + slug: 'definition-list', + title: + "`
`'s contain only properly-ordered `
` and `
` groups, `