Skip to content

Commit f4db136

Browse files
authored
feat(plugin-lighthouse): implement multiple URL support
1 parent 45683d7 commit f4db136

22 files changed

+2173
-87
lines changed

code-pushup.preset.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import {
2020
} from './packages/plugin-jsdocs/src/lib/constants.js';
2121
import { filterGroupsByOnlyAudits } from './packages/plugin-jsdocs/src/lib/utils.js';
2222
import lighthousePlugin, {
23+
type LighthouseUrls,
2324
lighthouseGroupRef,
25+
mergeLighthouseCategories,
2426
} from './packages/plugin-lighthouse/src/index.js';
2527
import typescriptPlugin, {
2628
type TypescriptPluginOptions,
@@ -135,11 +137,14 @@ export const jsPackagesCoreConfig = async (): Promise<CoreConfig> => ({
135137
});
136138

137139
export const lighthouseCoreConfig = async (
138-
url: string,
139-
): Promise<CoreConfig> => ({
140-
plugins: [await lighthousePlugin(url)],
141-
categories: lighthouseCategories,
142-
});
140+
urls: LighthouseUrls,
141+
): Promise<CoreConfig> => {
142+
const lhPlugin = await lighthousePlugin(urls);
143+
return {
144+
plugins: [lhPlugin],
145+
categories: mergeLighthouseCategories(lhPlugin, lighthouseCategories),
146+
};
147+
};
143148

