lighthouse 9.5.0-dev.20220313 → 9.5.0-dev.20220316

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 (30) hide show
  1. package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +0 -1
  2. package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +1 -0
  3. package/lighthouse-core/audits/byte-efficiency/duplicated-javascript.js +9 -8
  4. package/lighthouse-core/audits/byte-efficiency/legacy-javascript.js +11 -12
  5. package/lighthouse-core/audits/byte-efficiency/unminified-javascript.js +9 -10
  6. package/lighthouse-core/audits/byte-efficiency/unused-javascript.js +10 -6
  7. package/lighthouse-core/audits/deprecations.js +3 -3
  8. package/lighthouse-core/audits/dobetterweb/geolocation-on-start.js +1 -1
  9. package/lighthouse-core/audits/dobetterweb/no-document-write.js +1 -1
  10. package/lighthouse-core/audits/dobetterweb/notification-on-start.js +1 -1
  11. package/lighthouse-core/audits/dobetterweb/uses-passive-event-listeners.js +1 -1
  12. package/lighthouse-core/audits/errors-in-console.js +2 -2
  13. package/lighthouse-core/audits/no-unload-listeners.js +7 -15
  14. package/lighthouse-core/audits/script-elements-test-audit.js +30 -0
  15. package/lighthouse-core/audits/script-treemap-data.js +22 -21
  16. package/lighthouse-core/audits/valid-source-maps.js +12 -14
  17. package/lighthouse-core/audits/violation-audit.js +1 -1
  18. package/lighthouse-core/computed/js-bundles.js +11 -11
  19. package/lighthouse-core/computed/module-duplication.js +6 -5
  20. package/lighthouse-core/computed/unused-javascript-summary.js +16 -36
  21. package/lighthouse-core/config/default-config.js +1 -0
  22. package/lighthouse-core/fraggle-rock/config/default-config.js +3 -0
  23. package/lighthouse-core/gather/gatherers/console-messages.js +4 -2
  24. package/lighthouse-core/gather/gatherers/js-usage.js +6 -52
  25. package/lighthouse-core/gather/gatherers/script-elements.js +7 -56
  26. package/lighthouse-core/gather/gatherers/scripts.js +136 -0
  27. package/lighthouse-core/gather/gatherers/source-maps.js +3 -0
  28. package/lighthouse-core/lib/url-shim.js +2 -2
  29. package/package.json +1 -1
  30. package/types/artifacts.d.ts +22 -7
@@ -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(),
@@ -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)$/);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "9.5.0-dev.20220313",
3
+ "version": "9.5.0-dev.20220316",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -147,8 +147,9 @@ export interface GathererArtifacts extends PublicGathererArtifacts,LegacyBaseArt
147
147
  GlobalListeners: Array<Artifacts.GlobalListener>;
148
148
  /** The issues surfaced in the devtools Issues panel */
149
149
  InspectorIssues: Artifacts.InspectorIssues;
150
- /** JS coverage information for code used during page load. Keyed by network URL. */
151
- JsUsage: Record<string, Array<Omit<LH.Crdp.Profiler.ScriptCoverage, 'url'>>>;
150
+ /** JS coverage information for code used during audit. Keyed by script id. */
151
+ // 'url' is excluded because it can be overriden by a magic sourceURL= comment, which makes keeping it a dangerous footgun!
152
+ JsUsage: Record<string, Omit<LH.Crdp.Profiler.ScriptCoverage, 'url'>>;
152
153
  /** Parsed version of the page's Web App Manifest, or null if none found. */
153
154
  Manifest: Artifacts.Manifest | null;
154
155
  /** The URL loaded with interception */
@@ -161,6 +162,8 @@ export interface GathererArtifacts extends PublicGathererArtifacts,LegacyBaseArt
161
162
  ResponseCompression: {requestId: string, url: string, mimeType: string, transferSize: number, resourceSize: number, gzipSize?: number}[];
162
163
  /** Information on fetching and the content of the /robots.txt file. */
163
164
  RobotsTxt: {status: number|null, content: string|null, errorMessage?: string};
165
+ /** Information on all scripts in the page. */
166
+ Scripts: Artifacts.Script[];
164
167
  /** Version information for all ServiceWorkers active after the first page load. */
165
168
  ServiceWorker: {versions: LH.Crdp.ServiceWorker.ServiceWorkerVersion[], registrations: LH.Crdp.ServiceWorker.ServiceWorkerRegistration[]};
166
169
  /** Source maps of scripts executed in the page. */
@@ -287,6 +290,16 @@ declare module Artifacts {
287
290
 
288
291
  interface PasswordInputsWithPreventedPaste {node: NodeDetails}
289
292
 
293
+ interface Script extends Omit<LH.Crdp.Debugger.ScriptParsedEvent, 'url'|'embedderName'> {
294
+ /**
295
+ * Set by a sourceURL= magic comment if present, otherwise this is the same as the URL.
296
+ * Use this field for presentational purposes only.
297
+ */
298
+ name: string;
299
+ url: string;
300
+ content?: string;
301
+ }
302
+
290
303
  interface ScriptElement {
291
304
  type: string | null
292
305
  src: string | null
@@ -298,10 +311,6 @@ declare module Artifacts {
298
311
  node: NodeDetails | null
299
312
  /** Where the script was discovered, either in the head, the body, or network records. */
300
313
  source: 'head'|'body'|'network'
301
- /** The content of the inline script or the network record with the matching URL, null if the script had a src and no network record could be found. */
302
- content: string | null
303
- /** The ID of the network request that matched the URL of the src or the main document if inline, null if no request could be found. */
304
- requestId: string | null
305
314
  }
306
315
 
307
316
  /** @see https://sourcemaps.info/spec.html#h.qz3o9nc69um5 */
@@ -333,6 +342,8 @@ declare module Artifacts {
333
342
  * parsing the map, errorMessage will be defined instead of map.
334
343
  */
335
344
  type SourceMap = {
345
+ /** The DevTools protocol script identifier. */
346
+ scriptId: string;
336
347
  /** URL of code that source map applies to. */
337
348
  scriptUrl: string
338
349
  /** URL of the source map. undefined if from data URL. */
@@ -340,6 +351,8 @@ declare module Artifacts {
340
351
  /** Source map data structure. */
341
352
  map: RawSourceMap
342
353
  } | {
354
+ /** The DevTools protocol script identifier. */
355
+ scriptId: string;
343
356
  /** URL of code that source map applies to. */
344
357
  scriptUrl: string
345
358
  /** URL of the source map. undefined if from data URL. */
@@ -352,7 +365,7 @@ declare module Artifacts {
352
365
 
353
366
  interface Bundle {
354
367
  rawMap: RawSourceMap;
355
- script: ScriptElement;
368
+ script: LH.Artifacts.Script;
356
369
  map: TextSourceMap;
357
370
  sizes: {
358
371
  // TODO(cjamcl): Rename to `sources`.
@@ -848,6 +861,8 @@ declare module Artifacts {
848
861
  stackTrace?: LH.Crdp.Runtime.StackTrace;
849
862
  /** The URL of the log/exception, if known. */
850
863
  url?: string;
864
+ /** The script id of the log/exception, if known. */
865
+ scriptId?: string;
851
866
  /** Line number in the script (0-indexed), if known. */
852
867
  lineNumber?: number;
853
868
  /** Column number in the script (0-indexed), if known. */