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 +9 -0
- package/Dockerfile +1 -1
- package/Dockerfile-slim +2 -2
- package/lib/cli/cli.js +3 -7
- package/lib/core/resultsStorage/index.js +19 -10
- package/lib/core/resultsStorage/pathToFolder.js +56 -39
- package/lib/core/url-source.js +1 -2
- package/lib/plugins/browsertime/index.js +3 -5
- package/lib/plugins/domains/aggregator.js +1 -3
- package/lib/plugins/pagexray/index.js +1 -2
- package/lib/plugins/thirdparty/index.js +1 -3
- package/lib/support/flattenMessage.js +15 -4
- package/npm-shrinkwrap.json +601 -134
- package/package.json +2 -2
- package/docs/README.md +0 -10
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
package/Dockerfile-slim
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
FROM node:
|
|
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 =
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 =
|
|
38
|
-
|
|
39
|
-
url.pathname
|
|
40
|
-
|
|
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
|
-
|
|
11
|
-
return key.replaceAll(/[ %&()+,./:?|~–]|%7C/g, '-');
|
|
15
|
+
return key.replaceAll(/[ %&()+,./:?|~–]|%7C/gu, '-');
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
function md5Hex8(s) {
|
|
19
|
+
return createHash('md5').update(s).digest('hex').slice(0, 8);
|
|
20
|
+
}
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
46
|
+
hostname = 'file';
|
|
47
|
+
const fsNormalized = normalizeFsPath(input);
|
|
48
|
+
pathname = `${path.sep}${fsNormalized}`;
|
|
49
|
+
}
|
|
35
50
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
urlSegments.push(
|
|
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('_')
|
|
69
|
+
const folder = toSafeKey(`${urlSegments.join('_')}_`);
|
|
51
70
|
if (folder.length > 255) {
|
|
52
71
|
log.info(
|
|
53
|
-
`The URL ${
|
|
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 [
|
|
67
|
-
if (
|
|
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
|
|
89
|
+
return `${path.join(...pathSegments)}${path.sep}`;
|
|
73
90
|
}
|
package/lib/core/url-source.js
CHANGED
|
@@ -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
|
-
:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
?
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
}
|