sitespeed.io 35.7.5 → 36.0.0-alpha.2

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 (103) hide show
  1. package/.dockerignore +4 -1
  2. package/CHANGELOG.md +52 -0
  3. package/Dockerfile +9 -5
  4. package/Dockerfile-slim +9 -7
  5. package/LICENSE +1 -1
  6. package/bin/browsertimeWebPageReplay.js +1 -1
  7. package/lib/cli/cli.js +17 -25
  8. package/lib/core/logging.js +6 -98
  9. package/lib/core/queue.js +112 -0
  10. package/lib/core/queueHandler.js +20 -28
  11. package/lib/core/resultsStorage/pathToFolder.js +3 -3
  12. package/lib/core/resultsStorage/storageManager.js +7 -5
  13. package/lib/plugins/assets/index.js +1 -1
  14. package/lib/plugins/axe/axePostScript.cjs +4 -6
  15. package/lib/plugins/axe/index.js +2 -2
  16. package/lib/plugins/browsertime/analyzer.js +6 -7
  17. package/lib/plugins/browsertime/browsertimeAggregator.js +64 -42
  18. package/lib/plugins/browsertime/filmstrip.js +2 -2
  19. package/lib/plugins/browsertime/index.js +3 -3
  20. package/lib/plugins/budget/deprecatedVerify.js +2 -2
  21. package/lib/plugins/budget/index.js +2 -2
  22. package/lib/plugins/budget/json.js +2 -2
  23. package/lib/plugins/budget/junit.js +2 -2
  24. package/lib/plugins/budget/tap.js +2 -2
  25. package/lib/plugins/budget/verify.js +2 -2
  26. package/lib/plugins/coach/aggregator.js +7 -9
  27. package/lib/plugins/coach/index.js +2 -2
  28. package/lib/plugins/compare/helper.js +2 -2
  29. package/lib/plugins/compare/index.js +2 -2
  30. package/lib/plugins/crawler/index.js +2 -2
  31. package/lib/plugins/crux/index.js +2 -2
  32. package/lib/plugins/crux/send.js +2 -2
  33. package/lib/plugins/domains/aggregator.js +9 -11
  34. package/lib/plugins/domains/index.js +1 -1
  35. package/lib/plugins/gcs/index.js +17 -16
  36. package/lib/plugins/grafana/send-annotation.js +2 -2
  37. package/lib/plugins/graphite/data-generator.js +13 -17
  38. package/lib/plugins/graphite/index.js +4 -3
  39. package/lib/plugins/graphite/send-annotation.js +2 -2
  40. package/lib/plugins/graphite/sender.js +2 -2
  41. package/lib/plugins/html/dataCollector.js +8 -14
  42. package/lib/plugins/html/htmlBuilder.js +12 -10
  43. package/lib/plugins/html/index.js +2 -5
  44. package/lib/plugins/html/renderer.js +2 -2
  45. package/lib/plugins/html/setup/summaryBoxes.js +2 -2
  46. package/lib/plugins/html/templates/url/coach/technology.pug +10 -8
  47. package/lib/plugins/influxdb/data-generator.js +6 -8
  48. package/lib/plugins/influxdb/index.js +3 -4
  49. package/lib/plugins/influxdb/send-annotation.js +2 -2
  50. package/lib/plugins/influxdb/send-annotationV2.js +2 -2
  51. package/lib/plugins/lateststorer/index.js +1 -4
  52. package/lib/plugins/matrix/index.js +2 -2
  53. package/lib/plugins/matrix/send.js +2 -2
  54. package/lib/plugins/messagelogger/index.js +4 -3
  55. package/lib/plugins/pagexray/index.js +2 -2
  56. package/lib/plugins/pagexray/pagexrayAggregator.js +11 -10
  57. package/lib/plugins/remove/index.js +2 -2
  58. package/lib/plugins/s3/index.js +29 -28
  59. package/lib/plugins/s3/limit.js +34 -0
  60. package/lib/plugins/scp/index.js +6 -10
  61. package/lib/plugins/slack/index.js +20 -21
  62. package/lib/plugins/sustainable/index.js +35 -14
  63. package/lib/plugins/text/color.js +30 -0
  64. package/lib/plugins/text/textBuilder.js +50 -20
  65. package/lib/sitespeed.js +11 -15
  66. package/lib/support/fileUtil.js +40 -0
  67. package/lib/support/filterRegistry.js +4 -4
  68. package/lib/support/flattenMessage.js +2 -2
  69. package/lib/support/messageMaker.js +2 -2
  70. package/lib/support/metricsFilter.js +9 -16
  71. package/lib/support/osUtil.js +36 -0
  72. package/lib/support/util.js +24 -0
  73. package/npm-shrinkwrap.json +1098 -2593
  74. package/package.json +13 -31
  75. package/tools/postinstall.js +61 -0
  76. package/.github/CONTRIBUTING.md +0 -24
  77. package/.github/FUNDING.yml +0 -12
  78. package/.github/ISSUE_TEMPLATE/BUG_REPORT.yml +0 -65
  79. package/.github/ISSUE_TEMPLATE/FEATURE_IMPROVEMENT.yml +0 -15
  80. package/.github/ISSUE_TEMPLATE/QUESTION.yml +0 -15
  81. package/.github/PULL_REQUEST_TEMPLATE.md +0 -14
  82. package/.github/budget.json +0 -13
  83. package/.github/workflows/building-docker-autobuild.yml +0 -30
  84. package/.github/workflows/building-docker-release.yml +0 -76
  85. package/.github/workflows/crux-test.yml +0 -23
  86. package/.github/workflows/docker-scan.yml +0 -29
  87. package/.github/workflows/docker.yml +0 -39
  88. package/.github/workflows/linux.yml +0 -80
  89. package/.github/workflows/safari.yml +0 -30
  90. package/.github/workflows/sitespeed-io-action-example.yml +0 -22
  91. package/.github/workflows/unittests.yml +0 -41
  92. package/.github/workflows/windows.yml +0 -39
  93. package/.github/workflows/windowsFull.yml +0 -36
  94. package/.netlify +0 -1
  95. package/.spelling +0 -58
  96. package/Gemfile +0 -4
  97. package/Gemfile.lock +0 -53
  98. package/lib/plugins/sustainable/data/url2green.json.gz +0 -0
  99. package/release/feed.js +0 -198
  100. package/release/friendlyNames.js +0 -9
  101. package/release/friendlyNamesBudget.js +0 -15
  102. package/wpr-record.log +0 -102
  103. package/wpr-replay.log +0 -96
