lighthouse 9.2.0 → 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/changelog.md +67 -0
  2. package/dist/report/bundle.esm.js +23 -11
  3. package/dist/report/flow.js +11 -11
  4. package/dist/report/standalone.js +8 -8
  5. package/flow-report/src/common.tsx +1 -1
  6. package/flow-report/src/sidebar/flow.tsx +1 -1
  7. package/flow-report/src/util.ts +1 -2
  8. package/lighthouse-cli/bin.js +0 -4
  9. package/lighthouse-cli/cli-flags.js +22 -8
  10. package/lighthouse-cli/run.js +5 -1
  11. package/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js +4 -2
  12. package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +1 -1
  13. package/lighthouse-cli/test/smokehouse/lighthouse-runners/devtools.js +103 -0
  14. package/lighthouse-cli/test/smokehouse/readme.md +10 -8
  15. package/lighthouse-cli/test/smokehouse/report-assert.js +1 -1
  16. package/lighthouse-cli/test/smokehouse/smokehouse.js +4 -5
  17. package/lighthouse-core/audits/accessibility/axe-audit.js +4 -4
  18. package/lighthouse-core/audits/accessibility/object-alt.js +3 -3
  19. package/lighthouse-core/audits/audit.js +19 -4
  20. package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +1 -1
  21. package/lighthouse-core/audits/byte-efficiency/duplicated-javascript.js +1 -1
  22. package/lighthouse-core/audits/deprecations.js +2 -2
  23. package/lighthouse-core/audits/dobetterweb/inspector-issues.js +11 -11
  24. package/lighthouse-core/audits/dobetterweb/uses-http2.js +1 -1
  25. package/lighthouse-core/audits/font-display.js +1 -1
  26. package/lighthouse-core/audits/installable-manifest.js +1 -1
  27. package/lighthouse-core/audits/is-on-https.js +1 -1
  28. package/lighthouse-core/audits/maskable-icon.js +1 -1
  29. package/lighthouse-core/audits/multi-check-audit.js +1 -1
  30. package/lighthouse-core/audits/network-requests.js +4 -4
  31. package/lighthouse-core/audits/preload-fonts.js +2 -3
  32. package/lighthouse-core/audits/preload-lcp-image.js +2 -12
  33. package/lighthouse-core/audits/seo/is-crawlable.js +2 -1
  34. package/lighthouse-core/audits/themed-omnibox.js +1 -1
  35. package/lighthouse-core/audits/uses-rel-preload.js +2 -3
  36. package/lighthouse-core/audits/valid-source-maps.js +2 -2
  37. package/lighthouse-core/computed/image-records.js +1 -1
  38. package/lighthouse-core/computed/js-bundles.js +1 -1
  39. package/lighthouse-core/computed/metrics/lantern-metric.js +1 -1
  40. package/lighthouse-core/computed/metrics/timing-summary.js +35 -35
  41. package/lighthouse-core/computed/page-dependency-graph.js +2 -2
  42. package/lighthouse-core/computed/resource-summary.js +1 -1
  43. package/lighthouse-core/config/config-helpers.js +2 -2
  44. package/lighthouse-core/config/config.js +1 -1
  45. package/lighthouse-core/fraggle-rock/config/filters.js +23 -1
  46. package/lighthouse-core/fraggle-rock/config/validation.js +2 -2
  47. package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +5 -4
  48. package/lighthouse-core/fraggle-rock/gather/snapshot-runner.js +0 -1
  49. package/lighthouse-core/fraggle-rock/gather/timespan-runner.js +0 -1
  50. package/lighthouse-core/fraggle-rock/user-flow.js +4 -4
  51. package/lighthouse-core/gather/connections/connection.js +1 -1
  52. package/lighthouse-core/gather/driver/execution-context.js +1 -1
  53. package/lighthouse-core/gather/driver/network-monitor.js +2 -2
  54. package/lighthouse-core/gather/driver/target-manager.js +1 -1
  55. package/lighthouse-core/gather/driver.js +1 -1
  56. package/lighthouse-core/gather/gatherers/console-messages.js +2 -2
  57. package/lighthouse-core/gather/gatherers/inspector-issues.js +34 -46
  58. package/lighthouse-core/gather/gatherers/js-usage.js +1 -1
  59. package/lighthouse-core/gather/gatherers/seo/font-size.js +1 -1
  60. package/lighthouse-core/gather/gatherers/trace-elements.js +8 -43
  61. package/lighthouse-core/index.js +4 -3
  62. package/lighthouse-core/lib/arbitrary-equality-map.js +1 -1
  63. package/lighthouse-core/lib/asset-saver.js +6 -4
  64. package/lighthouse-core/lib/dependency-graph/simulator/connection-pool.js +1 -1
  65. package/lighthouse-core/lib/dependency-graph/simulator/network-analyzer.js +1 -1
  66. package/lighthouse-core/lib/emulation.js +1 -1
  67. package/lighthouse-core/lib/icons.js +1 -2
  68. package/lighthouse-core/lib/median-run.js +1 -2
  69. package/lighthouse-core/lib/network-recorder.js +2 -2
  70. package/lighthouse-core/lib/network-request.js +1 -1
  71. package/lighthouse-core/lib/stack-packs.js +5 -0
  72. package/lighthouse-core/lib/tracehouse/cpu-profile-model.js +4 -6
  73. package/lighthouse-core/lib/tracehouse/main-thread-tasks.js +1 -1
  74. package/lighthouse-core/lib/tracehouse/trace-processor.js +14 -17
  75. package/lighthouse-core/lib/url-shim.js +15 -0
  76. package/lighthouse-core/runner.js +62 -49
  77. package/lighthouse-core/scripts/download-chrome.sh +15 -2
  78. package/package.json +21 -25
  79. package/report/assets/templates.html +2 -1
  80. package/report/clients/standalone.js +5 -1
  81. package/report/renderer/category-renderer.js +1 -1
  82. package/report/renderer/components.js +2 -2
  83. package/report/renderer/performance-category-renderer.js +4 -4
  84. package/report/renderer/report-renderer.js +1 -1
  85. package/report/renderer/report-ui-features.js +15 -3
  86. package/report/test/generator/report-generator-test.js +1 -1
  87. package/report/test/renderer/report-ui-features-test.js +22 -4
  88. package/report/test-assets/faux-psi.js +2 -3
  89. package/report/types/report-renderer.d.ts +2 -0
  90. package/shared/localization/locales/ar-XB.json +40 -4
  91. package/shared/localization/locales/ar.json +59 -23
  92. package/shared/localization/locales/bg.json +40 -4
  93. package/shared/localization/locales/ca.json +45 -9
  94. package/shared/localization/locales/cs.json +39 -3
  95. package/shared/localization/locales/da.json +43 -7
  96. package/shared/localization/locales/de.json +42 -6
  97. package/shared/localization/locales/el.json +59 -23
  98. package/shared/localization/locales/en-GB.json +43 -7
  99. package/shared/localization/locales/en-US.json +48 -6
  100. package/shared/localization/locales/en-XA.json +40 -4
  101. package/shared/localization/locales/en-XL.json +48 -6
  102. package/shared/localization/locales/es-419.json +39 -3
  103. package/shared/localization/locales/es.json +46 -10
  104. package/shared/localization/locales/fi.json +57 -21
  105. package/shared/localization/locales/fil.json +40 -4
  106. package/shared/localization/locales/fr.json +45 -9
  107. package/shared/localization/locales/he.json +41 -5
  108. package/shared/localization/locales/hi.json +59 -23
  109. package/shared/localization/locales/hr.json +57 -21
  110. package/shared/localization/locales/hu.json +39 -3
  111. package/shared/localization/locales/id.json +40 -4
  112. package/shared/localization/locales/it.json +40 -4
  113. package/shared/localization/locales/ja.json +39 -3
  114. package/shared/localization/locales/ko.json +40 -4
  115. package/shared/localization/locales/lt.json +40 -4
  116. package/shared/localization/locales/lv.json +56 -20
  117. package/shared/localization/locales/nl.json +40 -4
  118. package/shared/localization/locales/no.json +56 -20
  119. package/shared/localization/locales/pl.json +56 -20
  120. package/shared/localization/locales/pt-PT.json +57 -21
  121. package/shared/localization/locales/pt.json +41 -5
  122. package/shared/localization/locales/ro.json +39 -3
  123. package/shared/localization/locales/ru.json +40 -4
  124. package/shared/localization/locales/sk.json +39 -3
  125. package/shared/localization/locales/sl.json +39 -3
  126. package/shared/localization/locales/sr-Latn.json +40 -4
  127. package/shared/localization/locales/sr.json +40 -4
  128. package/shared/localization/locales/sv.json +39 -3
  129. package/shared/localization/locales/ta.json +47 -11
  130. package/shared/localization/locales/te.json +61 -25
  131. package/shared/localization/locales/th.json +39 -3
  132. package/shared/localization/locales/tr.json +40 -4
  133. package/shared/localization/locales/uk.json +40 -4
  134. package/shared/localization/locales/vi.json +63 -27
  135. package/shared/localization/locales/zh-HK.json +40 -4
  136. package/shared/localization/locales/zh-TW.json +45 -9
  137. package/shared/localization/locales/zh.json +41 -5
  138. package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +1 -1
  139. package/third-party/snyk/snapshot.json +4 -1
  140. package/types/artifacts.d.ts +16 -6
  141. package/types/robots-parser/index.d.ts +0 -20
