Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/plugin-lighthouse/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { DEFAULT_PERSIST_OUTPUT_DIR } from '@code-pushup/models';
export const DEFAULT_CHROME_FLAGS = [...DEFAULT_FLAGS, '--headless'];

export const LIGHTHOUSE_PLUGIN_SLUG = 'lighthouse';
export const LIGHTHOUSE_PLUGIN_TITLE = 'Lighthouse';

export const LIGHTHOUSE_OUTPUT_PATH = path.join(
DEFAULT_PERSIST_OUTPUT_DIR,
LIGHTHOUSE_PLUGIN_SLUG,
Expand Down
4 changes: 4 additions & 0 deletions packages/plugin-lighthouse/src/lib/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { pluginMetaLogFormatter } from '@code-pushup/utils';
import { LIGHTHOUSE_PLUGIN_TITLE } from './constants.js';

export const formatMetaLog = pluginMetaLogFormatter(LIGHTHOUSE_PLUGIN_TITLE);
9 changes: 6 additions & 3 deletions packages/plugin-lighthouse/src/lib/lighthouse-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createRequire } from 'node:module';
import type { PluginConfig, PluginUrls } from '@code-pushup/models';
import { normalizeUrlInput } from '@code-pushup/utils';
import { LIGHTHOUSE_PLUGIN_SLUG } from './constants.js';
import {
LIGHTHOUSE_PLUGIN_SLUG,
LIGHTHOUSE_PLUGIN_TITLE,
} from './constants.js';
import { normalizeFlags } from './normalize-flags.js';
import { processAuditsAndGroups } from './processing.js';
import { createRunnerFunction } from './runner/runner.js';
Expand Down Expand Up @@ -33,10 +36,10 @@ export function lighthousePlugin(

return {
slug: LIGHTHOUSE_PLUGIN_SLUG,
title: LIGHTHOUSE_PLUGIN_TITLE,
icon: 'lighthouse',
packageName: packageJson.name,
version: packageJson.version,
title: 'Lighthouse',
icon: 'lighthouse',
audits,
groups,
runner: createRunnerFunction(normalizedUrls, {
Expand Down
71 changes: 62 additions & 9 deletions packages/plugin-lighthouse/src/lib/processing.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { Audit, Group } from '@code-pushup/models';
import {
addIndex,
expandAuditsForUrls,
expandGroupsForUrls,
logger,
objectFromEntries,
objectToEntries,
pluralizeToken,
shouldExpandForUrls,
} from '@code-pushup/utils';
import { formatMetaLog } from './format.js';
import {
LIGHTHOUSE_GROUPS,
LIGHTHOUSE_NAVIGATION_AUDITS,
Expand All @@ -14,35 +20,82 @@ export function expandOptionsForUrls(
options: FilterOptions,
urlCount: number,
): FilterOptions {
return Object.fromEntries(
Object.entries(options).map(([key, value]) => [
return objectFromEntries(
objectToEntries(options).map(([key, value = []]) => [
key,
Array.isArray(value)
? value.flatMap(slug =>
Array.from({ length: urlCount }, (_, i) => addIndex(slug, i)),
)
: value,
value.flatMap(slug =>
Array.from({ length: urlCount }, (_, i) => addIndex(slug, i)),
),
]),
);
}

export function processAuditsAndGroups(urls: string[], options: FilterOptions) {
logTotal();
if (!shouldExpandForUrls(urls.length)) {
return markSkippedAuditsAndGroups(
const marked = markSkippedAuditsAndGroups(
LIGHTHOUSE_NAVIGATION_AUDITS,
LIGHTHOUSE_GROUPS,
options,
);
logSkipped(marked);
return marked;
}
const expandedAudits = expandAuditsForUrls(
LIGHTHOUSE_NAVIGATION_AUDITS,
urls,
);
const expandedGroups = expandGroupsForUrls(LIGHTHOUSE_GROUPS, urls);
const expandedOptions = expandOptionsForUrls(options, urls.length);
return markSkippedAuditsAndGroups(
logExpanded(expandedAudits, expandedGroups, urls);
const marked = markSkippedAuditsAndGroups(
expandedAudits,
expandedGroups,
expandedOptions,
);
logSkipped(marked);
return marked;
}

function logTotal(): void {
logger.info(
formatMetaLog(
`Created ${pluralizeToken('group', LIGHTHOUSE_GROUPS.length)} and ${pluralizeToken('audit', LIGHTHOUSE_NAVIGATION_AUDITS.length)} from Lighthouse's categories and navigation audits`,
),
);
}

function logExpanded(
expandedAudits: Audit[],
expandedGroups: Group[],
urls: string[],
): void {
logger.info(
formatMetaLog(
`Expanded audits (${LIGHTHOUSE_NAVIGATION_AUDITS.length} → ${expandedAudits.length}) and groups (${LIGHTHOUSE_GROUPS.length} → ${expandedGroups.length}) for ${pluralizeToken('URL', urls.length)}`,
),
);
}

function logSkipped(marked: { audits: Audit[]; groups: Group[] }): void {
const { audits, groups } = marked;

const formattedCounts = [
{ name: 'audit', items: audits },
{ name: 'group', items: groups },
]
.map(({ name, items }) => {
const skipped = items.filter(({ isSkipped }) => isSkipped);
if (skipped.length === 0) {
return '';
}
return `${skipped.length} out of ${pluralizeToken(name, items.length)}`;
})
.filter(Boolean)
.join(' and ');

if (!formattedCounts) {
return;
}
logger.info(formatMetaLog(`Skipping ${formattedCounts}`));
}
147 changes: 104 additions & 43 deletions packages/plugin-lighthouse/src/lib/runner/runner.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import type { Config, RunnerResult } from 'lighthouse';
import ansis from 'ansis';
import type { Config, Result, RunnerResult } from 'lighthouse';
import { runLighthouse } from 'lighthouse/cli/run.js';
import path from 'node:path';
import type { AuditOutputs, RunnerFunction } from '@code-pushup/models';
import type {
AuditOutputs,
RunnerFunction,
TableColumnObject,
} from '@code-pushup/models';
import {
addIndex,
asyncSequential,
ensureDirectoryExists,
formatAsciiLink,
formatAsciiTable,
formatReportScore,
logger,
shouldExpandForUrls,
stringifyError,
Expand All @@ -15,8 +22,8 @@ import { DEFAULT_CLI_FLAGS } from './constants.js';
import type { LighthouseCliFlags } from './types.js';
import {
enrichFlags,
filterAuditOutputs,
getConfig,
normalizeAuditOutputs,
toAuditOutputs,
withLocalTmpDir,
} from './utils.js';
Expand All @@ -28,64 +35,118 @@ export function createRunnerFunction(
return withLocalTmpDir(async (): Promise<AuditOutputs> => {
const config = await getConfig(flags);
const normalizationFlags = enrichFlags(flags);
const isSingleUrl = !shouldExpandForUrls(urls.length);
const urlsCount = urls.length;
const isSingleUrl = !shouldExpandForUrls(urlsCount);

const allResults = await urls.reduce(async (prev, url, index) => {
const acc = await prev;
try {
const enrichedFlags = isSingleUrl
? normalizationFlags
: enrichFlags(flags, index + 1);
const allResults = await asyncSequential(urls, (url, urlIndex) => {
const enrichedFlags = isSingleUrl
? normalizationFlags
: enrichFlags(flags, urlIndex + 1);
const step = { urlIndex, urlsCount };
return runLighthouseForUrl(url, enrichedFlags, config, step);
});

const auditOutputs = await runLighthouseForUrl(
url,
enrichedFlags,
config,
);

const processedOutputs = isSingleUrl
? auditOutputs
: auditOutputs.map(audit => ({
...audit,
slug: addIndex(audit.slug, index),
}));

return [...acc, ...processedOutputs];
} catch (error) {
logger.warn(stringifyError(error));
return acc;
}
}, Promise.resolve<AuditOutputs>([]));

if (allResults.length === 0) {
const collectedResults = allResults.filter(res => res != null);
if (collectedResults.length === 0) {
throw new Error(
isSingleUrl
? 'Lighthouse did not produce a result.'
: 'Lighthouse failed to produce results for all URLs.',
);
}
return normalizeAuditOutputs(allResults, normalizationFlags);

logResultsForAllUrls(collectedResults);

const auditOutputs: AuditOutputs = collectedResults.flatMap(
res => res.auditOutputs,
);
return filterAuditOutputs(auditOutputs, normalizationFlags);
});
}

type ResultForUrl = {
url: string;
lhr: Result;
auditOutputs: AuditOutputs;
};

async function runLighthouseForUrl(
url: string,
flags: LighthouseOptions,
config: Config | undefined,
): Promise<AuditOutputs> {
if (flags.outputPath) {
await ensureDirectoryExists(path.dirname(flags.outputPath));
}
step: { urlIndex: number; urlsCount: number },
): Promise<ResultForUrl | null> {
const { urlIndex, urlsCount } = step;

const runnerResult: unknown = await runLighthouse(url, flags, config);
const prefix = ansis.gray(`[${step.urlIndex + 1}/${step.urlsCount}]`);

if (runnerResult == null) {
throw new Error(
`Lighthouse did not produce a result for URL: ${formatAsciiLink(url)}`,
try {
if (flags.outputPath) {
await ensureDirectoryExists(path.dirname(flags.outputPath));
}

const lhr: Result = await logger.task(
`${prefix} Running lighthouse on ${url}`,
async () => {
const runnerResult: RunnerResult | undefined = await runLighthouse(
url,
flags,
config,
);

if (runnerResult == null) {
throw new Error('Lighthouse did not produce a result');
}

return {
message: `${prefix} Completed lighthouse run on ${url}`,
result: runnerResult.lhr,
};
},
);

const auditOutputs = toAuditOutputs(Object.values(lhr.audits), flags);
if (shouldExpandForUrls(urlsCount)) {
return {
url,
lhr,
auditOutputs: auditOutputs.map(audit => ({
...audit,
slug: addIndex(audit.slug, urlIndex),
})),
};
}
return { url, lhr, auditOutputs };
} catch (error) {
logger.warn(`Lighthouse run failed for ${url} - ${stringifyError(error)}`);
return null;
}
}

const { lhr } = runnerResult as RunnerResult;
function logResultsForAllUrls(results: ResultForUrl[]): void {
const categoryNames = Object.fromEntries(
results
.flatMap(res => Object.values(res.lhr.categories))
.map(category => [category.id, category.title]),
);

return toAuditOutputs(Object.values(lhr.audits), flags);
logger.info(
formatAsciiTable({
columns: [
{ key: 'url', label: 'URL', align: 'left' },
...Object.entries(categoryNames).map(
([key, label]): TableColumnObject => ({ key, label, align: 'right' }),
),
],
rows: results.map(({ url, lhr }) => ({
url,
...Object.fromEntries(
Object.values(lhr.categories).map(category => [
category.id,
category.score == null ? '-' : formatReportScore(category.score),
]),
),
})),
}),
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import ansis from 'ansis';
import type { Config } from 'lighthouse';
import { runLighthouse } from 'lighthouse/cli/run.js';
import type { Result } from 'lighthouse/types/lhr/audit-result';
Expand Down Expand Up @@ -52,6 +51,7 @@ vi.mock('lighthouse/cli/run.js', async () => {
score: 0.9,
} satisfies Result,
},
categories: {},
},
},
);
Expand Down Expand Up @@ -177,7 +177,7 @@ describe('createRunnerFunction', () => {
it('should continue with other URLs when one fails in multiple URL scenario', async () => {
const runner = createRunnerFunction([
'https://localhost:8080',
'fail',
'http://fail.com',
'https://localhost:8082',
]);

Expand All @@ -199,7 +199,7 @@ describe('createRunnerFunction', () => {
);

expect(logger.warn).toHaveBeenCalledWith(
`Lighthouse did not produce a result for URL: ${ansis.blueBright('fail')}`,
'Lighthouse run failed for http://fail.com - Lighthouse did not produce a result',
);
});

Expand Down
Loading