Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
15 changes: 10 additions & 5 deletions code-pushup.preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import {
} from './packages/plugin-jsdocs/src/lib/constants.js';
import { filterGroupsByOnlyAudits } from './packages/plugin-jsdocs/src/lib/utils.js';
import lighthousePlugin, {
type LighthouseUrls,
lighthouseGroupRef,
mergeLighthouseCategories,
} from './packages/plugin-lighthouse/src/index.js';
import typescriptPlugin, {
type TypescriptPluginOptions,
Expand Down Expand Up @@ -135,11 +137,14 @@ export const jsPackagesCoreConfig = async (): Promise<CoreConfig> => ({
});

export const lighthouseCoreConfig = async (
url: string,
): Promise<CoreConfig> => ({
plugins: [await lighthousePlugin(url)],
categories: lighthouseCategories,
});
urls: LighthouseUrls,
): Promise<CoreConfig> => {
const lhPlugin = await lighthousePlugin(urls);
return {
plugins: [lhPlugin],
categories: mergeLighthouseCategories(lhPlugin, lighthouseCategories),
};
};

export const jsDocsCoreConfig = (
config: JsDocsPluginConfig | string[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ exports[`PLUGIN collect report with lighthouse-plugin NPM package > should run p
"title": "Document has a valid \`hreflang\`",
},
],
"context": {
"urlCount": 1,
"weights": {
"1": 1,
},
},
"groups": [
{
"refs": [
Expand Down
11 changes: 7 additions & 4 deletions packages/cli/src/lib/implementation/filter.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,13 @@ function applyPluginFilters(
options: Pick<FilterOptions, 'skipPlugins' | 'onlyPlugins'>,
): CoreConfig['plugins'] {
const { skipPlugins = [], onlyPlugins = [] } = options;
const filteredPlugins = filterPluginsFromCategories({
categories,
plugins,
});
const filteredPlugins =
onlyPlugins.length === 0
? filterPluginsFromCategories({
categories,
plugins,
})
: plugins;
if (skipPlugins.length === 0 && onlyPlugins.length === 0) {
return filteredPlugins;
}
Expand Down
38 changes: 38 additions & 0 deletions packages/cli/src/lib/implementation/filter.middleware.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,44 @@ describe('filterMiddleware', () => {
),
);
});

it('should allow onlyPlugins to include plugins not referenced by categories', () => {
const { plugins } = filterMiddleware({
plugins: [
{
slug: 'p1',
audits: [{ slug: 'a1-p1', isSkipped: false }],
groups: [
{
slug: 'g1-p1',
refs: [{ slug: 'a1-p1', weight: 1 }],
isSkipped: false,
},
],
},
{
slug: 'p2',
audits: [{ slug: 'a1-p2', isSkipped: false }],
groups: [
{
slug: 'g1-p2',
refs: [{ slug: 'a1-p2', weight: 1 }],
isSkipped: false,
},
],
},
] as PluginConfig[],
categories: [
{
slug: 'c1',
refs: [{ type: 'group', plugin: 'p1', slug: 'g1-p1', weight: 1 }],
},
] as CategoryConfig[],
onlyPlugins: ['p2'],
});

expect(plugins.map(plugin => plugin.slug)).toStrictEqual(['p2']);
});
});

describe('filterSkippedInPlugins', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/models/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ export {
} from './lib/persist-config.js';
export {
pluginConfigSchema,
pluginContextSchema,
pluginMetaSchema,
type PluginConfig,
type PluginContext,
type PluginMeta,
} from './lib/plugin-config.js';
export {
Expand Down
7 changes: 7 additions & 0 deletions packages/models/src/lib/plugin-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import {
import { errorItems, hasMissingStrings } from './implementation/utils.js';
import { runnerConfigSchema, runnerFunctionSchema } from './runner-config.js';

export const pluginContextSchema = z
.record(z.unknown())
.optional()
.describe('Plugin-specific context data for helpers');
export type PluginContext = z.infer<typeof pluginContextSchema>;

export const pluginMetaSchema = packageVersionSchema()
.merge(
metaSchema({
Expand All @@ -31,6 +37,7 @@ export const pluginDataSchema = z.object({
runner: z.union([runnerConfigSchema, runnerFunctionSchema]),
audits: pluginAuditsSchema,
groups: groupsSchema,
context: pluginContextSchema,
});
type PluginData = z.infer<typeof pluginDataSchema>;

Expand Down
75 changes: 75 additions & 0 deletions packages/plugin-lighthouse/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,81 @@ export default {
};
```

## Multiple URLs

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:

```ts
import lighthousePlugin from '@code-pushup/lighthouse-plugin';

export default {
// ...
plugins: [
// ...
await lighthousePlugin(['https://example.com', 'https://example.com/contact']),
],
};
```

### Assigning weights to URLs

You can assign custom weights to each URL by passing an object instead of an array. The keys are URLs, and the values are their weights:

```ts
import lighthousePlugin from '@code-pushup/lighthouse-plugin';

export default {
// ...
plugins: [
// ...
await lighthousePlugin({
'https://example.com': 2,
'https://example.com/contact': 1,
})
];
};
```

These weights influence how results from each URL contribute to the overall category scores.

### Categories with multiple URLs

When running Lighthouse against multiple URLs, it is recommended to use the `mergeLighthouseCategories` utility. This helper ensures that categories are correctly expanded and results are aggregated per URL, whether you provide your own custom categories or use the plugin's default categories.

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.

```ts
import lighthousePlugin, { lighthouseAuditRef, lighthouseGroupRef } from '@code-pushup/lighthouse-plugin';

const lhPlugin = await lighthousePlugin(urls);

export default {
// ...
plugins: [lhPlugin],
categories: mergeLighthouseCategories(lhPlugin, [
{
slug: 'performance',
title: 'Performance',
refs: [lighthouseGroupRef('performance'), lighthouseAuditRef('first-contentful-paint', 2)],
},
]),
};
```

If you do not provide custom categories, you can still use the merging utility to generate categories from the plugin's groups for all URLs.
Comment thread
matejchalk marked this conversation as resolved.
Outdated
Comment thread
matejchalk marked this conversation as resolved.
Outdated

**How weights are assigned:**

- If you assign weights to URLs, those weights take precedence for each expanded reference.
- If a reference (audit or group) also has a weight, it is used as a fallback.
- If neither is set, a default weight of `1` is used.

### Behavior Summary

- If you provide custom categories, the plugin expands all referenced audits and groups for each URL.
- If you do not provide categories, the plugin auto-generates categories from Lighthouse groups.
- If you provide an empty array (`categories: []`), no categories are created or expanded.

## Flags

The plugin accepts an optional second argument, `flags`.
Expand Down
13 changes: 7 additions & 6 deletions packages/plugin-lighthouse/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ export {
LIGHTHOUSE_PLUGIN_SLUG,
LIGHTHOUSE_OUTPUT_PATH,
} from './lib/constants.js';
export {
lighthouseAuditRef,
lighthouseGroupRef,
type LighthouseGroupSlugs,
} from './lib/utils.js';
export type { LighthouseOptions } from './lib/types.js';
export { lighthouseAuditRef, lighthouseGroupRef } from './lib/utils.js';
export type {
LighthouseGroupSlugs,
LighthouseOptions,
LighthouseUrls,
} from './lib/types.js';
export { lighthousePlugin } from './lib/lighthouse-plugin.js';
export default lighthousePlugin;
export { mergeLighthouseCategories } from './lib/merge-categories.js';
10 changes: 10 additions & 0 deletions packages/plugin-lighthouse/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,13 @@ export const LIGHTHOUSE_OUTPUT_PATH = path.join(
DEFAULT_PERSIST_OUTPUT_DIR,
LIGHTHOUSE_PLUGIN_SLUG,
);

export const LIGHTHOUSE_GROUP_SLUGS = [
'performance',
'accessibility',
'best-practices',
'seo',
'pwa',
] as const;

export const SINGLE_URL_THRESHOLD = 1;
25 changes: 12 additions & 13 deletions packages/plugin-lighthouse/src/lib/lighthouse-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,24 @@ import { createRequire } from 'node:module';
import type { PluginConfig } from '@code-pushup/models';
import { LIGHTHOUSE_PLUGIN_SLUG } from './constants.js';
import { normalizeFlags } from './normalize-flags.js';
import {
LIGHTHOUSE_GROUPS,
LIGHTHOUSE_NAVIGATION_AUDITS,
} from './runner/constants.js';
import { normalizeUrlInput, processAuditsAndGroups } from './processing.js';
import { createRunnerFunction } from './runner/runner.js';
import type { LighthouseOptions } from './types.js';
import { markSkippedAuditsAndGroups } from './utils.js';
import type { LighthouseOptions, LighthouseUrls } from './types.js';

export function lighthousePlugin(
url: string,
urls: LighthouseUrls,
flags?: LighthouseOptions,
): PluginConfig {
const { skipAudits, onlyAudits, onlyCategories, ...unparsedFlags } =
normalizeFlags(flags ?? {});

const { audits, groups } = markSkippedAuditsAndGroups(
LIGHTHOUSE_NAVIGATION_AUDITS,
LIGHTHOUSE_GROUPS,
{ skipAudits, onlyAudits, onlyCategories },
);
const { urls: normalizedUrls, context } = normalizeUrlInput(urls);

const { audits, groups } = processAuditsAndGroups(normalizedUrls, {
skipAudits,
onlyAudits,
onlyCategories,
});

const packageJson = createRequire(import.meta.url)(
'../../package.json',
Expand All @@ -35,11 +33,12 @@ export function lighthousePlugin(
icon: 'lighthouse',
audits,
groups,
runner: createRunnerFunction(url, {
runner: createRunnerFunction(normalizedUrls, {
skipAudits,
onlyAudits,
onlyCategories,
...unparsedFlags,
}),
context,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,28 @@ describe('lighthousePlugin-config-object', () => {
]);
});

it('should create valid plugin config with multiple URLs', () => {
const urls = [
'https://code-pushup-portal.com',
'https://code-pushup-portal.com/about',
];
const pluginConfig = lighthousePlugin(urls);
expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow();

const { audits, groups } = pluginConfig;
expect(audits.length).toBeGreaterThan(100);
expect(groups).toStrictEqual([
expect.objectContaining({ slug: 'performance-1' }),
expect.objectContaining({ slug: 'accessibility-1' }),
expect.objectContaining({ slug: 'best-practices-1' }),
expect.objectContaining({ slug: 'seo-1' }),
expect.objectContaining({ slug: 'performance-2' }),
expect.objectContaining({ slug: 'accessibility-2' }),
expect.objectContaining({ slug: 'best-practices-2' }),
expect.objectContaining({ slug: 'seo-2' }),
]);
});
Comment thread
matejchalk marked this conversation as resolved.

it.each([
[
{ onlyAudits: ['first-contentful-paint'] },
Expand Down
Loading