@@ -60,42 +60,6 @@ class TraceElements extends FRGatherer {
60
60
  if (name) this.animationIdToName.set(id, name);
61
61
  }
62
62
 
63
- /**
64
- * @param {LH.TraceEvent | undefined} event
65
- * @return {number | undefined}
66
- */
67
- static getNodeIDFromTraceEvent(event) {
68
- return event && event.args &&
69
- event.args.data && event.args.data.nodeId;
70
- }
71
-
72
- /**
73
- * @param {LH.TraceEvent | undefined} event
74
- * @return {string | undefined}
75
- */
76
- static getAnimationIDFromTraceEvent(event) {
77
- return event && event.args &&
78
- event.args.data && event.args.data.id;
79
- }
80
-
81
- /**
82
- * @param {LH.TraceEvent | undefined} event
83
- * @return {number | undefined}
84
- */
85
- static getFailureReasonsFromTraceEvent(event) {
86
- return event && event.args &&
87
- event.args.data && event.args.data.compositeFailed;
88
- }
89
-
90
- /**
91
- * @param {LH.TraceEvent | undefined} event
92
- * @return {string[] | undefined}
93
- */
94
- static getUnsupportedPropertiesFromTraceEvent(event) {
95
- return event && event.args &&
96
- event.args.data && event.args.data.unsupportedProperties;
97
- }
98
-
99
63
  /**
100
64
  * @param {Array<number>} rect
101
65
  * @return {LH.Artifacts.Rect}
@@ -124,7 +88,7 @@ class TraceElements extends FRGatherer {
124
88
  const clsPerNode = new Map();
125
89
  const shiftEvents = mainThreadEvents
126
90
  .filter(e => e.name === 'LayoutShift')
127
- .map(e => e.args && e.args.data);
91
+ .map(e => e.args?.data);
128
92
  const indexFirstEventWithoutInput =
129
93
  shiftEvents.findIndex(event => event && !event.had_recent_input);
130
94
 
@@ -207,11 +171,12 @@ class TraceElements extends FRGatherer {
207
171
  /** @type {Map<number, Set<{animationId: string, failureReasonsMask?: number, unsupportedProperties?: string[]}>>} */
