lighthouse 9.5.0-dev.20220314 → 9.5.0-dev.20220317

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 (32) hide show
  1. package/lighthouse-cli/test/smokehouse/core-tests.js +2 -0
  2. package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +1 -1
  3. package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +1 -0
  4. package/lighthouse-core/audits/byte-efficiency/duplicated-javascript.js +11 -13
  5. package/lighthouse-core/audits/byte-efficiency/legacy-javascript.js +12 -16
  6. package/lighthouse-core/audits/byte-efficiency/unminified-javascript.js +10 -10
  7. package/lighthouse-core/audits/byte-efficiency/unused-javascript.js +11 -6
  8. package/lighthouse-core/audits/deprecations.js +3 -3
  9. package/lighthouse-core/audits/dobetterweb/geolocation-on-start.js +1 -1
  10. package/lighthouse-core/audits/dobetterweb/no-document-write.js +1 -1
  11. package/lighthouse-core/audits/dobetterweb/notification-on-start.js +1 -1
  12. package/lighthouse-core/audits/dobetterweb/uses-passive-event-listeners.js +1 -1
  13. package/lighthouse-core/audits/errors-in-console.js +2 -2
  14. package/lighthouse-core/audits/no-unload-listeners.js +7 -15
  15. package/lighthouse-core/audits/script-elements-test-audit.js +30 -0
  16. package/lighthouse-core/audits/script-treemap-data.js +22 -21
  17. package/lighthouse-core/audits/valid-source-maps.js +12 -14
  18. package/lighthouse-core/audits/violation-audit.js +1 -1
  19. package/lighthouse-core/computed/js-bundles.js +11 -11
  20. package/lighthouse-core/computed/module-duplication.js +6 -5
  21. package/lighthouse-core/computed/unused-javascript-summary.js +16 -36
  22. package/lighthouse-core/config/default-config.js +1 -0
  23. package/lighthouse-core/fraggle-rock/config/default-config.js +3 -0
  24. package/lighthouse-core/gather/gatherers/console-messages.js +4 -2
  25. package/lighthouse-core/gather/gatherers/js-usage.js +6 -52
  26. package/lighthouse-core/gather/gatherers/script-elements.js +7 -56
  27. package/lighthouse-core/gather/gatherers/scripts.js +136 -0
  28. package/lighthouse-core/gather/gatherers/source-maps.js +3 -0
  29. package/lighthouse-core/lib/script-helpers.js +22 -0
  30. package/lighthouse-core/lib/url-shim.js +2 -2
  31. package/package.json +3 -2
  32. package/types/artifacts.d.ts +22 -7
@@ -12,14 +12,15 @@ const SDK = require('../lib/cdt/SDK.js');
12
12
  /**
13
13
  * Calculate the number of bytes contributed by each source file
14
14
  * @param {LH.Artifacts.Bundle['map']} map
15
+ * @param {number} contentLength
15
16
  * @param {string} content
16
17
  * @return {LH.Artifacts.Bundle['sizes']}
17
18
  */
