Skip to content

Commit 4ba86c7

Browse files
committed
refactor(models,utils): extract shared URL utilities for plugins
1 parent f15817b commit 4ba86c7

18 files changed

+795
-563
lines changed

code-pushup.preset.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import type {
33
CategoryConfig,
44
CoreConfig,
5+
PluginUrls,
56
} from './packages/models/src/index.js';
67
import coveragePlugin, {
78
getNxCoveragePaths,
@@ -20,7 +21,6 @@ import {
2021
} from './packages/plugin-jsdocs/src/lib/constants.js';
2122
import { filterGroupsByOnlyAudits } from './packages/plugin-jsdocs/src/lib/utils.js';
2223
import lighthousePlugin, {
23-
type LighthouseUrls,
2424
lighthouseGroupRef,
2525
mergeLighthouseCategories,
2626
} from './packages/plugin-lighthouse/src/index.js';
@@ -137,7 +137,7 @@ export const jsPackagesCoreConfig = async (): Promise<CoreConfig> => ({
137137
});
138138

139139
export const lighthouseCoreConfig = async (
140-
urls: LighthouseUrls,
140+
urls: PluginUrls,
141141
): Promise<CoreConfig> => {
142142
const lhPlugin = await lighthousePlugin(urls);
143143
return {

packages/models/docs/models-reference.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,16 @@ _Union of the following possible types:_
13661366
- _Object with dynamic keys of type_ `string` _and values of type_ `number` (_≥0, ≤1_)
13671367
(_optional_)
13681368

1369+
## PluginUrls
1370+
1371+
URL(s) to analyze. Single URL, array of URLs, or record of URLs with custom weights
1372+
1373+
_Union of the following possible types:_
1374+
1375+
- `string` (_url_)
1376+
- `Array<string (_url_)>`
1377+
- _Object with dynamic keys of type_ `string` (_url_) _and values of type_ `number` (_>0_)
1378+
13691379
## Report
13701380

13711381
Collect output data

packages/models/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,12 @@ export {
9393
pluginContextSchema,
9494
pluginMetaSchema,
9595
pluginScoreTargetsSchema,
96+
pluginUrlsSchema,
9697
type PluginConfig,
9798
type PluginContext,
9899
type PluginMeta,
99100
type PluginScoreTargets,
101+
type PluginUrls,
100102
} from './lib/plugin-config.js';
101103
export {
102104
auditReportSchema,

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ export const pluginConfigSchema = pluginMetaSchema
7272

7373
export type PluginConfig = z.infer<typeof pluginConfigSchema>;
7474

75+
export const pluginUrlsSchema = z
76+
.union([z.url(), z.array(z.url()), z.record(z.url(), z.number().positive())])
77+
.meta({
78+
title: 'PluginUrls',
79+
description:
80+
'URL(s) to analyze. Single URL, array of URLs, or record of URLs with custom weights',
81+
});
82+
83+
export type PluginUrls = z.infer<typeof pluginUrlsSchema>;
84+
7585
// every listed group ref points to an audit within the plugin
7686
export function findMissingSlugsInGroupRefs<
7787
T extends { audits: Audit[]; groups?: Group[] },

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { describe, expect, it } from 'vitest';
2-
import { type PluginConfig, pluginConfigSchema } from './plugin-config.js';
2+
import {
3+
type PluginConfig,
4+
pluginConfigSchema,
5+
pluginUrlsSchema,
6+
} from './plugin-config.js';
37

48
describe('pluginConfigSchema', () => {
59
it('should accept a valid plugin configuration with all entities', () => {
@@ -135,3 +139,37 @@ describe('pluginConfigSchema', () => {
135139
).toThrow('slug has to follow the pattern');
136140
});
137141
});
142+
143+
describe('pluginUrlsSchema', () => {
144+
it('should accept a single URL string', () => {
145+
expect(() => pluginUrlsSchema.parse('https://example.com')).not.toThrow();
146+
});
147+
148+
it('should accept an array of URLs', () => {
149+
expect(() =>
150+
pluginUrlsSchema.parse([
151+
'https://example.com',
152+
'https://example.com/about',
153+
]),
154+
).not.toThrow();
155+
});
156+
157+
it('should accept a weighted object of URLs', () => {
158+
expect(() =>
159+
pluginUrlsSchema.parse({
160+
'https://example.com': 2,
161+
'https://example.com/about': 1,
162+
}),
163+
).not.toThrow();
164+
});
165+
166+
it('should throw for invalid URL', () => {
167+
expect(() => pluginUrlsSchema.parse('not-a-url')).toThrow();
168+
});
169+
170+
it('should throw for array with invalid URL', () => {
171+
expect(() =>
172+
pluginUrlsSchema.parse(['https://example.com', 'invalid']),
173+
).toThrow();
174+
});
175+
});

packages/plugin-lighthouse/src/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@ export {
77
LIGHTHOUSE_OUTPUT_PATH,
88
} from './lib/constants.js';
99
export { lighthouseAuditRef, lighthouseGroupRef } from './lib/utils.js';
10-
export type {
11-
LighthouseGroupSlug,
12-
LighthouseOptions,
13-
LighthouseUrls,
14-
} from './lib/types.js';
10+
export type { LighthouseGroupSlug, LighthouseOptions } from './lib/types.js';
1511
export { lighthousePlugin } from './lib/lighthouse-plugin.js';
1612
export default lighthousePlugin;
1713
export { mergeLighthouseCategories } from './lib/merge-categories.js';

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { createRequire } from 'node:module';
2-
import type { PluginConfig } from '@code-pushup/models';
2+
import type { PluginConfig, PluginUrls } from '@code-pushup/models';
3+
import { normalizeUrlInput } from '@code-pushup/utils';
34
import { LIGHTHOUSE_PLUGIN_SLUG } from './constants.js';
45
import { normalizeFlags } from './normalize-flags.js';
5-
import { normalizeUrlInput, processAuditsAndGroups } from './processing.js';
6+
import { processAuditsAndGroups } from './processing.js';
67
import { createRunnerFunction } from './runner/runner.js';
7-
import type { LighthouseOptions, LighthouseUrls } from './types.js';
8+
import type { LighthouseOptions } from './types.js';
89

910
export function lighthousePlugin(
10-
urls: LighthouseUrls,
11+
urls: PluginUrls,
1112
flags?: LighthouseOptions,
1213
): PluginConfig {
1314
const {
Lines changed: 21 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import type { CategoryConfig, Group, PluginConfig } from '@code-pushup/models';
2+
import {
3+
type PluginUrlContext,
4+
createCategoryRefs,
5+
expandCategoryRefs,
6+
removeIndex,
7+
shouldExpandForUrls,
8+
validateUrlContext,
9+
} from '@code-pushup/utils';
210
import { LIGHTHOUSE_GROUP_SLUGS, LIGHTHOUSE_PLUGIN_SLUG } from './constants.js';
3-
import { orderSlug, shouldExpandForUrls } from './processing.js';
411
import { LIGHTHOUSE_GROUPS } from './runner/constants.js';
5-
import type { LighthouseContext, LighthouseGroupSlug } from './types.js';
12+
import type { LighthouseGroupSlug } from './types.js';
613
import { isLighthouseGroupSlug } from './utils.js';
714

815
/**
@@ -31,7 +38,7 @@ export function mergeLighthouseCategories(
3138
if (!plugin.groups || plugin.groups.length === 0) {
3239
return categories ?? [];
3340
}
34-
validateContext(plugin.context);
41+
validateUrlContext(plugin.context);
3542
if (!categories) {
3643
return createCategories(plugin.groups, plugin.context);
3744
}
@@ -40,7 +47,7 @@ export function mergeLighthouseCategories(
4047

4148
function createCategories(
4249
groups: Group[],
43-
context: LighthouseContext,
50+
context: PluginUrlContext,
4451
): CategoryConfig[] {
4552
if (!shouldExpandForUrls(context.urlCount)) {
4653
return [];
@@ -52,7 +59,7 @@ function createCategories(
5259

5360
function expandCategories(
5461
categories: CategoryConfig[],
55-
context: LighthouseContext,
62+
context: PluginUrlContext,
5663
): CategoryConfig[] {
5764
if (!shouldExpandForUrls(context.urlCount)) {
5865
return categories;
@@ -68,7 +75,7 @@ function expandCategories(
6875
*/
6976
export function createAggregatedCategory(
7077
groupSlug: LighthouseGroupSlug,
71-
context: LighthouseContext,
78+
context: PluginUrlContext,
7279
): CategoryConfig {
7380
const group = LIGHTHOUSE_GROUPS.find(({ slug }) => slug === groupSlug);
7481
if (!group) {
@@ -81,14 +88,7 @@ export function createAggregatedCategory(
8188
slug: group.slug,
8289
title: group.title,
8390
...(group.description && { description: group.description }),
84-
refs: Array.from({ length: context.urlCount }, (_, i) => ({
85-
plugin: LIGHTHOUSE_PLUGIN_SLUG,
86-
slug: shouldExpandForUrls(context.urlCount)
87-
? orderSlug(group.slug, i)
88-
: group.slug,
89-
type: 'group',
90-
weight: resolveWeight(context.weights, i),
91-
})),
91+
refs: createCategoryRefs(group.slug, LIGHTHOUSE_PLUGIN_SLUG, context),
9292
};
9393
}
9494

@@ -98,22 +98,15 @@ export function createAggregatedCategory(
9898
*/
9999
export function expandAggregatedCategory(
100100
category: CategoryConfig,
101-
context: LighthouseContext,
101+
context: PluginUrlContext,
102102
): CategoryConfig {
103103
return {
104104
...category,
105-
refs: category.refs.flatMap(ref => {
106-
if (ref.plugin === LIGHTHOUSE_PLUGIN_SLUG) {
107-
return Array.from({ length: context.urlCount }, (_, i) => ({
108-
...ref,
109-
slug: shouldExpandForUrls(context.urlCount)
110-
? orderSlug(ref.slug, i)
111-
: ref.slug,
112-
weight: resolveWeight(context.weights, i, ref.weight),
113-
}));
114-
}
115-
return [ref];
116-
}),
105+
refs: category.refs.flatMap(ref =>
106+
ref.plugin === LIGHTHOUSE_PLUGIN_SLUG
107+
? expandCategoryRefs(ref, context)
108+
: [ref],
109+
),
117110
};
118111
}
119112

@@ -122,38 +115,6 @@ export function expandAggregatedCategory(
122115
* Useful for deduplicating and normalizing group slugs when generating categories.
123116
*/
124117
export function extractGroupSlugs(groups: Group[]): LighthouseGroupSlug[] {
125-
const slugs = groups.map(({ slug }) => slug.replace(/-\d+$/, ''));
118+
const slugs = groups.map(({ slug }) => removeIndex(slug));
126119
return [...new Set(slugs)].filter(isLighthouseGroupSlug);
127120
}
128-
129-
export class ContextValidationError extends Error {
130-
constructor(message: string) {
131-
super(`Invalid Lighthouse context: ${message}`);
132-
}
133-
}
134-
135-
export function validateContext(
136-
context: PluginConfig['context'],
137-
): asserts context is LighthouseContext {
138-
if (!context || typeof context !== 'object') {
139-
throw new ContextValidationError('must be an object');
140-
}
141-
const { urlCount, weights } = context;
142-
if (typeof urlCount !== 'number' || urlCount < 0) {
143-
throw new ContextValidationError('urlCount must be a non-negative number');
144-
}
145-
if (!weights || typeof weights !== 'object') {
146-
throw new ContextValidationError('weights must be an object');
147-
}
148-
if (Object.keys(weights).length !== urlCount) {
149-
throw new ContextValidationError('weights count must match urlCount');
150-
}
151-
}
152-
153-
function resolveWeight(
154-
weights: LighthouseContext['weights'],
155-
index: number,
156-
userDefinedWeight?: number,
157-
): number {
158-
return weights[index + 1] ?? userDefinedWeight ?? 1;
159-
}

packages/plugin-lighthouse/src/lib/merge-categories.unit.test.ts

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ import { describe, expect, it } from 'vitest';
22
import type { CategoryConfig } from '@code-pushup/models';
33
import { LIGHTHOUSE_PLUGIN_SLUG } from './constants.js';
44
import {
5-
ContextValidationError,
65
createAggregatedCategory,
76
expandAggregatedCategory,
87
extractGroupSlugs,
98
mergeLighthouseCategories,
10-
validateContext,
119
} from './merge-categories.js';
1210

1311
describe('mergeLighthouseCategories', () => {
@@ -830,35 +828,3 @@ describe('expandAggregatedCategory', () => {
830828
]);
831829
});
832830
});
833-
834-
describe('validateContext', () => {
835-
it('should throw error for invalid context (undefined)', () => {
836-
expect(() => validateContext(undefined)).toThrow(
837-
new ContextValidationError('must be an object'),
838-
);
839-
});
840-
841-
it('should throw error for invalid context (missing urlCount)', () => {
842-
expect(() => validateContext({ weights: {} })).toThrow(
843-
new ContextValidationError('urlCount must be a non-negative number'),
844-
);
845-
});
846-
847-
it('should throw error for invalid context (negative urlCount)', () => {
848-
expect(() => validateContext({ urlCount: -1, weights: {} })).toThrow(
849-
new ContextValidationError('urlCount must be a non-negative number'),
850-
);
851-
});
852-
853-
it('should throw error for invalid context (missing weights)', () => {
854-
expect(() => validateContext({ urlCount: 2 })).toThrow(
855-
new ContextValidationError('weights must be an object'),
856-
);
857-
});
858-
859-
it('should accept valid context', () => {
860-
expect(() =>
861-
validateContext({ urlCount: 2, weights: { 1: 1, 2: 1 } }),
862-
).not.toThrow();
863-
});
864-
});

0 commit comments

Comments
 (0)