208
172
  const elementAnimations = new Map();
209
173
  for (const {begin, status} of animationPairs.values()) {
210
- const nodeId = TraceElements.getNodeIDFromTraceEvent(begin);
211
- const animationId = TraceElements.getAnimationIDFromTraceEvent(begin);
212
- const failureReasonsMask = TraceElements.getFailureReasonsFromTraceEvent(status);
213
- const unsupportedProperties = TraceElements.getUnsupportedPropertiesFromTraceEvent(status);
174
+ const nodeId = begin?.args?.data?.nodeId;
175
+ const animationId = begin?.args?.data?.id;
176
+ const failureReasonsMask = status?.args?.data?.compositeFailed;
177
+ const unsupportedProperties = status?.args?.data?.unsupportedProperties;
214
178
  if (!nodeId || !animationId) continue;
179
+
215
180
  const animationIds = elementAnimations.get(nodeId) || new Set();
216
181
  animationIds.add({animationId, failureReasonsMask, unsupportedProperties});
217
182
  elementAnimations.set(nodeId, animationIds);
@@ -270,7 +235,7 @@ class TraceElements extends FRGatherer {
270
235
  });
271
236
  const {mainThreadEvents} = processedTrace;
272
237
 
273
- const lcpNodeId = TraceElements.getNodeIDFromTraceEvent(largestContentfulPaintEvt);
238
+ const lcpNodeId = largestContentfulPaintEvt?.args?.data?.nodeId;
274
239
  const clsNodeData = TraceElements.getTopLayoutShiftElements(mainThreadEvents);
275
240
  const animatedElementData =
276
241
  await this.getAnimatedElements(mainThreadEvents);
@@ -308,7 +273,7 @@ class TraceElements extends FRGatherer {
308
273
  continue;
309
274
  }
310
275
 
