Skip to content

Commit

Permalink
build(plugin-lighthouse): add tools to lighthouse plugin (#458)
Browse files Browse the repository at this point in the history
  • Loading branch information
BioPhoton authored Feb 3, 2024
1 parent f002f7d commit 20eb989
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 35 deletions.
3 changes: 2 additions & 1 deletion packages/plugin-lighthouse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"license": "MIT",
"dependencies": {
"@code-pushup/models": "*",
"lighthouse": "^11.0.0"
"lighthouse": "^11.0.0",
"@code-pushup/utils": "*"
}
}
65 changes: 65 additions & 0 deletions packages/plugin-lighthouse/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
type Config,
type IcuMessage,
Audit as LHAudit,
defaultConfig,
} from 'lighthouse';
import { Audit, Group } from '@code-pushup/models';

export const LIGHTHOUSE_PLUGIN_SLUG = 'lighthouse';
export const LIGHTHOUSE_REPORT_NAME = 'lighthouse-report.json';

const { audits, categories } = defaultConfig;

export const GROUPS: Group[] = Object.entries(categories ?? {}).map(
([id, category]) => ({
slug: id,
title: getMetaString(category.title),
...(category.description && {
description: getMetaString(category.description),
}),
refs: category.auditRefs.map(ref => ({ slug: ref.id, weight: ref.weight })),
}),
);

export const AUDITS: Audit[] = await Promise.all(
(audits ?? []).map(async value => {
const audit = await loadLighthouseAudit(value);
return {
slug: audit.meta.id,
title: getMetaString(audit.meta.title),
description: getMetaString(audit.meta.description),
};
}),
);

function getMetaString(value: string | IcuMessage): string {
if (typeof value === 'string') {
return value;
}
return value.formattedDefault;
}

async function loadLighthouseAudit(
value: Config.AuditJson,
): Promise<typeof LHAudit> {
// the passed value directly includes the implementation as JS object
// shape: { implementation: typeof LHAudit; options?: {}; }
if (typeof value === 'object' && 'implementation' in value) {
return value.implementation;
}
// the passed value is a `LH.Audit` class instance
// shape: LHAudit
if (typeof value === 'function') {
return value;
}
// the passed value is the path directly
// shape: string
// otherwise it is a JS object maintaining a `path` property
// shape: { path: string, options?: {}; }
const path = typeof value === 'string' ? value : value.path;
const module = (await import(`lighthouse/core/audits/${path}.js`)) as {
default: typeof LHAudit;
};
return module.default;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
import { expect } from 'vitest';
import {
auditSchema,
groupSchema,
pluginConfigSchema,
} from '@code-pushup/models';
import { AUDITS, GROUPS } from './constants';
import { lighthousePlugin } from './lighthouse-plugin';

describe('lighthousePlugin', () => {
it('should initialize Lighthouse plugin', () => {
expect(lighthousePlugin({ config: '.lighthouserc.json' }).slug).toBe(
'lighthouse',
);
it('should create valid plugin config', () => {
const pluginConfig = lighthousePlugin({
url: 'https://code-pushup-portal.com',
});
expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow();
expect(pluginConfig.audits).toHaveLength(168);
expect(pluginConfig.groups).toHaveLength(5);
});
});

describe('generated-constants', () => {
it.each(AUDITS.map(a => [a.slug, a]))(
'should parse audit "%s" correctly',
(_, audit) => {
expect(() => auditSchema.parse(audit)).not.toThrow();
expect(audit.description).toEqual(expect.any(String));
},
);

it.each(GROUPS.map(a => [a.slug, a]))(
'should parse group "%s" correctly',
(_, group) => {
expect(() => groupSchema.parse(group)).not.toThrow();
},
);
});
50 changes: 20 additions & 30 deletions packages/plugin-lighthouse/src/lib/lighthouse-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,28 @@
import { defaultConfig } from 'lighthouse';
import { join } from 'node:path';
import { PluginConfig } from '@code-pushup/models';
import { echoRunnerConfigMock } from '@code-pushup/testing-utils';
import { AuditOutputs, PluginConfig } from '@code-pushup/models';
import { AUDITS, GROUPS, LIGHTHOUSE_PLUGIN_SLUG } from './constants';

type LighthousePluginConfig = {
config: string;
export type LighthousePluginOptions = {
url: string;
outputPath?: string;
onlyAudits?: string | string[];
verbose?: boolean;
headless?: boolean;
userDataDir?: string;
};

const outputDir = 'tmp';
const outputFile = join(outputDir, `out.${Date.now()}.json`);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function lighthousePlugin(_: LighthousePluginConfig): PluginConfig {
// This line is here to have import and engines errors still present
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
defaultConfig;
export function lighthousePlugin(_: LighthousePluginOptions): PluginConfig {
return {
slug: 'lighthouse',
title: 'ChromeDevTools Lighthouse',
slug: LIGHTHOUSE_PLUGIN_SLUG,
title: 'Lighthouse',
icon: 'lighthouse',
audits: [
{
slug: 'largest-contentful-paint',
title: 'Largest Contentful Paint',
},
],
runner: echoRunnerConfigMock(
[
{
slug: 'largest-contentful-paint',
value: 0,
score: 0,
},
],
outputFile,
),
audits: AUDITS,
groups: GROUPS,
runner: (): AuditOutputs =>
AUDITS.map(audit => ({
...audit,
score: 0,
value: 0,
})),
};
}
50 changes: 50 additions & 0 deletions packages/plugin-lighthouse/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { CliFlags } from 'lighthouse';
import { objectToCliArgs } from '@code-pushup/utils';
import { LIGHTHOUSE_REPORT_NAME } from './constants';

type RefinedLighthouseOption = {
url: CliFlags['_'];
chromeFlags?: Record<CliFlags['chromeFlags'][number], string>;
};
export type LighthouseCliOptions = RefinedLighthouseOption &
Partial<Omit<CliFlags, keyof RefinedLighthouseOption>>;

export function getLighthouseCliArguments(
options: LighthouseCliOptions,
): string[] {
const {
url,
outputPath = LIGHTHOUSE_REPORT_NAME,
onlyAudits = [],
output = 'json',
verbose = false,
chromeFlags = {},
} = options;

// eslint-disable-next-line functional/no-let
let argsObj: Record<string, unknown> = {
_: ['lighthouse', url.join(',')],
verbose,
output,
'output-path': outputPath,
};

if (onlyAudits != null && onlyAudits.length > 0) {
argsObj = {
...argsObj,
onlyAudits,
};
}

// handle chrome flags
if (Object.keys(chromeFlags).length > 0) {
argsObj = {
...argsObj,
chromeFlags: Object.entries(chromeFlags)
.map(([key, value]) => `--${key}=${value}`)
.join(' '),
};
}

return objectToCliArgs(argsObj);
}
24 changes: 24 additions & 0 deletions packages/plugin-lighthouse/src/lib/utils.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect } from 'vitest';
import { getLighthouseCliArguments } from './utils';

describe('getLighthouseCliArguments', () => {
it('should parse valid options', () => {
expect(
getLighthouseCliArguments({
url: ['https://code-pushup-portal.com'],
}),
).toEqual(expect.arrayContaining(['https://code-pushup-portal.com']));
});

it('should parse chrome-flags options correctly', () => {
const args = getLighthouseCliArguments({
url: ['https://code-pushup-portal.com'],
chromeFlags: { headless: 'new', 'user-data-dir': 'test' },
});
expect(args).toEqual(
expect.arrayContaining([
'--chromeFlags="--headless=new --user-data-dir=test"',
]),
);
});
});

0 comments on commit 20eb989

Please sign in to comment.