Skip to content

Commit 20eb989

Browse files
authored
build(plugin-lighthouse): add tools to lighthouse plugin (#458)
1 parent f002f7d commit 20eb989

6 files changed

Lines changed: 192 additions & 35 deletions

File tree

packages/plugin-lighthouse/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"license": "MIT",
55
"dependencies": {
66
"@code-pushup/models": "*",
7-
"lighthouse": "^11.0.0"
7+
"lighthouse": "^11.0.0",
8+
"@code-pushup/utils": "*"
89
}
910
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
type Config,
3+
type IcuMessage,
4+
Audit as LHAudit,
5+
defaultConfig,
6+
} from 'lighthouse';
7+
import { Audit, Group } from '@code-pushup/models';
8+
9+
export const LIGHTHOUSE_PLUGIN_SLUG = 'lighthouse';
10+
export const LIGHTHOUSE_REPORT_NAME = 'lighthouse-report.json';
11+
12+
const { audits, categories } = defaultConfig;
13+
14+
export const GROUPS: Group[] = Object.entries(categories ?? {}).map(
15+
([id, category]) => ({
16+
slug: id,
17+
title: getMetaString(category.title),
18+
...(category.description && {
19+
description: getMetaString(category.description),
20+
}),
21+
refs: category.auditRefs.map(ref => ({ slug: ref.id, weight: ref.weight })),
22+
}),
23+
);
24+
25+
export const AUDITS: Audit[] = await Promise.all(
26+
(audits ?? []).map(async value => {
27+
const audit = await loadLighthouseAudit(value);
28+
return {
29+
slug: audit.meta.id,
30+
title: getMetaString(audit.meta.title),
31+
description: getMetaString(audit.meta.description),
32+
};
33+
}),
34+
);
35+
36+
function getMetaString(value: string | IcuMessage): string {
37+
if (typeof value === 'string') {
38+
return value;
39+
}
40+
return value.formattedDefault;
41+
}
42+
43+
async function loadLighthouseAudit(
44+
value: Config.AuditJson,
45+
): Promise<typeof LHAudit> {
46+
// the passed value directly includes the implementation as JS object
47+
// shape: { implementation: typeof LHAudit; options?: {}; }
48+
if (typeof value === 'object' && 'implementation' in value) {
49+
return value.implementation;
50+
}
51+
// the passed value is a `LH.Audit` class instance
52+
// shape: LHAudit
53+
if (typeof value === 'function') {
54+
return value;
55+
}
56+
// the passed value is the path directly
57+
// shape: string
58+
// otherwise it is a JS object maintaining a `path` property
59+
// shape: { path: string, options?: {}; }
60+
const path = typeof value === 'string' ? value : value.path;
61+
const module = (await import(`lighthouse/core/audits/${path}.js`)) as {
62+
default: typeof LHAudit;
63+
};
64+
return module.default;
65+
}
Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
1+
import { expect } from 'vitest';
2+
import {
3+
auditSchema,
4+
groupSchema,
5+
pluginConfigSchema,
6+
} from '@code-pushup/models';
7+
import { AUDITS, GROUPS } from './constants';
18
import { lighthousePlugin } from './lighthouse-plugin';
29

