Skip to content

Commit 4e87cd5

Browse files
committed
feat(utils): add helper functions for diffing
1 parent 8cc73c3 commit 4e87cd5

File tree

5 files changed

+277
-1
lines changed

5 files changed

+277
-1
lines changed

packages/utils/src/index.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { exists } from '@code-pushup/models';
2+
export { Diff, matchArrayItemsByKey, comparePairs } from './lib/diff';
23
export {
34
ProcessConfig,
45
ProcessError,
@@ -54,11 +55,19 @@ export {
5455
README_LINK,
5556
TERMINAL_WIDTH,
5657
} from './lib/reports/constants';
58+
export {
59+
listAuditsFromAllPlugins,
60+
listGroupsFromAllPlugins,
61+
} from './lib/reports/flatten-plugins';
5762
export { generateMdReport } from './lib/reports/generate-md-report';
5863
export { generateStdoutSummary } from './lib/reports/generate-stdout-summary';
5964
export { scoreReport } from './lib/reports/scoring';
6065
export { sortReport } from './lib/reports/sorting';
61-
export { ScoredReport } from './lib/reports/types';
66+
export {
67+
ScoredCategoryConfig,
68+
ScoredGroup,
69+
ScoredReport,
70+
} from './lib/reports/types';
6271
export {
6372
calcDuration,
6473
compareIssueSeverity,

packages/utils/src/lib/diff.ts

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
export type Diff<T> = {
2+
before: T;
3+
after: T;
4+
};
5+
6+
export function matchArrayItemsByKey<T>({
7+
before,
8+
after,
9+
key,
10+
}: Diff<T[]> & { key: keyof T | ((item: T) => unknown) }) {
11+
const pairs: Diff<T>[] = [];
12+
const added: T[] = [];
13+
14+
const afterKeys = new Set<unknown>();
15+
const keyFn = typeof key === 'function' ? key : (item: T) => item[key];
16+
17+
// eslint-disable-next-line functional/no-loop-statements
18+
for (const afterItem of after) {
19+
const afterKey = keyFn(afterItem);
20+
afterKeys.add(afterKey);
21+
22+
const match = before.find(beforeItem => keyFn(beforeItem) === afterKey);
23+
if (match) {
24+
// eslint-disable-next-line functional/immutable-data
25+
pairs.push({ before: match, after: afterItem });
26+
} else {
27+
// eslint-disable-next-line functional/immutable-data
28+
added.push(afterItem);
29+
}
30+
}
31+
32+
const removed = before.filter(
33+
beforeItem => !afterKeys.has(keyFn(beforeItem)),
34+
);
35+
36+
return {
37+
pairs,
38+
added,
39+
removed,
40+
};
41+
}
42+
43+
export function comparePairs<T>(
44+
pairs: Diff<T>[],
45+
equalsFn: (pair: Diff<T>) => boolean,
46+
) {
47+
return pairs.reduce<{ changed: Diff<T>[]; unchanged: T[] }>(
48+
(acc, pair) => ({
49+
...acc,
50+
...(equalsFn(pair)
51+
? { unchanged: [...acc.unchanged, pair.after] }
52+
: { changed: [...acc.changed, pair] }),
53+
}),
54+
{
55+
changed: [],
56+
unchanged: [],
57+
},
58+
);
59+
}
+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { comparePairs, matchArrayItemsByKey } from './diff';
2+
3+
describe('matchArrayItemsByKey', () => {
4+
it('should pair up items by key string', () => {
5+
expect(
6+
matchArrayItemsByKey({
7+
before: [
8+
{ id: 1, name: 'Foo' },
9+
{ id: 2, name: 'Bar' },
10+
],
11+
after: [
12+
{ id: 2, name: 'Baz' },
13+
{ id: 3, name: 'Foo' },
14+
],
15+
key: 'id',
16+
}),
17+
).toEqual({
18+
pairs: [
19+
{ before: { id: 2, name: 'Bar' }, after: { id: 2, name: 'Baz' } },
20+
],
21+
added: [{ id: 3, name: 'Foo' }],
22+
removed: [{ id: 1, name: 'Foo' }],
23+
});
24+
});
25+
26+
it('should pair up items by key function', () => {
27+
expect(
28+
matchArrayItemsByKey({
29+
before: [
30+
{ id: 1, name: 'Foo' },
31+
{ id: 2, name: 'Bar' },
32+
],
33+
after: [
34+
{ id: 2, name: 'Baz' },
35+
{ id: 3, name: 'Foo' },
36+
],
37+
key: ({ id, name }) => `${id}-${name}`,
38+
}),
39+
).toEqual({
40+
pairs: [],
41+
added: [
42+
{ id: 2, name: 'Baz' },
43+
{ id: 3, name: 'Foo' },
44+
],
45+
removed: [
46+
{ id: 1, name: 'Foo' },
47+
{ id: 2, name: 'Bar' },
48+
],
49+
});
50+
});
51+
});
52+
53+
describe('comparePairs', () => {
54+
it('should split changed and unchanged according to equals function', () => {
55+
expect(
56+
comparePairs(
57+
[
58+
{ before: { id: 1, value: 100 }, after: { id: 1, value: 100 } },
59+
{ before: { id: 2, value: 200 }, after: { id: 2, value: 250 } },
60+
{ before: { id: 3, value: 300 }, after: { id: 3, value: 300 } },
61+
{ before: { id: 4, value: 400 }, after: { id: 4, value: 400 } },
62+
{ before: { id: 5, value: 500 }, after: { id: 5, value: 600 } },
63+
],
64+
({ before, after }) => before.value === after.value,
65+
),
66+
).toEqual({
67+
changed: [
68+
{ before: { id: 2, value: 200 }, after: { id: 2, value: 250 } },
69+
{ before: { id: 5, value: 500 }, after: { id: 5, value: 600 } },
70+
],
71+
unchanged: [
72+
{ id: 1, value: 100 },
73+
{ id: 3, value: 300 },
74+
{ id: 4, value: 400 },
75+
],
76+
});
77+
});
78+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Report } from '@code-pushup/models';
2+
3+
// generic type params infers ScoredGroup if ScoredReport provided
4+
export function listGroupsFromAllPlugins<T extends Report>(
5+
report: T,
6+
): {
7+
plugin: T['plugins'][0];
8+
group: NonNullable<T['plugins'][0]['groups']>[0];
9+
}[] {
10+
return report.plugins.flatMap(
11+
plugin => plugin.groups?.map(group => ({ plugin, group })) ?? [],
12+
);
13+
}
14+
15+
export function listAuditsFromAllPlugins<T extends Report>(
16+
report: T,
17+
): {
18+
plugin: T['plugins'][0];
19+
audit: T['plugins'][0]['audits'][0];
20+
}[] {
21+
return report.plugins.flatMap(plugin =>
22+
plugin.audits.map(audit => ({ plugin, audit })),
23+
);
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { Report } from '@code-pushup/models';
2+
import {
3+
listAuditsFromAllPlugins,
4+
listGroupsFromAllPlugins,
5+
} from './flatten-plugins';
6+
7+
describe('listGroupsFromAllPlugins', () => {
8+
it("should flatten plugins' groups", () => {
9+
expect(
10+
listGroupsFromAllPlugins({
11+
plugins: [
12+
{
13+
slug: 'eslint',
14+
groups: [
15+
{ slug: 'problems' },
16+
{ slug: 'suggestions' },
17+
{ slug: 'formatting' },
18+
],
19+
},
20+
{
21+
slug: 'lighthouse',
22+
groups: [
23+
{ slug: 'performance' },
24+
{ slug: 'accessibility' },
25+
{ slug: 'best-practices' },
26+
{ slug: 'seo' },
27+
],
28+
},
29+
],
30+
} as Report),
31+
).toEqual([
32+
{
33+
group: expect.objectContaining({ slug: 'problems' }),
34+
plugin: expect.objectContaining({ slug: 'eslint' }),
35+
},
36+
{
37+
group: expect.objectContaining({ slug: 'suggestions' }),
38+
plugin: expect.objectContaining({ slug: 'eslint' }),
39+
},
40+
{
41+
group: expect.objectContaining({ slug: 'formatting' }),
42+
plugin: expect.objectContaining({ slug: 'eslint' }),
43+
},
44+
{
45+
group: expect.objectContaining({ slug: 'performance' }),
46+
plugin: expect.objectContaining({ slug: 'lighthouse' }),
47+
},
48+
{
49+
group: expect.objectContaining({ slug: 'accessibility' }),
50+
plugin: expect.objectContaining({ slug: 'lighthouse' }),
51+
},
52+
{
53+
group: expect.objectContaining({ slug: 'best-practices' }),
54+
plugin: expect.objectContaining({ slug: 'lighthouse' }),
55+
},
56+
{
57+
group: expect.objectContaining({ slug: 'seo' }),
58+
plugin: expect.objectContaining({ slug: 'lighthouse' }),
59+
},
60+
]);
61+
});
62+
});
63+
64+
describe('listAuditsFromAllPlugins', () => {
65+
it("should flatten plugins' audits", () => {
66+
expect(
67+
listAuditsFromAllPlugins({
68+
plugins: [
69+
{
70+
slug: 'coverage',
71+
audits: [
72+
{ slug: 'function-coverage' },
73+
{ slug: 'branch-coverage' },
74+
{ slug: 'statement-coverage' },
75+
],
76+
},
77+
{
78+
slug: 'js-packages',
79+
audits: [{ slug: 'audit' }, { slug: 'outdated' }],
80+
},
81+
],
82+
} as Report),
83+
).toEqual([
84+
{
85+
audit: expect.objectContaining({ slug: 'function-coverage' }),
86+
plugin: expect.objectContaining({ slug: 'coverage' }),
87+
},
88+
{
89+
audit: expect.objectContaining({ slug: 'branch-coverage' }),
90+
plugin: expect.objectContaining({ slug: 'coverage' }),
91+
},
92+
{
93+
audit: expect.objectContaining({ slug: 'statement-coverage' }),
94+
plugin: expect.objectContaining({ slug: 'coverage' }),
95+
},
96+
{
97+
audit: expect.objectContaining({ slug: 'audit' }),
98+
plugin: expect.objectContaining({ slug: 'js-packages' }),
99+
},
100+
{
101+
audit: expect.objectContaining({ slug: 'outdated' }),
102+
plugin: expect.objectContaining({ slug: 'js-packages' }),
103+
},
104+
]);
105+
});
106+
});

0 commit comments

Comments
 (0)