lighthouse 8.4.0 → 8.5.0-dev.20210921

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 (107) hide show
  1. package/changelog.md +79 -0
  2. package/flow-report/assets/styles.css +85 -61
  3. package/flow-report/src/app.tsx +6 -2
  4. package/flow-report/src/common.tsx +0 -41
  5. package/flow-report/src/icons.tsx +11 -0
  6. package/flow-report/src/summary/summary.tsx +32 -25
  7. package/flow-report/src/topbar.tsx +60 -0
  8. package/flow-report/src/util.ts +0 -1
  9. package/flow-report/src/wrappers/{gauge.tsx → category-score.tsx} +11 -5
  10. package/flow-report/test/summary/summary-test.tsx +3 -3
  11. package/flow-report/tsconfig.json +6 -22
  12. package/{lighthouse-core/gather/gatherers/gather-context.js → lighthouse-cli/.eslintrc.cjs} +20 -18
  13. package/lighthouse-cli/bin.js +31 -21
  14. package/lighthouse-cli/cli-flags.js +12 -7
  15. package/lighthouse-cli/commands/commands.js +2 -7
  16. package/lighthouse-cli/commands/list-audits.js +2 -2
  17. package/lighthouse-cli/commands/list-trace-categories.js +2 -2
  18. package/lighthouse-cli/index.js +3 -1
  19. package/lighthouse-cli/package.json +4 -0
  20. package/lighthouse-cli/printer.js +4 -3
  21. package/lighthouse-cli/run.js +14 -15
  22. package/lighthouse-cli/sentry-prompt.js +5 -6
  23. package/lighthouse-cli/test/smokehouse/frontends/back-compat-util.js +1 -1
  24. package/lighthouse-cli/test/smokehouse/frontends/lib.js +5 -4
  25. package/lighthouse-cli/test/smokehouse/frontends/node.js +1 -1
  26. package/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js +93 -14
  27. package/lighthouse-cli/test/smokehouse/lib/child-process-error.js +1 -1
  28. package/lighthouse-cli/test/smokehouse/lib/concurrent-mapper.js +1 -1
  29. package/lighthouse-cli/test/smokehouse/lib/local-console.js +1 -1
  30. package/lighthouse-cli/test/smokehouse/lighthouse-runners/bundle.js +20 -16
  31. package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +15 -12
  32. package/lighthouse-cli/test/smokehouse/report-assert.js +23 -13
  33. package/lighthouse-cli/test/smokehouse/smokehouse.js +32 -11
  34. package/lighthouse-core/audits/byte-efficiency/byte-efficiency-audit.js +0 -2
  35. package/lighthouse-core/audits/byte-efficiency/uses-responsive-images-snapshot.js +6 -1
  36. package/lighthouse-core/audits/dobetterweb/js-libraries.js +5 -0
  37. package/lighthouse-core/audits/dobetterweb/uses-http2.js +1 -0
  38. package/lighthouse-core/audits/lcp-lazy-loaded.js +1 -0
  39. package/lighthouse-core/audits/oopif-iframe-test-audit.js +30 -0
  40. package/lighthouse-core/computed/metrics/metric.js +2 -1
  41. package/lighthouse-core/config/config-helpers.js +47 -3
  42. package/lighthouse-core/config/config.js +3 -46
  43. package/lighthouse-core/config/default-config.js +0 -1
  44. package/lighthouse-core/fraggle-rock/config/config.js +22 -4
  45. package/lighthouse-core/fraggle-rock/config/default-config.js +0 -9
  46. package/lighthouse-core/fraggle-rock/config/filters.js +3 -0
  47. package/lighthouse-core/fraggle-rock/config/validation.js +19 -1
  48. package/lighthouse-core/fraggle-rock/gather/base-artifacts.js +8 -3
  49. package/lighthouse-core/fraggle-rock/gather/driver.js +10 -0
  50. package/lighthouse-core/fraggle-rock/gather/navigation-runner.js +40 -19
  51. package/lighthouse-core/fraggle-rock/gather/runner-helpers.js +3 -0
  52. package/lighthouse-core/fraggle-rock/gather/session.js +44 -1
  53. package/lighthouse-core/fraggle-rock/gather/snapshot-runner.js +4 -1
  54. package/lighthouse-core/fraggle-rock/gather/timespan-runner.js +5 -1
  55. package/lighthouse-core/gather/devtools-log.js +9 -3
  56. package/lighthouse-core/gather/driver/network-monitor.js +57 -13
  57. package/lighthouse-core/gather/driver/prepare.js +43 -17
  58. package/lighthouse-core/gather/driver/target-manager.js +125 -0
  59. package/lighthouse-core/gather/driver.js +20 -0
  60. package/lighthouse-core/gather/fetcher.js +2 -2
  61. package/lighthouse-core/gather/gather-runner.js +1 -1
  62. package/lighthouse-core/gather/gatherers/css-usage.js +0 -2
  63. package/lighthouse-core/gather/gatherers/devtools-log.js +11 -9
  64. package/lighthouse-core/gather/gatherers/js-usage.js +0 -1
  65. package/lighthouse-core/gather/gatherers/script-elements.js +4 -5
  66. package/lighthouse-core/gather/gatherers/seo/robots-txt.js +3 -3
  67. package/lighthouse-core/gather/gatherers/trace.js +4 -5
  68. package/lighthouse-core/index.js +4 -0
  69. package/lighthouse-core/lib/emulation.js +0 -1
  70. package/lighthouse-core/lib/i18n/locales/en-US.json +6 -0
  71. package/lighthouse-core/lib/i18n/locales/en-XL.json +6 -0
  72. package/lighthouse-core/lib/network-recorder.js +8 -0
  73. package/lighthouse-core/lib/proto-preprocessor.js +5 -10
  74. package/lighthouse-core/runner.js +1 -3
  75. package/package.json +12 -9
  76. package/readme.md +2 -0
  77. package/report/assets/styles.css +120 -12
  78. package/report/assets/templates.html +13 -0
  79. package/report/clients/psi.js +5 -1
  80. package/report/generator/tsconfig.json +5 -16
  81. package/report/renderer/category-renderer.js +58 -5
  82. package/report/renderer/components.js +25 -5
  83. package/report/renderer/performance-category-renderer.js +5 -5
  84. package/report/renderer/pwa-category-renderer.js +10 -0
  85. package/report/renderer/report-renderer.js +12 -3
  86. package/report/renderer/report-ui-features.js +1 -4
  87. package/report/test/renderer/category-renderer-test.js +19 -0
  88. package/report/tsconfig.json +6 -21
  89. package/report/types/report-result.d.ts +1 -1
  90. package/root.js +3 -1
  91. package/third-party/snyk/snapshot.json +2 -2
  92. package/tsconfig-all.json +15 -0
  93. package/tsconfig-base.json +24 -0
  94. package/tsconfig.json +8 -18
  95. package/types/artifacts.d.ts +7 -7
  96. package/types/config.d.ts +1 -1
  97. package/types/gatherer.d.ts +7 -1
  98. package/types/lhr/flow.d.ts +0 -1
  99. package/types/lhr/lhr.d.ts +5 -1
  100. package/types/lhr/tsconfig.json +4 -14
  101. package/types/protocol.d.ts +2 -0
  102. package/types/smokehouse.d.ts +2 -1
  103. package/dist/report/flow.js +0 -149
  104. package/dist/report/standalone.js +0 -228
  105. package/flow-report/test/common-test.tsx +0 -131
  106. package/lighthouse-core/gather/gatherers/host-form-factor.js +0 -31
  107. package/lighthouse-core/gather/gatherers/host-user-agent.js +0 -29
