lighthouse 9.5.0-dev.20230110 → 9.5.0-dev.20230112
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/cli/bin.js +6 -6
- package/cli/run.js +1 -1
- package/cli/test/smokehouse/config/exclusions.js +5 -0
- package/cli/test/smokehouse/core-tests.js +14 -14
- package/cli/test/smokehouse/lighthouse-runners/bundle.js +9 -9
- package/cli/test/smokehouse/lighthouse-runners/cli.js +7 -7
- package/cli/test/smokehouse/lighthouse-runners/devtools.js +3 -3
- package/cli/test/smokehouse/readme.md +1 -1
- package/cli/test/smokehouse/smokehouse.js +13 -13
- package/core/audits/bf-cache.js +120 -0
- package/core/audits/installable-manifest.js +1 -1
- package/core/audits/seo/is-crawlable.js +114 -42
- package/core/config/config-helpers.js +11 -11
- package/core/config/config-plugin.js +2 -2
- package/core/config/config.js +15 -15
- package/core/config/default-config.js +8 -3
- package/core/config/desktop-config.js +1 -1
- package/core/config/experimental-config.js +1 -1
- package/core/config/full-config.js +1 -1
- package/core/config/lr-desktop-config.js +1 -1
- package/core/config/lr-mobile-config.js +1 -1
- package/core/config/perf-config.js +1 -1
- package/core/config/validation.js +4 -4
- package/core/gather/driver/execution-context.js +2 -2
- package/core/gather/driver/network-monitor.js +2 -2
- package/core/gather/driver/wait-for-condition.js +9 -2
- package/core/gather/gatherers/bf-cache-failures.js +21 -3
- package/core/gather/navigation-runner.js +1 -1
- package/core/gather/snapshot-runner.js +1 -1
- package/core/gather/timespan-runner.js +1 -1
- package/core/index.js +10 -10
- package/core/legacy/config/config.js +23 -23
- package/core/legacy/config/legacy-default-config.js +1 -1
- package/core/lib/{bfcache-strings.js → bf-cache-strings.js} +1 -0
- package/core/user-flow.js +9 -9
- package/core/util.cjs +13 -3
- package/dist/report/bundle.esm.js +13 -3
- package/dist/report/flow.js +1 -1
- package/dist/report/standalone.js +1 -1
- package/package.json +1 -1
- package/report/renderer/util.js +13 -3
- package/report/test/generator/report-generator-test.js +1 -1
- package/report/test/renderer/util-test.js +2 -0
- package/shared/localization/locales/en-US.json +141 -114
- package/shared/localization/locales/en-XL.json +141 -114
- package/tsconfig.json +1 -0
- package/types/config.d.ts +18 -18
- package/types/smokehouse.d.ts +2 -2
- package/types/user-flow.d.ts +1 -1
|
@@ -10,6 +10,14 @@ import {Audit} from '../audit.js';
|
|
|
10
10
|
import {MainResource} from '../../computed/main-resource.js';
|
|
11
11
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
12
12
|
|
|
13
|
+
const BOT_USER_AGENTS = new Set([
|
|
14
|
+
undefined,
|
|
15
|
+
'Googlebot',
|
|
16
|
+
'bingbot',
|
|
17
|
+
'DuckDuckBot',
|
|
18
|
+
'archive.org_bot',
|
|
19
|
+
]);
|
|
20
|
+
|
|
13
21
|
const BLOCKLIST = new Set([
|
|
14
22
|
'noindex',
|
|
15
23
|
'none',
|
|
@@ -48,7 +56,7 @@ function isUnavailable(directive) {
|
|
|
48
56
|
|
|
49
57
|
/**
|
|
50
58
|
* Returns true if any of provided directives blocks page from being indexed
|
|
51
|
-
* @param {string} directives
|
|
59
|
+
* @param {string} directives assumes no user-agent prefix
|
|
52
60
|
* @return {boolean}
|
|
53
61
|
*/
|
|
54
62
|
function hasBlockingDirective(directives) {
|
|
@@ -58,16 +66,18 @@ function hasBlockingDirective(directives) {
|
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
/**
|
|
61
|
-
* Returns
|
|
69
|
+
* Returns user agent if specified in robots header (e.g. `googlebot: noindex`)
|
|
62
70
|
* @param {string} directives
|
|
63
|
-
* @return {
|
|
71
|
+
* @return {string|undefined}
|
|
64
72
|
*/
|
|
65
|
-
function
|
|
73
|
+
function getUserAgentFromHeaderDirectives(directives) {
|
|
66
74
|
const parts = directives.match(/^([^,:]+):/);
|
|
67
75
|
|
|
68
76
|
// Check if directives are prefixed with `googlebot:`, `googlebot-news:`, `otherbot:`, etc.
|
|
69
77
|
// but ignore `unavailable_after:` which is a valid directive
|
|
70
|
-
|
|
78
|
+
if (!!parts && parts[1].toLowerCase() !== UNAVAILABLE_AFTER) {
|
|
79
|
+
return parts[1];
|
|
80
|
+
}
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
class IsCrawlable extends Audit {
|
|
@@ -85,6 +95,74 @@ class IsCrawlable extends Audit {
|
|
|
85
95
|
};
|
|
86
96
|
}
|
|
87
97
|
|
|
98
|
+
/**
|
|
99
|
+
* @param {LH.Artifacts.MetaElement} metaElement
|
|
100
|
+
*/
|
|
101
|
+
static handleMetaElement(metaElement) {
|
|
102
|
+
const content = metaElement.content || '';
|
|
103
|
+
if (hasBlockingDirective(content)) {
|
|
104
|
+
return {
|
|
105
|
+
source: {
|
|
106
|
+
...Audit.makeNodeItem(metaElement.node),
|
|
107
|
+
snippet: `<meta name="${metaElement.name}" content="${content}" />`,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string|undefined} userAgent
|
|
115
|
+
* @param {LH.Artifacts.NetworkRequest} mainResource
|
|
116
|
+
* @param {LH.Artifacts.MetaElement[]} metaElements
|
|
117
|
+
* @param {import('robots-parser').Robot|undefined} parsedRobotsTxt
|
|
118
|
+
* @param {URL} robotsTxtUrl
|
|
119
|
+
*/
|
|
120
|
+
static determineIfCrawlableForUserAgent(userAgent, mainResource, metaElements,
|
|
121
|
+
parsedRobotsTxt, robotsTxtUrl) {
|
|
122
|
+
/** @type {LH.Audit.Details.Table['items']} */
|
|
123
|
+
const blockingDirectives = [];
|
|
124
|
+
|
|
125
|
+
// Prefer a meta element specific to a user agent, fallback to generic 'robots' if not present.
|
|
126
|
+
// https://developers.google.com/search/blog/2007/03/using-robots-meta-tag#directing-a-robots-meta-tag-specifically-at-googlebot
|
|
127
|
+
let meta;
|
|
128
|
+
if (userAgent) meta = metaElements.find(meta => meta.name === userAgent.toLowerCase());
|
|
129
|
+
if (!meta) meta = metaElements.find(meta => meta.name === 'robots');
|
|
130
|
+
if (meta) {
|
|
131
|
+
const blockingDirective = IsCrawlable.handleMetaElement(meta);
|
|
132
|
+
if (blockingDirective) blockingDirectives.push(blockingDirective);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const header of mainResource.responseHeaders || []) {
|
|
136
|
+
if (header.name.toLowerCase() !== ROBOTS_HEADER) continue;
|
|
137
|
+
|
|
138
|
+
const directiveUserAgent = getUserAgentFromHeaderDirectives(header.value);
|
|
139
|
+
if (directiveUserAgent !== userAgent && directiveUserAgent !== undefined) continue;
|
|
140
|
+
|
|
141
|
+
let directiveWithoutUserAgentPrefix = header.value.trim();
|
|
142
|
+
if (userAgent && header.value.startsWith(`${userAgent}:`)) {
|
|
143
|
+
directiveWithoutUserAgentPrefix = header.value.replace(`${userAgent}:`, '');
|
|
144
|
+
}
|
|
145
|
+
if (!hasBlockingDirective(directiveWithoutUserAgentPrefix)) continue;
|
|
146
|
+
|
|
147
|
+
blockingDirectives.push({source: `${header.name}: ${header.value}`});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (parsedRobotsTxt && !parsedRobotsTxt.isAllowed(mainResource.url, userAgent)) {
|
|
151
|
+
const line = parsedRobotsTxt.getMatchingLineNumber(mainResource.url) || 1;
|
|
152
|
+
blockingDirectives.push({
|
|
153
|
+
source: {
|
|
154
|
+
type: /** @type {const} */ ('source-location'),
|
|
155
|
+
url: robotsTxtUrl.href,
|
|
156
|
+
urlProvider: /** @type {const} */ ('network'),
|
|
157
|
+
line: line - 1,
|
|
158
|
+
column: 0,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return blockingDirectives;
|
|
164
|
+
}
|
|
165
|
+
|
|
88
166
|
/**
|
|
89
167
|
* @param {LH.Artifacts} artifacts
|
|
90
168
|
* @param {LH.Audit.Context} context
|
|
@@ -92,57 +170,51 @@ class IsCrawlable extends Audit {
|
|
|
92
170
|
*/
|
|
93
171
|
static async audit(artifacts, context) {
|
|
94
172
|
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
95
|
-
const metaRobots = artifacts.MetaElements.find(meta => meta.name === 'robots');
|
|
96
173
|
const mainResource = await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
|
|
97
|
-
/** @type {LH.Audit.Details.Table['items']} */
|
|
98
|
-
const blockingDirectives = [];
|
|
99
174
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
175
|
+
const robotsTxtUrl = new URL('/robots.txt', mainResource.url);
|
|
176
|
+
const parsedRobotsTxt = artifacts.RobotsTxt.content ?
|
|
177
|
+
robotsParser(robotsTxtUrl.href, artifacts.RobotsTxt.content) :
|
|
178
|
+
undefined;
|
|
179
|
+
|
|
180
|
+
// Only fail if all known bots and generic bots (UserAgent '*' or 'robots' directive)
|
|
181
|
+
// are blocked from crawling.
|
|
182
|
+
// If at least one bot is allowed, we pass the audit. Any known bots that are not allowed
|
|
183
|
+
// will be listed in a warning.
|
|
184
|
+
|
|
185
|
+
/** @type {Array<string|undefined>} */
|
|
186
|
+
const blockedUserAgents = [];
|
|
187
|
+
const genericBlockingDirectives = [];
|
|
188
|
+
|
|
189
|
+
for (const userAgent of BOT_USER_AGENTS) {
|
|
190
|
+
const blockingDirectives = IsCrawlable.determineIfCrawlableForUserAgent(
|
|
191
|
+
userAgent, mainResource, artifacts.MetaElements, parsedRobotsTxt, robotsTxtUrl);
|
|
192
|
+
if (blockingDirectives.length > 0) {
|
|
193
|
+
blockedUserAgents.push(userAgent);
|
|
194
|
+
}
|
|
195
|
+
if (userAgent === undefined) {
|
|
196
|
+
genericBlockingDirectives.push(...blockingDirectives);
|
|
111
197
|
}
|
|
112
198
|
}
|
|
113
199
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const robotsFileUrl = new URL('/robots.txt', mainResource.url);
|
|
121
|
-
const robotsTxt = robotsParser(robotsFileUrl.href, artifacts.RobotsTxt.content);
|
|
122
|
-
|
|
123
|
-
if (!robotsTxt.isAllowed(mainResource.url)) {
|
|
124
|
-
const line = robotsTxt.getMatchingLineNumber(mainResource.url) || 1;
|
|
125
|
-
blockingDirectives.push({
|
|
126
|
-
source: {
|
|
127
|
-
type: /** @type {const} */ ('source-location'),
|
|
128
|
-
url: robotsFileUrl.href,
|
|
129
|
-
urlProvider: /** @type {const} */ ('network'),
|
|
130
|
-
line: line - 1,
|
|
131
|
-
column: 0,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
}
|
|
200
|
+
const score = blockedUserAgents.length === BOT_USER_AGENTS.size ? 0 : 1;
|
|
201
|
+
const warnings = [];
|
|
202
|
+
if (score && blockedUserAgents.length > 0) {
|
|
203
|
+
const list = blockedUserAgents.filter(Boolean).join(', ');
|
|
204
|
+
// eslint-disable-next-line max-len
|
|
205
|
+
warnings.push(`The following bot user agents are blocked from crawling: ${list}. The audit is otherwise passing, because at least one bot was explicitly allowed.`);
|
|
135
206
|
}
|
|
136
207
|
|
|
137
208
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
138
209
|
const headings = [
|
|
139
210
|
{key: 'source', valueType: 'code', label: 'Blocking Directive Source'},
|
|
140
211
|
];
|
|
141
|
-
const details = Audit.makeTableDetails(headings,
|
|
212
|
+
const details = Audit.makeTableDetails(headings, score === 0 ? genericBlockingDirectives : []);
|
|
142
213
|
|
|
143
214
|
return {
|
|
144
|
-
score
|
|
215
|
+
score,
|
|
145
216
|
details,
|
|
217
|
+
warnings,
|
|
146
218
|
};
|
|
147
219
|
}
|
|
148
220
|
}
|
|
@@ -364,18 +364,18 @@ function resolveSettings(settingsJson = {}, overrides = undefined) {
|
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
/**
|
|
367
|
-
* @param {LH.Config
|
|
367
|
+
* @param {LH.Config} config
|
|
368
368
|
* @param {string | undefined} configDir
|
|
369
369
|
* @param {{plugins?: string[]} | undefined} flags
|
|
370
|
-
* @return {Promise<LH.Config
|
|
370
|
+
* @return {Promise<LH.Config>}
|
|
371
371
|
*/
|
|
372
|
-
async function mergePlugins(
|
|
373
|
-
const configPlugins =
|
|
372
|
+
async function mergePlugins(config, configDir, flags) {
|
|
373
|
+
const configPlugins = config.plugins || [];
|
|
374
374
|
const flagPlugins = flags?.plugins || [];
|
|
375
375
|
const pluginNames = new Set([...configPlugins, ...flagPlugins]);
|
|
376
376
|
|
|
377
377
|
for (const pluginName of pluginNames) {
|
|
378
|
-
validation.assertValidPluginName(
|
|
378
|
+
validation.assertValidPluginName(config, pluginName);
|
|
379
379
|
|
|
380
380
|
// In bundled contexts, `resolveModulePath` will fail, so use the raw pluginName directly.
|
|
381
381
|
const pluginPath = isBundledEnvironment() ?
|
|
@@ -384,10 +384,10 @@ async function mergePlugins(configJSON, configDir, flags) {
|
|
|
384
384
|
const rawPluginJson = await requireWrapper(pluginPath);
|
|
385
385
|
const pluginJson = ConfigPlugin.parsePlugin(rawPluginJson, pluginName);
|
|
386
386
|
|
|
387
|
-
|
|
387
|
+
config = mergeConfigFragment(config, pluginJson);
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
return
|
|
390
|
+
return config;
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
|
|
@@ -428,7 +428,7 @@ async function resolveGathererToDefn(gathererJson, coreGathererList, configDir)
|
|
|
428
428
|
* Take an array of audits and audit paths and require any paths (possibly
|
|
429
429
|
* relative to the optional `configDir`) using `resolveModule`,
|
|
430
430
|
* leaving only an array of AuditDefns.
|
|
431
|
-
* @param {LH.Config
|
|
431
|
+
* @param {LH.Config['audits']} audits
|
|
432
432
|
* @param {string=} configDir
|
|
433
433
|
* @return {Promise<Array<LH.Config.AuditDefn>|null>}
|
|
434
434
|
*/
|
|
@@ -585,10 +585,10 @@ function deepClone(json) {
|
|
|
585
585
|
}
|
|
586
586
|
|
|
587
587
|
/**
|
|
588
|
-
* Deep clone a
|
|
588
|
+
* Deep clone a config, copying over any "live" gatherer or audit that
|
|
589
589
|
* wouldn't make the JSON round trip.
|
|
590
|
-
* @param {LH.Config
|
|
591
|
-
* @return {LH.Config
|
|
590
|
+
* @param {LH.Config} json
|
|
591
|
+
* @return {LH.Config}
|
|
592
592
|
*/
|
|
593
593
|
function deepCloneConfigJson(json) {
|
|
594
594
|
const cloned = deepClone(json);
|
|
@@ -215,11 +215,11 @@ class ConfigPlugin {
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
/**
|
|
218
|
-
* Extracts and validates a
|
|
218
|
+
* Extracts and validates a config from the provided plugin input, throwing
|
|
219
219
|
* if it deviates from the expected object shape.
|
|
220
220
|
* @param {unknown} pluginJson
|
|
221
221
|
* @param {string} pluginName
|
|
222
|
-
* @return {LH.Config
|
|
222
|
+
* @return {LH.Config}
|
|
223
223
|
*/
|
|
224
224
|
static parsePlugin(pluginJson, pluginName) {
|
|
225
225
|
// Clone to prevent modifications of original and to deactivate any live properties.
|
package/core/config/config.js
CHANGED
|
@@ -38,19 +38,19 @@ const defaultConfigPath = path.join(
|
|
|
38
38
|
);
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* @param {LH.Config
|
|
41
|
+
* @param {LH.Config|undefined} config
|
|
42
42
|
* @param {{configPath?: string}} context
|
|
43
|
-
* @return {{configWorkingCopy: LH.Config
|
|
43
|
+
* @return {{configWorkingCopy: LH.Config, configDir?: string, configPath?: string}}
|
|
44
44
|
*/
|
|
45
|
-
function resolveWorkingCopy(
|
|
45
|
+
function resolveWorkingCopy(config, context) {
|
|
46
46
|
let {configPath} = context;
|
|
47
47
|
|
|
48
48
|
if (configPath && !path.isAbsolute(configPath)) {
|
|
49
49
|
throw new Error('configPath must be an absolute path');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
if (!
|
|
53
|
-
|
|
52
|
+
if (!config) {
|
|
53
|
+
config = defaultConfig;
|
|
54
54
|
configPath = defaultConfigPath;
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -58,24 +58,24 @@ function resolveWorkingCopy(configJSON, context) {
|
|
|
58
58
|
const configDir = configPath ? path.dirname(configPath) : undefined;
|
|
59
59
|
|
|
60
60
|
return {
|
|
61
|
-
configWorkingCopy: deepCloneConfigJson(
|
|
61
|
+
configWorkingCopy: deepCloneConfigJson(config),
|
|
62
62
|
configPath,
|
|
63
63
|
configDir,
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* @param {LH.Config
|
|
69
|
-
* @return {LH.Config
|
|
68
|
+
* @param {LH.Config} config
|
|
69
|
+
* @return {LH.Config}
|
|
70
70
|
*/
|
|
71
|
-
function resolveExtensions(
|
|
72
|
-
if (!
|
|
71
|
+
function resolveExtensions(config) {
|
|
72
|
+
if (!config.extends) return config;
|
|
73
73
|
|
|
74
|
-
if (
|
|
74
|
+
if (config.extends !== 'lighthouse:default') {
|
|
75
75
|
throw new Error('`lighthouse:default` is the only valid extension method.');
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
const {artifacts, ...extensionJSON} =
|
|
78
|
+
const {artifacts, ...extensionJSON} = config;
|
|
79
79
|
const defaultClone = deepCloneConfigJson(defaultConfig);
|
|
80
80
|
const mergedConfig = mergeConfigFragment(defaultClone, extensionJSON);
|
|
81
81
|
|
|
@@ -236,15 +236,15 @@ function resolveFakeNavigations(artifactDefns, settings) {
|
|
|
236
236
|
|
|
237
237
|
/**
|
|
238
238
|
* @param {LH.Gatherer.GatherMode} gatherMode
|
|
239
|
-
* @param {LH.Config
|
|
239
|
+
* @param {LH.Config=} config
|
|
240
240
|
* @param {LH.Flags=} flags
|
|
241
241
|
* @return {Promise<{resolvedConfig: LH.Config.ResolvedConfig, warnings: string[]}>}
|
|
242
242
|
*/
|
|
243
|
-
async function initializeConfig(gatherMode,
|
|
243
|
+
async function initializeConfig(gatherMode, config, flags = {}) {
|
|
244
244
|
const status = {msg: 'Initialize config', id: 'lh:config'};
|
|
245
245
|
log.time(status, 'verbose');
|
|
246
246
|
|
|
247
|
-
let {configWorkingCopy, configDir} = resolveWorkingCopy(
|
|
247
|
+
let {configWorkingCopy, configDir} = resolveWorkingCopy(config, flags);
|
|
248
248
|
|
|
249
249
|
configWorkingCopy = resolveExtensions(configWorkingCopy);
|
|
250
250
|
configWorkingCopy = await mergePlugins(configWorkingCopy, configDir, flags);
|
|
@@ -170,7 +170,7 @@ for (const key of Object.keys(artifacts)) {
|
|
|
170
170
|
artifacts[/** @type {keyof typeof artifacts} */ (key)] = key;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
/** @type {LH.Config
|
|
173
|
+
/** @type {LH.Config} */
|
|
174
174
|
const defaultConfig = {
|
|
175
175
|
settings: constants.defaultSettings,
|
|
176
176
|
artifacts: [
|
|
@@ -217,8 +217,11 @@ const defaultConfig = {
|
|
|
217
217
|
{id: artifacts.devtoolsLogs, gatherer: 'devtools-log-compat'},
|
|
218
218
|
{id: artifacts.traces, gatherer: 'trace-compat'},
|
|
219
219
|
|
|
220
|
-
// FullPageScreenshot comes at the
|
|
220
|
+
// FullPageScreenshot comes at the end so all other node analysis is captured.
|
|
221
221
|
{id: artifacts.FullPageScreenshot, gatherer: 'full-page-screenshot'},
|
|
222
|
+
|
|
223
|
+
// BFCacheErrors comes at the very end because it can perform a page navigation.
|
|
224
|
+
{id: artifacts.BFCacheFailures, gatherer: 'bf-cache-failures'},
|
|
222
225
|
],
|
|
223
226
|
audits: [
|
|
224
227
|
'is-on-https',
|
|
@@ -374,6 +377,7 @@ const defaultConfig = {
|
|
|
374
377
|
'seo/canonical',
|
|
375
378
|
'seo/manual/structured-data',
|
|
376
379
|
'work-during-interaction',
|
|
380
|
+
'bf-cache',
|
|
377
381
|
],
|
|
378
382
|
groups: {
|
|
379
383
|
'metrics': {
|
|
@@ -513,9 +517,9 @@ const defaultConfig = {
|
|
|
513
517
|
{id: 'non-composited-animations', weight: 0},
|
|
514
518
|
{id: 'unsized-images', weight: 0},
|
|
515
519
|
{id: 'viewport', weight: 0},
|
|
516
|
-
{id: 'no-unload-listeners', weight: 0},
|
|
517
520
|
{id: 'uses-responsive-images-snapshot', weight: 0},
|
|
518
521
|
{id: 'work-during-interaction', weight: 0},
|
|
522
|
+
{id: 'bf-cache', weight: 0},
|
|
519
523
|
|
|
520
524
|
// Budget audits.
|
|
521
525
|
{id: 'performance-budget', weight: 0, group: 'budgets'},
|
|
@@ -618,6 +622,7 @@ const defaultConfig = {
|
|
|
618
622
|
{id: 'doctype', weight: 1, group: 'best-practices-browser-compat'},
|
|
619
623
|
{id: 'charset', weight: 1, group: 'best-practices-browser-compat'},
|
|
620
624
|
// General Group
|
|
625
|
+
{id: 'no-unload-listeners', weight: 1, group: 'best-practices-general'},
|
|
621
626
|
{id: 'js-libraries', weight: 0, group: 'best-practices-general'},
|
|
622
627
|
{id: 'deprecations', weight: 1, group: 'best-practices-general'},
|
|
623
628
|
{id: 'errors-in-console', weight: 1, group: 'best-practices-general'},
|
|
@@ -4,7 +4,7 @@
|
|
|
4
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
5
|
*/
|
|
6
6
|
|
|
7
|
-
/** @type {LH.Config
|
|
7
|
+
/** @type {LH.Config} */
|
|
8
8
|
const fullConfig = {
|
|
9
9
|
extends: 'lighthouse:default',
|
|
10
10
|
settings: {},
|
|
@@ -4,7 +4,7 @@
|
|
|
4
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
5
|
*/
|
|
6
6
|
|
|
7
|
-
/** @type {LH.Config
|
|
7
|
+
/** @type {LH.Config} */
|
|
8
8
|
const config = {
|
|
9
9
|
extends: 'lighthouse:default',
|
|
10
10
|
settings: {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
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
5
|
*/
|
|
6
6
|
|
|
7
|
-
/** @type {LH.Config
|
|
7
|
+
/** @type {LH.Config} */
|
|
8
8
|
const perfConfig = {
|
|
9
9
|
extends: 'lighthouse:default',
|
|
10
10
|
settings: {
|
|
@@ -39,16 +39,16 @@ function isValidArtifactDependency(dependent, dependency) {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Throws if pluginName is invalid or (somehow) collides with a category in the
|
|
42
|
-
*
|
|
43
|
-
* @param {LH.Config
|
|
42
|
+
* config being added to.
|
|
43
|
+
* @param {LH.Config} config
|
|
44
44
|
* @param {string} pluginName
|
|
45
45
|
*/
|
|
46
|
-
function assertValidPluginName(
|
|
46
|
+
function assertValidPluginName(config, pluginName) {
|
|
47
47
|
if (!pluginName.startsWith('lighthouse-plugin-')) {
|
|
48
48
|
throw new Error(`plugin name '${pluginName}' does not start with 'lighthouse-plugin-'`);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
if (
|
|
51
|
+
if (config.categories?.[pluginName]) {
|
|
52
52
|
throw new Error(`plugin name '${pluginName}' not allowed because it is the id of a category already found in config`); // eslint-disable-line max-len
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -61,8 +61,8 @@ class ExecutionContext {
|
|
|
61
61
|
await this._session.sendCommand('Page.enable');
|
|
62
62
|
await this._session.sendCommand('Runtime.enable');
|
|
63
63
|
|
|
64
|
-
const
|
|
65
|
-
const mainFrameId =
|
|
64
|
+
const frameTreeResponse = await this._session.sendCommand('Page.getFrameTree');
|
|
65
|
+
const mainFrameId = frameTreeResponse.frameTree.frame.id;
|
|
66
66
|
|
|
67
67
|
const isolatedWorldResponse = await this._session.sendCommand('Page.createIsolatedWorld', {
|
|
68
68
|
frameId: mainFrameId,
|
|
@@ -95,8 +95,8 @@ class NetworkMonitor extends NetworkMonitorEventEmitter {
|
|
|
95
95
|
const frameNavigations = this._frameNavigations;
|
|
96
96
|
if (!frameNavigations.length) return {};
|
|
97
97
|
|
|
98
|
-
const
|
|
99
|
-
const mainFrameId =
|
|
98
|
+
const frameTreeResponse = await this._session.sendCommand('Page.getFrameTree');
|
|
99
|
+
const mainFrameId = frameTreeResponse.frameTree.frame.id;
|
|
100
100
|
const mainFrameNavigations = frameNavigations.filter(frame => frame.id === mainFrameId);
|
|
101
101
|
if (!mainFrameNavigations.length) log.warn('NetworkMonitor', 'No detected navigations');
|
|
102
102
|
|
|
@@ -13,7 +13,13 @@ import {ExecutionContext} from './execution-context.js';
|
|
|
13
13
|
|
|
14
14
|
/** @typedef {InstanceType<import('./network-monitor.js')['NetworkMonitor']>} NetworkMonitor */
|
|
15
15
|
/** @typedef {import('./network-monitor.js').NetworkMonitorEvent} NetworkMonitorEvent */
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @template [T=void]
|
|
19
|
+
* @typedef CancellableWait
|
|
20
|
+
* @prop {Promise<T>} promise
|
|
21
|
+
* @prop {() => void} cancel
|
|
22
|
+
*/
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
25
|
* @typedef WaitOptions
|
|
@@ -40,7 +46,7 @@ function waitForNothing() {
|
|
|
40
46
|
* Returns a promise that resolve when a frame has been navigated.
|
|
41
47
|
* Used for detecting that our about:blank reset has been completed.
|
|
42
48
|
* @param {LH.Gatherer.FRProtocolSession} session
|
|
43
|
-
* @return {CancellableWait}
|
|
49
|
+
* @return {CancellableWait<LH.Crdp.Page.FrameNavigatedEvent>}
|
|
44
50
|
*/
|
|
45
51
|
function waitForFrameNavigated(session) {
|
|
46
52
|
/** @type {(() => void)} */
|
|
@@ -48,6 +54,7 @@ function waitForFrameNavigated(session) {
|
|
|
48
54
|
throw new Error('waitForFrameNavigated.cancel() called before it was defined');
|
|
49
55
|
};
|
|
50
56
|
|
|
57
|
+
/** @type {Promise<LH.Crdp.Page.FrameNavigatedEvent>} */
|
|
51
58
|
const promise = new Promise((resolve, reject) => {
|
|
52
59
|
session.once('Page.frameNavigated', resolve);
|
|
53
60
|
cancel = () => {
|
|
@@ -8,6 +8,8 @@ import FRGatherer from '../base-gatherer.js';
|
|
|
8
8
|
import {waitForFrameNavigated, waitForLoadEvent} from '../driver/wait-for-condition.js';
|
|
9
9
|
import DevtoolsLog from './devtools-log.js';
|
|
10
10
|
|
|
11
|
+
const FAILURE_EVENT_TIMEOUT = 100;
|
|
12
|
+
|
|
11
13
|
class BFCacheFailures extends FRGatherer {
|
|
12
14
|
/** @type {LH.Gatherer.GathererMeta<'DevtoolsLog'>} */
|
|
13
15
|
meta = {
|
|
@@ -101,16 +103,32 @@ class BFCacheFailures extends FRGatherer {
|
|
|
101
103
|
const history = await session.sendCommand('Page.getNavigationHistory');
|
|
102
104
|
const entry = history.entries[history.currentIndex];
|
|
103
105
|
|
|
106
|
+
// In theory, we should be able to use about:blank here
|
|
107
|
+
// but that sometimes produces BrowsingInstanceNotSwapped failures.
|
|
108
|
+
// DevTools uses chrome://terms as it's temporary page so we should stick with that.
|
|
109
|
+
// https://github.com/GoogleChrome/lighthouse/issues/14665
|
|
104
110
|
await Promise.all([
|
|
105
|
-
session.sendCommand('Page.navigate', {url: '
|
|
111
|
+
session.sendCommand('Page.navigate', {url: 'chrome://terms'}),
|
|
106
112
|
waitForLoadEvent(session, 0).promise,
|
|
107
113
|
]);
|
|
108
114
|
|
|
109
|
-
await Promise.all([
|
|
115
|
+
const [, frameNavigatedEvent] = await Promise.all([
|
|
110
116
|
session.sendCommand('Page.navigateToHistoryEntry', {entryId: entry.id}),
|
|
111
117
|
waitForFrameNavigated(session).promise,
|
|
112
118
|
]);
|
|
113
119
|
|
|
120
|
+
// The bfcache failure event is not necessarily emitted by this point.
|
|
121
|
+
// If we are expecting a bfcache failure event but haven't seen one, we should wait for it.
|
|
122
|
+
if (frameNavigatedEvent.type !== 'BackForwardCacheRestore' && !bfCacheEvent) {
|
|
123
|
+
await new Promise(resolve => setTimeout(resolve, FAILURE_EVENT_TIMEOUT));
|
|
124
|
+
|
|
125
|
+
// If we still can't get the failure reasons after the timeout we should fail loudly,
|
|
126
|
+
// otherwise this gatherer will return no failures when there should be failures.
|
|
127
|
+
if (!bfCacheEvent) {
|
|
128
|
+
throw new Error('bfcache failed but the failure reasons were not emitted in time');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
114
132
|
session.off('Page.backForwardCacheNotUsed', onBfCacheNotUsed);
|
|
115
133
|
|
|
116
134
|
return bfCacheEvent;
|
|
@@ -136,7 +154,7 @@ class BFCacheFailures extends FRGatherer {
|
|
|
136
154
|
*/
|
|
137
155
|
async getArtifact(context) {
|
|
138
156
|
const events = this.passivelyCollectBFCacheEvents(context);
|
|
139
|
-
if (context.gatherMode === 'navigation') {
|
|
157
|
+
if (context.gatherMode === 'navigation' && !context.settings.usePassiveGathering) {
|
|
140
158
|
const activelyCollectedEvent = await this.activelyCollectBFCacheEvent(context);
|
|
141
159
|
if (activelyCollectedEvent) events.push(activelyCollectedEvent);
|
|
142
160
|
}
|
|
@@ -311,7 +311,7 @@ async function _cleanup({requestedUrl, driver, resolvedConfig}) {
|
|
|
311
311
|
/**
|
|
312
312
|
* @param {LH.Puppeteer.Page|undefined} page
|
|
313
313
|
* @param {LH.NavigationRequestor|undefined} requestor
|
|
314
|
-
* @param {{config?: LH.Config
|
|
314
|
+
* @param {{config?: LH.Config, flags?: LH.Flags}} [options]
|
|
315
315
|
* @return {Promise<LH.Gatherer.FRGatherResult>}
|
|
316
316
|
*/
|
|
317
317
|
async function navigationGather(page, requestor, options = {}) {
|
|
@@ -14,7 +14,7 @@ import {getBaseArtifacts, finalizeArtifacts} from './base-artifacts.js';
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @param {LH.Puppeteer.Page} page
|
|
17
|
-
* @param {{config?: LH.Config
|
|
17
|
+
* @param {{config?: LH.Config, flags?: LH.Flags}} [options]
|
|
18
18
|
* @return {Promise<LH.Gatherer.FRGatherResult>}
|
|
19
19
|
*/
|
|
20
20
|
async function snapshotGather(page, options = {}) {
|
|
@@ -15,7 +15,7 @@ import {getBaseArtifacts, finalizeArtifacts} from './base-artifacts.js';
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* @param {LH.Puppeteer.Page} page
|
|
18
|
-
* @param {{config?: LH.Config
|
|
18
|
+
* @param {{config?: LH.Config, flags?: LH.Flags}} [options]
|
|
19
19
|
* @return {Promise<{endTimespanGather(): Promise<LH.Gatherer.FRGatherResult>}>}
|
|
20
20
|
*/
|
|
21
21
|
async function startTimespanGather(page, options = {}) {
|