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.
- package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +0 -1
- package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +1 -0
- package/lighthouse-core/audits/byte-efficiency/duplicated-javascript.js +9 -8
- package/lighthouse-core/audits/byte-efficiency/legacy-javascript.js +11 -12
- package/lighthouse-core/audits/byte-efficiency/unminified-javascript.js +9 -10
- package/lighthouse-core/audits/byte-efficiency/unused-javascript.js +10 -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/url-shim.js +2 -2
- package/package.json +1 -1
- package/types/artifacts.d.ts +22 -7
|
@@ -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(),
|
|
@@ -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)$/);
|
package/package.json
CHANGED
package/types/artifacts.d.ts
CHANGED
|
@@ -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
|
|
151
|
-
|
|
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:
|
|
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. */
|