-
Notifications
You must be signed in to change notification settings - Fork 9.4k
/
filters.js
316 lines (275 loc) · 11 KB
/
filters.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import log from 'lighthouse-logger';
import {Audit} from '../audits/audit.js';
/** @type {Record<keyof LH.BaseArtifacts, string>} */
const baseArtifactKeySource = {
fetchTime: '',
LighthouseRunWarnings: '',
BenchmarkIndex: '',
settings: '',
Timing: '',
URL: '',
PageLoadError: '',
HostFormFactor: '',
HostUserAgent: '',
HostProduct: '',
GatherContext: '',
};
const baseArtifactKeys = Object.keys(baseArtifactKeySource);
// Some audits are used by the report for additional information.
// Keep these audits unless they are *directly* skipped with `skipAudits`.
/** @type {string[]} */
const filterResistantAuditIds = [];
// Some artifacts are used by the report for additional information.
// Always run these artifacts even if audits do not request them.
// These are similar to base artifacts but they cannot be run in all 3 modes.
const filterResistantArtifactIds = ['Stacks', 'NetworkUserAgent', 'FullPageScreenshot'];
/**
* Returns the set of audit IDs used in the list of categories.
* If `onlyCategories` is not set, this function returns the list of all audit IDs across all
* categories.
*
* @param {LH.Config.ResolvedConfig['categories']} allCategories
* @param {string[] | undefined} onlyCategories
* @return {Set<string>}
*/
function getAuditIdsInCategories(allCategories, onlyCategories) {
if (!allCategories) return new Set();
onlyCategories = onlyCategories || Object.keys(allCategories);
const categories = onlyCategories.map(categoryId => allCategories[categoryId]);
const auditRefs = categories.flatMap(category => category?.auditRefs || []);
return new Set(auditRefs.map(auditRef => auditRef.id));
}
/**
* Filters an array of artifacts down to the set that's required by the specified audits.
*
* @param {LH.Config.ResolvedConfig['artifacts']} artifacts
* @param {LH.Config.ResolvedConfig['audits']} audits
* @return {LH.Config.ResolvedConfig['artifacts']}
*/
function filterArtifactsByAvailableAudits(artifacts, audits) {
if (!artifacts) return null;
if (!audits) return artifacts;
const artifactsById = new Map(artifacts.map(artifact => [artifact.id, artifact]));
/** @type {Set<string>} */
const artifactIdsToKeep = new Set([
...filterResistantArtifactIds,
...audits.flatMap(audit => audit.implementation.meta.requiredArtifacts),
]);
// Keep all artifacts in the dependency tree of required artifacts.
// Iterate through all kept artifacts, adding their dependencies along the way, until the set does not change.
let previousSize = 0;
while (previousSize !== artifactIdsToKeep.size) {
previousSize = artifactIdsToKeep.size;
for (const artifactId of artifactIdsToKeep) {
const artifact = artifactsById.get(artifactId);
// This shouldn't happen because the config has passed validation by this point.
if (!artifact) continue;
// If the artifact doesn't have any dependencies, we can move on.
if (!artifact.dependencies) continue;
// Add all of the artifact's dependencies to our set.
for (const dep of Object.values(artifact.dependencies)) {
artifactIdsToKeep.add(dep.id);
}
}
}
return artifacts.filter(artifact => artifactIdsToKeep.has(artifact.id));
}
/**
* Filters an array of artifacts down to the set that supports the specified gather mode.
*
* @param {LH.Config.ResolvedConfig['artifacts']} artifacts
* @param {LH.Gatherer.GatherMode} mode
* @return {LH.Config.ResolvedConfig['artifacts']}
*/
function filterArtifactsByGatherMode(artifacts, mode) {
if (!artifacts) return null;
return artifacts.filter(artifact => {
return artifact.gatherer.instance.meta.supportedModes.includes(mode);
});
}
/**
* Filters an array of audits down to the set that can be computed using only the specified artifacts.
*
* @param {LH.Config.ResolvedConfig['audits']} audits
* @param {Array<LH.Config.AnyArtifactDefn>} availableArtifacts
* @return {LH.Config.ResolvedConfig['audits']}
*/
function filterAuditsByAvailableArtifacts(audits, availableArtifacts) {
if (!audits) return null;
const availableArtifactIds = new Set(
availableArtifacts.map(artifact => artifact.id).concat(baseArtifactKeys)
);
return audits.filter(audit => {
const meta = audit.implementation.meta;
return meta.requiredArtifacts.every(id => availableArtifactIds.has(id));
});
}
/**
* Optional `supportedModes` property can explicitly exclude an audit even if all required artifacts are available.
*
* @param {LH.Config.ResolvedConfig['audits']} audits
* @param {LH.Gatherer.GatherMode} mode
* @return {LH.Config.ResolvedConfig['audits']}
*/
function filterAuditsByGatherMode(audits, mode) {
if (!audits) return null;
return audits.filter(audit => {
const meta = audit.implementation.meta;
return !meta.supportedModes || meta.supportedModes.includes(mode);
});
}
/**
* Optional `supportedModes` property can explicitly exclude a category even if some audits are available.
*
* @param {LH.Config.ResolvedConfig['categories']} categories
* @param {LH.Gatherer.GatherMode} mode
* @return {LH.Config.ResolvedConfig['categories']}
*/
function filterCategoriesByGatherMode(categories, mode) {
if (!categories) return null;
const categoriesToKeep = Object.entries(categories)
.filter(([_, category]) => {
return !category.supportedModes || category.supportedModes.includes(mode);
});
return Object.fromEntries(categoriesToKeep);
}
/**
* Filters a categories object and their auditRefs down to the specified category ids.
*
* @param {LH.Config.ResolvedConfig['categories']} categories
* @param {string[] | null | undefined} onlyCategories
* @return {LH.Config.ResolvedConfig['categories']}
*/
function filterCategoriesByExplicitFilters(categories, onlyCategories) {
if (!categories || !onlyCategories) return categories;
const categoriesToKeep = Object.entries(categories)
.filter(([categoryId]) => onlyCategories.includes(categoryId));
return Object.fromEntries(categoriesToKeep);
}
/**
* Logs a warning if any specified onlyCategory is not a known category that can
* be included.
*
* @param {LH.Config.ResolvedConfig['categories']} allCategories
* @param {string[] | null} onlyCategories
* @return {void}
*/
function warnOnUnknownOnlyCategories(allCategories, onlyCategories) {
if (!onlyCategories) return;
for (const onlyCategoryId of onlyCategories) {
if (!allCategories?.[onlyCategoryId]) {
log.warn('config', `unrecognized category in 'onlyCategories': ${onlyCategoryId}`);
}
}
}
/**
* Filters a categories object and their auditRefs down to the set that can be computed using
* only the specified audits.
*
* @param {LH.Config.ResolvedConfig['categories']} categories
* @param {Array<LH.Config.AuditDefn>} availableAudits
* @return {LH.Config.ResolvedConfig['categories']}
*/
function filterCategoriesByAvailableAudits(categories, availableAudits) {
if (!categories) return categories;
const availableAuditIdToMeta = new Map(
availableAudits.map(audit => [audit.implementation.meta.id, audit.implementation.meta])
);
const categoryEntries = Object.entries(categories)
.map(([categoryId, category]) => {
const filteredCategory = {
...category,
auditRefs: category.auditRefs.filter(ref => availableAuditIdToMeta.has(ref.id)),
};
const didFilter = filteredCategory.auditRefs.length < category.auditRefs.length;
const hasOnlyManualAudits = filteredCategory.auditRefs.every(ref => {
const meta = availableAuditIdToMeta.get(ref.id);
if (!meta) return false;
return meta.scoreDisplayMode === Audit.SCORING_MODES.MANUAL;
});
// If we filtered out audits and the only ones left are manual, remove them too.
if (didFilter && hasOnlyManualAudits) filteredCategory.auditRefs = [];
return [categoryId, filteredCategory];
})
.filter(entry => typeof entry[1] === 'object' && entry[1].auditRefs.length);
return Object.fromEntries(categoryEntries);
}
/**
* Filters a config's artifacts, audits, and categories down to the set that supports the specified gather mode.
*
* @param {LH.Config.ResolvedConfig} resolvedConfig
* @param {LH.Gatherer.GatherMode} mode
* @return {LH.Config.ResolvedConfig}
*/
function filterConfigByGatherMode(resolvedConfig, mode) {
const artifacts = filterArtifactsByGatherMode(resolvedConfig.artifacts, mode);
const supportedAudits = filterAuditsByGatherMode(resolvedConfig.audits, mode);
const audits = filterAuditsByAvailableArtifacts(supportedAudits, artifacts || []);
const supportedCategories = filterCategoriesByGatherMode(resolvedConfig.categories, mode);
const categories = filterCategoriesByAvailableAudits(supportedCategories, audits || []);
return {
...resolvedConfig,
artifacts,
audits,
categories,
};
}
/**
* Filters a config's artifacts, audits, and categories down to the requested set.
* Skip audits overrides inclusion via `onlyAudits`/`onlyCategories`.
*
* @param {LH.Config.ResolvedConfig} resolvedConfig
* @param {Pick<LH.Config.Settings, 'onlyAudits'|'onlyCategories'|'skipAudits'>} filters
* @return {LH.Config.ResolvedConfig}
*/
function filterConfigByExplicitFilters(resolvedConfig, filters) {
const {onlyAudits, onlyCategories, skipAudits} = filters;
warnOnUnknownOnlyCategories(resolvedConfig.categories, onlyCategories);
let baseAuditIds = getAuditIdsInCategories(resolvedConfig.categories, undefined);
if (onlyCategories) {
baseAuditIds = getAuditIdsInCategories(resolvedConfig.categories, onlyCategories);
} else if (onlyAudits) {
baseAuditIds = new Set();
} else if (!resolvedConfig.categories || !Object.keys(resolvedConfig.categories).length) {
baseAuditIds = new Set(resolvedConfig.audits?.map(audit => audit.implementation.meta.id));
}
const auditIdsToKeep = new Set(
[
...baseAuditIds, // Start with our base audits.
...(onlyAudits || []), // Additionally include the opt-in audits from `onlyAudits`.
...filterResistantAuditIds, // Always include any filter-resistant audits.
].filter(auditId => !skipAudits || !skipAudits.includes(auditId))
);
const audits = auditIdsToKeep.size && resolvedConfig.audits ?
resolvedConfig.audits.filter(audit => auditIdsToKeep.has(audit.implementation.meta.id)) :
resolvedConfig.audits;
const availableCategories =
filterCategoriesByAvailableAudits(resolvedConfig.categories, audits || []);
const categories = filterCategoriesByExplicitFilters(availableCategories, onlyCategories);
let artifacts = filterArtifactsByAvailableAudits(resolvedConfig.artifacts, audits);
if (artifacts && resolvedConfig.settings.disableFullPageScreenshot) {
artifacts = artifacts.filter(({id}) => id !== 'FullPageScreenshot');
}
return {
...resolvedConfig,
artifacts,
audits,
categories,
};
}
export {
filterConfigByGatherMode,
filterConfigByExplicitFilters,
filterArtifactsByGatherMode,
filterArtifactsByAvailableAudits,
filterAuditsByAvailableArtifacts,
filterAuditsByGatherMode,
filterCategoriesByAvailableAudits,
filterCategoriesByExplicitFilters,
filterCategoriesByGatherMode,
};