144149
export const jsDocsCoreConfig = (
145150
config: JsDocsPluginConfig | string[],

e2e/plugin-lighthouse-e2e/tests/__snapshots__/collect.e2e.test.ts.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ exports[`PLUGIN collect report with lighthouse-plugin NPM package > should run p
7777
"title": "Document has a valid \`hreflang\`",
7878
},
7979
],
80+
"context": {
81+
"urlCount": 1,
82+
"weights": {
83+
"1": 1,
84+
},
85+
},
8086
"groups": [
8187
{
8288
"refs": [

packages/cli/src/lib/implementation/filter.middleware.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,13 @@ function applyPluginFilters(
116116
options: Pick<FilterOptions, 'skipPlugins' | 'onlyPlugins'>,
117117
): CoreConfig['plugins'] {
118118
const { skipPlugins = [], onlyPlugins = [] } = options;
119-
const filteredPlugins = filterPluginsFromCategories({
120-
categories,
121-
plugins,
122-
});
119+
const filteredPlugins =
120+
onlyPlugins.length === 0
121+
? filterPluginsFromCategories({
122+
categories,
123+
plugins,
124+
})
125+
: plugins;
123126
if (skipPlugins.length === 0 && onlyPlugins.length === 0) {
124127
return filteredPlugins;
125128
}

packages/cli/src/lib/implementation/filter.middleware.unit.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,44 @@ describe('filterMiddleware', () => {
414414
),
415415
);
416416
});
417+
418+
it('should allow onlyPlugins to include plugins not referenced by categories', () => {
419+
const { plugins } = filterMiddleware({
420+
plugins: [
421+
{
422+
slug: 'p1',
423+
audits: [{ slug: 'a1-p1', isSkipped: false }],
424+
groups: [
425+
{
426+
slug: 'g1-p1',
427+
refs: [{ slug: 'a1-p1', weight: 1 }],
428+
isSkipped: false,
429+
},
430+
],
431+
},
432+
{
433+
slug: 'p2',
434+
audits: [{ slug: 'a1-p2', isSkipped: false }],
435+
groups: [
436+
{
437+
slug: 'g1-p2',
438+
refs: [{ slug: 'a1-p2', weight: 1 }],
439+
isSkipped: false,
440+
},
441+
],
442+
},
443+
] as PluginConfig[],
444+
categories: [
445+
{
446+
slug: 'c1',
447+
refs: [{ type: 'group', plugin: 'p1', slug: 'g1-p1', weight: 1 }],
448+
},
449+
] as CategoryConfig[],
450+
onlyPlugins: ['p2'],
451+
});
452+
453+
expect(plugins.map(plugin => plugin.slug)).toStrictEqual(['p2']);
454+
});
417455
});
418456

419457
describe('filterSkippedInPlugins', () => {

packages/models/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ export {
6767
} from './lib/persist-config.js';
6868
export {
6969
pluginConfigSchema,
70+
pluginContextSchema,
7071
pluginMetaSchema,
7172
type PluginConfig,
73+
type PluginContext,
7274
type PluginMeta,
7375
} from './lib/plugin-config.js';
7476
export {

packages/models/src/lib/plugin-config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import {
1010
import { errorItems, hasMissingStrings } from './implementation/utils.js';
1111
import { runnerConfigSchema, runnerFunctionSchema } from './runner-config.js';
1212

13+
export const pluginContextSchema = z
14+
.record(z.unknown())
15+
.optional()
16+
.describe('Plugin-specific context data for helpers');
17+
export type PluginContext = z.infer<typeof pluginContextSchema>;
18+
1319
export const pluginMetaSchema = packageVersionSchema()
1420
.merge(
1521
metaSchema({
@@ -31,6 +37,7 @@ export const pluginDataSchema = z.object({
3137
runner: z.union([runnerConfigSchema, runnerFunctionSchema]),
3238
audits: pluginAuditsSchema,
3339
groups: groupsSchema,
40+
context: pluginContextSchema,
3441
});
3542
type PluginData = z.infer<typeof pluginDataSchema>;
3643

packages/plugin-lighthouse/README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,100 @@ export default {
117117
};
118118
```
119119

120+
## Multiple URLs
121+
122+
The Lighthouse plugin supports running audits against multiple URLs in a single invocation. To do this, provide an array of URLs as the first argument to the plugin:
123+
124+
```ts
125+
import lighthousePlugin from '@code-pushup/lighthouse-plugin';
126+
127+
export default {
128+
// ...
129+
plugins: [
130+
// ...
131+
await lighthousePlugin(['https://example.com', 'https://example.com/contact']),
132+
],
133+
};
134+
```
135+
136+
### Assigning weights to URLs
137+
138+
You can assign custom weights to URLs by passing an object instead of an array. This is useful when some pages are more important than others (e.g., your homepage vs. a contact page). The keys are URLs, and the values are their weights.
139+
140+
URLs with higher weights contribute more to the overall category scores. For example, a URL with weight 2 has twice the influence of a URL with weight 1.
141+
142+
```ts
143+
import lighthousePlugin from '@code-pushup/lighthouse-plugin';
144+
145+
export default {
146+
// ...
147+
plugins: [
148+
// ...
149+
await lighthousePlugin({
150+
'https://example.com': 2,
151+
'https://example.com/contact': 1,
152+
})
153+
];
154+
};
155+
```
156+
157+
### Categories with multiple URLs
158+
159+
When running Lighthouse against multiple URLs, use the `mergeLighthouseCategories` utility to ensure categories are correctly expanded and results are aggregated per URL.
160+
161+
#### Basic usage
162+
163+
```ts
164+
import lighthousePlugin, { mergeLighthouseCategories } from '@code-pushup/lighthouse-plugin';
165+
166+
const lhPlugin = await lighthousePlugin(urls);
167+
168+
export default {
169+
plugins: [
170+
// ...
171+
lhPlugin,
172+
],
173+
categories: [
174+
// ...
175+
...mergeLighthouseCategories(lhPlugin),
176+
],
177+
};
178+
```
179+
180+
#### Custom categories
181+
182+
If you provide custom categories, you can reference both groups and audits as usual. The merging utility will expand each referenced group or audit for every URL, assigning the correct per-URL weight.
183+
184+
```ts
185+
import lighthousePlugin, { lighthouseAuditRef, lighthouseGroupRef, mergeLighthouseCategories } from '@code-pushup/lighthouse-plugin';
186+
187+
const lhPlugin = await lighthousePlugin(urls);
188+
189+
export default {
190+
// ...
191+
plugins: [
192+
// ...
193+
lhPlugin,
194+
],
195+
categories: [
196+
// ...
197+
...mergeLighthouseCategories(lhPlugin, [
198+
{
199+
slug: 'performance',
200+
title: 'Performance',
201+
refs: [lighthouseGroupRef('performance'), lighthouseAuditRef('first-contentful-paint', 2)],
202+
},
203+
]),
204+
],
205+
};
206+
```
207+
208+
### Behavior Summary
209+
210+
- **No categories**: The plugin auto-generates categories from the plugin's default Lighthouse groups.
211+
- **Custom categories**: The plugin expands all referenced audits and groups for each URL, applying appropriate weights.
212+
- **Empty array** (`categories: []`): No categories are created or expanded, which is useful when you only want audit data.
213+
120214
## Flags
121215

122216
The plugin accepts an optional second argument, `flags`.

packages/plugin-lighthouse/src/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ export {
66
LIGHTHOUSE_PLUGIN_SLUG,
77
LIGHTHOUSE_OUTPUT_PATH,
88
} from './lib/constants.js';
9-
export {
10-
lighthouseAuditRef,
11-
lighthouseGroupRef,
12-
type LighthouseGroupSlugs,
13-
} from './lib/utils.js';
14-
export type { LighthouseOptions } from './lib/types.js';
9+
export { lighthouseAuditRef, lighthouseGroupRef } from './lib/utils.js';
10+
export type {
11+
LighthouseGroupSlug,
12+
LighthouseOptions,
13+
LighthouseUrls,
14+
} from './lib/types.js';
1515
export { lighthousePlugin } from './lib/lighthouse-plugin.js';
1616
export default lighthousePlugin;
17+
export { mergeLighthouseCategories } from './lib/merge-categories.js';

packages/plugin-lighthouse/src/lib/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,13 @@ export const LIGHTHOUSE_OUTPUT_PATH = path.join(
1010
DEFAULT_PERSIST_OUTPUT_DIR,
1111
LIGHTHOUSE_PLUGIN_SLUG,
1212
);
13+
14+
export const LIGHTHOUSE_GROUP_SLUGS = [
15+
'performance',
16+
'accessibility',
17+
'best-practices',
18+
'seo',
19+
'pwa',
20+
] as const;
21+
22+
export const SINGLE_URL_THRESHOLD = 1;

packages/plugin-lighthouse/src/lib/lighthouse-plugin.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,24 @@ import { createRequire } from 'node:module';
22
import type { PluginConfig } from '@code-pushup/models';
33
import { LIGHTHOUSE_PLUGIN_SLUG } from './constants.js';
44
import { normalizeFlags } from './normalize-flags.js';
5-
import {
6-
LIGHTHOUSE_GROUPS,
7-
LIGHTHOUSE_NAVIGATION_AUDITS,
8-
} from './runner/constants.js';
5+
import { normalizeUrlInput, processAuditsAndGroups } from './processing.js';
96
import { createRunnerFunction } from './runner/runner.js';
10-
import type { LighthouseOptions } from './types.js';
11-
import { markSkippedAuditsAndGroups } from './utils.js';
7+
import type { LighthouseOptions, LighthouseUrls } from './types.js';
128

139
export function lighthousePlugin(
14-
url: string,
10+
urls: LighthouseUrls,
1511
flags?: LighthouseOptions,
1612
): PluginConfig {
1713
const { skipAudits, onlyAudits, onlyCategories, ...unparsedFlags } =
1814
normalizeFlags(flags ?? {});
1915

20-
const { audits, groups } = markSkippedAuditsAndGroups(
21-
LIGHTHOUSE_NAVIGATION_AUDITS,
22-
LIGHTHOUSE_GROUPS,
23-
{ skipAudits, onlyAudits, onlyCategories },
24-
);
16+
const { urls: normalizedUrls, context } = normalizeUrlInput(urls);
17+
18+
const { audits, groups } = processAuditsAndGroups(normalizedUrls, {
19+
skipAudits,
20+
onlyAudits,
21+
onlyCategories,
22+
});
2523

2624
const packageJson = createRequire(import.meta.url)(
2725
'../../package.json',
@@ -35,11 +33,12 @@ export function lighthousePlugin(
3533
icon: 'lighthouse',
3634
audits,
3735
groups,
38-
runner: createRunnerFunction(url, {
36+
runner: createRunnerFunction(normalizedUrls, {
3937
skipAudits,
4038
onlyAudits,
4139
onlyCategories,
4240
...unparsedFlags,
4341
}),
42+
context,
4443
};
4544
}

0 commit comments

Comments
 (0)