@@ -3,11 +3,11 @@ import fs from 'node:fs';
3
3
 
4
4
  import { SitespeedioPlugin } from '@sitespeed.io/plugin';
5
5
  import { Client } from 'node-scp';
6
- import readdir from 'recursive-readdir';
7
- import intel from 'intel';
6
+ import { getLogger } from '@sitespeed.io/log';
8
7
  import { throwIfMissing } from '../../support/util.js';
8
+ import { recursiveReaddir } from '../../support/fileUtil.js';
9
9
 
10
- const log = intel.getLogger('sitespeedio.plugin.scp');
10
+ const log = getLogger('sitespeedio.plugin.scp');
11
11
 
12
12
  async function getClient(scpOptions) {
13
13
  const options = {
@@ -46,7 +46,7 @@ async function upload(dir, scpOptions, prefix) {
46
46
  }
47
47
  await client.uploadDir(dir, path.join(scpOptions.destinationPath, prefix));
48
48
  } catch (error) {
49
- log.error(error);
49
+ log.error(`Error uploading dir ` + error);
50
50
  throw error;
51
51
  } finally {
52
52
  if (client) {
@@ -66,7 +66,7 @@ async function uploadFiles(files, scpOptions, prefix) {
66
66
  );
67
67
  }
68
68
  } catch (error) {
69
- log.error(error);
69
+ log.error(`Error uploading files ` + error);
70
70
  throw error;
71
71
  } finally {
72
72
  if (client) {
@@ -75,12 +75,8 @@ async function uploadFiles(files, scpOptions, prefix) {
75
75
  }
76
76
  }
77
77
 
78
- function ignoreDirectories(file, stats) {
79
- return stats.isDirectory();
80
- }
81
-
82
78
  async function uploadLatestFiles(dir, scpOptions, prefix) {
83
- const files = await readdir(dir, [ignoreDirectories]);
79
+ const files = await recursiveReaddir(dir, true);
84
80
 
85
81
  return uploadFiles(files, scpOptions, prefix);
86
82
  }
@@ -1,7 +1,5 @@
1
- import { promisify } from 'node:util';
2
-
3
- import intel from 'intel';
4
- import Slack from 'node-slack';
1
+ import { getLogger } from '@sitespeed.io/log';
2
+ import { IncomingWebhook } from '@slack/webhook';
5
3
  import merge from 'lodash.merge';
6
4
  import set from 'lodash.set';
7
5
  import { SitespeedioPlugin } from '@sitespeed.io/plugin';
@@ -11,7 +9,7 @@ import { getAttachements } from './attachements.js';
11
9
  import { getSummary } from './summary.js';
12
10
  import { throwIfMissing } from '../../support/util.js';
13
11
 
14
- const log = intel.getLogger('sitespeedio.plugin.slack');
12
+ const log = getLogger('sitespeedio.plugin.slack');
15
13
 
16
14
  const defaultConfig = {
17
15
  userName: 'Sitespeed.io',
@@ -23,8 +21,7 @@ const defaultConfig = {
23
21
 
24
22
  function send(options, dataCollector, context, screenshotType, alias) {
25
23
  const slackOptions = merge({}, defaultConfig, options.slack);
26
- const slack = new Slack(slackOptions.hookUrl);
27
- const send = promisify(slack.send.bind(slack));
24
+ const slack = new IncomingWebhook(slackOptions.hookUrl);
28
25
  const type = slackOptions.type;
29
26
  const pageErrors = [];
30
27
  let logo = 'https://www.sitespeed.io/img/slack/sitespeed-logo-slack.png';
@@ -73,20 +70,22 @@ function send(options, dataCollector, context, screenshotType, alias) {
73
70
  slackOptions.channel,
74
71
  slackOptions.userName
75
72
  );
76
- return send({
77
- text,
78
- icon_url: logo,
79
- channel,
80
- mrkdwn: true,
81
- username: slackOptions.userName,
82
- attachments
83
- }).catch(error => {
84
- if (error.errno === 'ETIMEDOUT') {
85
- log.warn('Timeout sending Slack message.');
86
- } else {
87
- throw error;
88
- }
89
- });
73
+ return slack
74
+ .send({
75
+ text,
76
+ icon_url: logo,
77
+ channel,
78
+ mrkdwn: true,
79
+ username: slackOptions.userName,
80
+ attachments
81
+ })
82
+ .catch(error => {
83
+ if (error.errno === 'ETIMEDOUT') {
84
+ log.warn('Timeout sending Slack message.');
85
+ } else {
86
+ throw error;
87
+ }
88
+ });
90
89
  }
91
90
  }
92
91
 
@@ -4,7 +4,7 @@ import fs from 'node:fs';
4
4
  import zlib from 'node:zlib';
5
5
  import { promisify } from 'node:util';
6
6
 
7
- import intel from 'intel';
7
+ import { getLogger } from '@sitespeed.io/log';
8
8
  import { co2, hosting } from '@tgwf/co2';
9
9
  import { SitespeedioPlugin } from '@sitespeed.io/plugin';
10
10
  import { Aggregator } from './aggregator.js';
@@ -27,7 +27,7 @@ const packageJson = JSON.parse(
27
27
  const co2Version = packageJson.dependencies['@tgwf/co2'];
28
28
 
29
29
  const gunzip = promisify(zlib.gunzip);
30
- const log = intel.getLogger('sitespeedio.plugin.sustainable');
30
+ const log = getLogger('sitespeedio.plugin.sustainable');
31
31
 
32
32
  const DEFAULT_METRICS_PAGE_SUMMARY = [
33
33
  'co2PerPageView',
@@ -79,7 +79,30 @@ export default class SustainablePlugin extends SitespeedioPlugin {
79
79
  DEFAULT_METRICS_PAGE_SUMMARY,
80
80
  'sustainable.pageSummary'
81
81
  );
82
+
83
+ const greenDomainJSONpath = path.join(
84
+ __dirname,
85
+ 'data',
86
+ 'url2green.json.gz'
87
+ );
88
+
89
+ if (
90
+ !this.sustainableOptions.disableHosting &&
91
+ !this.sustainableOptions.useGreenWebHostingAPI
92
+ ) {
93
+ log.info('Reading green domain data from disk');
94
+ try {
95
+ this.data = await loadJSON(greenDomainJSONpath);
96
+ } catch {
97
+ log.error('Green domain local file is missing on disk.');
98
+ log.error(
99
+ 'Run `DOWNLOAD_URL2GREEN=true npm install sitespeed.io` when you install to install the file locally or use the online API using `--sustainable.useGreenWebHostingAPI true` to check if a domain is green hosted.'
100
+ );
101
+ this.data = {};
102
+ }
103
+ }
82
104
  }
105
+
83
106
  async processMessage(message, queue) {
84
107
  const make = this.make;
85
108
  const aggregator = this.aggregator;
@@ -118,23 +141,21 @@ export default class SustainablePlugin extends SitespeedioPlugin {
118
141
  case 'pagexray.run': {
119
142
  // We got data for a URL, lets calculate co2, check green servers etc
120
143
  const listOfDomains = Object.keys(message.data.domains);
121
- const greenDomainJSONpath = path.join(
122
- __dirname,
123
- 'data',
124
- 'url2green.json.gz'
125
- );
126
144
 
127
145
  let hostingGreenCheck;
128
146
  if (this.sustainableOptions.disableHosting === true) {
129
147
  hostingGreenCheck = [];
130
148
  } else {
131
- hostingGreenCheck =
132
- this.sustainableOptions.useGreenWebHostingAPI === true
133
- ? await hosting.check(listOfDomains, undefined, 'sitespeed.io')
134
- : await hosting.check(
135
- listOfDomains,
136
- await loadJSON(greenDomainJSONpath)
137
- );
149
+ if (this.sustainableOptions.useGreenWebHostingAPI === true) {
150
+ log.info('Fetch hosting information from The Green Web Foundation');
151
+ hostingGreenCheck = await hosting(
152
+ listOfDomains,
153
+ undefined,
154
+ 'sitespeed.io'
155
+ );
156
+ } else {
157
+ hostingGreenCheck = await hosting(listOfDomains, this.data);
158
+ }
138
159
  }
139
160
 
140
161
  const configuration = { model: this.sustainableOptions.model };
@@ -0,0 +1,30 @@
1
+ const ansi = {
2
+ reset: '\u001B[0m',
3
+ bold: '\u001B[1m',
4
+ green: '\u001B[32m',
5
+ yellow: '\u001B[33m',
6
+ red: '\u001B[31m',
7
+ blackBright: '\u001B[90m'
8
+ };
9
+
10
+ function stripAnsi(str) {
11
+ // eslint-disable-next-line no-control-regex
12
+ return str.replaceAll(/\u001B\[[0-9;]*[A-Za-z]/g, '');
13
+ }
14
+
15
+ export function colorise(str, clr) {
16
+ if (!clr || !ansi[clr]) return str;
17
+ return ansi[clr] + str + ansi.reset;
18
+ }
19
+
20
+ export function makeBold(str) {
21
+ return ansi.bold + str + ansi.reset;
22
+ }
23
+
24
+ export function getStrippedLength(str) {
25
+ return stripAnsi(str).length;
26
+ }
27
+
28
+ export function blackBright(str) {
29
+ return colorise(str, 'blackBright');
30
+ }
@@ -1,16 +1,42 @@
1
- import table from 'text-table';
2
- import flatten from 'lodash.flatten';
3
- import clic from 'cli-color';
4
- import chunk from 'lodash.chunk';
5
-
6
1
  import { plural, short, cap } from '../../support/helpers/index.js';
7
2
  import summaryBoxesSetup from '../html/setup/summaryBoxes.js';
3
+ import { colorise, makeBold, getStrippedLength, blackBright } from './color.js';
8
4
 
9
- const { getStrippedLength, blackBright, bold } = clic;
10
5
  const tableOptions = {
11
6
  stringLength: getStrippedLength
12
7
  };
13
- const drab = blackBright;
8
+
9
+ function table(rows, options = {}) {
10
+ const { stringLength = str => str.length } = options;
11
+
12
+ const colWidths = [];
13
+ for (const row of rows) {
14
+ for (const [i, cell] of row.entries()) {
15
+ const len = stringLength(String(cell));
16
+ colWidths[i] = Math.max(colWidths[i] || 0, len);
17
+ }
18
+ }
19
+
20
+ return rows
21
+ .map(row =>
22
+ row
23
+ .map((cell, i) => {
24
+ const cellStr = String(cell);
25
+ const padding = colWidths[i] - stringLength(cellStr);
26
+ return cellStr + ' '.repeat(padding);
27
+ })
28
+ .join(' ')
29
+ )
30
+ .join('\n');
31
+ }
32
+
33
+ function chunk(array, size) {
34
+ const out = [];
35
+ for (let i = 0; i < array.length; i += size) {
36
+ out.push(array.slice(i, i + size));
37
+ }
38
+ return out;
39
+ }
14
40
 
15
41
  function getMarker(label) {
16
42
  return { ok: '√', warning: '!', error: '✗', info: '' }[label] || '';
@@ -25,22 +51,21 @@ function getColor(label) {
25
51
 
26
52
  function getHeader(context, options) {
27
53
  const noPages = options.urls.length;
28
- return drab(
54
+ return blackBright(
29
55
  [
30
56
  `${plural(noPages, 'page')} analysed for ${short(context.name, 30)} `,
31
57
  `(${plural(options.browsertime.iterations, 'run')}, `,
32
58
  `${cap(options.browsertime.browser)}/${
33
59
  options.mobile ? 'mobile' : 'desktop'
34
- }/${options.connectivity})`
60
+ })`
35
61
  ].join('')
36
62
  );
37
63
  }
38
64
 
39
65
  function getBoxes(metrics, html) {
40
- return flatten(chunk(summaryBoxesSetup(metrics, html).filter(Boolean), 3));
66
+ return chunk(summaryBoxesSetup(metrics, html).filter(Boolean), 3).flat();
41
67
  }
42
68
 
43
- // foo bar -> fb
44
69
  function abbr(string_) {
45
70
  if (/total|overall/i.test(string_)) return string_.trim();
46
71
  return string_.replaceAll(/\w+\s?/g, a => a[0]);
@@ -54,9 +79,9 @@ export function renderSummary(metrics, context, options) {
54
79
  let out = getHeader(context, options);
55
80
  let rows = getBoxes(metrics, options.html).map(b => {
56
81
  const marker = getMarker(b.label),
57
- c = getColor(b.label);
58
- // color.xterm(ansi)(label),
59
- return [clic[c](marker), clic[c](b.name), bold(b.median)];
82
+ clr = getColor(b.label);
83
+
84
+ return [colorise(marker, clr), colorise(b.name, clr), makeBold(b.median)];
60
85
  });
61
86
  rows.unshift(
62
87
  ['', 'Score / Metric', 'Median'],
@@ -72,13 +97,13 @@ export function renderBriefSummary(metrics, context, options) {
72
97
  reqs = '',
73
98
  rum = '';
74
99
  getBoxes(metrics, options.html).map(b => {
75
- let c = getColor(b.label),
76
- value = b.median,
77
- name;
78
- c = clic[c] || noop;
100
+ const clr = getColor(b.label);
101
+ let value = b.median;
102
+ let name;
103
+ const colorFn = clr ? str => colorise(str, clr) : noop;
79
104
  if (/score$/i.test(b.url)) {
80
105
  name = abbr(b.name.replace('score', ''));
81
- scores.push(c(`${name}:${value}`));
106
+ scores.push(colorFn(`${name}:${value}`));
82
107
  } else if ('pageSize' === b.url) {
83
108
  value = value.replace(' ', ''); // 10 KB -> 10KB
84
109
  size = `${value}`;
@@ -86,6 +111,11 @@ export function renderBriefSummary(metrics, context, options) {
86
111
  reqs = `${value} reqs`;
87
112
  }
88
113
  });
89
- lines.push(drab('Score: ') + scores.reverse().join(', '), size, reqs, rum);
114
+ lines.push(
115
+ blackBright('Score: ') + scores.reverse().join(', '),
116
+ size,
117
+ reqs,
118
+ rum
119
+ );
90
120
  options.summary = { out: `${out}\n` + lines.join(' / ') };
91
121
  }
package/lib/sitespeed.js CHANGED
@@ -6,9 +6,7 @@ import fs from 'node:fs/promises';
6
6
 
7
7
  import dayjs from 'dayjs';
8
8
  import utc from 'dayjs/plugin/utc.js';
9
- import intel from 'intel';
10
- import pullAll from 'lodash.pullall';
11
- import union from 'lodash.union';
9
+ import { getLogger } from '@sitespeed.io/log';
12
10
 
13
11
  import { configure } from './core/logging.js';
14
12
  import { toArray } from './support/util.js';
@@ -26,7 +24,7 @@ const packageJson = JSON.parse(
26
24
  await fs.readFile(path.resolve(path.join(__dirname, '..', 'package.json')))
27
25
  );
28
26
 
29
- const log = intel.getLogger('sitespeedio');
27
+ const log = getLogger('sitespeedio');
30
28
  dayjs.extend(utc);
31
29
 
32
30
  const budgetResult = {
@@ -61,9 +59,6 @@ export async function run(options) {
61
59
  options
62
60
  );
63
61
 
64
- // Setup logging
65
- const logDir = await storageManager.createDirectory('logs');
66
-
67
62
  if (
68
63
  options.browsertime &&
69
64
  options.browsertime.tcpdump &&
@@ -74,7 +69,7 @@ export async function run(options) {
74
69
  'SSLKEYLOGFILE.txt'
75
70
  );
76
71
  }
77
- configure(options, logDir);
72
+ configure(options);
78
73
 
79
74
  // Tell the world what we are using
80
75
  log.info(
@@ -85,9 +80,8 @@ export async function run(options) {
85
80
  packageJson.dependencies.browsertime,
86
81
  packageJson.dependencies['coach-core']
87
82
  );
88
- if (log.isEnabledFor(log.DEBUG)) {
89
- log.debug('Running with options: %:2j', options);
90
- }
83
+
84
+ log.debug('Running with options: %:2j', options);
91
85
 
92
86
  let pluginNames = await parsePluginNames(options);
93
87
 
@@ -107,8 +101,11 @@ export async function run(options) {
107
101
  // Finalize the plugins that we wanna run
108
102
  // First we add the new ones and then remove, that means remove
109
103
  // always wins
110
- pluginNames = union(pluginNames, toArray(plugins.add));
111
- pullAll(pluginNames, toArray(plugins.remove));
104
+ pluginNames = [...new Set([...pluginNames, ...toArray(plugins.add)])];
105
+
106
+ const removeSet = new Set(toArray(plugins.remove));
107
+ pluginNames = pluginNames.filter(name => !removeSet.has(name));
108
+
112
109
  if (plugins.list) {
113
110
  log.info('The following plugins are enabled: %s', pluginNames.join(', '));
114
111
  }
@@ -122,12 +119,11 @@ export async function run(options) {
122
119
  budget: budgetResult,
123
120
  name: url,
124
121
  log,
125
- intel,
122
+ getLogger,
126
123
  messageMaker,
127
124
  statsHelpers,
128
125
  filterRegistry
129
126
  };
130
-
131
127
  const queueHandler = new QueueHandler(options);
132
128
  const runningPlugins = await loadPlugins(
133
129
  pluginNames,
@@ -0,0 +1,40 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ export async function recursiveReaddir(dir, ignoreDirectories = false) {
6
+ const results = [];
7
+ const entries = await readdir(dir, { withFileTypes: true });
8
+
9
+ for (const entry of entries) {
10
+ const fullPath = path.join(dir, entry.name);
11
+
12
+ if (entry.isDirectory()) {
13
+ if (!ignoreDirectories) {
14
+ results.push(fullPath);
15
+ }
16
+ const subPaths = await recursiveReaddir(fullPath, ignoreDirectories);
17
+ results.push(...subPaths);
18
+ } else {
19
+ results.push(fullPath);
20
+ }
21
+ }
22
+
23
+ return results;
24
+ }
25
+
26
+ export function findUpSync(filenames, startDir = process.cwd()) {
27
+ let currentDir = startDir;
28
+
29
+ while (currentDir !== path.dirname(currentDir)) {
30
+ for (const filename of filenames) {
31
+ const filePath = path.resolve(currentDir, filename);
32
+ if (existsSync(filePath)) {
33
+ return filePath;
34
+ }
35
+ }
36
+ currentDir = path.dirname(currentDir);
37
+ }
38
+
39
+ return;
40
+ }
@@ -1,4 +1,3 @@
1
- import clone from 'lodash.clonedeep';
2
1
  import { filterMetrics } from './metricsFilter.js';
3
2
 
4
3
  let filterForType = {};
@@ -34,7 +33,8 @@ export function filterMessage(message) {
34
33
  return message;
35
34
  }
36
35
 
37
- const filteredMessage = clone(message);
38
- filteredMessage.data = filterMetrics(filteredMessage.data, filterConfig);
39
- return filteredMessage;
36
+ return {
37
+ ...message,
38
+ data: filterMetrics(message.data, filterConfig)
39
+ };
40
40
  }
@@ -1,6 +1,6 @@
1
1
  import { parse } from 'node:url';
2
- import intel from 'intel';
3
- const log = intel.getLogger('sitespeedio');
2
+ import { getLogger } from '@sitespeed.io/log';
3
+ const log = getLogger('sitespeedio');
4
4
 
5
5
  function joinNonEmpty(strings, delimeter) {
6
6
  return strings.filter(Boolean).join(delimeter);
@@ -1,12 +1,12 @@
1
1
  import dayjs from 'dayjs';
2
2
  import merge from 'lodash.merge';
3
- import { v4 as makeUuid } from 'uuid';
3
+ import { randomUUID } from 'node:crypto';
4
4
 
5
5
  export function messageMaker(source) {
6
6
  return {
7
7
  make(type, data, extras) {
8
8
  const timestamp = dayjs().format();
9
- const uuid = makeUuid();
9
+ const uuid = randomUUID();
10
10
 
11
11
  return merge({ uuid, type, timestamp, source, data }, extras);
12
12
  }
@@ -1,10 +1,8 @@
1
- import isEmpty from 'lodash.isempty';
2
1
  import get from 'lodash.get';
3
2
  import set from 'lodash.set';
4
3
  import merge from 'lodash.merge';
5
- import reduce from 'lodash.reduce';
6
4
 
7
- import { toArray } from './util.js';
5
+ import { toArray, isEmpty } from './util.js';
8
6
 
9
7
  function normalizePath(path) {
10
8
  if (path.endsWith('.*')) return path.slice(0, -2);
@@ -31,20 +29,15 @@ export function filterMetrics(json, metricPaths) {
31
29
  } else if (firstWildcard === 0) {
32
30
  const leafPath = path.slice(2);
33
31
 
34
- reduce(
35
- json,
36
- (result, value, key) => {
37
- if (typeof value === 'object') {
38
- const leaf = this.filterMetrics(value, leafPath);
39
-
40
- if (!isEmpty(leaf)) {
41
- result[key] = leaf;
42
- }
32
+ Object.entries(json).reduce((result, [key, value]) => {
33
+ if (typeof value === 'object') {
34
+ const leaf = this.filterMetrics(value, leafPath);
35
+ if (leaf && Object.keys(leaf).length > 0) {
36
+ result[key] = leaf;
43
37
  }
44
- return result;
45
- },
46
- result
47
- );
38
+ }
39
+ return result;
40
+ }, result);
48
41
  } else {
49
42
  let branchPath = path.slice(0, Math.max(0, firstWildcard));
50
43
  if (branchPath.endsWith('.')) branchPath = branchPath.slice(0, -1);
@@ -0,0 +1,36 @@
1
+ import os from 'node:os';
2
+ import { promises as fsPromises } from 'node:fs';
3
+
4
+ export function osName() {
5
+ const platform = os.platform();
6
+ if (platform === 'win32') return 'Windows';
7
+ if (platform === 'darwin') return 'macOS';
8
+ if (platform === 'linux') return 'Linux';
9
+ return platform;
10
+ }
11
+
12
+ export async function getOS() {
13
+ if (os.platform() !== 'linux') {
14
+ return { dist: osName(), release: '' };
15
+ }
16
+
17
+ try {
18
+ const data = await fsPromises.readFile('/etc/os-release', 'utf8');
19
+ const lines = data.split('\n');
20
+ let dist = 'Linux';
21
+ let release = '';
22
+
23
+ for (const line of lines) {
24
+ if (line.startsWith('NAME=')) {
25
+ dist = line.split('=')[1].replaceAll('"', '');
26
+ } else if (line.startsWith('VERSION_ID=')) {
27
+ release = line.split('=')[1].replaceAll('"', '');
28
+ }
29
+ }
30
+
31
+ return { dist, release };
32
+ } catch {
33
+ // Return fallback values if reading fails
34
+ return { dist: 'Linux', release: '' };
35
+ }
36
+ }
@@ -21,3 +21,27 @@ export function throwIfMissing(options, keys, namespace) {
21
21
  );
22
22
  }
23
23
  }
24
+
25
+ export function isEmpty(value) {
26
+ if (value === null) return true;
27
+
28
+ if (value === undefined) return true;
29
+
30
+ if (typeof value === 'boolean') return false;
31
+
32
+ if (typeof value === 'number') return false;
33
+
34
+ if (typeof value === 'string') return value.length === 0;
35
+
36
+ if (typeof value === 'function') return false;
37
+
38
+ if (Array.isArray(value)) return value.length === 0;
39
+
40
+ if (value instanceof Map || value instanceof Set) return value.size === 0;
41
+
42
+ if (typeof value === 'object') {
43
+ return Object.keys(value).length === 0;
44
+ }
45
+
46
+ return false;
47
+ }