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.
- package/lighthouse-cli/test/smokehouse/core-tests.js +2 -0
- package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +1 -1
- package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +1 -0
- package/lighthouse-core/audits/byte-efficiency/duplicated-javascript.js +11 -13
- package/lighthouse-core/audits/byte-efficiency/legacy-javascript.js +12 -16
- package/lighthouse-core/audits/byte-efficiency/unminified-javascript.js +10 -10
- package/lighthouse-core/audits/byte-efficiency/unused-javascript.js +11 -6
- package/lighthouse-core/audits/deprecations.js +3 -3
- package/lighthouse-core/audits/dobetterweb/geolocation-on-start.js +1 -1
- package/lighthouse-core/audits/dobetterweb/no-document-write.js +1 -1
- package/lighthouse-core/audits/dobetterweb/notification-on-start.js +1 -1
- package/lighthouse-core/audits/dobetterweb/uses-passive-event-listeners.js +1 -1
- package/lighthouse-core/audits/errors-in-console.js +2 -2
- package/lighthouse-core/audits/no-unload-listeners.js +7 -15
- package/lighthouse-core/audits/script-elements-test-audit.js +30 -0
- package/lighthouse-core/audits/script-treemap-data.js +22 -21
- package/lighthouse-core/audits/valid-source-maps.js +12 -14
- package/lighthouse-core/audits/violation-audit.js +1 -1
- package/lighthouse-core/computed/js-bundles.js +11 -11
- package/lighthouse-core/computed/module-duplication.js +6 -5
- package/lighthouse-core/computed/unused-javascript-summary.js +16 -36
- package/lighthouse-core/config/default-config.js +1 -0
- package/lighthouse-core/fraggle-rock/config/default-config.js +3 -0
- package/lighthouse-core/gather/gatherers/console-messages.js +4 -2
- package/lighthouse-core/gather/gatherers/js-usage.js +6 -52
- package/lighthouse-core/gather/gatherers/script-elements.js +7 -56
- package/lighthouse-core/gather/gatherers/scripts.js +136 -0
- package/lighthouse-core/gather/gatherers/source-maps.js +3 -0
- package/lighthouse-core/lib/script-helpers.js +22 -0
- package/lighthouse-core/lib/url-shim.js +2 -2
- package/package.json +3 -2
- 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 =
|
|
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'|'
|
|
82
|
+
* @param {Pick<LH.Artifacts, 'SourceMaps'|'Scripts'>} artifacts
|
|
82
83
|
*/
|
|
83
84
|
static async compute_(artifacts) {
|
|
84
|
-
const {SourceMaps,
|
|
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 {
|
|
93
|
+
const {scriptId, map: rawMap} = SourceMap;
|
|
93
94
|
|
|
94
95
|
if (!rawMap.mappings) continue;
|
|
95
96
|
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
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
|
|
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
|
|
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, ['
|
|
119
|
+
module.exports = makeComputedArtifact(JSBundles, ['Scripts', 'SourceMaps']);
|
|
@@ -43,7 +43,7 @@ class ModuleDuplication {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
* @param {Map<string, Array<{
|
|
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, '
|
|
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
|
-
|
|
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, ['
|
|
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}
|
|
20
|
-
* @property {
|
|
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}
|
|
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}
|
|
72
|
-
* @param {
|
|
71
|
+
* @param {string} scriptId
|
|
72
|
+
* @param {WasteData} wasteData
|
|
73
73
|
* @return {Summary}
|
|
74
74
|
*/
|
|
75
|
-
static createItem(
|
|
76
|
-
const wastedRatio = (
|
|
77
|
-
const wastedBytes = Math.round(
|
|
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
|
-
|
|
81
|
-
totalBytes:
|
|
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
|
|
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.
|
|
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 {
|
|
139
|
+
const {scriptId, scriptCoverage, bundle} = data;
|
|
159
140
|
|
|
160
|
-
const wasteData =
|
|
161
|
-
const
|
|
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', '
|
|
154
|
+
['bundle', 'scriptCoverage', 'scriptId']
|
|
175
155
|
);
|
|
@@ -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
|
|
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(
|
|
103
|
-
/** @type {Record<string,
|
|
104
|
-
const
|
|
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 === ''
|
|
88
|
+
if (scriptUsage.url === '') {
|
|
129
89
|
continue;
|
|
130
90
|
}
|
|
131
91
|
|
|
132
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
239
|
+
const match = url.pathname.match(/^(image\/(png|jpeg|svg\+xml|webp|gif|avif))[;,]/);
|
|
240
240
|
if (!match) return undefined;
|
|
241
|
-
return match[
|
|
241
|
+
return match[1];
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
const match = url.pathname.toLowerCase().match(/\.(png|jpeg|jpg|svg|webp|gif|avif)$/);
|