lighthouse 9.5.0-dev.20220608 → 9.5.0-dev.20220611

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/esm-utils.mjs ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @license Copyright 2021 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
+ // TODO(esmodules): rename to .js when root is type: module
8
+
9
+ import module from 'module';
10
+ import url from 'url';
11
+ import path from 'path';
12
+
13
+ /**
14
+ * Commonjs equivalent of `require.resolve`.
15
+ * @param {string} packageName
16
+ */
17
+ function resolveModulePath(packageName) {
18
+ const require = module.createRequire(import.meta.url);
19
+ return require.resolve(packageName);
20
+ }
21
+
22
+ /**
23
+ * @param {ImportMeta} importMeta
24
+ */
25
+ function getModulePath(importMeta) {
26
+ return url.fileURLToPath(importMeta.url);
27
+ }
28
+
29
+ /**
30
+ * @param {ImportMeta} importMeta
31
+ */
32
+ function getModuleDirectory(importMeta) {
33
+ return path.dirname(getModulePath(importMeta));
34
+ }
35
+
36
+ export {
37
+ resolveModulePath,
38
+ getModulePath,
39
+ getModuleDirectory,
40
+ };
@@ -120,7 +120,7 @@ async function begin() {
120
120
  }
121
121
 
122
122
  if (cliFlags.printConfig) {
123
- const config = lighthouse.generateConfig(configJson, cliFlags);
123
+ const config = await lighthouse.generateConfig(configJson, cliFlags);
124
124
  process.stdout.write(config.getPrintString());
125
125
  return;
126
126
  }
@@ -7,7 +7,4 @@
7
7
 
8
8
  import {begin} from './bin.js';
9
9
 
10
- begin().catch(err => {
11
- process.stderr.write(err.stack);
12
- process.exit(1);
13
- });
10
+ await begin();
@@ -255,7 +255,4 @@ async function begin() {
255
255
  process.exit(exitCode);
256
256
  }
257
257
 
258
- begin().catch(e => {
259
- console.error(e);
260
- process.exit(1);
261
- });
258
+ await begin();
@@ -211,7 +211,8 @@ const bundledModules = new Map(/* BUILD_REPLACE_BUNDLED_MODULES */);
211
211
  * See build-bundle.js
212
212
  * @param {string} requirePath
213
213
  */
