Skip to content

Commit 6ef64ba

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

File tree

14 files changed

+544
-46
lines changed

14 files changed

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

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: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,130 @@ 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+
refsExpression:
434+
"lighthouseGroupRefs(lhPlugin, 'accessibility')",
435+
},
436+
{
437+
slug: 'performance',
438+
title: 'Performance',
439+
refsExpression:
440+
"lighthouseGroupRefs(lhPlugin, 'performance')",
441+
},
442+
],
443+
},
444+
{
445+
imports: [
446+
{
447+
moduleSpecifier: '@code-pushup/axe-plugin',
448+
defaultImport: 'axePlugin',
449+
namedImports: ['axeGroupRefs'],
450+
},
451+
],
452+
pluginDeclaration: {
453+
identifier: 'axe',
454+
expression: "axePlugin('http://localhost:4200')",
455+
},
456+
pluginInit: ['axe,'],
457+
categories: [
458+
{
459+
slug: 'a11y',
460+
title: 'Accessibility',
461+
refsExpression: 'axeGroupRefs(axe)',
462+
},
463+
],
464+
},
465+
],
466+
'ts',
467+
),
468+
).toMatchInlineSnapshot(`
469+
"import axePlugin, { axeGroupRefs } from '@code-pushup/axe-plugin';
470+
import lighthousePlugin, { lighthouseGroupRefs } from '@code-pushup/lighthouse-plugin';
471+
import type { CoreConfig } from '@code-pushup/models';
472+
473+
const lhPlugin = lighthousePlugin('http://localhost:4200');
474+
const axe = axePlugin('http://localhost:4200');
475+
476+
export default {
477+
plugins: [
478+
lhPlugin,
479+
axe,
480+
],
481+
categories: [
482+
{
483+
slug: 'a11y',
484+
title: 'Accessibility',
485+
refs: [
486+
...lighthouseGroupRefs(lhPlugin, 'accessibility'),
487+
...axeGroupRefs(axe),
488+
],
489+
},
490+
{
491+
slug: 'performance',
492+
title: 'Performance',
493+
refs: [
494+
...lighthouseGroupRefs(lhPlugin, 'performance'),
495+
],
496+
},
497+
],
498+
} satisfies CoreConfig;
499+
"
500+
`);
501+
});
502+
});
379503
});
380504

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ 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,

packages/models/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,11 @@ export {
113113
type PluginUrls,
114114
} from './lib/plugin-config.js';
115115
export type {
116+
CategoryCodegenConfig,
116117
ImportDeclarationStructure,
117118
PluginAnswer,
118119
PluginCodegenResult,
120+
PluginDeclarationStructure,
119121
PluginPromptDescriptor,
120122
PluginSetupBinding,
121123
PluginSetupTree,

0 commit comments

Comments
 (0)