Skip to content

Commit

Permalink
feat: lighthouseServer reporter (#186)
Browse files Browse the repository at this point in the history
  • Loading branch information
lutejka committed Feb 29, 2024
1 parent b39eeac commit a688170
Show file tree
Hide file tree
Showing 13 changed files with 385 additions and 298 deletions.
10 changes: 7 additions & 3 deletions docs/content/2.integrations/1.ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,21 @@ You may want to generate reports that can be consumed by other tools. To do you
- `jsonExpanded` - A full JSON report which contains the URL, score, metric and category breakdowns.
- `csv` - A simple CSV report which contains the URL and score.
- `csvExpanded` - A full CSV report which contains the URL, score, metric and category breakdowns.
- `lighthouseServer` Uploads the report to your [lhci server](https://github.com/GoogleChrome/lighthouse-ci/blob/main/docs/server.md)
- `false` - Don't generate a report.

You can specify the reporter option with the `--reporter` flag or the `ci.reporter` config option.

```bash
# Run the CI with an expanded JSON report
unlighthouse-ci --site <your-site> --reporter jsonExpanded
unlighthouse-ci --site <your-site> --reporter lighthouseServer
```

```ts unlighthouse.config.ts
export default {
ci: {
reporter: 'jsonExpanded'
reporter: 'lighthouseServer'

}
}
```
Expand Down Expand Up @@ -107,7 +109,9 @@ Configuring the CLI can be done either through the CI arguments or through a con
| `--config-file <path>` | Path to config file. |
| `--output-path <path>` | Path to save the contents of the client and reports to. |
| `--budget <number>` | Budget (1-100), the minimum score which can pass. |
| `--reporter <reporter>` | Which reporter to use. Options are csv, json, csvExpanded, jsonExpanded. |
| `--reporter <reporter>` | Which reporter to use. Options are csv, json, csvExpanded, jsonExpanded, lighthouseServer. |
| `--lhci-host <lhci host>` | URL of your LHCI server. Needed in combination with the lighthouseServer reporter.|
| `--lhci-build-token <lhci buildToken>` | LHCI build token, used to add data. Needed in combination with the lighthouseServer reporter.|
| `--build-static` | Build a static website for the reports which can be uploaded. |
| `--cache` | Enable the caching. |
| `--no-cache` | Disable the caching. |
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"stub": "unbuild --stub"
},
"dependencies": {
"@lhci/utils": "^0.13.0",
"@unlighthouse/core": "workspace:../core",
"@unlighthouse/server": "workspace:../server",
"better-opn": "^3.0.2",
Expand Down
22 changes: 19 additions & 3 deletions packages/cli/src/ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { CiOptions } from './types'
import { pickOptions, validateHost, validateOptions } from './util'
import createCli from './createCli'
import { generateReportPayload, outputReport } from './reporters'
import { ReporterConfig } from './reporters/types'

async function run() {
const startTime = new Date()
Expand All @@ -17,6 +18,8 @@ async function run() {
cli.option('--budget <budget>', 'Budget (1-100), the minimum score which can pass.')
cli.option('--build-static <build-static>', 'Build a static website for the reports which can be uploaded.')
cli.option('--reporter <reporter>', 'The report to generate from results. Options: csv, csvExpanded, json, jsonExpanded or false. Default: json.')
cli.option('--lhci-host <lhci-host>', 'URL of your LHCI server.')
cli.option('--lhci-build-token <lhci-build-token>', 'LHCI build token, used to add data.')

const { options } = cli.parse() as unknown as { options: CiOptions }

Expand All @@ -28,6 +31,11 @@ async function run() {
budget: options.budget || undefined,
buildStatic: options.buildStatic || false,
reporter: options.reporter || 'jsonSimple',
reporterConfig: {
lhciHost: options.lhciHost,
lhciBuildToken: options.lhciBuildToken
}

}

await createUnlighthouse({
Expand Down Expand Up @@ -85,10 +93,18 @@ async function run() {
}
if (resolvedConfig.ci.reporter) {
const reporter = resolvedConfig.ci.reporter
const reporterConfig: ReporterConfig = {
columns: resolvedConfig.client.columns,
...(resolvedConfig.ci?.reporterConfig || {})

}
// @ts-expect-error untyped
const payload = generateReportPayload(reporter, worker.reports(), resolvedConfig.client.columns)
const path = relative(resolvedConfig.root, await outputReport(reporter, resolvedConfig, payload))
logger.success(`Generated \`${resolvedConfig.ci.reporter}\` report \`./${path}\``)
const payload = await Promise.resolve<Promise<any>>(generateReportPayload(reporter, worker.reports(), reporterConfig))
let path = ''
if(payload){
path = `\`./${relative(resolvedConfig.root, await outputReport(reporter, resolvedConfig, payload))}\``
}
logger.success(`Generated \`${resolvedConfig.ci.reporter}\` report`, path)
}

if (resolvedConfig.ci?.buildStatic) {
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/reporters/csvExpanded.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { UnlighthouseColumn, UnlighthouseTabs } from '@unlighthouse/core'

import { get } from 'lodash-es'
import type { UnlighthouseRouteReport } from '../types'
import { csvSimpleFormat } from './csvSimple'
import { ReporterConfig } from './types'

export function reportCSVExpanded(reports: UnlighthouseRouteReport[], columns: Record<UnlighthouseTabs, UnlighthouseColumn[]>): string {
export function reportCSVExpanded(reports: UnlighthouseRouteReport[], { columns }: ReporterConfig): string {
const { headers, body } = csvSimpleFormat(reports)
for (const k of Object.keys(columns)) {
// already have overview
Expand Down
13 changes: 9 additions & 4 deletions packages/cli/src/reporters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ import fse from 'fs-extra'
import type { ResolvedUserConfig, UnlighthouseColumn, UnlighthouseRouteReport, UnlighthouseTabs } from '@unlighthouse/core'
import { reportJsonSimple } from './jsonSimple'
import { reportJsonExpanded } from './jsonExpanded'
import type { ReportJsonExpanded, ReportJsonSimple } from './types'
import type { ReportJsonExpanded, ReportJsonSimple, ReporterConfig } from './types'
import { reportCSVSimple } from './csvSimple'
import { reportCSVExpanded } from './csvExpanded'
import { reportLighthouseServer } from './lighthouseServer'

export function generateReportPayload(reporter: 'lighthouseServer', reports: UnlighthouseRouteReport[], config?: ReporterConfig): Promise<void>
export function generateReportPayload(reporter: 'jsonExpanded', reports: UnlighthouseRouteReport[]): ReportJsonExpanded
export function generateReportPayload(reporter: 'jsonSimple' | 'json', reports: UnlighthouseRouteReport[]): ReportJsonSimple
export function generateReportPayload(reporter: 'csvSimple' | 'csv', reports: UnlighthouseRouteReport[]): string
export function generateReportPayload(reporter: 'csvExpanded', reports: UnlighthouseRouteReport[], columns?: Record<UnlighthouseTabs, UnlighthouseColumn[]>): string
export function generateReportPayload(reporter: string, reports: UnlighthouseRouteReport[], columns?: Record<UnlighthouseTabs, UnlighthouseColumn[]>): any {
export function generateReportPayload(reporter: 'csvExpanded', reports: UnlighthouseRouteReport[], config?: ReporterConfig): string
export function generateReportPayload(reporter: string, reports: UnlighthouseRouteReport[], config?: ReporterConfig): any {
const sortedReporters = reports.sort((a, b) => a.route.path.localeCompare(b.route.path))
if (reporter.startsWith('json')) {
if (reporter === 'jsonSimple' || reporter === 'json')
Expand All @@ -23,7 +25,10 @@ export function generateReportPayload(reporter: string, reports: UnlighthouseRou
if (reporter === 'csvSimple' || reporter === 'csv')
return reportCSVSimple(sortedReporters)
if (reporter === 'csvExpanded')
return reportCSVExpanded(sortedReporters, columns)
return reportCSVExpanded(sortedReporters, config)
}
if (reporter === 'lighthouseServer') {
return reportLighthouseServer(sortedReporters, config)
}
throw new Error(`Unsupported reporter: ${reporter}.`)
}
Expand Down
63 changes: 63 additions & 0 deletions packages/cli/src/reporters/lighthouseServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { UnlighthouseRouteReport } from "../types";
import ApiClient from "@lhci/utils/src/api-client.js";
import {
getCommitMessage,
getAuthor,
getAvatarUrl,
getExternalBuildUrl,
getCommitTime,
getCurrentHash,
getCurrentBranch,
getAncestorHash,
} from "@lhci/utils/src/build-context.js";
import fs from "fs-extra";
import { ReporterConfig } from "./types";
import { handleError } from "../errors";

export async function reportLighthouseServer(
reports: UnlighthouseRouteReport[],
{ lhciBuildToken, lhciHost }: ReporterConfig
): Promise<void> {
try {
const api = new ApiClient({ fetch, rootURL: lhciHost });
api.setBuildToken(lhciBuildToken);
const project = await api.findProjectByToken(lhciBuildToken);
const baseBranch = project.baseBranch || "master";
const hash = getCurrentHash();
const branch = getCurrentBranch();
const ancestorHash = getAncestorHash("HEAD", baseBranch);
const build = await api.createBuild({
projectId: project.id,
lifecycle: "unsealed",
hash: hash,
branch,
ancestorHash,
commitMessage: getCommitMessage(hash),
author: getAuthor(hash),
avatarUrl: getAvatarUrl(hash),
externalBuildUrl: getExternalBuildUrl(),
runAt: new Date().toISOString(),
committedAt: getCommitTime(hash),
ancestorCommittedAt: ancestorHash
? getCommitTime(ancestorHash)
: undefined,
});

for (const report of reports) {
const lighthouseResult = await fs.readJson(
`${report.artifactPath}/lighthouse.json`
);

await api.createRun({
projectId: project.id,
buildId: build.id,
representative: false,
url: `${report.route.url}${report.route.path}`,
lhr: JSON.stringify(lighthouseResult),
});
}
await api.sealBuild(build.projectId, build.id);
} catch (e) {
handleError(e)
}
}
8 changes: 8 additions & 0 deletions packages/cli/src/reporters/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UnlighthouseTabs, UnlighthouseColumn } from "../../../core/src"

export interface CategoryScore {
key: string
id: string
Expand Down Expand Up @@ -66,3 +68,9 @@ export interface ReportJsonExpanded {
}

export type ReportJsonSimple = SimpleRouteReport[]

export type ReporterConfig = Partial<{
columns: Record<UnlighthouseTabs, UnlighthouseColumn[]>
lhciHost: string
lhciBuildToken: string
}>
6 changes: 5 additions & 1 deletion packages/cli/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { UnlighthouseRouteReport, ValidReportTypes } from '@unlighthouse/core'
import type { ReporterConfig, UnlighthouseRouteReport, ValidReportTypes } from '@unlighthouse/core'

export interface CliOptions {
host?: string
Expand Down Expand Up @@ -36,6 +36,10 @@ export interface CiOptions extends CliOptions {
budget: number
buildStatic: boolean
reporter?: ValidReportTypes | false
lhciHost?: string
lhciBuildToken?: string


}

export { UnlighthouseRouteReport }
13 changes: 13 additions & 0 deletions packages/cli/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ export function validateOptions(resolvedOptions: UserConfig) {

if (!isValidUrl(resolvedOptions.site))
return handleError('Please provide a valid site URL.')

if(resolvedOptions?.ci?.reporter === 'lighthouseServer'){
if (!resolvedOptions?.ci?.reporterConfig?.lhciBuildToken) {
handleError(
"Please provide the lighthouse server build token with --lhci-build-token."
);
}
if (!resolvedOptions?.ci?.reporterConfig?.lhciHost) {
handleError(
"Please provide the lighthouse server build token with --lhci-host."
);
}
}
}

export function pickOptions(options: CiOptions | CliOptions): UserConfig {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/csv-reports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('csv reports', () => {
})

it('expanded', () => {
const actual = generateReportPayload('csvExpanded', lighthouseReport, DefaultColumns)
const actual = generateReportPayload('csvExpanded', lighthouseReport, { columns: DefaultColumns })
expect(actual).toMatchInlineSnapshot(`
"URL,Score,Performance,Accessibility,Best Practices,SEO,Largest Contentful Paint,Cumulative Layout Shift,FID,Blocking,Color Contrast,Headings,Image Alts,Link Names,Errors,Inspector Issues,Images Responsive,Image Aspect Ratio,Indexable
"/",98,100,100,100,92,279.17,0,68.82,0,1,1,1,1,1,1,1,1,1
Expand Down
99 changes: 99 additions & 0 deletions packages/cli/test/lighthouseServer-reports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, expect, it, vi } from "vitest"
import type { UnlighthouseRouteReport } from "../src/types"
import { generateReportPayload } from "../src/reporters"
import _lighthouseReport from "./__fixtures__/lighthouseReport.mjs"
import fs from 'fs-extra'
import ApiClient from "@lhci/utils/src/api-client.js"
import { create } from "lodash-es"

const lighthouseReport = _lighthouseReport as any as UnlighthouseRouteReport[]

vi.mock("fs-extra", () => {
return {
default: {
readJson: vi.fn(() => new Promise((resolve) => resolve({}))),
},
}
})

const setBuildToken = vi.fn()
const findProjectByToken = vi.fn(
() => new Promise((resolve) => resolve({ id: 1 }))
)
const createBuild = vi.fn(
() => new Promise((resolve) => resolve({ id: 1, projectId: 1 }))
)
const createRun = vi.fn(() => new Promise((resolve) => resolve({})))
const sealBuild = vi.fn(() => new Promise((resolve) => resolve({})))

vi.mock("@lhci/utils/src/api-client.js", () => {
const ApiClient = vi.fn(() => ({
setBuildToken,
findProjectByToken,
createBuild,
createRun,
sealBuild,
}))
return {
default: ApiClient,
}
})

vi.mock("@lhci/utils/src/build-context.js", () => {
return {
getCommitMessage: vi.fn( () => ''),
getAuthor: vi.fn( () => ''),
getAvatarUrl: vi.fn( () => ''),
getExternalBuildUrl: vi.fn( () => ''),
getCommitTime: vi.fn( () => ''),
getCurrentHash: vi.fn( () => ''),
getCurrentBranch: vi.fn( () => ''),
getAncestorHash: vi.fn( () => ''),
}
})

describe("lighthouseServer reports", () => {
it("expanded",async () => {
vi.useFakeTimers()

await Promise.resolve<Promise<any>>( generateReportPayload("lighthouseServer", lighthouseReport, {
lhciHost: "http:https://localhost",
lhciBuildToken: "token",
}))

expect(ApiClient).toBeCalledWith({ fetch, rootURL: "http:https://localhost" })
expect(setBuildToken).toBeCalledWith('token')

expect(createBuild).toBeCalledWith({
projectId: 1,
lifecycle: "unsealed",
hash: '',
branch: '',
ancestorHash: '',
commitMessage:'',
author: '',
avatarUrl: '',
externalBuildUrl: '',
runAt: new Date().toISOString(),
committedAt: '',
ancestorCommittedAt: undefined
})

expect(fs.readJson).toBeCalledTimes(lighthouseReport.length)

expect(createRun).toBeCalledTimes(lighthouseReport.length)

lighthouseReport.forEach(({ route}) => {
expect(createRun).toBeCalledWith({
projectId: 1,
buildId:1,
representative: false,
url: `${route.url}${route.path}`,
lhr: '{}',
})
})

expect(sealBuild).toBeCalledTimes(1)
expect(sealBuild).toBeCalledWith(1,1)
})
})
11 changes: 10 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,12 @@ export interface HTMLExtractPayload {

export type WindiResponsiveClasses = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'

export type ValidReportTypes = 'jsonSimple' | 'jsonExpanded'
export type ValidReportTypes = 'jsonSimple' | 'jsonExpanded' | 'lighthouseServer'

export interface ReporterConfig {
lhciHost?: string
lhciBuildToken?: string
}

/**
* A column will generally be either a direct mapping to a lighthouse audit (such as console errors) or a computed mapping to
Expand Down Expand Up @@ -342,6 +347,10 @@ export interface ResolvedUserConfig {
* @default 'jsonSimple'
*/
reporter: ValidReportTypes | false
/**
* Additional configuration passed to the reporter.
*/
reporterConfig?: ReporterConfig
}
/**
* See https://unlighthouse.dev/guide/client.html
Expand Down
Loading

0 comments on commit a688170

Please sign in to comment.