@@ -1,39 +1,23 @@
1
1
  {
2
+ "extends": "../tsconfig-base.json",
2
3
  "compilerOptions": {
3
- "composite": true,
4
4
  "outDir": "../.tmp/tsbuildinfo/flow-report",
5
- "emitDeclarationOnly": false,
6
- "declarationMap": true,
7
5
 
8
6
  // Limit to base JS and DOM defs.
9
7
  "lib": ["es2020", "dom", "dom.iterable"],
10
- // Selectively include types from node_modules.
8
+ // Selectively include types from node_modules/.
11
9
  "types": ["node", "jest"],
12
- "target": "es2020",
13
- "module": "es2020",
14
- "moduleResolution": "node",
15
- "esModuleInterop": true,
16
-
17
- "allowJs": true,
18
- "checkJs": true,
19
- "strict": true,
20
- // TODO: remove the next line to be fully `strict`.
21
- "useUnknownInCatchVariables": false,
22
-
23
- // "listFiles": true,
24
- // "noErrorTruncation": true,
25
- "extendedDiagnostics": true,
26
10
 
27
11
  "jsx": "react-jsx",
28
12
  "jsxImportSource": "preact",
29
13
  },
14
+ "references": [
15
+ {"path": "../types/lhr/"},
16
+ {"path": "../report"},
17
+ ],
30
18
  "include": [
31
19
  "**/*.ts",
32
20
  "**/*.tsx",
33
21
  "./types",
34
22
  ],
35
- "references": [
36
- {"path": "../types/lhr/"},
37
- {"path": "../report"},
38
- ],
39
23
  }
