browsertime 27.2.0 → 27.4.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 (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/browsertime.js +31 -5
  3. package/bin/browsertimeWebPageReplay.js +1 -1
  4. package/lib/chrome/coverage.js +198 -0
  5. package/lib/chrome/webdriver/chromium.js +14 -0
  6. package/lib/core/engine/collector.js +8 -0
  7. package/lib/core/engine/command/measure.js +4 -5
  8. package/lib/core/engine/commands.js +14 -26
  9. package/lib/core/engine/context.js +4 -9
  10. package/lib/core/engine/index.js +6 -2
  11. package/lib/core/seleniumRunner.js +1 -2
  12. package/lib/safari/safariInspectorClient.js +0 -1
  13. package/lib/screenshot/index.js +1 -1
  14. package/lib/support/browserScript.js +2 -4
  15. package/lib/support/cli.js +56 -21
  16. package/lib/support/har/index.js +6 -3
  17. package/lib/support/helpTopics.js +174 -0
  18. package/lib/support/util.js +46 -0
  19. package/lib/video/postprocessing/finetune/addTextToVideo.js +1 -1
  20. package/package.json +16 -15
  21. package/scripting.d.ts +21 -0
  22. package/types/android/index.d.ts +2 -2
  23. package/types/android/index.d.ts.map +1 -1
  24. package/types/core/engine/command/measure.d.ts +6 -6
  25. package/types/core/engine/command/measure.d.ts.map +1 -1
  26. package/types/core/engine/command/simpleperf.d.ts +1 -1
  27. package/types/core/engine/command/simpleperf.d.ts.map +1 -1
  28. package/types/core/engine/commands.d.ts +51 -48
  29. package/types/core/engine/commands.d.ts.map +1 -1
  30. package/types/core/engine/context.d.ts +9 -8
  31. package/types/core/engine/context.d.ts.map +1 -1
  32. package/types/support/tcpdump.d.ts +1 -1
  33. package/types/support/tcpdump.d.ts.map +1 -1
  34. package/types/support/util.d.ts +5 -0
  35. package/types/support/util.d.ts.map +1 -1
  36. package/types/video/postprocessing/finetune/addTextToVideo.d.ts +1 -1
  37. package/types/video/postprocessing/finetune/addTextToVideo.d.ts.map +1 -1
  38. package/types/video/postprocessing/finetune/convertFps.d.ts +1 -1
  39. package/types/video/postprocessing/finetune/convertFps.d.ts.map +1 -1
  40. package/types/video/postprocessing/finetune/removeOrange.d.ts +1 -1
  41. package/types/video/postprocessing/finetune/removeOrange.d.ts.map +1 -1
  42. package/types/video/postprocessing/visualmetrics/visualMetrics.d.ts +1 -1
  43. package/types/video/postprocessing/visualmetrics/visualMetrics.d.ts.map +1 -1
  44. package/types/video/screenRecording/desktop/convert.d.ts +1 -1
  45. package/types/video/screenRecording/desktop/convert.d.ts.map +1 -1
  46. package/types/video/screenRecording/desktop/desktopRecorder.d.ts +2 -2
  47. package/types/video/screenRecording/desktop/ffmpegRecorder.d.ts +1 -1
  48. package/types/video/screenRecording/desktop/osx/getSPDisplaysDataType.d.ts +1 -1
  49. package/types/video/screenRecording/desktop/osx/getSPDisplaysDataType.d.ts.map +1 -1
  50. package/types/video/screenRecording/desktop/osx/getScreen.d.ts +1 -1
  51. package/types/video/screenRecording/desktop/osx/getScreen.d.ts.map +1 -1
  52. package/types/video/screenRecording/iosSimulator/convertToMp4.d.ts +1 -1
  53. package/types/video/screenRecording/iosSimulator/convertToMp4.d.ts.map +1 -1
  54. package/types/video/screenRecording/iosSimulator/recorder.d.ts +2 -6
  55. package/types/video/screenRecording/iosSimulator/recorder.d.ts.map +1 -1
  56. package/types/scripting.d.ts +0 -2
@@ -1,6 +1,5 @@
1
1
  import { arch as _arch } from 'node:os';
2
2
  import { getLogger } from '@sitespeed.io/log';
3
- import merge from 'lodash.merge';
4
3
  import { execaCommand as command } from 'execa';
5
4
  import { StorageManager } from '../../support/storageManager.js';
6
5
  import { Firefox } from '../../firefox/webdriver/firefox.js';
@@ -10,7 +9,12 @@ import {
10
9
  addConnectivity,
11
10
  removeConnectivity
12
11
  } from '../../connectivity/index.js';
13
- import { logResultLogLine, getProperty, toArray } from '../../support/util.js';
12
+ import {
13
+ logResultLogLine,
14
+ getProperty,
15
+ toArray,
16
+ merge
17
+ } from '../../support/util.js';
14
18
  import {
15
19
  getFullyLoaded,
16
20
  getMainDocumentTimings,
@@ -1,5 +1,4 @@
1
1
  import { getLogger } from '@sitespeed.io/log';
2
- import merge from 'lodash.merge';
3
2
  import { Condition } from 'selenium-webdriver';
4
3
  import { isAndroidConfigured } from '../android/index.js';
5
4
  import { UrlLoadError, BrowserError, TimeoutError } from '../support/errors.js';
@@ -8,7 +7,7 @@ import { getViewPort } from '../support/getViewPort.js';
8
7
  import { defaultPageCompleteCheck } from './pageCompleteChecks/defaultPageCompleteCheck.js';
9
8
  import { pageCompleteCheckByInactivity } from './pageCompleteChecks/pageCompleteCheckByInactivity.js';
10
9
  import { spaInactivity as spaCheck } from './pageCompleteChecks/spaInactivity.js';
11
- import { getProperty } from '../support/util.js';
10
+ import { getProperty, merge } from '../support/util.js';
12
11
  const log = getLogger('browsertime');
13
12
 
14
13
  const defaults = {
@@ -1,4 +1,3 @@
1
- /* global WebSocket */
2
1
  import { execa } from 'execa';
3
2
  import { getLogger } from '@sitespeed.io/log';
4
3
 
@@ -1,5 +1,4 @@
1
1
  import path from 'node:path';
2
- import merge from 'lodash.merge';
3
2
  import { getLogger } from '@sitespeed.io/log';
4
3
  import { screenshotDefaults } from './defaults.js';
5
4
  import {
@@ -8,6 +7,7 @@ import {
8
7
  saveJpg
9
8
  } from '../support/images/index.js';
10
9
  import { loadCustomJimp } from './loadCustomJimp.js';
10
+ import { merge } from '../support/util.js';
11
11
 
12
12
  const SCREENSHOT_DIR = 'screenshots';
13
13
  const log = getLogger('browsertime.screenshot');
@@ -141,8 +141,7 @@ async function findAndParseScripts(root, category) {
141
141
  const categoryName = path.basename(dirPath);
142
142
 
143
143
  return scriptsFromDirectory(dirPath).then(scripts => {
144
- const categories = {};
145
- categories[categoryName] = scripts;
144
+ const categories = { [categoryName]: scripts };
146
145
  return categories;
147
146
  });
148
147
  }
@@ -154,8 +153,7 @@ async function findAndParseScripts(root, category) {
154
153
  let scripts = {};
155
154
 
156
155
  scripts[name] = generateScriptObject(name, root, content);
157
- let categories = {};
158
- categories[category] = scripts;
156
+ let categories = { [category]: scripts };
159
157
 
160
158
  return categories;
161
159
  })
@@ -14,6 +14,12 @@ import { geckoProfilerDefaults } from '../firefox/settings/geckoProfilerDefaults
14
14
  import { findUpSync } from './fileUtil.js';
15
15
  import { setProperty, getProperty } from './util.js';
16
16
  import { execaSync } from 'execa';
17
+ import {
18
+ classifyHelp,
19
+ captureTopics,
20
+ applyHelpFilter,
21
+ buildEpilog
22
+ } from './helpTopics.js';
17
23
 
18
24
  const configPath = findUpSync(['.browsertime.json']);
19
25
 
@@ -152,7 +158,16 @@ export function parseCommandLine() {
152
158
  arg === '--android' ? '--android.enabled' : arg
153
159
  );
154
160
 
155
- let yargsInstance = yargs(hideBin(argvFix));
161
+ // Detect --help <topic> / --help-all up front and rewrite them to a
162
+ // bare --help so yargs still triggers its normal help+exit flow.
163
+ const rawArgs = hideBin(argvFix);
164
+ const help = classifyHelp(rawArgs);
165
+
166
+ let yargsInstance = yargs(help.args);
167
+ // Wrap .option() to record each option's key under its inline
168
+ // `group: '<topic>'` field. Browsertime already annotates every option
169
+ // with a group, so this gives us a topic -> keys map for free.
170
+ const topicKeys = captureTopics(yargsInstance);
156
171
  let validated = yargsInstance
157
172
  .parserConfiguration({
158
173
  'camel-case-expansion': false,
@@ -358,6 +373,20 @@ export function parseCommandLine() {
358
373
  'If you use --user-data-dir as an argument to Chrome and want to clean that directory between each iteration you should use --chrome.cleanUserDataDir true.',
359
374
  group: 'chrome'
360
375
  })
376
+ .option('chrome.coverage', {
377
+ type: 'boolean',
378
+ default: false,
379
+ describe:
380
+ 'Collect JavaScript and CSS code coverage on every iteration. ' +
381
+ 'Per-iteration summaries with a per-file breakdown land in the ' +
382
+ 'per-URL result as coverage.js[] and coverage.css[]. This has a ' +
383
+ 'real performance cost: detailed JS coverage causes V8 to ' +
384
+ 'deoptimize and will skew timing metrics like LCP, TBT and INP. ' +
385
+ 'Use --enableProfileRun instead if you want coverage without ' +
386
+ 'affecting your timing data - it collects coverage during the ' +
387
+ 'extra profile iteration only.',
388
+ group: 'chrome'
389
+ })
361
390
  .option('cpu', {
362
391
  type: 'boolean',
363
392
  describe:
@@ -636,7 +665,7 @@ export function parseCommandLine() {
636
665
  .option('enableProfileRun', {
637
666
  type: 'boolean',
638
667
  describe:
639
- 'Make one extra run that collects the profiling trace log (no other metrics is collected). For Chrome it will collect the timeline trace, for Firefox it will get the Geckoprofiler trace. This means you do not need to get the trace for all runs and can skip the overhead it produces.'
668
+ 'Make one extra run that collects the profiling trace log (no other metrics is collected). For Chrome it will collect the timeline trace and JavaScript/CSS coverage, for Firefox it will get the Geckoprofiler trace. This means you do not need to get the trace for all runs and can skip the overhead it produces.'
640
669
  })
641
670
  .option('enableVideoRun', {
642
671
  type: 'boolean',
@@ -923,76 +952,76 @@ export function parseCommandLine() {
923
952
  type: 'boolean',
924
953
  default: false,
925
954
  describe: 'Save one screenshot per iteration.',
926
- group: 'Screenshot'
955
+ group: 'screenshot'
927
956
  })
928
957
  .option('screenshotLCP', {
929
958
  type: 'boolean',
930
959
  default: false,
931
960
  describe:
932
961
  'Save one screenshot per iteration that shows the largest contentful paint element (if the browser supports LCP).',
933
- group: 'Screenshot'
962
+ group: 'screenshot'
934
963
  })
935
964
  .option('screenshotLS', {
936
965
  type: 'boolean',
937
966
  default: false,
938
967
  describe:
939
968
  'Save one screenshot per iteration that shows the layout shift elements (if the browser supports layout shift).',
940
- group: 'Screenshot'
969
+ group: 'screenshot'
941
970
  })
942
971
  .option('screenshotParams.type', {
943
972
  describe: 'Set the file type of the screenshot',
944
973
  choices: ['png', 'jpg'],
945
974
  default: screenshotDefaults.type,
946
- group: 'Screenshot'
975
+ group: 'screenshot'
947
976
  })
948
977
  .option('screenshotParams.jpg.quality', {
949
978
  describe: 'Quality of the JPEG screenshot. 1-100',
950
979
  default: screenshotDefaults.jpg.quality,
951
- group: 'Screenshot'
980
+ group: 'screenshot'
952
981
  })
953
982
  .option('screenshotParams.maxSize', {
954
983
  describe: 'The max size of the screenshot (width and height).',
955
984
  default: screenshotDefaults.maxSize,
956
- group: 'Screenshot'
985
+ group: 'screenshot'
957
986
  })
958
987
  .option('pageCompleteCheck', {
959
988
  describe:
960
989
  'Supply a JavaScript (inline or JavaScript file) that decides when the browser is finished loading the page and can start to collect metrics. The JavaScript snippet is repeatedly queried to see if page has completed loading (indicated by the script returning true). Use it to fetch timings happening after the loadEventEnd. By default the tests ends 2 seconds after loadEventEnd. Also checkout --pageCompleteCheckInactivity and --pageCompleteCheckPollTimeout',
961
- group: 'PageLoad'
990
+ group: 'pageload'
962
991
  })
963
992
  .option('pageCompleteWaitTime', {
964
993
  describe:
965
994
  'How long time you want to wait for your pageComplteteCheck to finish, after it is signaled to closed. Extra parameter passed on to your pageCompleteCheck.',
966
995
  default: 8000,
967
- group: 'PageLoad'
996
+ group: 'pageload'
968
997
  })
969
998
  .option('pageCompleteCheckInactivity', {
970
999
  describe:
971
1000
  'Alternative way to choose when to end your test. This will wait for 2 seconds of inactivity that happens after loadEventEnd.',
972
1001
  type: 'boolean',
973
1002
  default: false,
974
- group: 'PageLoad'
1003
+ group: 'pageload'
975
1004
  })
976
1005
  .option('pageCompleteCheckNetworkIdle', {
977
1006
  describe:
978
1007
  'Alternative way to choose when to end your test that works in Chrome and Firefox. Uses CDP or WebDriver Bidi to look at network traffic instead of running JavaScript in the browser to know when to end the test. By default this will wait 5 seconds of inactivity in the network log (no requets/responses in 5 seconds). Use --timeouts.networkIdle to change the 5 seconds. The test will end after 2 minutes if there is still activity on the network. You can change that timout using --timeouts.pageCompleteCheck ',
979
1008
  type: 'boolean',
980
1009
  default: false,
981
- group: 'PageLoad'
1010
+ group: 'pageload'
982
1011
  })
983
1012
  .option('pageCompleteCheckPollTimeout', {
984
1013
  type: 'number',
985
1014
  default: 1500,
986
1015
  describe:
987
1016
  'The time in ms to wait for running the page complete check the next time.',
988
- group: 'PageLoad'
1017
+ group: 'pageload'
989
1018
  })
990
1019
  .option('pageCompleteCheckStartWait', {
991
1020
  type: 'number',
992
1021
  default: 5000,
993
1022
  describe:
994
1023
  'The time in ms to wait for running the page complete check for the first time. Use this when you have a pageLoadStrategy set to none',
995
- group: 'PageLoad'
1024
+ group: 'pageload'
996
1025
  })
997
1026
  .option('pageLoadStrategy', {
998
1027
  type: 'string',
@@ -1000,7 +1029,7 @@ export function parseCommandLine() {
1000
1029
  choices: ['eager', 'none', 'normal'],
1001
1030
  describe:
1002
1031
  'Set the strategy to waiting for document readiness after a navigation event. After the strategy is ready, your pageCompleteCheck will start running.',
1003
- group: 'PageLoad'
1032
+ group: 'pageload'
1004
1033
  })
1005
1034
  .option('iterations', {
1006
1035
  alias: 'n',
@@ -1025,14 +1054,14 @@ export function parseCommandLine() {
1025
1054
  default: 0,
1026
1055
  describe:
1027
1056
  'Extra time added for the browser to settle before starting to test a URL. This delay happens after the browser was opened and before the navigation to the URL',
1028
- group: 'PageLoad'
1057
+ group: 'pageload'
1029
1058
  })
1030
1059
  .option('webdriverPageload', {
1031
1060
  type: 'boolean',
1032
1061
  describe:
1033
1062
  'Use webdriver.get to initialize the page load instead of window.location.',
1034
1063
  default: false,
1035
- group: 'PageLoad'
1064
+ group: 'pageload'
1036
1065
  })
1037
1066
  .option('proxy.pac', {
1038
1067
  type: 'string',
@@ -1168,7 +1197,7 @@ export function parseCommandLine() {
1168
1197
  'Use internal browser functionality to clear browser cache between runs instead of only using Selenium.',
1169
1198
  type: 'boolean',
1170
1199
  default: false,
1171
- group: 'PageLoad'
1200
+ group: 'pageload'
1172
1201
  })
1173
1202
  .option('basicAuth', {
1174
1203
  describe:
@@ -1310,7 +1339,7 @@ export function parseCommandLine() {
1310
1339
  default: false,
1311
1340
  describe:
1312
1341
  'Flush DNS between runs, works on Mac OS and Linux. Your user needs sudo rights to be able to flush the DNS.',
1313
- group: 'PageLoad'
1342
+ group: 'pageload'
1314
1343
  })
1315
1344
  .option('extension', {
1316
1345
  describe:
@@ -1321,7 +1350,7 @@ export function parseCommandLine() {
1321
1350
  'Convenient parameter to use if you test a SPA application: will automatically wait for X seconds after last network activity and use hash in file names. Read more: https://www.sitespeed.io/documentation/sitespeed.io/spa/',
1322
1351
  type: 'boolean',
1323
1352
  default: false,
1324
- group: 'PageLoad'
1353
+ group: 'pageload'
1325
1354
  })
1326
1355
  .option('cjs', {
1327
1356
  describe:
@@ -1334,7 +1363,7 @@ export function parseCommandLine() {
1334
1363
  default: 3,
1335
1364
  describe:
1336
1365
  'If the browser fails to start, you can retry to start it this amount of times.',
1337
- group: 'PageLoad'
1366
+ group: 'pageload'
1338
1367
  })
1339
1368
  .option('preWarmServer', {
1340
1369
  type: 'boolean',
@@ -1363,8 +1392,14 @@ export function parseCommandLine() {
1363
1392
  .alias('V', 'version')
1364
1393
  .alias('v', 'verbose')
1365
1394
  .wrap(yargsInstance.terminalWidth())
1395
+ .epilog(buildEpilog(help.mode, help.topic, [...topicKeys.keys()]))
1366
1396
  .check(validateInput);
1367
1397
 
1398
+ // Filter the visible options based on the requested help mode. This
1399
+ // must happen before .argv is read because that's what triggers
1400
+ // yargs to render help and exit.
1401
+ applyHelpFilter(yargsInstance, help.mode, help.topic, topicKeys);
1402
+
1368
1403
  let argv = validated.argv;
1369
1404
 
1370
1405
  if (
@@ -1,12 +1,11 @@
1
1
  import { createRequire } from 'node:module';
2
- import merge from 'lodash.merge';
3
2
  import { pathToFolder } from '../pathToFolder.js';
4
3
  import { localTime } from '../util.js';
5
4
  import { getLogger } from '@sitespeed.io/log';
6
5
  const log = getLogger('browsertime');
7
6
  const require = createRequire(import.meta.url);
8
7
  const version = require('../../../package.json').version;
9
- import { pick, isEmpty, getProperty } from '../../support/util.js';
8
+ import { pick, isEmpty, getProperty, merge } from '../../support/util.js';
10
9
 
11
10
  const sensitiveHeaders = new Set([
12
11
  'authorization',
@@ -43,6 +42,7 @@ function getDocumentRequests(entries, pageId) {
43
42
 
44
43
  do {
45
44
  entry = pageEntries.shift();
45
+ if (!entry) break;
46
46
  requests.push(entry);
47
47
  } while (entry.response.redirectURL);
48
48
 
@@ -53,7 +53,7 @@ function getFinalURL(entries, pageref) {
53
53
  const requests = getDocumentRequests(entries, pageref);
54
54
 
55
55
  const finalEntry = requests.pop();
56
- return finalEntry.request.url;
56
+ return finalEntry ? finalEntry.request.url : undefined;
57
57
  }
58
58
 
59
59
  function addExtrasToHAR(
@@ -250,13 +250,16 @@ export function getMainDocumentTimings(har) {
250
250
  for (let page of har.log.pages) {
251
251
  const pageId = page.id;
252
252
  const url = page._url;
253
+ if (url === undefined) continue;
253
254
 
254
255
  let pageEntries = [...entries];
255
256
  const finalURL = getFinalURL(pageEntries, pageId);
257
+ if (finalURL === undefined) continue;
256
258
 
257
259
  pageEntries = pageEntries.filter(
258
260
  entry => entry.pageref === pageId && entry.request.url === finalURL
259
261
  );
262
+ if (pageEntries.length === 0) continue;
260
263
 
261
264
  timings.push({ url, timings: pageEntries[0].timings });
262
265
  }
@@ -0,0 +1,174 @@
1
+ /*eslint no-console: 0*/
2
+
3
+ // Help-topic dispatch for the Browsertime CLI.
4
+ //
5
+ // Browsertime declares ~250 options in one big yargs chain, with each
6
+ // option carrying an inline `group: '<topic>'` field. The unfiltered
7
+ // --help dump is overwhelming; this module lets users say:
8
+ //
9
+ // browsertime --help -> curated set of common options
10
+ // browsertime --help <topic> -> only that topic
11
+ // browsertime --help-all -> the full historical dump
12
+ //
13
+ // We intercept the yargs instance's .option() call to capture each
14
+ // key's topic from the inline `group` field, so the cli.js chain
15
+ // doesn't have to change shape.
16
+
17
+ // Curated set of "everyday" options the default --help should show.
18
+ // Keep this list short -- anything missing is one --help <topic> away.
19
+ const COMMON_KEYS = new Set([
20
+ // common everyday flags
21
+ 'browser',
22
+ 'b',
23
+ 'iterations',
24
+ 'n',
25
+ 'video',
26
+ 'visualMetrics',
27
+ 'speedIndex',
28
+ 'docker',
29
+ 'preURL',
30
+ 'headless',
31
+ 'connectivity.profile',
32
+ 'c',
33
+ 'connectivity.engine',
34
+ 'mobile',
35
+ 'resultDir',
36
+ 'cpu',
37
+ // meta
38
+ 'config',
39
+ 'help',
40
+ 'h',
41
+ 'verbose',
42
+ 'v',
43
+ 'version',
44
+ 'V'
45
+ ]);
46
+
47
+ // Meta-flags that always stay visible regardless of mode.
48
+ const ALWAYS_KEEP = new Set([
49
+ 'help',
50
+ 'h',
51
+ 'version',
52
+ 'V',
53
+ 'config',
54
+ 'verbose',
55
+ 'v'
56
+ ]);
57
+
58
+ const DOCS_URL = 'https://www.sitespeed.io/documentation/browsertime/';
59
+
60
+ // Parse the raw arg list, detect help intent, and return a cleaned arg
61
+ // list with `--help-all` / `--help <topic>` collapsed to a plain
62
+ // `--help` so yargs still triggers its normal help+exit flow.
63
+ //
64
+ // Returns { mode, topic, args } where mode is one of:
65
+ // 'none' -> no --help passed
66
+ // 'default' -> plain --help, show the curated common subset
67
+ // 'topic' -> --help <topic>, show only that topic
68
+ // 'all' -> --help-all (or --help all), show everything
69
+ export function classifyHelp(rawArgs) {
70
+ const cleaned = [];
71
+ let mode = 'none';
72
+ let topic;
73
+ for (let i = 0; i < rawArgs.length; i++) {
74
+ const a = rawArgs[i];
75
+ if (a === '--help-all') {
76
+ mode = 'all';
77
+ cleaned.push('--help');
78
+ continue;
79
+ }
80
+ if (a === '--help' || a === '-h' || a === '--help=true') {
81
+ const next = rawArgs[i + 1];
82
+ if (next && !next.startsWith('-')) {
83
+ if (next === 'all') {
84
+ mode = 'all';
85
+ i++;
86
+ } else {
87
+ mode = 'topic';
88
+ topic = next;
89
+ i++;
90
+ }
91
+ } else if (mode === 'none') {
92
+ mode = 'default';
93
+ }
94
+ cleaned.push('--help');
95
+ continue;
96
+ }
97
+ cleaned.push(a);
98
+ }
99
+ return { mode, topic, args: cleaned };
100
+ }
101
+
102
+ // Wrap a yargs instance so every subsequent .option(key, def) call also
103
+ // records `key` under `def.group` in the returned Map. Browsertime's
104
+ // option chain already carries inline `group: '<topic>'` annotations,
105
+ // so no chain edits are needed.
106
+ export function captureTopics(yargsInstance) {
107
+ const topicKeys = new Map();
108
+ const originalOption = yargsInstance.option.bind(yargsInstance);
109
+ yargsInstance.option = function (key, def) {
110
+ if (def && def.group) {
111
+ const list = topicKeys.get(def.group) || [];
112
+ list.push(key);
113
+ topicKeys.set(def.group, list);
114
+ }
115
+ return originalOption(key, def);
116
+ };
117
+ return topicKeys;
118
+ }
119
+
120
+ // Build the epilog (footer) shown under --help, varying by mode.
121
+ export function buildEpilog(mode, topic, topicNames) {
122
+ if (mode === 'all' || mode === 'none') {
123
+ return `Read the docs at ${DOCS_URL}`;
124
+ }
125
+ if (mode === 'topic') {
126
+ return [
127
+ `Showing only the "${topic}" options.`,
128
+ `Run with --help-all for the full reference, or --help to see common options.`,
129
+ `Read the docs at ${DOCS_URL}`
130
+ ].join('\n');
131
+ }
132
+ // default
133
+ const topics = topicNames.join(', ');
134
+ return [
135
+ 'Topics (use `browsertime --help <topic>`):',
136
+ ' ' + topics,
137
+ '',
138
+ 'Run with --help-all to see every option, or read the docs at ' + DOCS_URL
139
+ ].join('\n');
140
+ }
141
+
142
+ // Apply the topic filter by calling yargs.hide() on every key not in
143
+ // the set we want to display. Returns true if filtering was applied.
144
+ export function applyHelpFilter(yargsInstance, mode, topic, topicKeys) {
145
+ if (mode === 'none' || mode === 'all') return false;
146
+
147
+ let keep;
148
+ if (mode === 'topic') {
149
+ if (!topicKeys.has(topic)) {
150
+ console.error(`Unknown help topic: ${topic}`);
151
+ console.error('Available topics: ' + [...topicKeys.keys()].join(', '));
152
+ // CLI dispatch: bailing out here is intentional (deliberate "no
153
+ // such topic" exit code), not an error to bubble up the stack.
154
+ // eslint-disable-next-line unicorn/no-process-exit
155
+ process.exit(2);
156
+ }
157
+ // Topic mode = just the topic. No COMMON_KEYS floor here -- topic
158
+ // views should focus on the topic, not re-render common stuff
159
+ // that's one --help away.
160
+ keep = new Set(topicKeys.get(topic));
161
+ } else {
162
+ // default
163
+ keep = COMMON_KEYS;
164
+ }
165
+
166
+ const allKeys = Object.keys(yargsInstance.getOptions().key);
167
+ for (const k of allKeys) {
168
+ if (ALWAYS_KEEP.has(k)) continue;
169
+ if (!keep.has(k)) {
170
+ yargsInstance.hide(k);
171
+ }
172
+ }
173
+ return true;
174
+ }
@@ -388,3 +388,49 @@ export function getProperty(object, path, defaultValue) {
388
388
 
389
389
  return result === undefined ? defaultValue : result;
390
390
  }
391
+
392
+ function isPlainObject(value) {
393
+ if (value === null || typeof value !== 'object') return false;
394
+ const proto = Object.getPrototypeOf(value);
395
+ return proto === Object.prototype || proto === null;
396
+ }
397
+
398
+ function canMergeInto(value) {
399
+ return value !== null && typeof value === 'object';
400
+ }
401
+
402
+ function mergeInto(target, source) {
403
+ for (const key of Object.keys(source)) {
404
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
405
+ continue;
406
+ }
407
+ const sourceValue = source[key];
408
+ const targetValue = target[key];
409
+ if (Array.isArray(sourceValue)) {
410
+ const into = Array.isArray(targetValue) ? targetValue : [];
411
+ mergeInto(into, sourceValue);
412
+ target[key] = into;
413
+ } else if (isPlainObject(sourceValue)) {
414
+ const into = canMergeInto(targetValue) ? targetValue : {};
415
+ mergeInto(into, sourceValue);
416
+ target[key] = into;
417
+ } else if (sourceValue === undefined) {
418
+ if (!(key in target)) target[key] = undefined;
419
+ } else {
420
+ target[key] = sourceValue;
421
+ }
422
+ }
423
+ }
424
+
425
+ /**
426
+ * A replacement for lodash.merge(target, ...sources).
427
+ *
428
+ */
429
+ export function merge(target, ...sources) {
430
+ if (target === undefined || target === null) return target;
431
+ for (const source of sources) {
432
+ if (source === undefined || source === null) continue;
433
+ mergeInto(target, source);
434
+ }
435
+ return target;
436
+ }
@@ -45,7 +45,7 @@ export async function addTextToVideo(
45
45
 
46
46
  arguments_.push(
47
47
  '-vf',
48
- `drawtext=${fontFile}x=w/2-(tw/2): y=H-h/10:fontcolor=white:fontsize=h/${fontSize}:box=1:boxcolor=0x000000AA:text='%{pts\\:hms}'${allTimingMetrics}`,
48
+ String.raw`drawtext=${fontFile}x=w/2-(tw/2): y=H-h/10:fontcolor=white:fontsize=h/${fontSize}:box=1:boxcolor=0x000000AA:text='%{pts\:hms}'${allTimingMetrics}`,
49
49
  '-y',
50
50
  outputFile
51
51
  );
package/package.json CHANGED
@@ -1,24 +1,23 @@
1
1
  {
2
2
  "name": "browsertime",
3
3
  "description": "Get performance metrics from your web page using Browsertime.",
4
- "version": "27.2.0",
4
+ "version": "27.4.0",
5
5
  "bin": "./bin/browsertime.js",
6
6
  "type": "module",
7
- "types": "./types/scripting.d.ts",
7
+ "types": "./scripting.d.ts",
8
8
  "dependencies": {
9
9
  "@devicefarmer/adbkit": "3.3.8",
10
- "@sitespeed.io/chromedriver": "143.0.7499",
11
- "@sitespeed.io/edgedriver": "143.0.3650",
12
- "@sitespeed.io/geckodriver": "0.36.0",
13
- "@sitespeed.io/log": "1.0.0",
10
+ "@sitespeed.io/chromedriver": "148.0.7778",
11
+ "@sitespeed.io/edgedriver": "148.0.3967",
12
+ "@sitespeed.io/geckodriver": "0.36.2",
13
+ "@sitespeed.io/log": "2.0.0",
14
14
  "@sitespeed.io/throttle": "6.0.0",
15
- "chrome-har": "1.3.0",
16
- "chrome-remote-interface": "0.33.3",
15
+ "chrome-har": "1.3.1",
16
+ "chrome-remote-interface": "0.34.0",
17
17
  "execa": "9.6.1",
18
18
  "fast-stats": "0.0.7",
19
19
  "ff-test-bidi-har-export": "0.0.22",
20
- "lodash.merge": "4.6.2",
21
- "selenium-webdriver": "4.43.0",
20
+ "selenium-webdriver": "4.44.0",
22
21
  "yargs": "18.0.0"
23
22
  },
24
23
  "optionalDependencies": {
@@ -26,13 +25,14 @@
26
25
  "usb-power-profiling": "1.6.0"
27
26
  },
28
27
  "devDependencies": {
28
+ "@eslint/js": "10.0.1",
29
29
  "@types/selenium-webdriver": "4.35.5",
30
30
  "ava": "6.4.1",
31
31
  "clean-jsdoc-theme": "4.3.2",
32
- "eslint": "9.37.0",
32
+ "eslint": "10.4.0",
33
33
  "eslint-config-prettier": "10.1.8",
34
34
  "eslint-plugin-prettier": "5.5.5",
35
- "eslint-plugin-unicorn": "61.0.2",
35
+ "eslint-plugin-unicorn": "64.0.0",
36
36
  "jsdoc": "4.0.5",
37
37
  "prettier": "3.8.3",
38
38
  "serve": "14.2.6",
@@ -55,13 +55,14 @@
55
55
  "index.js",
56
56
  "lib",
57
57
  "package.json",
58
+ "scripting.d.ts",
58
59
  "vendor",
59
60
  "types"
60
61
  ],
61
62
  "exports": {
62
63
  ".": {
63
- "import": "./index.js",
64
- "types": "./types/scripting.d.ts"
64
+ "types": "./scripting.d.ts",
65
+ "import": "./index.js"
65
66
  }
66
67
  },
67
68
  "scripts": {
@@ -72,7 +73,7 @@
72
73
  "lint:fix": "eslint . --fix",
73
74
  "tsc": "tsc",
74
75
  "jsdoc": "jsdoc --configure jsdoc/jsdoc.json",
75
- "prepublishOnly": "npm run lint && npm run tsc"
76
+ "prepublishOnly": "npm run lint && npm run tsc && npx -y publint"
76
77
  },
77
78
  "author": "Peter Hedenskog",
78
79
  "contributors": [