214
- function requireWrapper(requirePath) {
214
+ async function requireWrapper(requirePath) {
215
+ // This is async because eventually this function needs to do async dynamic imports.
215
216
  return bundledModules.get(requirePath) || require(requirePath);
216
217
  }
217
218
 
@@ -219,9 +220,9 @@ function requireWrapper(requirePath) {
219
220
  * @param {string} gathererPath
220
221
  * @param {Array<string>} coreGathererList
221
222
  * @param {string=} configDir
222
- * @return {LH.Config.GathererDefn}
223
+ * @return {Promise<LH.Config.GathererDefn>}
223
224
  */
224
- function requireGatherer(gathererPath, coreGathererList, configDir) {
225
+ async function requireGatherer(gathererPath, coreGathererList, configDir) {
225
226
  const coreGatherer = coreGathererList.find(a => a === `${gathererPath}.js`);
226
227
 
227
228
  let requirePath = `../gather/gatherers/${gathererPath}`;
@@ -230,7 +231,7 @@ function requireGatherer(gathererPath, coreGathererList, configDir) {
230
231
  requirePath = resolveModulePath(gathererPath, configDir, 'gatherer');
231
232
  }
232
233
 
233
- const GathererClass = /** @type {GathererConstructor} */ (requireWrapper(requirePath));
234
+ const GathererClass = /** @type {GathererConstructor} */ (await requireWrapper(requirePath));
234
235
 
235
236
  return {
236
237
  instance: new GathererClass(),
@@ -243,7 +244,7 @@ function requireGatherer(gathererPath, coreGathererList, configDir) {
243
244
  * @param {string} auditPath
244
245
  * @param {Array<string>} coreAuditList
245
246
  * @param {string=} configDir
246
- * @return {LH.Config.AuditDefn['implementation']}
247
+ * @return {Promise<LH.Config.AuditDefn['implementation']>}
247
248
  */
248
249
  function requireAudit(auditPath, coreAuditList, configDir) {
249
250
  // See if the audit is a Lighthouse core audit.
@@ -328,9 +329,9 @@ function resolveSettings(settingsJson = {}, overrides = undefined) {
328
329
  * @param {LH.Config.Json} configJSON
329
330
  * @param {string | undefined} configDir
330
331
  * @param {{plugins?: string[]} | undefined} flags
331
- * @return {LH.Config.Json}
332
+ * @return {Promise<LH.Config.Json>}
332
333
  */
333
- function mergePlugins(configJSON, configDir, flags) {
334
+ async function mergePlugins(configJSON, configDir, flags) {
334
335
  const configPlugins = configJSON.plugins || [];
335
336
  const flagPlugins = flags?.plugins || [];
336
337
  const pluginNames = new Set([...configPlugins, ...flagPlugins]);
@@ -342,7 +343,7 @@ function mergePlugins(configJSON, configDir, flags) {
342
343
  const pluginPath = isBundledEnvironment() ?
343
344
  pluginName :
344
345
  resolveModulePath(pluginName, configDir, 'plugin');
345
- const rawPluginJson = requireWrapper(pluginPath);
346
+ const rawPluginJson = await requireWrapper(pluginPath);
346
347
  const pluginJson = ConfigPlugin.parsePlugin(rawPluginJson, pluginName);
347
348
 
348
349
  configJSON = mergeConfigFragment(configJSON, pluginJson);
@@ -360,9 +361,9 @@ function mergePlugins(configJSON, configDir, flags) {
360
361
  * @param {LH.Config.GathererJson} gathererJson
361
362
  * @param {Array<string>} coreGathererList
362
363
  * @param {string=} configDir
363
- * @return {LH.Config.GathererDefn}
364
+ * @return {Promise<LH.Config.GathererDefn>}
364
365
  */
365
- function resolveGathererToDefn(gathererJson, coreGathererList, configDir) {
366
+ async function resolveGathererToDefn(gathererJson, coreGathererList, configDir) {
366
367
  const gathererDefn = expandGathererShorthand(gathererJson);
367
368
  if (gathererDefn.instance) {
368
369
  return {
@@ -391,21 +392,21 @@ function resolveGathererToDefn(gathererJson, coreGathererList, configDir) {
391
392
  * leaving only an array of AuditDefns.
392
393
  * @param {LH.Config.Json['audits']} audits
393
394
  * @param {string=} configDir
394
- * @return {Array<LH.Config.AuditDefn>|null}
395
+ * @return {Promise<Array<LH.Config.AuditDefn>|null>}
395
396
  */
396
- function resolveAuditsToDefns(audits, configDir) {
397
+ async function resolveAuditsToDefns(audits, configDir) {
397
398
  if (!audits) {
398
399
  return null;
399
400
  }
400
401
 
401
402
  const coreList = Runner.getAuditList();
402
- const auditDefns = audits.map(auditJson => {
403
+ const auditDefnsPromises = audits.map(async (auditJson) => {
403
404
  const auditDefn = expandAuditShorthand(auditJson);
404
405
  let implementation;
405
406
  if ('implementation' in auditDefn) {
406
407
  implementation = auditDefn.implementation;
407
408
  } else {
408
- implementation = requireAudit(auditDefn.path, coreList, configDir);
409
+ implementation = await requireAudit(auditDefn.path, coreList, configDir);
409
410
  }
410
411
 
411
412
  return {
@@ -414,6 +415,7 @@ function resolveAuditsToDefns(audits, configDir) {
414
415
  options: auditDefn.options || {},
415
416
  };
416
417
  });
418
+ const auditDefns = await Promise.all(auditDefnsPromises);
417
419
 
418
420
  const mergedAuditDefns = mergeOptionsOfItems(auditDefns);
419
421
  mergedAuditDefns.forEach(audit => validation.assertValidAudit(audit));
@@ -149,17 +149,18 @@ function assertValidFlags(flags) {
149
149
  }
150
150
  }
151
151
 
152
-
153
152
  /**
154
153
  * @implements {LH.Config.Config}
155
154
  */
156
155
  class Config {
157
156
  /**
158
- * @constructor
159
- * @param {LH.Config.Json=} configJSON
157
+ * Resolves the provided config (inherits from extended config, if set), resolves
158
+ * all referenced modules, and validates.
159
+ * @param {LH.Config.Json=} configJSON If not provided, uses the default config.
160
160
  * @param {LH.Flags=} flags
161
+ * @return {Promise<Config>}
161
162
  */
162
- constructor(configJSON, flags) {
163
+ static async fromJson(configJSON, flags) {
163
164
  const status = {msg: 'Create config', id: 'lh:init:config'};
164
165
  log.time(status, 'verbose');
165
166
  let configPath = flags?.configPath;
@@ -188,7 +189,7 @@ class Config {
188
189
  const configDir = configPath ? path.dirname(configPath) : undefined;
189
190
 
190
191
  // Validate and merge in plugins (if any).
191
- configJSON = mergePlugins(configJSON, configDir, flags);
192
+ configJSON = await mergePlugins(configJSON, configDir, flags);
192
193
 
193
194
  if (flags) {
194
195
  assertValidFlags(flags);
@@ -198,14 +199,28 @@ class Config {
198
199
  // Augment passes with necessary defaults and require gatherers.
199
200
  const passesWithDefaults = Config.augmentPassesWithDefaults(configJSON.passes);
200
201
  Config.adjustDefaultPassForThrottling(settings, passesWithDefaults);
201
- const passes = Config.requireGatherers(passesWithDefaults, configDir);
202
+ const passes = await Config.requireGatherers(passesWithDefaults, configDir);
202
203
 
204
+ const audits = await Config.requireAudits(configJSON.audits, configDir);
205
+
206
+ const config = new Config(configJSON, {settings, passes, audits});
207
+ log.timeEnd(status);
208
+ return config;
209
+ }
210
+
211
+ /**
212
+ * @deprecated `Config.fromJson` should be used instead.
213
+ * @constructor
214
+ * @param {LH.Config.Json} configJSON
215
+ * @param {{settings: LH.Config.Settings, passes: ?LH.Config.Pass[], audits: ?LH.Config.AuditDefn[]}} opts
216
+ */
217
+ constructor(configJSON, opts) {
203
218
  /** @type {LH.Config.Settings} */
204
- this.settings = settings;
219
+ this.settings = opts.settings;
205
220
  /** @type {?Array<LH.Config.Pass>} */
206
- this.passes = passes;
221
+ this.passes = opts.passes;
207
222
  /** @type {?Array<LH.Config.AuditDefn>} */
208
- this.audits = Config.requireAudits(configJSON.audits, configDir);
223
+ this.audits = opts.audits;
209
224
  /** @type {?Record<string, LH.Config.Category>} */
210
225
  this.categories = configJSON.categories || null;
211
226
  /** @type {?Record<string, LH.Config.Group>} */
@@ -215,8 +230,6 @@ class Config {
215
230
 
216
231
  assertValidPasses(this.passes, this.audits);
217
232
  validation.assertValidCategories(this.categories, this.audits, this.groups);
218
-
219
- log.timeEnd(status);
220
233
  }
221
234
 
222
235
  /**
@@ -499,12 +512,12 @@ class Config {
499
512
  * leaving only an array of AuditDefns.
500
513
  * @param {LH.Config.Json['audits']} audits
501
514
  * @param {string=} configDir
502
- * @return {Config['audits']}
515
+ * @return {Promise<Config['audits']>}
503
516
  */
504
- static requireAudits(audits, configDir) {
517
+ static async requireAudits(audits, configDir) {
505
518
  const status = {msg: 'Requiring audits', id: 'lh:config:requireAudits'};
506
519
  log.time(status, 'verbose');
507
- const auditDefns = resolveAuditsToDefns(audits, configDir);
520
+ const auditDefns = await resolveAuditsToDefns(audits, configDir);
508
521
  log.timeEnd(status);
509
522
  return auditDefns;
510
523
  }
@@ -515,9 +528,9 @@ class Config {
515
528
  * provided) using `resolveModulePath`, returning an array of full Passes.
516
529
  * @param {?Array<Required<LH.Config.PassJson>>} passes
517
530
  * @param {string=} configDir
518
- * @return {Config['passes']}
531
+ * @return {Promise<Config['passes']>}
519
532
  */
520
- static requireGatherers(passes, configDir) {
533
+ static async requireGatherers(passes, configDir) {
521
534
  if (!passes) {
522
535
  return null;
523
536
  }
@@ -525,9 +538,11 @@ class Config {
525
538
  log.time(status, 'verbose');
526
539
 
527
540
  const coreList = Runner.getGathererList();
528
- const fullPasses = passes.map(pass => {
529
- const gathererDefns = pass.gatherers
530
- .map(gatherer => resolveGathererToDefn(gatherer, coreList, configDir));
541
+ const fullPassesPromises = passes.map(async (pass) => {
542
+ const gathererDefns = await Promise.all(
543
+ pass.gatherers
544
+ .map(gatherer => resolveGathererToDefn(gatherer, coreList, configDir))
545
+ );
531
546
 
532
547
  // De-dupe gatherers by artifact name because artifact IDs must be unique at runtime.
533
548
  const uniqueDefns = Array.from(
@@ -537,6 +552,8 @@ class Config {
537
552
 
538
553
  return Object.assign(pass, {gatherers: uniqueDefns});
539
554
  });
555
+ const fullPasses = await Promise.all(fullPassesPromises);
556
+
540
557
  log.timeEnd(status);
541
558
  return fullPasses;
542
559
  }
@@ -121,9 +121,9 @@ function resolveArtifactDependencies(artifact, gatherer, artifactDefnsBySymbol)
121
121
  *
122
122
  * @param {LH.Config.ArtifactJson[]|null|undefined} artifacts
123
123
  * @param {string|undefined} configDir
124
- * @return {LH.Config.AnyArtifactDefn[] | null}
124
+ * @return {Promise<LH.Config.AnyArtifactDefn[] | null>}
125
125
  */
126
- function resolveArtifactsToDefns(artifacts, configDir) {
126
+ async function resolveArtifactsToDefns(artifacts, configDir) {
127
127
  if (!artifacts) return null;
128
128
 
129
129
  const status = {msg: 'Resolve artifact definitions', id: 'lh:config:resolveArtifactsToDefns'};
@@ -133,12 +133,12 @@ function resolveArtifactsToDefns(artifacts, configDir) {
133
133
  const artifactDefnsBySymbol = new Map();
134
134
 
135
135
  const coreGathererList = Runner.getGathererList();
136
- const artifactDefns = artifacts.map(artifactJson => {
136
+ const artifactDefnsPromises = artifacts.map(async (artifactJson) => {
137
137
  /** @type {LH.Config.GathererJson} */
138
138
  // @ts-expect-error - remove when legacy runner path is removed.
139
139
  const gathererJson = artifactJson.gatherer;
140
140
 
141
- const gatherer = resolveGathererToDefn(gathererJson, coreGathererList, configDir);
141
+ const gatherer = await resolveGathererToDefn(gathererJson, coreGathererList, configDir);
142
142
  if (!isFRGathererDefn(gatherer)) {
143
143
  throw new Error(`${gatherer.instance.name} gatherer does not have a Fraggle Rock meta obj`);
144
144
  }
@@ -156,6 +156,7 @@ function resolveArtifactsToDefns(artifacts, configDir) {
156
156
  if (symbol) artifactDefnsBySymbol.set(symbol, artifact);
157
157
  return artifact;
158
158
  });
159
+ const artifactDefns = await Promise.all(artifactDefnsPromises);
159
160
 
160
161
  log.timeEnd(status);
161
162
  return artifactDefns;
@@ -242,28 +243,28 @@ function resolveNavigationsToDefns(navigations, artifactDefns, settings) {
242
243
  /**
243
244
  * @param {LH.Config.Json|undefined} configJSON
244
245
  * @param {ConfigContext} context
245
- * @return {{config: LH.Config.FRConfig, warnings: string[]}}
246
+ * @return {Promise<{config: LH.Config.FRConfig, warnings: string[]}>}
246
247
  */
247
- function initializeConfig(configJSON, context) {
248
+ async function initializeConfig(configJSON, context) {
248
249
  const status = {msg: 'Initialize config', id: 'lh:config'};
249
250
  log.time(status, 'verbose');
250
251
 
251
252
  let {configWorkingCopy, configDir} = resolveWorkingCopy(configJSON, context); // eslint-disable-line prefer-const
252
253
 
253
254
  configWorkingCopy = resolveExtensions(configWorkingCopy);
254
- configWorkingCopy = mergePlugins(configWorkingCopy, configDir, context.settingsOverrides);
255
+ configWorkingCopy = await mergePlugins(configWorkingCopy, configDir, context.settingsOverrides);
255
256
 
256
257
  const settings = resolveSettings(configWorkingCopy.settings || {}, context.settingsOverrides);
257
258
  overrideSettingsForGatherMode(settings, context);
258
259
 
259
- const artifacts = resolveArtifactsToDefns(configWorkingCopy.artifacts, configDir);
260
+ const artifacts = await resolveArtifactsToDefns(configWorkingCopy.artifacts, configDir);
260
261
  const navigations = resolveNavigationsToDefns(configWorkingCopy.navigations, artifacts, settings);
261
262
 
262
263
  /** @type {LH.Config.FRConfig} */
263
264
  let config = {
264
265
  artifacts,
265
266
  navigations,
266
- audits: resolveAuditsToDefns(configWorkingCopy.audits, configDir),
267
+ audits: await resolveAuditsToDefns(configWorkingCopy.audits, configDir),
267
268
  categories: configWorkingCopy.categories || null,
268
269
  groups: configWorkingCopy.groups || null,
269
270
  settings,
@@ -26,8 +26,6 @@ const throwingSession = {
26
26
  off: throwNotConnectedFn,
27
27
  addProtocolMessageListener: throwNotConnectedFn,
28
28
  removeProtocolMessageListener: throwNotConnectedFn,
29
- addSessionAttachedListener: throwNotConnectedFn,
30
- removeSessionAttachedListener: throwNotConnectedFn,
31
29
  sendCommand: throwNotConnectedFn,
32
30
  dispose: throwNotConnectedFn,
33
31
  };
@@ -310,7 +310,8 @@ async function navigationGather(requestor, options) {
310
310
  const {configContext = {}} = options;
311
311
  log.setLevel(configContext.logLevel || 'error');
312
312
 
313
- const {config} = initializeConfig(options.config, {...configContext, gatherMode: 'navigation'});
313
+ const {config} =
314
+ await initializeConfig(options.config, {...configContext, gatherMode: 'navigation'});
314
315
  const computedCache = new Map();
315
316
  const internalOptions = {
316
317
  skipAboutBlank: configContext.skipAboutBlank,
@@ -27,6 +27,7 @@ class ProtocolSession {
27
27
 
28
28
  sessionId() {
29
29
  return this._targetInfo && this._targetInfo.type === 'iframe' ?
30
+ // TODO: use this._session.id() for real session id.
30
31
  this._targetInfo.targetId :
31
32
  undefined;
32
33
  }
@@ -77,27 +78,6 @@ class ProtocolSession {
77
78
  this._cdpSession.once(eventName, /** @type {*} */ (callback));
78
79
  }
79
80
 
80
- /**
81
- * Bind to the puppeteer `sessionattached` listener and return an LH ProtocolSession.
82
- * @param {(session: ProtocolSession) => void} callback
83
- */
84
- addSessionAttachedListener(callback) {
85
- /** @param {LH.Puppeteer.CDPSession} session */
86
- const listener = session => callback(new ProtocolSession(session));
87
- this._callbackMap.set(callback, listener);
88
- this._getConnection().on('sessionattached', listener);
89
- }
90
-
91
- /**
92
- * Unbind to the puppeteer `sessionattached` listener.
93
- * @param {(session: ProtocolSession) => void} callback
94
- */
95
- removeSessionAttachedListener(callback) {
96
- const listener = this._callbackMap.get(callback);
97
- if (!listener) return;
98
- this._getConnection().off('sessionattached', listener);
99
- }
100
-
101
81
  /**
102
82
  * Bind to puppeteer's '*' event that fires for *any* protocol event,
103
83
  * and wrap it with data about the protocol message instead of just the event.
@@ -172,12 +152,6 @@ class ProtocolSession {
172
152
  this._cdpSession.removeAllListeners();
173
153
  await this._cdpSession.detach();
174
154
  }
175
-
176
- _getConnection() {
177
- const connection = this._cdpSession.connection();
178
- if (!connection) throw new Error('Connection has been closed.');
179
- return connection;
180
- }
181
155
  }
182
156
 
183
157
  module.exports = ProtocolSession;
@@ -24,7 +24,8 @@ async function snapshotGather(options) {
24
24
  const {configContext = {}} = options;
25
25
  log.setLevel(configContext.logLevel || 'error');
26
26
 
27
- const {config} = initializeConfig(options.config, {...configContext, gatherMode: 'snapshot'});
27
+ const {config} =
28
+ await initializeConfig(options.config, {...configContext, gatherMode: 'snapshot'});
28
29
  const driver = new Driver(options.page);
29
30
  await driver.connect();
30
31
 
@@ -25,7 +25,8 @@ async function startTimespanGather(options) {
25
25
  const {configContext = {}} = options;
26
26
  log.setLevel(configContext.logLevel || 'error');
27
27
 
28
- const {config} = initializeConfig(options.config, {...configContext, gatherMode: 'timespan'});
28
+ const {config} =
29
+ await initializeConfig(options.config, {...configContext, gatherMode: 'timespan'});
29
30
  const driver = new Driver(options.page);
30
31
  await driver.connect();
31
32
 
@@ -205,7 +205,7 @@ async function auditGatherSteps(gatherSteps, options) {
205
205
  // Step specific configs take precedence over a config for the entire flow.
206
206
  const configJson = gatherStep.config || options.config;
207
207
  const {gatherMode} = artifacts.GatherContext;
208
- const {config} = initializeConfig(configJson, {...configContext, gatherMode});
208
+ const {config} = await initializeConfig(configJson, {...configContext, gatherMode});
209
209
  runnerOptions = {
210
210
  config,
211
211
  computedCache: new Map(),
@@ -33,23 +33,9 @@ class CriConnection extends Connection {
33
33
  * @override
34
34
  * @return {Promise<void>}
35
35
  */
36
- connect() {
37
- return this._runJsonCommand('new')
38
- .then(response => this._connectToSocket(/** @type {LH.DevToolsJsonTarget} */(response)))
39
- .catch(_ => {
40
- // COMPAT: headless didn't support `/json/new` before m59. (#970, crbug.com/699392)
41
- // If no support, we fallback and reuse an existing open tab
42
- log.warn('CriConnection', 'Cannot create new tab; reusing open tab.');
43
- return this._runJsonCommand('list').then(tabs => {
44
- if (!Array.isArray(tabs) || tabs.length === 0) {
45
- return Promise.reject(new Error('Cannot create new tab, and no tabs already open.'));
46
- }
47
- const firstTab = tabs[0];
48
- // first, we activate it to a foreground tab, then we connect
49
- return this._runJsonCommand(`activate/${firstTab.id}`)
50
- .then(() => this._connectToSocket(firstTab));
51
- });
52
- });
36
+ async connect() {
37
+ const response = await this._runJsonCommand('new');
38
+ return this._connectToSocket(/** @type {LH.DevToolsJsonTarget} */(response));
53
39
  }
54
40
 
55
41
  /**
@@ -90,7 +90,10 @@ class NetworkMonitor {
90
90
  this._frameNavigations = [];
91
91
  this._sessions = new Map();
92
92
  this._networkRecorder = new NetworkRecorder();
93
- this._targetManager = new TargetManager(this._session);
93
+ /** @type {LH.Puppeteer.CDPSession} */
94
+ // @ts-expect-error - temporarily reach in to get CDPSession
95
+ const rootCdpSession = this._session._cdpSession;
96
+ this._targetManager = new TargetManager(rootCdpSession);
94
97
 
95
98
  /**
96
99
  * Reemit the same network recorder events.
@@ -11,21 +11,23 @@
11
11
  */
12
12
 
13
13
  const log = require('lighthouse-logger');
14
+ const ProtocolSession = require('../../fraggle-rock/gather/session.js');
14
15
 
15
16
  /** @typedef {{target: LH.Crdp.Target.TargetInfo, session: LH.Gatherer.FRProtocolSession}} TargetWithSession */
16
17
 
17
18
  class TargetManager {
18
- /** @param {LH.Gatherer.FRProtocolSession} session */
19
- constructor(session) {
19
+ /** @param {LH.Puppeteer.CDPSession} cdpSession */
20
+ constructor(cdpSession) {
20
21
  this._enabled = false;
21
- this._session = session;
22
+ this._rootCdpSession = cdpSession;
23
+
22
24
  /** @type {Array<(targetWithSession: TargetWithSession) => Promise<void>|void>} */
23
25
  this._listeners = [];
24
26
 
25
27
  this._onSessionAttached = this._onSessionAttached.bind(this);
26
28
 
27
29
  /** @type {Map<string, TargetWithSession>} */
28
- this._targets = new Map();
30
+ this._targetIdToTargets = new Map();
29
31
 
30
32
  /** @param {LH.Crdp.Page.FrameNavigatedEvent} event */
31
33
  this._onFrameNavigated = async event => {
@@ -35,7 +37,7 @@ class TargetManager {
35
37
  // It's not entirely clear when this is necessary, but when the page switches processes on
36
38
  // navigating from about:blank to the `requestedUrl`, resetting `setAutoAttach` has been
37
39
  // necessary in the past.
38
- await this._session.sendCommand('Target.setAutoAttach', {
40
+ await this._rootCdpSession.send('Target.setAutoAttach', {
39
41
  autoAttach: true,
40
42
  flatten: true,
41
43
  waitForDebuggerOnStart: true,
@@ -44,9 +46,11 @@ class TargetManager {
44
46
  }
45
47
 
46
48
  /**
47
- * @param {LH.Gatherer.FRProtocolSession} session
49
+ * @param {LH.Puppeteer.CDPSession} cdpSession
48
50
  */
49
- async _onSessionAttached(session) {
51
+ async _onSessionAttached(cdpSession) {
52
+ const session = new ProtocolSession(cdpSession);
53
+
50
54
  try {
51
55
  const target = await session.sendCommand('Target.getTargetInfo').catch(() => null);
52
56
  const targetType = target?.targetInfo?.type;
@@ -55,13 +59,13 @@ class TargetManager {
55
59
 
56
60
  const targetId = target.targetInfo.targetId;
57
61
  session.setTargetInfo(target.targetInfo);
58
- if (this._targets.has(targetId)) return;
62
+ if (this._targetIdToTargets.has(targetId)) return;
59
63
 
60
64
  const targetName = target.targetInfo.url || target.targetInfo.targetId;
61
65
  log.verbose('target-manager', `target ${targetName} attached`);
62
66
 
63
67
  const targetWithSession = {target: target.targetInfo, session};
64
- this._targets.set(targetId, targetWithSession);
68
+ this._targetIdToTargets.set(targetId, targetWithSession);
65
69
  for (const listener of this._listeners) await listener(targetWithSession);
66
70
 
67
71
  await session.sendCommand('Target.setAutoAttach', {
@@ -87,24 +91,32 @@ class TargetManager {
87
91
  if (this._enabled) return;
88
92
 
89
93
  this._enabled = true;
90
- this._targets = new Map();
94
+ this._targetIdToTargets = new Map();
95
+
96
+ this._rootCdpSession.on('Page.frameNavigated', this._onFrameNavigated);
97
+
98
+ const rootConnection = this._rootCdpSession.connection();
99
+ if (!rootConnection) {
100
+ throw new Error('Connection has been closed.');
101
+ }
102
+ rootConnection.on('sessionattached', this._onSessionAttached);
91
103
 
92
- this._session.on('Page.frameNavigated', this._onFrameNavigated);
93
- this._session.addSessionAttachedListener(this._onSessionAttached);
104
+ await this._rootCdpSession.send('Page.enable');
94
105
 
95
- await this._session.sendCommand('Page.enable');
96
- await this._onSessionAttached(this._session);
106
+ // Start with the already attached root session.
107
+ await this._onSessionAttached(this._rootCdpSession);
97
108
  }
98
109
 
99
110
  /**
100
111
  * @return {Promise<void>}
101
112
  */
102
113
  async disable() {
103
- this._session.off('Page.frameNavigated', this._onFrameNavigated);
104
- this._session.removeSessionAttachedListener(this._onSessionAttached);
114
+ this._rootCdpSession.off('Page.frameNavigated', this._onFrameNavigated);
115
+ // No need to remove listener if connection is already closed.
116
+ this._rootCdpSession.connection()?.off('sessionattached', this._onSessionAttached);
105
117
 
106
118
  this._enabled = false;
107
- this._targets = new Map();
119
+ this._targetIdToTargets = new Map();
108
120
  }
109
121
 
110
122
  /**
@@ -12,7 +12,7 @@ const {fetchResponseBodyFromCache} = require('../gather/driver/network.js');
12
12
  const EventEmitter = require('events').EventEmitter;
13
13
 
14
14
  const log = require('lighthouse-logger');
15
- const DevtoolsLog = require('./devtools-log.js');
15
+ const DevtoolsMessageLog = require('./gatherers/devtools-log.js').DevtoolsMessageLog;
16
16
  const TraceGatherer = require('./gatherers/trace.js');
17
17
 
18
18
  // Pulled in for Connection type checking.
@@ -41,7 +41,7 @@ class Driver {
41
41
  * @private
42
42
  * Used to save network and lifecycle protocol traffic. Just Page and Network are needed.
43
43
  */
44
- _devtoolsLog = new DevtoolsLog(/^(Page|Network)\./);
44
+ _devtoolsLog = new DevtoolsMessageLog(/^(Page|Network)\./);
45
45
 
46
46
  /**
47
47
  * @private
@@ -208,16 +208,6 @@ class Driver {
208
208
  // OOPIF handling in legacy driver is implicit.
209
209
  }
210
210
 
211
- /** @param {(session: LH.Gatherer.FRProtocolSession) => void} callback */
212
- addSessionAttachedListener(callback) { // eslint-disable-line no-unused-vars
213
- // OOPIF handling in legacy driver is implicit.
214
- }
215
-
216
- /** @param {(session: LH.Gatherer.FRProtocolSession) => void} callback */
217
- removeSessionAttachedListener(callback) { // eslint-disable-line no-unused-vars
218
- // OOPIF handling in legacy driver is implicit.
219
- }
220
-
221
211
  /**
222
212
  * Debounce enabling or disabling domains to prevent driver users from
223
213
  * stomping on each other. Maintains an internal count of the times a domain
@@ -12,7 +12,6 @@
12
12
  */
13
13
 
14
14
  const NetworkMonitor = require('../driver/network-monitor.js');
15
- const MessageLog = require('../devtools-log.js');
16
15
  const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
17
16
 
18
17
  class DevtoolsLog extends FRGatherer {
@@ -30,7 +29,7 @@ class DevtoolsLog extends FRGatherer {
30
29
  /** @type {NetworkMonitor|undefined} */
31
30
  this._networkMonitor = undefined;
32
31
 
33
- this._messageLog = new MessageLog(/^(Page|Network|Target|Runtime)\./);
32
+ this._messageLog = new DevtoolsMessageLog(/^(Page|Network|Target|Runtime)\./);
34
33
 
35
34
  /** @param {LH.Protocol.RawEventMessage} e */
36
35
  this._onProtocolMessage = e => this._messageLog.record(e);
@@ -63,4 +62,59 @@ class DevtoolsLog extends FRGatherer {
63
62
  }
64
63
  }
65
64
 
65
+
66
+ /**
67
+ * This class saves all protocol messages whose method match a particular
68
+ * regex filter. Used when saving assets for later analysis by another tool such as
69
+ * Webpagetest.
70
+ */
71
+ class DevtoolsMessageLog {
72
+ /**
73
+ * @param {RegExp=} regexFilter
74
+ */
75
+ constructor(regexFilter) {
76
+ this._filter = regexFilter;
77
+
78
+ /** @type {LH.DevtoolsLog} */
79
+ this._messages = [];
80
+ this._isRecording = false;
81
+ }
82
+
83
+ /**
84
+ * @return {LH.DevtoolsLog}
85
+ */
86
+ get messages() {
87
+ return this._messages;
88
+ }
89
+
90
+ reset() {
91
+ this._messages = [];
92
+ }
93
+
94
+ beginRecording() {
95
+ this._isRecording = true;
96
+ }
97
+
98
+ endRecording() {
99
+ this._isRecording = false;
100
+ }
101
+
102
+ /**
103
+ * Records a message if method matches filter and recording has been started.
104
+ * @param {LH.Protocol.RawEventMessage} message
105
+ */
106
+ record(message) {
107
+ // We're not recording, skip the rest of the checks.
108
+ if (!this._isRecording) return;
109
+ // The event was likely an internal puppeteer method that uses Symbols.
110
+ if (typeof message.method !== 'string') return;
111
+ // The event didn't pass our filter, do not record it.
112
+ if (this._filter && !this._filter.test(message.method)) return;
113
+
114
+ // We passed all the checks, record the message.
115
+ this._messages.push(message);
116
+ }
117
+ }
118
+
66
119
  module.exports = DevtoolsLog;
120
+ module.exports.DevtoolsMessageLog = DevtoolsMessageLog;
@@ -64,7 +64,7 @@ async function legacyNavigation(url, flags = {}, configJSON, userConnection) {
64
64
  flags.logLevel = flags.logLevel || 'error';
65
65
  log.setLevel(flags.logLevel);
66
66
 
67
- const config = generateConfig(configJSON, flags);
67
+ const config = await generateConfig(configJSON, flags);
68
68
  const computedCache = new Map();
69
69
  const options = {config, computedCache};
70
70
  const connection = userConnection || new ChromeProtocol(flags.port, flags.hostname);
@@ -83,10 +83,10 @@ async function legacyNavigation(url, flags = {}, configJSON, userConnection) {
83
83
  * not present, the default config is used.
84
84
  * @param {LH.Flags=} flags Optional settings for the Lighthouse run. If present,
85
85
  * they will override any settings in the config.
86
- * @return {Config}
86
+ * @return {Promise<Config>}
87
87
  */
88
88
  function generateConfig(configJson, flags) {
89
- return new Config(configJson, flags);
89
+ return Config.fromJson(configJson, flags);
90
90
  }
91
91
 
92
92
  lighthouse.legacyNavigation = legacyNavigation;
@@ -288,10 +288,9 @@ async function saveAssets(artifacts, audits, pathWithBasename) {
288
288
  fs.writeFileSync(devtoolsLogFilename, JSON.stringify(passAssets.devtoolsLog, null, 2));
289
289
  log.log('saveAssets', 'devtools log saved to disk: ' + devtoolsLogFilename);
290
290
 
291
- const streamTraceFilename = `${pathWithBasename}-${index}${traceSuffix}`;
292
- log.log('saveAssets', 'streaming trace file to disk: ' + streamTraceFilename);
293
- await saveTrace(passAssets.traceData, streamTraceFilename);
294
- log.log('saveAssets', 'trace file streamed to disk: ' + streamTraceFilename);
291
+ const traceFilename = `${pathWithBasename}-${index}${traceSuffix}`;
292
+ await saveTrace(passAssets.traceData, traceFilename);
293
+ log.log('saveAssets', 'trace file streamed to disk: ' + traceFilename);
295
294
  });
296
295
 
297
296
  await Promise.all(saveAll);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "9.5.0-dev.20220608",
3
+ "version": "9.5.0-dev.20220611",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -125,8 +125,8 @@
125
125
  "@types/ws": "^7.0.0",
126
126
  "@types/yargs": "^17.0.8",
127
127
  "@types/yargs-parser": "^20.2.1",
128
- "@typescript-eslint/eslint-plugin": "^5.26.0",
129
- "@typescript-eslint/parser": "^5.26.0",
128
+ "@typescript-eslint/eslint-plugin": "^5.27.1",
129
+ "@typescript-eslint/parser": "^5.27.1",
130
130
  "acorn": "^8.5.0",
131
131
  "angular": "^1.7.4",
132
132
  "archiver": "^3.0.0",
@@ -175,7 +175,7 @@
175
175
  "terser": "^5.3.8",
176
176
  "ts-jest": "^27.0.4",
177
177
  "typed-query-selector": "^2.6.1",
178
- "typescript": "^4.7.2",
178
+ "typescript": "^4.7.3",
179
179
  "wait-for-expect": "^3.0.2",
180
180
  "webtreemap-cdt": "^3.2.1"
181
181
  },
@@ -12,7 +12,7 @@
12
12
  "rootDir": ".",
13
13
 
14
14
  "target": "es2020",
15
- "module": "es2020",
15
+ "module": "es2022",
16
16
  "moduleResolution": "node",
17
17
  "esModuleInterop": true,
18
18
 
package/tsconfig.json CHANGED
@@ -30,6 +30,7 @@
30
30
  "third-party/snyk/snapshot.json",
31
31
  "lighthouse-core/audits/byte-efficiency/polyfill-graph-data.json",
32
32
  "shared/localization/locales/en-US.json",
33
+ "esm-utils.mjs",
33
34
  ],
34
35
  "exclude": [
35
36
  "lighthouse-core/test/audits/**/*.js",
@@ -46,11 +47,11 @@
46
47
  "lighthouse-core/test/config/config-test.js",
47
48
  "lighthouse-core/test/config/default-config-test.js",
48
49
  "lighthouse-core/test/create-test-trace.js",
49
- "lighthouse-core/test/gather/devtools-log-test.js",
50
50
  "lighthouse-core/test/gather/driver/wait-for-condition-test.js",
51
51
  "lighthouse-core/test/gather/gatherers/accessibility-test.js",
52
52
  "lighthouse-core/test/gather/gatherers/cache-contents-test.js",
53
53
  "lighthouse-core/test/gather/gatherers/console-messages-test.js",
54
+ "lighthouse-core/test/gather/gatherers/devtools-log-test.js",
54
55
  "lighthouse-core/test/gather/gatherers/dobetterweb/optimized-images-test.js",
55
56
  "lighthouse-core/test/gather/gatherers/dobetterweb/password-inputs-with-prevented-paste-test.js",
56
57
  "lighthouse-core/test/gather/gatherers/dobetterweb/response-compression-test.js",
@@ -30,8 +30,6 @@ declare module Gatherer {
30
30
  once<TEvent extends keyof LH.CrdpEvents>(event: TEvent, callback: (...args: LH.CrdpEvents[TEvent]) => void): void;
31
31
  addProtocolMessageListener(callback: (payload: Protocol.RawEventMessage) => void): void
32
32
  removeProtocolMessageListener(callback: (payload: Protocol.RawEventMessage) => void): void
33
- addSessionAttachedListener(callback: (session: FRProtocolSession) => void): void
34
- removeSessionAttachedListener(callback: (session: FRProtocolSession) => void): void
35
33
  off<TEvent extends keyof LH.CrdpEvents>(event: TEvent, callback: (...args: LH.CrdpEvents[TEvent]) => void): void;
36
34
  sendCommand<TMethod extends keyof LH.CrdpCommands>(method: TMethod, ...params: LH.CrdpCommands[TMethod]['paramsType']): Promise<LH.CrdpCommands[TMethod]['returnType']>;
37
35
  dispose(): Promise<void>;
@@ -1,61 +0,0 @@
1
- /**
2
- * @license Copyright 2017 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
- * @fileoverview This class saves all protocol messages whose method match a particular
10
- * regex filter. Used when saving assets for later analysis by another tool such as
11
- * Webpagetest.
12
- */
13
- class DevtoolsLog {
14
- /**
15
- * @param {RegExp=} regexFilter
16
- */
17
- constructor(regexFilter) {
18
- this._filter = regexFilter;
19
-
20
- /** @type {LH.DevtoolsLog} */
21
- this._messages = [];
22
- this._isRecording = false;
23
- }
24
-
25
- /**
26
- * @return {LH.DevtoolsLog}
27
- */
28
- get messages() {
29
- return this._messages;
30
- }
31
-
32
- reset() {
33
- this._messages = [];
34
- }
35
-
36
- beginRecording() {
37
- this._isRecording = true;
38
- }
39
-
40
- endRecording() {
41
- this._isRecording = false;
42
- }
43
-
44
- /**
45
- * Records a message if method matches filter and recording has been started.
46
- * @param {LH.Protocol.RawEventMessage} message
47
- */
48
- record(message) {
49
- // We're not recording, skip the rest of the checks.
50
- if (!this._isRecording) return;
51
- // The event was likely an internal puppeteer method that uses Symbols.
52
- if (typeof message.method !== 'string') return;
53
- // The event didn't pass our filter, do not record it.
54
- if (this._filter && !this._filter.test(message.method)) return;
55
-
56
- // We passed all the checks, record the message.
57
- this._messages.push(message);
58
- }
59
- }
60
-
61
- module.exports = DevtoolsLog;