Skip to content

Commit ce78358

Browse files
committed
feat(plugin-lighthouse): add setup wizard binding
1 parent 28f5ad1 commit ce78358

File tree

12 files changed

+543
-47
lines changed

12 files changed

+543
-47
lines changed

packages/create-cli/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ Each plugin exposes its own configuration keys that can be passed as CLI argumen
6565
| **`--typescript.tsconfig`** | `string` | auto-detected | TypeScript config file |
6666
| **`--typescript.categories`** | `boolean` | `true` | Add TypeScript categories |
6767

68+
#### Lighthouse
69+
70+
| Option | Type | Default | Description |
71+
| ----------------------------- | ---------------------------------------------------------------- | ----------------------- | ------------------------------- |
72+
| **`--lighthouse.urls`** | `string` | `http://localhost:4200` | Target URL(s) (comma-separated) |
73+
| **`--lighthouse.categories`** | `('performance'` \| `'a11y'` \| `'best-practices'` \| `'seo')[]` | all | Lighthouse categories |
74+
6875
### Examples
6976

7077
Run interactively (default):

packages/create-cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@code-pushup/coverage-plugin": "0.123.0",
3030
"@code-pushup/eslint-plugin": "0.123.0",
3131
"@code-pushup/js-packages-plugin": "0.123.0",
32+
"@code-pushup/lighthouse-plugin": "0.123.0",
3233
"@code-pushup/models": "0.123.0",
3334
"@code-pushup/typescript-plugin": "0.123.0",
3435
"@code-pushup/utils": "0.123.0",

