lighthouse 9.5.0-dev.20221114 → 9.5.0-dev.20221116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/cli-flags.js +1 -1
- package/cli/printer.js +1 -1
- package/cli/test/smokehouse/frontends/smokehouse-bin.js +1 -1
- package/core/audits/byte-efficiency/render-blocking-resources.js +1 -1
- package/core/audits/byte-efficiency/uses-long-cache-ttl.js +87 -88
- package/core/audits/critical-request-chains.js +41 -42
- package/core/audits/is-on-https.js +38 -39
- package/core/audits/metrics/first-contentful-paint-3g.js +1 -1
- package/core/audits/performance-budget.js +1 -1
- package/core/audits/seo/is-crawlable.js +51 -54
- package/core/audits/timing-budget.js +1 -1
- package/core/audits/user-timings.js +42 -43
- package/core/computed/computed-artifact.js +3 -3
- package/core/computed/load-simulator.js +1 -1
- package/core/computed/metrics/responsiveness.js +1 -1
- package/core/computed/metrics/timing-summary.js +2 -2
- package/core/computed/network-analysis.js +1 -1
- package/core/computed/resource-summary.js +2 -2
- package/core/config/budget.js +2 -2
- package/core/config/config-helpers.js +2 -2
- package/core/gather/gatherers/full-page-screenshot.js +3 -1
- package/core/index.js +81 -2
- package/core/lib/lantern-trace-saver.js +1 -1
- package/core/lib/network-request.js +1 -1
- package/package.json +1 -1
- package/report/generator/report-generator.js +19 -5
- package/report/test/generator/report-generator-test.js +18 -0
- package/types/artifacts.d.ts +5 -3
- package/types/audit.d.ts +2 -1
- package/types/externs.d.ts +0 -95
- package/types/global-lh.d.ts +3 -0
- package/types/node.d.ts +32 -0
- package/types/utility-types.d.ts +72 -0
- package/core/api.js +0 -80
package/cli/cli-flags.js
CHANGED
|
@@ -351,7 +351,7 @@ function getFlags(manualArgv, options = {}) {
|
|
|
351
351
|
// Augmenting yargs type with auto-camelCasing breaks in tsc@4.1.2 and @types/yargs@15.0.11,
|
|
352
352
|
// so for now cast to add yarg's camelCase properties to type.
|
|
353
353
|
const argv = /** @type {Awaited<typeof parser.argv>} */ (parser.argv);
|
|
354
|
-
const cliFlags = /** @type {typeof argv & CamelCasify<typeof argv>} */ (argv);
|
|
354
|
+
const cliFlags = /** @type {typeof argv & LH.Util.CamelCasify<typeof argv>} */ (argv);
|
|
355
355
|
|
|
356
356
|
// yargs will return `undefined` for options that have a `coerce` function but
|
|
357
357
|
// are not actually present in the user input. Instead of passing properties
|
package/cli/printer.js
CHANGED
|
@@ -13,7 +13,7 @@ import log from 'lighthouse-logger';
|
|
|
13
13
|
* 'json': JSON formatted results
|
|
14
14
|
* 'html': An HTML report
|
|
15
15
|
* 'csv': CSV formatted results
|
|
16
|
-
* @type {SelfMap<LH.OutputMode>}
|
|
16
|
+
* @type {LH.Util.SelfMap<LH.OutputMode>}
|
|
17
17
|
*/
|
|
18
18
|
const OutputMode = {
|
|
19
19
|
json: 'json',
|
|
@@ -174,7 +174,7 @@ async function begin() {
|
|
|
174
174
|
// Augmenting yargs type with auto-camelCasing breaks in tsc@4.1.2 and @types/yargs@15.0.11,
|
|
175
175
|
// so for now cast to add yarg's camelCase properties to type.
|
|
176
176
|
const argv =
|
|
177
|
-
/** @type {Awaited<typeof rawArgv> & CamelCasify<Awaited<typeof rawArgv>>} */ (rawArgv);
|
|
177
|
+
/** @type {Awaited<typeof rawArgv> & LH.Util.CamelCasify<Awaited<typeof rawArgv>>} */ (rawArgv);
|
|
178
178
|
|
|
179
179
|
const jobs = Number.isFinite(argv.jobs) ? argv.jobs : undefined;
|
|
180
180
|
const retries = Number.isFinite(argv.retries) ? argv.retries : undefined;
|
|
@@ -138,7 +138,7 @@ class RenderBlockingResources extends Audit {
|
|
|
138
138
|
const simulator = await LoadSimulator.request(simulatorData, context);
|
|
139
139
|
const wastedCssBytes = await RenderBlockingResources.computeWastedCSSBytes(artifacts, context);
|
|
140
140
|
|
|
141
|
-
/** @type {
|
|
141
|
+
/** @type {LH.Audit.Context['settings']} */
|
|
142
142
|
const metricSettings = {
|
|
143
143
|
...context.settings,
|
|
144
144
|
throttlingMethod: 'simulate',
|
|
@@ -194,103 +194,102 @@ class CacheHeaders extends Audit {
|
|
|
194
194
|
* @param {LH.Audit.Context} context
|
|
195
195
|
* @return {Promise<LH.Audit.Product>}
|
|
196
196
|
*/
|
|
197
|
-
static audit(artifacts, context) {
|
|
197
|
+
static async audit(artifacts, context) {
|
|
198
198
|
const devtoolsLogs = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const cacheControl = parseCacheControl(headers.get('cache-control'));
|
|
219
|
-
if (this.shouldSkipRecord(headers, cacheControl)) {
|
|
220
|
-
continue;
|
|
199
|
+
const records = await NetworkRecords.request(devtoolsLogs, context);
|
|
200
|
+
const results = [];
|
|
201
|
+
let totalWastedBytes = 0;
|
|
202
|
+
|
|
203
|
+
for (const record of records) {
|
|
204
|
+
if (!CacheHeaders.isCacheableAsset(record)) continue;
|
|
205
|
+
|
|
206
|
+
/** @type {Map<string, string>} */
|
|
207
|
+
const headers = new Map();
|
|
208
|
+
for (const header of record.responseHeaders || []) {
|
|
209
|
+
if (headers.has(header.name.toLowerCase())) {
|
|
210
|
+
const previousHeaderValue = headers.get(header.name.toLowerCase());
|
|
211
|
+
headers.set(header.name.toLowerCase(),
|
|
212
|
+
`${previousHeaderValue}, ${header.value}`);
|
|
213
|
+
} else {
|
|
214
|
+
headers.set(header.name.toLowerCase(), header.value);
|
|
221
215
|
}
|
|
216
|
+
}
|
|
222
217
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
(!Number.isFinite(cacheLifetimeInSeconds) || cacheLifetimeInSeconds <= 0)) {
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
cacheLifetimeInSeconds = cacheLifetimeInSeconds || 0;
|
|
231
|
-
|
|
232
|
-
// Ignore assets whose cache lifetime is already high enough
|
|
233
|
-
const cacheHitProbability = CacheHeaders.getCacheHitProbability(cacheLifetimeInSeconds);
|
|
234
|
-
if (cacheHitProbability > IGNORE_THRESHOLD_IN_PERCENT) continue;
|
|
235
|
-
|
|
236
|
-
const url = UrlUtils.elideDataURI(record.url);
|
|
237
|
-
const totalBytes = record.transferSize || 0;
|
|
238
|
-
const wastedBytes = (1 - cacheHitProbability) * totalBytes;
|
|
239
|
-
|
|
240
|
-
totalWastedBytes += wastedBytes;
|
|
241
|
-
|
|
242
|
-
// Include cacheControl info (if it exists) per url as a diagnostic.
|
|
243
|
-
/** @type {LH.Audit.Details.DebugData|undefined} */
|
|
244
|
-
let debugData;
|
|
245
|
-
if (cacheControl) {
|
|
246
|
-
debugData = {
|
|
247
|
-
type: 'debugdata',
|
|
248
|
-
...cacheControl,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
218
|
+
const cacheControl = parseCacheControl(headers.get('cache-control'));
|
|
219
|
+
if (this.shouldSkipRecord(headers, cacheControl)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
251
222
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
223
|
+
// Ignore if cacheLifetimeInSeconds is a nonpositive number.
|
|
224
|
+
let cacheLifetimeInSeconds = CacheHeaders.computeCacheLifetimeInSeconds(
|
|
225
|
+
headers, cacheControl);
|
|
226
|
+
if (cacheLifetimeInSeconds !== null &&
|
|
227
|
+
(!Number.isFinite(cacheLifetimeInSeconds) || cacheLifetimeInSeconds <= 0)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
cacheLifetimeInSeconds = cacheLifetimeInSeconds || 0;
|
|
231
|
+
|
|
232
|
+
// Ignore assets whose cache lifetime is already high enough
|
|
233
|
+
const cacheHitProbability = CacheHeaders.getCacheHitProbability(cacheLifetimeInSeconds);
|
|
234
|
+
if (cacheHitProbability > IGNORE_THRESHOLD_IN_PERCENT) continue;
|
|
235
|
+
|
|
236
|
+
const url = UrlUtils.elideDataURI(record.url);
|
|
237
|
+
const totalBytes = record.transferSize || 0;
|
|
238
|
+
const wastedBytes = (1 - cacheHitProbability) * totalBytes;
|
|
239
|
+
|
|
240
|
+
totalWastedBytes += wastedBytes;
|
|
241
|
+
|
|
242
|
+
// Include cacheControl info (if it exists) per url as a diagnostic.
|
|
243
|
+
/** @type {LH.Audit.Details.DebugData|undefined} */
|
|
244
|
+
let debugData;
|
|
245
|
+
if (cacheControl) {
|
|
246
|
+
debugData = {
|
|
247
|
+
type: 'debugdata',
|
|
248
|
+
...cacheControl,
|
|
249
|
+
};
|
|
260
250
|
}
|
|
261
251
|
|
|
262
|
-
results.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
252
|
+
results.push({
|
|
253
|
+
url,
|
|
254
|
+
debugData,
|
|
255
|
+
cacheLifetimeMs: cacheLifetimeInSeconds * 1000,
|
|
256
|
+
cacheHitProbability,
|
|
257
|
+
totalBytes,
|
|
258
|
+
wastedBytes,
|
|
266
259
|
});
|
|
260
|
+
}
|
|
267
261
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
/** @type {LH.Audit.Details.Table['headings']} */
|
|
274
|
-
const headings = [
|
|
275
|
-
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
|
|
276
|
-
// TODO(i18n): pre-compute localized duration
|
|
277
|
-
{key: 'cacheLifetimeMs', valueType: 'ms', label: str_(i18n.UIStrings.columnCacheTTL),
|
|
278
|
-
displayUnit: 'duration'},
|
|
279
|
-
{key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnTransferSize),
|
|
280
|
-
displayUnit: 'kb', granularity: 1},
|
|
281
|
-
];
|
|
282
|
-
|
|
283
|
-
const summary = {wastedBytes: totalWastedBytes};
|
|
284
|
-
const details = Audit.makeTableDetails(headings, results, summary);
|
|
285
|
-
|
|
286
|
-
return {
|
|
287
|
-
score,
|
|
288
|
-
numericValue: totalWastedBytes,
|
|
289
|
-
numericUnit: 'byte',
|
|
290
|
-
displayValue: str_(UIStrings.displayValue, {itemCount: results.length}),
|
|
291
|
-
details,
|
|
292
|
-
};
|
|
262
|
+
results.sort((a, b) => {
|
|
263
|
+
return a.cacheLifetimeMs - b.cacheLifetimeMs ||
|
|
264
|
+
b.totalBytes - a.totalBytes ||
|
|
265
|
+
a.url.localeCompare(b.url);
|
|
293
266
|
});
|
|
267
|
+
|
|
268
|
+
const score = Audit.computeLogNormalScore(
|
|
269
|
+
{p10: context.options.p10, median: context.options.median},
|
|
270
|
+
totalWastedBytes
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
274
|
+
const headings = [
|
|
275
|
+
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
|
|
276
|
+
// TODO(i18n): pre-compute localized duration
|
|
277
|
+
{key: 'cacheLifetimeMs', valueType: 'ms', label: str_(i18n.UIStrings.columnCacheTTL),
|
|
278
|
+
displayUnit: 'duration'},
|
|
279
|
+
{key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnTransferSize),
|
|
280
|
+
displayUnit: 'kb', granularity: 1},
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
const summary = {wastedBytes: totalWastedBytes};
|
|
284
|
+
const details = Audit.makeTableDetails(headings, results, summary);
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
score,
|
|
288
|
+
numericValue: totalWastedBytes,
|
|
289
|
+
numericUnit: 'byte',
|
|
290
|
+
displayValue: str_(UIStrings.displayValue, {itemCount: results.length}),
|
|
291
|
+
details,
|
|
292
|
+
};
|
|
294
293
|
}
|
|
295
294
|
}
|
|
296
295
|
|
|
@@ -166,52 +166,51 @@ class CriticalRequestChains extends Audit {
|
|
|
166
166
|
* @param {LH.Audit.Context} context
|
|
167
167
|
* @return {Promise<LH.Audit.Product>}
|
|
168
168
|
*/
|
|
169
|
-
static audit(artifacts, context) {
|
|
169
|
+
static async audit(artifacts, context) {
|
|
170
170
|
const trace = artifacts.traces[Audit.DEFAULT_PASS];
|
|
171
171
|
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
172
172
|
const URL = artifacts.URL;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
childIds.forEach(id => {
|
|
183
|
-
const child = node[id];
|
|
184
|
-
if (child.children) {
|
|
185
|
-
walk(child.children, depth + 1);
|
|
186
|
-
} else {
|
|
187
|
-
// if the node doesn't have a children field, then it is a leaf, so +1
|
|
188
|
-
chainCount++;
|
|
189
|
-
}
|
|
190
|
-
}, '');
|
|
191
|
-
}
|
|
192
|
-
// Convert
|
|
193
|
-
const flattenedChains = CriticalRequestChains.flattenRequests(chains);
|
|
194
|
-
|
|
195
|
-
// Account for initial navigation
|
|
196
|
-
const initialNavKey = Object.keys(flattenedChains)[0];
|
|
197
|
-
const initialNavChildren = initialNavKey && flattenedChains[initialNavKey].children;
|
|
198
|
-
if (initialNavChildren && Object.keys(initialNavChildren).length > 0) {
|
|
199
|
-
walk(initialNavChildren, 0);
|
|
200
|
-
}
|
|
173
|
+
const chains = await ComputedChains.request({devtoolsLog, trace, URL}, context);
|
|
174
|
+
let chainCount = 0;
|
|
175
|
+
/**
|
|
176
|
+
* @param {LH.Audit.Details.SimpleCriticalRequestNode} node
|
|
177
|
+
* @param {number} depth
|
|
178
|
+
*/
|
|
179
|
+
function walk(node, depth) {
|
|
180
|
+
const childIds = Object.keys(node);
|
|
201
181
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
182
|
+
childIds.forEach(id => {
|
|
183
|
+
const child = node[id];
|
|
184
|
+
if (child.children) {
|
|
185
|
+
walk(child.children, depth + 1);
|
|
186
|
+
} else {
|
|
187
|
+
// if the node doesn't have a children field, then it is a leaf, so +1
|
|
188
|
+
chainCount++;
|
|
189
|
+
}
|
|
190
|
+
}, '');
|
|
191
|
+
}
|
|
192
|
+
// Convert
|
|
193
|
+
const flattenedChains = CriticalRequestChains.flattenRequests(chains);
|
|
194
|
+
|
|
195
|
+
// Account for initial navigation
|
|
196
|
+
const initialNavKey = Object.keys(flattenedChains)[0];
|
|
197
|
+
const initialNavChildren = initialNavKey && flattenedChains[initialNavKey].children;
|
|
198
|
+
if (initialNavChildren && Object.keys(initialNavChildren).length > 0) {
|
|
199
|
+
walk(initialNavChildren, 0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const longestChain = CriticalRequestChains._getLongestChain(flattenedChains);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
score: Number(chainCount === 0),
|
|
206
|
+
notApplicable: chainCount === 0,
|
|
207
|
+
displayValue: chainCount ? str_(UIStrings.displayValue, {itemCount: chainCount}) : '',
|
|
208
|
+
details: {
|
|
209
|
+
type: 'criticalrequestchain',
|
|
210
|
+
chains: flattenedChains,
|
|
211
|
+
longestChain,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
215
214
|
}
|
|
216
215
|
}
|
|
217
216
|
|
|
@@ -70,53 +70,52 @@ class HTTPS extends Audit {
|
|
|
70
70
|
* @param {LH.Audit.Context} context
|
|
71
71
|
* @return {Promise<LH.Audit.Product>}
|
|
72
72
|
*/
|
|
73
|
-
static audit(artifacts, context) {
|
|
73
|
+
static async audit(artifacts, context) {
|
|
74
74
|
const devtoolsLogs = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
const networkRecords = await NetworkRecords.request(devtoolsLogs, context);
|
|
76
|
+
const insecureURLs = networkRecords
|
|
77
|
+
.filter(record => !NetworkRequest.isSecureRequest(record))
|
|
78
|
+
.map(record => UrlUtils.elideDataURI(record.url));
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
/** @type {Array<{url: string, resolution?: LH.IcuMessage|string}>} */
|
|
81
|
+
const items = Array.from(new Set(insecureURLs)).map(url => ({url, resolution: undefined}));
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
84
|
+
const headings = [
|
|
85
|
+
{key: 'url', valueType: 'url', label: str_(UIStrings.columnInsecureURL)},
|
|
86
|
+
{key: 'resolution', valueType: 'text', label: str_(UIStrings.columnResolution)},
|
|
87
|
+
];
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
item.resolution = resolutionToString[details.resolutionStatus] ?
|
|
96
|
-
str_(resolutionToString[details.resolutionStatus]) :
|
|
97
|
-
details.resolutionStatus;
|
|
89
|
+
for (const details of artifacts.InspectorIssues.mixedContentIssue) {
|
|
90
|
+
let item = items.find(item => item.url === details.insecureURL);
|
|
91
|
+
if (!item) {
|
|
92
|
+
item = {url: details.insecureURL};
|
|
93
|
+
items.push(item);
|
|
98
94
|
}
|
|
95
|
+
item.resolution = resolutionToString[details.resolutionStatus] ?
|
|
96
|
+
str_(resolutionToString[details.resolutionStatus]) :
|
|
97
|
+
details.resolutionStatus;
|
|
98
|
+
}
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
// If a resolution wasn't assigned from an InspectorIssue, then the item
|
|
101
|
+
// is not blocked by the browser but we've determined it is insecure anyhow.
|
|
102
|
+
// For example, if the URL is localhost, all `http` requests are valid
|
|
103
|
+
// (localhost is a secure context), but we still identify `http` requests
|
|
104
|
+
// as an "Allowed" insecure URL.
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
if (!item.resolution) item.resolution = str_(UIStrings.allowed);
|
|
107
|
+
}
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
let displayValue;
|
|
110
|
+
if (items.length > 0) {
|
|
111
|
+
displayValue = str_(UIStrings.displayValue, {itemCount: items.length});
|
|
112
|
+
}
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
});
|
|
114
|
+
return {
|
|
115
|
+
score: Number(items.length === 0),
|
|
116
|
+
displayValue,
|
|
117
|
+
details: Audit.makeTableDetails(headings, items),
|
|
118
|
+
};
|
|
120
119
|
}
|
|
121
120
|
}
|
|
122
121
|
|
|
@@ -49,7 +49,7 @@ class FirstContentfulPaint3G extends Audit {
|
|
|
49
49
|
const gatherContext = artifacts.GatherContext;
|
|
50
50
|
const trace = artifacts.traces[Audit.DEFAULT_PASS];
|
|
51
51
|
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
52
|
-
/** @type {
|
|
52
|
+
/** @type {LH.Audit.Context['settings']} */
|
|
53
53
|
const settings = {...context.settings, throttlingMethod: 'simulate', throttling: regular3G};
|
|
54
54
|
const metricComputationData = {trace, devtoolsLog, gatherContext, settings, URL: artifacts.URL};
|
|
55
55
|
const metricResult = await ComputedFcp.request(metricComputationData, context);
|
|
@@ -90,63 +90,60 @@ class IsCrawlable extends Audit {
|
|
|
90
90
|
* @param {LH.Audit.Context} context
|
|
91
91
|
* @return {Promise<LH.Audit.Product>}
|
|
92
92
|
*/
|
|
93
|
-
static audit(artifacts, context) {
|
|
93
|
+
static async audit(artifacts, context) {
|
|
94
94
|
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
95
95
|
const metaRobots = artifacts.MetaElements.find(meta => meta.name === 'robots');
|
|
96
|
+
const mainResource = await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
|
|
97
|
+
/** @type {LH.Audit.Details.Table['items']} */
|
|
98
|
+
const blockingDirectives = [];
|
|
99
|
+
|
|
100
|
+
if (metaRobots) {
|
|
101
|
+
const metaRobotsContent = metaRobots.content || '';
|
|
102
|
+
const isBlocking = hasBlockingDirective(metaRobotsContent);
|
|
103
|
+
|
|
104
|
+
if (isBlocking) {
|
|
105
|
+
blockingDirectives.push({
|
|
106
|
+
source: {
|
|
107
|
+
...Audit.makeNodeItem(metaRobots.node),
|
|
108
|
+
snippet: `<meta name="robots" content="${metaRobotsContent}" />`,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
mainResource.responseHeaders && mainResource.responseHeaders
|
|
115
|
+
.filter(h => h.name.toLowerCase() === ROBOTS_HEADER && !hasUserAgent(h.value) &&
|
|
116
|
+
hasBlockingDirective(h.value))
|
|
117
|
+
.forEach(h => blockingDirectives.push({source: `${h.name}: ${h.value}`}));
|
|
118
|
+
|
|
119
|
+
if (artifacts.RobotsTxt.content) {
|
|
120
|
+
const robotsFileUrl = new URL('/robots.txt', mainResource.url);
|
|
121
|
+
const robotsTxt = robotsParser(robotsFileUrl.href, artifacts.RobotsTxt.content);
|
|
122
|
+
|
|
123
|
+
if (!robotsTxt.isAllowed(mainResource.url)) {
|
|
124
|
+
const line = robotsTxt.getMatchingLineNumber(mainResource.url) || 1;
|
|
125
|
+
blockingDirectives.push({
|
|
126
|
+
source: {
|
|
127
|
+
type: /** @type {const} */ ('source-location'),
|
|
128
|
+
url: robotsFileUrl.href,
|
|
129
|
+
urlProvider: /** @type {const} */ ('network'),
|
|
130
|
+
line: line - 1,
|
|
131
|
+
column: 0,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
138
|
+
const headings = [
|
|
139
|
+
{key: 'source', valueType: 'code', label: 'Blocking Directive Source'},
|
|
140
|
+
];
|
|
141
|
+
const details = Audit.makeTableDetails(headings, blockingDirectives);
|
|
96
142
|
|
|
97
|
-
return
|
|
98
|
-
.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (metaRobots) {
|
|
103
|
-
const metaRobotsContent = metaRobots.content || '';
|
|
104
|
-
const isBlocking = hasBlockingDirective(metaRobotsContent);
|
|
105
|
-
|
|
106
|
-
if (isBlocking) {
|
|
107
|
-
blockingDirectives.push({
|
|
108
|
-
source: {
|
|
109
|
-
...Audit.makeNodeItem(metaRobots.node),
|
|
110
|
-
snippet: `<meta name="robots" content="${metaRobotsContent}" />`,
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
mainResource.responseHeaders && mainResource.responseHeaders
|
|
117
|
-
.filter(h => h.name.toLowerCase() === ROBOTS_HEADER && !hasUserAgent(h.value) &&
|
|
118
|
-
hasBlockingDirective(h.value))
|
|
119
|
-
.forEach(h => blockingDirectives.push({source: `${h.name}: ${h.value}`}));
|
|
120
|
-
|
|
121
|
-
if (artifacts.RobotsTxt.content) {
|
|
122
|
-
const robotsFileUrl = new URL('/robots.txt', mainResource.url);
|
|
123
|
-
const robotsTxt = robotsParser(robotsFileUrl.href, artifacts.RobotsTxt.content);
|
|
124
|
-
|
|
125
|
-
if (!robotsTxt.isAllowed(mainResource.url)) {
|
|
126
|
-
const line = robotsTxt.getMatchingLineNumber(mainResource.url) || 1;
|
|
127
|
-
blockingDirectives.push({
|
|
128
|
-
source: {
|
|
129
|
-
type: /** @type {const} */ ('source-location'),
|
|
130
|
-
url: robotsFileUrl.href,
|
|
131
|
-
urlProvider: /** @type {const} */ ('network'),
|
|
132
|
-
line: line - 1,
|
|
133
|
-
column: 0,
|
|
134
|
-
},
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** @type {LH.Audit.Details.Table['headings']} */
|
|
140
|
-
const headings = [
|
|
141
|
-
{key: 'source', valueType: 'code', label: 'Blocking Directive Source'},
|
|
142
|
-
];
|
|
143
|
-
const details = Audit.makeTableDetails(headings, blockingDirectives);
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
score: Number(blockingDirectives.length === 0),
|
|
147
|
-
details,
|
|
148
|
-
};
|
|
149
|
-
});
|
|
143
|
+
return {
|
|
144
|
+
score: Number(blockingDirectives.length === 0),
|
|
145
|
+
details,
|
|
146
|
+
};
|
|
150
147
|
}
|
|
151
148
|
}
|
|
152
149
|
|
|
@@ -64,54 +64,53 @@ class UserTimings extends Audit {
|
|
|
64
64
|
* @param {LH.Audit.Context} context
|
|
65
65
|
* @return {Promise<LH.Audit.Product>}
|
|
66
66
|
*/
|
|
67
|
-
static audit(artifacts, context) {
|
|
67
|
+
static async audit(artifacts, context) {
|
|
68
68
|
const trace = artifacts.traces[Audit.DEFAULT_PASS];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
69
|
+
const computedUserTimings = await ComputedUserTimings.request(trace, context);
|
|
70
|
+
const userTimings = computedUserTimings.filter(UserTimings.excludeEvent);
|
|
71
|
+
const tableRows = userTimings.map(item => {
|
|
72
|
+
return {
|
|
73
|
+
name: item.name,
|
|
74
|
+
startTime: item.startTime,
|
|
75
|
+
duration: item.isMark ? undefined : item.duration,
|
|
76
|
+
timingType: item.isMark ? 'Mark' : 'Measure',
|
|
77
|
+
};
|
|
78
|
+
}).sort((itemA, itemB) => {
|
|
79
|
+
if (itemA.timingType === itemB.timingType) {
|
|
80
|
+
// If both items are the same type, sort in ascending order by time
|
|
81
|
+
return itemA.startTime - itemB.startTime;
|
|
82
|
+
} else if (itemA.timingType === 'Measure') {
|
|
83
|
+
// Put measures before marks
|
|
84
|
+
return -1;
|
|
85
|
+
} else {
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
90
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
91
|
+
const headings = [
|
|
92
|
+
{key: 'name', valueType: 'text', label: str_(i18n.UIStrings.columnName)},
|
|
93
|
+
{key: 'timingType', valueType: 'text', label: str_(UIStrings.columnType)},
|
|
94
|
+
{key: 'startTime', valueType: 'ms', granularity: 0.01,
|
|
95
|
+
label: str_(i18n.UIStrings.columnStartTime)},
|
|
96
|
+
{key: 'duration', valueType: 'ms', granularity: 0.01,
|
|
97
|
+
label: str_(i18n.UIStrings.columnDuration)},
|
|
98
|
+
];
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
const details = Audit.makeTableDetails(headings, tableRows);
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
let displayValue;
|
|
103
|
+
if (userTimings.length) {
|
|
104
|
+
displayValue = str_(UIStrings.displayValue, {itemCount: userTimings.length});
|
|
105
|
+
}
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
});
|
|
107
|
+
return {
|
|
108
|
+
// mark the audit as notApplicable if there were no user timings
|
|
109
|
+
score: Number(userTimings.length === 0),
|
|
110
|
+
notApplicable: userTimings.length === 0,
|
|
111
|
+
displayValue,
|
|
112
|
+
details,
|
|
113
|
+
};
|
|
115
114
|
}
|
|
116
115
|
}
|
|
117
116
|
|