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.
Files changed (34) hide show
  1. package/cli/cli-flags.js +1 -1
  2. package/cli/printer.js +1 -1
  3. package/cli/test/smokehouse/frontends/smokehouse-bin.js +1 -1
  4. package/core/audits/byte-efficiency/render-blocking-resources.js +1 -1
  5. package/core/audits/byte-efficiency/uses-long-cache-ttl.js +87 -88
  6. package/core/audits/critical-request-chains.js +41 -42
  7. package/core/audits/is-on-https.js +38 -39
  8. package/core/audits/metrics/first-contentful-paint-3g.js +1 -1
  9. package/core/audits/performance-budget.js +1 -1
  10. package/core/audits/seo/is-crawlable.js +51 -54
  11. package/core/audits/timing-budget.js +1 -1
  12. package/core/audits/user-timings.js +42 -43
  13. package/core/computed/computed-artifact.js +3 -3
  14. package/core/computed/load-simulator.js +1 -1
  15. package/core/computed/metrics/responsiveness.js +1 -1
  16. package/core/computed/metrics/timing-summary.js +2 -2
  17. package/core/computed/network-analysis.js +1 -1
  18. package/core/computed/resource-summary.js +2 -2
  19. package/core/config/budget.js +2 -2
  20. package/core/config/config-helpers.js +2 -2
  21. package/core/gather/gatherers/full-page-screenshot.js +3 -1
  22. package/core/index.js +81 -2
  23. package/core/lib/lantern-trace-saver.js +1 -1
  24. package/core/lib/network-request.js +1 -1
  25. package/package.json +1 -1
  26. package/report/generator/report-generator.js +19 -5
  27. package/report/test/generator/report-generator-test.js +18 -0
  28. package/types/artifacts.d.ts +5 -3
  29. package/types/audit.d.ts +2 -1
  30. package/types/externs.d.ts +0 -95
  31. package/types/global-lh.d.ts +3 -0
  32. package/types/node.d.ts +32 -0
  33. package/types/utility-types.d.ts +72 -0
  34. 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 {Immutable<LH.Config.Settings>} */
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
- return NetworkRecords.request(devtoolsLogs, context).then(records => {
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);
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
- // 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
- };
250
- }
218
+ const cacheControl = parseCacheControl(headers.get('cache-control'));
219
+ if (this.shouldSkipRecord(headers, cacheControl)) {
220
+ continue;
221
+ }
251
222
 
252
- results.push({
253
- url,
254
- debugData,
255
- cacheLifetimeMs: cacheLifetimeInSeconds * 1000,
256
- cacheHitProbability,
257
- totalBytes,
258
- wastedBytes,
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.sort((a, b) => {
263
- return a.cacheLifetimeMs - b.cacheLifetimeMs ||
264
- b.totalBytes - a.totalBytes ||
265
- a.url.localeCompare(b.url);
252
+ results.push({
253
+ url,
254
+ debugData,
255
+ cacheLifetimeMs: cacheLifetimeInSeconds * 1000,
256
+ cacheHitProbability,
257
+ totalBytes,
258
+ wastedBytes,
266
259
  });
260
+ }
267
261
 
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
- };
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
- return ComputedChains.request({devtoolsLog, trace, URL}, context).then(chains => {
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);
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
- 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
- };
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
- return NetworkRecords.request(devtoolsLogs, context).then(networkRecords => {
76
- const insecureURLs = networkRecords
77
- .filter(record => !NetworkRequest.isSecureRequest(record))
78
- .map(record => UrlUtils.elideDataURI(record.url));
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
- /** @type {Array<{url: string, resolution?: LH.IcuMessage|string}>} */
81
- const items = Array.from(new Set(insecureURLs)).map(url => ({url, resolution: undefined}));
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
- /** @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
- ];
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
- 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);
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
- // 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
- }
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
- let displayValue;
110
- if (items.length > 0) {
111
- displayValue = str_(UIStrings.displayValue, {itemCount: items.length});
112
- }
109
+ let displayValue;
110
+ if (items.length > 0) {
111
+ displayValue = str_(UIStrings.displayValue, {itemCount: items.length});
112
+ }
113
113
 
114
- return {
115
- score: Number(items.length === 0),
116
- displayValue,
117
- details: Audit.makeTableDetails(headings, items),
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 {Immutable<LH.Config.Settings>} */
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);
@@ -65,7 +65,7 @@ class ResourceBudget extends Audit {
65
65
  }
66
66
 
67
67
  /**
68
- * @param {Immutable<LH.Budget>} budget
68
+ * @param {LH.Util.Immutable<LH.Budget>} budget
69
69
  * @param {Record<LH.Budget.ResourceType, ResourceEntry>} summary
70
70
  * @return {Array<BudgetItem>}
71
71
  */
@@ -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 MainResource.request({devtoolsLog, URL: artifacts.URL}, context)
98
- .then(mainResource => {
99
- /** @type {LH.Audit.Details.Table['items']} */
100
- const blockingDirectives = [];
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
 
@@ -80,7 +80,7 @@ class TimingBudget extends Audit {
80
80
  }
81
81
 
82
82
  /**
83
- * @param {Immutable<LH.Budget>} budget
83
+ * @param {LH.Util.Immutable<LH.Budget>} budget
84
84
  * @param {LH.Artifacts.TimingSummary} summary
85
85
  * @return {Array<BudgetItem>}
86
86
  */
@@ -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
- return ComputedUserTimings.request(trace, context).then(computedUserTimings => {
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
- });
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
- /** @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
- ];
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
- const details = Audit.makeTableDetails(headings, tableRows);
100
+ const details = Audit.makeTableDetails(headings, tableRows);
101
101
 
102
- let displayValue;
103
- if (userTimings.length) {
104
- displayValue = str_(UIStrings.displayValue, {itemCount: userTimings.length});
105
- }
102
+ let displayValue;
103
+ if (userTimings.length) {
104
+ displayValue = str_(UIStrings.displayValue, {itemCount: userTimings.length});
105
+ }
106
106
 
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
- };
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