packages/create-cli/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { hideBin } from 'yargs/helpers';
44
import { coverageSetupBinding } from '@code-pushup/coverage-plugin';
55
import { eslintSetupBinding } from '@code-pushup/eslint-plugin';
66
import { jsPackagesSetupBinding } from '@code-pushup/js-packages-plugin';
7+
import { lighthouseSetupBinding } from '@code-pushup/lighthouse-plugin';
78
import { typescriptSetupBinding } from '@code-pushup/typescript-plugin';
89
import { parsePluginSlugs, validatePluginSlugs } from './lib/setup/plugins.js';
910
import {
@@ -14,12 +15,13 @@ import {
1415
} from './lib/setup/types.js';
1516
import { runSetupWizard } from './lib/setup/wizard.js';
1617

17-
// TODO: create, import and pass remaining plugin bindings (lighthouse, jsdocs, axe)
18+
// TODO: create, import and pass remaining plugin bindings (jsdocs, axe)
1819
const bindings: PluginSetupBinding[] = [
1920
eslintSetupBinding,
2021
coverageSetupBinding,
2122
jsPackagesSetupBinding,
2223
typescriptSetupBinding,
24+
lighthouseSetupBinding,
2325
];
2426

2527
const argv = await yargs(hideBin(process.argv))
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { type Prettify, singleQuote } from '@code-pushup/utils';
2+
import type { CodeBuilder } from './codegen.js';
3+
import type {
4+
CategoryCodegenConfig,
5+
PluginCodegenResult,
6+
RefsExpression,
7+
} from './types.js';
8+
9+
type FlattenUnionArrays<T> = T extends (infer U)[] ? U : T;
10+
11+
type CategoryCodegenRef = FlattenUnionArrays<CategoryCodegenConfig['refs']>;
12+
13+
type MergedCategory = Prettify<
14+
Omit<CategoryCodegenConfig, 'refs'> & {
15+
refs: CategoryCodegenRef[];
16+
}
17+
>;
18+
19+
export function addCategories(
20+
builder: CodeBuilder,
21+
plugins: PluginCodegenResult[],
22+
depth = 1,
23+
): void {
24+
const categories = mergeCategoriesBySlug(
25+
plugins.flatMap(p => p.categories ?? []).map(toMergedCategory),
26+
);
27+
if (categories.length === 0) {
28+
return;
29+
}
30+
builder.addLine('categories: [', depth);
31+
categories.forEach(({ slug, title, description, docsUrl, refs }) => {
32+
builder.addLine('{', depth + 1);
33+
builder.addLine(`slug: '${slug}',`, depth + 2);
34+
builder.addLine(`title: ${singleQuote(title)},`, depth + 2);
35+
if (description) {
36+
builder.addLine(`description: ${singleQuote(description)},`, depth + 2);
37+
}
38+
if (docsUrl) {
39+
builder.addLine(`docsUrl: ${singleQuote(docsUrl)},`, depth + 2);
40+
}
41+
addCategoryRefs(builder, refs, depth + 2);
42+
builder.addLine('},', depth + 1);
43+
});
44+
builder.addLine('],', depth);
45+
}
46+
47+
function isRefsExpression(value: unknown): value is RefsExpression {
48+
return typeof value === 'string';
49+
}
50+
51+
function toMergedCategory({
52+
slug,
53+
title,
54+
description,
55+
docsUrl,
56+
refs,
57+
}: CategoryCodegenConfig): MergedCategory {
58+
return {
59+
slug,
60+
title,
61+
description,
62+
docsUrl,
63+
refs: isRefsExpression(refs) ? [refs] : refs,
64+
};
65+
}
66+
67+
function mergeCategoriesBySlug(categories: MergedCategory[]): MergedCategory[] {
68+
const map = categories.reduce((acc, category) => {
69+
const existing = acc.get(category.slug);
70+
acc.set(
71+
category.slug,
72+
existing ? mergeCategory(existing, category) : category,
73+
);
74+
return acc;
75+
}, new Map<string, MergedCategory>());
76+
return [...map.values()];
77+
}
78+
79+
function mergeCategory(
80+
existing: MergedCategory,
81+
incoming: MergedCategory,
82+
): MergedCategory {
83+
return {
84+
...existing,
85+
description: existing.description ?? incoming.description,
86+
docsUrl: existing.docsUrl ?? incoming.docsUrl,
87+
refs: [...existing.refs, ...incoming.refs],
88+
};
89+
}
90+
91+
function addCategoryRefs(
92+
builder: CodeBuilder,
93+
refs: CategoryCodegenRef[],
94+
depth: number,
95+
): void {
96+
builder.addLine('refs: [', depth);
97+
builder.addLines(refs.map(formatCategoryRef), depth + 1);
98+
builder.addLine('],', depth);
99+
}
100+
101+
function formatCategoryRef(ref: CategoryCodegenRef): string {
102+
if (isRefsExpression(ref)) {
103+
return `...${ref},`;
104+
}
105+
return `{ type: '${ref.type}', plugin: '${ref.plugin}', slug: '${ref.slug}', weight: ${ref.weight} },`;
106+
}

packages/create-cli/src/lib/setup/codegen.ts

Lines changed: 19 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import path from 'node:path';
2-
import type { CategoryRef } from '@code-pushup/models';
3-
import {
4-
mergeCategoriesBySlug,
5-
singleQuote,
6-
toUnixPath,
7-
} from '@code-pushup/utils';
2+
import { exists, toUnixPath } from '@code-pushup/utils';
3+
import { addCategories } from './codegen-categories.js';
84
import type {
95
ConfigFileFormat,
106
ImportDeclarationStructure,
@@ -17,7 +13,7 @@ const CORE_CONFIG_IMPORT: ImportDeclarationStructure = {
1713
isTypeOnly: true,
1814
};
1915

20-
class CodeBuilder {
16+
export class CodeBuilder {
2117
private lines: string[] = [];
2218

2319
addLine(text: string, depth = 0): void {
@@ -45,6 +41,7 @@ export function generateConfigSource(
4541
): string {
4642
const builder = new CodeBuilder();
4743
addImports(builder, collectImports(plugins, format));
44+
addPluginDeclarations(builder, plugins);
4845
if (format === 'ts') {
4946
builder.addLine('export default {');
5047
addPlugins(builder, plugins);
@@ -66,6 +63,7 @@ export function generatePresetSource(
6663
): string {
6764
const builder = new CodeBuilder();
6865
addImports(builder, collectImports(plugins, format));
66+
addPluginDeclarations(builder, plugins);
6967
addPresetExport(builder, plugins, format);
7068
return builder.toString();
7169
}
@@ -137,6 +135,20 @@ function addImports(
137135
}
138136
}
139137

138+
function addPluginDeclarations(
139+
builder: CodeBuilder,
140+
plugins: PluginCodegenResult[],
141+
): void {
142+
const declarations = plugins
143+
.map(({ pluginDeclaration }) => pluginDeclaration)
144+
.filter(exists)
145+
.map(d => `const ${d.identifier} = ${d.expression};`);
146+
if (declarations.length > 0) {
147+
builder.addLines(declarations);
148+
builder.addEmptyLine();
149+
}
150+
}
151+
140152
function addPlugins(
141153
builder: CodeBuilder,
142154
plugins: PluginCodegenResult[],
@@ -183,37 +195,3 @@ function addPresetExport(
183195
builder.addLine('};', 1);
184196
builder.addLine('}');
185197
}
186-
187-
function addCategories(
188-
builder: CodeBuilder,
189-
plugins: PluginCodegenResult[],
190-
depth = 1,
191-
): void {
192-
const categories = mergeCategoriesBySlug(
193-
plugins.flatMap(p => p.categories ?? []),
194-
);
195-
if (categories.length === 0) {
196-
return;
197-
}
198-
builder.addLine('categories: [', depth);
199-
categories.forEach(({ slug, title, description, docsUrl, refs }) => {
200-
builder.addLine('{', depth + 1);
201-
builder.addLine(`slug: '${slug}',`, depth + 2);
202-
builder.addLine(`title: ${singleQuote(title)},`, depth + 2);
203-
if (description) {
204-
builder.addLine(`description: ${singleQuote(description)},`, depth + 2);
205-
}
206-
if (docsUrl) {
207-
builder.addLine(`docsUrl: ${singleQuote(docsUrl)},`, depth + 2);
208-
}
209-
builder.addLine('refs: [', depth + 2);
210-
builder.addLines(refs.map(formatCategoryRef), depth + 3);
211-
builder.addLine('],', depth + 2);
212-
builder.addLine('},', depth + 1);
213-
});
214-
builder.addLine('],', depth);
215-
}
216-
217-
function formatCategoryRef(ref: CategoryRef): string {
218-
return `{ type: '${ref.type}', plugin: '${ref.plugin}', slug: '${ref.slug}', weight: ${ref.weight} },`;
219-
}

packages/create-cli/src/lib/setup/codegen.unit.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,128 @@ describe('generateConfigSource', () => {
376376
expect(source).toContain("plugin: 'ts'");
377377
});
378378
});
379+
380+
describe('pluginDeclaration', () => {
381+
it('should emit variable declaration between imports and config export', () => {
382+
const plugin: PluginCodegenResult = {
383+
imports: [
384+
{
385+
moduleSpecifier: '@code-pushup/lighthouse-plugin',
386+
defaultImport: 'lighthousePlugin',
387+
},
388+
],
389+
pluginDeclaration: {
390+
identifier: 'lhPlugin',
391+
expression: "lighthousePlugin('http://localhost:4200')",
392+
},
393+
pluginInit: ['lhPlugin,'],
394+
};
395+
expect(generateConfigSource([plugin], 'ts')).toMatchInlineSnapshot(`
396+
"import lighthousePlugin from '@code-pushup/lighthouse-plugin';
397+
import type { CoreConfig } from '@code-pushup/models';
398+
399+
const lhPlugin = lighthousePlugin('http://localhost:4200');
400+
401+
export default {
402+
plugins: [
403+
lhPlugin,
404+
],
405+
} satisfies CoreConfig;
406+
"
407+
`);
408+
});
409+
});
410+
411+
describe('expression refs', () => {
412+
it('should generate config with expression refs and merged categories', () => {
413+
expect(
414+
generateConfigSource(
415+
[
416+
{
417+
imports: [
418+
{
419+
moduleSpecifier: '@code-pushup/lighthouse-plugin',
420+
defaultImport: 'lighthousePlugin',
421+
namedImports: ['lighthouseGroupRefs'],
422+
},
423+
],
424+
pluginDeclaration: {
425+
identifier: 'lhPlugin',
426+
expression: "lighthousePlugin('http://localhost:4200')",
427+
},
428+
pluginInit: ['lhPlugin,'],
429+
categories: [
430+
{
431+
slug: 'a11y',
432+
title: 'Accessibility',
433+
refs: "lighthouseGroupRefs(lhPlugin, 'accessibility')",
434+
},
435+
{
436+
slug: 'performance',
437+
title: 'Performance',
438+
refs: "lighthouseGroupRefs(lhPlugin, 'performance')",
439+
},
440+
],
441+
},
442+
{
443+
imports: [
444+
{
445+
moduleSpecifier: '@code-pushup/axe-plugin',
446+
defaultImport: 'axePlugin',
447+
namedImports: ['axeGroupRefs'],
448+
},
449+
],
450+
pluginDeclaration: {
451+
identifier: 'axe',
452+
expression: "axePlugin('http://localhost:4200')",
453+
},
454+
pluginInit: ['axe,'],
455+
categories: [
456+
{
457+
slug: 'a11y',
458+
title: 'Accessibility',
459+
refs: 'axeGroupRefs(axe)',
460+
},
461+
],
462+
},
463+
],
464+
'ts',
465+
),
466+
).toMatchInlineSnapshot(`
467+
"import axePlugin, { axeGroupRefs } from '@code-pushup/axe-plugin';
468+
import lighthousePlugin, { lighthouseGroupRefs } from '@code-pushup/lighthouse-plugin';
469+
import type { CoreConfig } from '@code-pushup/models';
470+
471+
const lhPlugin = lighthousePlugin('http://localhost:4200');
472+
const axe = axePlugin('http://localhost:4200');
473+
474+
export default {
475+
plugins: [
476+
lhPlugin,
477+
axe,
478+
],
479+
categories: [
480+
{
481+
slug: 'a11y',
482+
title: 'Accessibility',
483+
refs: [
484+
...lighthouseGroupRefs(lhPlugin, 'accessibility'),
485+
...axeGroupRefs(axe),
486+
],
487+
},
488+
{
489+
slug: 'performance',
490+
title: 'Performance',
491+
refs: [
492+
...lighthouseGroupRefs(lhPlugin, 'performance'),
493+
],
494+
},
495+
],
496+
} satisfies CoreConfig;
497+
"
498+
`);
499+
});
500+
});
379501
});
380502

381503
describe('generatePresetSource', () => {

packages/create-cli/src/lib/setup/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import type { PluginCodegenResult } from '@code-pushup/models';
22
import type { MonorepoTool } from '@code-pushup/utils';
33

44
export type {
5+
CategoryCodegenConfig,
56
ImportDeclarationStructure,
67
PluginAnswer,
78
PluginCodegenResult,
89
PluginPromptDescriptor,
910
PluginSetupBinding,
1011
PluginSetupTree,
12+
RefsExpression,
1113
} from '@code-pushup/models';
1214

1315
export const CI_PROVIDERS = ['github', 'gitlab', 'none'] as const;

0 commit comments

Comments
 (0)