lighthouse 9.4.0-dev.20220307 → 9.5.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.
@@ -75,10 +75,13 @@ async function runLighthouse(url, configJson, testRunnerOptions = {}) {
75
75
  await buildDevtoolsPromise;
76
76
 
77
77
  const outputDir = fs.mkdtempSync(os.tmpdir() + '/lh-smoke-cdt-runner-');
78
+ const chromeFlags = [
79
+ `--custom-devtools-frontend=file://${devtoolsDir}/out/Default/gen/front_end`,
80
+ ];
78
81
  const args = [
79
82
  'run-devtools',
80
83
  url,
81
- `--custom-devtools-frontend=file://${devtoolsDir}/out/Default/gen/front_end`,
84
+ `--chrome-flags=${chromeFlags.join(' ')}`,
82
85
  '--output-dir', outputDir,
83
86
  ];
84
87
  if (configJson) {
@@ -5,16 +5,16 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
+ const {UserFlow, auditGatherSteps} = require('./user-flow.js');
8
9
  const {snapshotGather} = require('./gather/snapshot-runner.js');
9
10
  const {startTimespanGather} = require('./gather/timespan-runner.js');
10
11
  const {navigationGather} = require('./gather/navigation-runner.js');
11
12
  const {generateFlowReportHtml} = require('../../report/generator/report-generator.js');
12
13
  const Runner = require('../runner.js');
13
- const UserFlow = require('./user-flow.js');
14
14
 
15
15
  /**
16
16
  * @param {import('puppeteer').Page} page
17
- * @param {UserFlow.UserFlowOptions} [options]
17
+ * @param {ConstructorParameters<LH.UserFlow>[1]} [options]
18
18
  */
19
19
  async function startFlow(page, options) {
20
20
  return new UserFlow(page, options);
@@ -58,10 +58,20 @@ async function generateFlowReport(flowResult) {
58
58
  return generateFlowReportHtml(flowResult);
59
59
  }
60
60
 
61
+ /**
62
+ * @param {LH.UserFlow.FlowArtifacts} flowArtifacts
63
+ * @param {LH.Config.Json} [config]
64
+ */
65
+ async function auditFlowArtifacts(flowArtifacts, config) {
66
+ const {gatherSteps, name} = flowArtifacts;
67
+ return await auditGatherSteps(gatherSteps, {name, config});
68
+ }
69
+
61
70
  module.exports = {
62
71
  snapshot,
63
72
  startTimespan,
64
73
  navigation,
65
74
  startFlow,
66
75
  generateFlowReport,
76
+ auditFlowArtifacts,
67
77
  };
@@ -10,11 +10,12 @@ const {snapshotGather} = require('./gather/snapshot-runner.js');
10
10
  const {startTimespanGather} = require('./gather/timespan-runner.js');
11
11
  const {navigationGather} = require('./gather/navigation-runner.js');
12
12
  const Runner = require('../runner.js');
13
+ const {initializeConfig} = require('./config/config.js');
13
14
 
14
15
  /** @typedef {Parameters<snapshotGather>[0]} FrOptions */
15
16
  /** @typedef {Omit<FrOptions, 'page'> & {name?: string}} UserFlowOptions */
16
17
  /** @typedef {Omit<FrOptions, 'page'> & {stepName?: string}} StepOptions */
17
- /** @typedef {{gatherResult: LH.Gatherer.FRGatherResult, name: string}} StepArtifact */
18
+ /** @typedef {WeakMap<LH.UserFlow.GatherStep, LH.Gatherer.FRGatherResult['runnerOptions']>} GatherStepRunnerOptions */
18
19
 
19
20
  class UserFlow {
20
21
  /**
@@ -26,8 +27,10 @@ class UserFlow {
26
27
  this.options = {page, ...options};
27
28
  /** @type {string|undefined} */
28
29
  this.name = options?.name;
29
- /** @type {StepArtifact[]} */
30
- this.stepArtifacts = [];
30
+ /** @type {LH.UserFlow.GatherStep[]} */
31
+ this._gatherSteps = [];
32
+ /** @type {GatherStepRunnerOptions} */
33
+ this._gatherStepRunnerOptions = new WeakMap();
31
34
  }
32
35
 
33
36
  /**
@@ -68,8 +71,8 @@ class UserFlow {
68
71
  }
69
72
 
70
73
  // On repeat navigations, we want to disable storage reset by default (i.e. it's not a cold load).
71
- const isSubsequentNavigation = this.stepArtifacts
72
- .some(step => step.gatherResult.artifacts.GatherContext.gatherMode === 'navigation');
74
+ const isSubsequentNavigation = this._gatherSteps
75
+ .some(step => step.artifacts.GatherContext.gatherMode === 'navigation');
73
76
  if (isSubsequentNavigation) {
74
77
  if (settingsOverrides.disableStorageReset === undefined) {
75
78
  settingsOverrides.disableStorageReset = true;
@@ -82,6 +85,23 @@ class UserFlow {
82
85
  return options;
83
86
  }
84
87
 
88
+ /**
89
+ *
90
+ * @param {LH.Gatherer.FRGatherResult} gatherResult
91
+ * @param {StepOptions} options
92
+ */
93
+ _addGatherStep(gatherResult, options) {
94
+ const providedName = options?.stepName;
95
+ const gatherStep = {
96
+ artifacts: gatherResult.artifacts,
97
+ name: providedName || this._getDefaultStepName(gatherResult.artifacts),
98
+ config: options.config,
99
+ configContext: options.configContext,
100
+ };
101
+ this._gatherSteps.push(gatherStep);
102
+ this._gatherStepRunnerOptions.set(gatherStep, gatherResult.runnerOptions);
103
+ }
104
+
85
105
  /**
86
106
  * @param {LH.NavigationRequestor} requestor
87
107
  * @param {StepOptions=} stepOptions
@@ -89,16 +109,10 @@ class UserFlow {
89
109
  async navigate(requestor, stepOptions) {
90
110
  if (this.currentTimespan) throw new Error('Timespan already in progress');
91
111
 
92
- const gatherResult = await navigationGather(
93
- requestor,
94
- this._getNextNavigationOptions(stepOptions)
95
- );
112
+ const options = this._getNextNavigationOptions(stepOptions);
113
+ const gatherResult = await navigationGather(requestor, options);
96
114
 
97
- const providedName = stepOptions?.stepName;
98
- this.stepArtifacts.push({
99
- gatherResult,
100
- name: providedName || this._getDefaultStepName(gatherResult.artifacts),
101
- });
115
+ this._addGatherStep(gatherResult, options);
102
116
 
103
117
  return gatherResult;
104
118
  }
@@ -121,11 +135,7 @@ class UserFlow {
121
135
  const gatherResult = await timespan.endTimespanGather();
122
136
  this.currentTimespan = undefined;
123
137
 
124
- const providedName = options?.stepName;
125
- this.stepArtifacts.push({
126
- gatherResult,
127
- name: providedName || this._getDefaultStepName(gatherResult.artifacts),
128
- });
138
+ this._addGatherStep(gatherResult, options);
129
139
 
130
140
  return gatherResult;
131
141
  }
@@ -139,11 +149,7 @@ class UserFlow {
139
149
  const options = {...this.options, ...stepOptions};
140
150
  const gatherResult = await snapshotGather(options);
141
151
 
142
- const providedName = stepOptions?.stepName;
143
- this.stepArtifacts.push({
144
- gatherResult,
145
- name: providedName || this._getDefaultStepName(gatherResult.artifacts),
146
- });
152
+ this._addGatherStep(gatherResult, options);
147
153
 
148
154
  return gatherResult;
149
155
  }
@@ -152,21 +158,11 @@ class UserFlow {
152
158
  * @returns {Promise<LH.FlowResult>}
153
159
  */
154
160
  async createFlowResult() {
155
- if (!this.stepArtifacts.length) {
156
- throw new Error('Need at least one step before getting the result');
157
- }
158
- const url = new URL(this.stepArtifacts[0].gatherResult.artifacts.URL.finalUrl);
159
- const flowName = this.name || `User flow (${url.hostname})`;
160
-
161
- /** @type {LH.FlowResult['steps']} */
162
- const steps = [];
163
- for (const {gatherResult, name} of this.stepArtifacts) {
164
- const result = await Runner.audit(gatherResult.artifacts, gatherResult.runnerOptions);
165
- if (!result) throw new Error(`Step "${name}" did not return a result`);
166
- steps.push({lhr: result.lhr, name});
167
- }
168
-
169
- return {steps, name: flowName};
161
+ return auditGatherSteps(this._gatherSteps, {
162
+ name: this.name,
163
+ config: this.options.config,
164
+ gatherStepRunnerOptions: this._gatherStepRunnerOptions,
165
+ });
170
166
  }
171
167
 
172
168
  /**
@@ -176,6 +172,58 @@ class UserFlow {
176
172
  const flowResult = await this.createFlowResult();
177
173
  return generateFlowReportHtml(flowResult);
178
174
  }
175
+
176
+ /**
177
+ * @return {LH.UserFlow.FlowArtifacts}
178
+ */
179
+ createArtifactsJson() {
180
+ return {
181
+ gatherSteps: this._gatherSteps,
182
+ name: this.name,
183
+ };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * @param {Array<LH.UserFlow.GatherStep>} gatherSteps
189
+ * @param {{name?: string, config?: LH.Config.Json, gatherStepRunnerOptions?: GatherStepRunnerOptions}} options
190
+ */
191
+ async function auditGatherSteps(gatherSteps, options) {
192
+ if (!gatherSteps.length) {
193
+ throw new Error('Need at least one step before getting the result');
194
+ }
195
+
196
+ /** @type {LH.FlowResult['steps']} */
197
+ const steps = [];
198
+ for (const gatherStep of gatherSteps) {
199
+ const {artifacts, name, configContext} = gatherStep;
200
+
201
+ let runnerOptions = options.gatherStepRunnerOptions?.get(gatherStep);
202
+
203
+ // If the gather step is not active, we must recreate the runner options.
204
+ if (!runnerOptions) {
205
+ // Step specific configs take precedence over a config for the entire flow.
206
+ const configJson = gatherStep.config || options.config;
207
+ const {gatherMode} = artifacts.GatherContext;
208
+ const {config} = initializeConfig(configJson, {...configContext, gatherMode});
209
+ runnerOptions = {
210
+ config,
211
+ computedCache: new Map(),
212
+ };
213
+ }
214
+
215
+ const result = await Runner.audit(artifacts, runnerOptions);
216
+ if (!result) throw new Error(`Step "${name}" did not return a result`);
217
+ steps.push({lhr: result.lhr, name});
218
+ }
219
+
220
+ const url = new URL(gatherSteps[0].artifacts.URL.finalUrl);
221
+ const flowName = options.name || `User flow (${url.hostname})`;
222
+ return {steps, name: flowName};
179
223
  }
180
224
 
181
- module.exports = UserFlow;
225
+
226
+ module.exports = {
227
+ UserFlow,
228
+ auditGatherSteps,
229
+ };
@@ -70,21 +70,22 @@ class FullPageScreenshot extends FRGatherer {
70
70
 
71
71
  /**
72
72
  * @param {LH.Gatherer.FRTransitionalContext} context
73
+ * @param {{height: number, width: number, mobile: boolean}} deviceMetrics
73
74
  * @return {Promise<LH.Artifacts.FullPageScreenshot['screenshot']>}
74
75
  */
75
- async _takeScreenshot(context) {
76
+ async _takeScreenshot(context, deviceMetrics) {
76
77
  const session = context.driver.defaultSession;
77
78
  const maxTextureSize = await this.getMaxTextureSize(context);
78
79
  const metrics = await session.sendCommand('Page.getLayoutMetrics');
79
80
 
80
- // Width should match emulated width, without considering content overhang.
81
- // Both layoutViewport and visualViewport capture this. visualViewport accounts
82
- // for page zoom/scale, which we currently don't account for (or expect). So we use layoutViewport.width.
83
- // Note: If the page is zoomed, many assumptions fail.
84
- //
85
- // Height should be as tall as the content. So we use contentSize.height
86
- const width = Math.min(metrics.layoutViewport.clientWidth, maxTextureSize);
87
- const height = Math.min(metrics.contentSize.height, maxTextureSize);
81
+ // Height should be as tall as the content.
82
+ // Scale the emulated height to reach the content height.
83
+ const fullHeight = Math.round(
84
+ deviceMetrics.height *
85
+ metrics.contentSize.height /
86
+ metrics.layoutViewport.clientHeight
87
+ );
88
+ const height = Math.min(fullHeight, maxTextureSize);
88
89
 
89
90
  // Setup network monitor before we change the viewport.
90
91
  const networkMonitor = new NetworkMonitor(session);
@@ -98,13 +99,10 @@ class FullPageScreenshot extends FRGatherer {
98
99
  await networkMonitor.enable();
99
100
 
100
101
  await session.sendCommand('Emulation.setDeviceMetricsOverride', {
101
- // If we're gathering with mobile screenEmulation on (overlay scrollbars, etc), continue to use that for this screenshot.
102
- mobile: context.settings.screenEmulation.mobile,
103
- height,
104
- width,
102
+ mobile: deviceMetrics.mobile,
105
103
  deviceScaleFactor: 1,
106
- scale: 1,
107
- screenOrientation: {angle: 0, type: 'portraitPrimary'},
104
+ height,
105
+ width: 0, // Leave width unchanged
108
106
  });
109
107
 
110
108
  // Now that the viewport is taller, give the page some time to fetch new resources that
@@ -127,7 +125,7 @@ class FullPageScreenshot extends FRGatherer {
127
125
 
128
126
  return {
129
127
  data,
130
- width,
128
+ width: deviceMetrics.width,
131
129
  height,
132
130
  };
133
131
  }
@@ -185,29 +183,37 @@ class FullPageScreenshot extends FRGatherer {
185
183
  const session = context.driver.defaultSession;
186
184
  const executionContext = context.driver.executionContext;
187
185
  const settings = context.settings;
188
-
189
- // In case some other program is controlling emulation, remember what the device looks
190
- // like now and reset after gatherer is done.
191
- let observedDeviceMetrics;
192
186
  const lighthouseControlsEmulation = !settings.screenEmulation.disabled;
187
+
188
+ // Make a copy so we don't modify the config settings.
189
+ /** @type {{width: number, height: number, deviceScaleFactor: number, mobile: boolean}} */
190
+ const deviceMetrics = {...settings.screenEmulation};
191
+
192
+ // In case some other program is controlling emulation, remember what the device looks like now and reset after gatherer is done.
193
+ // If we're gathering with mobile screenEmulation on (overlay scrollbars, etc), continue to use that for this screenshot.
193
194
  if (!lighthouseControlsEmulation) {
194
- observedDeviceMetrics = await executionContext.evaluate(getObservedDeviceMetrics, {
195
+ const observedDeviceMetrics = await executionContext.evaluate(getObservedDeviceMetrics, {
195
196
  args: [],
196
197
  useIsolation: true,
197
198
  deps: [kebabCaseToCamelCase],
198
199
  });
200
+ deviceMetrics.height = observedDeviceMetrics.height;
201
+ deviceMetrics.width = observedDeviceMetrics.width;
202
+ deviceMetrics.deviceScaleFactor = observedDeviceMetrics.deviceScaleFactor;
203
+ // If screen emulation is disabled, use formFactor to determine if we are on mobile.
204
+ deviceMetrics.mobile = settings.formFactor === 'mobile';
199
205
  }
200
206
 
201
207
  try {
202
208
  return {
203
- screenshot: await this._takeScreenshot(context),
209
+ screenshot: await this._takeScreenshot(context, deviceMetrics),
204
210
  nodes: await this._resolveNodes(context),
205
211
  };
206
212
  } finally {
207
213
  // Revert resized page.
208
214
  if (lighthouseControlsEmulation) {
209
215
  await emulation.emulate(session, settings);
210
- } else if (observedDeviceMetrics) {
216
+ } else {
211
217
  // Best effort to reset emulation to what it was.
212
218
  // https://github.com/GoogleChrome/lighthouse/pull/10716#discussion_r428970681
213
219
  // TODO: seems like this would be brittle. Should at least work for devtools, but what
@@ -216,8 +222,10 @@ class FullPageScreenshot extends FRGatherer {
216
222
  // and then just call that to reset?
217
223
  // https://github.com/GoogleChrome/lighthouse/issues/11122
218
224
  await session.sendCommand('Emulation.setDeviceMetricsOverride', {
219
- mobile: settings.formFactor === 'mobile',
220
- ...observedDeviceMetrics,
225
+ mobile: deviceMetrics.mobile,
226
+ deviceScaleFactor: deviceMetrics.deviceScaleFactor,
227
+ height: deviceMetrics.height,
228
+ width: 0, // Leave width unchanged
221
229
  });
222
230
  }
223
231
  }
@@ -313,6 +313,20 @@ async function saveLanternNetworkData(devtoolsLog, outputPath) {
313
313
  fs.writeFileSync(outputPath, JSON.stringify(lanternData));
314
314
  }
315
315
 
316
+ /**
317
+ * Normalize timing data so it doesn't change every update.
318
+ * @param {LH.Result.MeasureEntry[]} timings
319
+ */
320
+ function normalizeTimingEntries(timings) {
321
+ let baseTime = 0;
322
+ for (const timing of timings) {
323
+ // @ts-expect-error: Value actually is writeable at this point.
324
+ timing.startTime = baseTime++;
325
+ // @ts-expect-error: Value actually is writeable at this point.
326
+ timing.duration = 1;
327
+ }
328
+ }
329
+
316
330
  module.exports = {
317
331
  saveArtifacts,
318
332
  saveLhr,
@@ -323,4 +337,5 @@ module.exports = {
323
337
  saveDevtoolsLog,
324
338
  saveLanternNetworkData,
325
339
  stringifyReplacer,
340
+ normalizeTimingEntries,
326
341
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lighthouse",
3
- "version": "9.4.0-dev.20220307",
3
+ "version": "9.5.0",
4
4
  "description": "Automated auditing, performance metrics, and best practices for the web.",
5
5
  "main": "./lighthouse-core/index.js",
6
6
  "bin": {
@@ -76,6 +76,7 @@
76
76
  "update:sample-artifacts": "node lighthouse-core/scripts/update-report-fixtures.js",
77
77
  "update:sample-json": "yarn i18n:collect-strings && node ./lighthouse-cli -A=./lighthouse-core/test/results/artifacts --config-path=./lighthouse-core/test/results/sample-config.js --output=json --output-path=./lighthouse-core/test/results/sample_v2.json && node lighthouse-core/scripts/cleanup-LHR-for-diff.js ./lighthouse-core/test/results/sample_v2.json --only-remove-timing",
78
78
  "update:flow-sample-json": "node ./lighthouse-core/scripts/update-flow-fixtures.js",
79
+ "update:snapshot-sample-json": "node ./lighthouse-core/scripts/update-snapshot-sample.js",
79
80
  "update:test-devtools": "bash lighthouse-core/test/chromium-web-tests/test-locally.sh --reset-results",
80
81
  "test-devtools": "bash lighthouse-core/test/chromium-web-tests/test-locally.sh",
81
82
  "open-devtools": "bash lighthouse-core/scripts/open-devtools.sh",
@@ -189,7 +189,8 @@ limitations under the License.
189
189
  animation: none;
190
190
  }
191
191
 
192
- .lh-sticky-header .lh-gauge__label {
192
+ .lh-sticky-header .lh-gauge__label,
193
+ .lh-sticky-header .lh-fraction__label {
193
194
  display: none;
194
195
  }
195
196
 
@@ -445,7 +445,7 @@ function createScorescaleComponent(dom) {
445
445
  function createScoresWrapperComponent(dom) {
446
446
  const el0 = dom.createFragment();
447
447
  const el1 = dom.createElement('style');
448
- el1.append('\n .lh-scores-container {\n display: flex;\n flex-direction: column;\n padding: var(--default-padding) 0;\n position: relative;\n width: 100%;\n }\n\n .lh-sticky-header {\n --gauge-circle-size: var(--gauge-circle-size-sm);\n --plugin-badge-size: 16px;\n --plugin-icon-size: 75%;\n --gauge-wrapper-width: 60px;\n --gauge-percentage-font-size: 13px;\n position: fixed;\n left: 0;\n right: 0;\n top: var(--topbar-height);\n font-weight: 500;\n display: none;\n justify-content: center;\n background-color: var(--sticky-header-background-color);\n border-bottom: 1px solid var(--color-gray-200);\n padding-top: var(--score-container-padding);\n padding-bottom: 4px;\n z-index: 1;\n pointer-events: none;\n }\n\n .lh-devtools .lh-sticky-header {\n /* The report within DevTools is placed in a container with overflow, which changes the placement of this header unless we change `position` to `sticky.` */\n position: sticky;\n }\n\n .lh-sticky-header--visible {\n display: grid;\n grid-auto-flow: column;\n pointer-events: auto;\n }\n\n /* Disable the gauge arc animation for the sticky header, so toggling display: none\n does not play the animation. */\n .lh-sticky-header .lh-gauge-arc {\n animation: none;\n }\n\n .lh-sticky-header .lh-gauge__label {\n display: none;\n }\n\n .lh-highlighter {\n width: var(--gauge-wrapper-width);\n height: 1px;\n background-color: var(--highlighter-background-color);\n /* Position at bottom of first gauge in sticky header. */\n position: absolute;\n grid-column: 1;\n bottom: -1px;\n }\n\n .lh-gauge__wrapper:first-of-type {\n contain: none;\n }\n ');
448
+ el1.append('\n .lh-scores-container {\n display: flex;\n flex-direction: column;\n padding: var(--default-padding) 0;\n position: relative;\n width: 100%;\n }\n\n .lh-sticky-header {\n --gauge-circle-size: var(--gauge-circle-size-sm);\n --plugin-badge-size: 16px;\n --plugin-icon-size: 75%;\n --gauge-wrapper-width: 60px;\n --gauge-percentage-font-size: 13px;\n position: fixed;\n left: 0;\n right: 0;\n top: var(--topbar-height);\n font-weight: 500;\n display: none;\n justify-content: center;\n background-color: var(--sticky-header-background-color);\n border-bottom: 1px solid var(--color-gray-200);\n padding-top: var(--score-container-padding);\n padding-bottom: 4px;\n z-index: 1;\n pointer-events: none;\n }\n\n .lh-devtools .lh-sticky-header {\n /* The report within DevTools is placed in a container with overflow, which changes the placement of this header unless we change `position` to `sticky.` */\n position: sticky;\n }\n\n .lh-sticky-header--visible {\n display: grid;\n grid-auto-flow: column;\n pointer-events: auto;\n }\n\n /* Disable the gauge arc animation for the sticky header, so toggling display: none\n does not play the animation. */\n .lh-sticky-header .lh-gauge-arc {\n animation: none;\n }\n\n .lh-sticky-header .lh-gauge__label,\n .lh-sticky-header .lh-fraction__label {\n display: none;\n }\n\n .lh-highlighter {\n width: var(--gauge-wrapper-width);\n height: 1px;\n background-color: var(--highlighter-background-color);\n /* Position at bottom of first gauge in sticky header. */\n position: absolute;\n grid-column: 1;\n bottom: -1px;\n }\n\n .lh-gauge__wrapper:first-of-type {\n contain: none;\n }\n ');
449
449
  el0.append(el1);
450
450
  const el2 = dom.createElement('div', 'lh-scores-wrapper');
451
451
  const el3 = dom.createElement('div', 'lh-scores-container');
@@ -306,7 +306,8 @@ export class TopbarFeatures {
306
306
  categoriesAboveTheMiddle.length > 0 ? categoriesAboveTheMiddle.length - 1 : 0;
307
307
 
308
308
  // Category order matches gauge order in sticky header.
309
- const gaugeWrapperEls = this.stickyHeaderEl.querySelectorAll('.lh-gauge__wrapper');
309
+ const gaugeWrapperEls =
310
+ this.stickyHeaderEl.querySelectorAll('.lh-gauge__wrapper, .lh-fraction__wrapper');
310
311
  const gaugeToHighlight = gaugeWrapperEls[highlightIndex];
311
312
  const origin = gaugeWrapperEls[0].getBoundingClientRect().left;
312
313
  const offset = gaugeToHighlight.getBoundingClientRect().left - origin;
@@ -133,6 +133,7 @@
133
133
  {"id":"npm:mustache:20110814","severity":"medium","semver":{"vulnerable":["< 0.3.1"]}}
134
134
  ],
135
135
  "next":[
136
+ {"id":"SNYK-JS-NEXT-2405694","severity":"medium","semver":{"vulnerable":[">=10.0.0 <12.1.0"]}},
136
137
  {"id":"SNYK-JS-NEXT-2388583","severity":"medium","semver":{"vulnerable":[">=12.0.0 <12.0.9"]}},
137
138
  {"id":"SNYK-JS-NEXT-2312745","severity":"high","semver":{"vulnerable":[">=12.0.0 <12.0.5",">=11.1.0 <11.1.3"]}},
138
139
  {"id":"SNYK-JS-NEXT-1577139","severity":"medium","semver":{"vulnerable":[">=10.0.0 <11.1.1"]}},
@@ -21,6 +21,7 @@ import Protocol_ from './protocol';
21
21
  import * as Settings from './lhr/settings';
22
22
  import StructuredData_ from './structured-data';
23
23
  import Treemap_ from './lhr/treemap';
24
+ import UserFlow_ from './user-flow';
24
25
 
25
26
  // Construct hierarchy of global types under the LH namespace.
26
27
  declare global {
@@ -46,6 +47,8 @@ declare global {
46
47
  export import CrdpEvents = _CrdpMappings.Events;
47
48
  export import CrdpCommands = _CrdpMappings.Commands;
48
49
 
50
+ export import UserFlow = UserFlow_;
51
+
49
52
  // externs.d.ts
50
53
  export import Flags = Externs.Flags;
51
54
  export import CliFlags = Externs.CliFlags;
@@ -0,0 +1,19 @@
1
+ import {UserFlow as UserFlow_} from '../lighthouse-core/fraggle-rock/user-flow';
2
+
3
+ declare module UserFlow {
4
+ export interface FlowArtifacts {
5
+ gatherSteps: GatherStep[];
6
+ name?: string;
7
+ }
8
+
9
+ export interface GatherStep {
10
+ artifacts: LH.Artifacts;
11
+ name: string;
12
+ config?: LH.Config.Json;
13
+ configContext?: LH.Config.FRContext;
14
+ }
15
+ }
16
+
17
+ type UserFlow = typeof UserFlow_;
18
+
19
+ export default UserFlow;