310
describe('lighthousePlugin', () => {
4-
it('should initialize Lighthouse plugin', () => {
5-
expect(lighthousePlugin({ config: '.lighthouserc.json' }).slug).toBe(
6-
'lighthouse',
7-
);
11+
it('should create valid plugin config', () => {
12+
const pluginConfig = lighthousePlugin({
13+
url: 'https://code-pushup-portal.com',
14+
});
15+
expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow();
16+
expect(pluginConfig.audits).toHaveLength(168);
17+
expect(pluginConfig.groups).toHaveLength(5);
818
});
919
});
20+
21+
describe('generated-constants', () => {
22+
it.each(AUDITS.map(a => [a.slug, a]))(
23+
'should parse audit "%s" correctly',
24+
(_, audit) => {
25+
expect(() => auditSchema.parse(audit)).not.toThrow();
26+
expect(audit.description).toEqual(expect.any(String));
27+
},
28+
);
29+
30+
it.each(GROUPS.map(a => [a.slug, a]))(
31+
'should parse group "%s" correctly',
32+
(_, group) => {
33+
expect(() => groupSchema.parse(group)).not.toThrow();
34+
},
35+
);
36+
});
Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,28 @@
1-
import { defaultConfig } from 'lighthouse';
2-
import { join } from 'node:path';
3-
import { PluginConfig } from '@code-pushup/models';
4-
import { echoRunnerConfigMock } from '@code-pushup/testing-utils';
1+
import { AuditOutputs, PluginConfig } from '@code-pushup/models';
2+
import { AUDITS, GROUPS, LIGHTHOUSE_PLUGIN_SLUG } from './constants';
53

6-
type LighthousePluginConfig = {
7-
config: string;
4+
export type LighthousePluginOptions = {
5+
url: string;
6+
outputPath?: string;
7+
onlyAudits?: string | string[];
8+
verbose?: boolean;
9+
headless?: boolean;
10+
userDataDir?: string;
811
};
912

10-
const outputDir = 'tmp';
11-
const outputFile = join(outputDir, `out.${Date.now()}.json`);
1213
// eslint-disable-next-line @typescript-eslint/no-unused-vars
13-
export function lighthousePlugin(_: LighthousePluginConfig): PluginConfig {
14-
// This line is here to have import and engines errors still present
15-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
16-
defaultConfig;
14+
export function lighthousePlugin(_: LighthousePluginOptions): PluginConfig {
1715
return {
18-
slug: 'lighthouse',
19-
title: 'ChromeDevTools Lighthouse',
16+
slug: LIGHTHOUSE_PLUGIN_SLUG,
17+
title: 'Lighthouse',
2018
icon: 'lighthouse',
21-
audits: [
22-
{
23-
slug: 'largest-contentful-paint',
24-
title: 'Largest Contentful Paint',
25-
},
26-
],
27-
runner: echoRunnerConfigMock(
28-
[
29-
{
30-
slug: 'largest-contentful-paint',
31-
value: 0,
32-
score: 0,
33-
},
34-
],
35-
outputFile,
36-
),
19+
audits: AUDITS,
20+
groups: GROUPS,
21+
runner: (): AuditOutputs =>
22+
AUDITS.map(audit => ({
23+
...audit,
24+
score: 0,
25+
value: 0,
26+
})),
3727
};
3828
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { CliFlags } from 'lighthouse';
2+
import { objectToCliArgs } from '@code-pushup/utils';
3+
import { LIGHTHOUSE_REPORT_NAME } from './constants';
4+
5+
type RefinedLighthouseOption = {
6+
url: CliFlags['_'];
7+
chromeFlags?: Record<CliFlags['chromeFlags'][number], string>;
8+
};
9+
export type LighthouseCliOptions = RefinedLighthouseOption &
10+
Partial<Omit<CliFlags, keyof RefinedLighthouseOption>>;
11+
12+
export function getLighthouseCliArguments(
13+
options: LighthouseCliOptions,
14+
): string[] {
15+
const {
16+
url,
17+
outputPath = LIGHTHOUSE_REPORT_NAME,
18+
onlyAudits = [],
19+
output = 'json',
20+
verbose = false,
21+
chromeFlags = {},
22+
} = options;
23+
24+
// eslint-disable-next-line functional/no-let
25+
let argsObj: Record<string, unknown> = {
26+
_: ['lighthouse', url.join(',')],
27+
verbose,
28+
output,
29+
'output-path': outputPath,
30+
};
31+
32+
if (onlyAudits != null && onlyAudits.length > 0) {
33+
argsObj = {
34+
...argsObj,
35+
onlyAudits,
36+
};
37+
}
38+
39+
// handle chrome flags
40+
if (Object.keys(chromeFlags).length > 0) {
41+
argsObj = {
42+
...argsObj,
43+
chromeFlags: Object.entries(chromeFlags)
44+
.map(([key, value]) => `--${key}=${value}`)
45+
.join(' '),
46+
};
47+
}
48+
49+
return objectToCliArgs(argsObj);
50+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { expect } from 'vitest';
2+
import { getLighthouseCliArguments } from './utils';
3+
4+
describe('getLighthouseCliArguments', () => {
5+
it('should parse valid options', () => {
6+
expect(
7+
getLighthouseCliArguments({
8+
url: ['https://code-pushup-portal.com'],
9+
}),
10+
).toEqual(expect.arrayContaining(['https://code-pushup-portal.com']));
11+
});
12+
13+
it('should parse chrome-flags options correctly', () => {
14+
const args = getLighthouseCliArguments({
15+
url: ['https://code-pushup-portal.com'],
16+
chromeFlags: { headless: 'new', 'user-data-dir': 'test' },
17+
});
18+
expect(args).toEqual(
19+
expect.arrayContaining([
20+
'--chromeFlags="--headless=new --user-data-dir=test"',
21+
]),
22+
);
23+
});
24+
});

0 commit comments

Comments
 (0)