@@ -5,21 +5,23 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
- const FRGatherer = require('../../fraggle-rock/gather/base-gatherer.js');
9
-
10
- class GatherContext extends FRGatherer {
11
- /** @type {LH.Gatherer.GathererMeta} */
12
- meta = {
13
- supportedModes: ['snapshot', 'timespan', 'navigation'],
14
- }
15
-
16
- /**
17
- * @param {LH.Gatherer.FRTransitionalContext} phaseContext
18
- * @return {Promise<LH.Artifacts['GatherContext']>}
19
- */
20
- async getArtifact(phaseContext) {
21
- return {gatherMode: phaseContext.gatherMode};
22
- }
23
- }
24
-
25
- module.exports = GatherContext;
8
+ module.exports = {
9
+ env: {
10
+ browser: true,
11
+ },
12
+ rules: {
13
+ // TODO(esmodules): move to root eslint when all code is ESM
14
+ // or when this is resolved: https://github.com/import-js/eslint-plugin-import/issues/2214
15
+ 'import/order': [2, {
16
+ 'groups': [
17
+ 'builtin',
18
+ 'external',
19
+ ['sibling', 'parent'],
20
+ 'index',
21
+ 'object',
22
+ 'type',
23
+ ],
24
+ 'newlines-between': 'always',
25
+ }],
26
+ },
27
+ };
@@ -18,19 +18,23 @@
18
18
  * cli-flags lh-core/index
19
19
  */
20
20
 
21
- const fs = require('fs');
22
- const path = require('path');
23
- const commands = require('./commands/commands.js');
24
- const printer = require('./printer.js');
25
- const {getFlags} = require('./cli-flags.js');
26
- const {runLighthouse} = require('./run.js');
27
- const {generateConfig} = require('../lighthouse-core/index.js');
28
- const log = require('lighthouse-logger');
29
- const pkg = require('../package.json');
30
- const Sentry = require('../lighthouse-core/lib/sentry.js');
31
- const updateNotifier = require('update-notifier');
32
- const {askPermission} = require('./sentry-prompt.js');
33
- const {LH_ROOT} = require('../root.js');
21
+ import fs from 'fs';
22
+ import path from 'path';
23
+ import url from 'url';
24
+
25
+ import log from 'lighthouse-logger';
26
+ import updateNotifier from 'update-notifier';
27
+
28
+ import * as commands from './commands/commands.js';
29
+ import * as Printer from './printer.js';
30
+ import {getFlags} from './cli-flags.js';
31
+ import {runLighthouse} from './run.js';
32
+ import lighthouse from '../lighthouse-core/index.js';
33
+ import * as Sentry from '../lighthouse-core/lib/sentry.js';
34
+ import {askPermission} from './sentry-prompt.js';
35
+ import {LH_ROOT} from '../root.js';
36
+
37
+ const pkg = JSON.parse(fs.readFileSync(LH_ROOT + '/package.json', 'utf-8'));
34
38
 
35
39
  /**
36
40
  * @return {boolean}
@@ -58,16 +62,22 @@ async function begin() {
58
62
  commands.listTraceCategories();
59
63
  }
60
64
 
61
- const url = cliFlags._[0];
65
+ const urlUnderTest = cliFlags._[0];
62
66
 
63
67
  /** @type {LH.Config.Json|undefined} */
64
68
  let configJson;