18
- function computeGeneratedFileSizes(map, content) {
19
+ function computeGeneratedFileSizes(map, contentLength, content) {
19
20
  const lines = content.split('\n');
20
21
  /** @type {Record<string, number>} */
21
22
  const files = {};
22
- const totalBytes = content.length;
23
+ const totalBytes = contentLength;
23
24
  let unmappedBytes = totalBytes;
24
25
 
25
26
  // @ts-expect-error: This function is added in SDK.js. This will eventually be added to CDT.
@@ -78,10 +79,10 @@ function computeGeneratedFileSizes(map, content) {
78
79
 
79
80
  class JSBundles {
80
81
  /**
81
- * @param {Pick<LH.Artifacts, 'SourceMaps'|'ScriptElements'>} artifacts
82
+ * @param {Pick<LH.Artifacts, 'SourceMaps'|'Scripts'>} artifacts
82
83
  */
83
84
  static async compute_(artifacts) {
84
- const {SourceMaps, ScriptElements} = artifacts;
85
+ const {SourceMaps, Scripts} = artifacts;
85
86
 
86
87
  /** @type {LH.Artifacts.Bundle[]} */
87
88
  const bundles = [];
@@ -89,23 +90,22 @@ class JSBundles {
89
90
  // Collate map and script, compute file sizes.
90
91
  for (const SourceMap of SourceMaps) {
91
92
  if (!SourceMap.map) continue;
92
- const {scriptUrl, map: rawMap} = SourceMap;
93
+ const {scriptId, map: rawMap} = SourceMap;
93
94
 
94
95
  if (!rawMap.mappings) continue;
95
96
 
96
- const scriptElement = ScriptElements.find(s => s.src === scriptUrl);
97
- if (!scriptElement) continue;
97
+ const script = Scripts.find(s => s.scriptId === scriptId);
98
+ if (!script) continue;
98
99
 
99
100
  const compiledUrl = SourceMap.scriptUrl || 'compiled.js';
100
101
  const mapUrl = SourceMap.sourceMapUrl || 'compiled.js.map';
101
102
  const map = new SDK.TextSourceMap(compiledUrl, mapUrl, rawMap);
102
103
 
103
- const content = scriptElement?.content ? scriptElement.content : '';
104
- const sizes = computeGeneratedFileSizes(map, content);
104
+ const sizes = computeGeneratedFileSizes(map, script.length || 0, script.content || '');
105
105
 
106
106
  const bundle = {
107
107
  rawMap,
108
- script: scriptElement,
108
+ script,
109
109
  map,
110
110
  sizes,
111
111
  };
@@ -116,4 +116,4 @@ class JSBundles {
116
116
  }
117
117
  }
118
118
 
119
- module.exports = makeComputedArtifact(JSBundles, ['ScriptElements', 'SourceMaps']);
119
+ module.exports = makeComputedArtifact(JSBundles, ['Scripts', 'SourceMaps']);
@@ -43,7 +43,7 @@ class ModuleDuplication {
43
43
  }
44
44
 
45
45
  /**
46
- * @param {Map<string, Array<{scriptUrl: string, resourceSize: number}>>} moduleNameToSourceData
46
+ * @param {Map<string, Array<{scriptId: string, resourceSize: number}>>} moduleNameToSourceData
47
47
  */
48
48
  static _normalizeAggregatedData(moduleNameToSourceData) {
49
49
  for (const [key, originalSourceData] of moduleNameToSourceData.entries()) {
@@ -74,7 +74,7 @@ class ModuleDuplication {
74
74
  }
75
75
 
76
76
  /**
77
- * @param {Pick<LH.Artifacts, 'ScriptElements'|'SourceMaps'>} artifacts
77
+ * @param {Pick<LH.Artifacts, 'Scripts'|'SourceMaps'>} artifacts
78
78
  * @param {LH.Artifacts.ComputedContext} context
79
79
  */
80
80
  static async compute_(artifacts, context) {
@@ -109,7 +109,7 @@ class ModuleDuplication {
109
109
  }
110
110
  }
111
111
 
112
- /** @type {Map<string, Array<{scriptUrl: string, resourceSize: number}>>} */
112
+ /** @type {Map<string, Array<{scriptId: string, scriptUrl: string, resourceSize: number}>>} */
113
113
  const moduleNameToSourceData = new Map();
114
114
  for (const {rawMap, script} of bundles) {
115
115
  const sourceDataArray = sourceDatasMap.get(rawMap);
@@ -122,7 +122,8 @@ class ModuleDuplication {
122
122
  moduleNameToSourceData.set(sourceData.source, data);
123
123
  }
124
124
  data.push({
125
- scriptUrl: script.src || '',
125
+ scriptId: script.scriptId,
126
+ scriptUrl: script.url,
126
127
  resourceSize: sourceData.resourceSize,
127
128
  });
128
129
  }
@@ -133,4 +134,4 @@ class ModuleDuplication {
133
134
  }
134
135
  }
135
136
 
136
- module.exports = makeComputedArtifact(ModuleDuplication, ['ScriptElements', 'SourceMaps']);
137
+ module.exports = makeComputedArtifact(ModuleDuplication, ['Scripts', 'SourceMaps']);
@@ -16,14 +16,14 @@ const makeComputedArtifact = require('./computed-artifact.js');
16
16
 
17
17
  /**
18
18
  * @typedef ComputeInput
19
- * @property {string} url
20
- * @property {Array<Omit<LH.Crdp.Profiler.ScriptCoverage, 'url'>>} scriptCoverages
19
+ * @property {string} scriptId
20
+ * @property {Omit<LH.Crdp.Profiler.ScriptCoverage, 'url'>} scriptCoverage
21
21
  * @property {LH.Artifacts.Bundle=} bundle
22
22
  */
23
23
 
24
24
  /**
25
25
  * @typedef Summary
26
- * @property {string} url
26
+ * @property {string} scriptId
27
27
  * @property {number} wastedBytes
28
28
  * @property {number} totalBytes
29
29
  * @property {number} wastedBytes
@@ -68,43 +68,24 @@ class UnusedJavascriptSummary {
68
68
  }
69
69
 
70
70
  /**
71
- * @param {string} url
72
- * @param {ReturnType<typeof UnusedJavascriptSummary.determineLengths>} lengths
71
+ * @param {string} scriptId
72
+ * @param {WasteData} wasteData
73
73
  * @return {Summary}
74
74
  */
75
- static createItem(url, lengths) {
76
- const wastedRatio = (lengths.unused / lengths.content) || 0;
77
- const wastedBytes = Math.round(lengths.content * wastedRatio);
75
+ static createItem(scriptId, wasteData) {
76
+ const wastedRatio = (wasteData.unusedLength / wasteData.contentLength) || 0;
77
+ const wastedBytes = Math.round(wasteData.contentLength * wastedRatio);
78
78
 
79
79
  return {
80
- url,
81
- totalBytes: lengths.content,
80
+ scriptId,
81
+ totalBytes: wasteData.contentLength,
82
82
  wastedBytes,
83
83
  wastedPercent: 100 * wastedRatio,
84
84
  };
85
85
  }
86
86
 
87
87
  /**
88
- * @param {WasteData[]} wasteData
89
- */
90
- static determineLengths(wasteData) {
91
- let unused = 0;
92
- let content = 0;
93
- // TODO: this is right for multiple script tags in an HTML document,
94
- // but may be wrong for multiple frames using the same script resource.
95
- for (const usage of wasteData) {
96
- unused += usage.unusedLength;
97
- content += usage.contentLength;
98
- }
99
-
100
- return {
101
- content,
102
- unused,
103
- };
104
- }
105
-
106
- /**
107
- * @param {WasteData[]} wasteData
88
+ * @param {WasteData} wasteData
108
89
  * @param {LH.Artifacts.Bundle} bundle
109
90
  */
110
91
  static createSourceWastedBytes(wasteData, bundle) {
@@ -131,7 +112,7 @@ class UnusedJavascriptSummary {
131
112
  mapping.lastColumnNumber - 1 :
132
113
  lineLengths[mapping.lineNumber];
133
114
  for (let i = mapping.columnNumber; i <= lastColumnOfMapping; i++) {
134
- if (wasteData.every(data => data.unusedByIndex[offset] === 1)) {
115
+ if (wasteData.unusedByIndex[offset] === 1) {
135
116
  const key = mapping.sourceURL || '(unmapped)';
136
117
  files[key] = (files[key] || 0) + 1;
137
118
  }
@@ -155,11 +136,10 @@ class UnusedJavascriptSummary {
155
136
  * @return {Promise<Summary>}
156
137
  */
157
138
  static async compute_(data) {
158
- const {url, scriptCoverages, bundle} = data;
139
+ const {scriptId, scriptCoverage, bundle} = data;
159
140
 
160
- const wasteData = scriptCoverages.map(UnusedJavascriptSummary.computeWaste);
161
- const lengths = UnusedJavascriptSummary.determineLengths(wasteData);
162
- const item = UnusedJavascriptSummary.createItem(url, lengths);
141
+ const wasteData = UnusedJavascriptSummary.computeWaste(scriptCoverage);
142
+ const item = UnusedJavascriptSummary.createItem(scriptId, wasteData);
163
143
  if (!bundle) return item;
164
144
 
165
145
  return {
@@ -171,5 +151,5 @@ class UnusedJavascriptSummary {
171
151
 
172
152
  module.exports = makeComputedArtifact(
173
153
  UnusedJavascriptSummary,
174
- ['bundle', 'scriptCoverages', 'url']
154
+ ['bundle', 'scriptCoverage', 'scriptId']
175
155
  );
@@ -143,6 +143,7 @@ const defaultConfig = {
143
143
  'link-elements',
144
144
  'meta-elements',
145
145
  'script-elements',
146
+ 'scripts',
146
147
  'iframe-elements',
147
148
  'inputs',
148
149
  'main-document-content',
@@ -64,6 +64,7 @@ const artifacts = {
64
64
  RobotsTxt: '',
65
65
  ServiceWorker: '',
66
66
  ScriptElements: '',
67
+ Scripts: '',
67
68
  SourceMaps: '',
68
69
  Stacks: '',
69
70
  TagsBlockingFirstPaint: '',
@@ -114,6 +115,7 @@ const defaultConfig = {
114
115
  {id: artifacts.RobotsTxt, gatherer: 'seo/robots-txt'},
115
116
  {id: artifacts.ServiceWorker, gatherer: 'service-worker'},
116
117
  {id: artifacts.ScriptElements, gatherer: 'script-elements'},
118
+ {id: artifacts.Scripts, gatherer: 'scripts'},
117
119
  {id: artifacts.SourceMaps, gatherer: 'source-maps'},
118
120
  {id: artifacts.Stacks, gatherer: 'stacks'},
119
121
  {id: artifacts.TagsBlockingFirstPaint, gatherer: 'dobetterweb/tags-blocking-first-paint'},
@@ -165,6 +167,7 @@ const defaultConfig = {
165
167
  artifacts.RobotsTxt,
166
168
  artifacts.ServiceWorker,
167
169
  artifacts.ScriptElements,
170
+ artifacts.Scripts,
168
171
  artifacts.SourceMaps,
169
172
  artifacts.Stacks,
170
173
  artifacts.TagsBlockingFirstPaint,
@@ -101,6 +101,7 @@ class ConsoleMessages extends FRGatherer {
101
101
  stackTrace: event.exceptionDetails.stackTrace,
102
102
  timestamp: event.timestamp,
103
103
  url: event.exceptionDetails.url,
104
+ scriptId: event.exceptionDetails.scriptId,
104
105
  lineNumber: event.exceptionDetails.lineNumber,
105
106
  columnNumber: event.exceptionDetails.columnNumber,
106
107
  };
@@ -117,7 +118,7 @@ class ConsoleMessages extends FRGatherer {
117
118
 
118
119
  // JS events have a stack trace, which we use to get the column.
119
120
  // CSS/HTML events only expose a line number.
120
- const {columnNumber} = event.entry.stackTrace?.callFrames[0] || {};
121
+ const firstStackFrame = event.entry.stackTrace?.callFrames[0];
121
122
 
122
123
  this._logEntries.push({
123
124
  eventType: 'protocolLog',
@@ -127,8 +128,9 @@ class ConsoleMessages extends FRGatherer {
127
128
  stackTrace,
128
129
  timestamp,
129
130
  url,
131
+ scriptId: firstStackFrame?.scriptId,
130
132
  lineNumber,
131
- columnNumber,
133
+ columnNumber: firstStackFrame?.columnNumber,
132
134
  });
133
135
  }
134
136
 
@@ -73,72 +73,26 @@ class JsUsage extends FRGatherer {
73
73
  }
74
74
 
75
75
  /**
76
- * Usages alone do not always generate an exhaustive list of scripts in timespan and snapshot.
77
- * For audits which use this for url/scriptId mappings, we can include an empty usage object.
78
- *
79
- * @param {Record<string, Array<LH.Crdp.Profiler.ScriptCoverage>>} usageByUrl
80
- */
81
- _addMissingScriptIds(usageByUrl) {
82
- for (const scriptParsedEvent of this._scriptParsedEvents) {
83
- const url = scriptParsedEvent.embedderName;
84
- if (!url) continue;
85
-
86
- const scripts = usageByUrl[url] || [];
87
- if (!scripts.find(s => s.scriptId === scriptParsedEvent.scriptId)) {
88
- scripts.push({
89
- url,
90
- scriptId: scriptParsedEvent.scriptId,
91
- functions: [],
92
- });
93
- }
94
- usageByUrl[url] = scripts;
95
- }
96
- }
97
-
98
- /**
99
- * @param {LH.Gatherer.FRTransitionalContext} context
100
76
  * @return {Promise<LH.Artifacts['JsUsage']>}
101
77
  */
102
- async getArtifact(context) {
103
- /** @type {Record<string, Array<LH.Crdp.Profiler.ScriptCoverage>>} */
104
- const usageByUrl = {};
105
-
106
- // Force `Debugger.scriptParsed` events for url to scriptId mappings in snapshot mode.
107
- if (context.gatherMode === 'snapshot') {
108
- await this.startSensitiveInstrumentation(context);
109
- await this.stopSensitiveInstrumentation(context);
110
- }
78
+ async getArtifact() {
79
+ /** @type {Record<string, LH.Crdp.Profiler.ScriptCoverage>} */
80
+ const usageByScriptId = {};
111
81
 
112
82
  for (const scriptUsage of this._scriptUsages) {
113
- // `ScriptCoverage.url` can be overridden by a magic sourceURL comment.
114
- // Get the associated ScriptParsedEvent and use embedderName, which is the original url.
115
- // See https://chromium-review.googlesource.com/c/v8/v8/+/2317310
116
- let url = scriptUsage.url;
117
- const scriptParsedEvent =
118
- this._scriptParsedEvents.find(e => e.scriptId === scriptUsage.scriptId);
119
- if (scriptParsedEvent?.embedderName) {
120
- url = scriptParsedEvent.embedderName;
121
- }
122
-
123
83
  // If `url` is blank, that means the script was anonymous (eval, new Function, onload, ...).
124
84
  // Or, it's because it was code Lighthouse over the protocol via `Runtime.evaluate`.
125
85
  // We currently don't consider coverage of anonymous scripts, and we definitely don't want
126
86
  // coverage of code Lighthouse ran to inspect the page, so we ignore this ScriptCoverage if
127
87
  // url is blank.
128
- if (scriptUsage.url === '' || (scriptParsedEvent && scriptParsedEvent.embedderName === '')) {
88
+ if (scriptUsage.url === '') {
129
89
  continue;
130
90
  }
131
91
 
132
- const scripts = usageByUrl[url] || [];
133
- scripts.push(scriptUsage);
134
- usageByUrl[url] = scripts;
135
- }
136
-
137
- if (context.gatherMode !== 'navigation') {
138
- this._addMissingScriptIds(usageByUrl);
92
+ usageByScriptId[scriptUsage.scriptId] = scriptUsage;
139
93
  }
140
94
 
141
- return usageByUrl;
95
+ return usageByScriptId;
142
96
  }
143
97
  }
144
98
 
@@ -7,10 +7,8 @@
7
7
 
8
8
  const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
9
9
  const NetworkRecords = require('../../computed/network-records.js');
10
- const NetworkAnalyzer = require('../../lib/dependency-graph/simulator/network-analyzer.js');
11
10
  const NetworkRequest = require('../../lib/network-request.js');
12
11
  const pageFunctions = require('../../lib/page-functions.js');
13
- const {fetchResponseBodyFromCache} = require('../driver/network.js');
14
12
  const DevtoolsLog = require('./devtools-log.js');
15
13
 
16
14
  /* global getNodeDetails */
@@ -32,8 +30,6 @@ function collectAllScriptElements() {
32
30
  async: script.async,
33
31
  defer: script.defer,
34
32
  source: script.closest('head') ? 'head' : 'body',
35
- content: script.src ? null : script.text,
36
- requestId: null,
37
33
  // @ts-expect-error - getNodeDetails put into scope via stringification
38
34
  node: getNodeDetails(script),
39
35
  };
@@ -41,27 +37,6 @@ function collectAllScriptElements() {
41
37
  }
42
38
  /* c8 ignore stop */
43
39
 
44
- /**
45
- * @template T, U
46
- * @param {Array<T>} values
47
- * @param {(value: T) => Promise<U>} promiseMapper
48
- * @param {boolean} runInSeries
49
- * @return {Promise<Array<U>>}
50
- */
51
- async function runInSeriesOrParallel(values, promiseMapper, runInSeries) {
52
- if (runInSeries) {
53
- const results = [];
54
- for (const value of values) {
55
- const result = await promiseMapper(value);
56
- results.push(result);
57
- }
58
- return results;
59
- } else {
60
- const promises = values.map(promiseMapper);
61
- return await Promise.all(promises);
62
- }
63
- }
64
-
65
40
  /**
66
41
  * @fileoverview Gets JavaScript file contents.
67
42
  */
@@ -75,13 +50,10 @@ class ScriptElements extends FRGatherer {
75
50
  /**
76
51
  * @param {LH.Gatherer.FRTransitionalContext} context
77
52
  * @param {LH.Artifacts.NetworkRequest[]} networkRecords
78
- * @param {LH.Artifacts['HostFormFactor']} formFactor
79
53
  * @return {Promise<LH.Artifacts['ScriptElements']>}
80
54
  */
81
- async _getArtifact(context, networkRecords, formFactor) {
82
- const session = context.driver.defaultSession;
55
+ async _getArtifact(context, networkRecords) {
83
56
  const executionContext = context.driver.executionContext;
84
- const mainResource = NetworkAnalyzer.findOptionalMainDocument(networkRecords, context.url);
85
57
 
86
58
  const scripts = await executionContext.evaluate(collectAllScriptElements, {
87
59
  args: [],
@@ -92,34 +64,16 @@ class ScriptElements extends FRGatherer {
92
64
  ],
93
65
  });
94
66
 
95
- for (const script of scripts) {
96
- if (mainResource && script.content) script.requestId = mainResource.requestId;
97
- }
98
-
99
67
  const scriptRecords = networkRecords
68
+ .filter(record => record.resourceType === NetworkRequest.TYPES.Script)
100
69
  // Ignore records from OOPIFs
101
- .filter(record => !record.sessionId)
102
- // Only get the content of script requests
103
- .filter(record => record.resourceType === NetworkRequest.TYPES.Script);
104
-
105
- // If run on a mobile device, be sensitive to memory limitations and only request one
106
- // record at a time.
107
- const scriptRecordContents = await runInSeriesOrParallel(
108
- scriptRecords,
109
- record => fetchResponseBodyFromCache(session, record.requestId).catch(() => ''),
110
- formFactor === 'mobile' /* runInSeries */
111
- );
70
+ .filter(record => !record.sessionId);
112
71
 
113
72
  for (let i = 0; i < scriptRecords.length; i++) {
114
73
  const record = scriptRecords[i];
115
- const content = scriptRecordContents[i];
116
- if (!content) continue;
117
74
 
118
75
  const matchedScriptElement = scripts.find(script => script.src === record.url);
119
- if (matchedScriptElement) {
120
- matchedScriptElement.requestId = record.requestId;
121
- matchedScriptElement.content = content;
122
- } else {
76
+ if (!matchedScriptElement) {
123
77
  scripts.push({
124
78
  type: null,
125
79
  src: record.url,
@@ -127,12 +81,11 @@ class ScriptElements extends FRGatherer {
127
81
  async: false,
128
82
  defer: false,
129
83
  source: 'network',
130
- requestId: record.requestId,
131
- content,
132
84
  node: null,
133
85
  });
134
86
  }
135
87
  }
88
+
136
89
  return scripts;
137
90
  }
138
91
 
@@ -141,9 +94,8 @@ class ScriptElements extends FRGatherer {
141
94
  */
142
95
  async getArtifact(context) {
143
96
  const devtoolsLog = context.dependencies.DevtoolsLog;
144
- const formFactor = context.baseArtifacts.HostFormFactor;
145
97
  const networkRecords = await NetworkRecords.request(devtoolsLog, context);
146
- return this._getArtifact(context, networkRecords, formFactor);
98
+ return this._getArtifact(context, networkRecords);
147
99
  }
148
100
 
149
101
  /**
@@ -152,8 +104,7 @@ class ScriptElements extends FRGatherer {
152
104
  */
153
105
  async afterPass(passContext, loadData) {
154
106
  const networkRecords = loadData.networkRecords;
155
- const formFactor = passContext.baseArtifacts.HostFormFactor;
156
- return this._getArtifact({...passContext, dependencies: {}}, networkRecords, formFactor);
107
+ return this._getArtifact({...passContext, dependencies: {}}, networkRecords);
157
108
  }
158
109
  }
159
110
 
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
3
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
+ */
6
+ 'use strict';
7
+
8
+ const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
9
+
10
+ /**
11
+ * @template T, U
12
+ * @param {Array<T>} values
13
+ * @param {(value: T) => Promise<U>} promiseMapper
14
+ * @param {boolean} runInSeries
15
+ * @return {Promise<Array<U>>}
16
+ */
17
+ async function runInSeriesOrParallel(values, promiseMapper, runInSeries) {
18
+ if (runInSeries) {
19
+ const results = [];
20
+ for (const value of values) {
21
+ const result = await promiseMapper(value);
22
+ results.push(result);
23
+ }
24
+ return results;
25
+ } else {
26
+ const promises = values.map(promiseMapper);
27
+ return await Promise.all(promises);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * @fileoverview Gets JavaScript file contents.
33
+ */
34
+ class Scripts extends FRGatherer {
35
+ /** @type {LH.Gatherer.GathererMeta} */
36
+ meta = {
37
+ supportedModes: ['timespan', 'navigation'],
38
+ };
39
+
40
+ /** @type {LH.Crdp.Debugger.ScriptParsedEvent[]} */
41
+ _scriptParsedEvents = [];
42
+
43
+ /** @type {Array<string | undefined>} */
44
+ _scriptContents = [];
45
+
46
+ /** @type {string|null|undefined} */
47
+ _mainSessionId = null;
48
+
49
+ constructor() {
50
+ super();
51
+ this.onProtocolMessage = this.onProtocolMessage.bind(this);
52
+ }
53
+
54
+ /**
55
+ * @param {LH.Protocol.RawEventMessage} event
56
+ */
57
+ onProtocolMessage(event) {
58
+ // Go read the comments in network-recorder.js _findRealRequestAndSetSession.
59
+ let sessionId = event.sessionId;
60
+ if (this._mainSessionId === null) {
61
+ this._mainSessionId = sessionId;
62
+ }
63
+ if (this._mainSessionId === sessionId) {
64
+ sessionId = undefined;
65
+ }
66
+
67
+ // We want to ignore scripts from OOPIFs. In reality, this does more than block just OOPIFs,
68
+ // it also blocks scripts from the same origin but that happen to run in a different process,
69
+ // like a worker.
70
+ if (event.method === 'Debugger.scriptParsed' && !sessionId) {
71
+ // Events without an embedderName (read: a url) are for JS that we ran over the protocol.
72
+ if (event.params.embedderName) this._scriptParsedEvents.push(event.params);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * @param {LH.Gatherer.FRTransitionalContext} context
78
+ */
79
+ async startInstrumentation(context) {
80
+ const session = context.driver.defaultSession;
81
+ session.addProtocolMessageListener(this.onProtocolMessage);
82
+ await session.sendCommand('Debugger.enable');
83
+ }
84
+
85
+ /**
86
+ * @param {LH.Gatherer.FRTransitionalContext} context
87
+ */
88
+ async stopInstrumentation(context) {
89
+ const session = context.driver.defaultSession;
90
+ const formFactor = context.baseArtifacts.HostFormFactor;
91
+
92
+ session.removeProtocolMessageListener(this.onProtocolMessage);
93
+
94
+ // Without this line the Debugger domain will be off in FR runner,
95
+ // because only the legacy gatherer has special handling for multiple,
96
+ // overlapped enabled/disable calls.
97
+ await session.sendCommand('Debugger.enable');
98
+
99
+ // If run on a mobile device, be sensitive to memory limitations and only
100
+ // request one at a time.
101
+ this._scriptContents = await runInSeriesOrParallel(
102
+ this._scriptParsedEvents,
103
+ ({scriptId}) => {
104
+ return session.sendCommand('Debugger.getScriptSource', {scriptId})
105
+ .then((resp) => resp.scriptSource)
106
+ .catch(() => undefined);
107
+ },
108
+ formFactor === 'mobile' /* runInSeries */
109
+ );
110
+ await session.sendCommand('Debugger.disable');
111
+ }
112
+
113
+ async getArtifact() {
114
+ /** @type {LH.Artifacts['Scripts']} */
115
+ const scripts = this._scriptParsedEvents.map((event, i) => {
116
+ // 'embedderName' and 'url' are confusingly named, so we rewrite them here.
117
+ // On the protocol, 'embedderName' always refers to the URL of the script (or HTML if inline).
118
+ // Same for 'url' ... except, magic "sourceURL=" comments will override the value.
119
+ // It's nice to display the user-provided value in Lighthouse, so we add a field 'name'
120
+ // to make it clear this is for presentational purposes.
121
+ // See https://chromium-review.googlesource.com/c/v8/v8/+/2317310
122
+ return {
123
+ name: event.url,
124
+ ...event,
125
+ // embedderName is optional on the protocol because backends like Node may not set it.
126
+ // For our purposes, it is always set. But just in case it isn't... fallback to the url.
127
+ url: event.embedderName || event.url,
128
+ content: this._scriptContents[i],
129
+ };
130
+ });
131
+
132
+ return scripts;
133
+ }
134
+ }
135
+
136
+ module.exports = Scripts;
@@ -88,6 +88,7 @@ class SourceMaps extends FRGatherer {
88
88
 
89
89
  if (!rawSourceMapUrl) {
90
90
  return {
91
+ scriptId: event.scriptId,
91
92
  scriptUrl,
92
93
  errorMessage: `Could not resolve map url: ${event.sourceMapURL}`,
93
94
  };
@@ -104,12 +105,14 @@ class SourceMaps extends FRGatherer {
104
105
  map.sections = map.sections.filter(section => section.map);
105
106
  }
106
107
  return {
108
+ scriptId: event.scriptId,
107
109
  scriptUrl,
108
110
  sourceMapUrl,
109
111
  map,
110
112
  };
111
113
  } catch (err) {
112
114
  return {
115
+ scriptId: event.scriptId,
113
116
  scriptUrl,
114
117
  sourceMapUrl,
115
118
  errorMessage: err.toString(),
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
3
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
+ */
6
+ 'use strict';
7
+
8
+ /**
9
+ * @param {LH.Artifacts.NetworkRequest[]} networkRecords
10
+ * @param {LH.Artifacts.Script|undefined} script
11
+ * @return {LH.Artifacts.NetworkRequest|undefined}
12
+ */
13
+ function getRequestForScript(networkRecords, script) {
14
+ if (!script) return;
15
+ let networkRequest = networkRecords.find(request => request.url === script.url);
16
+ while (networkRequest?.redirectDestination) {
17
+ networkRequest = networkRequest.redirectDestination;
18
+ }
19
+ return networkRequest;
20
+ }
21
+
22
+ module.exports = {getRequestForScript};
@@ -236,9 +236,9 @@ class URLShim extends URL {
236
236
  }
237
237
 
238
238
  if (url.protocol === 'data:') {
239
- const match = url.pathname.match(/image\/(png|jpeg|svg\+xml|webp|gif|avif)(?=;)/);
239
+ const match = url.pathname.match(/^(image\/(png|jpeg|svg\+xml|webp|gif|avif))[;,]/);
240
240
  if (!match) return undefined;
241
- return match[0];
241
+ return match[1];
242
242
  }
243
243
 
244
244
  const match = url.pathname.toLowerCase().match(/\.(png|jpeg|jpg|svg|webp|gif|avif)$/);