311
- if (response && response.result && response.result.value) {
276
+ if (response?.result?.value) {
312
277
  traceElements.push({
313
278
  traceEventType,
314
279
  ...response.result.value,
@@ -9,6 +9,7 @@ const Runner = require('./runner.js');
9
9
  const log = require('lighthouse-logger');
10
10
  const ChromeProtocol = require('./gather/connections/cri.js');
11
11
  const Config = require('./config/config.js');
12
+ const URL = require('./lib/url-shim.js');
12
13
 
13
14
  /** @typedef {import('./gather/connections/connection.js')} Connection */
14
15
 
@@ -42,12 +43,12 @@ async function lighthouse(url, flags = {}, configJSON, userConnection) {
42
43
 
43
44
  const config = generateConfig(configJSON, flags);
44
45
  const computedCache = new Map();
45
- const options = {url, config, computedCache};
46
+ const options = {config, computedCache};
46
47
  const connection = userConnection || new ChromeProtocol(flags.port, flags.hostname);
47
48
 
48
49
  // kick off a lighthouse run
49
- /** @param {{requestedUrl: string}} runnerData */
50
- const gatherFn = ({requestedUrl}) => {
50
+ const gatherFn = () => {
51
+ const requestedUrl = URL.normalizeUrl(url);
51
52
  return Runner._gatherArtifactsFromBrowser(requestedUrl, options, connection);
52
53
  };
53
54
  return Runner.run(gatherFn, options);
@@ -40,7 +40,7 @@ class ArbitraryEqualityMap {
40
40
  */
41
41
  get(key) {
42
42
  const entry = this._entries[this._findIndexOf(key)];
43
- return entry && entry.value;
43
+ return entry?.value;
44
44
  }
45
45
 
46
46
  /**
@@ -19,6 +19,8 @@ const LHError = require('../lib/lh-error.js');
19
19
  // TODO(esmodules): Rollup does not support `promisfy` or `stream.pipeline`. Bundled files
20
20
  // don't need anything in this file except for `stringifyReplacer`, so a check for
21
21
  // truthiness before using is enough.
22
+ // TODO: Can remove promisify(pipeline) in Node 15.
23
+ // https://nodejs.org/api/stream.html#streams-promises-api
22
24
  const pipeline = promisify && promisify(stream.pipeline);
23
25
 
24
26
  const artifactsFilename = 'artifacts.json';
@@ -238,8 +240,6 @@ async function saveTrace(traceData, traceFilename) {
238
240
  const traceIter = traceJsonGenerator(traceData);
239
241
  const writeStream = fs.createWriteStream(traceFilename);
240
242
 
241
- // TODO: Can remove promisify(pipeline) in Node 15.
242
- // https://nodejs.org/api/stream.html#stream_stream_pipeline_streams_callback
243
243
  return pipeline(traceIter, writeStream);
244
244
  }
245
245
 
@@ -250,10 +250,12 @@ async function saveTrace(traceData, traceFilename) {
250
250
  * @return {Promise<void>}
251
251
  */
252
252
  function saveDevtoolsLog(devtoolsLog, devtoolLogFilename) {
253
- const logIter = arrayOfObjectsJsonGenerator(devtoolsLog);
254
253
  const writeStream = fs.createWriteStream(devtoolLogFilename);
255
254
 
256
- return pipeline(logIter, writeStream);
255
+ return pipeline(function* () {
256
+ yield* arrayOfObjectsJsonGenerator(devtoolsLog);
257
+ yield '\n';
258
+ }, writeStream);
257
259
  }
258
260
 
259
261
  /**
@@ -109,7 +109,7 @@ module.exports = class ConnectionPool {
109
109
  }
110
110
 
111
111
  // This connection is a match and is available! Update our max if it has a larger congestionWindow
112
- const currentMax = (maxConnection && maxConnection.congestionWindow) || -Infinity;
112
+ const currentMax = (maxConnection?.congestionWindow) || -Infinity;
113
113
  if (connection.congestionWindow > currentMax) maxConnection = connection;
114
114
  }
115
115
 
@@ -390,7 +390,7 @@ class NetworkAnalyzer {
390
390
  // downloading those bytes. We slice up all the network records into start/end boundaries, so
391
391
  // it's easier to deal with the gaps in downloading.
392
392
  const timeBoundaries = networkRecords.reduce((boundaries, record) => {
393
- const scheme = record.parsedURL && record.parsedURL.scheme;
393
+ const scheme = record.parsedURL?.scheme;
394
394
  // Requests whose bodies didn't come over the network or didn't completely finish will mess
395
395
  // with the computation, just skip over them.
396
396
  if (scheme === 'data' || record.failed || !record.finished ||
@@ -25,7 +25,7 @@ const NO_CPU_THROTTLE_METRICS = {
25
25
  */
26
26
  function parseUseragentIntoMetadata(userAgent, formFactor) {
27
27
  const match = userAgent.match(/Chrome\/([\d.]+)/); // eg 'Chrome/(71.0.3577.0)'
28
- const fullVersion = (match && match[1]) || '99.0.1234.0';
28
+ const fullVersion = match?.[1] || '99.0.1234.0';
29
29
  const [version] = fullVersion.split('.', 1);
30
30
  const brands = [
31
31
  {brand: 'Chromium', version},
@@ -73,8 +73,7 @@ function pngSizedAtLeast(sizeRequirement, manifest) {
73
73
  function containsMaskableIcon(manifest) {
74
74
  const iconValues = manifest.icons.value;
75
75
  return iconValues.some(icon => {
76
- return icon.value.purpose &&
77
- icon.value.purpose.value &&
76
+ return icon.value.purpose?.value &&
78
77
  icon.value.purpose.value.includes('maskable');
79
78
  });
80
79
  }
@@ -6,8 +6,7 @@
6
6
  'use strict';
7
7
 
8
8
  /** @param {LH.Result} lhr @param {string} auditName */
9
- const getNumericValue = (lhr, auditName) =>
10
- (lhr.audits[auditName] && lhr.audits[auditName].numericValue) || NaN;
9
+ const getNumericValue = (lhr, auditName) => lhr.audits[auditName]?.numericValue || NaN;
11
10
 
12
11
  /**
13
12
  * @param {Array<number>} numbers
@@ -251,8 +251,8 @@ class NetworkRecorder extends EventEmitter {
251
251
  if (record.redirectSource) {
252
252
  return record.redirectSource;
253
253
  }
254
- const stackFrames = (record.initiator.stack && record.initiator.stack.callFrames) || [];
255
- const initiatorURL = record.initiator.url || (stackFrames[0] && stackFrames[0].url);
254
+ const stackFrames = record.initiator.stack?.callFrames || [];
255
+ const initiatorURL = record.initiator.url || stackFrames[0]?.url;
256
256
 
257
257
  let candidates = recordsByURL.get(initiatorURL) || [];
258
258
  // The initiator must come before the initiated request.
@@ -510,7 +510,7 @@ class NetworkRequest {
510
510
 
511
511
  const reasonHeader = record.responseHeaders
512
512
  .find(header => header.name === 'Non-Authoritative-Reason');
513
- const reason = reasonHeader && reasonHeader.value;
513
+ const reason = reasonHeader?.value;
514
514
  return reason === 'HSTS' && NetworkRequest.isSecureRequest(destination);
515
515
  }
516
516
 
@@ -19,6 +19,11 @@ const stackPacksToInclude = [
19
19
  packId: 'wordpress',
20
20
  requiredStacks: ['js:wordpress'],
21
21
  },
22
+ // waiting for https://github.com/johnmichel/Library-Detector-for-Chrome/pull/193
23
+ // {
24
+ // packId: 'ezoic',
25
+ // requiredStacks: ['js:ezoic'],
26
+ // },
22
27
  {
23
28
  packId: 'drupal',
24
29
  requiredStacks: ['js:drupal'],
@@ -184,8 +184,7 @@ class CpuProfilerModel {
184
184
  if (!event) return false;
185
185
  return Boolean(
186
186
  event.name === SAMPLER_TRACE_EVENT_NAME &&
187
- event.args.data &&
188
- event.args.data._syntheticProfilerRange
187
+ event.args.data?._syntheticProfilerRange
189
188
  );
190
189
  }
191
190
 
@@ -562,9 +561,8 @@ class CpuProfilerModel {
562
561
 
563
562
  // `Profile` or `ProfileChunk` can partially define these across multiple events.
564
563
  // We'll fallback to empty values and worry about validation in the `synthesizeTraceEvents` phase.
565
- const cpuProfileArg = (event.args.data && event.args.data.cpuProfile) || {};
566
- const timeDeltas =
567
- (event.args.data && event.args.data.timeDeltas) || cpuProfileArg.timeDeltas;
564
+ const cpuProfileArg = event.args.data?.cpuProfile || {};
565
+ const timeDeltas = event.args.data?.timeDeltas || cpuProfileArg.timeDeltas;
568
566
  let profile = profiles.get(event.id);
569
567
 
570
568
  if (event.name === 'Profile') {
@@ -572,7 +570,7 @@ class CpuProfilerModel {
572
570
  id: event.id,
573
571
  pid: event.pid,
574
572
  tid: event.tid,
575
- startTime: (event.args.data && event.args.data.startTime) || event.ts,
573
+ startTime: event.args.data?.startTime || event.ts,
576
574
  nodes: cpuProfileArg.nodes || [],
577
575
  samples: cpuProfileArg.samples || [],
578
576
  timeDeltas: timeDeltas || [],
@@ -234,7 +234,7 @@ class MainThreadTasks {
234
234
  // Do bookkeeping on XHR requester data.
235
235
  if (nextTask.event.name === 'XHRReadyStateChange') {
236
236
  const data = nextTask.event.args.data;
237
- const url = data && data.url;
237
+ const url = data?.url;
238
238
  if (data && url && data.readyState === 1) priorTaskData.xhrs.set(url, nextTask);
239
239
  }
240
240
 
@@ -425,15 +425,14 @@ class TraceProcessor {
425
425
  static findMainFrameIds(events) {
426
426
  // Prefer the newer TracingStartedInBrowser event first, if it exists
427
427
  const startedInBrowserEvt = events.find(e => e.name === 'TracingStartedInBrowser');
428
- if (startedInBrowserEvt && startedInBrowserEvt.args.data &&
429
- startedInBrowserEvt.args.data.frames) {
428
+ if (startedInBrowserEvt?.args.data?.frames) {
430
429
  const mainFrame = startedInBrowserEvt.args.data.frames.find(frame => !frame.parent);
431
- const frameId = mainFrame && mainFrame.frame;
432
- const pid = mainFrame && mainFrame.processId;
430
+ const frameId = mainFrame?.frame;
431
+ const pid = mainFrame?.processId;
433
432
 
434
433
  const threadNameEvt = events.find(e => e.pid === pid && e.ph === 'M' &&
435
434
  e.cat === '__metadata' && e.name === 'thread_name' && e.args.name === 'CrRendererMain');
436
- const tid = threadNameEvt && threadNameEvt.tid;
435
+ const tid = threadNameEvt?.tid;
437
436
 
438
437
  if (pid && tid && frameId) {
439
438
  return {
@@ -448,7 +447,7 @@ class TraceProcessor {
448
447
  // The first TracingStartedInPage in the trace is definitely our renderer thread of interest
449
448
  // Beware: the tracingStartedInPage event can appear slightly after a navigationStart
450
449
  const startedInPageEvt = events.find(e => e.name === 'TracingStartedInPage');
451
- if (startedInPageEvt && startedInPageEvt.args && startedInPageEvt.args.data) {
450
+ if (startedInPageEvt?.args?.data) {
452
451
  const frameId = startedInPageEvt.args.data.page;
453
452
  if (frameId) {
454
453
  return {
@@ -463,12 +462,12 @@ class TraceProcessor {
463
462
  // If we can't find either TracingStarted event, then we'll fallback to the first navStart that
464
463
  // looks like it was loading the main frame with a real URL. Because the schema for this event
465
464
  // has changed across Chrome versions, we'll be extra defensive about finding this case.
466
- const navStartEvt = events.find(e => Boolean(e.name === 'navigationStart' && e.args &&
467
- e.args.data && e.args.data.isLoadingMainFrame && e.args.data.documentLoaderURL));
465
+ const navStartEvt = events.find(e => Boolean(e.name === 'navigationStart' &&
466
+ e.args?.data?.isLoadingMainFrame && e.args.data.documentLoaderURL));
468
467
  // Find the first resource that was requested and make sure it agrees on the id.
469
468
  const firstResourceSendEvt = events.find(e => e.name === 'ResourceSendRequest');
470
469
  // We know that these properties exist if we found the events, but TSC doesn't.
471
- if (navStartEvt && navStartEvt.args && navStartEvt.args.data &&
470
+ if (navStartEvt?.args?.data &&
472
471
  firstResourceSendEvt &&
473
472
  firstResourceSendEvt.pid === navStartEvt.pid &&
474
473
  firstResourceSendEvt.tid === navStartEvt.tid) {
@@ -503,7 +502,7 @@ class TraceProcessor {
503
502
  static isLCPEvent(evt) {
504
503
  if (evt.name !== 'largestContentfulPaint::Invalidate' &&
505
504
  evt.name !== 'largestContentfulPaint::Candidate') return false;
506
- return Boolean(evt.args && evt.args.frame);
505
+ return Boolean(evt.args?.frame);
507
506
  }
508
507
 
509
508
  /**
@@ -513,8 +512,7 @@ class TraceProcessor {
513
512
  static isLCPCandidateEvent(evt) {
514
513
  return Boolean(
515
514
  evt.name === 'largestContentfulPaint::Candidate' &&
516
- evt.args &&
517
- evt.args.frame &&
515
+ evt.args?.frame &&
518
516
  evt.args.data &&
519
517
  evt.args.data.size !== undefined
520
518
  );
@@ -613,8 +611,7 @@ class TraceProcessor {
613
611
  .filter(/** @return {evt is FrameCommittedEvent} */ evt => {
614
612
  return Boolean(
615
613
  evt.name === 'FrameCommittedInBrowser' &&
616
- evt.args.data &&
617
- evt.args.data.frame &&
614
+ evt.args.data?.frame &&
618
615
  evt.args.data.url
619
616
  );
620
617
  })
@@ -723,7 +720,7 @@ class TraceProcessor {
723
720
  firstContentfulPaintAllFrames: getTiming(fcpAllFramesEvt.ts),
724
721
  firstMeaningfulPaint: frameTimings.timings.firstMeaningfulPaint,
725
722
  largestContentfulPaint: frameTimings.timings.largestContentfulPaint,
726
- largestContentfulPaintAllFrames: maybeGetTiming(lcpAllFramesEvt && lcpAllFramesEvt.ts),
723
+ largestContentfulPaintAllFrames: maybeGetTiming(lcpAllFramesEvt?.ts),
727
724
  load: frameTimings.timings.load,
728
725
  domContentLoaded: frameTimings.timings.domContentLoaded,
729
726
  traceEnd: timings.traceEnd,
@@ -735,7 +732,7 @@ class TraceProcessor {
735
732
  firstContentfulPaintAllFrames: fcpAllFramesEvt.ts,
736
733
  firstMeaningfulPaint: frameTimings.timestamps.firstMeaningfulPaint,
737
734
  largestContentfulPaint: frameTimings.timestamps.largestContentfulPaint,
738
- largestContentfulPaintAllFrames: lcpAllFramesEvt && lcpAllFramesEvt.ts,
735
+ largestContentfulPaintAllFrames: lcpAllFramesEvt?.ts,
739
736
  load: frameTimings.timestamps.load,
740
737
  domContentLoaded: frameTimings.timestamps.domContentLoaded,
741
738
  traceEnd: timestamps.traceEnd,
@@ -886,7 +883,7 @@ class TraceProcessor {
886
883
  );
887
884
 
888
885
  /** @param {{ts: number}=} event */
889
- const getTimestamp = (event) => event && event.ts;
886
+ const getTimestamp = (event) => event?.ts;
890
887
  /** @type {TraceNavigationTimesForFrame} */
891
888
  const timestamps = {
892
889
  timeOrigin: timeOriginEvt.ts,
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  const {Util} = require('../util-commonjs.js');
13
+ const LHError = require('../lib/lh-error.js');
13
14
 
14
15
  /** @typedef {import('./network-request.js')} NetworkRequest */
15
16
 
@@ -248,6 +249,20 @@ class URLShim extends URL {
248
249
  if (ext === 'jpg') return 'image/jpeg';
249
250
  return `image/${ext}`;
250
251
  }
252
+
253
+ /**
254
+ * @param {string|undefined} url
255
+ * @return {string}
256
+ */
257
+ static normalizeUrl(url) {
258
+ // Verify the url is valid and that protocol is allowed
259
+ if (url && this.isValid(url) && this.isProtocolAllowed(url)) {
260
+ // Use canonicalized URL (with trailing slashes and such)
261
+ return new URL(url).href;
262
+ } else {
263
+ throw new LHError(LHError.errors.INVALID_URL);
264
+ }
265
+ }
251
266
  }
252
267
 
253
268
  URLShim.URL = URL;
@@ -16,7 +16,6 @@ const stackPacks = require('./lib/stack-packs.js');
16
16
  const assetSaver = require('./lib/asset-saver.js');
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
- const URL = require('./lib/url-shim.js');
20
19
  const Sentry = require('./lib/sentry.js');
21
20
  const generateReport = require('../report/generator/report-generator.js').generateReport;
22
21
  const LHError = require('./lib/lh-error.js');
@@ -29,8 +28,8 @@ const {version: lighthouseVersion} = require('../package.json');
29
28
  class Runner {
30
29
  /**
31
30
  * @template {LH.Config.Config | LH.Config.FRConfig} TConfig
32
- * @param {(runnerData: {requestedUrl: string, config: TConfig, driverMock?: Driver}) => Promise<LH.Artifacts>} gatherFn
33
- * @param {{config: TConfig, computedCache: Map<string, ArbitraryEqualityMap>, url?: string, driverMock?: Driver}} runOpts
31
+ * @param {(runnerData: {config: TConfig, driverMock?: Driver}) => Promise<LH.Artifacts>} gatherFn
32
+ * @param {{config: TConfig, computedCache: Map<string, ArbitraryEqualityMap>, driverMock?: Driver}} runOpts
34
33
  * @return {Promise<LH.RunnerResult|undefined>}
35
34
  */
36
35
  static async run(gatherFn, runOpts) {
@@ -49,50 +48,10 @@ class Runner {
49
48
  Sentry.captureBreadcrumb({
50
49
  message: 'Run started',
51
50
  category: 'lifecycle',
52
- data: sentryContext && sentryContext.extra,
51
+ data: sentryContext?.extra,
53
52
  });
54
53
 
55
- // User can run -G solo, -A solo, or -GA together
56
- // -G and -A will run partial lighthouse pipelines,
57
- // and -GA will run everything plus save artifacts and lhr to disk.
58
-
59
- // Gather phase
60
- // Either load saved artifacts off disk or from the browser
61
- let artifacts;
62
- let requestedUrl;
63
- if (settings.auditMode && !settings.gatherMode) {
64
- // No browser required, just load the artifacts from disk.
65
- const path = Runner._getDataSavePath(settings);
66
- artifacts = assetSaver.loadArtifacts(path);
67
- requestedUrl = artifacts.URL.requestedUrl;
68
-
69
- if (!requestedUrl) {
70
- throw new Error('Cannot run audit mode on empty URL');
71
- }
72
- if (runOpts.url && !URL.equalWithExcludedFragments(runOpts.url, requestedUrl)) {
73
- throw new Error('Cannot run audit mode on different URL');
74
- }
75
- } else {
76
- // verify the url is valid and that protocol is allowed
77
- if (runOpts.url && URL.isValid(runOpts.url) && URL.isProtocolAllowed(runOpts.url)) {
78
- // Use canonicalized URL (with trailing slashes and such)
79
- requestedUrl = new URL(runOpts.url).href;
80
- } else {
81
- throw new LHError(LHError.errors.INVALID_URL);
82
- }
83
-
84
- artifacts = await gatherFn({
85
- requestedUrl,
86
- config: runOpts.config,
87
- driverMock: runOpts.driverMock,
88
- });
89
-
90
- // -G means save these to ./latest-run, etc.
91
- if (settings.gatherMode) {
92
- const path = Runner._getDataSavePath(settings);
93
- await assetSaver.saveArtifacts(artifacts, path);
94
- }
95
- }
54
+ const artifacts = await this.gatherAndManageArtifacts(gatherFn, runOpts);
96
55
 
97
56
  // Potentially quit early
98
57
  if (settings.gatherMode && !settings.auditMode) return;
@@ -116,7 +75,7 @@ class Runner {
116
75
 
117
76
  // Use version from gathering stage.
118
77
  // If accessibility gatherer didn't run or errored, it won't be in credits.
119
- const axeVersion = artifacts.Accessibility && artifacts.Accessibility.version;
78
+ const axeVersion = artifacts.Accessibility?.version;
120
79
  const credits = {
121
80
  'axe-core': axeVersion,
122
81
  };
@@ -133,7 +92,7 @@ class Runner {
133
92
  /** @type {LH.RawIcu<LH.Result>} */
134
93
  const i18nLhr = {
135
94
  lighthouseVersion,
136
- requestedUrl,
95
+ requestedUrl: artifacts.URL.requestedUrl,
137
96
  finalUrl: artifacts.URL.finalUrl,
138
97
  fetchTime: artifacts.fetchTime,
139
98
  gatherMode: artifacts.GatherContext.gatherMode,
@@ -184,6 +143,47 @@ class Runner {
184
143
  }
185
144
  }
186
145
 
146
+ /**
147
+ * User can run -G solo, -A solo, or -GA together
148
+ * -G and -A will run partial lighthouse pipelines,
149
+ * and -GA will run everything plus save artifacts and lhr to disk.
150
+ *
151
+ * @template {LH.Config.Config | LH.Config.FRConfig} TConfig
152
+ * @param {(runnerData: {config: TConfig, driverMock?: Driver}) => Promise<LH.Artifacts>} gatherFn
153
+ * @param {{config: TConfig, driverMock?: Driver}} options
154
+ * @return {Promise<LH.Artifacts>}
155
+ */
156
+ static async gatherAndManageArtifacts(gatherFn, options) {
157
+ const settings = options.config.settings;
158
+
159
+ // Gather phase
160
+ // Either load saved artifacts from disk or from the browser.
161
+ let artifacts;
162
+ if (settings.auditMode && !settings.gatherMode) {
163
+ // No browser required, just load the artifacts from disk.
164
+ const path = this._getDataSavePath(settings);
165
+ artifacts = assetSaver.loadArtifacts(path);
166
+ const requestedUrl = artifacts.URL.requestedUrl;
167
+
168
+ if (!requestedUrl) {
169
+ throw new Error('Cannot run audit mode on empty URL');
170
+ }
171
+ } else {
172
+ artifacts = await gatherFn({
173
+ config: options.config,
174
+ driverMock: options.driverMock,
175
+ });
176
+
177
+ // -G means save these to disk (e.g. ./latest-run).
178
+ if (settings.gatherMode) {
179
+ const path = this._getDataSavePath(settings);
180
+ await assetSaver.saveArtifacts(artifacts, path);
181
+ }
182
+ }
183
+
184
+ return artifacts;
185
+ }
186
+
187
187
  /**
188
188
  * This handles both the auditMode case where gatherer entries need to be merged in and
189
189
  * the gather/audit case where timingEntriesFromRunner contains all entries from this run,
@@ -213,7 +213,7 @@ class Runner {
213
213
  };
214
214
  }).sort((a, b) => a.startTime - b.startTime);
215
215
  const runnerEntry = timingEntries.find(e => e.name === 'lh:runner:run');
216
- return {entries: timingEntries, total: runnerEntry && runnerEntry.duration || 0};
216
+ return {entries: timingEntries, total: runnerEntry?.duration || 0};
217
217
  }
218
218
 
219
219
  /**
@@ -263,6 +263,19 @@ class Runner {
263
263
  const normalizedGatherSettings = Object.assign({}, artifacts.settings, overrides);
264
264
  const normalizedAuditSettings = Object.assign({}, settings, overrides);
265
265
 
266
+ // First, try each key individually so we can print which key differed.
267
+ const keys = new Set([
268
+ ...Object.keys(normalizedGatherSettings),
269
+ ...Object.keys(normalizedAuditSettings),
270
+ ]);
271
+ for (const k of keys) {
272
+ if (!isDeepEqual(normalizedGatherSettings[k], normalizedAuditSettings[k])) {
273
+ throw new Error(
274
+ `Cannot change settings between gathering and auditing. Difference found at: ${k}`);
275
+ }
276
+ }
277
+
278
+ // Call `isDeepEqual` on the entire thing, just in case something was missed.
266
279
  if (!isDeepEqual(normalizedGatherSettings, normalizedAuditSettings)) {
267
280
  throw new Error('Cannot change settings between gathering and auditing');
268
281
  }
@@ -367,7 +380,7 @@ class Runner {
367
380
  return narrowedArtifacts;
368
381
  }, /** @type {LH.Artifacts} */ ({}));
369
382
  const product = await audit.audit(narrowedArtifacts, auditContext);
370
- runWarnings.push(...product.runWarnings || []);
383
+ runWarnings.push(...(product.runWarnings || []));
371
384
 
372
385
  auditResult = Audit.generateAuditResult(audit, product);
373
386
  } catch (err) {
@@ -10,10 +10,23 @@
10
10
 
11
11
  set -euo pipefail
12
12
 
13
- if [ "$OSTYPE" == "msys" ]; then
13
+ unameOut="$(uname -s)"
14
+ case "${unameOut}" in
15
+ Linux*) machine=Linux;;
16
+ Darwin*) machine=Mac;;
17
+ MINGW*) machine=MinGw;;
18
+ *) machine="UNKNOWN:${unameOut}"
19
+ esac
20
+
21
+ if [ "$machine" == "MinGw" ]; then
14
22
  url="https://download-chromium.appspot.com/dl/Win?type=snapshots"
15
- else
23
+ elif [ "$machine" == "Linux" ]; then
16
24
  url="https://download-chromium.appspot.com/dl/Linux_x64?type=snapshots"
25
+ elif [ "$machine" == "Mac" ]; then
26
+ url="https://download-chromium.appspot.com/dl/Mac?type=snapshots"
27
+ else
28
+ echo "unsupported platform"
29
+ exit 1
17
30
  fi
18
31
 
19
32
  if [ -e "$CHROME_PATH" ]; then