65
69
  if (cliFlags.configPath) {
66
70
  // Resolve the config file path relative to where cli was called.
67
71
  cliFlags.configPath = path.resolve(process.cwd(), cliFlags.configPath);
68
- configJson = require(cliFlags.configPath);
72
+
73
+ if (cliFlags.configPath.endsWith('.json')) {
74
+ configJson = JSON.parse(fs.readFileSync(cliFlags.configPath, 'utf-8'));
75
+ } else {
76
+ const configModuleUrl = url.pathToFileURL(cliFlags.configPath).href;
77
+ configJson = (await import(configModuleUrl)).default;
78
+ }
69
79
  } else if (cliFlags.preset) {
70
- configJson = require(`../lighthouse-core/config/${cliFlags.preset}-config.js`);
80
+ configJson = (await import(`../lighthouse-core/config/${cliFlags.preset}-config.js`)).default;
71
81
  }
72
82
 
73
83
  if (cliFlags.budgetPath) {
@@ -88,7 +98,7 @@ async function begin() {
88
98
 
89
99
  if (
90
100
  cliFlags.output.length === 1 &&
91
- cliFlags.output[0] === printer.OutputMode.json &&
101
+ cliFlags.output[0] === Printer.OutputMode.json &&
92
102
  !cliFlags.outputPath
93
103
  ) {
94
104
  cliFlags.outputPath = 'stdout';
@@ -106,7 +116,7 @@ async function begin() {
106
116
  }
107
117
 
108
118
  if (cliFlags.printConfig) {
109
- const config = generateConfig(configJson, cliFlags);
119
+ const config = lighthouse.generateConfig(configJson, cliFlags);
110
120
  process.stdout.write(config.getPrintString());
111
121
  return;
112
122
  }
@@ -119,7 +129,7 @@ async function begin() {
119
129
  }
120
130
  if (cliFlags.enableErrorReporting) {
121
131
  Sentry.init({
122
- url,
132
+ url: urlUnderTest,
123
133
  flags: cliFlags,
124
134
  environmentData: {
125
135
  name: 'redacted', // prevent sentry from using hostname
@@ -132,9 +142,9 @@ async function begin() {
132
142
  });
133
143
  }
134
144
 
135
- return runLighthouse(url, cliFlags, configJson);
145
+ return runLighthouse(urlUnderTest, cliFlags, configJson);
136
146
  }
137
147
 
138
- module.exports = {
148
+ export {
139
149
  begin,
140
150
  };
@@ -7,9 +7,12 @@
7
7
 
8
8
  /* eslint-disable max-len */
9
9
 
10
- const yargs = require('yargs');
11
- const fs = require('fs');
12
- const {isObjectOfUnknownValues} = require('../lighthouse-core/lib/type-verifiers.js');
10
+ import fs from 'fs';
11
+
12
+ import yargs from 'yargs';
13
+ import * as yargsHelpers from 'yargs/helpers';
14
+
15
+ import {isObjectOfUnknownValues} from '../lighthouse-core/lib/type-verifiers.js';
13
16
 
14
17
  /**
15
18
  * @param {string=} manualArgv
@@ -17,8 +20,10 @@ const {isObjectOfUnknownValues} = require('../lighthouse-core/lib/type-verifiers
17
20
  * @return {LH.CliFlags}
18
21
  */
19
22
  function getFlags(manualArgv, options = {}) {
20
- // @ts-expect-error - undocumented, but yargs() supports parsing a single `string`.
21
- const y = manualArgv ? yargs(manualArgv) : yargs;
23
+ const y = manualArgv ?
24
+ // @ts-expect-error - undocumented, but yargs() supports parsing a single `string`.
25
+ yargs(manualArgv) :
26
+ yargs(yargsHelpers.hideBin(process.argv));
22
27
 
23
28
  let parser = y.help('help')
24
29
  .showHelpOnFail(false, 'Specify --help for available options')
@@ -318,7 +323,7 @@ function getFlags(manualArgv, options = {}) {
318
323
  throw new Error('Please provide a url');
319
324
  })
320
325
  .epilogue('For more information on Lighthouse, see https://developers.google.com/web/tools/lighthouse/.')
321
- .wrap(yargs.terminalWidth());
326
+ .wrap(y.terminalWidth());
322
327
 
323
328
  if (options.noExitOnFailure) {
324
329
  // Silence console.error() logging and don't process.exit().
@@ -499,6 +504,6 @@ function coerceScreenEmulation(value) {
499
504
  return screenEmulationSettings;
500
505
  }
501
506
 
502
- module.exports = {
507
+ export {
503
508
  getFlags,
504
509
  };
@@ -5,10 +5,5 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
- const listAudits = require('./list-audits.js');
9
- const listTraceCategories = require('./list-trace-categories.js');
10
-
11
- module.exports = {
12
- listAudits,
13
- listTraceCategories,
14
- };
8
+ export {listAudits} from './list-audits.js';
9
+ export {listTraceCategories} from './list-trace-categories.js';
@@ -5,7 +5,7 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
- const lighthouse = require('../../lighthouse-core/index.js');
8
+ import lighthouse from '../../lighthouse-core/index.js';
9
9
 
10
10
  function listAudits() {
11
11
  const audits = lighthouse.getAuditList().map((i) => i.replace(/\.js$/, ''));
@@ -13,4 +13,4 @@ function listAudits() {
13
13
  process.exit(0);
14
14
  }
15
15
 
16
- module.exports = listAudits;
16
+ export {listAudits};
@@ -5,7 +5,7 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
- const lighthouse = require('../../lighthouse-core/index.js');
8
+ import lighthouse from '../../lighthouse-core/index.js';
9
9
 
10
10
  function listTraceCategories() {
11
11
  const traceCategories = lighthouse.traceCategories;
@@ -13,4 +13,4 @@ function listTraceCategories() {
13
13
  process.exit(0);
14
14
  }
15
15
 
16
- module.exports = listTraceCategories;
16
+ export {listTraceCategories};
@@ -6,7 +6,9 @@
6
6
  */
7
7
  'use strict';
8
8
 
9
- require('./bin.js').begin().catch(err => {
9
+ import {begin} from './bin.js';
10
+
11
+ begin().catch(err => {
10
12
  process.stderr.write(err.stack);
11
13
  process.exit(1);
12
14
  });
@@ -0,0 +1,4 @@
1
+ {
2
+ "type": "module",
3
+ "//": "Any directory that uses `import ... from` or `export ...` must be type module. Temporary file until root package.json is type: module"
4
+ }
@@ -5,8 +5,9 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
- const fs = require('fs');
9
- const log = require('lighthouse-logger');
8
+ import fs from 'fs';
9
+
10
+ import log from 'lighthouse-logger';
10
11
 
11
12
  /**
12
13
  * An enumeration of acceptable output modes:
@@ -91,7 +92,7 @@ function getValidOutputOptions() {
91
92
  return Object.keys(OutputMode);
92
93
  }
93
94
 
94
- module.exports = {
95
+ export {
95
96
  checkOutputPath,
96
97
  write,
97
98
  OutputMode,
@@ -7,20 +7,19 @@
7
7
 
8
8
  /* eslint-disable no-console */
9
9
 
10
- const path = require('path');
11
- const psList = require('ps-list');
10
+ import path from 'path';
12
11
 
13
- const Printer = require('./printer.js');
14
- const ChromeLauncher = require('chrome-launcher');
12
+ import psList from 'ps-list';
13
+ import * as ChromeLauncher from 'chrome-launcher';
14
+ import yargsParser from 'yargs-parser';
15
+ import log from 'lighthouse-logger';
16
+ import open from 'open';
15
17
 
16
- const yargsParser = require('yargs-parser');
17
- const lighthouse = require('../lighthouse-core/index.js');
18
- const log = require('lighthouse-logger');
19
- const getFilenamePrefix = require('../report/generator/file-namer.js').getFilenamePrefix;
20
- const assetSaver = require('../lighthouse-core/lib/asset-saver.js');
21
- const URL = require('../lighthouse-core/lib/url-shim.js');
22
-
23
- const open = require('open');
18
+ import * as Printer from './printer.js';
19
+ import lighthouse from '../lighthouse-core/index.js';
20
+ import {getFilenamePrefix} from '../report/generator/file-namer.js';
21
+ import * as assetSaver from '../lighthouse-core/lib/asset-saver.js';
22
+ import URL from '../lighthouse-core/lib/url-shim.js';
24
23
 
25
24
  /** @typedef {Error & {code: string, friendlyMessage?: string}} ExitError */
26
25
 
@@ -203,8 +202,8 @@ async function potentiallyKillChrome(launchedChrome) {
203
202
  * @return {Promise<LH.RunnerResult|undefined>}
204
203
  */
205
204
  async function runLighthouseWithFraggleRock(url, flags, config, launchedChrome) {
206
- const fraggleRock = require('../lighthouse-core/fraggle-rock/api.js');
207
- const puppeteer = require('puppeteer');
205
+ const fraggleRock = (await import('../lighthouse-core/fraggle-rock/api.js')).default;
206
+ const puppeteer = (await import('puppeteer')).default;
208
207
  const browser = await puppeteer.connect({browserURL: `http://localhost:${launchedChrome.port}`});
209
208
  const page = await browser.newPage();
210
209
  flags.channel = 'fraggle-rock-cli';
@@ -272,7 +271,7 @@ async function runLighthouse(url, flags, config) {
272
271
  }
273
272
  }
274
273
 
275
- module.exports = {
274
+ export {
276
275
  parseChromeFlags,
277
276
  saveResults,
278
277
  runLighthouse,
@@ -5,10 +5,9 @@
5
5
  */
6
6
  'use strict';
7
7
 
8
- const Configstore = require('configstore');
9
- const {Confirm} = require('enquirer');
10
-
11
- const log = require('lighthouse-logger');
8
+ import Configstore from 'configstore';
9
+ import Confirm from 'enquirer';
10
+ import log from 'lighthouse-logger';
12
11
 
13
12
  const MAXIMUM_WAIT_TIME = 20 * 1000;
14
13
 
@@ -29,7 +28,7 @@ function prompt() {
29
28
  /** @type {NodeJS.Timer|undefined} */
30
29
  let timeout;
31
30
 
32
- const prompt = new Confirm({
31
+ const prompt = new Confirm.Confirm({
33
32
  name: 'isErrorReportingEnabled',
34
33
  initial: false,
35
34
  message: MESSAGE,
@@ -75,6 +74,6 @@ function askPermission() {
75
74
  }).catch(_ => false);
76
75
  }
77
76
 
78
- module.exports = {
77
+ export {
79
78
  askPermission,
80
79
  };
@@ -37,6 +37,6 @@ function updateTestDefnFormat(allTestDefns) {
37
37
  return expandedTestDefns.flat();
38
38
  }
39
39
 
40
- module.exports = {
40
+ export {
41
41
  updateTestDefnFormat,
42
42
  };
@@ -13,9 +13,10 @@
13
13
 
14
14
  /* eslint-disable no-console */
15
15
 
16
- const cloneDeep = require('lodash.clonedeep');
17
- const smokeTests = require('../test-definitions/core-tests.js');
18
- const {runSmokehouse} = require('../smokehouse.js');
16
+ import cloneDeep from 'lodash.clonedeep';
17
+
18
+ import smokeTests from '../test-definitions/core-tests.js';
19
+ import {runSmokehouse} from '../smokehouse.js';
19
20
 
20
21
  /**
21
22
  * @param {Smokehouse.SmokehouseLibOptions} options
@@ -43,4 +44,4 @@ async function smokehouse(options) {
43
44
  return runSmokehouse(modifiedTests, smokehouseOptions);
44
45
  }
45
46
 
46
- module.exports = smokehouse;
47
+ export {smokehouse};
@@ -10,4 +10,4 @@
10
10
  */
11
11
 
12
12
  // Smokehouse is runnable from within node, so just a no-op for now.
13
- module.exports = require('../smokehouse.js');
13
+ export * from '../smokehouse.js';
@@ -13,14 +13,22 @@
13
13
 
14
14
  /* eslint-disable no-console */
15
15
 
16
- const path = require('path');
17
- const cloneDeep = require('lodash.clonedeep');
18
- const yargs = require('yargs');
19
- const log = require('lighthouse-logger');
20
- const {runSmokehouse} = require('../smokehouse.js');
21
- const {updateTestDefnFormat} = require('./back-compat-util.js');
16
+ import {strict as assert} from 'assert';
17
+ import path from 'path';
18
+ import fs from 'fs';
19
+ import url from 'url';
22
20
 
23
- const coreTestDefnsPath = path.join(__dirname, '../test-definitions/core-tests.js');
21
+ import cloneDeep from 'lodash.clonedeep';
22
+ import yargs from 'yargs';
23
+ import * as yargsHelpers from 'yargs/helpers';
24
+ import log from 'lighthouse-logger';
25
+
26
+ import {runSmokehouse} from '../smokehouse.js';
27
+ import {updateTestDefnFormat} from './back-compat-util.js';
28
+ import {LH_ROOT} from '../../../../root.js';
29
+
30
+ const coreTestDefnsPath =
31
+ path.join(LH_ROOT, 'lighthouse-cli/test/smokehouse/test-definitions/core-tests.js');
24
32
 
25
33
  /**
26
34
  * Possible Lighthouse runners. Loaded dynamically so e.g. a CLI run isn't
@@ -72,6 +80,52 @@ function getDefinitionsToRun(allTestDefns, requestedIds, {invertMatch}) {
72
80
  return smokes;
73
81
  }
74
82
 
83
+ /**
84
+ * Parses the cli `shardArg` flag into `shardNumber/shardTotal`. Splits
85
+ * `testDefns` into `shardTotal` shards and returns the `shardNumber`th shard.
86
+ * Shards will differ in size by at most 1.
87
+ * Shard params must be 1 ≤ shardNumber ≤ shardTotal.
88
+ * @param {Array<Smokehouse.TestDfn>} testDefns
89
+ * @param {string=} shardArg
90
+ * @return {Array<Smokehouse.TestDfn>}
91
+ */
92
+ function getShardedDefinitions(testDefns, shardArg) {
93
+ if (!shardArg) return testDefns;
94
+
95
+ // eslint-disable-next-line max-len
96
+ const errorMessage = `'shard' must be of the form 'n/d' and n and d must be positive integers with 1 ≤ n ≤ d. Got '${shardArg}'`;
97
+ const match = /^(?<shardNumber>\d+)\/(?<shardTotal>\d+)$/.exec(shardArg);
98
+ assert(match && match.groups, errorMessage);
99
+ const shardNumber = Number(match.groups.shardNumber);
100
+ const shardTotal = Number(match.groups.shardTotal);
101
+ assert(shardNumber > 0 && Number.isInteger(shardNumber), errorMessage);
102
+ assert(shardTotal > 0 && Number.isInteger(shardTotal));
103
+ assert(shardNumber <= shardTotal, errorMessage);
104
+
105
+ // Array is sharded with `Math.ceil(length / shardTotal)` shards first
106
+ // and then the remaining `Math.floor(length / shardTotal) shards.
107
+ // e.g. `[0, 1, 2, 3]` split into 3 shards is `[[0, 1], [2], [3]]`.
108
+ const baseSize = Math.floor(testDefns.length / shardTotal);
109
+ const biggerSize = baseSize + 1;
110
+ const biggerShardCount = testDefns.length % shardTotal;
111
+
112
+ // Since we don't have tests for this file, construct all shards so correct
113
+ // structure can be asserted.
114
+ const shards = [];
115
+ let index = 0;
116
+ for (let i = 0; i < shardTotal; i++) {
117
+ const shardSize = i < biggerShardCount ? biggerSize : baseSize;
118
+ shards.push(testDefns.slice(index, index + shardSize));
119
+ index += shardSize;
120
+ }
121
+ assert.equal(shards.length, shardTotal);
122
+ assert.deepEqual(shards.flat(), testDefns);
123
+
124
+ const shardDefns = shards[shardNumber - 1];
125
+ console.log(`In this shard (${shardArg}), running: ${shardDefns.map(d => d.id).join(' ')}\n`);
126
+ return shardDefns;
127
+ }
128
+
75
129
  /**
76
130
  * Prune the `networkRequests` from the test expectations when `takeNetworkRequestUrls`
77
131
  * is not defined. Custom servers may not have this method available in-process.
@@ -112,7 +166,8 @@ function pruneExpectedNetworkRequests(testDefns, takeNetworkRequestUrls) {
112
166
  * CLI entry point.
113
167
  */
114
168
  async function begin() {
115
- const rawArgv = yargs
169
+ const y = yargs(yargsHelpers.hideBin(process.argv));
170
+ const rawArgv = y
116
171
  .help('help')
117
172
  .usage('node $0 [<options>] <test-ids>')
118
173
  .example('node $0 -j=1 pwa seo', 'run pwa and seo tests serially')
@@ -155,8 +210,13 @@ async function begin() {
155
210
  default: false,
156
211
  describe: 'Run all available tests except the ones provided',
157
212
  },
213
+ 'shard': {
214
+ type: 'string',
215
+ // eslint-disable-next-line max-len
216
+ describe: 'A argument of the form "n/d", which divides the selected tests into d groups and runs the nth group. n and d must be positive integers with 1 ≤ n ≤ d.',
217
+ },
158
218
  })
159
- .wrap(yargs.terminalWidth())
219
+ .wrap(y.terminalWidth())
160
220
  .argv;
161
221
 
162
222
  // Augmenting yargs type with auto-camelCasing breaks in tsc@4.1.2 and @types/yargs@15.0.11,
@@ -170,16 +230,17 @@ async function begin() {
170
230
  if (argv.runner === 'bundle') {
171
231
  console.log('\n✨ Be sure to have recently run this: yarn build-all');
172
232
  }
173
- const lighthouseRunner = require(runnerPath).runLighthouse;
233
+ const {runLighthouse} = await import(runnerPath);
174
234
 
175
235
  // Find test definition file and filter by requestedTestIds.
176
236
  let testDefnPath = argv.testsPath || coreTestDefnsPath;
177
237
  testDefnPath = path.resolve(process.cwd(), testDefnPath);
178
238
  const requestedTestIds = argv._;
179
- const rawTestDefns = require(testDefnPath);
239
+ const {default: rawTestDefns} = await import(url.pathToFileURL(testDefnPath).href);
180
240
  const allTestDefns = updateTestDefnFormat(rawTestDefns);
181
241
  const invertMatch = argv.invertMatch;
182
- const testDefns = getDefinitionsToRun(allTestDefns, requestedTestIds, {invertMatch});
242
+ const requestedTestDefns = getDefinitionsToRun(allTestDefns, requestedTestIds, {invertMatch});
243
+ const testDefns = getShardedDefinitions(requestedTestDefns, argv.shard);
183
244
 
184
245
  let smokehouseResult;
185
246
  let server;
@@ -189,7 +250,7 @@ async function begin() {
189
250
  try {
190
251
  // If running the core tests, spin up the test server.
191
252
  if (testDefnPath === coreTestDefnsPath) {
192
- ({server, serverForOffline} = require('../../fixtures/static-server.js'));
253
+ ({server, serverForOffline} = await import('../../fixtures/static-server.js'));
193
254
  server.listen(10200, 'localhost');
194
255
  serverForOffline.listen(10503, 'localhost');
195
256
  takeNetworkRequestUrls = server.takeRequestUrls.bind(server);
@@ -201,7 +262,7 @@ async function begin() {
201
262
  retries,
202
263
  isDebug: argv.debug,
203
264
  useFraggleRock: argv.fraggleRock,
204
- lighthouseRunner,
265
+ lighthouseRunner: runLighthouse,
205
266
  takeNetworkRequestUrls,
206
267
  };
207
268
 
@@ -213,6 +274,24 @@ async function begin() {
213
274
 
214
275
  if (!smokehouseResult.success) {
215
276
  const failedTestResults = smokehouseResult.testResults.filter(r => r.failed);
277
+
278
+ // For CI, save failed runs to directory to be uploaded.
279
+ if (process.env.CI) {
280
+ const failuresDir = `${LH_ROOT}/.tmp/smokehouse-ci-failures`;
281
+ fs.mkdirSync(failuresDir, {recursive: true});
282
+
283
+ for (const testResult of failedTestResults) {
284
+ for (let i = 0; i < testResult.runs.length; i++) {
285
+ const run = testResult.runs[i];
286
+ fs.writeFileSync(`${failuresDir}/${testResult.id}-${i}.json`, JSON.stringify({
287
+ ...run,
288
+ lighthouseLog: run.lighthouseLog.split('\n'),
289
+ assertionLog: run.assertionLog.split('\n'),
290
+ }, null, 2));
291
+ }
292
+ }
293
+ }
294
+
216
295
  const cmd = `yarn smoke ${failedTestResults.map(r => r.id).join(' ')}`;
217
296
  console.log(`rerun failures: ${cmd}`);
218
297
  }
@@ -23,4 +23,4 @@ class ChildProcessError extends Error {
23
23
  }
24
24
  }
25
25
 
26
- module.exports = ChildProcessError;
26
+ export {ChildProcessError};
@@ -123,4 +123,4 @@ class ConcurrentMapper {
123
123
  }
124
124
  }
125
125
 
126
- module.exports = ConcurrentMapper;
126
+ export {ConcurrentMapper};
@@ -48,4 +48,4 @@ class LocalConsole {
48
48
  }
49
49
  }
50
50
 
51
- module.exports = LocalConsole;
51
+ export {LocalConsole};