sitespeed.io 38.6.0 → 39.0.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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,15 @@
1
1
 
2
2
  # CHANGELOG - sitespeed.io (we use [semantic versioning](https://semver.org))
3
3
 
4
+ ## 39.0.0 - 2025-12-15
5
+
6
+ ### Breaking
7
+ * We removed support for setting the compression level for png screenshots, see the added section why.
8
+
9
+ ### Added
10
+ * Upgrade to support NodeJS 24 without warnings, include NodeJS 24 in the Docker container, and base the Docker container on Ubuntu 24.04. To make this work I needed to upgrade the Jimp library and then we lost the settings for png screenshots `--browsertime.screenshotParams.png.compressionLevel` [#4570](https://github.com/sitespeedio/sitespeed.io/pull/4570).
11
+
12
+
4
13
  ## 38.6.0 - 2025-11-02
5
14
  ### Added
6
15
  * Browsertime 25.4.0 [#4566](https://github.com/sitespeedio/sitespeed.io/pull/4566).
package/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM sitespeedio/webbrowsers:chrome-142.0-firefox-144.0-edge-141.0
1
+ FROM sitespeedio/webbrowsers:chrome-142.0-firefox-144.0-edge-142.0-b
2
2
 
3
3
  ARG TARGETPLATFORM=linux/amd64
4
4
 
package/Dockerfile-slim CHANGED
@@ -1,4 +1,4 @@
1
- FROM node:22.13.0-bookworm-slim
1
+ FROM node:24.11.0-bookworm-slim
2
2
 
3
3
  ARG TARGETPLATFORM=linux/amd64
4
4
 
@@ -15,7 +15,7 @@ RUN echo "deb http://deb.debian.org/debian/ unstable main contrib non-free" >> /
15
15
  apt-get install -y --no-install-recommends firefox tcpdump iproute2 ca-certificates sudo --no-install-recommends --no-install-suggests && \
16
16
  # Cleanup
17
17
  apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
18
- && rm -rf /var/lib/apt/lists/* /tmp/*
18
+ && rm -rf /var/lib/apt/lists/* /tmp/*
19
19
 
20
20
  # Install sitespeed.io
21
21
  RUN mkdir -p /usr/src/app
package/lib/cli/cli.js CHANGED
@@ -1436,12 +1436,6 @@ export async function parseCommandLine() {
1436
1436
  default: browsertimeConfig.screenshotParams.type,
1437
1437
  group: 'Screenshot'
1438
1438
  })
1439
- .option('browsertime.screenshotParams.png.compressionLevel', {
1440
- alias: 'screenshot.png.compressionLevel',
1441
- describe: 'zlib compression level',
1442
- default: browsertimeConfig.screenshotParams.png.compressionLevel,
1443
- group: 'Screenshot'
1444
- })
1445
1439
  .option('browsertime.screenshotParams.jpg.quality', {
1446
1440
  alias: 'screenshot.jpg.quality',
1447
1441
  describe: 'Quality of the JPEG screenshot. 1-100',
@@ -2195,7 +2189,9 @@ export async function parseCommandLine() {
2195
2189
  );
2196
2190
  }
2197
2191
 
2198
- let urlsMetaData = getAliases(argv._, argv.urlAlias, argv.groupAlias);
2192
+ let urlsMetaData = argv.multi
2193
+ ? {}
2194
+ : getAliases(argv._, argv.urlAlias, argv.groupAlias);
2199
2195
  // Copy the alias so it is also used by Browsertime
2200
2196
  if (argv.urlAlias) {
2201
2197
  // Browsertime has it own way of handling alias
@@ -1,15 +1,15 @@
1
- import { parse, format } from 'node:url';
2
1
  import path from 'node:path';
3
2
 
4
3
  import { resultUrls } from './resultUrls.js';
5
4
  import { storageManager } from './storageManager.js';
6
5
 
7
6
  function getDomainOrFileName(input) {
8
- let domainOrFile = input;
9
- domainOrFile = domainOrFile.startsWith('http')
10
- ? parse(domainOrFile).hostname
11
- : path.basename(domainOrFile).replaceAll('.', '_');
12
- return domainOrFile;
7
+ if (input.startsWith('http')) {
8
+ const url = new URL(input);
9
+ return url.hostname;
10
+ }
11
+
12
+ return path.basename(input).replaceAll('.', '_');
13
13
  }
14
14
 
15
15
  export function resultsStorage(input, timestamp, options) {
@@ -34,10 +34,19 @@ export function resultsStorage(input, timestamp, options) {
34
34
  storagePathPrefix = path.join(...resultsSubFolders);
35
35
 
36
36
  if (resultBaseURL) {
37
- const url = parse(resultBaseURL);
38
- resultsSubFolders.unshift(url.pathname.slice(1));
39
- url.pathname = resultsSubFolders.join('/');
40
- resultUrl = format(url);
37
+ const url = new URL(resultBaseURL);
38
+
39
+ const basePath = url.pathname.slice(1); // drop leading '/'
40
+ if (basePath) {
41
+ resultsSubFolders.unshift(basePath);
42
+ }
43
+
44
+ const newPath = resultsSubFolders.join('/');
45
+
46
+ // Ensure leading slash for pathname
47
+ url.pathname = newPath.startsWith('/') ? newPath : `/${newPath}`;
48
+
49
+ resultUrl = url.toString();
41
50
  }
42
51
 
43
52
  return {
@@ -1,56 +1,75 @@
1
- import { parse } from 'node:url';
2
1
  import { createHash } from 'node:crypto';
3
-
2
+ import path from 'node:path';
4
3
  import { getLogger } from '@sitespeed.io/log';
5
-
6
4
  import { isEmpty } from '../../support/util.js';
5
+
7
6
  const log = getLogger('sitespeedio.file');
8
7
 
8
+ function isHttpLikeUrl(s) {
9
+ if (typeof s !== 'string' || s.length === 0) return false;
10
+ if (s.startsWith('//')) return true;
11
+ return /^https?:\/\//iu.test(s);
12
+ }
13
+
9
14
  function toSafeKey(key) {
10
- // U+2013 : EN DASH – as used on https://en.wikipedia.org/wiki/2019–20_coronavirus_pandemic
11
- return key.replaceAll(/[ %&()+,./:?|~–]|%7C/g, '-');
15
+ return key.replaceAll(/[ %&()+,./:?|~–]|%7C/gu, '-');
12
16
  }
13
17
 
14
- export function pathToFolder(url, options, alias) {
15
- const useHash = options.useHash;
16
- const parsedUrl = parse(decodeURIComponent(url));
18
+ function md5Hex8(s) {
19
+ return createHash('md5').update(s).digest('hex').slice(0, 8);
20
+ }
17
21
 
18
- const pathSegments = [];
19
- const urlSegments = [];
22
+ function normalizeFsPath(input) {
23
+ let n = path.normalize(input);
24
+ if (n.startsWith(`.${path.sep}`)) n = n.slice(2);
25
+ return n;
26
+ }
20
27
 
21
- // If a measurements fail and we use a script file and no URL
22
- // has been tested, we don't have a hostname
23
- if (parsedUrl.hostname) {
24
- pathSegments.push('pages', parsedUrl.hostname.split('.').join('_'));
25
- }
28
+ export function pathToFolder(input, options) {
29
+ if (options.useSameDir) return '';
26
30
 
27
- if (options.urlMetaData && options.urlMetaData[url]) {
28
- pathSegments.push(options.urlMetaData[url]);
29
- } else if (alias) {
30
- pathSegments.push(alias);
31
+ let hostname = '';
32
+ let pathname = '';
33
+ let search = '';
34
+ let hash = '';
35
+
36
+ const isUrl = isHttpLikeUrl(input);
37
+
38
+ if (isUrl) {
39
+ const raw = input.startsWith('//') ? `http:${input}` : input;
40
+ const u = new URL(raw);
41
+ hostname = u.hostname;
42
+ pathname = u.pathname; // '/'-separated
43
+ search = u.search; // includes '?'
44
+ hash = u.hash; // includes '#'
31
45
  } else {
32
- if (!isEmpty(parsedUrl.pathname)) {
33
- urlSegments.push(...parsedUrl.pathname.split('/').filter(Boolean));
34
- }
46
+ hostname = 'file';
47
+ const fsNormalized = normalizeFsPath(input);
48
+ pathname = `${path.sep}${fsNormalized}`;
49
+ }
35
50
 
36
- if (useHash && !isEmpty(parsedUrl.hash)) {
37
- const md5 = createHash('md5'),
38
- hash = md5.update(parsedUrl.hash).digest('hex').slice(0, 8);
39
- urlSegments.push('hash-' + hash);
40
- }
51
+ const pathSegments = ['pages', hostname.split('.').join('_')];
52
+ const urlSegments = [];
53
+
54
+ if (options.urlMetaData && options.urlMetaData[input]) {
55
+ pathSegments.push(options.urlMetaData[input]);
56
+ } else {
57
+ const parts = isUrl
58
+ ? pathname.split('/').filter(Boolean)
59
+ : pathname.split(/[\\/]/u).filter(Boolean);
60
+ if (!isEmpty(parts)) urlSegments.push(...parts);
41
61
 
42
- if (!isEmpty(parsedUrl.search)) {
43
- const md5 = createHash('md5'),
44
- hash = md5.update(parsedUrl.search).digest('hex').slice(0, 8);
45
- urlSegments.push('query-' + hash);
62
+ if (isUrl) {
63
+ if (options.useHash && !isEmpty(hash))
64
+ urlSegments.push(`hash-${md5Hex8(hash)}`);
65
+ if (!isEmpty(search)) urlSegments.push(`query-${md5Hex8(search)}`);
46
66
  }
47
67
 
48
- // This is used from sitespeed.io to match URLs on Graphite
49
68
  if (options.storeURLsAsFlatPageOnDisk) {
50
- const folder = toSafeKey(urlSegments.join('_').concat('_'));
69
+ const folder = toSafeKey(`${urlSegments.join('_')}_`);
51
70
  if (folder.length > 255) {
52
71
  log.info(
53
- `The URL ${url} hit the 255 character limit used when stored on disk, you may want to give your URL an alias to make sure it will not collide with other URLs.`
72
+ `The URL ${input} hit the 255 character limit used when stored on disk, you may want to give your URL an alias to make sure it will not collide with other URLs.`
54
73
  );
55
74
  pathSegments.push(folder.slice(0, 254));
56
75
  } else {
@@ -63,11 +82,9 @@ export function pathToFolder(url, options, alias) {
63
82
 
64
83
  // pathSegments.push('data');
65
84
 
66
- for (const [index, segment] of pathSegments.entries()) {
67
- if (segment) {
68
- pathSegments[index] = segment.replaceAll(/[^\w.\u0621-\u064A-]/gi, '-');
69
- }
85
+ for (const [i, seg] of pathSegments.entries()) {
86
+ if (seg) pathSegments[i] = seg.replaceAll(/[^\w.\u0621-\u064A-]/giu, '-');
70
87
  }
71
88
 
72
- return pathSegments.join('/').concat('/');
89
+ return `${path.join(...pathSegments)}${path.sep}`;
73
90
  }
@@ -1,4 +1,3 @@
1
- import { parse } from 'node:url';
2
1
  import { messageMaker } from '../support/messageMaker.js';
3
2
  const make = messageMaker('url-reader').make;
4
3
 
@@ -15,7 +14,7 @@ export function findUrls(queue, options) {
15
14
  options.urlsMetaData[url] &&
16
15
  options.urlsMetaData[url].groupAlias
17
16
  ? options.urlsMetaData[url].groupAlias
18
- : parse(url).hostname
17
+ : new URL(url).hostname
19
18
  }
20
19
  )
21
20
  );
@@ -1,5 +1,3 @@
1
- import { parse } from 'node:url';
2
-
3
1
  // eslint-disable-next-line unicorn/no-named-default
4
2
  import { default as _merge } from 'lodash.merge';
5
3
 
@@ -107,7 +105,7 @@ export default class BrowsertimePlugin extends SitespeedioPlugin {
107
105
  if (this.options.urlMetaData) {
108
106
  for (let url of Object.keys(this.options.urlMetaData)) {
109
107
  const alias = this.options.urlMetaData[url];
110
- const group = parse(url).hostname;
108
+ const group = new URL(url).hostname;
111
109
  this.allAlias[alias] = url;
112
110
  super.sendMessage('browsertime.alias', alias, {
113
111
  url,
@@ -190,7 +188,7 @@ export default class BrowsertimePlugin extends SitespeedioPlugin {
190
188
  if (alias) {
191
189
  if (this.scriptOrMultiple) {
192
190
  url = element.info.url;
193
- group = parse(url).hostname;
191
+ group = new URL(url).hostname;
194
192
  }
195
193
  this.allAlias[url] = alias;
196
194
  super.sendMessage('browsertime.alias', alias, {
@@ -232,7 +230,7 @@ export default class BrowsertimePlugin extends SitespeedioPlugin {
232
230
  if (this.scriptOrMultiple) {
233
231
  url = result[resultIndex].info?.url;
234
232
  if (url) {
235
- group = parse(url).hostname;
233
+ group = new URL(url).hostname;
236
234
  }
237
235
  }
238
236
  let runIndex = 0;
@@ -1,5 +1,3 @@
1
- import { parse } from 'node:url';
2
-
3
1
  import { Stats } from 'fast-stats';
4
2
  import { getLogger } from '@sitespeed.io/log';
5
3
 
@@ -18,7 +16,7 @@ const timingNames = [
18
16
  ];
19
17
 
20
18
  function parseDomainName(url) {
21
- return parse(url).hostname;
19
+ return new URL(url).hostname;
22
20
  }
23
21
 
24
22
  function getDomain(domainName) {
@@ -1,4 +1,3 @@
1
- import { parse } from 'node:url';
2
1
  import { getLogger } from '@sitespeed.io/log';
3
2
  import coach from 'coach-core';
4
3
  import { SitespeedioPlugin } from '@sitespeed.io/plugin';
@@ -102,7 +101,7 @@ export default class PageXrayPlugin extends SitespeedioPlugin {
102
101
  const sentURL = {};
103
102
  for (let summary of pageSummary) {
104
103
  // The group can be different so take it per url
105
- const myGroup = parse(summary.url).hostname;
104
+ const myGroup = new URL(summary.url).hostname;
106
105
  if (sentURL[summary.url]) {
107
106
  sentURL[summary.url] += 1;
108
107
  } else {
@@ -1,5 +1,3 @@
1
- import { parse } from 'node:url';
2
-
3
1
  import coach from 'coach-core';
4
2
  import { SitespeedioPlugin } from '@sitespeed.io/plugin';
5
3
 
@@ -133,7 +131,7 @@ export default class ThirdPartyPlugin extends SitespeedioPlugin {
133
131
  // fallback to domain
134
132
  if (!entity) {
135
133
  const hostname = ent.url.startsWith('http')
136
- ? parse(ent.url).hostname
134
+ ? new URL(ent.url).hostname
137
135
  : ent.url;
138
136
  entity = {
139
137
  name: hostname
@@ -1,5 +1,5 @@
1
- import { parse } from 'node:url';
2
1
  import { getLogger } from '@sitespeed.io/log';
2
+
3
3
  const log = getLogger('sitespeedio');
4
4
 
5
5
  function joinNonEmpty(strings, delimeter) {
@@ -11,7 +11,12 @@ function toSafeKey(key) {
11
11
  return key.replaceAll(/[ %&()+,./:?|~–]|%7C/g, '_');
12
12
  }
13
13
 
14
- export function keypathFromUrl(url, includeQueryParameters, useHash, group) {
14
+ export function keypathFromUrl(
15
+ urlString,
16
+ includeQueryParameters,
17
+ useHash,
18
+ group
19
+ ) {
15
20
  function flattenQueryParameters(parameters) {
16
21
  return Object.keys(parameters).reduce(
17
22
  (result, key) => joinNonEmpty([result, key, parameters[key]], '_'),
@@ -19,16 +24,22 @@ export function keypathFromUrl(url, includeQueryParameters, useHash, group) {
19
24
  );
20
25
  }
21
26
 
22
- url = parse(url, !!includeQueryParameters);
27
+ const url = new URL(urlString);
23
28
 
24
29
  let path = toSafeKey(url.pathname);
25
30
 
26
31
  if (includeQueryParameters) {
32
+ const parameters = {};
33
+ for (const [key, value] of url.searchParams) {
34
+ parameters[key] = value;
35
+ }
36
+
27
37
  path = joinNonEmpty(
28
- [path, toSafeKey(flattenQueryParameters(url.query))],
38
+ [path, toSafeKey(flattenQueryParameters(parameters))],
29
39
  '_'
30
40
  );
31
41
  }
42
+
32
43
  if (useHash && url.hash) {
33
44
  path = joinNonEmpty([path, toSafeKey(url.hash)], '_');
34
45
  }