From 631271126f9711614e2bb6e700d6bf231ca68670 Mon Sep 17 00:00:00 2001 From: Peter Hedenskog Date: Sat, 25 Feb 2023 11:16:58 +0100 Subject: [PATCH] New plugins structure and esmodule (#3769) * New plugins structure and esmodule --- .eslintignore | 3 +- .eslintrc.json | 13 +- .github/workflows/docker.yml | 12 +- Dockerfile | 2 +- bin/browsertimeWebPageReplay.js | 32 +- bin/sitespeed.js | 41 +- cz-config.js | 74 - .../browsertime/configuration/config.md | 149 +- lib/cli/cli.js | 479 ++- lib/cli/util.js | 233 +- lib/core/logging.js | 52 +- lib/core/pluginLoader.js | 110 +- lib/core/queueHandler.js | 175 +- lib/core/queueStatistics.js | 123 +- lib/core/resultsStorage/index.js | 38 +- lib/core/resultsStorage/pathToFolder.js | 38 +- lib/core/resultsStorage/resultUrls.js | 13 +- lib/core/resultsStorage/storageManager.js | 73 +- lib/core/script-source.js | 18 +- lib/core/url-source.js | 47 +- lib/plugins/analysisstorer/index.js | 80 +- lib/plugins/assets/aggregator.js | 42 +- lib/plugins/assets/assetsBySize.js | 6 +- lib/plugins/assets/assetsBySpeed.js | 6 +- lib/plugins/assets/index.js | 22 +- lib/plugins/axe/axePostScript.cjs | 5 +- lib/plugins/axe/index.js | 29 +- lib/plugins/browsertime/analyzer.js | 166 +- lib/plugins/browsertime/axeAggregator.js | 17 +- ...aggregator.js => browsertimeAggregator.js} | 99 +- .../browsertime/consoleLogAggregator.js | 15 +- lib/plugins/browsertime/default/config.js | 4 +- .../browsertime/default/metricsPageSummary.js | 4 +- lib/plugins/browsertime/default/metricsRun.js | 2 +- .../browsertime/default/metricsRunLimited.js | 2 +- .../browsertime/default/metricsSummary.js | 4 +- lib/plugins/browsertime/filmstrip.js | 228 +- lib/plugins/browsertime/index.js | 341 +- lib/plugins/browsertime/reader.js | 25 +- lib/plugins/budget/deprecatedVerify.js | 109 +- lib/plugins/budget/index.js | 57 +- lib/plugins/budget/json.js | 18 +- lib/plugins/budget/junit.js | 34 +- lib/plugins/budget/tap.js | 17 +- lib/plugins/budget/verify.js | 286 +- lib/plugins/coach/aggregator.js | 32 +- lib/plugins/coach/index.js | 22 +- lib/plugins/crawler/index.js | 41 +- lib/plugins/crux/cli.js | 29 - lib/plugins/crux/index.js | 55 +- lib/plugins/crux/repackage.js | 6 +- lib/plugins/crux/send.js | 91 +- lib/plugins/domains/aggregator.js | 51 +- lib/plugins/domains/index.js | 23 +- lib/plugins/gcs/index.js | 63 +- lib/plugins/grafana/cli.js | 37 - lib/plugins/grafana/index.js | 164 +- lib/plugins/grafana/send-annotation.js | 266 +- lib/plugins/graphite/cli.js | 104 - lib/plugins/graphite/data-generator.js | 89 +- lib/plugins/graphite/graphite-sender.js | 12 +- lib/plugins/graphite/helpers/format-entry.js | 11 +- lib/plugins/graphite/helpers/is-statsd.js | 4 +- lib/plugins/graphite/index.js | 154 +- lib/plugins/graphite/send-annotation.js | 249 +- lib/plugins/graphite/sender.js | 13 +- lib/plugins/graphite/statsd-sender.js | 12 +- lib/plugins/harstorer/index.js | 54 +- lib/plugins/html/dataCollector.js | 14 +- lib/plugins/html/defaultConfig.js | 4 +- lib/plugins/html/getScripts.js | 11 +- lib/plugins/html/htmlBuilder.js | 122 +- lib/plugins/html/index.js | 36 +- lib/plugins/html/metricHelper.js | 156 +- lib/plugins/html/renderer.js | 39 +- lib/plugins/html/setup/detailed.js | 94 +- lib/plugins/html/setup/summaryBoxes.js | 44 +- .../html/setup/summaryBoxesDefaultLimits.js | 9 +- .../html/templates/url/includes/tabScripts.js | 16 +- lib/plugins/influxdb/cli.js | 54 - lib/plugins/influxdb/data-generator.js | 319 +- lib/plugins/influxdb/index.js | 112 +- lib/plugins/influxdb/send-annotation.js | 219 +- lib/plugins/influxdb/sender.js | 10 +- lib/plugins/lateststorer/index.js | 55 +- lib/plugins/matrix/cli.js | 31 - lib/plugins/matrix/index.js | 98 +- lib/plugins/matrix/send.js | 25 +- lib/plugins/messagelogger/index.js | 38 +- lib/plugins/metrics/index.js | 86 +- lib/plugins/pagexray/index.js | 42 +- lib/plugins/pagexray/pagexrayAggregator.js | 113 +- lib/plugins/remove/index.js | 14 +- lib/plugins/s3/contentType.js | 12 +- lib/plugins/s3/index.js | 78 +- lib/plugins/scp/index.js | 71 +- lib/plugins/slack/attachements.js | 38 +- lib/plugins/slack/dataCollector.js | 12 +- lib/plugins/slack/index.js | 51 +- lib/plugins/slack/summary.js | 50 +- lib/plugins/sustainable/aggregator.js | 48 +- lib/plugins/sustainable/index.js | 422 +-- lib/plugins/sustainable/pug/index.pug | 14 +- lib/plugins/text/index.js | 24 +- lib/plugins/text/textBuilder.js | 119 +- lib/plugins/thirdparty/aggregator.js | 56 +- lib/plugins/thirdparty/index.js | 86 +- lib/plugins/tracestorer/index.js | 17 +- lib/sitespeed.js | 297 +- lib/support/annotationsHelper.js | 127 +- lib/support/filterRegistry.js | 86 +- lib/support/flattenMessage.js | 257 +- lib/support/friendlynames.js | 12 +- lib/support/helpers/cap.js | 8 +- lib/support/helpers/co2.js | 20 +- lib/support/helpers/decimals.js | 10 +- lib/support/helpers/get.js | 9 +- lib/support/helpers/httpErrors.js | 6 +- lib/support/helpers/index.js | 32 +- lib/support/helpers/label.js | 6 +- lib/support/helpers/noop.js | 6 +- lib/support/helpers/percent.js | 6 +- lib/support/helpers/plural.js | 6 +- lib/support/helpers/scoreLabel.js | 7 +- lib/support/helpers/short.js | 6 +- lib/support/helpers/shortAsset.js | 10 +- lib/support/helpers/size.js | 5 +- lib/support/helpers/time.js | 12 +- lib/support/messageMaker.js | 16 +- lib/support/metricsFilter.js | 134 +- lib/support/statsHelpers.js | 126 +- lib/support/tsdbUtil.js | 69 +- lib/support/util.js | 46 +- npm-shrinkwrap.json | 3082 +++++++++++------ package.json | 24 +- release/feed.js | 45 +- release/friendlyNames.js | 4 +- release/friendlyNamesBudget.js | 8 +- test/cliTests.js | 21 +- test/cliUtilTests.js | 138 +- test/coachTests.js | 20 +- test/domainTests.js | 19 +- test/grafanaTests.js | 13 - test/graphiteTests.js | 26 +- test/influxdbTests.js | 7 +- test/pathToFolderTests.js | 13 +- test/resultUrlTests.js | 13 +- test/runWithoutCli.js | 12 +- test/slackTests.js | 38 +- test/storageManagerTests.js | 17 +- tools/check-licenses.js | 21 +- tools/tcp-server.js | 18 +- tools/udp-server.js | 8 +- 153 files changed, 6723 insertions(+), 5927 deletions(-) delete mode 100644 cz-config.js rename lib/plugins/browsertime/{aggregator.js => browsertimeAggregator.js} (73%) delete mode 100644 lib/plugins/crux/cli.js delete mode 100644 lib/plugins/grafana/cli.js delete mode 100644 lib/plugins/graphite/cli.js delete mode 100644 lib/plugins/influxdb/cli.js delete mode 100644 lib/plugins/matrix/cli.js delete mode 100644 test/grafanaTests.js diff --git a/.eslintignore b/.eslintignore index 7da563ec9..d7d2a400b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,7 +4,6 @@ assets/* sitespeed-result/* lib/plugins/yslow/scripts/* lib/plugins/html/assets/js/* -lib/plugins/browsertime/index.js -lib/plugins/browsertime/analyzer.js bin/browsertimeWebPageReplay.js test/data/* +test/prepostscripts/* diff --git a/.eslintrc.json b/.eslintrc.json index 023e52570..5e1e09a98 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,10 +5,11 @@ "es6": true }, "parserOptions": { - "ecmaVersion": 2018 + "ecmaVersion": "latest", + "sourceType": "module" }, - "plugins": ["prettier"], - "extends": "eslint:recommended", + "plugins": ["prettier", "unicorn"], + "extends": ["eslint:recommended", "plugin:unicorn/recommended"], "rules": { "prettier/prettier": [ "error", @@ -21,6 +22,10 @@ ], "require-atomic-updates": 0, "no-extra-semi": 0, - "no-mixed-spaces-and-tabs": 0 + "no-mixed-spaces-and-tabs": 0, + "unicorn/filename-case": 0, + "unicorn/prevent-abbreviations": 0, + "unicorn/no-array-reduce": 0, + "unicorn/prefer-spread":0 } } diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index db4a7a42c..8b27fe603 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -21,14 +21,14 @@ jobs: - name: Start local HTTP server run: (serve test/data/html/ -l 3001&) - name: Run test on default container for Chrome - run: docker run --rm --network=host sitespeedio/sitespeed.io http://127.0.0.1:3001 -n 1 -b chrome + run: docker run --rm -v "$(pwd)":/sitespeed.io --network=host sitespeedio/sitespeed.io http://127.0.0.1:3001 -n 1 -b chrome - name: Run test on default container for Firefox - run: docker run --rm --network=host sitespeedio/sitespeed.io http://127.0.0.1:3001 -n 1 -b firefox + run: docker run --rm -v "$(pwd)":/sitespeed.io --network=host sitespeedio/sitespeed.io http://127.0.0.1:3001 -n 1 -b firefox - name: Run test on default container for Edge - run: docker run --rm --network=host sitespeedio/sitespeed.io http://127.0.0.1:3001 -n 1 -b edge + run: docker run --rm -v "$(pwd)":/sitespeed.io --network=host sitespeedio/sitespeed.io http://127.0.0.1:3001 -n 1 -b edge - name: Run test on slim container - run: docker run --rm --network=host sitespeedio/sitespeed.io:slim http://127.0.0.1:3001 -n 1 --browsertime.firefox.preference "devtools.netmonitor.persistlog:true" + run: docker run --rm -v "$(pwd)":/sitespeed.io --network=host sitespeedio/sitespeed.io:slim http://127.0.0.1:3001 -n 1 --browsertime.firefox.preference "devtools.netmonitor.persistlog:true" - name: Test WebPageReplay with Chrome - run: docker run --cap-add=NET_ADMIN --rm -e REPLAY=true -e LATENCY=100 sitespeedio/sitespeed.io https://www.sitespeed.io -n 3 -b chrome + run: docker run --cap-add=NET_ADMIN --rm -v "$(pwd)":/sitespeed.io -e REPLAY=true -e LATENCY=100 sitespeedio/sitespeed.io https://www.sitespeed.io -n 3 -b chrome - name: Test WebPageReplay with Firefox - run: docker run --cap-add=NET_ADMIN --rm --network=host -e REPLAY=true -e LATENCY=100 sitespeedio/sitespeed.io https://www.sitespeed.io -n 3 -b firefox --browsertime.firefox.acceptInsecureCerts true \ No newline at end of file + run: docker run --cap-add=NET_ADMIN --rm -v "$(pwd)":/sitespeed.io --network=host -e REPLAY=true -e LATENCY=100 sitespeedio/sitespeed.io https://www.sitespeed.io -n 3 -b firefox --browsertime.firefox.acceptInsecureCerts true \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2e495ceab..f4de8ee4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM sitespeedio/webbrowsers:chrome-110.0-firefox-109.0-edge-109.0 +FROM sitespeedio/webbrowsers:chrome-110.0-firefox-110.0-edge-110.0 ARG TARGETPLATFORM=linux/amd64 diff --git a/bin/browsertimeWebPageReplay.js b/bin/browsertimeWebPageReplay.js index 1c723b096..cba9072b6 100755 --- a/bin/browsertimeWebPageReplay.js +++ b/bin/browsertimeWebPageReplay.js @@ -1,25 +1,29 @@ #!/usr/bin/env node -'use strict'; +import { readFileSync } from 'node:fs'; -const yargs = require('yargs'); -const merge = require('lodash.merge'); -const getURLs = require('../lib/cli/util').getURLs; -const get = require('lodash.get'); -const set = require('lodash.set'); -const findUp = require('find-up'); -const fs = require('fs'); -const browsertimeConfig = require('../lib/plugins/browsertime/index').config; +import merge from 'lodash.merge'; +import set from 'lodash.set'; +import get from 'lodash.get'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +import { findUpSync } from 'find-up'; +import { BrowsertimeEngine, configureLogging } from 'browsertime'; + +import { getURLs } from '../lib/cli/util.js'; + +import {config as browsertimeConfig} from '../lib/plugins/browsertime/index.js'; const iphone6UserAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_1_3 like Mac OS X) AppleWebKit/536.26 ' + '(KHTML, like Gecko) Version/6.0 Mobile/10B329 Safari/8536.25'; -const configPath = findUp.sync(['.sitespeed.io.json']); +const configPath = findUpSync(['.sitespeed.io.json']); let config; try { - config = configPath ? JSON.parse(fs.readFileSync(configPath)) : {}; + config = configPath ? JSON.parse(readFileSync(configPath)) : {}; } catch (e) { if (e instanceof SyntaxError) { /* eslint no-console: off */ @@ -49,7 +53,8 @@ async function testURLs(engine, urls) { } async function runBrowsertime() { - let parsed = yargs + let yargsInstance = yargs(hideBin(process.argv)); + let parsed = yargsInstance .env('SITESPEED_IO') .require(1, 'urlOrFile') .option('browsertime.browser', { @@ -139,9 +144,6 @@ async function runBrowsertime() { } }; - - - const {BrowsertimeEngine, configureLogging} = await import ('browsertime'); const btOptions = merge({}, parsed.argv.browsertime, defaultConfig); // hack to keep backward compability to --android if (parsed.argv.android[0] === true) { diff --git a/bin/sitespeed.js b/bin/sitespeed.js index 885c23b6a..b97f9e445 100755 --- a/bin/sitespeed.js +++ b/bin/sitespeed.js @@ -2,24 +2,30 @@ /*eslint no-console: 0*/ -'use strict'; +import { writeFileSync } from 'node:fs'; +import { execSync } from 'node:child_process'; +import { platform } from 'node:os'; +import { parseCommandLine } from '../lib/cli/cli.js'; +import { run } from '../lib/sitespeed.js'; -const fs = require('fs'); -const cli = require('../lib/cli/cli'); -const sitespeed = require('../lib/sitespeed'); -const { execSync } = require('child_process'); -const os = require('os'); +async function start() { + let parsed = await parseCommandLine(); + let budgetFailing = false; + // hack for getting in the unchanged cli options + parsed.options.explicitOptions = parsed.explicitOptions; + parsed.options.urls = parsed.urls; + parsed.options.urlsMetaData = parsed.urlsMetaData; -async function run(options) { + let options = parsed.options; process.exitCode = 1; try { - const result = await sitespeed.run(options); + const result = await run(options); if (options.storeResult) { if (options.storeResult != 'true') { - fs.writeFileSync(options.storeResult, JSON.stringify(result)); + writeFileSync(options.storeResult, JSON.stringify(result)); } else { - fs.writeFileSync('result.json', JSON.stringify(result)); + writeFileSync('result.json', JSON.stringify(result)); } } @@ -27,9 +33,9 @@ async function run(options) { throw new Error('Errors while running:\n' + result.errors.join('\n')); } - if ((options.open || options.o) && os.platform() === 'darwin') { + if ((options.open || options.o) && platform() === 'darwin') { execSync('open ' + result.localPath + '/index.html'); - } else if ((options.open || options.o) && os.platform() === 'linux') { + } else if ((options.open || options.o) && platform() === 'linux') { execSync('xdg-open ' + result.localPath + '/index.html'); } @@ -47,17 +53,12 @@ async function run(options) { ) { process.exitCode = 0; } - } catch (e) { + } catch (error) { process.exitCode = 1; + console.log(error); } finally { process.exit(); } } -let parsed = cli.parseCommandLine(); -let budgetFailing = false; -// hack for getting in the unchanged cli options -parsed.options.explicitOptions = parsed.explicitOptions; -parsed.options.urls = parsed.urls; -parsed.options.urlsMetaData = parsed.urlsMetaData; -run(parsed.options); +await start(); diff --git a/cz-config.js b/cz-config.js deleted file mode 100644 index b8a01c7bc..000000000 --- a/cz-config.js +++ /dev/null @@ -1,74 +0,0 @@ -module.exports = { - types: [ - { value: 'feat', name: 'feat: A new feature' }, - { value: 'fix', name: 'fix: A bug fix' }, - { value: 'docs', name: 'docs: Documentation only changes' }, - { - value: 'style', - name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)' - }, - { - value: 'refactor', - name: 'refactor: A code change that neither fixes a bug nor adds a feature' - }, - { - value: 'perf', - name: 'perf: A code change that improves performance' - }, - { value: 'test', name: 'test: Adding missing tests' }, - { - value: 'chore', - name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation' - }, - { value: 'revert', name: 'revert: Revert to a commit' }, - { value: 'WIP', name: 'WIP: Work in progress' } - ], - - // scopes: [ - // { name: 'accounts' }, - // { name: 'admin' }, - // { name: 'exampleScope' }, - // { name: 'changeMe' } - // ], - - allowTicketNumber: false, - isTicketNumberRequired: false, - // ticketNumberPrefix: 'TICKET-', - // ticketNumberRegExp: '\\d{1,5}', - - // it needs to match the value for field type. Eg.: 'fix' - /* - scopeOverrides: { - fix: [ - {name: 'merge'}, - {name: 'style'}, - {name: 'e2eTest'}, - {name: 'unitTest'} - ] - }, - */ - // override the messages, defaults are as follows - messages: { - type: "Select the type of change that you're committing:", - // scope: '\nDenote the SCOPE of this change (optional):', - // used if allowCustomScopes is true - // customScope: 'Denote the SCOPE of this change:', - subject: 'Write a SHORT, IMPERATIVE tense description of the change:\n', - body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n', - breaking: 'List any BREAKING CHANGES (optional):\n', - footer: - 'List any ISSUES CLOSED by this change (optional). E.g.: #31, #34:\n', - confirmCommit: 'Are you sure you want to proceed with the commit above?' - }, - - // allowCustomScopes: true, - allowBreakingChanges: ['feat', 'fix'], - // skip any questions you want - skipQuestions: ['body'], - - // limit subject length - subjectLimit: 100 - // breaklineChar: '|', // It is supported for fields body and footer. - // footerPrefix : 'ISSUES CLOSED:' - // askForBreakingChangeFirst : true, // default is false -}; diff --git a/docs/documentation/browsertime/configuration/config.md b/docs/documentation/browsertime/configuration/config.md index eae773505..12a9d1bb1 100644 --- a/docs/documentation/browsertime/configuration/config.md +++ b/docs/documentation/browsertime/configuration/config.md @@ -38,6 +38,18 @@ chrome --chrome.blockDomainsExcept, --blockDomainsExcept Block all domains except this domain. Use it multiple time to keep multiple domains. You can also wildcard domains like *.sitespeed.io. Use this when you wanna block out all third parties. --chrome.ignoreCertificateErrors Make Chrome ignore certificate errors. Defaults to true. [boolean] [default: true] +android + --android.powerTesting, --androidPower Enables android power testing - charging must be disabled for this.(You have to disable charging yourself for this - it depends on the phone model). [boolean] + --android.ignoreShutdownFailures, --ignoreShutdownFailures If set, shutdown failures will be ignored on Android. [boolean] [default: false] + --android.rooted, --androidRooted If your phone is rooted you can use this to set it up following Mozillas best practice for stable metrics. [boolean] [default: false] + --android.batteryTemperatureLimit, --androidBatteryTemperatureLimit Do the battery temperature need to be below a specific limit before we start the test? + --android.batteryTemperatureWaitTimeInSeconds, --androidBatteryTemperatureWaitTimeInSeconds How long time to wait (in seconds) if the androidBatteryTemperatureWaitTimeInSeconds is not met before the next try [default: 120] + --android.batteryTemperatureReboot, --androidBatteryTemperatureReboot If your phone does not get the minimum temperature aftet the wait time, reboot the phone. [boolean] [default: false] + --android.pretestPowerPress, --androidPretestPowerPress Press the power button on the phone before a test starts. [boolean] [default: false] + --android.pretestPressHomeButton, --androidPretestPressHomeButton Press the home button on the phone before a test starts. [boolean] [default: false] + --android.verifyNetwork, --androidVerifyNetwork Before a test start, verify that the device has a Internet connection by pinging 8.8.8.8 (or a configurable domain with --androidPingAddress) [boolean] [default: false] + --android.gnirehtet, --gnirehtet Start gnirehtet and reverse tethering the traffic from your Android phone. [boolean] [default: false] + firefox --firefox.binaryPath Path to custom Firefox binary (e.g. Firefox Nightly). On OS X, the path should be to the binary inside the app bundle, e.g. /Applications/Firefox.app/Contents/MacOS/firefox-bin --firefox.geckodriverPath Path to custom geckodriver binary. Make sure to use a geckodriver version that's compatible with the version of Firefox (Gecko) you're using @@ -134,76 +146,67 @@ debug --debug Run Browsertime in debug mode. [boolean] [default: false] Options: - --cpu Easy way to enable both chrome.timeline for Chrome and geckoProfile for Firefox [boolean] - --androidPower Enables android power testing - charging must be disabled for this.(You have to disable charging yourself for this - it depends on the phone model). [boolean] - --video Record a video and store the video. Set it to false to remove the video that is created by turning on visualMetrics. To remove fully turn off video recordings, make sure to set video and visualMetrics to false. Requires FFMpeg to be installed. [boolean] - --visualMetrics Collect Visual Metrics like First Visual Change, SpeedIndex, Perceptual Speed Index and Last Visual Change. Requires FFMpeg and Python dependencies [boolean] - --visualElements, --visuaElements Collect Visual Metrics from elements. Works only with --visualMetrics turned on. By default you will get visual metrics from the largest image within the view port and the largest h1. You can also configure to pickup your own defined elements with --scriptInput.visualElements [boolean] - --visualMetricsPerceptual Collect Perceptual Speed Index when you run --visualMetrics. [boolean] - --visualMetricsContentful Collect Contentful Speed Index when you run --visualMetrics. [boolean] - --visualMetricsPortable Use the portable visual-metrics processing script (no ImageMagick dependencies). [boolean] - --scriptInput.visualElements Include specific elements in visual elements. Give the element a name and select it with document.body.querySelector. Use like this: --scriptInput.visualElements name:domSelector see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors. Add multiple instances to measure multiple elements. Visual Metrics will use these elements and calculate when they are visible and fully rendered. - --scriptInput.longTask, --minLongTaskLength Set the minimum length of a task to be categorised as a CPU Long Task. It can never be smaller than 50. The value is in ms and only works in Chromium browsers at the moment. [number] [default: 50] - -b, --browser Specify browser. Safari only works on OS X/iOS. Edge only work on OS that supports Edge. [choices: "chrome", "firefox", "edge", "safari"] [default: "chrome"] - --ignoreShutdownFailures If set, shutdown failures will be ignored on Android. [boolean] [default: false] - --android Short key to use Android. Defaults to use com.android.chrome unless --browser is specified. [boolean] [default: false] - --androidRooted If your phone is rooted you can use this to set it up following Mozillas best practice for stable metrics. [boolean] [default: false] - --androidBatteryTemperatureLimit Do the battery temperature need to be below a specific limit before we start the test? - --androidBatteryTemperatureWaitTimeInSeconds How long time to wait (in seconds) if the androidBatteryTemperatureWaitTimeInSeconds is not met before the next try [default: 120] - --androidBatteryTemperatureReboot If your phone does not get the minimum temperature aftet the wait time, reboot the phone. [boolean] [default: false] - --androidPretestPowerPress Press the power button on the phone before a test starts. [boolean] [default: false] - --androidPretestPressHomeButton Press the home button on the phone before a test starts. [boolean] [default: false] - --androidVerifyNetwork Before a test start, verify that the device has a Internet connection by pinging 8.8.8.8 (or a configurable domain with --androidPingAddress) [boolean] [default: false] - --processStartTime Capture browser process start time (in milliseconds). Android only for now. [boolean] [default: false] - --pageCompleteCheck 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 - --pageCompleteWaitTime 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. [default: 8000] - --pageCompleteCheckInactivity Alternative way to choose when to end your test. This will wait for 2 seconds of inactivity that happens after loadEventEnd. [boolean] [default: false] - --pageCompleteCheckPollTimeout The time in ms to wait for running the page complete check the next time. [number] [default: 1500] - --pageCompleteCheckStartWait 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 [number] [default: 5000] - --pageLoadStrategy Set the strategy to waiting for document readiness after a navigation event. After the strategy is ready, your pageCompleteCheck will start runninhg. [string] [choices: "eager", "none", "normal"] [default: "none"] - -n, --iterations Number of times to test the url (restarting the browser between each test) [number] [default: 3] - --prettyPrint Enable to print json/har with spaces and indentation. Larger files, but easier on the eye. [boolean] [default: false] - --delay Delay between runs, in milliseconds [number] [default: 0] - --timeToSettle 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 [number] [default: 0] - --webdriverPageload Use webdriver.get to initialize the page load instead of window.location. [boolean] [default: false] - -r, --requestheader Request header that will be added to the request. Add multiple instances to add multiple request headers. Works for Firefox and Chrome. Use the following format key:value - --cookie Cookie that will be added to the request. Add multiple instances to add multiple request cookies. Works for Firefox and Chrome. Use the following format cookieName=cookieValue - --injectJs Inject JavaScript into the current page at document_start. Works for Firefox and Chrome. More info: https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/contentScripts - --block Domain to block or URL or URL pattern to block. If you use Chrome you can also use --blockDomainsExcept (that is more performant). Works in Chrome/Edge. For Firefox you can only block domains. - --percentiles The percentile values within the data browsertime will calculate and report. This argument uses Yargs arrays and you you to set them correctly it is recommended to use a configuraration file instead. [array] [default: [0,10,90,99,100]] - --decimals The decimal points browsertime statistics round to. [number] [default: 0] - --iqr Use IQR, or Inter Quartile Range filtering filters data based on the spread of the data. See https://en.wikipedia.org/wiki/Interquartile_range. In some cases, IQR filtering may not filter out anything. This can happen if the acceptable range is wider than the bounds of your dataset. [boolean] [default: false] - --cacheClearRaw Use internal browser functionality to clear browser cache between runs instead of only using Selenium. [boolean] [default: false] - --basicAuth Use it if your server is behind Basic Auth. Format: username@password (Only Chrome and Firefox at the moment). - --preScript, --setUp Selenium script(s) to run before you test your URL/script. They will run outside of the analyse phase. Note that --preScript can be passed multiple times. - --postScript, --tearDown Selenium script(s) to run after you test your URL. They will run outside of the analyse phase. Note that --postScript can be passed multiple times. - --script Add custom Javascript to run after the page has finished loading to collect metrics. If a single js file is specified, it will be included in the category named "custom" in the output json. Pass a folder to include all .js scripts in the folder, and have the folder name be the category. Note that --script can be passed multiple times. - --userAgent Override user agent - --appendToUserAgent Append a String to the user agent. Works in Chrome/Edge and Firefox. - -q, --silent Only output info in the logs, not to the console. Enter twice to suppress summary line. [count] - -o, --output Specify file name for Browsertime data (ex: 'browsertime'). Unless specified, file will be named browsertime.json - --har Specify file name for .har file (ex: 'browsertime'). Unless specified, file will be named browsertime.har - --skipHar Pass --skipHar to not collect a HAR file. [boolean] - --gzipHar Pass --gzipHar to gzip the HAR file [boolean] - --config Path to JSON config file. You can also use a .browsertime.json file that will automatically be found by Browsertime using find-up. - --viewPort Size of browser window WIDTHxHEIGHT or "maximize". Note that "maximize" is ignored for xvfb. - --resultDir Set result directory for the files produced by Browsertime - --useSameDir Store all files in the same structure and do not use the path structure released in 4.0. Use this only if you are testing ONE URL. - --xvfb Start xvfb before the browser is started [boolean] [default: false] - --xvfbParams.display The display used for xvfb [default: 99] - --tcpdump Collect a tcpdump for each tested URL. [boolean] [default: false] - --tcpdumpPacketBuffered Use together with --tcpdump to save each packet directly to the file, instead of buffering. [boolean] [default: false] - --urlAlias Use an alias for the URL. You need to pass on the same amount of alias as URLs. The alias is used as the name of the URL and used for filepath. Pass on multiple --urlAlias for multiple alias/URLs. You can also add alias direct in your script. [string] - --preURL, --warmLoad A URL that will be accessed first by the browser before the URL that you wanna analyze. Use it to fill the browser cache. - --preURLDelay, --warmLoadDealy Delay between preURL and the URL you want to test (in milliseconds) [default: 1500] - --userTimingWhitelist All userTimings are captured by default this option takes a regex that will whitelist which userTimings to capture in the results. - --headless Run the browser in headless mode. Works for Firefox and Chrome. [boolean] [default: false] - --gnirehtet Start gnirehtet and reverse tethering the traffic from your Android phone. [boolean] [default: false] - --flushDNS Flush DNS between runs, works on Mac OS and Linux. Your user needs sudo rights to be able to flush the DNS. [boolean] [default: false] - --extension Path to a WebExtension to be installed in the browser. Note that --extension can be passed multiple times. - --spa 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/ [boolean] [default: false] - --browserRestartTries If the browser fails to start, you can retry to start it this amount of times. [number] [default: 3] - --preWarmServer Do pre test requests to the URL(s) that you want to test that is not measured. Do that to make sure your web server is ready to serve. The pre test requests is done with another browser instance that is closed after pre testing is done. [boolean] [default: false] - --preWarmServerWaitTime The wait time before you start the real testing after your pre-cache request. [number] [default: 5000] - -h, --help Show help [boolean] - -V, --version Show version number [boolean] + --cpu Easy way to enable both chrome.timeline for Chrome and geckoProfile for Firefox [boolean] + --video Record a video and store the video. Set it to false to remove the video that is created by turning on visualMetrics. To remove fully turn off video recordings, make sure to set video and visualMetrics to false. Requires FFMpeg to be installed. [boolean] + --visualMetrics Collect Visual Metrics like First Visual Change, SpeedIndex, Perceptual Speed Index and Last Visual Change. Requires FFMpeg and Python dependencies [boolean] + --visualElements, --visuaElements Collect Visual Metrics from elements. Works only with --visualMetrics turned on. By default you will get visual metrics from the largest image within the view port and the largest h1. You can also configure to pickup your own defined elements with --scriptInput.visualElements [boolean] + --visualMetricsPerceptual Collect Perceptual Speed Index when you run --visualMetrics. [boolean] + --visualMetricsContentful Collect Contentful Speed Index when you run --visualMetrics. [boolean] + --visualMetricsPortable Use the portable visual-metrics processing script (no ImageMagick dependencies). [boolean] [default: true] + --scriptInput.visualElements Include specific elements in visual elements. Give the element a name and select it with document.body.querySelector. Use like this: --scriptInput.visualElements name:domSelector see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors. Add multiple instances to measure multiple elements. Visual Metrics will use these elements and calculate when they are visible and fully rendered. + --scriptInput.longTask, --minLongTaskLength Set the minimum length of a task to be categorised as a CPU Long Task. It can never be smaller than 50. The value is in ms and only works in Chromium browsers at the moment. [number] [default: 50] + -b, --browser Specify browser. Safari only works on OS X/iOS. Edge only work on OS that supports Edge. [choices: "chrome", "firefox", "edge", "safari"] [default: "chrome"] + --android Short key to use Android. Defaults to use com.android.chrome unless --browser is specified. [boolean] [default: false] + --processStartTime Capture browser process start time (in milliseconds). Android only for now. [boolean] [default: false] + --pageCompleteCheck 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 + --pageCompleteWaitTime 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. [default: 8000] + --pageCompleteCheckInactivity Alternative way to choose when to end your test. This will wait for 2 seconds of inactivity that happens after loadEventEnd. [boolean] [default: false] + --pageCompleteCheckPollTimeout The time in ms to wait for running the page complete check the next time. [number] [default: 1500] + --pageCompleteCheckStartWait 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 [number] [default: 5000] + --pageLoadStrategy Set the strategy to waiting for document readiness after a navigation event. After the strategy is ready, your pageCompleteCheck will start runninhg. [string] [choices: "eager", "none", "normal"] [default: "none"] + -n, --iterations Number of times to test the url (restarting the browser between each test) [number] [default: 3] + --prettyPrint Enable to print json/har with spaces and indentation. Larger files, but easier on the eye. [boolean] [default: false] + --delay Delay between runs, in milliseconds [number] [default: 0] + --timeToSettle 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 [number] [default: 0] + --webdriverPageload Use webdriver.get to initialize the page load instead of window.location. [boolean] [default: false] + -r, --requestheader Request header that will be added to the request. Add multiple instances to add multiple request headers. Works for Firefox and Chrome. Use the following format key:value + --cookie Cookie that will be added to the request. Add multiple instances to add multiple request cookies. Works for Firefox and Chrome. Use the following format cookieName=cookieValue + --injectJs Inject JavaScript into the current page at document_start. Works for Firefox and Chrome. More info: https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/contentScripts + --block Domain to block or URL or URL pattern to block. If you use Chrome you can also use --blockDomainsExcept (that is more performant). Works in Chrome/Edge. For Firefox you can only block domains. + --percentiles The percentile values within the data browsertime will calculate and report. This argument uses Yargs arrays and you you to set them correctly it is recommended to use a configuraration file instead. [array] [default: [0,10,90,99,100]] + --decimals The decimal points browsertime statistics round to. [number] [default: 0] + --iqr Use IQR, or Inter Quartile Range filtering filters data based on the spread of the data. See https://en.wikipedia.org/wiki/Interquartile_range. In some cases, IQR filtering may not filter out anything. This can happen if the acceptable range is wider than the bounds of your dataset. [boolean] [default: false] + --cacheClearRaw Use internal browser functionality to clear browser cache between runs instead of only using Selenium. [boolean] [default: false] + --basicAuth Use it if your server is behind Basic Auth. Format: username@password (Only Chrome and Firefox at the moment). + --preScript, --setUp Selenium script(s) to run before you test your URL/script. They will run outside of the analyse phase. Note that --preScript can be passed multiple times. + --postScript, --tearDown Selenium script(s) to run after you test your URL. They will run outside of the analyse phase. Note that --postScript can be passed multiple times. + --script Add custom Javascript to run after the page has finished loading to collect metrics. If a single js file is specified, it will be included in the category named "custom" in the output json. Pass a folder to include all .js scripts in the folder, and have the folder name be the category. Note that --script can be passed multiple times. + --userAgent Override user agent + --appendToUserAgent Append a String to the user agent. Works in Chrome/Edge and Firefox. + -q, --silent Only output info in the logs, not to the console. Enter twice to suppress summary line. [count] + -o, --output Specify file name for Browsertime data (ex: 'browsertime'). Unless specified, file will be named browsertime.json + --har Specify file name for .har file (ex: 'browsertime'). Unless specified, file will be named browsertime.har + --skipHar Pass --skipHar to not collect a HAR file. [boolean] + --gzipHar Pass --gzipHar to gzip the HAR file [boolean] + --config Path to JSON config file. You can also use a .browsertime.json file that will automatically be found by Browsertime using find-up. + --viewPort Size of browser window WIDTHxHEIGHT or "maximize". Note that "maximize" is ignored for xvfb. + --resultDir Set result directory for the files produced by Browsertime + --useSameDir Store all files in the same structure and do not use the path structure released in 4.0. Use this only if you are testing ONE URL. + --xvfb Start xvfb before the browser is started [boolean] [default: false] + --xvfbParams.display The display used for xvfb [default: 99] + --tcpdump Collect a tcpdump for each tested URL. [boolean] [default: false] + --tcpdumpPacketBuffered Use together with --tcpdump to save each packet directly to the file, instead of buffering. [boolean] [default: false] + --urlAlias Use an alias for the URL. You need to pass on the same amount of alias as URLs. The alias is used as the name of the URL and used for filepath. Pass on multiple --urlAlias for multiple alias/URLs. You can also add alias direct in your script. [string] + --preURL, --warmLoad A URL that will be accessed first by the browser before the URL that you wanna analyze. Use it to fill the browser cache. + --preURLDelay, --warmLoadDealy Delay between preURL and the URL you want to test (in milliseconds) [default: 1500] + --userTimingWhitelist All userTimings are captured by default this option takes a regex that will whitelist which userTimings to capture in the results. + --headless Run the browser in headless mode. Works for Firefox and Chrome. [boolean] [default: false] + --flushDNS Flush DNS between runs, works on Mac OS and Linux. Your user needs sudo rights to be able to flush the DNS. [boolean] [default: false] + --extension Path to a WebExtension to be installed in the browser. Note that --extension can be passed multiple times. + --spa 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/ [boolean] [default: false] + --cjs Load scripting files that ends with .js as common js. Default (false) loads files as esmodules. [boolean] [default: false] + --browserRestartTries If the browser fails to start, you can retry to start it this amount of times. [number] [default: 3] + --preWarmServer Do pre test requests to the URL(s) that you want to test that is not measured. Do that to make sure your web server is ready to serve. The pre test requests is done with another browser instance that is closed after pre testing is done. [boolean] [default: false] + --preWarmServerWaitTime The wait time before you start the real testing after your pre-cache request. [number] [default: 5000] + -h, --help Show help [boolean] + -V, --version Show version number [boolean] diff --git a/lib/cli/cli.js b/lib/cli/cli.js index 4653b65a3..23dbab562 100644 --- a/lib/cli/cli.js +++ b/lib/cli/cli.js @@ -1,29 +1,24 @@ -'use strict'; +import { resolve } from 'node:path'; +import { platform } from 'node:os'; +import { readFileSync, statSync, existsSync } from 'node:fs'; -const yargs = require('yargs'); -const path = require('path'); -const merge = require('lodash.merge'); -const reduce = require('lodash.reduce'); -const cliUtil = require('./util'); -const fs = require('fs'); -const set = require('lodash.set'); -const get = require('lodash.get'); -const findUp = require('find-up'); -const os = require('os'); -const toArray = require('../support/util').toArray; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import merge from 'lodash.merge'; +import reduce from 'lodash.reduce'; +import set from 'lodash.set'; +import get from 'lodash.get'; +import { findUpSync } from 'find-up'; -const grafanaPlugin = require('../plugins/grafana/index'); -const graphitePlugin = require('../plugins/graphite/index'); -const influxdbPlugin = require('../plugins/influxdb/index'); -const cruxPlugin = require('../plugins/crux/index'); -const matrixPlugin = require('../plugins/matrix/index'); +import { getURLs, getAliases } from './util.js'; +import { toArray } from '../support/util.js'; +import friendlynames from '../support/friendlynames.js'; +import { config as browsertimeConfig } from '../plugins/browsertime/index.js'; +import { config as metricsConfig } from '../plugins/metrics/index.js'; +import { config as slackConfig } from '../plugins/slack/index.js'; +import { config as htmlConfig } from '../plugins/html/index.js'; +import { messageTypes as matrixMessageTypes } from '../plugins/matrix/index.js'; -const browsertimeConfig = require('../plugins/browsertime/index').config; -const metricsConfig = require('../plugins/metrics/index').config; -const slackConfig = require('../plugins/slack/index').config; -const htmlConfig = require('../plugins/html/index').config; - -const friendlynames = require('../support/friendlynames'); const metricList = Object.keys(friendlynames); const configFiles = ['.sitespeed.io.json']; @@ -33,20 +28,20 @@ if (process.argv.includes('--config')) { configFiles.unshift(process.argv[index + 1]); } -const configPath = findUp.sync(configFiles); +const configPath = findUpSync(configFiles); let config; try { - config = configPath ? JSON.parse(fs.readFileSync(configPath)) : undefined; -} catch (e) { - if (e instanceof SyntaxError) { + config = configPath ? JSON.parse(readFileSync(configPath)) : undefined; +} catch (error) { + if (error instanceof SyntaxError) { console.error( 'Error: Could not parse the config JSON file ' + configPath + '. Is the file really valid JSON?' ); } - throw e; + throw error; } function validateInput(argv) { @@ -80,7 +75,7 @@ function validateInput(argv) { } if (argv.slug) { - const characters = /[^A-Za-z_\-0-9]/g; + const characters = /[^\w-]/g; if (characters.test(argv.slug)) { return 'The slug can only use characters A-Z a-z 0-9 and -_.'; } @@ -100,7 +95,7 @@ function validateInput(argv) { if ( argv.urlAlias && argv._ && - cliUtil.getURLs(argv._).length !== toArray(argv.urlAlias).length + getURLs(argv._).length !== toArray(argv.urlAlias).length ) { return 'Error: You have a miss match between number of alias and URLs.'; } @@ -108,21 +103,18 @@ function validateInput(argv) { if ( argv.groupAlias && argv._ && - cliUtil.getURLs(argv._).length !== toArray(argv.groupAlias).length + getURLs(argv._).length !== toArray(argv.groupAlias).length ) { return 'Error: You have a miss match between number of alias for groups and URLs.'; } if ( argv.browsertime.connectivity && - argv.browsertime.connectivity.engine === 'humble' + argv.browsertime.connectivity.engine === 'humble' && + (!argv.browsertime.connectivity.humble || + !argv.browsertime.connectivity.humble.url) ) { - if ( - !argv.browsertime.connectivity.humble || - !argv.browsertime.connectivity.humble.url - ) { - return 'You need to specify the URL to Humble by using the --browsertime.connectivity.humble.url option.'; - } + return 'You need to specify the URL to Humble by using the --browsertime.connectivity.humble.url option.'; } if ( @@ -159,8 +151,8 @@ function validateInput(argv) { if (!urlOrFile.startsWith('http')) { // is existing file? try { - fs.statSync(urlOrFile); - } catch (e) { + statSync(urlOrFile); + } catch { return ( 'Error: ' + urlOrFile + @@ -182,23 +174,23 @@ function validateInput(argv) { } for (let metric of toArray(argv.html.summaryBoxes)) { - if (htmlConfig.html.summaryBoxes.indexOf(metric) === -1) { + if (!htmlConfig.html.summaryBoxes.includes(metric)) { return `Error: ${metric} is not part of summary box metric.`; } } if (argv.html && argv.html.summaryBoxesThresholds) { try { - const box = fs.readFileSync( - path.resolve(argv.html.summaryBoxesThresholds), - { - encoding: 'utf8' - } - ); + const box = readFileSync(resolve(argv.html.summaryBoxesThresholds), { + encoding: 'utf8' + }); argv.html.summaryBoxesThresholds = JSON.parse(box); - } catch (e) { + } catch (error) { return ( - 'Error: Could not read ' + argv.html.summaryBoxesThresholds + ' ' + e + 'Error: Could not read ' + + argv.html.summaryBoxesThresholds + + ' ' + + error ); } } @@ -206,8 +198,9 @@ function validateInput(argv) { return true; } -module.exports.parseCommandLine = function parseCommandLine() { - let parsed = yargs +export async function parseCommandLine() { + let yargsInstance = yargs(hideBin(process.argv)); + let parsed = yargsInstance .parserConfiguration({ 'deep-merge-config': true }) .env('SITESPEED_IO') .usage('$0 [options] /') @@ -344,7 +337,7 @@ module.exports.parseCommandLine = function parseCommandLine() { }) .option('browsertime.timeouts.pageCompleteCheck', { alias: 'maxLoadTime', - default: 120000, + default: 120_000, type: 'number', describe: 'The max load time to wait for a page to finish loading (in milliseconds).', @@ -693,7 +686,7 @@ module.exports.parseCommandLine = function parseCommandLine() { .option('browsertime.firefox.geckoProfilerParams.bufferSize', { alias: 'firefox.geckoProfilerParams.bufferSize', describe: 'Buffer size in elements. Default is ~90MB.', - default: 1000000, + default: 1_000_000, type: 'number', group: 'Firefox' }) @@ -1142,14 +1135,203 @@ module.exports.parseCommandLine = function parseCommandLine() { describe: 'Remove the files locally when the files has been copied to the other server.', group: 'scp' + }) + + .option('grafana.host', { + describe: 'The Grafana host used when sending annotations.', + group: 'Grafana' + }) + + .option('grafana.port', { + default: 80, + describe: 'The Grafana port used when sending annotations to Grafana.', + group: 'Grafana' + }) + .option('grafana.auth', { + describe: + 'The Grafana auth/bearer value used when sending annotations to Grafana. If you do not set Bearer/Auth, Bearer is automatically set. See http://docs.grafana.org/http_api/auth/#authentication-api', + group: 'Grafana' + }) + .option('grafana.annotationTitle', { + describe: 'Add a title to the annotation sent for a run.', + group: 'Grafana' + }) + .option('grafana.annotationMessage', { + describe: + 'Add an extra message that will be attached to the annotation sent for a run. The message is attached after the default message and can contain HTML.', + group: 'Grafana' + }) + + .option('grafana.annotationTag', { + describe: + 'Add a extra tag to the annotation sent for a run. Repeat the --grafana.annotationTag option for multiple tags. Make sure they do not collide with the other tags.', + group: 'Grafana' + }) + .option('grafana.annotationScreenshot', { + default: false, + type: 'boolean', + describe: + 'Include screenshot (from Browsertime/WebPageTest) in the annotation. You need to specify a --resultBaseURL for this to work.', + group: 'Grafana' + }) + + .option('graphite.host', { + describe: 'The Graphite host used to store captured metrics.', + group: 'Graphite' + }) + .option('graphite.port', { + default: 2003, + describe: 'The Graphite port used to store captured metrics.', + group: 'Graphite' + }) + .option('graphite.auth', { + describe: + 'The Graphite user and password used for authentication. Format: user:password', + group: 'Graphite' + }) + .option('graphite.httpPort', { + describe: + 'The Graphite port used to access the user interface and send annotations event', + default: 8080, + group: 'Graphite' + }) + .option('graphite.webHost', { + describe: + 'The graphite-web host. If not specified graphite.host will be used.', + group: 'Graphite' + }) + .option('graphite.namespace', { + default: 'sitespeed_io.default', + describe: 'The namespace key added to all captured metrics.', + group: 'Graphite' + }) + .option('graphite.includeQueryParams', { + default: false, + describe: + 'Whether to include query parameters from the URL in the Graphite keys or not', + type: 'boolean', + group: 'Graphite' + }) + .option('graphite.arrayTags', { + default: true, + type: 'boolean', + describe: + 'Send the tags as Array or a String. In Graphite 1.0 the tags is a array. Before a String', + group: 'Graphite' + }) + .option('graphite.annotationTitle', { + describe: 'Add a title to the annotation sent for a run.', + group: 'Graphite' + }) + .option('graphite.annotationMessage', { + describe: + 'Add an extra message that will be attached to the annotation sent for a run. The message is attached after the default message and can contain HTML.', + group: 'Graphite' + }) + .option('graphite.annotationScreenshot', { + default: false, + type: 'boolean', + describe: + 'Include screenshot (from Browsertime/WebPageTest) in the annotation. You need to specify a --resultBaseURL for this to work.', + group: 'Graphite' + }) + .option('graphite.sendAnnotation', { + default: true, + type: 'boolean', + describe: + 'Send annotations when a run is finished. You need to specify a --resultBaseURL for this to work. However if you for example use a Prometheus exporter, you may want to make sure annotations are not sent, then set it to false.', + group: 'Graphite' + }) + .option('graphite.annotationRetentionMinutes', { + type: 'number', + describe: + 'The retention in minutes, to make annotation match the retention in Graphite.', + group: 'Graphite' + }) + .option('graphite.statsd', { + default: false, + type: 'boolean', + describe: 'Uses the StatsD interface', + group: 'Graphite' + }) + .option('graphite.annotationTag', { + describe: + 'Add a extra tag to the annotation sent for a run. Repeat the --graphite.annotationTag option for multiple tags. Make sure they do not collide with the other tags.', + group: 'Graphite' + }) + .option('graphite.addSlugToKey', { + default: true, + type: 'boolean', + describe: + 'Add the slug (name of the test) as an extra key in the namespace.', + group: 'Graphite' + }) + .option('graphite.bulkSize', { + default: undefined, + type: 'number', + describe: 'Break up number of metrics to send with each request.', + group: 'Graphite' + }) + .option('graphite.messages', { + default: ['pageSummary', 'summary'], + options: ['pageSummary', 'summary', 'run'], + group: 'Graphite' + }) + + .option('influxdb.protocol', { + describe: 'The protocol used to store connect to the InfluxDB host.', + default: 'http', + group: 'InfluxDB' + }) + .option('influxdb.host', { + describe: 'The InfluxDB host used to store captured metrics.', + group: 'InfluxDB' + }) + .option('influxdb.port', { + default: 8086, + describe: 'The InfluxDB port used to store captured metrics.', + group: 'InfluxDB' + }) + .option('influxdb.username', { + describe: 'The InfluxDB username for your InfluxDB instance.', + group: 'InfluxDB' + }) + .option('influxdb.password', { + describe: 'The InfluxDB password for your InfluxDB instance.', + group: 'InfluxDB' + }) + .option('influxdb.database', { + default: 'sitespeed', + describe: 'The database name used to store captured metrics.', + group: 'InfluxDB' + }) + .option('influxdb.tags', { + default: 'category=default', + describe: + 'A comma separated list of tags and values added to each metric', + group: 'InfluxDB' + }) + .option('influxdb.includeQueryParams', { + default: false, + describe: + 'Whether to include query parameters from the URL in the InfluxDB keys or not', + type: 'boolean', + group: 'InfluxDB' + }) + .option('influxdb.groupSeparator', { + default: '_', + describe: + 'Choose which character that will separate a group/domain. Default is underscore, set it to a dot if you wanna keep the original domain name.', + group: 'InfluxDB' + }) + .option('influxdb.annotationScreenshot', { + default: false, + type: 'boolean', + describe: + 'Include screenshot (from Browsertime) in the annotation. You need to specify a --resultBaseURL for this to work.', + group: 'InfluxDB' }); - // Grafana CLI options - cliUtil.registerPluginOptions(parsed, grafanaPlugin); - - // Graphite CLI options - cliUtil.registerPluginOptions(parsed, graphitePlugin); - parsed /** Plugins */ .option('plugins.list', { @@ -1244,7 +1426,6 @@ module.exports.parseCommandLine = function parseCommandLine() { /** InfluxDB cli option */ - cliUtil.registerPluginOptions(parsed, influxdbPlugin); parsed // Metrics @@ -1267,6 +1448,36 @@ module.exports.parseCommandLine = function parseCommandLine() { 'Add/change/remove filters for metrics. If you want to send all metrics, use: *+ . If you want to remove all current metrics and send only the coach score: *- coach.summary.score.*', group: 'Metrics' }) + + .option('matrix.host', { + describe: 'The Matrix host.', + group: 'Matrix' + }) + .option('matrix.accessToken', { + describe: 'The Matrix access token.', + group: 'Matrix' + }) + .option('matrix.room', { + describe: + 'The default Matrix room. It is alsways used. You can override the room per message type using --matrix.rooms', + group: 'Matrix' + }) + .option('matrix.messages', { + describe: + 'Choose what type of message to send to Matrix. There are two types of messages: Error messages and budget messages. Errors are errors that happens through the tests (failures like strarting a test) and budget is test failing against your budget.', + choices: matrixMessageTypes(), + default: matrixMessageTypes(), + group: 'Matrix' + }) + + .option('matrix.rooms', { + describe: + 'Send messages to different rooms. Current message types are [' + + matrixMessageTypes + + ']. If you want to send error messages to a specific room use --matrix.rooms.error ROOM', + group: 'Matrix' + }) + /** Slack options */ @@ -1411,6 +1622,34 @@ module.exports.parseCommandLine = function parseCommandLine() { type: 'boolean', group: 'GoogleCloudStorage' }) + + .option('crux.key', { + describe: + 'You need to use a key to get data from CrUx. Get the key from https://developers.google.com/web/tools/chrome-user-experience-report/api/guides/getting-started#APIKey', + group: 'CrUx' + }) + .option('crux.enable', { + default: true, + describe: + 'Enable the CrUx plugin. This is on by defauly but you also need the Crux key. If you chose to disable it with this key, set this to false and you can still use the CrUx key in your configuration.', + group: 'CrUx' + }) + .option('crux.formFactor', { + default: 'ALL', + type: 'string', + choices: ['ALL', 'DESKTOP', 'PHONE', 'TABLET'], + describe: + 'A form factor is the type of device on which a user visits a website.', + group: 'CrUx' + }) + .option('crux.collect', { + default: 'ALL', + type: 'string', + choices: ['ALL', 'URL', 'ORIGIN'], + describe: + 'Choose what data to collect. URL is data for a specific URL, ORIGIN for the domain and ALL for both of them', + group: 'CrUx' + }) /** Html options */ @@ -1517,8 +1756,6 @@ module.exports.parseCommandLine = function parseCommandLine() { 'Instead of using the local copy of the hosting database, you can use the latest version through the Green Web Foundation API. This means sitespeed.io will make HTTP GET to the the hosting info.', group: 'Sustainable' }); - cliUtil.registerPluginOptions(parsed, cruxPlugin); - cliUtil.registerPluginOptions(parsed, matrixPlugin); parsed .option('mobile', { describe: @@ -1600,17 +1837,19 @@ module.exports.parseCommandLine = function parseCommandLine() { .alias('help', 'h') .config(config) .alias('version', 'V') - .coerce('budget', function (arg) { - if (arg) { - if (typeof arg === 'object' && !Array.isArray(arg)) { - if (arg.configPath) { - arg.config = JSON.parse(fs.readFileSync(arg.configPath, 'utf8')); - } else if (arg.config) { - arg.config = JSON.parse(arg.config); + .coerce('budget', function (argument) { + if (argument) { + if (typeof argument === 'object' && !Array.isArray(argument)) { + if (argument.configPath) { + argument.config = JSON.parse( + readFileSync(argument.configPath, 'utf8') + ); + } else if (argument.config) { + argument.config = JSON.parse(argument.config); } - return arg; + return argument; } else { - throw new Error( + throw new TypeError( '[ERROR] Something looks wrong with your budget configuration. Since sitespeed.io 4.4 you should pass the path to your budget file through the --budget.configPath flag instead of directly through the --budget flag.' ); } @@ -1646,30 +1885,30 @@ module.exports.parseCommandLine = function parseCommandLine() { return plugins; } }) - .coerce('webpagetest', function (arg) { - if (arg) { + .coerce('webpagetest', function (argument) { + if (argument) { // for backwards compatible reasons we check if the passed parameters is a path to a script, if so just us it (PR #1445) - if (arg.script && fs.existsSync(arg.script)) { - arg.script = fs.readFileSync(path.resolve(arg.script), 'utf8'); + if (argument.script && existsSync(argument.script)) { + argument.script = readFileSync(resolve(argument.script), 'utf8'); /* eslint no-console: off */ console.log( '[WARNING] Since sitespeed.io 4.4 you should pass the path to the script file through the --webpagetest.file flag (https://github.com/sitespeedio/sitespeed.io/pull/1445).' ); - return arg; + return argument; } - if (arg.file) { - arg.script = fs.readFileSync(path.resolve(arg.file), 'utf8'); - } else if (arg.script) { + if (argument.file) { + argument.script = readFileSync(resolve(argument.file), 'utf8'); + } else if (argument.script) { // because the escaped characters are passed re-escaped from the console - arg.script = arg.script.split('\\t').join('\t'); - arg.script = arg.script.split('\\n').join('\n'); + argument.script = argument.script.split('\\t').join('\t'); + argument.script = argument.script.split('\\n').join('\n'); } - return arg; + return argument; } }) // .describe('browser', 'Specify browser') - .wrap(yargs.terminalWidth()) + .wrap(yargsInstance.terminalWidth()) // .check(validateInput) .epilog( 'Read the docs at https://www.sitespeed.io/documentation/sitespeed.io/' @@ -1693,8 +1932,12 @@ module.exports.parseCommandLine = function parseCommandLine() { new Map() ); - let explicitOptions = require('yargs/yargs')(process.argv.slice(2)).argv; - explicitOptions = merge(explicitOptions, yargs.getOptions().configObjects[0]); + let explicitOptions = yargs(hideBin(process.argv)).argv; + + explicitOptions = merge( + explicitOptions, + yargsInstance.getOptions().configObjects[0] + ); explicitOptions = reduce( explicitOptions, @@ -1710,17 +1953,14 @@ module.exports.parseCommandLine = function parseCommandLine() { ); if (argv.config) { - const config = require(path.resolve(process.cwd(), argv.config)); + const config = await import(resolve(process.cwd(), argv.config)); explicitOptions = merge(explicitOptions, config); } if (argv.webpagetest && argv.webpagetest.custom) { - argv.webpagetest.custom = fs.readFileSync( - path.resolve(argv.webpagetest.custom), - { - encoding: 'utf8' - } - ); + argv.webpagetest.custom = readFileSync(resolve(argv.webpagetest.custom), { + encoding: 'utf8' + }); } if (argv.summaryDetail) argv.summary = true; @@ -1750,15 +1990,13 @@ module.exports.parseCommandLine = function parseCommandLine() { if (argv.ios) { set(argv, 'safari.ios', true); - } else if (argv.android) { - if (argv.browser === 'chrome') { - // Default to Chrome Android. - set( - argv, - 'browsertime.chrome.android.package', - get(argv, 'browsertime.chrome.android.package', 'com.android.chrome') - ); - } + } else if (argv.android && argv.browser === 'chrome') { + // Default to Chrome Android. + set( + argv, + 'browsertime.chrome.android.package', + get(argv, 'browsertime.chrome.android.package', 'com.android.chrome') + ); } // Always use hash by default when you configure spa @@ -1767,20 +2005,19 @@ module.exports.parseCommandLine = function parseCommandLine() { set(argv, 'browsertime.useHash', true); } - if (argv.cpu) { - if ( - argv.browsertime.browser === 'chrome' || - argv.browsertime.browser === 'edge' - ) { - set(argv, 'browsertime.chrome.collectLongTasks', true); - set(argv, 'browsertime.chrome.timeline', true); - } - // Enable when we know how much overhead it will add - /* + if ( + argv.cpu && + (argv.browsertime.browser === 'chrome' || + argv.browsertime.browser === 'edge') + ) { + set(argv, 'browsertime.chrome.collectLongTasks', true); + set(argv, 'browsertime.chrome.timeline', true); + } + // Enable when we know how much overhead it will add + /* else if (argv.browsertime.browser === 'firefox') { set(argv, 'browsertime.firefox.geckoProfiler', true); }*/ - } // we missed to populate this to Browsertime in the cli // so to stay backward compatible we do it manually @@ -1800,7 +2037,7 @@ module.exports.parseCommandLine = function parseCommandLine() { if (argv.browsertime.safari && argv.browsertime.safari.useSimulator) { set(argv, 'browsertime.connectivity.engine', 'throttle'); } else if ( - (os.platform() === 'darwin' || os.platform() === 'linux') && + (platform() === 'darwin' || platform() === 'linux') && !argv.browsertime.android && !argv.browsertime.safari.ios && !argv.browsertime.docker && @@ -1823,7 +2060,7 @@ module.exports.parseCommandLine = function parseCommandLine() { ); } - let urlsMetaData = cliUtil.getAliases(argv._, argv.urlAlias, argv.groupAlias); + let urlsMetaData = getAliases(argv._, argv.urlAlias, argv.groupAlias); // Copy the alias so it is also used by Browsertime if (argv.urlAlias) { // Browsertime has it own way of handling alias @@ -1833,8 +2070,8 @@ module.exports.parseCommandLine = function parseCommandLine() { if (!Array.isArray(argv.urlAlias)) argv.urlAlias = [argv.urlAlias]; - for (let i = 0; i < urls.length; i++) { - meta[urls[i]] = argv.urlAlias[i]; + for (const [index, url] of urls.entries()) { + meta[url] = argv.urlAlias[index]; } set(argv, 'browsertime.urlMetaData', meta); } else if (Object.keys(urlsMetaData).length > 0) { @@ -1847,15 +2084,15 @@ module.exports.parseCommandLine = function parseCommandLine() { // Set the timeouts to a maximum while debugging if (argv.debug) { - set(argv, 'browsertime.timeouts.pageload', 2147483647); - set(argv, 'browsertime.timeouts.script', 2147483647); - set(argv, 'browsertime.timeouts.pageCompleteCheck', 2147483647); + set(argv, 'browsertime.timeouts.pageload', 2_147_483_647); + set(argv, 'browsertime.timeouts.script', 2_147_483_647); + set(argv, 'browsertime.timeouts.pageCompleteCheck', 2_147_483_647); } return { - urls: argv.multi ? argv._ : cliUtil.getURLs(argv._), + urls: argv.multi ? argv._ : getURLs(argv._), urlsMetaData, options: argv, explicitOptions: explicitOptions }; -}; +} diff --git a/lib/cli/util.js b/lib/cli/util.js index 96de92b7b..5ef0e78a2 100644 --- a/lib/cli/util.js +++ b/lib/cli/util.js @@ -1,11 +1,10 @@ -'use strict'; - /*eslint no-console: 0*/ -const fs = require('fs'); -const path = require('path'); -const toArray = require('../support/util').toArray; -const format = require('util').format; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { format } from 'node:util'; + +import { toArray } from '../support/util.js'; /** * @@ -18,7 +17,7 @@ function sanitizePluginOptions(options) { const isValidType = typeof cliOptions === 'object' && - cliOptions !== null && + cliOptions !== undefined && cliOptions.constructor === Object; if (!isValidType) { @@ -29,130 +28,112 @@ function sanitizePluginOptions(options) { return cliOptions; } -module.exports = { - getURLs(urls) { - const allUrls = []; - urls = urls.map(url => url.trim()); +export function getURLs(urls) { + const allUrls = []; + urls = urls.map(url => url.trim()); - for (let url of urls) { - if (url.startsWith('http')) { - allUrls.push(url); - } else { - const filePath = path.resolve(url); - try { - const lines = fs.readFileSync(filePath).toString().split('\n'); - for (let line of lines) { - if (line.trim().length > 0) { - let lineArray = line.split(' ', 2); - let url = lineArray[0].trim(); - if (url) { - if (url.startsWith('http')) { - allUrls.push(url); - } else if (url.startsWith('module.exports')) { - // This looks like someone is trying to run a script without adding the --multi parameter - // For now just write to the log and in the future we can maybe automatically fix it - console.error( - 'Please use --multi if you want to run scripts. See https://www.sitespeed.io/documentation/sitespeed.io/scripting/#run' - ); - } else { - // We use skip adding it - } + for (let url of urls) { + if (url.startsWith('http')) { + allUrls.push(url); + } else { + const filePath = resolve(url); + try { + const lines = readFileSync(filePath).toString().split('\n'); + for (let line of lines) { + if (line.trim().length > 0) { + let lineArray = line.split(' ', 2); + let url = lineArray[0].trim(); + if (url) { + if (url.startsWith('http')) { + allUrls.push(url); + } else if (url.startsWith('module.exports')) { + // This looks like someone is trying to run a script without adding the --multi parameter + // For now just write to the log and in the future we can maybe automatically fix it + console.error( + 'Please use --multi if you want to run scripts. See https://www.sitespeed.io/documentation/sitespeed.io/scripting/#run' + ); + } else { + // do nada } } } - } catch (e) { - if (e.code === 'ENOENT') { - throw new Error(`Couldn't find url file at ${filePath}`); - } - throw e; } + } catch (error) { + if (error.code === 'ENOENT') { + throw new Error(`Couldn't find url file at ${filePath}`); + } + throw error; } } - return allUrls; - }, - getAliases(urls, alias, groupAlias) { - const urlMetaData = {}; - urls = urls.map(url => url.trim()); - let al = toArray(alias); - let allGroupAlias = toArray(groupAlias); - let pos = 0; - - for (let url of urls) { - if (url.startsWith('http')) { - if (al.length > 0 && al[pos]) { - urlMetaData[url] = { urlAlias: al[pos] }; - } - if (allGroupAlias.length > 0 && allGroupAlias[pos]) { - urlMetaData[url] = { groupAlias: allGroupAlias[pos] }; - } - pos += 1; - } else { - const filePath = url; - const lines = fs.readFileSync(filePath).toString().split('\n'); - for (let line of lines) { - if (line.trim().length > 0) { - let url, - alias, - groupAlias = null; - let lineArray = line.split(' ', 3); - url = lineArray[0].trim(); - if (lineArray[1]) { - alias = lineArray[1].trim(); - } - if (lineArray[2]) { - groupAlias = lineArray[2].trim(); - } - if (url && alias) { - urlMetaData[url] = { urlAlias: alias }; - } - if (url && groupAlias) { - urlMetaData[url].groupAlias = groupAlias; - } - } - } - } - } - return urlMetaData; - }, - - /** - * Retrieve a mapping of option names and their corressponding default values based on a plugin CLI configuration - * - * @param {Object} cliOptions a map of option names: yargs option - */ - pluginDefaults(cliOptions) { - let config = {}; - try { - Object.entries(sanitizePluginOptions(cliOptions)).forEach(values => { - const [key, options] = values; - if (typeof options.default !== 'undefined') { - config[key] = options.default; - } - }); - } catch (err) { - // In case of invalid values, just assume an empty object. - config = {}; - } - return config; - }, - - /** - * Configure yargs options for a given plugin defining a `cliOptions` method or property. The names - * of configuration options are namespaced with the plugin name. - * - * @param {require('yargs').Argv} parsed yargs instance - * @param {object} plugin a sitespeed plugin instance - */ - registerPluginOptions(parsed, plugin) { - if (typeof plugin.name !== 'function' || !plugin.name()) { - throw new Error( - 'Missing name() method for plugin registering CLI options' - ); - } - const cliOptions = sanitizePluginOptions(plugin.cliOptions); - Object.entries(cliOptions).forEach(value => { - const [key, yargsOptions] = value; - parsed.option(`${plugin.name()}.${key}`, yargsOptions); - }); } -}; + return allUrls; +} +export function getAliases(urls, alias, groupAlias) { + const urlMetaData = {}; + urls = urls.map(url => url.trim()); + let al = toArray(alias); + let allGroupAlias = toArray(groupAlias); + let pos = 0; + + for (let url of urls) { + if (url.startsWith('http')) { + if (al.length > 0 && al[pos]) { + urlMetaData[url] = { urlAlias: al[pos] }; + } + if (allGroupAlias.length > 0 && allGroupAlias[pos]) { + urlMetaData[url] = { groupAlias: allGroupAlias[pos] }; + } + pos += 1; + } else { + const filePath = url; + const lines = readFileSync(filePath).toString().split('\n'); + for (let line of lines) { + if (line.trim().length > 0) { + let url, alias, groupAlias; + let lineArray = line.split(' ', 3); + url = lineArray[0].trim(); + if (lineArray[1]) { + alias = lineArray[1].trim(); + } + if (lineArray[2]) { + groupAlias = lineArray[2].trim(); + } + if (url && alias) { + urlMetaData[url] = { urlAlias: alias }; + } + if (url && groupAlias) { + urlMetaData[url].groupAlias = groupAlias; + } + } + } + } + } + return urlMetaData; +} +export function pluginDefaults(cliOptions) { + let config = {}; + try { + for (const values of Object.entries(sanitizePluginOptions(cliOptions))) { + const [key, options] = values; + if (typeof options.default !== 'undefined') { + config[key] = options.default; + } + } + } catch { + // In case of invalid values, just assume an empty object. + config = {}; + } + return config; +} +export function registerPluginOptions(parsed, plugin) { + if (typeof plugin.name !== 'function' || !plugin.getName()) { + throw new Error( + 'Missing getName() method for plugin registering CLI options' + ); + } + const cliOptions = sanitizePluginOptions(plugin.cliOptions); + for (const value of Object.entries(cliOptions)) { + const [key, yargsOptions] = value; + parsed.option(`${plugin.getName()}.${key}`, yargsOptions); + } +} diff --git a/lib/core/logging.js b/lib/core/logging.js index 4b3449e02..2439dfb28 100644 --- a/lib/core/logging.js +++ b/lib/core/logging.js @@ -1,51 +1,65 @@ -'use strict'; +import intel from 'intel'; -let log = require('intel'); +const { + INFO, + DEBUG, + VERBOSE, + TRACE, + NONE, + basicConfig, + addHandler, + handlers, + Formatter +} = intel; -module.exports.configure = function configure(options, logDir) { +export function configure(options, logDir) { options = options || {}; - let level = log.INFO; + let level = INFO; switch (options.verbose) { - case 1: - level = log.DEBUG; + case 1: { + level = DEBUG; break; - case 2: - level = log.VERBOSE; + } + case 2: { + level = VERBOSE; break; - case 3: - level = log.TRACE; + } + case 3: { + level = TRACE; break; - default: + } + default: { break; + } } if (options.silent) { - level = log.NONE; + level = NONE; } - if (level === log.INFO) { - log.basicConfig({ + if (level === INFO) { + basicConfig({ format: '[%(date)s] %(levelname)s: %(message)s', level: level }); } else { - log.basicConfig({ + basicConfig({ format: '[%(date)s] %(levelname)s: [%(name)s] %(message)s', level: level }); } if (options.logToFile) { - log.addHandler( - new log.handlers.File({ + addHandler( + new handlers.File({ file: logDir + '/sitespeed.io.log', - formatter: new log.Formatter({ + formatter: new Formatter({ format: '[%(date)s] %(levelname)s: [%(name)s] %(message)s', level: level }) }) ); } -}; +} diff --git a/lib/core/pluginLoader.js b/lib/core/pluginLoader.js index d9a747cad..b72f4c245 100644 --- a/lib/core/pluginLoader.js +++ b/lib/core/pluginLoader.js @@ -1,9 +1,11 @@ -'use strict'; +import { join, basename, resolve, dirname } from 'node:path'; +import { readdir as _readdir } from 'node:fs'; +import { promisify } from 'node:util'; -const path = require('path'); -const fs = require('fs'); -const { promisify } = require('util'); -const readdir = promisify(fs.readdir); +import pkg from 'import-global'; +const { silent } = pkg; +const readdir = promisify(_readdir); +const __dirname = dirname(import.meta.url); const defaultPlugins = new Set([ 'browsertime', @@ -22,60 +24,72 @@ const defaultPlugins = new Set([ 'remove' ]); -const pluginsDir = path.join(__dirname, '..', 'plugins'); +const pluginsDir = join(__dirname, '..', 'plugins'); -module.exports = { - async parsePluginNames(options) { - // There's a problem with Safari on iOS runninhg a big blob - // of JavaScript - // https://github.com/sitespeedio/browsertime/issues/1275 - if (options.safari && options.safari.ios) { - defaultPlugins.delete('coach'); +export async function parsePluginNames(options) { + // There's a problem with Safari on iOS runninhg a big blob + // of JavaScript + // https://github.com/sitespeedio/browsertime/issues/1275 + if (options.safari && options.safari.ios) { + defaultPlugins.delete('coach'); + } + + // if we don't use the cli, this will work out fine as long + // we configure only what we need + const possibleConfiguredPlugins = options.explicitOptions || options; + const isDefaultOrConfigured = name => + defaultPlugins.has(name) || + typeof possibleConfiguredPlugins[name] === 'object'; + + const addMessageLoggerIfDebug = pluginNames => { + if (options.debugMessages) { + // Need to make sure logger is first, so message logs appear + // before messages are handled by other plugins + pluginNames = ['messagelogger'].concat(pluginNames); } + return pluginNames; + }; - // if we don't use the cli, this will work out fine as long - // we configure only what we need - const possibleConfiguredPlugins = options.explicitOptions || options; - const isDefaultOrConfigured = name => - defaultPlugins.has(name) || - typeof possibleConfiguredPlugins[name] === 'object'; - const addMessageLoggerIfDebug = pluginNames => { - if (options.debugMessages) { - // Need to make sure logger is first, so message logs appear - // before messages are handled by other plugins - pluginNames = ['messagelogger'].concat(pluginNames); - } - return pluginNames; - }; + const files = await readdir(new URL(pluginsDir)); - const files = await readdir(pluginsDir); - const builtins = files.map(name => path.basename(name, '.js')); - const plugins = builtins.filter(isDefaultOrConfigured); - return addMessageLoggerIfDebug(plugins); - }, - async loadPlugins(pluginNames) { - const plugins = []; - for (let name of pluginNames) { + const builtins = files.map(name => basename(name, '.js')); + // eslint-disable-next-line unicorn/no-array-callback-reference + const plugins = builtins.filter(isDefaultOrConfigured); + return addMessageLoggerIfDebug(plugins); +} +export async function loadPlugins(pluginNames, options, context, queue) { + const plugins = []; + for (let name of pluginNames) { + try { + let { default: plugin } = await import( + join(pluginsDir, name, 'index.js') + ); + let p = new plugin(options, context, queue); + plugins.push(p); + } catch (error_) { try { - const plugin = require(path.join(pluginsDir, name)); - if (!plugin.name) { - plugin.name = () => name; - } - plugins.push(plugin); - } catch (err) { + let { default: plugin } = await import(resolve(process.cwd(), name)); + let p = new plugin(options, context, queue); + plugins.push(p); + } catch { try { - plugins.push(require(path.resolve(process.cwd(), name))); + let { default: plugin } = await import(name); + let p = new plugin(options, context, queue); + plugins.push(p); } catch (error) { - try { - plugins.push(require(name)); - } catch (error) { - console.error("Couldn't load plugin %s: %s", name, err); // eslint-disable-line no-console + // try global + let plugin = silent(name); + if (plugin) { + let p = new plugin(options, context, queue); + plugins.push(p); + } else { + console.error("Couldn't load plugin %s: %s", name, error_); // eslint-disable-line no-console // if it fails here, let it fail hard throw error; } } } } - return plugins; } -}; + return plugins; +} diff --git a/lib/core/queueHandler.js b/lib/core/queueHandler.js index c3682f47f..8af2cfa38 100644 --- a/lib/core/queueHandler.js +++ b/lib/core/queueHandler.js @@ -1,13 +1,17 @@ -'use strict'; - /* eslint no-console:0 */ -const cq = require('concurrent-queue'); -const log = require('intel').getLogger('sitespeedio.queuehandler'); -const messageMaker = require('../support/messageMaker'); -const queueStats = require('./queueStatistics'); +import cq from 'concurrent-queue'; +import intel from 'intel'; + +import { messageMaker } from '../support/messageMaker.js'; +import { + registerQueueTime, + registerProcessingTime, + generateStatistics +} from './queueStatistics.js'; const make = messageMaker('queueHandler').make; +const log = intel.getLogger('sitespeedio.queuehandler'); function shortenData(key, value) { if (key === 'data') { @@ -16,6 +20,60 @@ function shortenData(key, value) { return value; } +function validatePageSummary(message) { + const type = message.type; + if (!type.endsWith('.pageSummary')) return; + + if (!message.url) + throw new Error(`Page summary message (${type}) didn't specify a url`); + + if (!message.group) + throw new Error(`Page summary message (${type}) didn't specify a group.`); +} + +function validateTypeStructure(message) { + const typeParts = message.type.split('.'), + baseType = typeParts[0], + typeDepth = typeParts.length; + + if (typeDepth > 2) + throw new Error( + 'Message type has too many dot separated sections: ' + message.type + ); + + const previousDepth = messageTypeDepths[baseType]; + + if (previousDepth && previousDepth !== typeDepth) { + throw new Error( + `All messages of type ${baseType} must have the same structure. ` + + `${message.type} has ${typeDepth} part(s), but earlier messages had ${previousDepth} part(s).` + ); + } + + messageTypeDepths[baseType] = typeDepth; +} + +function validateSummaryMessage(message) { + const type = message.type; + if (!type.endsWith('.summary')) return; + + if (message.url) + throw new Error( + `Summary message (${type}) shouldn't be url specific, use .pageSummary instead.` + ); + + if (!message.group) + throw new Error(`Summary message (${type}) didn't specify a group.`); + + const groups = groupsPerSummaryType[type] || []; + if (groups.includes(message.group)) { + throw new Error( + `Multiple summary messages of type ${type} and group ${message.group}` + ); + } + groupsPerSummaryType[type] = groups.concat(message.group); +} + const messageTypeDepths = {}; const groupsPerSummaryType = {}; @@ -25,70 +83,18 @@ const groupsPerSummaryType = {}; * @param message the message to check */ function validateMessageFormat(message) { - function validateTypeStructure(message) { - const typeParts = message.type.split('.'), - baseType = typeParts[0], - typeDepth = typeParts.length; - - if (typeDepth > 2) - throw new Error( - 'Message type has too many dot separated sections: ' + message.type - ); - - const previousDepth = messageTypeDepths[baseType]; - - if (previousDepth && previousDepth !== typeDepth) { - throw new Error( - `All messages of type ${baseType} must have the same structure. ` + - `${message.type} has ${typeDepth} part(s), but earlier messages had ${previousDepth} part(s).` - ); - } - - messageTypeDepths[baseType] = typeDepth; - } - - function validatePageSummary(message) { - const type = message.type; - if (!type.endsWith('.pageSummary')) return; - - if (!message.url) - throw new Error(`Page summary message (${type}) didn't specify a url`); - - if (!message.group) - throw new Error(`Page summary message (${type}) didn't specify a group.`); - } - - function validateSummaryMessage(message) { - const type = message.type; - if (!type.endsWith('.summary')) return; - - if (message.url) - throw new Error( - `Summary message (${type}) shouldn't be url specific, use .pageSummary instead.` - ); - - if (!message.group) - throw new Error(`Summary message (${type}) didn't specify a group.`); - - const groups = groupsPerSummaryType[type] || []; - if (groups.includes(message.group)) { - throw new Error( - `Multiple summary messages of type ${type} and group ${message.group}` - ); - } - groupsPerSummaryType[type] = groups.concat(message.group); - } - validateTypeStructure(message); validatePageSummary(message); validateSummaryMessage(message); } -class QueueHandler { - constructor(plugins, options) { +export class QueueHandler { + constructor(options) { this.options = options; this.errors = []; + } + setup(plugins) { this.createQueues(plugins); } @@ -96,7 +102,7 @@ class QueueHandler { this.queues = plugins .filter(plugin => plugin.processMessage) .map(plugin => { - const concurrency = plugin.concurrency || Infinity; + const concurrency = plugin.concurrency || Number.POSITIVE_INFINITY; const queue = cq().limit({ concurrency }); queue.plugin = plugin; @@ -104,42 +110,42 @@ class QueueHandler { const messageWaitingStart = {}, messageProcessingStart = {}; - queue.enqueued(obj => { - const message = obj.item; + queue.enqueued(object => { + const message = object.item; messageWaitingStart[message.uuid] = process.hrtime(); }); - queue.processingStarted(obj => { - const message = obj.item; + queue.processingStarted(object => { + const message = object.item; const waitingDuration = process.hrtime( messageWaitingStart[message.uuid] ), waitingNanos = waitingDuration[0] * 1e9 + waitingDuration[1]; - queueStats.registerQueueTime(message, queue.plugin, waitingNanos); + registerQueueTime(message, queue.plugin, waitingNanos); messageProcessingStart[message.uuid] = process.hrtime(); }); // FIXME handle rejections (i.e. failures while processing messages) properly - queue.processingEnded(obj => { - const message = obj.item; - const err = obj.err; - if (err) { + queue.processingEnded(object => { + const message = object.item; + const error = object.err; + if (error) { let rejectionMessage = 'Rejected ' + JSON.stringify(message, shortenData, 2) + ' for plugin: ' + - plugin.name(); + plugin.getName(); if (message && message.url) rejectionMessage += ', url: ' + message.url; - if (err.stack) { - log.error(err.stack); + if (error.stack) { + log.error(error.stack); } - this.errors.push(rejectionMessage + '\n' + JSON.stringify(err)); + this.errors.push(rejectionMessage + '\n' + JSON.stringify(error)); } const processingDuration = process.hrtime( @@ -148,11 +154,7 @@ class QueueHandler { const processingNanos = processingDuration[0] * 1e9 + processingDuration[1]; - queueStats.registerProcessingTime( - message, - queue.plugin, - processingNanos - ); + registerProcessingTime(message, queue.plugin, processingNanos); }); return { plugin, queue }; @@ -172,7 +174,7 @@ class QueueHandler { .then(() => this.drainAllQueues()) .then(async () => { for (let source of sources) { - await source.findUrls(this); + await source.findUrls(this, this.options); } }) .then(() => this.drainAllQueues()) @@ -184,7 +186,7 @@ class QueueHandler { .then(() => this.drainAllQueues()) .then(() => { if (this.options.queueStats) { - log.info(JSON.stringify(queueStats.generateStatistics(), null, 2)); + log.info(JSON.stringify(generateStatistics(), undefined, 2)); } return this.errors; }); @@ -218,15 +220,12 @@ class QueueHandler { async drainAllQueues() { const queues = this.queues; return new Promise(resolve => { - queues.forEach(item => + for (const item of queues) item.queue.drained(() => { if (queues.every(item => item.queue.isDrained)) { resolve(); } - }) - ); + }); }); } } - -module.exports = QueueHandler; diff --git a/lib/core/queueStatistics.js b/lib/core/queueStatistics.js index 9884a5c3c..347c11f19 100644 --- a/lib/core/queueStatistics.js +++ b/lib/core/queueStatistics.js @@ -1,8 +1,7 @@ -'use strict'; +import get from 'lodash.get'; +import set from 'lodash.set'; -const stats = require('../support/statsHelpers'), - get = require('lodash.get'), - set = require('lodash.set'); +import { pushStats, summarizeStats } from '../support/statsHelpers.js'; const queueTimeByPluginName = {}, queueTimeByMessageType = {}, @@ -11,80 +10,58 @@ const queueTimeByPluginName = {}, messageTypes = new Set(), pluginNames = new Set(); -module.exports = { - registerQueueTime(message, plugin, nanos) { - messageTypes.add(message.type); - pluginNames.add(plugin.name()); +export function registerQueueTime(message, plugin, nanos) { + messageTypes.add(message.type); + pluginNames.add(plugin.getName()); - stats.pushStats(queueTimeByMessageType, message.type, nanos / 1000000); - stats.pushStats(queueTimeByPluginName, plugin.name(), nanos / 1000000); - }, + pushStats(queueTimeByMessageType, message.type, nanos / 1_000_000); + pushStats(queueTimeByPluginName, plugin.getName(), nanos / 1_000_000); +} +export function registerProcessingTime(message, plugin, nanos) { + messageTypes.add(message.type); + pluginNames.add(plugin.getName()); - registerProcessingTime(message, plugin, nanos) { - messageTypes.add(message.type); - pluginNames.add(plugin.name()); + pushStats(processingTimeByMessageType, message.type, nanos / 1_000_000); + pushStats(processingTimeByPluginName, plugin.getName(), nanos / 1_000_000); +} +export function generateStatistics() { + const statOptions = { + percentiles: [0, 100], + includeSum: true + }; - stats.pushStats(processingTimeByMessageType, message.type, nanos / 1000000); - stats.pushStats(processingTimeByPluginName, plugin.name(), nanos / 1000000); - }, - - generateStatistics() { - const statOptions = { - percentiles: [0, 100], - includeSum: true - }; - - const byPluginName = Array.from(pluginNames).reduce( - (summary, pluginName) => { - set( - summary, - ['queueTime', pluginName], - stats.summarizeStats( - get(queueTimeByPluginName, pluginName), - statOptions - ) - ); - set( - summary, - ['processingTime', pluginName], - stats.summarizeStats( - get(processingTimeByPluginName, pluginName), - statOptions - ) - ); - - return summary; - }, - {} + const byPluginName = [...pluginNames].reduce((summary, pluginName) => { + set( + summary, + ['queueTime', pluginName], + summarizeStats(get(queueTimeByPluginName, pluginName), statOptions) + ); + set( + summary, + ['processingTime', pluginName], + summarizeStats(get(processingTimeByPluginName, pluginName), statOptions) ); - const byMessageType = Array.from(messageTypes).reduce( - (summary, messageType) => { - set( - summary, - ['queueTime', messageType], - stats.summarizeStats( - get(queueTimeByMessageType, messageType), - statOptions - ) - ); - set( - summary, - ['processingTime', messageType], - stats.summarizeStats( - get(processingTimeByMessageType, messageType), - statOptions - ) - ); + return summary; + }, {}); - return summary; - }, - {} + const byMessageType = [...messageTypes].reduce((summary, messageType) => { + set( + summary, + ['queueTime', messageType], + summarizeStats(get(queueTimeByMessageType, messageType), statOptions) + ); + set( + summary, + ['processingTime', messageType], + summarizeStats(get(processingTimeByMessageType, messageType), statOptions) ); - return { - byPluginName, - byMessageType - }; - } -}; + return summary; + }, {}); + + return { + byPluginName, + byMessageType + }; +} diff --git a/lib/core/resultsStorage/index.js b/lib/core/resultsStorage/index.js index 69fcce251..d3178038a 100644 --- a/lib/core/resultsStorage/index.js +++ b/lib/core/resultsStorage/index.js @@ -1,54 +1,50 @@ -'use strict'; +import { parse, format } from 'node:url'; +import { basename, resolve, join } from 'node:path'; -const urlParser = require('url'); -const path = require('path'); -const resultUrls = require('./resultUrls'); -const storageManager = require('./storageManager'); +import { resultUrls } from './resultUrls.js'; +import { storageManager } from './storageManager.js'; function getDomainOrFileName(input) { let domainOrFile = input; - if (domainOrFile.startsWith('http')) { - domainOrFile = urlParser.parse(domainOrFile).hostname; - } else { - domainOrFile = path.basename(domainOrFile).replace(/\./g, '_'); - } + domainOrFile = domainOrFile.startsWith('http') + ? parse(domainOrFile).hostname + : basename(domainOrFile).replace(/\./g, '_'); return domainOrFile; } -module.exports = function (input, timestamp, options) { +export function resultsStorage(input, timestamp, options) { const outputFolder = options.outputFolder; const resultBaseURL = options.resultBaseURL; const resultsSubFolders = []; let storageBasePath; let storagePathPrefix; - let resultUrl = undefined; + let resultUrl; if (outputFolder) { - resultsSubFolders.push(path.basename(outputFolder)); - storageBasePath = path.resolve(outputFolder); + resultsSubFolders.push(basename(outputFolder)); + storageBasePath = resolve(outputFolder); } else { resultsSubFolders.push( options.slug || getDomainOrFileName(input), timestamp.format('YYYY-MM-DD-HH-mm-ss') ); - - storageBasePath = path.resolve('sitespeed-result', ...resultsSubFolders); + storageBasePath = resolve('sitespeed-result', ...resultsSubFolders); } // backfill the slug options.slug = options.slug || getDomainOrFileName(input).replace(/\./g, '_'); - storagePathPrefix = path.join(...resultsSubFolders); + storagePathPrefix = join(...resultsSubFolders); if (resultBaseURL) { - const url = urlParser.parse(resultBaseURL); - resultsSubFolders.unshift(url.pathname.substr(1)); + const url = parse(resultBaseURL); + resultsSubFolders.unshift(url.pathname.slice(1)); url.pathname = resultsSubFolders.join('/'); - resultUrl = urlParser.format(url); + resultUrl = format(url); } return { storageManager: storageManager(storageBasePath, storagePathPrefix, options), resultUrls: resultUrls(resultUrl, options) }; -}; +} diff --git a/lib/core/resultsStorage/pathToFolder.js b/lib/core/resultsStorage/pathToFolder.js index 088283ef7..efdcb0a11 100644 --- a/lib/core/resultsStorage/pathToFolder.js +++ b/lib/core/resultsStorage/pathToFolder.js @@ -1,23 +1,23 @@ -'use strict'; +import { parse } from 'node:url'; +import { createHash } from 'node:crypto'; -const isEmpty = require('lodash.isempty'); -const crypto = require('crypto'); -const log = require('intel').getLogger('sitespeedio.file'); -const urlParser = require('url'); +import isEmpty from 'lodash.isempty'; +import intel from 'intel'; + +const log = intel.getLogger('sitespeedio.file'); function toSafeKey(key) { // U+2013 : EN DASH – as used on https://en.wikipedia.org/wiki/2019–20_coronavirus_pandemic - return key.replace(/[.~ /+|,:?&%–)(]|%7C/g, '-'); + return key.replace(/[ %&()+,./:?|~–]|%7C/g, '-'); } -module.exports = function pathFromRootToPageDir(url, options, alias) { +export function pathToFolder(url, options, alias) { const useHash = options.useHash; - const parsedUrl = urlParser.parse(decodeURIComponent(url)); + const parsedUrl = parse(decodeURIComponent(url)); const pathSegments = []; const urlSegments = []; - pathSegments.push('pages'); - pathSegments.push(parsedUrl.hostname.split('.').join('_')); + pathSegments.push('pages', parsedUrl.hostname.split('.').join('_')); if (options.urlMetaData && options.urlMetaData[url]) { pathSegments.push(options.urlMetaData[url]); @@ -29,14 +29,14 @@ module.exports = function pathFromRootToPageDir(url, options, alias) { } if (useHash && !isEmpty(parsedUrl.hash)) { - const md5 = crypto.createHash('md5'), - hash = md5.update(parsedUrl.hash).digest('hex').substring(0, 8); + const md5 = createHash('md5'), + hash = md5.update(parsedUrl.hash).digest('hex').slice(0, 8); urlSegments.push('hash-' + hash); } if (!isEmpty(parsedUrl.search)) { - const md5 = crypto.createHash('md5'), - hash = md5.update(parsedUrl.search).digest('hex').substring(0, 8); + const md5 = createHash('md5'), + hash = md5.update(parsedUrl.search).digest('hex').slice(0, 8); urlSegments.push('query-' + hash); } @@ -49,7 +49,7 @@ module.exports = function pathFromRootToPageDir(url, options, alias) { log.info( `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.` ); - pathSegments.push(folder.substr(0, 254)); + pathSegments.push(folder.slice(0, 254)); } else { pathSegments.push(folder); } @@ -58,11 +58,11 @@ module.exports = function pathFromRootToPageDir(url, options, alias) { // pathSegments.push('data'); - pathSegments.forEach(function (segment, index) { + for (const [index, segment] of pathSegments.entries()) { if (segment) { - pathSegments[index] = segment.replace(/[^-a-z0-9_.\u0621-\u064A]/gi, '-'); + pathSegments[index] = segment.replace(/[^\w.\u0621-\u064A-]/gi, '-'); } - }); + } return pathSegments.join('/').concat('/'); -}; +} diff --git a/lib/core/resultsStorage/resultUrls.js b/lib/core/resultsStorage/resultUrls.js index d17515329..6c3c7ebb2 100644 --- a/lib/core/resultsStorage/resultUrls.js +++ b/lib/core/resultsStorage/resultUrls.js @@ -1,17 +1,16 @@ -'use strict'; +import { parse, format } from 'node:url'; -const urlParser = require('url'); -const pathToFolder = require('./pathToFolder'); +import { pathToFolder } from './pathToFolder.js'; function getPageUrl({ url, resultBaseUrl, options, alias }) { - const pageUrl = urlParser.parse(resultBaseUrl); + const pageUrl = parse(resultBaseUrl); pageUrl.pathname = [pageUrl.pathname, pathToFolder(url, options, alias)].join( '/' ); - return urlParser.format(pageUrl); + return format(pageUrl); } -module.exports = function resultUrls(resultBaseUrl, options) { +export function resultUrls(resultBaseUrl, options) { return { hasBaseUrl() { return !!resultBaseUrl; @@ -30,4 +29,4 @@ module.exports = function resultUrls(resultBaseUrl, options) { return pathToFolder(url, options, alias); } }; -}; +} diff --git a/lib/core/resultsStorage/storageManager.js b/lib/core/resultsStorage/storageManager.js index 4340a5775..45a7935a8 100644 --- a/lib/core/resultsStorage/storageManager.js +++ b/lib/core/resultsStorage/storageManager.js @@ -1,38 +1,51 @@ -'use strict'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; +import { + rmdir as _rmdir, + mkdir as _mkdir, + lstat as _lstat, + readdir as _readdir, + unlink as _unlink, + writeFile as _writeFile +} from 'node:fs'; -const fs = require('fs-extra'); -const path = require('path'); -const log = require('intel').getLogger('sitespeedio.storageManager'); -const { promisify } = require('util'); -const mkdir = promisify(fs.mkdir); -const readdir = promisify(fs.readdir); -const lstat = promisify(fs.lstat); -const unlink = promisify(fs.unlink); -const rmdir = promisify(fs.rmdir); -const pathToFolder = require('./pathToFolder'); +import { copy } from 'fs-extra/esm'; +import intel from 'intel'; + +import { pathToFolder } from './pathToFolder.js'; + +const log = intel.getLogger('sitespeedio.storageManager'); +const mkdir = promisify(_mkdir); +const readdir = promisify(_readdir); +const lstat = promisify(_lstat); +const unlink = promisify(_unlink); +const rmdir = promisify(_rmdir); +const writeFile = promisify(_writeFile); function write(dirPath, filename, data) { - return fs.writeFile(path.join(dirPath, filename), data); + return writeFile(join(dirPath, filename), data); } function isValidDirectoryName(name) { return name !== undefined && name !== ''; } -module.exports = function storageManager(baseDir, storagePathPrefix, options) { +export function storageManager(baseDir, storagePathPrefix, options) { return { rootPathFromUrl(url, alias) { return pathToFolder(url, options, alias) .split('/') - .filter(isValidDirectoryName) + .filter(element => isValidDirectoryName(element)) .map(() => '..') .join('/') .concat('/'); }, - createDirectory(...subDirs) { - const pathSegments = [baseDir, ...subDirs].filter(isValidDirectoryName); + createDirectory(...subDirectories) { + const pathSegments = [baseDir, ...subDirectories].filter(element => + isValidDirectoryName(element) + ); - const dirPath = path.join.apply(null, pathSegments); + const dirPath = join.apply(undefined, pathSegments); return mkdir(dirPath, { recursive: true }).then(() => dirPath); }, writeData(data, filename) { @@ -50,41 +63,37 @@ module.exports = function storageManager(baseDir, storagePathPrefix, options) { return baseDir; }, getFullPathToURLDir(url, alias) { - return path.join(baseDir, pathToFolder(url, options, alias)); + return join(baseDir, pathToFolder(url, options, alias)); }, getStoragePrefix() { return storagePathPrefix; }, copyToResultDir(filename) { - return this.createDirectory().then(dir => fs.copy(filename, dir)); + return this.createDirectory().then(dir => copy(filename, dir)); }, copyFileToDir(filename, dir) { - return fs.copy(filename, dir); + return copy(filename, dir); }, // TODO is missing alias removeDataForUrl(url) { - const dirName = path.join(baseDir, pathToFolder(url, options)); + const dirName = join(baseDir, pathToFolder(url, options)); const removeDir = async dir => { try { const files = await readdir(dir); await Promise.all( files.map(async file => { try { - const p = path.join(dir, file); + const p = join(dir, file); const stat = await lstat(p); - if (stat.isDirectory()) { - await removeDir(p); - } else { - await unlink(p); - } - } catch (err) { - log.error('Could not remove file:' + file, err); + await (stat.isDirectory() ? removeDir(p) : unlink(p)); + } catch (error) { + log.error('Could not remove file:' + file, error); } }) ); await rmdir(dir); - } catch (err) { - log.error('Could not remove dir:' + dir, err); + } catch (error) { + log.error('Could not remove dir:' + dir, error); } }; return removeDir(dirName); @@ -105,4 +114,4 @@ module.exports = function storageManager(baseDir, storagePathPrefix, options) { ); } }; -}; +} diff --git a/lib/core/script-source.js b/lib/core/script-source.js index 6f32643e7..aacf79fe4 100644 --- a/lib/core/script-source.js +++ b/lib/core/script-source.js @@ -1,15 +1,9 @@ -'use strict'; -const messageMaker = require('../support/messageMaker'); +import { messageMaker } from '../support/messageMaker.js'; const make = messageMaker('script-reader').make; -module.exports = { - open(context, options) { - this.options = options; - }, - findUrls(queue) { - queue.postMessage( - make('browsertime.navigationScripts', {}, { url: this.options.urls }) - ); - } -}; +export function findUrls(queue, options) { + queue.postMessage( + make('browsertime.navigationScripts', {}, { url: options.urls }) + ); +} diff --git a/lib/core/url-source.js b/lib/core/url-source.js index 99362af40..52b6bbe82 100644 --- a/lib/core/url-source.js +++ b/lib/core/url-source.js @@ -1,30 +1,23 @@ -'use strict'; - -const urlParser = require('url'); -const messageMaker = require('../support/messageMaker'); +import { parse } from 'node:url'; +import { messageMaker } from '../support/messageMaker.js'; const make = messageMaker('url-reader').make; -module.exports = { - open(context, options) { - this.options = options; - }, - findUrls(queue) { - for (const url of this.options.urls) { - queue.postMessage( - make( - 'url', - {}, - { - url: url, - group: - this.options.urlsMetaData && - this.options.urlsMetaData[url] && - this.options.urlsMetaData[url].groupAlias - ? this.options.urlsMetaData[url].groupAlias - : urlParser.parse(url).hostname - } - ) - ); - } +export function findUrls(queue, options) { + for (const url of options.urls) { + queue.postMessage( + make( + 'url', + {}, + { + url: url, + group: + options.urlsMetaData && + options.urlsMetaData[url] && + options.urlsMetaData[url].groupAlias + ? options.urlsMetaData[url].groupAlias + : parse(url).hostname + } + ) + ); } -}; +} diff --git a/lib/plugins/analysisstorer/index.js b/lib/plugins/analysisstorer/index.js index d3c5230c9..13a3f3604 100644 --- a/lib/plugins/analysisstorer/index.js +++ b/lib/plugins/analysisstorer/index.js @@ -1,48 +1,50 @@ -'use strict'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; function shouldIgnoreMessage(message) { - return ( - [ - 'url', - 'browsertime.navigationScripts', - 'error', - 'sitespeedio.summarize', - 'sitespeedio.prepareToRender', - 'sitespeedio.render', - 'html.finished', - 'axe.setup', - 'browsertime.har', - 'browsertime.config', - 'browsertime.setup', - 'browsertime.scripts', - 'browsertime.asyncscripts', - 'sitespeedio.setup', - 'webpagetest.har', - 'webpagetest.setup', - 'aggregateassets.summary', - 'slowestassets.summary', - 'largestassets.summary', - 'budget.addMessageType', - 'html.css', - 'html.pug', - 's3.finished', - 'scp.finished', - 'gcs.finished', - 'ftp.finished', - 'graphite.setup', - 'influxdb.setup', - 'grafana.setup', - 'sustainable.setup', - 'scp.setup' - ].indexOf(message.type) >= 0 - ); + return [ + 'url', + 'browsertime.navigationScripts', + 'error', + 'sitespeedio.summarize', + 'sitespeedio.prepareToRender', + 'sitespeedio.render', + 'html.finished', + 'axe.setup', + 'browsertime.har', + 'browsertime.config', + 'browsertime.setup', + 'browsertime.scripts', + 'browsertime.asyncscripts', + 'sitespeedio.setup', + 'webpagetest.har', + 'webpagetest.setup', + 'aggregateassets.summary', + 'slowestassets.summary', + 'largestassets.summary', + 'budget.addMessageType', + 'html.css', + 'html.pug', + 's3.finished', + 'scp.finished', + 'gcs.finished', + 'ftp.finished', + 'graphite.setup', + 'influxdb.setup', + 'grafana.setup', + 'sustainable.setup', + 'scp.setup' + ].includes(message.type); } -module.exports = { +export default class AnalysisstorerPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'analysisstorer', options, context, queue }); + } + open(context) { this.storageManager = context.storageManager; this.alias = {}; - }, + } processMessage(message) { if (shouldIgnoreMessage(message)) { return; @@ -75,4 +77,4 @@ module.exports = { return this.storageManager.writeData(jsonData, fileName); } } -}; +} diff --git a/lib/plugins/assets/aggregator.js b/lib/plugins/assets/aggregator.js index 491518799..bb94f65c1 100644 --- a/lib/plugins/assets/aggregator.js +++ b/lib/plugins/assets/aggregator.js @@ -1,17 +1,18 @@ -'use strict'; +import get from 'lodash.get'; +import { AssetsBySize } from './assetsBySize.js'; +import { AssetsBySpeed } from './assetsBySpeed.js'; -let AssetsBySize = require('./assetsBySize'), - get = require('lodash.get'), - AssetsBySpeed = require('./assetsBySpeed'); +export class AssetsAggregator { + constructor() { + this.assets = {}; + this.groups = {}; + this.largestAssets = {}; + this.largestAssetsByGroup = {}; + this.slowestAssetsByGroup = {}; + this.largestThirdPartyAssetsByGroup = {}; + this.slowestThirdPartyAssetsByGroup = {}; + } -module.exports = { - assets: {}, - groups: {}, - largestAssets: {}, - largestAssetsByGroup: {}, - slowestAssetsByGroup: {}, - largestThirdPartyAssetsByGroup: {}, - slowestThirdPartyAssetsByGroup: {}, addToAggregate(data, group, url, resultUrls, runIndex, options, alias) { const maxSize = get(options, 'html.topListSize', 10); let page = resultUrls.relativeSummaryPageUrl(url, alias[url]); @@ -53,13 +54,11 @@ module.exports = { const url = asset.url; - if (options.firstParty) { - if (!url.match(options.firstParty)) { - this.slowestAssetsThirdParty.add(asset, page, runPage); - this.largestAssetsThirdParty.add(asset, page, runPage); - this.slowestThirdPartyAssetsByGroup[group].add(asset, page, runPage); - this.largestThirdPartyAssetsByGroup[group].add(asset, page, runPage); - } + if (options.firstParty && !options.firstParty.test(url)) { + this.slowestAssetsThirdParty.add(asset, page, runPage); + this.largestAssetsThirdParty.add(asset, page, runPage); + this.slowestThirdPartyAssetsByGroup[group].add(asset, page, runPage); + this.largestThirdPartyAssetsByGroup[group].add(asset, page, runPage); } const urlInfo = this.assets[url] || { @@ -91,8 +90,7 @@ module.exports = { urlInfoGroup.requestCount++; this.groups[group][url] = urlInfoGroup; } - }, - + } summarize() { const summary = { groups: { @@ -134,4 +132,4 @@ module.exports = { return summary; } -}; +} diff --git a/lib/plugins/assets/assetsBySize.js b/lib/plugins/assets/assetsBySize.js index 76eba241f..e052cbd48 100644 --- a/lib/plugins/assets/assetsBySize.js +++ b/lib/plugins/assets/assetsBySize.js @@ -1,6 +1,4 @@ -'use strict'; - -class AssetsBySize { +export class AssetsBySize { constructor(maxSize) { this.maxSize = maxSize; this.items = []; @@ -42,5 +40,3 @@ class AssetsBySize { return this.items; } } - -module.exports = AssetsBySize; diff --git a/lib/plugins/assets/assetsBySpeed.js b/lib/plugins/assets/assetsBySpeed.js index 2db86b55e..fce6d63b7 100644 --- a/lib/plugins/assets/assetsBySpeed.js +++ b/lib/plugins/assets/assetsBySpeed.js @@ -1,6 +1,4 @@ -'use strict'; - -class AssetsBySpeed { +export class AssetsBySpeed { constructor(maxSize) { this.maxSize = maxSize; this.items = []; @@ -52,5 +50,3 @@ class AssetsBySpeed { return this.items; } } - -module.exports = AssetsBySpeed; diff --git a/lib/plugins/assets/index.js b/lib/plugins/assets/index.js index df6d66e9e..bd2ef3b17 100644 --- a/lib/plugins/assets/index.js +++ b/lib/plugins/assets/index.js @@ -1,15 +1,19 @@ -'use strict'; - -const isEmpty = require('lodash.isempty'); -const aggregator = require('./aggregator'); +import isEmpty from 'lodash.isempty'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +import { AssetsAggregator } from './aggregator.js'; const DEFAULT_METRICS_LARGEST_ASSETS = ['image.0.transferSize']; -module.exports = { +export default class AssetsPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'assets', options, context, queue }); + } + open(context, options) { this.make = context.messageMaker('assets').make; this.options = options; this.alias = {}; this.resultUrls = context.resultUrls; + this.assetsAggregator = new AssetsAggregator(); context.filterRegistry.registerFilterForType( DEFAULT_METRICS_LARGEST_ASSETS, 'largestassets.summary' @@ -24,12 +28,12 @@ module.exports = { [], 'largestthirdpartyassets.summary' ); - }, + } processMessage(message, queue) { const make = this.make; switch (message.type) { case 'pagexray.run': { - aggregator.addToAggregate( + this.assetsAggregator.addToAggregate( message.data, message.group, message.url, @@ -46,7 +50,7 @@ module.exports = { break; } case 'sitespeedio.summarize': { - const summary = aggregator.summarize(); + const summary = this.assetsAggregator.summarize(); if (!isEmpty(summary)) { for (let group of Object.keys(summary.groups)) { queue.postMessage( @@ -87,4 +91,4 @@ module.exports = { } } } -}; +} diff --git a/lib/plugins/axe/axePostScript.cjs b/lib/plugins/axe/axePostScript.cjs index 1282a6f7f..bf44b0874 100644 --- a/lib/plugins/axe/axePostScript.cjs +++ b/lib/plugins/axe/axePostScript.cjs @@ -36,10 +36,11 @@ module.exports = async function (context) { ); // Use the extras field in Browsertime and pass on the result context.result[context.result.length - 1].extras.axe = result; - } catch (e) { + + } catch (error) { context.log.error( 'Could not run the AXE script, no AXE information collected', - e + error ); } }; diff --git a/lib/plugins/axe/index.js b/lib/plugins/axe/index.js index 3a70aabfe..f35031c90 100644 --- a/lib/plugins/axe/index.js +++ b/lib/plugins/axe/index.js @@ -1,25 +1,30 @@ -'use strict'; -const path = require('path'); -const fs = require('fs'); -const log = require('intel').getLogger('sitespeedio.plugin.axe'); +import { resolve } from 'node:path'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import intel from 'intel'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +const log = intel.getLogger('sitespeedio.plugin.axe'); +const __dirname = fileURLToPath(new URL('.', import.meta.url)); + +export default class AxePlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'axe', options, context, queue }); + } -module.exports = { open(context, options) { this.options = options; this.make = context.messageMaker('axe').make; - this.pug = fs.readFileSync( - path.resolve(__dirname, 'pug', 'index.pug'), - 'utf8' - ); + this.pug = readFileSync(resolve(__dirname, 'pug', 'index.pug'), 'utf8'); log.info('Axe plugin activated'); - }, + } + processMessage(message, queue) { const make = this.make; switch (message.type) { case 'browsertime.setup': { queue.postMessage( make('browsertime.config', { - postURLScript: path.resolve(__dirname, 'axePostScript.cjs') + postURLScript: resolve(__dirname, 'axePostScript.cjs') }) ); break; @@ -53,4 +58,4 @@ module.exports = { } } } -}; +} diff --git a/lib/plugins/browsertime/analyzer.js b/lib/plugins/browsertime/analyzer.js index b6dbcea2f..c40c76542 100644 --- a/lib/plugins/browsertime/analyzer.js +++ b/lib/plugins/browsertime/analyzer.js @@ -1,12 +1,12 @@ -'use strict'; - -const merge = require('lodash.merge'); -const forEach = require('lodash.foreach'); -const path = require('path'); -const set = require('lodash.set'); -const get = require('lodash.get'); -const coach = require('coach-core'); -const log = require('intel').getLogger('plugin.browsertime'); +import { resolve } from 'node:path'; +import merge from 'lodash.merge'; +import forEach from 'lodash.foreach'; +import set from 'lodash.set'; +import get from 'lodash.get'; +import coach from 'coach-core'; +const { getDomAdvice } = coach; +import intel from 'intel'; +const log = intel.getLogger('plugin.browsertime'); const defaultBrowsertimeOptions = { statistics: true @@ -52,28 +52,26 @@ async function preWarmServer(urls, options, scriptOrMultiple) { } } - const {BrowsertimeEngine} = await import ('browsertime'); - const engine = new BrowsertimEngine(preWarmOptions); + const { BrowsertimeEngine } = await import('browsertime'); + const engine = new BrowsertimeEngine(preWarmOptions); await engine.start(); log.info('Start pre-testing/warming' + urls); - if (scriptOrMultiple) { - await engine.runMultiple(urls, {}); - } else { - await engine.run(urls, {}); - } + await (scriptOrMultiple + ? engine.runMultiple(urls, {}) + : engine.run(urls, {})); await engine.stop(); log.info('Pre-testing done, closed the browser.'); return delay(options.preWarmServerWaitTime || 5000); } async function parseUserScripts(scripts) { - const {browserScripts} = await import ('browsertime'); + const { browserScripts } = await import('browsertime'); if (!Array.isArray(scripts)) scripts = [scripts]; const allUserScripts = {}; for (let script of scripts) { let myScript = await browserScripts.findAndParseScripts( - path.resolve(script), + resolve(script), 'custom' ); if (!myScript['custom']) { @@ -85,7 +83,7 @@ async function parseUserScripts(scripts) { } async function addCoachScripts(scripts) { - const coachAdvice = await coach.getDomAdvice(); + const coachAdvice = await getDomAdvice(); scripts.coach = { coachAdvice: coachAdvice }; @@ -115,74 +113,68 @@ function setupAsynScripts(asyncScripts) { return allAsyncScripts; } -module.exports = { - async analyzeUrl( - url, - scriptOrMultiple, - pluginScripts, - pluginAsyncScripts, - options - ) { - const btOptions = merge({}, defaultBrowsertimeOptions, options); +export async function analyzeUrl( + url, + scriptOrMultiple, + pluginScripts, + pluginAsyncScripts, + options +) { + const btOptions = merge({}, defaultBrowsertimeOptions, options); - // set mobile options - if (options.mobile) { - btOptions.viewPort = '360x640'; - if (btOptions.browser === 'chrome' || btOptions.browser === 'edge') { - const emulation = get( - btOptions, - 'chrome.mobileEmulation.deviceName', - 'Moto G4' - ); - btOptions.chrome.mobileEmulation = { - deviceName: emulation - }; - } else { - btOptions.userAgent = iphone6UserAgent; - } - } - const {BrowsertimeEngine, browserScripts } = await import ('browsertime'); - const scriptCategories = await browserScripts.allScriptCategories(); - let scriptsByCategory = await browserScripts.getScriptsForCategories( - scriptCategories - ); - - if (btOptions.script) { - const userScripts = await parseUserScripts(btOptions.script); - scriptsByCategory = merge(scriptsByCategory, userScripts); - } - - if (btOptions.coach) { - scriptsByCategory = addCoachScripts(scriptsByCategory); - } - scriptsByCategory = await addExtraScripts(scriptsByCategory, pluginScripts); - - if (btOptions.preWarmServer) { - await preWarmServer(url, btOptions, scriptOrMultiple); - } - - const engine = new BrowsertimeEngine(btOptions); - - const asyncScript = - pluginAsyncScripts.length > 0 - ? await setupAsynScripts(pluginAsyncScripts) - : undefined; - - try { - await engine.start(); - if (scriptOrMultiple) { - const res = await engine.runMultiple( - url, - scriptsByCategory, - asyncScript - ); - return res; - } else { - const res = await engine.run(url, scriptsByCategory, asyncScript); - return res; - } - } finally { - await engine.stop(); + // set mobile options + if (options.mobile) { + btOptions.viewPort = '360x640'; + if (btOptions.browser === 'chrome' || btOptions.browser === 'edge') { + const emulation = get( + btOptions, + 'chrome.mobileEmulation.deviceName', + 'Moto G4' + ); + btOptions.chrome.mobileEmulation = { + deviceName: emulation + }; + } else { + btOptions.userAgent = iphone6UserAgent; } } -}; + const { BrowsertimeEngine, browserScripts } = await import('browsertime'); + const scriptCategories = await browserScripts.allScriptCategories(); + let scriptsByCategory = await browserScripts.getScriptsForCategories( + scriptCategories + ); + + if (btOptions.script) { + const userScripts = await parseUserScripts(btOptions.script); + scriptsByCategory = merge(scriptsByCategory, userScripts); + } + + if (btOptions.coach) { + scriptsByCategory = addCoachScripts(scriptsByCategory); + } + scriptsByCategory = await addExtraScripts(scriptsByCategory, pluginScripts); + + if (btOptions.preWarmServer) { + await preWarmServer(url, btOptions, scriptOrMultiple); + } + + const engine = new BrowsertimeEngine(btOptions); + + const asyncScript = + pluginAsyncScripts.length > 0 + ? await setupAsynScripts(pluginAsyncScripts) + : undefined; + + try { + await engine.start(); + if (scriptOrMultiple) { + const res = await engine.runMultiple(url, scriptsByCategory, asyncScript); + return res; + } else { + const res = await engine.run(url, scriptsByCategory, asyncScript); + return res; + } + } finally { + await engine.stop(); + } +} diff --git a/lib/plugins/browsertime/axeAggregator.js b/lib/plugins/browsertime/axeAggregator.js index 3084a4aab..7ffcef1ce 100644 --- a/lib/plugins/browsertime/axeAggregator.js +++ b/lib/plugins/browsertime/axeAggregator.js @@ -1,7 +1,6 @@ -'use strict'; -const Stats = require('fast-stats').Stats; -const statsHelpers = require('../../support/statsHelpers'); -class AxeAggregator { +import { Stats } from 'fast-stats'; +import { summarizeStats as _summarizeStats } from '../../support/statsHelpers.js'; +export class AxeAggregator { constructor(options) { this.options = options; this.axeViolations = { @@ -33,13 +32,11 @@ class AxeAggregator { summarizeStats() { return { violations: { - critical: statsHelpers.summarizeStats(this.axeViolations.critical), - serious: statsHelpers.summarizeStats(this.axeViolations.serious), - minor: statsHelpers.summarizeStats(this.axeViolations.minor), - moderate: statsHelpers.summarizeStats(this.axeViolations.moderate) + critical: _summarizeStats(this.axeViolations.critical), + serious: _summarizeStats(this.axeViolations.serious), + minor: _summarizeStats(this.axeViolations.minor), + moderate: _summarizeStats(this.axeViolations.moderate) } }; } } - -module.exports = AxeAggregator; diff --git a/lib/plugins/browsertime/aggregator.js b/lib/plugins/browsertime/browsertimeAggregator.js similarity index 73% rename from lib/plugins/browsertime/aggregator.js rename to lib/plugins/browsertime/browsertimeAggregator.js index 3eeba57c8..dbdf6eedd 100644 --- a/lib/plugins/browsertime/aggregator.js +++ b/lib/plugins/browsertime/browsertimeAggregator.js @@ -1,13 +1,13 @@ -'use strict'; - -const forEach = require('lodash.foreach'), - statsHelpers = require('../../support/statsHelpers'); +import forEach from 'lodash.foreach'; +import { pushGroupStats, setStatsSummary } from '../../support/statsHelpers.js'; const timings = ['firstPaint', 'timeToDomContentFlushed']; -module.exports = { - statsPerType: {}, - groups: {}, +export class BrowsertimeAggregator { + constructor() { + this.statsPerType = {}; + this.groups = {}; + } addToAggregate(browsertimeRunData, group) { if (this.groups[group] === undefined) { @@ -15,7 +15,7 @@ module.exports = { } if (browsertimeRunData.fullyLoaded) { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['timings', 'fullyLoaded'], @@ -24,7 +24,7 @@ module.exports = { } if (browsertimeRunData.memory) { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['memory'], @@ -34,7 +34,7 @@ module.exports = { if (browsertimeRunData.googleWebVitals) { for (let metric of Object.keys(browsertimeRunData.googleWebVitals)) { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['googleWebVitals', metric], @@ -44,7 +44,7 @@ module.exports = { } if (browsertimeRunData.timings.largestContentfulPaint) { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['timings', 'largestContentfulPaint'], @@ -53,7 +53,7 @@ module.exports = { } if (browsertimeRunData.timings.interactionToNextPaint) { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['timings', 'interactionToNextPaint'], @@ -62,7 +62,7 @@ module.exports = { } if (browsertimeRunData.pageinfo.cumulativeLayoutShift) { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['pageinfo', 'cumulativeLayoutShift'], @@ -72,7 +72,7 @@ module.exports = { forEach(timings, timing => { if (browsertimeRunData.timings[timing]) { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], timing, @@ -83,7 +83,7 @@ module.exports = { forEach(browsertimeRunData.timings.navigationTiming, (value, name) => { if (value) { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['navigationTiming', name], @@ -94,7 +94,7 @@ module.exports = { // pick up one level of custom metrics forEach(browsertimeRunData.custom, (value, name) => { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['custom', name], @@ -103,7 +103,7 @@ module.exports = { }); forEach(browsertimeRunData.timings.pageTimings, (value, name) => { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['pageTimings', name], @@ -112,7 +112,7 @@ module.exports = { }); forEach(browsertimeRunData.timings.paintTiming, (value, name) => { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['paintTiming', name], @@ -121,7 +121,7 @@ module.exports = { }); forEach(browsertimeRunData.timings.userTimings.marks, timing => { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['userTimings', 'marks', timing.name], @@ -130,7 +130,7 @@ module.exports = { }); forEach(browsertimeRunData.timings.userTimings.measures, timing => { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['userTimings', 'measures', timing.name], @@ -141,8 +141,8 @@ module.exports = { forEach(browsertimeRunData.visualMetrics, (value, name) => { // Sometimes visual elements fails and gives us null values // And skip VisualProgress, ContentfulSpeedIndexProgress and others - if (name.indexOf('Progress') === -1 && value !== null) { - statsHelpers.pushGroupStats( + if (!name.includes('Progress') && value !== null) { + pushGroupStats( this.statsPerType, this.groups[group], ['visualMetrics', name], @@ -153,28 +153,28 @@ module.exports = { if (browsertimeRunData.cpu) { if (browsertimeRunData.cpu.longTasks) { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['cpu', 'longTasks', 'tasks'], browsertimeRunData.cpu.longTasks.tasks ); - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['cpu', 'longTasks', 'totalDuration'], browsertimeRunData.cpu.longTasks.totalDuration ); - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['cpu', 'longTasks', 'totalBlockingTime'], browsertimeRunData.cpu.longTasks.totalBlockingTime ); - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['cpu', 'longTasks', 'maxPotentialFid'], @@ -185,7 +185,7 @@ module.exports = { for (let categoryName of Object.keys( browsertimeRunData.cpu.categories )) { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerType, this.groups[group], ['cpu', 'categories', categoryName], @@ -194,10 +194,11 @@ module.exports = { } } } - }, + } + summarize() { if (Object.keys(this.statsPerType).length === 0) { - return undefined; + return; } const summary = { @@ -210,51 +211,51 @@ module.exports = { summary.groups[group] = this.summarizePerObject(this.groups[group]); } return summary; - }, + } - summarizePerObject(obj) { - return Object.keys(obj).reduce((summary, name) => { - if (timings.indexOf(name) > -1) { - statsHelpers.setStatsSummary(summary, name, obj[name]); - } else if ('userTimings'.indexOf(name) > -1) { + summarizePerObject(object) { + return Object.keys(object).reduce((summary, name) => { + if (timings.includes(name)) { + setStatsSummary(summary, name, object[name]); + } else if ('userTimings'.includes(name)) { summary.userTimings = {}; const marksData = {}, measuresData = {}; - forEach(obj.userTimings.marks, (stats, timingName) => { - statsHelpers.setStatsSummary(marksData, timingName, stats); + forEach(object.userTimings.marks, (stats, timingName) => { + setStatsSummary(marksData, timingName, stats); }); - forEach(obj.userTimings.measures, (stats, timingName) => { - statsHelpers.setStatsSummary(measuresData, timingName, stats); + forEach(object.userTimings.measures, (stats, timingName) => { + setStatsSummary(measuresData, timingName, stats); }); summary.userTimings.marks = marksData; summary.userTimings.measures = measuresData; - } else if ('cpu'.indexOf(name) > -1) { + } else if ('cpu'.includes(name)) { const longTasks = {}; const categories = {}; summary.cpu = {}; - forEach(obj.cpu.longTasks, (stats, name) => { - statsHelpers.setStatsSummary(longTasks, name, stats); + forEach(object.cpu.longTasks, (stats, name) => { + setStatsSummary(longTasks, name, stats); }); - forEach(obj.cpu.categories, (stats, name) => { - statsHelpers.setStatsSummary(categories, name, stats); + forEach(object.cpu.categories, (stats, name) => { + setStatsSummary(categories, name, stats); }); summary.cpu.longTasks = longTasks; summary.cpu.categories = categories; - } else if ('memory'.indexOf(name) > -1) { + } else if ('memory'.includes(name)) { const memory = {}; - statsHelpers.setStatsSummary(memory, 'memory', obj[name]); + setStatsSummary(memory, 'memory', object[name]); summary.memory = memory.memory; } else { const categoryData = {}; - forEach(obj[name], (stats, timingName) => { - statsHelpers.setStatsSummary(categoryData, timingName, stats); + forEach(object[name], (stats, timingName) => { + setStatsSummary(categoryData, timingName, stats); }); summary[name] = categoryData; } return summary; }, {}); } -}; +} diff --git a/lib/plugins/browsertime/consoleLogAggregator.js b/lib/plugins/browsertime/consoleLogAggregator.js index c29c4ba05..483f6534e 100644 --- a/lib/plugins/browsertime/consoleLogAggregator.js +++ b/lib/plugins/browsertime/consoleLogAggregator.js @@ -1,8 +1,7 @@ -'use strict'; -const Stats = require('fast-stats').Stats; -const statsHelpers = require('../../support/statsHelpers'); -const { getGzippedFileAsJson } = require('./reader.js'); -class ConsoleLogAggregator { +import { Stats } from 'fast-stats'; +import { summarizeStats as _summarizeStats } from '../../support/statsHelpers.js'; +import { getGzippedFileAsJson } from './reader.js'; +export class ConsoleLogAggregator { constructor(options) { this.options = options; this.logs = { SEVERE: new Stats(), WARNING: new Stats() }; @@ -35,10 +34,8 @@ class ConsoleLogAggregator { summarizeStats() { return { - error: statsHelpers.summarizeStats(this.logs.SEVERE), - warning: statsHelpers.summarizeStats(this.logs.WARNING) + error: _summarizeStats(this.logs.SEVERE), + warning: _summarizeStats(this.logs.WARNING) }; } } - -module.exports = ConsoleLogAggregator; diff --git a/lib/plugins/browsertime/default/config.js b/lib/plugins/browsertime/default/config.js index 8d46c9184..a862d8be0 100644 --- a/lib/plugins/browsertime/default/config.js +++ b/lib/plugins/browsertime/default/config.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = { +export const browsertimeDefaultSettings = { browser: 'chrome', iterations: 3, connectivity: { diff --git a/lib/plugins/browsertime/default/metricsPageSummary.js b/lib/plugins/browsertime/default/metricsPageSummary.js index 8333d8641..9a3b5de71 100644 --- a/lib/plugins/browsertime/default/metricsPageSummary.js +++ b/lib/plugins/browsertime/default/metricsPageSummary.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = [ +export const metricsPageSummary = [ 'statistics.timings.pageTimings', 'statistics.timings.fullyLoaded', 'statistics.timings.firstPaint', diff --git a/lib/plugins/browsertime/default/metricsRun.js b/lib/plugins/browsertime/default/metricsRun.js index 3591417f4..a69a9d8d7 100644 --- a/lib/plugins/browsertime/default/metricsRun.js +++ b/lib/plugins/browsertime/default/metricsRun.js @@ -1,4 +1,4 @@ -module.exports = [ +export const metricsRun = [ 'fullyLoaded', 'timings.ttfb', 'timings.firstPaint', diff --git a/lib/plugins/browsertime/default/metricsRunLimited.js b/lib/plugins/browsertime/default/metricsRunLimited.js index a17c8a04e..37a88d636 100644 --- a/lib/plugins/browsertime/default/metricsRunLimited.js +++ b/lib/plugins/browsertime/default/metricsRunLimited.js @@ -1,4 +1,4 @@ -module.exports = [ +export const metricsRunLimited = [ 'fullyLoaded', 'timings.ttfb', 'timings.paintTiming.first-contentful-paint', diff --git a/lib/plugins/browsertime/default/metricsSummary.js b/lib/plugins/browsertime/default/metricsSummary.js index fb2bcd564..1cb22e1df 100644 --- a/lib/plugins/browsertime/default/metricsSummary.js +++ b/lib/plugins/browsertime/default/metricsSummary.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = [ +export const metricsSummary = [ 'firstPaint', 'timeToDomContentFlushed', 'fullyLoaded', diff --git a/lib/plugins/browsertime/filmstrip.js b/lib/plugins/browsertime/filmstrip.js index 56e0a8b1c..ce30fb2d9 100644 --- a/lib/plugins/browsertime/filmstrip.js +++ b/lib/plugins/browsertime/filmstrip.js @@ -1,10 +1,9 @@ -'use strict'; - -const fs = require('fs'); -const { promisify } = require('util'); -const readdir = promisify(fs.readdir); -const path = require('path'); -const log = require('intel').getLogger('sitespeedio.plugin.browsertime'); +import { readdir as _readdir } from 'node:fs'; +import { promisify } from 'node:util'; +import { join } from 'node:path'; +const readdir = promisify(_readdir); +import intel from 'intel'; +const log = intel.getLogger('sitespeedio.plugin.browsertime'); function findFrame(videoFrames, time) { let frame = videoFrames[0]; @@ -29,7 +28,7 @@ function getMetricsFromBrowsertime(data) { for (let mark of data.timings.userTimings.marks) { metrics.push({ metric: mark.name, - value: mark.startTime.toFixed() + value: mark.startTime.toFixed(0) }); userTimings++; if (userTimings > maxUserTimings) { @@ -56,16 +55,18 @@ function getMetricsFromBrowsertime(data) { }); if (data.timings.pageTimings) { - metrics.push({ - metric: 'domContentLoadedTime', - name: 'DOM Content Loaded Time', - value: data.timings.pageTimings.domContentLoadedTime - }); - metrics.push({ - metric: 'pageLoadTime', - name: 'Page Load Time', - value: data.timings.pageTimings.pageLoadTime - }); + metrics.push( + { + metric: 'domContentLoadedTime', + name: 'DOM Content Loaded Time', + value: data.timings.pageTimings.domContentLoadedTime + }, + { + metric: 'pageLoadTime', + name: 'Page Load Time', + value: data.timings.pageTimings.pageLoadTime + } + ); } if ( @@ -84,20 +85,17 @@ function getMetricsFromBrowsertime(data) { data.timings.largestContentfulPaint.renderTime ) { let name = 'LCP'; - if (data.timings.largestContentfulPaint.tagName === 'IMG') { - name += - ' <IMG>'; - } else { - name += - ' <' + - data.timings.largestContentfulPaint.tagName + - '>' + - (data.timings.largestContentfulPaint.id !== '' - ? ' ' + data.timings.largestContentfulPaint.id - : ''); - } + name += + data.timings.largestContentfulPaint.tagName === 'IMG' + ? ' <IMG>' + : ' <' + + data.timings.largestContentfulPaint.tagName + + '>' + + (data.timings.largestContentfulPaint.id !== '' + ? ' ' + data.timings.largestContentfulPaint.id + : ''); metrics.push({ metric: 'largestContentfulPaint', name, @@ -106,31 +104,33 @@ function getMetricsFromBrowsertime(data) { } if (data.visualMetrics) { - metrics.push({ - metric: 'FirstVisualChange', - name: 'First Visual Change', - value: data.visualMetrics.FirstVisualChange - }); - metrics.push({ - metric: 'LastVisualChange', - name: 'Last Visual Change', - value: data.visualMetrics.LastVisualChange - }); - metrics.push({ - metric: 'VisualComplete85', - name: 'Visual Complete 85%', - value: data.visualMetrics.VisualComplete85 - }); - metrics.push({ - metric: 'VisualComplete95', - name: 'Visual Complete 95%', - value: data.visualMetrics.VisualComplete95 - }); - metrics.push({ - metric: 'VisualComplete99', - name: 'Visual Complete 99%', - value: data.visualMetrics.VisualComplete99 - }); + metrics.push( + { + metric: 'FirstVisualChange', + name: 'First Visual Change', + value: data.visualMetrics.FirstVisualChange + }, + { + metric: 'LastVisualChange', + name: 'Last Visual Change', + value: data.visualMetrics.LastVisualChange + }, + { + metric: 'VisualComplete85', + name: 'Visual Complete 85%', + value: data.visualMetrics.VisualComplete85 + }, + { + metric: 'VisualComplete95', + name: 'Visual Complete 95%', + value: data.visualMetrics.VisualComplete95 + }, + { + metric: 'VisualComplete99', + name: 'Visual Complete 99%', + value: data.visualMetrics.VisualComplete99 + } + ); if (data.visualMetrics.LargestImage) { metrics.push({ metric: 'LargestImage', @@ -169,59 +169,65 @@ function getMetricsFromBrowsertime(data) { function findTimings(timings, start, end) { return timings.filter(timing => timing.value > start && timing.value <= end); } -module.exports = { - async getFilmstrip(browsertimeData, run, dir, options, fullPath) { - let doWeHaveFilmstrip = - options.browsertime.visualMetrics === true && - options.browsertime.videoParams.createFilmstrip === true; +export async function getFilmstrip( + browsertimeData, + run, + dir, + options, + fullPath +) { + let doWeHaveFilmstrip = + options.browsertime.visualMetrics === true && + options.browsertime.videoParams.createFilmstrip === true; - if (doWeHaveFilmstrip === false) { - return []; - } - - const toTheFront = []; - - try { - let metrics = []; - if (browsertimeData) { - metrics = getMetricsFromBrowsertime(browsertimeData); - } - const files = await readdir( - path.join(dir, 'data', 'filmstrip', run + '') - ); - const timings = []; - for (let file of files) { - timings.push({ time: file.replace(/\D/g, ''), file }); - } - - const maxTiming = timings.slice(-1)[0].time; - - // We step 100 ms each step ... but if you wanna show all and the last change is late - // use 200 ms - const step = - maxTiming > 10000 && options.filmstrip && options.filmstrip.showAll - ? 200 - : 100; - let fileName = ''; - for (let i = 0; i <= Number(maxTiming) + step; i = i + step) { - const entry = findFrame(timings, i); - const timingMetrics = findTimings(metrics, i - step, i); - if ( - entry.file !== fileName || - timingMetrics.length > 0 || - (options.filmstrip && options.filmstrip.showAll) - ) { - toTheFront.push({ - time: i / 1000, - file: fullPath ? fullPath + entry.file : entry.file, - timings: timingMetrics - }); - } - fileName = entry.file; - } - } catch (e) { - log.info('Could not read filmstrip dir', e); - } - return toTheFront; + if (doWeHaveFilmstrip === false) { + return []; } -}; + + const toTheFront = []; + + try { + let metrics = []; + if (browsertimeData) { + metrics = getMetricsFromBrowsertime(browsertimeData); + } + const files = await readdir(join(dir, 'data', 'filmstrip', run + '')); + const timings = []; + for (let file of files) { + timings.push({ time: file.replace(/\D/g, ''), file }); + } + + const maxTiming = timings.slice(-1)[0].time; + + // We step 100 ms each step ... but if you wanna show all and the last change is late + // use 200 ms + const step = + maxTiming > 10_000 && options.filmstrip && options.filmstrip.showAll + ? 200 + : 100; + let fileName = ''; + for ( + let index = 0; + index <= Number(maxTiming) + step; + index = index + step + ) { + const entry = findFrame(timings, index); + const timingMetrics = findTimings(metrics, index - step, index); + if ( + entry.file !== fileName || + timingMetrics.length > 0 || + (options.filmstrip && options.filmstrip.showAll) + ) { + toTheFront.push({ + time: index / 1000, + file: fullPath ? fullPath + entry.file : entry.file, + timings: timingMetrics + }); + } + fileName = entry.file; + } + } catch (error) { + log.info('Could not read filmstrip dir', error); + } + return toTheFront; +} diff --git a/lib/plugins/browsertime/index.js b/lib/plugins/browsertime/index.js index 13dd17dbf..9560dd39d 100644 --- a/lib/plugins/browsertime/index.js +++ b/lib/plugins/browsertime/index.js @@ -1,35 +1,46 @@ -'use strict'; +import { parse } from 'node:url'; + +import { default as _merge } from 'lodash.merge'; + +import intel from 'intel'; +const log = intel.getLogger('plugin.browsertime'); + +import dayjs from 'dayjs'; +import isEmpty from 'lodash.isempty'; +import get from 'lodash.get'; +import { Stats } from 'fast-stats'; +import coach from 'coach-core'; +const { pickAPage, analyseHar, merge } = coach; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; + +import { summarizeStats } from '../../support/statsHelpers.js'; +import { analyzeUrl } from './analyzer.js'; + +import { BrowsertimeAggregator } from './browsertimeAggregator.js'; +import { metricsPageSummary as DEFAULT_METRICS_PAGE_SUMMARY } from './default/metricsPageSummary.js'; +import { metricsSummary as DEFAULT_METRICS_SUMMARY } from './default/metricsSummary.js'; +import { metricsRun as DEFAULT_METRICS_RUN } from './default/metricsRun.js'; +import { metricsRunLimited as DEFAULT_METRICS_RUN_LIMITED } from './default/metricsRunLimited.js'; +import { ConsoleLogAggregator } from './consoleLogAggregator.js'; +import { AxeAggregator } from './axeAggregator.js'; +import { getFilmstrip } from './filmstrip.js'; +import { getGzippedFileAsJson } from './reader.js'; +import { browsertimeDefaultSettings as defaultConfig } from './default/config.js'; -const aggregator = require('./aggregator'); -const api = require('coach-core'); -const log = require('intel').getLogger('plugin.browsertime'); -const merge = require('lodash.merge'); -const analyzer = require('./analyzer'); -const dayjs = require('dayjs'); -const isEmpty = require('lodash.isempty'); -const get = require('lodash.get'); -const defaultConfig = require('./default/config'); -const urlParser = require('url'); -const Stats = require('fast-stats').Stats; -const statsHelpers = require('../../support/statsHelpers'); const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; -const DEFAULT_METRICS_PAGE_SUMMARY = require('./default/metricsPageSummary'); -const DEFAULT_METRICS_SUMMARY = require('./default/metricsSummary'); -const DEFAULT_METRICS_RUN = require('./default/metricsRun'); -const DEFAULT_METRICS_RUN_LIMITED = require('./default/metricsRunLimited'); -const ConsoleLogAggregator = require('./consoleLogAggregator'); -const AxeAggregator = require('./axeAggregator'); -const filmstrip = require('./filmstrip'); -const { getGzippedFileAsJson } = require('./reader.js'); +export default class BrowsertimePlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'browsertime', options, context, queue }); + } + + concurrency = 1; -module.exports = { - concurrency: 1, open(context, options) { - this.make = context.messageMaker('browsertime').make; + // this.make = context.messageMaker('browsertime').make; this.useAxe = options.axe && options.axe.enable; - this.options = merge({}, defaultConfig, options.browsertime); + this.options = _merge({}, defaultConfig, options.browsertime); this.allOptions = options; - merge(this.options, { verbose: options.verbose, axe: options.axe }); + _merge(this.options, { verbose: options.verbose, axe: options.axe }); this.firstParty = options.firstParty; this.options.mobile = options.mobile; this.storageManager = context.storageManager; @@ -42,8 +53,10 @@ module.exports = { 'browsertime.screenshotParams.type', defaultConfig.screenshotParams.type ); + this.scriptOrMultiple = options.multi; this.allAlias = {}; + this.browsertimeAggregator = new BrowsertimeAggregator(); // hack for disabling viewport on Android that's not supported if ( @@ -69,11 +82,10 @@ module.exports = { 'browsertime.run' ); this.axeAggregatorTotal = new AxeAggregator(this.options); - }, - async processMessage(message, queue) { - const {configureLogging} = await import ('browsertime'); + } + async processMessage(message) { + const { configureLogging } = await import('browsertime'); configureLogging(this.options); - const make = this.make; const options = this.options; switch (message.type) { // When sistespeed.io starts, a setup messages is posted on the queue @@ -81,19 +93,17 @@ module.exports = { // to receive configuration case 'sitespeedio.setup': { // Let other plugins know that the browsertime plugin is alive - queue.postMessage(make('browsertime.setup')); + super.sendMessage('browsertime.setup'); // Unfify alias setup if (this.options.urlMetaData) { for (let url of Object.keys(this.options.urlMetaData)) { const alias = this.options.urlMetaData[url]; - const group = urlParser.parse(url).hostname; + const group = parse(url).hostname; this.allAlias[alias] = url; - queue.postMessage( - make('browsertime.alias', alias, { - url, - group - }) - ); + super.sendMessage('browsertime.alias', alias, { + url, + group + }); } } @@ -101,18 +111,16 @@ module.exports = { // what type of images that are used (so for exmaple the HTML pluin can create // correct links). if (options.screenshot) { - queue.postMessage( - make('browsertime.config', { - screenshot: true, - screenshotType: this.screenshotType - }) - ); + super.sendMessage('browsertime.config', { + screenshot: true, + screenshotType: this.screenshotType + }); } break; } // Another plugin sent configuration options to Browsertime case 'browsertime.config': { - merge(options, message.data); + _merge(options, message.data); break; } // Andother plugin got JavaScript that they want to run in Browsertime @@ -151,7 +159,7 @@ module.exports = { // it's used in BT when we record a video options.resultDir = await this.storageManager.getBaseDir(); const consoleLogAggregator = new ConsoleLogAggregator(options); - const result = await analyzer.analyzeUrl( + const result = await analyzeUrl( url, this.scriptOrMultiple, this.pluginScripts, @@ -159,28 +167,22 @@ module.exports = { options ); - // We need to check for alias first, since when we send the HAR (incliude all runs) + // We need to check for alias first, since when we send the HAR (include all runs) // need to know if alias exists, else we will end up with things like // https://github.com/sitespeedio/sitespeed.io/issues/2341 - for ( - let resultIndex = 0; - resultIndex < result.length; - resultIndex++ - ) { + for (const element of result) { // Browsertime supports alias for URLS in a script - const alias = result[resultIndex].info.alias; + const alias = element.info.alias; if (alias) { if (this.scriptOrMultiple) { - url = result[resultIndex].info.url; - group = urlParser.parse(url).hostname; + url = element.info.url; + group = parse(url).hostname; } this.allAlias[url] = alias; - queue.postMessage( - make('browsertime.alias', alias, { - url, - group - }) - ); + super.sendMessage('browsertime.alias', alias, { + url, + group + }); } } @@ -199,7 +201,7 @@ module.exports = { // multiple/scripting lets do it like this for now if (this.scriptOrMultiple) { url = result[resultIndex].info.url; - group = urlParser.parse(url).hostname; + group = parse(url).hostname; } let runIndex = 0; for (let run of result[resultIndex].browserScripts) { @@ -223,7 +225,7 @@ module.exports = { if (result.har.log._android) { testInfo.android = result.har.log._android; } - queue.postMessage(make('browsertime.browser', testInfo)); + super.sendMessage('browsertime.browser', testInfo); // Add meta data to be used when we compare multiple HARs // the meta field is added in Browsertime if (result.har.log.pages[harIndex]) { @@ -244,7 +246,7 @@ module.exports = { _meta.result = `${base}${runIndex + 1}.html`; if (options.video) { _meta.video = `${base}data/video/${runIndex + 1}.mp4`; - _meta.filmstrip = await filmstrip.getFilmstrip( + _meta.filmstrip = await getFilmstrip( run, `${runIndex + 1}`, `${ @@ -265,7 +267,7 @@ module.exports = { url ); } - run.har = api.pickAPage(result.har, harIndex); + run.har = pickAPage(result.har, harIndex); } else { // If we do not have a HAR, use browser info from the result if (result.length > 0) { @@ -275,39 +277,42 @@ module.exports = { version: result[0].info.browser.version } }; - queue.postMessage(make('browsertime.browser', testInfo)); + super.sendMessage('browsertime.browser', testInfo); } } // Hack to get axe data. In the future we can make this more generic - if (result[resultIndex].extras.length > 0) { - if (result[resultIndex].extras[runIndex].axe) { - const order = ['critical', 'serious', 'moderate', 'minor']; - result[resultIndex].extras[runIndex].axe.violations.sort( - (a, b) => order.indexOf(a.impact) > order.indexOf(b.impact) - ); + if ( + result[resultIndex].extras.length > 0 && + result[resultIndex].extras[runIndex].axe + ) { + const order = ['critical', 'serious', 'moderate', 'minor']; + result[resultIndex].extras[runIndex].axe.violations.sort( + (a, b) => order.indexOf(a.impact) > order.indexOf(b.impact) + ); - axeAggregatorPerURL.addStats( - result[resultIndex].extras[runIndex].axe - ); + axeAggregatorPerURL.addStats( + result[resultIndex].extras[runIndex].axe + ); - this.axeAggregatorTotal.addStats( - result[resultIndex].extras[runIndex].axe - ); + this.axeAggregatorTotal.addStats( + result[resultIndex].extras[runIndex].axe + ); - queue.postMessage( - make('axe.run', result[resultIndex].extras[runIndex].axe, { - url, - group, - runIndex, - iteration: runIndex + 1 - }) - ); - // Another hack: Browsertime automatically creates statistics for alla data in extras - // but we don't really need that for AXE. - delete result[resultIndex].extras[runIndex].axe; - delete result[resultIndex].statistics.extras.axe; - } + super.sendMessage( + 'axe.run', + result[resultIndex].extras[runIndex].axe, + { + url, + group, + runIndex, + iteration: runIndex + 1 + } + ); + // Another hack: Browsertime automatically creates statistics for alla data in extras + // but we don't really need that for AXE. + delete result[resultIndex].extras[runIndex].axe; + delete result[resultIndex].statistics.extras.axe; } if (result[resultIndex].cpu) { run.cpu = result[resultIndex].cpu[runIndex]; @@ -360,7 +365,6 @@ module.exports = { // The packaging of screenshots from browsertime // Is not optimal, the same array of screenshots hold all // screenshots from one run (the automatic ones and user generated) - // If we only test one page per run, take all screenshots (user generated etc) if (result.length === 1) { run.screenshots = @@ -372,12 +376,12 @@ module.exports = { runIndex ]) { if ( - screenshot.indexOf( + screenshot.includes( `${this.resultUrls.relativeSummaryPageUrl( url, this.allAlias[url] )}data` - ) > -1 + ) ) { run.screenshots.push(screenshot); } @@ -391,15 +395,13 @@ module.exports = { errorStats.push(error.length); } - queue.postMessage( - make('browsertime.run', run, { - url, - group, - runIndex, - runTime: run.timestamp, - iteration: runIndex + 1 - }) - ); + super.sendMessage('browsertime.run', run, { + url, + group, + runIndex, + runTime: run.timestamp, + iteration: runIndex + 1 + }); if ( options.chrome && @@ -412,15 +414,13 @@ module.exports = { result[resultIndex].files.consoleLog[runIndex] ); - queue.postMessage( - make('browsertime.console', consoleData, { - url, - group, - runIndex, - iteration: runIndex + 1 - }) - ); - } catch (e) { + super.sendMessage('browsertime.console', consoleData, { + url, + group, + runIndex, + iteration: runIndex + 1 + }); + } catch { // This could happen if the run failed somehow log.error('Could not fetch the console log'); } @@ -438,14 +438,12 @@ module.exports = { options.resultDir, `trace-${runIndex + 1}.json.gz` ); - queue.postMessage( - make('browsertime.chrometrace', traceData, { - url, - group, - name: `trace-${runIndex + 1}.json`, // backward compatible to 2.x - runIndex - }) - ); + super.sendMessage('browsertime.chrometrace', traceData, { + url, + group, + name: `trace-${runIndex + 1}.json`, + runIndex + }); } // If the coach is turned on, collect the coach result @@ -458,17 +456,15 @@ module.exports = { url, coachAdvice.errors ); - queue.postMessage( - make( - 'error', - 'The coach got the following errors: ' + - JSON.stringify(coachAdvice.errors), - { - url, - runIndex, - iteration: runIndex + 1 - } - ) + super.sendMessage( + 'error', + 'The coach got the following errors: ' + + JSON.stringify(coachAdvice.errors), + { + url, + runIndex, + iteration: runIndex + 1 + } ); } @@ -476,26 +472,25 @@ module.exports = { // If we run without HAR if (result.har) { // make sure to get the right run in the HAR - const myHar = api.pickAPage(result.har, harIndex); - const harResult = await api.analyseHar( + const myHar = pickAPage(result.har, harIndex); + + const harResult = await analyseHar( myHar, undefined, coachAdvice, options ); - advice = api.merge(coachAdvice, harResult); + advice = merge(coachAdvice, harResult); } - queue.postMessage( - make('coach.run', advice, { - url, - group, - runIndex, - iteration: runIndex + 1 - }) - ); + super.sendMessage('coach.run', advice, { + url, + group, + runIndex, + iteration: runIndex + 1 + }); } - aggregator.addToAggregate(run, group); + this.browsertimeAggregator.addToAggregate(run, group); runIndex++; } @@ -509,34 +504,31 @@ module.exports = { consoleLogAggregator.summarizeStats(); } - result[resultIndex].statistics.errors = - statsHelpers.summarizeStats(errorStats); + result[resultIndex].statistics.errors = summarizeStats(errorStats); // Post the result on the queue so other plugins can use it - queue.postMessage( - make('browsertime.pageSummary', result[resultIndex], { - url, - group, - runTime: result.timestamp - }) - ); + super.sendMessage('browsertime.pageSummary', result[resultIndex], { + url, + group, + runTime: result.timestamp + }); // Post the HAR on the queue so other plugins can use it if (result.har) { - queue.postMessage( - make('browsertime.har', result.har, { - url, - group - }) - ); + super.sendMessage('browsertime.har', result.har, { + url, + group + }); } // Post the result on the queue so other plugins can use it if (this.useAxe) { - queue.postMessage( - make('axe.pageSummary', axeAggregatorPerURL.summarizeStats(), { + super.sendMessage( + 'axe.pageSummary', + axeAggregatorPerURL.summarizeStats(), + { url, group - }) + } ); } @@ -544,13 +536,13 @@ module.exports = { // [[],[],[]] where one iteration can have multiple errors for (let errorsForOneIteration of result[resultIndex].errors) { for (let error of errorsForOneIteration) { - queue.postMessage(make('error', error, merge({ url }))); + super.sendMessage('error', error, _merge({ url })); } } } break; } catch (error) { - queue.postMessage(make('error', error, merge({ url }))); + super.sendMessage('error', error, _merge({ url })); log.error('Caught error from Browsertime', error); break; } @@ -559,26 +551,29 @@ module.exports = { // and post the summary on the queue case 'sitespeedio.summarize': { log.debug('Generate summary metrics from Browsertime'); - const summary = aggregator.summarize(); + const summary = this.browsertimeAggregator.summarize(); if (summary) { for (let group of Object.keys(summary.groups)) { - queue.postMessage( - make('browsertime.summary', summary.groups[group], { group }) - ); + super.sendMessage('browsertime.summary', summary.groups[group], { + group + }); } } if (this.useAxe) { - queue.postMessage( - make('axe.summary', this.axeAggregatorTotal.summarizeStats(), { + super.sendMessage( + 'axe.summary', + this.axeAggregatorTotal.summarizeStats(), + { group: 'total' - }) + } ); } break; } } - }, - config: defaultConfig -}; + } +} + +export { browsertimeDefaultSettings as config } from './default/config.js'; diff --git a/lib/plugins/browsertime/reader.js b/lib/plugins/browsertime/reader.js index 724c97841..b13601121 100644 --- a/lib/plugins/browsertime/reader.js +++ b/lib/plugins/browsertime/reader.js @@ -1,9 +1,8 @@ -'use strict'; -const fs = require('fs'); -const path = require('path'); -const zlib = require('zlib'); -const { promisify } = require('util'); -const gunzip = promisify(zlib.gunzip); +import { createReadStream } from 'node:fs'; +import { join } from 'node:path'; +import { gunzip as _gunzip } from 'node:zlib'; +import { promisify } from 'node:util'; +const gunzip = promisify(_gunzip); async function streamToString(stream) { return new Promise((resolve, reject) => { @@ -14,11 +13,9 @@ async function streamToString(stream) { }); } -module.exports = { - async getGzippedFileAsJson(dir, file) { - const readStream = fs.createReadStream(path.join(dir, file)); - const text = await streamToString(readStream); - const unzipped = await gunzip(text); - return JSON.parse(unzipped.toString()); - } -}; +export async function getGzippedFileAsJson(dir, file) { + const readStream = createReadStream(join(dir, file)); + const text = await streamToString(readStream); + const unzipped = await gunzip(text); + return JSON.parse(unzipped.toString()); +} diff --git a/lib/plugins/budget/deprecatedVerify.js b/lib/plugins/budget/deprecatedVerify.js index 4f41de6f6..39c31706a 100644 --- a/lib/plugins/budget/deprecatedVerify.js +++ b/lib/plugins/budget/deprecatedVerify.js @@ -1,12 +1,10 @@ -'use strict'; - /** * The old verificatuion and budget.json was deprecated in 8.0 */ -const get = require('lodash.get'); -const noop = require('../../support/helpers').noop; -const size = require('../../support/helpers').size.format; -const log = require('intel').getLogger('sitespeedio.plugin.budget'); +import get from 'lodash.get'; +import { noop, size } from '../../support/helpers/index.js'; +import intel from 'intel'; +const log = intel.getLogger('sitespeedio.plugin.budget'); function getItem(url, type, metric, value, limit, limitType) { return { @@ -20,71 +18,66 @@ function getItem(url, type, metric, value, limit, limitType) { } function getHelperFunction(metric) { - if ( - metric.indexOf('transferSize') > -1 || - metric.indexOf('contentSize') > -1 - ) { - return size; - } else if (metric.indexOf('timings') > -1) { + if (metric.includes('transferSize') || metric.includes('contentSize')) { + return size.format; + } else if (metric.includes('timings')) { return function (time) { return time + ' ms'; }; } else return noop; } -module.exports = { - verify(message, result, budgets) { - const failing = []; - const working = []; - // do we have an entry in the budget for this kind of message? - if (budgets[message.type]) { - for (var budget of budgets[message.type]) { - let value = get(message.data, budget.metric); +export function verify(message, result, budgets) { + const failing = []; + const working = []; + // do we have an entry in the budget for this kind of message? + if (budgets[message.type]) { + for (var budget of budgets[message.type]) { + let value = get(message.data, budget.metric); - if (value !== undefined) { - const format = getHelperFunction(budget.metric); + if (value !== undefined) { + const format = getHelperFunction(budget.metric); - const item = getItem( - message.url, - message.type, - budget.metric, - format(value), - budget.max !== undefined ? format(budget.max) : format(budget.min), - budget.max !== undefined ? 'max' : 'min' - ); + const item = getItem( + message.url, + message.type, + budget.metric, + format(value), + budget.max !== undefined ? format(budget.max) : format(budget.min), + budget.max !== undefined ? 'max' : 'min' + ); - if (budget.max !== undefined) { - if (value <= budget.max) { - working.push(item); - } else { - item.status = 'failing'; - failing.push(item); - } + if (budget.max !== undefined) { + if (value <= budget.max) { + working.push(item); } else { - if (value >= budget.min) { - working.push(item); - } else { - item.status = 'failing'; - failing.push(item); - } + item.status = 'failing'; + failing.push(item); } } else { - log.debug('Missing data for budget metric ' + budget.metric); + if (value >= budget.min) { + working.push(item); + } else { + item.status = 'failing'; + failing.push(item); + } } + } else { + log.debug('Missing data for budget metric ' + budget.metric); } } - - // group working/failing per URL - if (failing.length > 0) { - result.failing[message.url] = result.failing[message.url] - ? result.failing[message.url].concat(failing) - : failing; - } - - if (working.length > 0) { - result.working[message.url] = result.working[message.url] - ? result.working[message.url].concat(working) - : working; - } } -}; + + // group working/failing per URL + if (failing.length > 0) { + result.failing[message.url] = result.failing[message.url] + ? [...result.failing[message.url], ...failing] + : failing; + } + + if (working.length > 0) { + result.working[message.url] = result.working[message.url] + ? [...result.working[message.url], ...working] + : working; + } +} diff --git a/lib/plugins/budget/index.js b/lib/plugins/budget/index.js index 87c1a922b..3e4c5047c 100644 --- a/lib/plugins/budget/index.js +++ b/lib/plugins/budget/index.js @@ -1,13 +1,18 @@ -'use strict'; +import intel from 'intel'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +import { verify as deprecatedVerify } from './deprecatedVerify.js'; +import { verify } from './verify.js'; +import { writeTap } from './tap.js'; +import { writeJunit } from './junit.js'; +import { writeJson } from './json.js'; -const deprecatedVerify = require('./deprecatedVerify').verify; -const verify = require('./verify').verify; -const tap = require('./tap'); -const junit = require('./junit'); -const json = require('./json'); -const log = require('intel').getLogger('sitespeedio.plugin.budget'); +const log = intel.getLogger('sitespeedio.plugin.budget'); + +export default class BudgetPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'budget', options, context, queue }); + } -module.exports = { open(context, options) { this.options = options; this.budgetOptions = options.budget || {}; @@ -22,7 +27,7 @@ module.exports = { 'coach.pageSummary', 'axe.pageSummary' ]; - }, + } processMessage(message, queue) { // if there's no configured budget do nothing if (!this.options.budget) { @@ -35,7 +40,7 @@ module.exports = { } const budget = this.options.budget.config; - if (this.budgetTypes.indexOf(message.type) > -1) { + if (this.budgetTypes.includes(message.type)) { // If it doesn't have the new structure of a budget file // use the old verdion if (!budget.budget) { @@ -102,20 +107,30 @@ module.exports = { case 'sitespeedio.render': { if (this.options.budget) { - if (this.options.budget.output === 'json') { - json.writeJson(this.result, this.storageManager.getBaseDir()); - } else if (this.options.budget.output === 'tap') { - tap.writeTap(this.result, this.storageManager.getBaseDir()); - } else if (this.options.budget.output === 'junit') { - junit.writeJunit( - this.result, - this.storageManager.getBaseDir(), - this.options - ); + switch (this.options.budget.output) { + case 'json': { + writeJson(this.result, this.storageManager.getBaseDir()); + + break; + } + case 'tap': { + writeTap(this.result, this.storageManager.getBaseDir()); + + break; + } + case 'junit': { + writeJunit( + this.result, + this.storageManager.getBaseDir(), + this.options + ); + + break; + } } } } } } } -}; +} diff --git a/lib/plugins/budget/json.js b/lib/plugins/budget/json.js index f5c606d18..cb08a32df 100644 --- a/lib/plugins/budget/json.js +++ b/lib/plugins/budget/json.js @@ -1,11 +1,11 @@ -'use strict'; +import { writeFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; -const fs = require('fs'), - log = require('intel').getLogger('sitespeedio.plugin.budget'), - path = require('path'); +import intel from 'intel'; +const log = intel.getLogger('sitespeedio.plugin.budget'); -exports.writeJson = function (results, dir) { - const file = path.join(dir, 'budgetResult.json'); - log.info('Write budget to %s', path.resolve(file)); - fs.writeFileSync(file, JSON.stringify(results, null, 2)); -}; +export function writeJson(results, dir) { + const file = join(dir, 'budgetResult.json'); + log.info('Write budget to %s', resolve(file)); + writeFileSync(file, JSON.stringify(results, undefined, 2)); +} diff --git a/lib/plugins/budget/junit.js b/lib/plugins/budget/junit.js index c163e2fd2..235da0ed7 100644 --- a/lib/plugins/budget/junit.js +++ b/lib/plugins/budget/junit.js @@ -1,12 +1,14 @@ -'use strict'; +import { join, resolve } from 'node:path'; +import { parse } from 'node:url'; -const builder = require('junit-report-builder'), - urlParser = require('url'), - log = require('intel').getLogger('sitespeedio.plugin.budget'), - path = require('path'), - merge = require('lodash.merge'); +import jrp from 'junit-report-builder'; -exports.writeJunit = function (results, dir, options) { +import intel from 'intel'; +const log = intel.getLogger('sitespeedio.plugin.budget'); + +import merge from 'lodash.merge'; + +export function writeJunit(results, dir, options) { // lets have one suite per URL const urls = Object.keys(merge({}, results.failing, results.working)); @@ -14,20 +16,16 @@ exports.writeJunit = function (results, dir, options) { // The URL can be an alias let name = url; if (url.startsWith('http')) { - const parsedUrl = urlParser.parse(url); + const parsedUrl = parse(url); name = url.startsWith('http') ? url : url; parsedUrl.hostname.replace(/\./g, '_') + '.' + parsedUrl.path.replace(/\./g, '_').replace(/\//g, '_'); } - const suite = builder + const suite = jrp .testSuite() - .name( - options.budget.friendlyName - ? options.budget.friendlyName - : 'sitespeed.io' + '.' + name - ); + .name(options.budget.friendlyName || 'sitespeed.io' + '.' + name); if (results.failing[url]) { for (const result of results.failing[url]) { @@ -65,7 +63,7 @@ exports.writeJunit = function (results, dir, options) { } } } - const file = path.join(dir, 'junit.xml'); - log.info('Write junit budget to %s', path.resolve(file)); - builder.writeTo(file); -}; + const file = join(dir, 'junit.xml'); + log.info('Write junit budget to %s', resolve(file)); + jrp.writeTo(file); +} diff --git a/lib/plugins/budget/tap.js b/lib/plugins/budget/tap.js index ea66a71ba..1b45dac78 100644 --- a/lib/plugins/budget/tap.js +++ b/lib/plugins/budget/tap.js @@ -1,12 +1,11 @@ -'use strict'; +import path from 'node:path'; +import fs from 'node:fs'; +import { EOL } from 'node:os'; +import tap from 'tape'; +import intel from 'intel'; +const log = intel.getLogger('sitespeedio.plugin.budget'); -const tap = require('tape'), - fs = require('fs'), - log = require('intel').getLogger('sitespeedio.plugin.budget'), - path = require('path'), - EOL = require('os').EOL; - -exports.writeTap = function (results, dir) { +export function writeTap(results, dir) { const file = path.join(dir, 'budget.tap'); log.info('Write budget to %s', path.resolve(file)); const tapOutput = fs.createWriteStream(file); @@ -34,4 +33,4 @@ exports.writeTap = function (results, dir) { } } } -}; +} diff --git a/lib/plugins/budget/verify.js b/lib/plugins/budget/verify.js index fe22a3877..dfa23e702 100644 --- a/lib/plugins/budget/verify.js +++ b/lib/plugins/budget/verify.js @@ -1,13 +1,12 @@ -'use strict'; - /** * The old verificatuion and budget.json was deprecated in 8.0 */ -const get = require('lodash.get'); -const merge = require('lodash.merge'); -const log = require('intel').getLogger('sitespeedio.plugin.budget'); -const friendlyNames = require('../../support/friendlynames'); -const time = require('../../support/helpers/time'); +import get from 'lodash.get'; +import merge from 'lodash.merge'; +import intel from 'intel'; +const log = intel.getLogger('sitespeedio.plugin.budget'); +import friendlyNames from '../../support/friendlynames.js'; +import { time } from '../../support/helpers/time.js'; function getItem(friendlyName, type, metric, value, limit, limitType) { return { @@ -22,163 +21,156 @@ function getItem(friendlyName, type, metric, value, limit, limitType) { }; } -module.exports = { - verify(message, result, budgets, alias) { - // Let us merge the specific configuration for this URL together - // with the generic one. In the future we can fine tune this, since - // we keep all metrics within a specific URL +export function verify(message, result, budgets, alias) { + // Let us merge the specific configuration for this URL together + // with the generic one. In the future we can fine tune this, since + // we keep all metrics within a specific URL + // If we have a match for the alias, use that first, if not use the URL and + // then the specific one + let budgetSetupSpecificForThisURL; + if (alias) { + budgetSetupSpecificForThisURL = get(budgets.budget, alias); + } + if (!budgetSetupSpecificForThisURL) { + budgetSetupSpecificForThisURL = get(budgets.budget, message.url); + } + const budgetForThisURL = {}; + merge(budgetForThisURL, budgets.budget, budgetSetupSpecificForThisURL); - // If we have a match for the alias, use that first, if not use the URL and - // then the specific one - let budgetSetupSpecificForThisURL; - if (alias) { - budgetSetupSpecificForThisURL = get(budgets.budget, alias); - } - if (!budgetSetupSpecificForThisURL) { - budgetSetupSpecificForThisURL = get(budgets.budget, message.url); - } - const budgetForThisURL = {}; - merge(budgetForThisURL, budgets.budget, budgetSetupSpecificForThisURL); - - // Keep failing/working metrics here - const failing = []; - const working = []; - const url = message.url; - log.verbose('Applying budget to url %s', url); - const tool = message.type.split('.')[0]; - // Go through all metrics that are configured - // timing, request, coach etc - for (let metricType of Object.keys(budgetForThisURL)) { - for (let metric of Object.keys(budgetForThisURL[metricType])) { - if (friendlyNames[tool][metricType]) { - if (!friendlyNames[tool][metricType][metric]) { - log.error( - `It seems like you configure a metric ${metric} that we do not have a friendly name. Please check the docs if it is right.` + // Keep failing/working metrics here + const failing = []; + const working = []; + const url = message.url; + log.verbose('Applying budget to url %s', url); + const tool = message.type.split('.')[0]; + // Go through all metrics that are configured + // timing, request, coach etc + for (let metricType of Object.keys(budgetForThisURL)) { + for (let metric of Object.keys(budgetForThisURL[metricType])) { + if (friendlyNames[tool][metricType]) { + if (!friendlyNames[tool][metricType][metric]) { + log.error( + `It seems like you configure a metric ${metric} that we do not have a friendly name. Please check the docs if it is right.` + ); + } + const fullPath = friendlyNames[tool][metricType][metric].path; + let value = get(message.data, fullPath); + if (value && message.type === 'lighthouse.pageSummary') { + value = value * 100; // The score in Lighthouse is 0-1 + } else if ( + value && + message.type === 'pagexray.pageSummary' && + metricType === 'httpErrors' + ) { + // count number of http server error + value = Object.keys(value).filter(code => code > 399).length; + } + // We got a matching metric + if (value !== undefined) { + const budgetValue = budgetForThisURL[metricType][metric]; + const item = getItem( + friendlyNames[tool][metricType][metric], + metricType, + metric, + value, + budgetValue, + tool === 'coach' || tool === 'lighthouse' || tool === 'gpsi' + ? 'min' + : 'max' + ); + if (tool === 'coach' || tool === 'lighthouse' || tool === 'gpsi') { + if (value < budgetValue) { + item.status = 'failing'; + failing.push(item); + } else { + working.push(item); + } + } else { + if (value > budgetValue) { + item.status = 'failing'; + failing.push(item); + } else { + working.push(item); + } + } + } else { + log.debug('Missing data for budget metric ' + metric); + } + } else { + if (metricType === 'usertimings' && tool === 'browsertime') { + const budgetValue = budgetForThisURL[metricType][metric]; + let value = + get( + message.data, + 'statistics.timings.userTimings.marks.' + metric + '.median' + ) || + get( + message.data, + 'statistics.timings.userTimings.measures.' + metric + '.median' ); - } - const fullPath = friendlyNames[tool][metricType][metric].path; - let value = get(message.data, fullPath); - if (value && message.type === 'lighthouse.pageSummary') { - value = value * 100; // The score in Lighthouse is 0-1 - } else if ( - value && - message.type === 'pagexray.pageSummary' && - metricType === 'httpErrors' - ) { - // count number of http server error - value = Object.keys(value).filter(code => code > 399).length; - } - // We got a matching metric - if (value !== undefined) { - const budgetValue = budgetForThisURL[metricType][metric]; + if (value) { const item = getItem( - friendlyNames[tool][metricType][metric], + { name: metric, format: time.ms }, metricType, metric, value, budgetValue, - tool === 'coach' || tool === 'lighthouse' || tool === 'gpsi' - ? 'min' - : 'max' + 'max' ); - if (tool === 'coach' || tool === 'lighthouse' || tool === 'gpsi') { - if (value < budgetValue) { - item.status = 'failing'; - failing.push(item); - } else { - working.push(item); - } + if (value > budgetValue) { + item.status = 'failing'; + failing.push(item); } else { - if (value > budgetValue) { - item.status = 'failing'; - failing.push(item); - } else { - working.push(item); - } + working.push(item); } + continue; } else { - log.debug('Missing data for budget metric ' + metric); + log.error(`Could not find the user timing ${metric}`); + continue; } - } else { - if (metricType === 'usertimings' && tool === 'browsertime') { - const budgetValue = budgetForThisURL[metricType][metric]; - let value = - get( - message.data, - 'statistics.timings.userTimings.marks.' + metric + '.median' - ) || - get( - message.data, - 'statistics.timings.userTimings.measures.' + metric + '.median' - ); - if (value) { - const item = getItem( - { name: metric, format: time.ms }, - metricType, - metric, - value, - budgetValue, - 'max' - ); - if (value > budgetValue) { - item.status = 'failing'; - failing.push(item); - } else { - working.push(item); - } - continue; - } else { - log.error(`Could not find the user timing ${metric}`); - continue; - } - } else if ( - metricType === 'scriptingmetrics' && - tool === 'browsertime' - ) { - const budgetValue = budgetForThisURL[metricType][metric]; - const value = get( - message.data, - 'statistics.extras.' + metric + '.median' + } else if ( + metricType === 'scriptingmetrics' && + tool === 'browsertime' + ) { + const budgetValue = budgetForThisURL[metricType][metric]; + const value = get( + message.data, + 'statistics.extras.' + metric + '.median' + ); + if (value) { + const item = getItem( + { name: metric, format: time.ms }, + metricType, + metric, + value, + budgetValue, + 'max' ); - if (value) { - const item = getItem( - { name: metric, format: time.ms }, - metricType, - metric, - value, - budgetValue, - 'max' - ); - if (value > budgetValue) { - item.status = 'failing'; - failing.push(item); - } else { - working.push(item); - } - continue; + if (value > budgetValue) { + item.status = 'failing'; + failing.push(item); } else { - log.error(`Could not find the scripting metric ${metric}`); - continue; + working.push(item); } + continue; + } else { + log.error(`Could not find the scripting metric ${metric}`); + continue; } } } } - // group working/failing per URL - if (failing.length > 0) { - result.failing[alias || message.url] = result.failing[ - alias || message.url - ] - ? result.failing[alias || message.url].concat(failing) - : failing; - } - - if (working.length > 0) { - result.working[alias || message.url] = result.working[ - alias || message.url - ] - ? result.working[alias || message.url].concat(working) - : working; - } } -}; + // group working/failing per URL + if (failing.length > 0) { + result.failing[alias || message.url] = result.failing[alias || message.url] + ? [...result.failing[alias || message.url], ...failing] + : failing; + } + + if (working.length > 0) { + result.working[alias || message.url] = result.working[alias || message.url] + ? [...result.working[alias || message.url], ...working] + : working; + } +} diff --git a/lib/plugins/coach/aggregator.js b/lib/plugins/coach/aggregator.js index 726c509e7..ad74362bb 100644 --- a/lib/plugins/coach/aggregator.js +++ b/lib/plugins/coach/aggregator.js @@ -1,11 +1,11 @@ -'use strict'; +import forEach from 'lodash.foreach'; +import { pushGroupStats, setStatsSummary } from '../../support/statsHelpers.js'; -const forEach = require('lodash.foreach'), - statsHelpers = require('../../support/statsHelpers'); - -module.exports = { - statsPerCategory: {}, - groups: {}, +export class CoachAggregator { + constructor() { + this.statsPerCategory = {}; + this.groups = {}; + } addToAggregate(coachData, group) { if (this.groups[group] === undefined) { @@ -13,7 +13,7 @@ module.exports = { } // push the total score - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerCategory, this.groups[group], 'score', @@ -25,7 +25,7 @@ module.exports = { return; } // Push the score per category - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerCategory, this.groups[group], [categoryName, 'score'], @@ -33,7 +33,7 @@ module.exports = { ); forEach(category.adviceList, (advice, adviceName) => { - statsHelpers.pushGroupStats( + pushGroupStats( this.statsPerCategory, this.groups[group], [categoryName, adviceName], @@ -41,10 +41,10 @@ module.exports = { ); }); }); - }, + } summarize() { if (Object.keys(this.statsPerCategory).length === 0) { - return undefined; + return; } const summary = { @@ -57,16 +57,16 @@ module.exports = { summary.groups[group] = this.summarizePerObject(this.groups[group]); } return summary; - }, + } summarizePerObject(type) { return Object.keys(type).reduce((summary, categoryName) => { if (categoryName === 'score') { - statsHelpers.setStatsSummary(summary, 'score', type[categoryName]); + setStatsSummary(summary, 'score', type[categoryName]); } else { const categoryData = {}; forEach(type[categoryName], (stats, name) => { - statsHelpers.setStatsSummary(categoryData, name, stats); + setStatsSummary(categoryData, name, stats); }); summary[categoryName] = categoryData; @@ -75,4 +75,4 @@ module.exports = { return summary; }, {}); } -}; +} diff --git a/lib/plugins/coach/index.js b/lib/plugins/coach/index.js index a979da232..8b9a8c827 100644 --- a/lib/plugins/coach/index.js +++ b/lib/plugins/coach/index.js @@ -1,6 +1,7 @@ -'use strict'; -const aggregator = require('./aggregator'); -const log = require('intel').getLogger('plugin.coach'); +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +import { CoachAggregator } from './aggregator.js'; +import intel from 'intel'; +const log = intel.getLogger('plugin.coach'); const DEFAULT_METRICS_RUN = []; @@ -24,10 +25,15 @@ const DEFAULT_METRICS_PAGESUMMARY = [ 'advice.info.localStorageSize' ]; -module.exports = { +export default class CoachPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'coach', options, context, queue }); + } + open(context, options) { this.options = options; this.make = context.messageMaker('coach').make; + this.coachAggregator = new CoachAggregator(); context.filterRegistry.registerFilterForType( DEFAULT_METRICS_SUMMARY, @@ -41,7 +47,7 @@ module.exports = { DEFAULT_METRICS_PAGESUMMARY, 'coach.pageSummary' ); - }, + } processMessage(message, queue) { const make = this.make; switch (message.type) { @@ -60,13 +66,13 @@ module.exports = { ); } - aggregator.addToAggregate(message.data, message.group); + this.coachAggregator.addToAggregate(message.data, message.group); break; } case 'sitespeedio.summarize': { log.debug('Generate summary metrics from the Coach'); - let summary = aggregator.summarize(); + let summary = this.coachAggregator.summarize(); if (summary) { for (let group of Object.keys(summary.groups)) { queue.postMessage( @@ -78,4 +84,4 @@ module.exports = { } } } -}; +} diff --git a/lib/plugins/crawler/index.js b/lib/plugins/crawler/index.js index f111ea17b..4ea0f6e96 100644 --- a/lib/plugins/crawler/index.js +++ b/lib/plugins/crawler/index.js @@ -1,17 +1,22 @@ -'use strict'; +import { extname } from 'node:path'; +import merge from 'lodash.merge'; +import intel from 'intel'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; -const path = require('path'); -const merge = require('lodash.merge'); -const log = require('intel').getLogger('sitespeedio.plugin.crawler'); -const Crawler = require('simplecrawler'); -const throwIfMissing = require('../../support/util').throwIfMissing; -const toArray = require('../../support/util').toArray; +const log = intel.getLogger('sitespeedio.plugin.crawler'); +import Crawler from 'simplecrawler'; +import { throwIfMissing } from '../../support/util'; +import { toArray } from '../../support/util'; const defaultOptions = { depth: 3 }; -module.exports = { +export default class CrawlerPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'crawler', options, context, queue }); + } + open(context, options) { throwIfMissing(options.crawler, ['depth'], 'crawler'); this.options = merge({}, defaultOptions, options.crawler); @@ -22,10 +27,9 @@ module.exports = { this.basicAuth = options.browsertime ? options.browsertime.basicAuth : undefined; - this.cookie = options.browsertime.cookie - ? options.browsertime.cookie - : undefined; - }, + this.cookie = options.browsertime.cookie || undefined; + } + processMessage(message, queue) { const make = this.make; if (message.type === 'url' && message.source !== 'crawler') { @@ -66,9 +70,9 @@ module.exports = { } crawler.addFetchCondition(queueItem => { - const extension = path.extname(queueItem.path); + const extension = extname(queueItem.path); // Don't try to download these, based on file name. - if (['png', 'jpg', 'gif', 'pdf'].indexOf(extension) !== -1) { + if (['png', 'jpg', 'gif', 'pdf'].includes(extension)) { return false; } @@ -112,8 +116,11 @@ module.exports = { crawler.userAgent = this.userAgent; } - crawler.on('fetchconditionerror', (queueItem, err) => { - log.warn('An error occurred in the fetchCondition callback: %s', err); + crawler.on('fetchconditionerror', (queueItem, error) => { + log.warn( + 'An error occurred in the fetchCondition callback: %s', + error + ); }); crawler.on('fetchredirect', (queueItem, parsedURL, response) => { @@ -157,4 +164,4 @@ module.exports = { }); } } -}; +} diff --git a/lib/plugins/crux/cli.js b/lib/plugins/crux/cli.js deleted file mode 100644 index 8960336c5..000000000 --- a/lib/plugins/crux/cli.js +++ /dev/null @@ -1,29 +0,0 @@ -module.exports = { - key: { - describe: - 'You need to use a key to get data from CrUx. Get the key from https://developers.google.com/web/tools/chrome-user-experience-report/api/guides/getting-started#APIKey', - group: 'CrUx' - }, - enable: { - default: true, - describe: - 'Enable the CrUx plugin. This is on by defauly but you also need the Crux key. If you chose to disable it with this key, set this to false and you can still use the CrUx key in your configuration.', - group: 'CrUx' - }, - formFactor: { - default: 'ALL', - type: 'string', - choices: ['ALL', 'DESKTOP', 'PHONE', 'TABLET'], - describe: - 'A form factor is the type of device on which a user visits a website.', - group: 'CrUx' - }, - collect: { - default: 'ALL', - type: 'string', - choices: ['ALL', 'URL', 'ORIGIN'], - describe: - 'Choose what data to collect. URL is data for a specific URL, ORIGIN for the domain and ALL for both of them', - group: 'CrUx' - } -}; diff --git a/lib/plugins/crux/index.js b/lib/plugins/crux/index.js index e7e0e5463..9d12c4465 100644 --- a/lib/plugins/crux/index.js +++ b/lib/plugins/crux/index.js @@ -1,14 +1,19 @@ -'use strict'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import intel from 'intel'; +import merge from 'lodash.merge'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; + +import { throwIfMissing } from '../../support/util.js'; + +import { repackage } from './repackage.js'; +import { send } from './send.js'; +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const log = intel.getLogger('plugin.crux'); const defaultConfig = {}; -const log = require('intel').getLogger('plugin.crux'); -const merge = require('lodash.merge'); -const throwIfMissing = require('../../support/util').throwIfMissing; -const cliUtil = require('../../cli/util'); -const send = require('./send'); -const path = require('path'); -const repackage = require('./repackage'); -const fs = require('fs'); const DEFAULT_METRICS_PAGESUMMARY = [ 'loadingExperience.*.FIRST_CONTENTFUL_PAINT_MS.*', @@ -33,10 +38,11 @@ function wait(ms) { const CRUX_WAIT_TIME = 300; -module.exports = { - name() { - return path.basename(__dirname); - }, +export default class CruxPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'crux', options, context, queue }); + } + open(context, options) { this.make = context.messageMaker('crux').make; this.options = merge({}, defaultConfig, options.crux); @@ -46,10 +52,7 @@ module.exports = { this.formFactors = Array.isArray(this.options.formFactor) ? this.options.formFactor : [this.options.formFactor]; - this.pug = fs.readFileSync( - path.resolve(__dirname, 'pug', 'index.pug'), - 'utf8' - ); + this.pug = readFileSync(resolve(__dirname, 'pug', 'index.pug'), 'utf8'); if (this.options.collect === 'ALL' || this.options.collect === 'URL') { context.filterRegistry.registerFilterForType( @@ -63,7 +66,7 @@ module.exports = { 'crux.summary' ); } - }, + } async processMessage(message, queue) { if (this.options.enable === true) { const make = this.make; @@ -101,7 +104,7 @@ module.exports = { this.testedOrigins[group] = true; log.info(`Get CrUx data for domain ${group}`); for (let formFactor of this.formFactors) { - originResult.originLoadingExperience[formFactor] = await send.get( + originResult.originLoadingExperience[formFactor] = await send( url, this.options.key, formFactor, @@ -127,7 +130,7 @@ module.exports = { originResult.originLoadingExperience[formFactor] = repackage( originResult.originLoadingExperience[formFactor] ); - } catch (e) { + } catch { log.error( 'Could not repackage the JSON for origin from CrUx, is it broken? %j', originResult.originLoadingExperience[formFactor] @@ -146,7 +149,7 @@ module.exports = { log.info(`Get CrUx data for url ${url}`); const urlResult = { loadingExperience: {} }; for (let formFactor of this.formFactors) { - urlResult.loadingExperience[formFactor] = await send.get( + urlResult.loadingExperience[formFactor] = await send( url, this.options.key, formFactor, @@ -172,7 +175,7 @@ module.exports = { urlResult.loadingExperience[formFactor] = repackage( urlResult.loadingExperience[formFactor] ); - } catch (e) { + } catch { log.error( 'Could not repackage the JSON from CrUx, is it broken? %j', urlResult.loadingExperience[formFactor] @@ -208,11 +211,5 @@ module.exports = { } } } - }, - get cliOptions() { - return require(path.resolve(__dirname, 'cli.js')); - }, - get config() { - return cliUtil.pluginDefaults(this.cliOptions); } -}; +} diff --git a/lib/plugins/crux/repackage.js b/lib/plugins/crux/repackage.js index 01b94aa67..6e1908772 100644 --- a/lib/plugins/crux/repackage.js +++ b/lib/plugins/crux/repackage.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = function (cruxResult) { +export function repackage(cruxResult) { const result = {}; if (cruxResult.record.metrics.first_contentful_paint) { result.FIRST_CONTENTFUL_PAINT_MS = { @@ -77,4 +75,4 @@ module.exports = function (cruxResult) { result.data = cruxResult; return result; -}; +} diff --git a/lib/plugins/crux/send.js b/lib/plugins/crux/send.js index 574cec675..fb5b0c928 100644 --- a/lib/plugins/crux/send.js +++ b/lib/plugins/crux/send.js @@ -1,49 +1,46 @@ -'use strict'; +import { request as _request } from 'node:https'; +import intel from 'intel'; +const log = intel.getLogger('plugin.crux'); -const https = require('https'); -const log = require('intel').getLogger('plugin.crux'); - -module.exports = { - async get(url, key, formFactor, shouldWeTestTheURL) { - let data = shouldWeTestTheURL ? { url } : { origin: url }; - if (formFactor !== 'ALL') { - data.formFactor = formFactor; - } - data = JSON.stringify(data); - // Return new promise - return new Promise(function (resolve, reject) { - // Do async job - const req = https.request( - { - host: 'chromeuxreport.googleapis.com', - port: 443, - path: `/v1/records:queryRecord?key=${key}`, - headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(data, 'utf8') - }, - method: 'POST' - }, - function (res) { - if (res.statusCode >= 499) { - log.error( - `Got error from CrUx. Error Code: ${res.statusCode} Message: ${res.statusMessage}` - ); - return reject(new Error(`Status Code: ${res.statusCode}`)); - } - const data = []; - - res.on('data', chunk => { - data.push(chunk); - }); - - res.on('end', () => - resolve(JSON.parse(Buffer.concat(data).toString())) - ); - } - ); - req.write(data); - req.end(); - }); +export async function send(url, key, formFactor, shouldWeTestTheURL) { + let data = shouldWeTestTheURL ? { url } : { origin: url }; + if (formFactor !== 'ALL') { + data.formFactor = formFactor; } -}; + data = JSON.stringify(data); + // Return new promise + return new Promise(function (resolve, reject) { + // Do async job + const request = _request( + { + host: 'chromeuxreport.googleapis.com', + port: 443, + path: `/v1/records:queryRecord?key=${key}`, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(data, 'utf8') + }, + method: 'POST' + }, + function (res) { + if (res.statusCode >= 499) { + log.error( + `Got error from CrUx. Error Code: ${res.statusCode} Message: ${res.statusMessage}` + ); + return reject(new Error(`Status Code: ${res.statusCode}`)); + } + const data = []; + + res.on('data', chunk => { + data.push(chunk); + }); + + res.on('end', () => + resolve(JSON.parse(Buffer.concat(data).toString())) + ); + } + ); + request.write(data); + request.end(); + }); +} diff --git a/lib/plugins/domains/aggregator.js b/lib/plugins/domains/aggregator.js index 4c0913959..635041f0f 100644 --- a/lib/plugins/domains/aggregator.js +++ b/lib/plugins/domains/aggregator.js @@ -1,11 +1,13 @@ -'use strict'; +import { parse } from 'node:url'; -const Stats = require('fast-stats').Stats, - urlParser = require('url'), - log = require('intel').getLogger('sitespeedio.plugin.domains'), - statsHelpers = require('../../support/statsHelpers'), - isEmpty = require('lodash.isempty'), - reduce = require('lodash.reduce'); +import { Stats } from 'fast-stats'; +import intel from 'intel'; +import isEmpty from 'lodash.isempty'; +import reduce from 'lodash.reduce'; + +import { summarizeStats } from '../../support/statsHelpers.js'; + +const log = intel.getLogger('sitespeedio.plugin.domains'); const timingNames = [ 'blocked', @@ -18,7 +20,7 @@ const timingNames = [ ]; function parseDomainName(url) { - return urlParser.parse(url).hostname; + return parse(url).hostname; } function getDomain(domainName) { @@ -45,16 +47,16 @@ function calc(domains) { domainName }; - const stats = statsHelpers.summarizeStats(domainStats.totalTime); + const stats = summarizeStats(domainStats.totalTime); if (!isEmpty(stats)) { domainSummary.totalTime = stats; } - timingNames.forEach(name => { - const stats = statsHelpers.summarizeStats(domainStats[name]); + for (const name of timingNames) { + const stats = summarizeStats(domainStats[name]); if (!isEmpty(stats)) { domainSummary[name] = stats; } - }); + } summary[domainName] = domainSummary; return summary; @@ -66,12 +68,15 @@ function calc(domains) { function isValidTiming(timing) { // The HAR format uses -1 to indicate invalid/missing timings // isNan see https://github.com/sitespeedio/sitespeed.io/issues/2159 - return typeof timing === 'number' && timing !== -1 && !isNaN(timing); + return typeof timing === 'number' && timing !== -1 && !Number.isNaN(timing); } -module.exports = { - groups: {}, - domains: {}, +export class DomainsAggregator { + constructor() { + this.domains = {}; + this.groups = {}; + } + addToAggregate(har, url) { const mainDomain = parseDomainName(url); if (this.groups[mainDomain] === undefined) { @@ -79,10 +84,10 @@ module.exports = { } const firstPageId = har.log.pages[0].id; - har.log.entries.forEach(entry => { + for (const entry of har.log.entries) { if (entry.pageref !== firstPageId) { // Only pick the first request out of multiple runs. - return; + continue; } const domainName = parseDomainName(entry.request.url), @@ -101,18 +106,18 @@ module.exports = { log.debug('Missing time from har entry for url: ' + entry.request.url); } - timingNames.forEach(name => { + for (const name of timingNames) { const timing = entry.timings[name]; if (isValidTiming(timing)) { domain[name].push(timing); groupDomain[name].push(timing); } - }); + } this.domains[domainName] = domain; this.groups[mainDomain][domainName] = groupDomain; - }); - }, + } + } summarize() { const summary = { groups: { @@ -126,4 +131,4 @@ module.exports = { return summary; } -}; +} diff --git a/lib/plugins/domains/index.js b/lib/plugins/domains/index.js index 8b07d1ea4..99be2aebe 100644 --- a/lib/plugins/domains/index.js +++ b/lib/plugins/domains/index.js @@ -1,14 +1,19 @@ -'use strict'; -const isEmpty = require('lodash.isempty'); -const aggregator = require('./aggregator'); +import isEmpty from 'lodash.isempty'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +import { DomainsAggregator } from './aggregator.js'; + +export default class DomainsPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'domains', options, context, queue }); + } -module.exports = { open(context) { this.make = context.messageMaker('domains').make; // '*.requestCounts, 'domains.summary' context.filterRegistry.registerFilterForType([], 'domains.summary'); this.browsertime = false; - }, + this.domainsAggregator = new DomainsAggregator(); + } processMessage(message, queue) { const make = this.make; switch (message.type) { @@ -18,20 +23,20 @@ module.exports = { } case 'browsertime.har': { - aggregator.addToAggregate(message.data, message.url); + this.domainsAggregator.addToAggregate(message.data, message.url); break; } case 'webpagetest.har': { // Only collect WebPageTest data if we don't run Browsertime if (this.browsertime === false) { - aggregator.addToAggregate(message.data, message.url); + this.domainsAggregator.addToAggregate(message.data, message.url); } break; } case 'sitespeedio.summarize': { - const summary = aggregator.summarize(); + const summary = this.domainsAggregator.summarize(); if (!isEmpty(summary)) { for (let group of Object.keys(summary.groups)) { queue.postMessage( @@ -43,4 +48,4 @@ module.exports = { } } } -}; +} diff --git a/lib/plugins/gcs/index.js b/lib/plugins/gcs/index.js index d39636fa8..823174c67 100644 --- a/lib/plugins/gcs/index.js +++ b/lib/plugins/gcs/index.js @@ -1,26 +1,26 @@ -'use strict'; - -const fs = require('fs-extra'); -const path = require('path'); -const readdir = require('recursive-readdir'); +import { relative, join, resolve, sep } from 'node:path'; +import { statSync, remove } from 'fs-extra'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +import readdir from 'recursive-readdir'; // Documentation of @google-cloud/storage: https://cloud.google.com/nodejs/docs/reference/storage/2.3.x/Bucket#upload -const { Storage } = require('@google-cloud/storage'); +import { Storage } from '@google-cloud/storage'; +import intel from 'intel'; +import { throwIfMissing } from '../../support/util'; -const log = require('intel').getLogger('sitespeedio.plugin.gcs'); -const throwIfMissing = require('../../support/util').throwIfMissing; +const log = intel.getLogger('sitespeedio.plugin.gcs'); + +function ignoreDirectories(file, stats) { + return stats.isDirectory(); +} async function uploadLatestFiles(dir, gcsOptions, prefix) { - function ignoreDirs(file, stats) { - return stats.isDirectory(); - } - const storage = new Storage({ projectId: gcsOptions.projectId, keyFilename: gcsOptions.key }); const bucket = storage.bucket(gcsOptions.bucketname); - const files = await readdir(dir, [ignoreDirs]); + const files = await readdir(dir, [ignoreDirectories]); const promises = []; for (let file of files) { @@ -41,7 +41,7 @@ async function upload(dir, gcsOptions, prefix) { const bucket = storage.bucket(gcsOptions.bucketname); for (let file of files) { - const stats = fs.statSync(file); + const stats = statSync(file); if (stats.isFile()) { promises.push(uploadFile(file, bucket, gcsOptions, prefix, dir)); @@ -60,10 +60,10 @@ async function uploadFile( baseDir, noCacheTime ) { - const subPath = path.relative(baseDir, file); - const fileName = path.join(gcsOptions.path || prefix, subPath); + const subPath = relative(baseDir, file); + const fileName = join(gcsOptions.path || prefix, subPath); - const params = { + const parameters = { public: !!gcsOptions.public, destination: fileName, resumable: false, @@ -71,23 +71,26 @@ async function uploadFile( gzip: !!gcsOptions.gzip, metadata: { metadata: { - cacheControl: 'public, max-age=' + noCacheTime ? 0 : 31536000 + cacheControl: 'public, max-age=' + noCacheTime ? 0 : 31_536_000 } } }; - return bucket.upload(file, params); + return bucket.upload(file, parameters); } -module.exports = { +export default class GcsPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'gcs', options, context, queue }); + } + open(context, options) { this.gcsOptions = options.gcs; this.options = options; this.make = context.messageMaker('gcs').make; throwIfMissing(this.gcsOptions, ['bucketname'], 'gcs'); this.storageManager = context.storageManager; - }, - + } async processMessage(message, queue) { if (message.type === 'sitespeedio.setup') { // Let other plugins know that the GCS plugin is alive @@ -108,9 +111,9 @@ module.exports = { this.storageManager.getStoragePrefix() ); if (this.options.copyLatestFilesToBase) { - const rootPath = path.resolve(baseDir, '..'); - const dirsAsArray = rootPath.split(path.sep); - const rootName = dirsAsArray.slice(-1)[0]; + const rootPath = resolve(baseDir, '..'); + const directoriesAsArray = rootPath.split(sep); + const rootName = directoriesAsArray.slice(-1)[0]; await uploadLatestFiles(rootPath, gcsOptions, rootName); } log.info('Finished upload to Google Cloud Storage'); @@ -120,18 +123,18 @@ module.exports = { ); } if (gcsOptions.removeLocalResult) { - await fs.remove(baseDir); + await remove(baseDir); log.debug(`Removed local files and directory ${baseDir}`); } else { log.debug( `Local result files and directories are stored in ${baseDir}` ); } - } catch (e) { - queue.postMessage(make('error', e)); - log.error('Could not upload to Google Cloud Storage', e); + } catch (error) { + queue.postMessage(make('error', error)); + log.error('Could not upload to Google Cloud Storage', error); } queue.postMessage(make('gcs.finished')); } } -}; +} diff --git a/lib/plugins/grafana/cli.js b/lib/plugins/grafana/cli.js deleted file mode 100644 index 7a7ea26c2..000000000 --- a/lib/plugins/grafana/cli.js +++ /dev/null @@ -1,37 +0,0 @@ -module.exports = { - host: { - describe: 'The Grafana host used when sending annotations.', - group: 'Grafana' - }, - port: { - default: 80, - describe: 'The Grafana port used when sending annotations to Grafana.', - group: 'Grafana' - }, - auth: { - describe: - 'The Grafana auth/bearer value used when sending annotations to Grafana. If you do not set Bearer/Auth, Bearer is automatically set. See http://docs.grafana.org/http_api/auth/#authentication-api', - group: 'Grafana' - }, - annotationTitle: { - describe: 'Add a title to the annotation sent for a run.', - group: 'Grafana' - }, - annotationMessage: { - describe: - 'Add an extra message that will be attached to the annotation sent for a run. The message is attached after the default message and can contain HTML.', - group: 'Grafana' - }, - annotationTag: { - describe: - 'Add a extra tag to the annotation sent for a run. Repeat the --grafana.annotationTag option for multiple tags. Make sure they do not collide with the other tags.', - group: 'Grafana' - }, - annotationScreenshot: { - default: false, - type: 'boolean', - describe: - 'Include screenshot (from Browsertime/WebPageTest) in the annotation. You need to specify a --resultBaseURL for this to work.', - group: 'Grafana' - } -}; diff --git a/lib/plugins/grafana/index.js b/lib/plugins/grafana/index.js index 63b06b5a8..30648f973 100644 --- a/lib/plugins/grafana/index.js +++ b/lib/plugins/grafana/index.js @@ -1,24 +1,13 @@ -'use strict'; -const path = require('path'); -const sendAnnotations = require('./send-annotation'); -const tsdbUtil = require('../../support/tsdbUtil'); -const throwIfMissing = require('../../support/util').throwIfMissing; -const cliUtil = require('../../cli/util'); +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; -module.exports = { - name() { - return path.basename(__dirname); - }, +import { send } from './send-annotation.js'; +import { toSafeKey } from '../../support/tsdbUtil.js'; +import { throwIfMissing } from '../../support/util.js'; - /** - * Define `yargs` options with their respective default values. When displayed by the CLI help message - * all options are namespaced by its plugin name. - * - * @return {Object 0) { + tags.push(...extraTags); + } + + if (webPageTestExtraData) { + tags.push(webPageTestExtraData.connectivity, webPageTestExtraData.location); + } + + const tagsArray = getTagsAsArray(tags); + + const message = getAnnotationMessage( absolutePagePath, screenShotsEnabledInBrowsertime, screenshotType, - time, - tsdbType, - alias, - webPageTestExtraData, + webPageTestExtraData + ? webPageTestExtraData.webPageTestResultURL + : undefined, usingBrowsertime, - browserNameAndVersion, options - ) { - // The tags make it possible for the dashboard to use the - // templates to choose which annotations that will be showed. - // That's why we need to send tags that matches the template - // variables in Grafana. - const connectivity = tsdbUtil.getConnectivity(options); - const browser = options.browser; - // Hmm, here we have hardcoded Graphite ... - const namespace = options.graphite.namespace.split('.'); - const urlAndGroup = tsdbUtil - .getURLAndGroup( - options, - group, - url, - tsdbType === 'graphite' - ? options.graphite.includeQueryParams - : options.influxdb.includeQueryParams, - alias - ) - .split('.'); - - const tags = [ - connectivity, - browser, - namespace[0], - namespace[1], - urlAndGroup[0], - urlAndGroup[1] - ]; - // Avoid having the same annotations twice https://github.com/sitespeedio/sitespeed.io/issues/3277# - if (options.slug && options.slug !== urlAndGroup[0]) { - tags.push(options.slug); - } - const extraTags = util.toArray(options.grafana.annotationTag); - // We got some extra tag(s) from the user, let us add them to the annotation - if (extraTags.length > 0) { - tags.push(...extraTags); - } - - if (webPageTestExtraData) { - tags.push(webPageTestExtraData.connectivity); - tags.push(webPageTestExtraData.location); - } - - const tagsArray = annotationsHelper.getTagsAsArray(tags); - - const message = annotationsHelper.getAnnotationMessage( - absolutePagePath, - screenShotsEnabledInBrowsertime, - screenshotType, - webPageTestExtraData - ? webPageTestExtraData.webPageTestResultURL - : undefined, - usingBrowsertime, - options - ); - let what = - packageInfo.version + - (browserNameAndVersion ? ` - ${browserNameAndVersion.version}` : ''); - if (options.grafana.annotationTitle) { - what = options.grafana.annotationTitle; - } - const timestamp = Math.round(time.valueOf() / 1000); - const postData = `{"what": "${what}", "tags": ${tagsArray}, "data": "${message}", "when": ${timestamp}}`; - const postOptions = { - hostname: options.grafana.host, - port: options.grafana.port, - path: '/api/annotations/graphite', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(postData) - } - }; - // If Grafana is behind auth, use it! - if (options.grafana.auth) { - log.debug('Using auth for Grafana'); - if ( - options.grafana.auth.startsWith('Bearer') || - options.grafana.auth.startsWith('Basic') - ) { - postOptions.headers.Authorization = options.grafana.auth; - } else { - postOptions.headers.Authorization = 'Bearer ' + options.grafana.auth; - } - } - log.verbose('Send annotation to Grafana: %j', postData); - return new Promise((resolve, reject) => { - // not perfect but maybe work for us - const lib = options.grafana.port === 443 ? https : http; - const req = lib.request(postOptions, res => { - if (res.statusCode !== 200) { - const e = new Error( - `Got ${res.statusCode} from Grafana when sending annotation` - ); - if (res.statusCode === 403) { - log.warn('Authentication required.', e.message); - } else if (res.statusCode === 401) { - log.warn('No valid authentication.', e.message); - } else { - log.warn(e.message); - } - reject(e); - } else { - res.setEncoding('utf8'); - log.debug('Sent annotation to Grafana'); - resolve(); - } - }); - req.on('error', err => { - log.error('Got error from Grafana when sending annotation', err); - reject(err); - }); - req.write(postData); - req.end(); - }); + ); + let what = + version + + (browserNameAndVersion ? ` - ${browserNameAndVersion.version}` : ''); + if (options.grafana.annotationTitle) { + what = options.grafana.annotationTitle; } -}; + const timestamp = Math.round(time.valueOf() / 1000); + const postData = `{"what": "${what}", "tags": ${tagsArray}, "data": "${message}", "when": ${timestamp}}`; + const postOptions = { + hostname: options.grafana.host, + port: options.grafana.port, + path: '/api/annotations/graphite', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData) + } + }; + // If Grafana is behind auth, use it! + if (options.grafana.auth) { + log.debug('Using auth for Grafana'); + postOptions.headers.Authorization = + options.grafana.auth.startsWith('Bearer') || + options.grafana.auth.startsWith('Basic') + ? options.grafana.auth + : 'Bearer ' + options.grafana.auth; + } + log.verbose('Send annotation to Grafana: %j', postData); + return new Promise((resolve, reject) => { + // not perfect but maybe work for us + const library = options.grafana.port === 443 ? https : http; + const request = library.request(postOptions, res => { + if (res.statusCode !== 200) { + const e = new Error( + `Got ${res.statusCode} from Grafana when sending annotation` + ); + if (res.statusCode === 403) { + log.warn('Authentication required.', e.message); + } else if (res.statusCode === 401) { + log.warn('No valid authentication.', e.message); + } else { + log.warn(e.message); + } + reject(e); + } else { + res.setEncoding('utf8'); + log.debug('Sent annotation to Grafana'); + resolve(); + } + }); + request.on('error', error => { + log.error('Got error from Grafana when sending annotation', error); + reject(error); + }); + request.write(postData); + request.end(); + }); +} diff --git a/lib/plugins/graphite/cli.js b/lib/plugins/graphite/cli.js deleted file mode 100644 index 24cd8daea..000000000 --- a/lib/plugins/graphite/cli.js +++ /dev/null @@ -1,104 +0,0 @@ -module.exports = { - host: { - describe: 'The Graphite host used to store captured metrics.', - group: 'Graphite' - }, - port: { - default: 2003, - describe: 'The Graphite port used to store captured metrics.', - group: 'Graphite' - }, - auth: { - describe: - 'The Graphite user and password used for authentication. Format: user:password', - group: 'Graphite' - }, - httpPort: { - describe: - 'The Graphite port used to access the user interface and send annotations event', - default: 8080, - group: 'Graphite' - }, - webHost: { - describe: - 'The graphite-web host. If not specified graphite.host will be used.', - group: 'Graphite' - }, - namespace: { - default: 'sitespeed_io.default', - describe: 'The namespace key added to all captured metrics.', - group: 'Graphite' - }, - includeQueryParams: { - default: false, - describe: - 'Whether to include query parameters from the URL in the Graphite keys or not', - type: 'boolean', - group: 'Graphite' - }, - arrayTags: { - default: true, - type: 'boolean', - describe: - 'Send the tags as Array or a String. In Graphite 1.0 the tags is a array. Before a String', - group: 'Graphite' - }, - annotationTitle: { - describe: 'Add a title to the annotation sent for a run.', - group: 'Graphite' - }, - annotationMessage: { - describe: - 'Add an extra message that will be attached to the annotation sent for a run. The message is attached after the default message and can contain HTML.', - group: 'Graphite' - }, - annotationScreenshot: { - default: false, - type: 'boolean', - describe: - 'Include screenshot (from Browsertime/WebPageTest) in the annotation. You need to specify a --resultBaseURL for this to work.', - group: 'Graphite' - }, - sendAnnotation: { - default: true, - type: 'boolean', - describe: - 'Send annotations when a run is finished. You need to specify a --resultBaseURL for this to work. However if you for example use a Prometheus exporter, you may want to make sure annotations are not sent, then set it to false.', - group: 'Graphite' - }, - annotationRetentionMinutes: { - type: 'number', - describe: - 'The retention in minutes, to make annotation match the retention in Graphite.', - group: 'Graphite' - }, - statsd: { - default: false, - type: 'boolean', - describe: 'Uses the StatsD interface', - group: 'Graphite' - }, - annotationTag: { - describe: - 'Add a extra tag to the annotation sent for a run. Repeat the --graphite.annotationTag option for multiple tags. Make sure they do not collide with the other tags.', - group: 'Graphite' - }, - addSlugToKey: { - default: true, - type: 'boolean', - describe: - 'Add the slug (name of the test) as an extra key in the namespace.', - group: 'Graphite' - }, - bulkSize: { - default: null, - type: 'number', - describe: 'Break up number of metrics to send with each request.', - group: 'Graphite' - }, - messages: { - default: ['pageSummary', 'summary'], - options: ['pageSummary', 'summary', 'run'], - group: 'Graphite' - } -}; diff --git a/lib/plugins/graphite/data-generator.js b/lib/plugins/graphite/data-generator.js index 9d349a22d..40c05b52b 100644 --- a/lib/plugins/graphite/data-generator.js +++ b/lib/plugins/graphite/data-generator.js @@ -1,38 +1,40 @@ -'use strict'; - -const flatten = require('../../support/flattenMessage'), - util = require('util'), - graphiteUtil = require('../../support/tsdbUtil'), - reduce = require('lodash.reduce'), - formatEntry = require('./helpers/format-entry'), - isStatsd = require('./helpers/is-statsd'); +import util, { format } from 'node:util'; +import reduce from 'lodash.reduce'; +import { + getConnectivity, + toSafeKey, + getURLAndGroup +} from '../../support/tsdbUtil.js'; +import { flattenMessageData } from '../../support/flattenMessage.js'; +import { formatEntry } from './helpers/format-entry.js'; +import { isStatsD } from './helpers/is-statsd.js'; const STATSD = 'statsd'; const GRAPHITE = 'graphite'; -function keyPathFromMessage(message, options, includeQueryParams, alias) { +function keyPathFromMessage(message, options, includeQueryParameters, alias) { let typeParts = message.type.split('.'); typeParts.push(typeParts.shift()); // always have browser and connectivity in Browsertime and related tools if ( - message.type.match( - /(^pagexray|^coach|^browsertime|^largestassets|^slowestassets|^aggregateassets|^domains|^thirdparty|^axe|^sustainable)/ + /(^pagexray|^coach|^browsertime|^largestassets|^slowestassets|^aggregateassets|^domains|^thirdparty|^axe|^sustainable)/.test( + message.type ) ) { // if we have a friendly name for your connectivity, use that! - let connectivity = graphiteUtil.getConnectivity(options); + let connectivity = getConnectivity(options); typeParts.splice(1, 0, connectivity); typeParts.splice(1, 0, options.browser); - } else if (message.type.match(/(^webpagetest)/)) { + } else if (/(^webpagetest)/.test(message.type)) { if (message.connectivity) { typeParts.splice(2, 0, message.connectivity); } if (message.location) { - typeParts.splice(2, 0, graphiteUtil.toSafeKey(message.location)); + typeParts.splice(2, 0, toSafeKey(message.location)); } - } else if (message.type.match(/(^gpsi)/)) { + } else if (/(^gpsi)/.test(message.type)) { typeParts.splice(2, 0, options.mobile ? 'mobile' : 'desktop'); } @@ -41,17 +43,17 @@ function keyPathFromMessage(message, options, includeQueryParams, alias) { typeParts.splice( 1, 0, - graphiteUtil.getURLAndGroup( + getURLAndGroup( options, message.group, message.url, - includeQueryParams, + includeQueryParameters, alias ) ); } else if (message.group) { // add the group of the summary message - typeParts.splice(1, 0, graphiteUtil.toSafeKey(message.group)); + typeParts.splice(1, 0, toSafeKey(message.group)); } if (options.graphite && options.graphite.addSlugToKey) { @@ -60,13 +62,12 @@ function keyPathFromMessage(message, options, includeQueryParams, alias) { return typeParts.join('.'); } - -class GraphiteDataGenerator { - constructor(namespace, includeQueryParams, options) { +export class GraphiteDataGenerator { + constructor(namespace, includeQueryParameters, options) { this.namespace = namespace; - this.includeQueryParams = !!includeQueryParams; + this.includeQueryParams = !!includeQueryParameters; this.options = options; - this.entryFormat = isStatsd(options.graphite) ? STATSD : GRAPHITE; + this.entryFormat = isStatsD(options.graphite) ? STATSD : GRAPHITE; } dataFromMessage(message, time, alias) { @@ -80,50 +81,46 @@ class GraphiteDataGenerator { ); return reduce( - flatten.flattenMessageData(message), + flattenMessageData(message), (entries, value, key) => { if (message.type === 'browsertime.run') { if (key.includes('timings') && key.includes('marks')) { - key = key.replace(/marks\.(\d+)/, function (match, idx) { + key = key.replace(/marks\.(\d+)/, function (match, index) { return ( - 'marks.' + message.data.timings.userTimings.marks[idx].name + 'marks.' + message.data.timings.userTimings.marks[index].name ); }); } if (key.includes('timings') && key.includes('measures')) { - key = key.replace(/measures\.(\d+)/, function (match, idx) { + key = key.replace(/measures\.(\d+)/, function (match, index) { return ( 'measures.' + - message.data.timings.userTimings.measures[idx].name + message.data.timings.userTimings.measures[index].name ); }); } } - if (message.type === 'pagexray.run') { - if (key.includes('assets')) { - key = key.replace( - /assets\.(\d+)/, - function (match, idx) { - let url = new URL(message.data.assets[idx].url); - url.search = ''; - return 'assets.' + graphiteUtil.toSafeKey(url.toString()); - }, - {} - ); - } + if (message.type === 'pagexray.run' && key.includes('assets')) { + key = key.replace( + /assets\.(\d+)/, + function (match, index) { + let url = new URL(message.data.assets[index].url); + url.search = ''; + return 'assets.' + toSafeKey(url.toString()); + }, + {} + ); } - const fullKey = util.format('%s.%s.%s', this.namespace, keypath, key); - const args = [formatEntry(this.entryFormat), fullKey, value]; - this.entryFormat === GRAPHITE && args.push(timestamp); + const fullKey = format('%s.%s.%s', this.namespace, keypath, key); + const arguments_ = [formatEntry(this.entryFormat), fullKey, value]; + this.entryFormat === GRAPHITE && arguments_.push(timestamp); - entries.push(util.format.apply(util, args)); + entries.push(format.apply(util, arguments_)); return entries; }, [] ); } } - -module.exports = GraphiteDataGenerator; diff --git a/lib/plugins/graphite/graphite-sender.js b/lib/plugins/graphite/graphite-sender.js index a3faa27c0..49196b2d2 100644 --- a/lib/plugins/graphite/graphite-sender.js +++ b/lib/plugins/graphite/graphite-sender.js @@ -1,9 +1,7 @@ -'use strict'; +import { connect } from 'node:net'; +import { Sender } from './sender.js'; -const net = require('net'); -const Sender = require('./sender'); - -class GraphiteSender extends Sender { +export class GraphiteSender extends Sender { get facility() { return 'Graphite'; } @@ -12,7 +10,7 @@ class GraphiteSender extends Sender { this.log(data); return new Promise((resolve, reject) => { - const socket = net.connect(this.port, this.host, () => { + const socket = connect(this.port, this.host, () => { socket.write(data); socket.end(); resolve(); @@ -21,5 +19,3 @@ class GraphiteSender extends Sender { }); } } - -module.exports = GraphiteSender; diff --git a/lib/plugins/graphite/helpers/format-entry.js b/lib/plugins/graphite/helpers/format-entry.js index ae98516d6..5aa6fd3c0 100644 --- a/lib/plugins/graphite/helpers/format-entry.js +++ b/lib/plugins/graphite/helpers/format-entry.js @@ -3,12 +3,13 @@ * @param {string} [type='graphite'] ['statsd', 'graphite'] * @return {string} The string template */ -module.exports = type => { +export function formatEntry(type) { switch (type) { - case 'statsd': + case 'statsd': { return '%s:%s|ms'; - case 'graphite': - default: + } + default: { return '%s %s %s'; + } } -}; +} diff --git a/lib/plugins/graphite/helpers/is-statsd.js b/lib/plugins/graphite/helpers/is-statsd.js index c88c199be..dd1102b5e 100644 --- a/lib/plugins/graphite/helpers/is-statsd.js +++ b/lib/plugins/graphite/helpers/is-statsd.js @@ -3,4 +3,6 @@ * @param {Object} opts graphite options * @return {boolean} */ -module.exports = (opts = {}) => opts.statsd === true; +export function isStatsD(options = {}) { + return options.statsd === true; +} diff --git a/lib/plugins/graphite/index.js b/lib/plugins/graphite/index.js index 4d16e9850..49403fc44 100644 --- a/lib/plugins/graphite/index.js +++ b/lib/plugins/graphite/index.js @@ -1,34 +1,25 @@ -'use strict'; -const path = require('path'); -const isEmpty = require('lodash.isempty'); -const GraphiteSender = require('./graphite-sender'); -const StatsDSender = require('./statsd-sender'); -const merge = require('lodash.merge'); -const get = require('lodash.get'); -const dayjs = require('dayjs'); -const log = require('intel').getLogger('sitespeedio.plugin.graphite'); -const sendAnnotations = require('./send-annotation'); -const DataGenerator = require('./data-generator'); -const graphiteUtil = require('../../support/tsdbUtil'); -const isStatsd = require('./helpers/is-statsd'); -const throwIfMissing = require('../../support/util').throwIfMissing; -const cliUtil = require('../../cli/util'); -const toArray = require('../../support/util').toArray; +import isEmpty from 'lodash.isempty'; +import merge from 'lodash.merge'; +import get from 'lodash.get'; +import dayjs from 'dayjs'; +import intel from 'intel'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; -module.exports = { - name() { - return path.basename(__dirname); - }, +import { send } from './send-annotation.js'; +import { GraphiteDataGenerator as DataGenerator } from './data-generator.js'; +import { toSafeKey } from '../../support/tsdbUtil.js'; +import { isStatsD } from './helpers/is-statsd.js'; +import { throwIfMissing } from '../../support/util.js'; +import { toArray } from '../../support/util.js'; +import { GraphiteSender } from './graphite-sender.js'; +import { StatsDSender } from './statsd-sender.js'; - /** - * Define `yargs` options with their respective default values. When displayed by the CLI help message - * all options are namespaced by its plugin name. - * - * @return {Object 0) { + tags.push(...extraTags); + } + if (webPageTestExtraData) { + tags.push(webPageTestExtraData.connectivity, webPageTestExtraData.location); + } + const theTags = options.graphite.arrayTags + ? getTagsAsArray(tags) + : getTagsAsString(tags); + + const message = getAnnotationMessage( absolutePagePath, screenShotsEnabledInBrowsertime, screenshotType, - time, - alias, - webPageTestExtraData, + webPageTestExtraData + ? webPageTestExtraData.webPageTestResultURL + : undefined, usingBrowsertime, - browserNameAndVersion, options - ) { - // The tags make it possible for the dashboard to use the - // templates to choose which annotations that will be showed. - // That's why we need to send tags that matches the template - // variables in Grafana. - const connectivity = graphiteUtil.getConnectivity(options); - const browser = options.browser; - const namespace = options.graphite.namespace.split('.'); - const urlAndGroup = graphiteUtil - .getURLAndGroup( - options, - group, - url, - options.graphite.includeQueryParams, - alias - ) - .split('.'); - const tags = [ - connectivity, - browser, - namespace[0], - namespace[1], - urlAndGroup[0], - urlAndGroup[1] - ]; - // See https://github.com/sitespeedio/sitespeed.io/issues/3277 - if (options.slug && options.slug !== urlAndGroup[0]) { - tags.push(options.slug); - } - const extraTags = util.toArray(options.graphite.annotationTag); - // We got some extra tag(s) from the user, let us add them to the annotation - if (extraTags.length > 0) { - tags.push(...extraTags); - } - if (webPageTestExtraData) { - tags.push(webPageTestExtraData.connectivity); - tags.push(webPageTestExtraData.location); - } - const theTags = options.graphite.arrayTags - ? annotationsHelper.getTagsAsArray(tags) - : annotationsHelper.getTagsAsString(tags); + ); + const roundDownTo = roundTo => x => Math.floor(x / roundTo) * roundTo; + const roundDownToMinutes = roundDownTo( + 1000 * 60 * options.graphite.annotationRetentionMinutes + ); - const message = annotationsHelper.getAnnotationMessage( - absolutePagePath, - screenShotsEnabledInBrowsertime, - screenshotType, - webPageTestExtraData - ? webPageTestExtraData.webPageTestResultURL - : undefined, - usingBrowsertime, - options - ); - const roundDownTo = roundTo => x => Math.floor(x / roundTo) * roundTo; - const roundDownToMinutes = roundDownTo( - 1000 * 60 * options.graphite.annotationRetentionMinutes - ); + const timestamp = options.graphite.annotationRetentionMinutes + ? Math.round(roundDownToMinutes(time.valueOf()) / 1000) + : Math.round(time.valueOf() / 1000); - const timestamp = options.graphite.annotationRetentionMinutes - ? Math.round(roundDownToMinutes(time.valueOf()) / 1000) - : Math.round(time.valueOf() / 1000); - - let what = - packageInfo.version + - (browserNameAndVersion ? ` - ${browserNameAndVersion.version}` : ''); - if (options.graphite.annotationTitle) { - what = options.graphite.annotationTitle; - } - const postData = `{"what": "${what}", "tags": ${theTags}, "data": "${message}", "when": ${timestamp}}`; - const postOptions = { - hostname: options.graphite.webHost || options.graphite.host, - port: options.graphite.httpPort || 8080, - path: '/events/', - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(postData) - } - }; - - // If Graphite is behind auth, use it! - if (options.graphite.auth) { - log.debug('Using auth for Graphite'); - postOptions.auth = options.graphite.auth; - } - - return new Promise((resolve, reject) => { - log.verbose('Send annotation to Graphite: %j', postData); - // not perfect but maybe work for us - const lib = options.graphite.httpPort === 443 ? https : http; - const req = lib.request(postOptions, res => { - if (res.statusCode !== 200) { - const e = new Error( - `Got ${res.statusCode} from Graphite when sending annotation` - ); - log.warn(e.message); - reject(e); - } else { - res.setEncoding('utf8'); - log.debug('Sent annotation to Graphite'); - resolve(); - } - }); - req.on('error', err => { - log.error('Got error from Graphite when sending annotation', err); - reject(err); - }); - req.write(postData); - req.end(); - }); + let what = + version + + (browserNameAndVersion ? ` - ${browserNameAndVersion.version}` : ''); + if (options.graphite.annotationTitle) { + what = options.graphite.annotationTitle; } -}; + const postData = `{"what": "${what}", "tags": ${theTags}, "data": "${message}", "when": ${timestamp}}`; + const postOptions = { + hostname: options.graphite.webHost || options.graphite.host, + port: options.graphite.httpPort || 8080, + path: '/events/', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(postData) + } + }; + + // If Graphite is behind auth, use it! + if (options.graphite.auth) { + log.debug('Using auth for Graphite'); + postOptions.auth = options.graphite.auth; + } + + return new Promise((resolve, reject) => { + log.verbose('Send annotation to Graphite: %j', postData); + // not perfect but maybe work for us + const library = options.graphite.httpPort === 443 ? https : http; + const request = library.request(postOptions, res => { + if (res.statusCode !== 200) { + const e = new Error( + `Got ${res.statusCode} from Graphite when sending annotation` + ); + log.warn(e.message); + reject(e); + } else { + res.setEncoding('utf8'); + log.debug('Sent annotation to Graphite'); + resolve(); + } + }); + request.on('error', error => { + log.error('Got error from Graphite when sending annotation', error); + reject(error); + }); + request.write(postData); + request.end(); + }); +} diff --git a/lib/plugins/graphite/sender.js b/lib/plugins/graphite/sender.js index 0c3738d23..8c5b73c23 100644 --- a/lib/plugins/graphite/sender.js +++ b/lib/plugins/graphite/sender.js @@ -1,8 +1,7 @@ -'use strict'; +import intel from 'intel'; +const log = intel.getLogger('sitespeedio.plugin.graphite'); -const log = require('intel').getLogger('sitespeedio.plugin.graphite'); - -class Sender { +export class Sender { constructor(host, port, bulkSize) { this.host = host; this.port = port; @@ -29,14 +28,12 @@ class Sender { bulks(data) { const lines = data.split('\n'); const promises = []; - const bulkSize = this.bulkSize || lines.length; + const bulkSize = this.bulkSize || lines.length > 0; - while (lines.length) { + while (lines.length > 0) { promises.push(this.bulk(lines.splice(-bulkSize).join('\n'))); } return Promise.all(promises); } } - -module.exports = Sender; diff --git a/lib/plugins/graphite/statsd-sender.js b/lib/plugins/graphite/statsd-sender.js index 4583aa789..03538cc78 100644 --- a/lib/plugins/graphite/statsd-sender.js +++ b/lib/plugins/graphite/statsd-sender.js @@ -1,9 +1,7 @@ -'use strict'; +import { createSocket } from 'node:dgram'; +import { Sender } from './sender.js'; -const dgram = require('dgram'); -const Sender = require('./sender'); - -class StatsDSender extends Sender { +export class StatsDSender extends Sender { get facility() { return 'StatsD'; } @@ -12,7 +10,7 @@ class StatsDSender extends Sender { this.log(data); return new Promise((resolve, reject) => { - const client = dgram.createSocket('udp4'); + const client = createSocket('udp4'); client.send(data, 0, data.length, this.port, this.host, error => client.close() && error ? reject(error) : resolve() @@ -20,5 +18,3 @@ class StatsDSender extends Sender { }); } } - -module.exports = StatsDSender; diff --git a/lib/plugins/harstorer/index.js b/lib/plugins/harstorer/index.js index bac07b9d8..7b2dda99f 100644 --- a/lib/plugins/harstorer/index.js +++ b/lib/plugins/harstorer/index.js @@ -1,15 +1,19 @@ -'use strict'; +import { gzip as _gzip } from 'node:zlib'; +import { promisify } from 'node:util'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +const gzip = promisify(_gzip); -const zlib = require('zlib'); -const { promisify } = require('util'); -const gzip = promisify(zlib.gzip); +export default class HarstorerPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'harstorer', options, context, queue }); + } -module.exports = { open(context, options) { this.storageManager = context.storageManager; this.gzipHAR = options.gzipHAR; this.alias = {}; - }, + } + processMessage(message) { switch (message.type) { case 'browsertime.alias': { @@ -20,29 +24,27 @@ module.exports = { case 'webpagetest.har': { const json = JSON.stringify(message.data); - if (this.gzipHAR) { - return gzip(Buffer.from(json), { - level: 1 - }).then(gziped => - this.storageManager.writeDataForUrl( - gziped, - `${message.type}.gz`, + return this.gzipHAR + ? gzip(Buffer.from(json), { + level: 1 + }).then(gziped => + this.storageManager.writeDataForUrl( + gziped, + `${message.type}.gz`, + message.url, + undefined, + + this.alias[message.url] + ) + ) + : this.storageManager.writeDataForUrl( + json, + message.type, message.url, undefined, - this.alias[message.url] - ) - ); - } else { - return this.storageManager.writeDataForUrl( - json, - message.type, - message.url, - undefined, - this.alias[message.url] - ); - } + ); } } } -}; +} diff --git a/lib/plugins/html/dataCollector.js b/lib/plugins/html/dataCollector.js index e03facc56..8561b7d0c 100644 --- a/lib/plugins/html/dataCollector.js +++ b/lib/plugins/html/dataCollector.js @@ -1,11 +1,9 @@ -'use strict'; +import merge from 'lodash.merge'; +import get from 'lodash.get'; +import reduce from 'lodash.reduce'; +import set from 'lodash.set'; -const merge = require('lodash.merge'), - get = require('lodash.get'), - reduce = require('lodash.reduce'), - set = require('lodash.set'); - -class DataCollector { +export class DataCollector { constructor(resultUrls) { this.resultUrls = resultUrls; this.urlRunPages = {}; @@ -138,5 +136,3 @@ class DataCollector { return this.budget; } } - -module.exports = DataCollector; diff --git a/lib/plugins/html/defaultConfig.js b/lib/plugins/html/defaultConfig.js index 3d93871cf..2e6d00476 100644 --- a/lib/plugins/html/defaultConfig.js +++ b/lib/plugins/html/defaultConfig.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = { +export default { html: { showAllWaterfallSummary: false, pageSummaryMetrics: [ diff --git a/lib/plugins/html/getScripts.js b/lib/plugins/html/getScripts.js index c29ed1a6b..df7b2c266 100644 --- a/lib/plugins/html/getScripts.js +++ b/lib/plugins/html/getScripts.js @@ -1,9 +1,8 @@ -'use strict'; -const fs = require('fs'); -const { promisify } = require('util'); -const readFile = promisify(fs.readFile); +import { readFile as _readFile } from 'node:fs'; +import { promisify } from 'node:util'; +const readFile = promisify(_readFile); -module.exports = async options => { +export default async options => { const scripts = []; for (let file of options._) { // We could promise all these in the future @@ -11,7 +10,7 @@ module.exports = async options => { try { const code = await readFile(file); scripts.push({ name: file, code: code }); - } catch (e) { + } catch { // do nada } } diff --git a/lib/plugins/html/htmlBuilder.js b/lib/plugins/html/htmlBuilder.js index 81fd4108a..425792c6b 100644 --- a/lib/plugins/html/htmlBuilder.js +++ b/lib/plugins/html/htmlBuilder.js @@ -1,34 +1,43 @@ -'use strict'; +import { join } from 'node:path'; +import osName from 'os-name'; +import { promisify } from 'node:util'; +import { platform } from 'node:os'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; + +import dayjs from 'dayjs'; +import getos from 'getos'; +import intel from 'intel'; +import { markdown } from 'markdown'; +import merge from 'lodash.merge'; +import get from 'lodash.get'; +import isEmpty from 'lodash.isempty'; +import chunk from 'lodash.chunk'; -const helpers = require('../../support/helpers'); -const path = require('path'); -const osName = require('os-name'); -const getos = require('getos'); -const { promisify } = require('util'); const getOS = promisify(getos); -const os = require('os'); -const merge = require('lodash.merge'); -const get = require('lodash.get'); -const log = require('intel').getLogger('sitespeedio.plugin.html'); -const chunk = require('lodash.chunk'); -const packageInfo = require('../../../package'); -const renderer = require('./renderer'); -const metricHelper = require('./metricHelper'); -const markdown = require('markdown').markdown; -const isEmpty = require('lodash.isempty'); -const dayjs = require('dayjs'); -const defaultConfigHTML = require('./defaultConfig'); -const summaryBoxesSetup = require('./setup/summaryBoxes'); -const detailedSetup = require('./setup/detailed'); +const log = intel.getLogger('sitespeedio.plugin.html'); +const require = createRequire(import.meta.url); +const { dependencies, version } = require('../../../package.json'); +import { renderTemplate } from './renderer.js'; +import { + pickMedianRun, + getMetricsFromPageSummary, + getMetricsFromRun +} from './metricHelper.js'; -const filmstrip = require('../browsertime/filmstrip'); -const getScripts = require('./getScripts'); -const friendlyNames = require('../../support/friendlynames'); -const toArray = require('../../support/util').toArray; +import * as helpers from '../../support/helpers/index.js'; +import * as _html from './defaultConfig.js'; +import summaryBoxesSetup from './setup/summaryBoxes.js'; +import detailedSetup from './setup/detailed.js'; +import { getFilmstrip } from '../browsertime/filmstrip.js'; +import getScripts from './getScripts.js'; +import friendlyNames from '../../support/friendlynames.js'; +import { toArray } from '../../support/util.js'; +const __dirname = fileURLToPath(new URL('.', import.meta.url)); const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; -class HTMLBuilder { +export class HTMLBuilder { constructor(context, options) { this.storageManager = context.storageManager; this.timestamp = context.timestamp.format(TIME_FORMAT); @@ -55,8 +64,9 @@ class HTMLBuilder { this.summaries.push({ id, name }); break; } - default: + default: { log.info('Got a undefined page type ' + type); + } } } @@ -197,7 +207,7 @@ class HTMLBuilder { for (let url of Object.keys(validPages)) { const pageInfo = validPages[url]; const runPages = dataCollector.getURLRuns(url); - const medianRun = metricHelper.pickMedianRun(runPages, pageInfo); + const medianRun = pickMedianRun(runPages, pageInfo); // If we have multiple URLs in the same HAR the median run must be converted // to the right run in the HAR const harIndex = pageNumber + (medianRun.runIndex - 1) * testedPages; @@ -272,7 +282,7 @@ class HTMLBuilder { const medianPageInfo = runPages[medianRun.runIndex - 1]; let filmstripData = medianPageInfo && medianPageInfo.data && medianPageInfo.data.browsertime - ? await filmstrip.getFilmstrip( + ? await getFilmstrip( medianPageInfo.data.browsertime.run, medianRun.runIndex, this.storageManager.getFullPathToURLDir(url, daurlAlias), @@ -310,18 +320,18 @@ class HTMLBuilder { this.options.browsertime.iterations, 'run' )} ${url} at ${summaryTimestamp}`, - pageDescription: `${metricHelper.getMetricsFromPageSummary( + pageDescription: `${getMetricsFromPageSummary( pageInfo - )} collected by sitespeed.io ${packageInfo.version}`, + )} collected by sitespeed.io ${version}`, headers: this.summary, - version: packageInfo.version, + version: version, timestamp: summaryTimestamp, context: this.context, pageSummaries }; for (const summary of pageSummaries) { - pugs[summary.id] = renderer.renderTemplate(summary.id, data); + pugs[summary.id] = renderTemplate(summary.id, data); } data.pugs = pugs; @@ -339,7 +349,7 @@ class HTMLBuilder { ); const filmstripData = pageInfo.data.browsertime - ? await filmstrip.getFilmstrip( + ? await getFilmstrip( pageInfo.data.browsertime.run, iteration, this.storageManager.getFullPathToURLDir(url, daurlAlias), @@ -379,13 +389,13 @@ class HTMLBuilder { assetsPath: assetsBaseURL || rootPath, menu: 'pages', pageTitle: `Run ${ - parseInt(runIndex) + 1 + Number.parseInt(runIndex) + 1 } for ${url} at ${runTimestamp}`, - pageDescription: `${metricHelper.getMetricsFromRun( + pageDescription: `${getMetricsFromRun( pageInfo - )} collected by sitespeed.io ${packageInfo.version}`, + )} collected by sitespeed.io ${version}`, headers: this.summary, - version: packageInfo.version, + version: version, timestamp: runTimestamp, friendlyNames, context: this.context, @@ -393,16 +403,21 @@ class HTMLBuilder { }; // Add pugs for extra plugins for (const run of pageRuns) { - pugs[run.id] = renderer.renderTemplate(run.id, data); + pugs[run.id] = renderTemplate(run.id, data); } data.pugs = pugs; urlPageRenders.push( - this._renderUrlRunPage(url, parseInt(runIndex) + 1, data, daurlAlias) + this._renderUrlRunPage( + url, + Number.parseInt(runIndex) + 1, + data, + daurlAlias + ) ); // Do only once per URL - if (parseInt(runIndex) === 0) { + if (Number.parseInt(runIndex) === 0) { data.mySummary = mySummary; urlPageRenders.push( this._renderMetricSummaryPage(url, 'metrics', data, daurlAlias) @@ -414,11 +429,10 @@ class HTMLBuilder { // Kind of clumsy way to decide if the user changed HTML summaries, // so we in the pug can automatically add visual metrics const hasPageSummaryMetricInput = - options.html.pageSummaryMetrics !== - defaultConfigHTML.html.pageSummaryMetrics; + options.html.pageSummaryMetrics !== _html.pageSummaryMetrics; let osInfo = osName(); - if (os.platform() === 'linux') { + if (platform() === 'linux') { const linux = await getOS(); osInfo = `${linux.dist} ${linux.release}`; } @@ -451,8 +465,8 @@ class HTMLBuilder { usingBrowsertime, usingWebPageTest, headers: this.summary, - version: packageInfo.version, - browsertimeVersion: packageInfo.dependencies.browsertime, + version: version, + browsertimeVersion: dependencies.browsertime, timestamp: this.timestamp, context: this.context, get, @@ -469,11 +483,9 @@ class HTMLBuilder { ); let res; - if (this.options.html.assetsBaseURL) { - res = Promise.resolve(); - } else { - res = this.storageManager.copyToResultDir(path.join(__dirname, 'assets')); - } + res = this.options.html.assetsBaseURL + ? Promise.resolve() + : this.storageManager.copyToResultDir(join(__dirname, 'assets')); return res.then(() => Promise.allSettled(summaryRenders) @@ -487,7 +499,7 @@ class HTMLBuilder { async _renderUrlPage(url, name, locals, alias) { log.debug('Render URL page %s', name); return this.storageManager.writeHtmlForUrl( - renderer.renderTemplate('url/summary/' + name, locals), + renderTemplate('url/summary/' + name, locals), name + '.html', url, alias @@ -497,7 +509,7 @@ class HTMLBuilder { async _renderUrlRunPage(url, name, locals, alias) { log.debug('Render URL run page %s', name); return this.storageManager.writeHtmlForUrl( - renderer.renderTemplate('url/iteration/index', locals), + renderTemplate('url/iteration/index', locals), name + '.html', url, alias @@ -507,7 +519,7 @@ class HTMLBuilder { async _renderMetricSummaryPage(url, name, locals, alias) { log.debug('Render URL metric page %s', name); return this.storageManager.writeHtmlForUrl( - renderer.renderTemplate('url/summary/metrics/index', locals), + renderTemplate('url/summary/metrics/index', locals), name + '.html', url, alias @@ -518,10 +530,8 @@ class HTMLBuilder { log.debug('Render summary page %s', name); return this.storageManager.writeHtml( - renderer.renderTemplate(name, locals), + renderTemplate(name, locals), name + '.html' ); } } - -module.exports = HTMLBuilder; diff --git a/lib/plugins/html/index.js b/lib/plugins/html/index.js index d40b3d9db..0528410b5 100644 --- a/lib/plugins/html/index.js +++ b/lib/plugins/html/index.js @@ -1,17 +1,20 @@ -'use strict'; - -const HTMLBuilder = require('./htmlBuilder'); -const get = require('lodash.get'); -const set = require('lodash.set'); -const reduce = require('lodash.reduce'); -const DataCollector = require('./dataCollector'); -const renderer = require('./renderer'); +import get from 'lodash.get'; +import set from 'lodash.set'; +import reduce from 'lodash.reduce'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +import { DataCollector } from './dataCollector.js'; +import { HTMLBuilder } from './htmlBuilder.js'; +import { addTemplate } from './renderer.js'; // lets keep this in the HTML context, since we need things from the // regular options object in the output -const defaultConfig = require('./defaultConfig'); +import defaultConfig from './defaultConfig.js'; + +export default class HTMLPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'html', options, context, queue }); + } -module.exports = { open(context, options) { this.make = context.messageMaker('html').make; // we have to overwrite the default summary metrics, if given @@ -37,13 +40,13 @@ module.exports = { 'thirdparty.pageSummary', 'crux.pageSummary' ]; - }, + } processMessage(message, queue) { const dataCollector = this.dataCollector; const make = this.make; // If this type is registered - if (this.collectDataFrom.indexOf(message.type) > -1) { + if (this.collectDataFrom.includes(message.type)) { dataCollector.addDataForUrl( message.url, message.type, @@ -81,7 +84,7 @@ module.exports = { case 'html.pug': { // we got a pug from plugins, let compile and cache them - renderer.addTemplate(message.data.id, message.data.pug); + addTemplate(message.data.id, message.data.pug); // and also keep the types so we can render them this.HTMLBuilder.addType( message.data.id, @@ -230,6 +233,7 @@ module.exports = { } } } - }, - config: defaultConfig -}; + } +} + +export { default as config } from './defaultConfig.js'; diff --git a/lib/plugins/html/metricHelper.js b/lib/plugins/html/metricHelper.js index a68ef69e6..d0f73a36e 100644 --- a/lib/plugins/html/metricHelper.js +++ b/lib/plugins/html/metricHelper.js @@ -1,90 +1,86 @@ -const get = require('lodash.get'); +/* eslint-disable unicorn/no-nested-ternary */ +import get from 'lodash.get'; -module.exports = { - pickMedianRun(runs, pageInfo) { - // Choose the median run. Early first version, in the future we can make - // this configurable through the CLI - // If we have SpeedIndex use that else backup with loadEventEnd - - const speedIndexMedian = get( - pageInfo, - 'data.browsertime.pageSummary.statistics.visualMetrics.SpeedIndex.median' - ); - const loadEventEndMedian = get( - pageInfo, - 'data.browsertime.pageSummary.statistics.timings.loadEventEnd.median' - ); - if (speedIndexMedian) { - for (let run of runs) { - if ( - // https://github.com/sitespeedio/sitespeed.io/issues/3618 - run.data.browsertime.run.visualMetrics && - speedIndexMedian === run.data.browsertime.run.visualMetrics.SpeedIndex - ) { - return { - name: 'SpeedIndex', - runIndex: run.runIndex + 1 - }; - } - } - } else if (loadEventEndMedian) { - for (let rumRuns of runs) { - // make sure we run Browsertime for that run = 3 runs WPT and 2 runs BT - if ( - rumRuns.data.browsertime && - loadEventEndMedian === - rumRuns.data.browsertime.run.timings.loadEventEnd - ) { - return { - name: 'LoadEventEnd', - runIndex: rumRuns.runIndex + 1 - }; - } +export function pickMedianRun(runs, pageInfo) { + // Choose the median run. Early first version, in the future we can make + // this configurable through the CLI + // If we have SpeedIndex use that else backup with loadEventEnd + const speedIndexMedian = get( + pageInfo, + 'data.browsertime.pageSummary.statistics.visualMetrics.SpeedIndex.median' + ); + const loadEventEndMedian = get( + pageInfo, + 'data.browsertime.pageSummary.statistics.timings.loadEventEnd.median' + ); + if (speedIndexMedian) { + for (let run of runs) { + if ( + // https://github.com/sitespeedio/sitespeed.io/issues/3618 + run.data.browsertime.run.visualMetrics && + speedIndexMedian === run.data.browsertime.run.visualMetrics.SpeedIndex + ) { + return { + name: 'SpeedIndex', + runIndex: run.runIndex + 1 + }; } } - return { - name: '', - runIndex: 1, - default: true - }; - }, - // Get metrics from a run as a String to use in description - getMetricsFromRun(pageInfo) { - const visualMetrics = get(pageInfo, 'data.browsertime.run.visualMetrics'); - const timings = get(pageInfo, 'data.browsertime.run.timings'); - if (visualMetrics) { - return `First Visual Change: ${visualMetrics.FirstVisualChange}, + } else if (loadEventEndMedian) { + for (let rumRuns of runs) { + // make sure we run Browsertime for that run = 3 runs WPT and 2 runs BT + if ( + rumRuns.data.browsertime && + loadEventEndMedian === rumRuns.data.browsertime.run.timings.loadEventEnd + ) { + return { + name: 'LoadEventEnd', + runIndex: rumRuns.runIndex + 1 + }; + } + } + } + return { + name: '', + runIndex: 1, + default: true + }; +} +export function getMetricsFromRun(pageInfo) { + const visualMetrics = get(pageInfo, 'data.browsertime.run.visualMetrics'); + const timings = get(pageInfo, 'data.browsertime.run.timings'); + if (visualMetrics) { + return `First Visual Change: ${visualMetrics.FirstVisualChange}, Speed Index: ${visualMetrics.SpeedIndex}, Visual Complete 85%: ${visualMetrics.VisualComplete85}, Last Visual Change: ${visualMetrics.LastVisualChange}`; - } else if (timings) { - return `Load Event End: ${timings.loadEventEnd}`; - } else { - return ''; - } - }, - getMetricsFromPageSummary(pageInfo) { - const visualMetrics = get( - pageInfo, - 'data.browsertime.pageSummary.statistics.visualMetrics' - ); - const timings = get( - pageInfo, - 'data.browsertime.pageSummary.statistics.timings' - ); - if (visualMetrics) { - return `Median First Visual Change: ${visualMetrics.FirstVisualChange.median}, + } else if (timings) { + return `Load Event End: ${timings.loadEventEnd}`; + } else { + return ''; + } +} +export function getMetricsFromPageSummary(pageInfo) { + const visualMetrics = get( + pageInfo, + 'data.browsertime.pageSummary.statistics.visualMetrics' + ); + const timings = get( + pageInfo, + 'data.browsertime.pageSummary.statistics.timings' + ); + if (visualMetrics) { + return `Median First Visual Change: ${visualMetrics.FirstVisualChange.median}, Median Speed Index: ${visualMetrics.SpeedIndex.median}, Median Visual Complete 85%: ${visualMetrics.VisualComplete85.median}, Median Last Visual Change: ${visualMetrics.LastVisualChange.median}`; - } else if (timings) { - return timings.loadEventEnd - ? `Median LoadEventEnd: ${timings.loadEventEnd.median}` - : '' + timings.fullyLoaded - ? `Median Fully loaded: ${timings.fullyLoaded.median}` - : ''; - } else { - return ''; - } + } else if (timings) { + return timings.loadEventEnd + ? `Median LoadEventEnd: ${timings.loadEventEnd.median}` + : '' + timings.fullyLoaded + ? `Median Fully loaded: ${timings.fullyLoaded.median}` + : ''; + } else { + return ''; } -}; +} diff --git a/lib/plugins/html/renderer.js b/lib/plugins/html/renderer.js index 541237eb3..035a515a6 100644 --- a/lib/plugins/html/renderer.js +++ b/lib/plugins/html/renderer.js @@ -1,9 +1,12 @@ -'use strict'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; -const pug = require('pug'); -const path = require('path'); -const log = require('intel').getLogger('sitespeedio.plugin.html'); -const basePath = path.resolve(__dirname, 'templates'); +import { compileFile, compile } from 'pug'; +import intel from 'intel'; + +const log = intel.getLogger('sitespeedio.plugin.html'); +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const basePath = resolve(__dirname, 'templates'); const templateCache = {}; @@ -15,23 +18,21 @@ function getTemplate(templateName) { return template; } - const filename = path.resolve(basePath, templateName); - const renderedTemplate = pug.compileFile(filename); + const filename = resolve(basePath, templateName); + const renderedTemplate = compileFile(filename); templateCache[templateName] = renderedTemplate; return renderedTemplate; } -module.exports = { - renderTemplate(templateName, locals) { - try { - return getTemplate(templateName)(locals); - } catch (e) { - log.error('Could not generate %s, %s', templateName, e.message); - } - }, - addTemplate(templateName, templateString) { - const compiledTemplate = pug.compile(templateString); - templateCache[templateName + '.pug'] = compiledTemplate; +export function renderTemplate(templateName, locals) { + try { + return getTemplate(templateName)(locals); + } catch (error) { + log.error('Could not generate %s, %s', templateName, error.message); } -}; +} +export function addTemplate(templateName, templateString) { + const compiledTemplate = compile(templateString); + templateCache[templateName + '.pug'] = compiledTemplate; +} diff --git a/lib/plugins/html/setup/detailed.js b/lib/plugins/html/setup/detailed.js index 6b59cef78..793107921 100644 --- a/lib/plugins/html/setup/detailed.js +++ b/lib/plugins/html/setup/detailed.js @@ -1,22 +1,20 @@ -'use strict'; - -const h = require('../../../support/helpers'); -const get = require('lodash.get'); +import { noop, size, time } from '../../../support/helpers/index.js'; +import get from 'lodash.get'; function row(stat, name, metricName, formatter) { if (typeof stat === 'undefined') { - return undefined; + return; } return { name, metricName, node: stat, - h: formatter ? formatter : h.noop + h: formatter ?? noop }; } -module.exports = function (data) { +export default function (data) { if (!data) { return []; } @@ -65,41 +63,38 @@ module.exports = function (data) { 'jsRequestsPerPage' ), row(contentTypes.font.requests, 'Font requests', 'fontRequestsPerPage'), - row(summary.requests, 'Total requests', 'totalRequestsPerPage') - ); - - rows.push( + row(summary.requests, 'Total requests', 'totalRequestsPerPage'), row( contentTypes.image.transferSize, 'Image size', 'imageSizePerPage', - h.size.format + size.format ), row( contentTypes.html.transferSize, 'HTML size', 'htmlSizePerPage', - h.size.format + size.format ), row( contentTypes.css.transferSize, 'CSS size', 'cssSizePerPage', - h.size.format + size.format ), row( contentTypes.javascript.transferSize, 'Javascript size', 'jsSizePerPage', - h.size.format + size.format ), row( contentTypes.font.transferSize, 'Font size', 'fontSizePerPage', - h.size.format + size.format ), - row(summary.transferSize, 'Total size', 'totalSizePerPage', h.size.format) + row(summary.transferSize, 'Total size', 'totalSizePerPage', size.format) ); const responseCodes = Object.keys(summary.responseCodes); @@ -110,16 +105,11 @@ module.exports = function (data) { if (browsertime) { const summary = browsertime.summary; - rows.push(row(summary.firstPaint, 'First Paint', 'firstPaint', h.time.ms)); + rows.push(row(summary.firstPaint, 'First Paint', 'firstPaint', time.ms)); if (summary.timings) { rows.push( - row( - summary.timings.fullyLoaded, - 'Fully Loaded', - 'fullyLoaded', - h.time.ms - ) + row(summary.timings.fullyLoaded, 'Fully Loaded', 'fullyLoaded', time.ms) ); } @@ -129,7 +119,7 @@ module.exports = function (data) { summary.timeToDomContentFlushed, 'DOMContentFlushed', 'timeToDomContentFlushed', - h.time.ms + time.ms ) ); } @@ -140,13 +130,13 @@ module.exports = function (data) { summary.timings.largestContentfulPaint, 'Largest Contentful Paint', 'largestContentfulPaint', - h.time.ms + time.ms ) ); } if (summary.memory) { - rows.push(row(summary.memory, 'Memory usage', 'memory', h.size.format)); + rows.push(row(summary.memory, 'Memory usage', 'memory', size.format)); } if (summary.paintTiming) { @@ -156,13 +146,13 @@ module.exports = function (data) { 'first-contentful-paint': 'First Contentful Paint' }; for (let pt of paintTimings) { - rows.push(row(summary.paintTiming[pt], lookup[pt], pt, h.time.ms)); + rows.push(row(summary.paintTiming[pt], lookup[pt], pt, time.ms)); } } const timings = Object.keys(summary.pageTimings); for (let timing of timings) { - rows.push(row(summary.pageTimings[timing], timing, timing, h.time.ms)); + rows.push(row(summary.pageTimings[timing], timing, timing, time.ms)); } if (summary.custom) { @@ -187,49 +177,49 @@ module.exports = function (data) { summary.visualMetrics.FirstVisualChange, 'First Visual Change', 'FirstVisualChange', - h.time.ms + time.ms ), row( summary.visualMetrics.SpeedIndex, 'Speed Index', 'SpeedIndex', - h.time.ms + time.ms ), row( summary.visualMetrics.PerceptualSpeedIndex, 'Perceptual Speed Index', 'PerceptualSpeedIndex', - h.time.ms + time.ms ), row( summary.visualMetrics.ContentfulSpeedIndex, 'Contentful Speed Index', 'ContentfulSpeedIndex', - h.time.ms + time.ms ), row( summary.visualMetrics.VisualComplete85, 'Visual Complete 85%', 'VisualComplete85', - h.time.ms + time.ms ), row( summary.visualMetrics.VisualComplete95, 'Visual Complete 95%', 'VisualComplete95', - h.time.ms + time.ms ), row( summary.visualMetrics.VisualComplete99, 'Visual Complete 99%', 'VisualComplete99', - h.time.ms + time.ms ), row( summary.visualMetrics.LastVisualChange, 'Last Visual Change', 'LastVisualChange', - h.time.ms + time.ms ) ); @@ -239,17 +229,17 @@ module.exports = function (data) { summary.visualMetrics.LargestImage, 'Largest Image', 'LargestImage', - h.time.ms + time.ms ) ); } if (summary.visualMetrics.Heading) { rows.push( - row(summary.visualMetrics.Heading, 'Heading', 'Heading', h.time.ms) + row(summary.visualMetrics.Heading, 'Heading', 'Heading', time.ms) ); } if (summary.visualMetrics.Logo) { - rows.push(row(summary.visualMetrics.Logo, 'Logo', 'Logo', h.time.ms)); + rows.push(row(summary.visualMetrics.Logo, 'Logo', 'Logo', time.ms)); } } @@ -265,19 +255,19 @@ module.exports = function (data) { summary.cpu.longTasks.totalDuration, 'CPU Long Tasks total duration', 'cpuLongTasksTotalDurationPerPage', - h.time.ms + time.ms ), row( summary.cpu.longTasks.totalBlockingTime, 'Total Blocking Time', 'totalBlockingTime', - h.time.ms + time.ms ), row( summary.cpu.longTasks.maxPotentialFid, 'Max Potential First Input Delay', 'maxPotentialFirstInputDelay', - h.time.ms + time.ms ) ); } @@ -288,31 +278,31 @@ module.exports = function (data) { summary.cpu.categories.parseHTML, 'CPU Parse HTML', 'parseHTMLPerPage', - h.time.ms + time.ms ), row( summary.cpu.categories.styleLayout, 'CPU Style Layout', 'styleLayoutPerPage', - h.time.ms + time.ms ), row( summary.cpu.categories.paintCompositeRender, 'CPU Paint Composite Render', 'paintCompositeRenderPerPage', - h.time.ms + time.ms ), row( summary.cpu.categories.scriptParseCompile, 'CPU Script Parse Compile', 'scriptParseCompilePerPage', - h.time.ms + time.ms ), row( summary.cpu.categories.scriptEvaluation, 'CPU Script Evaluation', 'scriptEvaluationPerPage', - h.time.ms + time.ms ) ); } @@ -323,18 +313,18 @@ module.exports = function (data) { const firstView = get(webpagetest, 'summary.timing.firstView'); if (firstView) { rows.push( - row(firstView.render, 'WPT render (firstView)', 'render', h.time.ms), + row(firstView.render, 'WPT render (firstView)', 'render', time.ms), row( firstView.SpeedIndex, 'WPT SpeedIndex (firstView)', 'SpeedIndex', - h.time.ms + time.ms ), row( firstView.fullyLoaded, 'WPT Fully loaded (firstView)', 'fullyLoaded', - h.time.ms + time.ms ) ); } @@ -383,4 +373,4 @@ module.exports = function (data) { } return rows.filter(Boolean); -}; +} diff --git a/lib/plugins/html/setup/summaryBoxes.js b/lib/plugins/html/setup/summaryBoxes.js index ebc24c4d4..db5785c8b 100644 --- a/lib/plugins/html/setup/summaryBoxes.js +++ b/lib/plugins/html/setup/summaryBoxes.js @@ -1,15 +1,14 @@ -'use strict'; - -const log = require('intel').getLogger('sitespeedio.plugin.html'); -const toArray = require('../../../support/util').toArray; -const friendlyNames = require('../../../support/friendlynames'); -const get = require('lodash.get'); -const defaultLimits = require('./summaryBoxesDefaultLimits'); -const merge = require('lodash.merge'); +import intel from 'intel'; +const log = intel.getLogger('sitespeedio.plugin.html'); +import { toArray } from '../../../support/util.js'; +import friendlyNames from '../../../support/friendlynames.js'; +import get from 'lodash.get'; +import defaultLimits from './summaryBoxesDefaultLimits.js'; +import merge from 'lodash.merge'; function infoBox(stat, name, formatter) { if (typeof stat === 'undefined') { - return undefined; + return; } return _box(stat, name, 'info', formatter, name.replace(/\s/g, '')); @@ -17,7 +16,7 @@ function infoBox(stat, name, formatter) { function scoreBox(stat, name, formatter, box, limits) { if (typeof stat === 'undefined') { - return undefined; + return; } let label = 'info'; @@ -36,7 +35,7 @@ function scoreBox(stat, name, formatter, box, limits) { function timingBox(stat, name, formatter, box, limits) { if (typeof stat === 'undefined') { - return undefined; + return; } let label = 'info'; @@ -56,7 +55,7 @@ function timingBox(stat, name, formatter, box, limits) { function pagexrayBox(stat, name, formatter, box, limits) { if (typeof stat === 'undefined') { - return undefined; + return; } let label = 'info'; @@ -75,7 +74,7 @@ function pagexrayBox(stat, name, formatter, box, limits) { function axeBox(stat, name, formatter, url, limits) { if (typeof stat === 'undefined') { - return undefined; + return; } let label = 'info'; @@ -106,7 +105,7 @@ function _box(stat, name, label, formatter, url) { }; } -module.exports = function (data, html) { +export default function (data, html) { if (!data) { return []; } @@ -122,20 +121,25 @@ module.exports = function (data, html) { if (friendly) { let boxType; switch (tool) { - case 'coach': + case 'coach': { boxType = scoreBox; break; - case 'axe': + } + case 'axe': { boxType = axeBox; break; - case 'pagexray': + } + case 'pagexray': { boxType = pagexrayBox; break; - case 'browsertime': + } + case 'browsertime': { boxType = timingBox; break; - default: + } + default: { boxType = infoBox; + } } const stats = get(data, tool + '.summary.' + friendly.summaryPath); @@ -158,4 +162,4 @@ module.exports = function (data, html) { } } return boxes; -}; +} diff --git a/lib/plugins/html/setup/summaryBoxesDefaultLimits.js b/lib/plugins/html/setup/summaryBoxesDefaultLimits.js index 57edc68ae..ef2dc491e 100644 --- a/lib/plugins/html/setup/summaryBoxesDefaultLimits.js +++ b/lib/plugins/html/setup/summaryBoxesDefaultLimits.js @@ -1,5 +1,4 @@ -'use strict'; -module.exports = { +export default { score: { score: { green: 90, @@ -22,7 +21,6 @@ module.exports = { yellow: 80 } }, - // All timings are in ms timings: { firstPaint: { green: 1000, yellow: 2000 }, firstContentfulPaint: { green: 2000, yellow: 4000 }, @@ -45,16 +43,15 @@ module.exports = { css: {}, image: {} }, - // Size in byte transferSize: { - total: { green: 1000000, yellow: 1500000 }, + total: { green: 1_000_000, yellow: 1_500_000 }, html: {}, css: {}, image: {}, javascript: {} }, contentSize: { - javascript: { green: 100000, yellow: 150000 } + javascript: { green: 100_000, yellow: 150_000 } }, thirdParty: { requests: {}, diff --git a/lib/plugins/html/templates/url/includes/tabScripts.js b/lib/plugins/html/templates/url/includes/tabScripts.js index 9d67d7829..5dab9efdb 100644 --- a/lib/plugins/html/templates/url/includes/tabScripts.js +++ b/lib/plugins/html/templates/url/includes/tabScripts.js @@ -2,8 +2,8 @@ window.addEventListener('DOMContentLoaded', function () { let tabsRoot = document.querySelector('#tabs'); let navigationLinks = document.querySelectorAll('#pageNavigation a'); - for (let i = 0; i < navigationLinks.length; ++i) { - navigationLinks[i].addEventListener('click', event => { + for (const navigationLink of navigationLinks) { + navigationLink.addEventListener('click', event => { if (!location.hash) return; event.preventDefault(); location.href = `${event.target.href}${location.hash}_ref`; @@ -22,8 +22,8 @@ window.addEventListener('DOMContentLoaded', function () { if (!currentTab) currentTab = tabsRoot.querySelector('a'); let sections = document.querySelectorAll('#tabSections section'); - for (let i = 0; i < sections.length; i++) { - sections[i].style.display = 'none'; + for (const section of sections) { + section.style.display = 'none'; } selectTab(currentTab, false); }); @@ -47,14 +47,14 @@ function selectTab(newSelection, updateUrlFragment) { section.style.display = 'block'; let charts = section.querySelectorAll('.ct-chart'); - for (let i = 0; i < charts.length; i++) { - if (charts[i].__chartist__) { - charts[i].__chartist__.update(); + for (const chart of charts) { + if (chart.__chartist__) { + chart.__chartist__.update(); } } if (updateUrlFragment && history.replaceState) { - history.replaceState(null, null, '#' + newSelection.id); + history.replaceState(undefined, undefined, '#' + newSelection.id); } return false; diff --git a/lib/plugins/influxdb/cli.js b/lib/plugins/influxdb/cli.js deleted file mode 100644 index 354625287..000000000 --- a/lib/plugins/influxdb/cli.js +++ /dev/null @@ -1,54 +0,0 @@ -module.exports = { - protocol: { - describe: 'The protocol used to store connect to the InfluxDB host.', - default: 'http', - group: 'InfluxDB' - }, - host: { - describe: 'The InfluxDB host used to store captured metrics.', - group: 'InfluxDB' - }, - port: { - default: 8086, - describe: 'The InfluxDB port used to store captured metrics.', - group: 'InfluxDB' - }, - username: { - describe: 'The InfluxDB username for your InfluxDB instance.', - group: 'InfluxDB' - }, - password: { - describe: 'The InfluxDB password for your InfluxDB instance.', - group: 'InfluxDB' - }, - database: { - default: 'sitespeed', - describe: 'The database name used to store captured metrics.', - group: 'InfluxDB' - }, - tags: { - default: 'category=default', - describe: 'A comma separated list of tags and values added to each metric', - group: 'InfluxDB' - }, - includeQueryParams: { - default: false, - describe: - 'Whether to include query parameters from the URL in the InfluxDB keys or not', - type: 'boolean', - group: 'InfluxDB' - }, - groupSeparator: { - default: '_', - describe: - 'Choose which character that will separate a group/domain. Default is underscore, set it to a dot if you wanna keep the original domain name.', - group: 'InfluxDB' - }, - annotationScreenshot: { - default: false, - type: 'boolean', - describe: - 'Include screenshot (from Browsertime) in the annotation. You need to specify a --resultBaseURL for this to work.', - group: 'InfluxDB' - } -}; diff --git a/lib/plugins/influxdb/data-generator.js b/lib/plugins/influxdb/data-generator.js index 5cb845804..57b0879ae 100644 --- a/lib/plugins/influxdb/data-generator.js +++ b/lib/plugins/influxdb/data-generator.js @@ -1,115 +1,28 @@ -'use strict'; +import merge from 'lodash.merge'; +import reduce from 'lodash.reduce'; -const flatten = require('../../support/flattenMessage'), - merge = require('lodash.merge'), - util = require('../../support/tsdbUtil'), - reduce = require('lodash.reduce'); +import { flattenMessageData } from '../../support/flattenMessage.js'; +import { + getConnectivity, + getURLAndGroup, + toSafeKey +} from '../../support/tsdbUtil.js'; -class InfluxDBDataGenerator { - constructor(includeQueryParams, options) { - this.includeQueryParams = !!includeQueryParams; - this.options = options; - this.defaultTags = {}; - for (let row of options.influxdb.tags.split(',')) { - const keyAndValue = row.split('='); - this.defaultTags[keyAndValue[0]] = keyAndValue[1]; - } - } - - dataFromMessage(message, time, alias) { - function getTagsFromMessage( - message, - includeQueryParams, - options, - defaultTags - ) { - const tags = merge({}, defaultTags); - let typeParts = message.type.split('.'); - tags.origin = typeParts[0]; - typeParts.push(typeParts.shift()); - tags.summaryType = typeParts[0]; - - // always have browser and connectivity in Browsertime and related tools - if ( - message.type.match( - /(^pagexray|^coach|^browsertime|^thirdparty|^axe|^sustainable)/ - ) - ) { - // if we have a friendly name for your connectivity, use that! - let connectivity = util.getConnectivity(options); - tags.connectivity = connectivity; - tags.browser = options.browser; - } else if (message.type.match(/(^webpagetest)/)) { - if (message.connectivity) { - tags.connectivity = message.connectivity; - } - if (message.location) { - tags.location = message.location; - } - } else if (message.type.match(/(^gpsi)/)) { - tags.strategy = options.mobile ? 'mobile' : 'desktop'; - } - - // if we get a URL type, add the URL - if (message.url) { - const urlAndGroup = util - .getURLAndGroup( - options, - message.group, - message.url, - includeQueryParams, - alias - ) - .split('.'); - tags.page = urlAndGroup[1]; - tags.group = urlAndGroup[0]; - } else if (message.group) { - // add the group of the summary message - tags.group = util.toSafeKey( - message.group, - options.influxdb.groupSeparator - ); - } - - tags.testName = options.slug; - - return tags; - } - - function getFieldAndSeriesName(key) { - const functions = [ - 'min', - 'p10', - 'median', - 'mean', - 'avg', - 'max', - 'p90', - 'p99', - 'mdev', - 'stddev' - ]; - const keyArray = key.split('.'); - const end = keyArray.pop(); - if (functions.indexOf(end) > -1) { - return { field: end, seriesName: keyArray.pop() }; - } - return { field: 'value', seriesName: end }; - } - - function getAdditionalTags(key, type) { - let tags = {}; - const keyArray = key.split('.'); - if (key.match(/(^contentTypes)/)) { - // contentTypes.favicon.requests.mean - // contentTypes.favicon.requests - // contentTypes.css.transferSize - tags.contentType = keyArray[1]; - } else if (key.match(/(^pageTimings|^visualMetrics)/)) { - // pageTimings.serverResponseTime.max - // visualMetrics.SpeedIndex.median - tags.timings = keyArray[0]; - } else if (type === 'browsertime.pageSummary') { +function getAdditionalTags(key, type) { + let tags = {}; + const keyArray = key.split('.'); + if (/(^contentTypes)/.test(key)) { + // contentTypes.favicon.requests.mean + // contentTypes.favicon.requests + // contentTypes.css.transferSize + tags.contentType = keyArray[1]; + } else if (/(^pageTimings|^visualMetrics)/.test(key)) { + // pageTimings.serverResponseTime.max + // visualMetrics.SpeedIndex.median + tags.timings = keyArray[0]; + } else + switch (type) { + case 'browsertime.pageSummary': { // statistics.timings.pageTimings.backEndTime.median // statistics.timings.userTimings.marks.logoTime.median // statistics.visualMetrics.SpeedIndex.median @@ -117,20 +30,26 @@ class InfluxDBDataGenerator { if (keyArray.length >= 5) { tags[keyArray[2]] = keyArray[3]; } - if (key.indexOf('cpu.categories') > -1) { + if (key.includes('cpu.categories')) { tags.cpu = 'category'; - } else if (key.indexOf('cpu.events') > -1) { + } else if (key.includes('cpu.events')) { tags.cpu = 'event'; - } else if (key.indexOf('cpu.longTasks') > -1) { + } else if (key.includes('cpu.longTasks')) { tags.cpu = 'longTask'; } - } else if (type === 'browsertime.summary') { + + break; + } + case 'browsertime.summary': { // firstPaint.median // userTimings.marks.logoTime.median - if (key.indexOf('userTimings') > -1) { + if (key.includes('userTimings')) { tags[keyArray[0]] = keyArray[1]; } - } else if (type === 'coach.pageSummary') { + + break; + } + case 'coach.pageSummary': { // advice.score // advice.performance.score if (keyArray.length > 2) { @@ -142,89 +61,205 @@ class InfluxDBDataGenerator { if (keyArray.length > 4) { tags.adviceName = keyArray[3]; } - } else if (type === 'coach.summary') { + + break; + } + case 'coach.summary': { // score.max // performance.score.median if (keyArray.length === 3) { tags.advice = keyArray[0]; } - } else if (type === 'webpagetest.pageSummary') { + + break; + } + case 'webpagetest.pageSummary': { // data.median.firstView.SpeedIndex webpagetest.pageSummary tags.view = keyArray[2]; // data.median.firstView.breakdown.html.requests // data.median.firstView.breakdown.html.bytes - if (key.indexOf('breakdown') > -1) { + if (key.includes('breakdown')) { tags.contentType = keyArray[4]; } - } else if (type === 'webpagetest.summary') { + + break; + } + case 'webpagetest.summary': { // timing.firstView.SpeedIndex.median tags.view = keyArray[1]; // asset.firstView.breakdown.html.requests.median - if (key.indexOf('breakdown') > -1) { + if (key.includes('breakdown')) { tags.contentType = keyArray[4]; } - } else if (type === 'pagexray.summary') { + + break; + } + case 'pagexray.summary': { // firstParty.requests.min pagexray.summary // requests.median // responseCodes.307.max pagexray.summary // requests.min pagexray.summary - if (key.indexOf('responseCodes') > -1) { + if (key.includes('responseCodes')) { tags.responseCodes = 'response'; } - if (key.indexOf('firstParty') > -1 || key.indexOf('thirdParty') > -1) { + if (key.includes('firstParty') || key.includes('thirdParty')) { tags.party = keyArray[0]; } - } else if (type === 'pagexray.pageSummary') { + + break; + } + case 'pagexray.pageSummary': { // thirdParty.contentTypes.json.requests pagexray.pageSummary // thirdParty.requests pagexray.pageSummary // firstParty.cookieStats.max pagexray.pageSummary // responseCodes.200 pagexray.pageSummary // expireStats.max pagexray.pageSummary // totalDomains pagexray.pageSummary - if (key.indexOf('firstParty') > -1 || key.indexOf('thirdParty') > -1) { + if (key.includes('firstParty') || key.includes('thirdParty')) { tags.party = keyArray[0]; } - if (key.indexOf('responseCodes') > -1) { + if (key.includes('responseCodes')) { tags.responseCodes = 'response'; } - if (key.indexOf('contentTypes') > -1) { + if (key.includes('contentTypes')) { tags.contentType = keyArray[2]; } - } else if (type === 'thirdparty.pageSummary') { + + break; + } + case 'thirdparty.pageSummary': { tags.thirdPartyCategory = keyArray[1]; tags.thirdPartyType = keyArray[2]; - } else if (type === 'lighthouse.pageSummary') { + + break; + } + case 'lighthouse.pageSummary': { // categories.seo.score // categories.performance.score - if (key.indexOf('score') > -1) { + if (key.includes('score')) { tags.audit = keyArray[1]; } - if (key.indexOf('audits') > -1) { + if (key.includes('audits')) { tags.audit = keyArray[1]; } - } else if (type === 'crux.pageSummary') { + + break; + } + case 'crux.pageSummary': { tags.experience = keyArray[0]; tags.formFactor = keyArray[1]; tags.metric = keyArray[2]; - } else if (type === 'gpsi.pageSummary') { - if (key.indexOf('googleWebVitals') > -1) { + + break; + } + case 'gpsi.pageSummary': { + if (key.includes('googleWebVitals')) { tags.testType = 'googleWebVitals'; - } else if (key.indexOf('score') > -1) { + } else if (key.includes('score')) { tags.testType = 'score'; - } else if (key.indexOf('loadingExperience') > -1) { + } else if (key.includes('loadingExperience')) { tags.experience = keyArray[0]; tags.metric = keyArray[1]; tags.testType = 'crux'; } - } else { - // console.log('Missed added tags to ' + key + ' ' + type); + + break; } + default: + // console.log('Missed added tags to ' + key + ' ' + type); + } + return tags; +} + +function getFieldAndSeriesName(key) { + const functions = [ + 'min', + 'p10', + 'median', + 'mean', + 'avg', + 'max', + 'p90', + 'p99', + 'mdev', + 'stddev' + ]; + const keyArray = key.split('.'); + const end = keyArray.pop(); + if (functions.includes(end)) { + return { field: end, seriesName: keyArray.pop() }; + } + return { field: 'value', seriesName: end }; +} +export class InfluxDBDataGenerator { + constructor(includeQueryParameters, options) { + this.includeQueryParams = !!includeQueryParameters; + this.options = options; + this.defaultTags = {}; + for (let row of options.influxdb.tags.split(',')) { + const keyAndValue = row.split('='); + this.defaultTags[keyAndValue[0]] = keyAndValue[1]; + } + } + + dataFromMessage(message, time, alias) { + console.log('GET DATA'); + function getTagsFromMessage( + message, + includeQueryParameters, + options, + defaultTags + ) { + const tags = merge({}, defaultTags); + let typeParts = message.type.split('.'); + tags.origin = typeParts[0]; + typeParts.push(typeParts.shift()); + tags.summaryType = typeParts[0]; + + // always have browser and connectivity in Browsertime and related tools + if ( + /(^pagexray|^coach|^browsertime|^thirdparty|^axe|^sustainable)/.test( + message.type + ) + ) { + // if we have a friendly name for your connectivity, use that! + let connectivity = getConnectivity(options); + tags.connectivity = connectivity; + tags.browser = options.browser; + } else if (/(^webpagetest)/.test(message.type)) { + if (message.connectivity) { + tags.connectivity = message.connectivity; + } + if (message.location) { + tags.location = message.location; + } + } else if (/(^gpsi)/.test(message.type)) { + tags.strategy = options.mobile ? 'mobile' : 'desktop'; + } + + // if we get a URL type, add the URL + if (message.url) { + const urlAndGroup = getURLAndGroup( + options, + message.group, + message.url, + includeQueryParameters, + alias + ).split('.'); + tags.page = urlAndGroup[1]; + tags.group = urlAndGroup[0]; + } else if (message.group) { + // add the group of the summary message + tags.group = toSafeKey(message.group, options.influxdb.groupSeparator); + } + + tags.testName = options.slug; + return tags; } - return reduce( - flatten.flattenMessageData(message), + flattenMessageData(message), (entries, value, key) => { const fieldAndSeriesName = getFieldAndSeriesName(key); let tags = getTagsFromMessage( @@ -249,5 +284,3 @@ class InfluxDBDataGenerator { ); } } - -module.exports = InfluxDBDataGenerator; diff --git a/lib/plugins/influxdb/index.js b/lib/plugins/influxdb/index.js index eb9aea04a..5db69db77 100644 --- a/lib/plugins/influxdb/index.js +++ b/lib/plugins/influxdb/index.js @@ -1,29 +1,17 @@ -'use strict'; +import isEmpty from 'lodash.isempty'; +import intel from 'intel'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +import { InfluxDBSender as Sender } from './sender.js'; +import { toSafeKey } from '../../support/tsdbUtil.js'; +import { send } from './send-annotation.js'; +import { InfluxDBDataGenerator as DataGenerator } from './data-generator.js'; +import { throwIfMissing } from '../../support/util.js'; -const throwIfMissing = require('../../support/util').throwIfMissing; -const isEmpty = require('lodash.isempty'); -const log = require('intel').getLogger('sitespeedio.plugin.influxdb'); -const Sender = require('./sender'); -const tsdbUtil = require('../../support/tsdbUtil'); -const sendAnnotations = require('./send-annotation'); -const DataGenerator = require('./data-generator'); -const path = require('path'); -const cliUtil = require('../../cli/util'); - -module.exports = { - name() { - return path.basename(__dirname); - }, - - /** - * Define `yargs` options with their respective default values. When displayed by the CLI help message - * all options are namespaced by its plugin name. - * - * @return {Object { - log.debug('Send annotation to Influx: %j', postData); - // not perfect but maybe work for us - const lib = options.influxdb.protocol === 'https' ? https : http; - const req = lib.request(postOptions, res => { - if (res.statusCode !== 204) { - const e = new Error( - `Got ${res.statusCode} from InfluxDB when sending annotation ${res.statusMessage}` - ); - log.warn(e.message); - reject(e); - } else { - res.setEncoding('utf-8'); - log.debug('Sent annotation to InfluxDB'); - resolve(); - } - }); - req.on('error', err => { - log.error('Got error from InfluxDB when sending annotation', err); - reject(err); - }); - req.write(postData); - req.end(); - }); } -}; + const influxDBTags = getTagsAsString(tags); + const postData = `events title="Sitespeed.io",text="${message}",tags=${influxDBTags} ${timestamp}`; + const postOptions = { + hostname: options.influxdb.host, + port: options.influxdb.port, + path: '/write?db=' + options.influxdb.database + '&precision=s', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(postData) + } + }; + + if (options.influxdb.username) { + postOptions.path = + postOptions.path + + '&' + + stringify({ + u: options.influxdb.username, + p: options.influxdb.password + }); + } + + return new Promise((resolve, reject) => { + log.debug('Send annotation to Influx: %j', postData); + // not perfect but maybe work for us + const library = options.influxdb.protocol === 'https' ? https : http; + const request = library.request(postOptions, res => { + if (res.statusCode !== 204) { + const e = new Error( + `Got ${res.statusCode} from InfluxDB when sending annotation ${res.statusMessage}` + ); + log.warn(e.message); + reject(e); + } else { + res.setEncoding('utf8'); + log.debug('Sent annotation to InfluxDB'); + resolve(); + } + }); + request.on('error', error => { + log.error('Got error from InfluxDB when sending annotation', error); + reject(error); + }); + request.write(postData); + request.end(); + }); +} diff --git a/lib/plugins/influxdb/sender.js b/lib/plugins/influxdb/sender.js index 082e8aee7..e76b8dde6 100644 --- a/lib/plugins/influxdb/sender.js +++ b/lib/plugins/influxdb/sender.js @@ -1,10 +1,8 @@ -'use strict'; +import { InfluxDB } from 'influx'; -const Influx = require('influx'); - -class InfluxDBSender { +export class InfluxDBSender { constructor({ protocol, host, port, database, username, password }) { - this.client = new Influx.InfluxDB({ + this.client = new InfluxDB({ protocol, host, port, @@ -26,5 +24,3 @@ class InfluxDBSender { return this.client.writePoints(points); } } - -module.exports = InfluxDBSender; diff --git a/lib/plugins/lateststorer/index.js b/lib/plugins/lateststorer/index.js index 1c5154568..e1c6ceddf 100644 --- a/lib/plugins/lateststorer/index.js +++ b/lib/plugins/lateststorer/index.js @@ -1,22 +1,27 @@ -'use strict'; +import { resolve, join } from 'node:path'; +import { platform } from 'node:os'; +import { promisify } from 'node:util'; + +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +import osName from 'os-name'; +import getos from 'getos'; +import get from 'lodash.get'; + +import { getURLAndGroup, getConnectivity } from '../../support/tsdbUtil.js'; +import { cap, plural } from '../../support/helpers/index.js'; -const path = require('path'); -const osName = require('os-name'); -const getos = require('getos'); -const { promisify } = require('util'); const getOS = promisify(getos); -const os = require('os'); -const get = require('lodash.get'); -const graphiteUtil = require('../../support/tsdbUtil'); -const helpers = require('../../support/helpers'); +export default class LatestStorerPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'lateststorer', options, context, queue }); + } -module.exports = { open(context, options) { this.storageManager = context.storageManager; this.alias = {}; this.options = options; this.context = context; - }, + } async processMessage(message) { switch (message.type) { // Collect alias so we can use it @@ -47,7 +52,7 @@ module.exports = { const browserData = this.browserData; const baseDir = this.storageManager.getBaseDir(); // Hack to get out of the date dir - const newPath = path.resolve(baseDir, '..'); + const newPath = resolve(baseDir, '..'); // This is a hack to get the same name as in Grafana, meaning we can // generate the path to the URL there @@ -55,14 +60,14 @@ module.exports = { (options.copyLatestFilesToBaseGraphiteNamespace ? `${options.graphite.namespace}.` : '') + - graphiteUtil.getURLAndGroup( + getURLAndGroup( options, message.group, message.url, this.options.graphite.includeQueryParams, this.alias ); - const connectivity = graphiteUtil.getConnectivity(options); + const connectivity = getConnectivity(options); if (this.useScreenshots) { let imagePath = ''; @@ -74,13 +79,13 @@ module.exports = { screenshot ) ) { - const type = screenshot.substring( + const type = screenshot.slice( screenshot.lastIndexOf('/') + 1, screenshot.lastIndexOf('.') ); imagePath = screenshot; - const imageFullPath = path.join(baseDir, imagePath); + const imageFullPath = join(baseDir, imagePath); await this.storageManager.copyFileToDir( imageFullPath, newPath + @@ -95,12 +100,12 @@ module.exports = { this.screenshotType ); } else { - // This is a user generated screenshot, we do not copy that + // do nada } } } if (options.browsertime && options.browsertime.video) { - const videoFullPath = path.join(baseDir, message.data.video); + const videoFullPath = join(baseDir, message.data.video); await this.storageManager.copyFileToDir( videoFullPath, @@ -147,14 +152,12 @@ module.exports = { } json.browser = {}; - json.browser.name = helpers.cap( - get(browserData, 'browser.name', 'unknown') - ); + json.browser.name = cap(get(browserData, 'browser.name', 'unknown')); json.browser.version = get(browserData, 'browser.version', 'unknown'); json.friendlyHTML = `${ - json.alias ? json.alias : message.url - } ${helpers.plural( + json.alias ?? message.url + } ${plural( options.browsertime.iterations, 'iteration' )} at ${json.timestamp} using ${json.browser.name} ${ @@ -189,7 +192,7 @@ module.exports = { } else { // We are testing on desktop let osInfo = osName(); - if (os.platform() === 'linux') { + if (platform() === 'linux') { const linux = await getOS(); osInfo = `${linux.dist} ${linux.release}`; } @@ -211,7 +214,7 @@ module.exports = { json.result = resultURL; } - const data = JSON.stringify(json, null, 0); + const data = JSON.stringify(json, undefined, 0); return this.storageManager.writeDataToDir( data, name + '.' + options.browser + '.' + connectivity + '.json', @@ -221,4 +224,4 @@ module.exports = { } } } -}; +} diff --git a/lib/plugins/matrix/cli.js b/lib/plugins/matrix/cli.js deleted file mode 100644 index 572d1d397..000000000 --- a/lib/plugins/matrix/cli.js +++ /dev/null @@ -1,31 +0,0 @@ -const { messageTypes } = require('.'); -module.exports = { - host: { - describe: 'The Matrix host.', - group: 'Matrix' - }, - accessToken: { - describe: 'The Matrix access token.', - group: 'Matrix' - }, - room: { - describe: - 'The default Matrix room. It is alsways used. You can override the room per message type using --matrix.rooms', - group: 'Matrix' - }, - messages: { - describe: - 'Choose what type of message to send to Matrix. There are two types of messages: Error messages and budget messages. Errors are errors that happens through the tests (failures like strarting a test) and budget is test failing against your budget.', - choices: messageTypes, - default: messageTypes, - group: 'Matrix' - }, - - rooms: { - describe: - 'Send messages to different rooms. Current message types are [' + - messageTypes + - ']. If you want to send error messages to a specific room use --matrix.rooms.error ROOM', - group: 'Matrix' - } -}; diff --git a/lib/plugins/matrix/index.js b/lib/plugins/matrix/index.js index a19b451f5..59f3c2458 100644 --- a/lib/plugins/matrix/index.js +++ b/lib/plugins/matrix/index.js @@ -1,33 +1,31 @@ -'use strict'; +import intel from 'intel'; +import get from 'lodash.get'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; -const throwIfMissing = require('../../support/util').throwIfMissing; -const log = require('intel').getLogger('sitespeedio.plugin.matrix'); -const path = require('path'); -const get = require('lodash.get'); -const cliUtil = require('../../cli/util'); -const send = require('./send'); +import send from './send.js'; +import { throwIfMissing } from '../../support/util.js'; + +const log = intel.getLogger('sitespeedio.plugin.matrix'); function getBrowserData(data) { - if (data && data.browser) { - return `${data.browser.name} ${data.browser.version} ${get( - data, - 'android.model', - '' - )} ${get(data, 'android.androidVersion', '')} ${get( - data, - 'android.id', - '' - )} `; - } else return ''; + return data && data.browser + ? `${data.browser.name} ${data.browser.version} ${get( + data, + 'android.model', + '' + )} ${get(data, 'android.androidVersion', '')} ${get( + data, + 'android.id', + '' + )} ` + : ''; } -module.exports = { - name() { - return path.basename(__dirname); - }, - get cliOptions() { - return require(path.resolve(__dirname, 'cli.js')); - }, +export default class MatrixPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'matrix', options, context, queue }); + } + open(context, options = {}) { this.matrixOptions = options.matrix || {}; this.options = options; @@ -37,7 +35,8 @@ module.exports = { this.errorTexts = ''; this.waitForUpload = false; this.alias = {}; - }, + } + async processMessage(message) { const options = this.matrixOptions; switch (message.type) { @@ -67,7 +66,7 @@ module.exports = { this.errorTexts ); log.debug('Got %j from the matrix server', answer); - } catch (e) { + } catch { // TODO what todo? } } @@ -82,17 +81,15 @@ module.exports = { case 'error': { // We can send too many messages to Matrix and get 429 so instead // we bulk send them all one time - if (options.messages.indexOf('error') > -1) { + if (options.messages.includes('error')) { this.errorTexts += `⚠️ Error from ${ message.source - } testing ${message.url ? message.url : ''}
${
-            message.data
-          }
`; + } testing ${message.url || ''}
${message.data}
`; } break; } case 'budget.result': { - if (options.messages.indexOf('budget') > -1) { + if (options.messages.includes('budget')) { let text = ''; // We have failing URLs in the budget if (Object.keys(message.data.failing).length > 0) { @@ -103,19 +100,17 @@ module.exports = { }${getBrowserData(this.browserData)}

`; for (let url of failingURLs) { text += `
❌ ${url}`; - if (this.resultUrls.hasBaseUrl()) { - text += ` (result - screenshot)
`; - } else { - text += ''; - } + text += this.resultUrls.hasBaseUrl() + ? ` (result - screenshot)` + : ''; text += '
    '; for (let failing of message.data.failing[url]) { text += `
  • ${failing.metric} : ${failing.friendlyValue} (${failing.friendlyLimit})
  • `; @@ -155,18 +150,15 @@ module.exports = { case 'scp.finished': case 'ftp.finished': case 's3.finished': { - if (this.waitForUpload && options.messages.indexOf('budget') > -1) { + if (this.waitForUpload && options.messages.includes('budget')) { const room = get(options, 'rooms.budget', options.room); await send(options.host, room, options.accessToken, this.budgetText); } break; } } - }, - get config() { - return cliUtil.pluginDefaults(this.cliOptions); - }, - get messageTypes() { - return ['error', 'budget']; } -}; +} +export function messageTypes() { + return ['error', 'budget']; +} diff --git a/lib/plugins/matrix/send.js b/lib/plugins/matrix/send.js index 4d47f2eb8..058978b86 100644 --- a/lib/plugins/matrix/send.js +++ b/lib/plugins/matrix/send.js @@ -1,7 +1,6 @@ -'use strict'; - -const https = require('https'); -const log = require('intel').getLogger('sitespeedio.plugin.matrix'); +import { request as _request } from 'node:https'; +import intel from 'intel'; +const log = intel.getLogger('sitespeedio.plugin.matrix'); function send( host, @@ -12,9 +11,9 @@ function send( retries = 3, backoff = 5000 ) { - const retryCodes = [408, 429, 500, 503]; + const retryCodes = new Set([408, 429, 500, 503]); return new Promise((resolve, reject) => { - const req = https.request( + const request = _request( { host, port: 443, @@ -26,9 +25,9 @@ function send( method: 'POST' }, res => { - const { statusCode } = res; + const { statusCode, statusMessage } = res; if (statusCode < 200 || statusCode > 299) { - if (retries > 0 && retryCodes.includes(statusCode)) { + if (retries > 0 && retryCodes.has(statusCode)) { setTimeout(() => { return send( host, @@ -42,9 +41,9 @@ function send( }, backoff); } else { log.error( - `Got error from Matrix. Error Code: ${res.statusCode} Message: ${res.statusMessage}` + `Got error from Matrix. Error Code: ${statusCode} Message: ${statusMessage}` ); - reject(new Error(`Status Code: ${res.statusCode}`)); + reject(new Error(`Status Code: ${statusCode}`)); } } else { const data = []; @@ -57,12 +56,12 @@ function send( } } ); - req.write(JSON.stringify(data)); - req.end(); + request.write(JSON.stringify(data)); + request.end(); }); } -module.exports = async (host, room, accessToken, message) => { +export default async (host, room, accessToken, message) => { const data = { msgtype: 'm.notice', body: '', diff --git a/lib/plugins/messagelogger/index.js b/lib/plugins/messagelogger/index.js index b6339d158..3c978c9b9 100644 --- a/lib/plugins/messagelogger/index.js +++ b/lib/plugins/messagelogger/index.js @@ -1,18 +1,20 @@ -'use strict'; - /* eslint no-console:0 */ -const log = require('intel').getLogger('sitespeedio.plugin.messagelogger'); -const isEmpty = require('lodash.isempty'); +import intel from 'intel'; +import isEmpty from 'lodash.isempty'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; + +const log = intel.getLogger('sitespeedio.plugin.messagelogger'); function shortenData(key, value) { if (key === 'data' && !isEmpty(value)) { switch (typeof value) { - case 'object': + case 'object': { return Array.isArray(value) ? '[...]' : '{...}'; + } case 'string': { if (value.length > 100) { - return value.substring(0, 97) + '...'; + return value.slice(0, 97) + '...'; } } } @@ -20,26 +22,32 @@ function shortenData(key, value) { return value; } -module.exports = { +export default class MessageLoggerPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'messagelogger', options, context, queue }); + } + open(context, options) { this.verbose = Number(options.verbose || 0); - }, + } processMessage(message) { - let replacerFunc; + let replacerFunction; switch (message.type) { case 'browsertime.har': case 'browsertime.run': case 'domains.summary': case 'webpagetest.pageSummary': - case 'browsertime.screenshot': - replacerFunc = this.verbose > 1 ? null : shortenData; + case 'browsertime.screenshot': { + replacerFunction = this.verbose > 1 ? undefined : shortenData; break; + } - default: - replacerFunc = this.verbose > 0 ? null : shortenData; + default: { + replacerFunction = this.verbose > 0 ? undefined : shortenData; + } } - log.info(JSON.stringify(message, replacerFunc, 2)); + log.info(JSON.stringify(message, replacerFunction, 2)); } -}; +} diff --git a/lib/plugins/metrics/index.js b/lib/plugins/metrics/index.js index a27237457..f0f4f4b40 100644 --- a/lib/plugins/metrics/index.js +++ b/lib/plugins/metrics/index.js @@ -1,20 +1,24 @@ -'use strict'; - -const flatten = require('../../support/flattenMessage'); -const merge = require('lodash.merge'); +import merge from 'lodash.merge'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +import { flattenMessageData } from '../../support/flattenMessage.js'; const defaultConfig = { list: false, filterList: false }; -module.exports = { +export default class MetricsPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'metrics', options, context, queue }); + } + open(context, options) { this.options = merge({}, defaultConfig, options.metrics); this.metrics = {}; this.storageManager = context.storageManager; this.filterRegistry = context.filterRegistry; - }, + } + processMessage(message) { const filterRegistry = this.filterRegistry; @@ -38,40 +42,38 @@ module.exports = { } // only dance if we all wants to - if (this.options.filter) { - if (message.type === 'sitespeedio.setup') { - const filters = Array.isArray(this.options.filter) - ? this.options.filter - : [this.options.filter]; + if (this.options.filter && message.type === 'sitespeedio.setup') { + const filters = Array.isArray(this.options.filter) + ? this.options.filter + : [this.options.filter]; - for (let metric of filters) { - // for all filters - // cleaning all filters means (right now) that all - // metrics are sent - if (metric === '*+') { - filterRegistry.clearAll(); - } else if (metric === '*-') { - // all registered types will be set as unmatching, - // use it if you want to have a clean filter where - // all types are removed and then you can add your own - let types = filterRegistry.getTypes(); - filterRegistry.clearAll(); - for (let type of types) { - filterRegistry.registerFilterForType('-', type); - } - } else { - let parts = metric.split('.'); - // the type is "always" the first two - let type = parts.shift() + '.' + parts.shift(); - let filter = parts.join('.'); - let oldFilter = filterRegistry.getFilterForType(type); - if (oldFilter && typeof oldFilter === 'object') { - oldFilter.push(filter); - } else { - oldFilter = [filter]; - } - filterRegistry.registerFilterForType(oldFilter, type); + for (let metric of filters) { + // for all filters + // cleaning all filters means (right now) that all + // metrics are sent + if (metric === '*+') { + filterRegistry.clearAll(); + } else if (metric === '*-') { + // all registered types will be set as unmatching, + // use it if you want to have a clean filter where + // all types are removed and then you can add your own + let types = filterRegistry.getTypes(); + filterRegistry.clearAll(); + for (let type of types) { + filterRegistry.registerFilterForType('-', type); } + } else { + let parts = metric.split('.'); + // the type is "always" the first two + let type = parts.shift() + '.' + parts.shift(); + let filter = parts.join('.'); + let oldFilter = filterRegistry.getFilterForType(type); + if (oldFilter && typeof oldFilter === 'object') { + oldFilter.push(filter); + } else { + oldFilter = [filter]; + } + filterRegistry.registerFilterForType(oldFilter, type); } } } @@ -86,11 +88,11 @@ module.exports = { ) { return; } - let flattenMess = flatten.flattenMessageData(message); + let flattenMess = flattenMessageData(message); for (let key of Object.keys(flattenMess)) { this.metrics[message.type + '.' + key] = 1; } } - }, - config: defaultConfig -}; + } +} +export const config = defaultConfig; diff --git a/lib/plugins/pagexray/index.js b/lib/plugins/pagexray/index.js index acbde3658..3d2f3b532 100644 --- a/lib/plugins/pagexray/index.js +++ b/lib/plugins/pagexray/index.js @@ -1,9 +1,15 @@ -'use strict'; -const pagexrayAggregator = require('./pagexrayAggregator'); -const pagexray = require('coach-core').getPageXray(); -const urlParser = require('url'); -const log = require('intel').getLogger('plugin.pagexray'); -const h = require('../../support/helpers'); +import { parse } from 'node:url'; +import intel from 'intel'; +import coach from 'coach-core'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; + +import { PageXrayAggregator } from './pagexrayAggregator.js'; +import { short } from '../../support/helpers/index.js'; + +const { getPageXray } = coach; +const pagexray = getPageXray(); +const log = intel.getLogger('plugin.pagexray'); + const DEFAULT_PAGEXRAY_PAGESUMMARY_METRICS = [ 'contentTypes', 'transferSize', @@ -33,11 +39,17 @@ const DEFAULT_PAGEXRAY_SUMMARY_METRICS = [ const DEFAULT_PAGEXRAY_RUN_METRICS = []; -module.exports = { +export default class PageXrayPlugin extends SitespeedioPlugin { + constructor(options, context) { + super({ name: 'pagexray', options, context }); + } + open(context, options) { this.options = options; this.make = context.messageMaker('pagexray').make; + this.pageXrayAggregator = new PageXrayAggregator(); + context.filterRegistry.registerFilterForType( DEFAULT_PAGEXRAY_PAGESUMMARY_METRICS, 'pagexray.pageSummary' @@ -54,7 +66,7 @@ module.exports = { this.usingWebpagetest = false; this.usingBrowsertime = false; this.multi = options.multi; - }, + } processMessage(message, queue) { const make = this.make; switch (message.type) { @@ -75,9 +87,7 @@ module.exports = { const group = message.group; let config = { includeAssets: true, - firstParty: this.options.firstParty - ? this.options.firstParty - : undefined + firstParty: this.options.firstParty ?? undefined }; const pageSummary = pagexray.convert(message.data, config); //check and print any http server error > 399 @@ -88,20 +98,20 @@ module.exports = { log.info( `The server responded with a ${ asset.status - } status code for ${h.short(asset.url, 60)}` + } status code for ${short(asset.url, 60)}` ); } } } } - pagexrayAggregator.addToAggregate(pageSummary, group); + this.pageXrayAggregator.addToAggregate(pageSummary, group); if (this.multi) { // The HAR file can have multiple URLs const sentURL = {}; for (let summary of pageSummary) { // The group can be different so take it per url - const myGroup = urlParser.parse(summary.url).hostname; + const myGroup = parse(summary.url).hostname; if (!sentURL[summary.url]) { sentURL[summary.url] = 1; queue.postMessage( @@ -149,7 +159,7 @@ module.exports = { case 'sitespeedio.summarize': { log.debug('Generate summary metrics from PageXray'); - let pagexraySummary = pagexrayAggregator.summarize(); + let pagexraySummary = this.pageXrayAggregator.summarize(); if (pagexraySummary) { for (let group of Object.keys(pagexraySummary.groups)) { queue.postMessage( @@ -161,4 +171,4 @@ module.exports = { } } } -}; +} diff --git a/lib/plugins/pagexray/pagexrayAggregator.js b/lib/plugins/pagexray/pagexrayAggregator.js index 2598d154d..83b2d7661 100644 --- a/lib/plugins/pagexray/pagexrayAggregator.js +++ b/lib/plugins/pagexray/pagexrayAggregator.js @@ -1,13 +1,15 @@ -'use strict'; +import forEach from 'lodash.foreach'; -const statsHelpers = require('../../support/statsHelpers'), - forEach = require('lodash.foreach'); +import { pushGroupStats, setStatsSummary } from '../../support/statsHelpers.js'; const METRIC_NAMES = ['transferSize', 'contentSize', 'requests']; -module.exports = { - stats: {}, - groups: {}, +export class PageXrayAggregator { + constructor() { + this.stats = {}; + this.groups = {}; + } + addToAggregate(pageSummary, group) { if (this.groups[group] === undefined) { this.groups[group] = {}; @@ -16,52 +18,56 @@ module.exports = { let stats = this.stats; let groups = this.groups; - pageSummary.forEach(function (summary) { + for (const summary of pageSummary) { // stats for the whole page - METRIC_NAMES.forEach(function (metric) { + for (const metric of METRIC_NAMES) { // There's a bug in Firefox/https://github.com/devtools-html/har-export-trigger // that sometimes generate content size that is null, see // https://github.com/sitespeedio/sitespeed.io/issues/2090 - if (!isNaN(summary[metric])) { - statsHelpers.pushGroupStats( - stats, - groups[group], - metric, - summary[metric] - ); + if (!Number.isNaN(summary[metric])) { + pushGroupStats(stats, groups[group], metric, summary[metric]); } - }); + } - Object.keys(summary.contentTypes).forEach(function (contentType) { - METRIC_NAMES.forEach(function (metric) { + for (const contentType of Object.keys(summary.contentTypes)) { + for (const metric of METRIC_NAMES) { // There's a bug in Firefox/https://github.com/devtools-html/har-export-trigger // that sometimes generate content size that is null, see // https://github.com/sitespeedio/sitespeed.io/issues/2090 - if (!isNaN(summary.contentTypes[contentType][metric])) { - statsHelpers.pushGroupStats( + if (!Number.isNaN(summary.contentTypes[contentType][metric])) { + pushGroupStats( stats, groups[group], 'contentTypes.' + contentType + '.' + metric, summary.contentTypes[contentType][metric] ); } - }); - }); + } + } - Object.keys(summary.responseCodes).forEach(function (responseCode) { - statsHelpers.pushGroupStats( + for (const responseCode of Object.keys(summary.responseCodes)) { + pushGroupStats( stats, groups[group], 'responseCodes.' + responseCode, summary.responseCodes[responseCode] ); - }); + } + /* + for (const responseCode of Object.keys(summary.responseCodes)) { + pushGroupStats( + stats, + groups[group], + 'responseCodes.' + responseCode, + summary.responseCodes[responseCode] + ); + }*/ // extras for firstParty vs third if (summary.firstParty.requests) { - METRIC_NAMES.forEach(function (metric) { + for (const metric of METRIC_NAMES) { if (summary.firstParty[metric] !== undefined) { - statsHelpers.pushGroupStats( + pushGroupStats( stats, groups[group], 'firstParty' + '.' + metric, @@ -69,18 +75,18 @@ module.exports = { ); } if (summary.thirdParty[metric] !== undefined) { - statsHelpers.pushGroupStats( + pushGroupStats( stats, groups[group], 'thirdParty' + '.' + metric, summary.thirdParty[metric] ); } - }); + } } // Add the total amount of domains on this page - statsHelpers.pushGroupStats( + pushGroupStats( stats, groups[group], 'domains', @@ -88,32 +94,22 @@ module.exports = { ); // And the total amounts of cookies - statsHelpers.pushGroupStats( - stats, - groups[group], - 'cookies', - summary.cookies - ); + pushGroupStats(stats, groups[group], 'cookies', summary.cookies); forEach(summary.assets, asset => { - statsHelpers.pushGroupStats( - stats, - groups[group], - 'expireStats', - asset.expires - ); - statsHelpers.pushGroupStats( + pushGroupStats(stats, groups[group], 'expireStats', asset.expires); + pushGroupStats( stats, groups[group], 'lastModifiedStats', asset.timeSinceLastModified ); }); - }); - }, + } + } summarize() { if (Object.keys(this.stats).length === 0) { - return undefined; + return; } const total = this.summarizePerObject(this.stats); @@ -128,37 +124,34 @@ module.exports = { summary.groups[group] = this.summarizePerObject(this.groups[group]); } return summary; - }, + } summarizePerObject(type) { return Object.keys(type).reduce((summary, name) => { if ( - METRIC_NAMES.indexOf(name) > -1 || - name.match(/(^domains|^expireStats|^lastModifiedStats|^cookies)/) + METRIC_NAMES.includes(name) || + /(^domains|^expireStats|^lastModifiedStats|^cookies)/.test(name) ) { - statsHelpers.setStatsSummary(summary, name, type[name]); + setStatsSummary(summary, name, type[name]); } else { if (name === 'contentTypes') { const contentTypeData = {}; forEach(Object.keys(type[name]), contentType => { forEach(type[name][contentType], (stats, metric) => { - statsHelpers.setStatsSummary( - contentTypeData, - [contentType, metric], - stats - ); + setStatsSummary(contentTypeData, [contentType, metric], stats); }); }); summary[name] = contentTypeData; } else if (name === 'responseCodes') { const responseCodeData = {}; - type.responseCodes.forEach((stats, metric) => { - statsHelpers.setStatsSummary(responseCodeData, metric, stats); - }); + for (const [metric, stats] of type.responseCodes.entries()) { + if (stats != undefined) + setStatsSummary(responseCodeData, metric, stats); + } summary[name] = responseCodeData; } else { const data = {}; forEach(type[name], (stats, metric) => { - statsHelpers.setStatsSummary(data, metric, stats); + setStatsSummary(data, metric, stats); }); summary[name] = data; } @@ -166,4 +159,4 @@ module.exports = { return summary; }, {}); } -}; +} diff --git a/lib/plugins/remove/index.js b/lib/plugins/remove/index.js index 0d5f3f50c..38192d4c6 100644 --- a/lib/plugins/remove/index.js +++ b/lib/plugins/remove/index.js @@ -1,12 +1,16 @@ -'use strict'; +import intel from 'intel'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; -const log = require('intel').getLogger('sitespeedio.plugin.remove'); +const log = intel.getLogger('sitespeedio.plugin.remove'); -module.exports = { +export default class RemovePlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'remove', options, context, queue }); + } open(context, options) { this.storageManager = context.storageManager; this.options = options; - }, + } async processMessage(message) { switch (message.type) { case 'remove.url': { @@ -16,4 +20,4 @@ module.exports = { } } } -}; +} diff --git a/lib/plugins/s3/contentType.js b/lib/plugins/s3/contentType.js index facbb7434..57eea6c89 100644 --- a/lib/plugins/s3/contentType.js +++ b/lib/plugins/s3/contentType.js @@ -1,5 +1,3 @@ -'use strict'; - const DEFAULT_CONTENT_TYPE = 'application/octet-stream'; const types = { @@ -19,12 +17,10 @@ const types = { log: 'text/plain' }; -function getExt(filename) { +function getExtension(filename) { return filename.split('.').pop(); } -module.exports = { - getContentType(file) { - return types[getExt(file)] || DEFAULT_CONTENT_TYPE; - } -}; +export function getContentType(file) { + return types[getExtension(file)] || DEFAULT_CONTENT_TYPE; +} diff --git a/lib/plugins/s3/index.js b/lib/plugins/s3/index.js index 4be8a9d1b..7dafb6248 100644 --- a/lib/plugins/s3/index.js +++ b/lib/plugins/s3/index.js @@ -1,19 +1,25 @@ -'use strict'; +import { relative, join, resolve as _resolve, sep } from 'node:path'; -const fs = require('fs-extra'); -const path = require('path'); -const AWS = require('aws-sdk'); -const readdir = require('recursive-readdir'); -const pLimit = require('p-limit'); +import { createReadStream, remove } from 'fs-extra'; +import { Endpoint, S3 } from 'aws-sdk'; +import readdir from 'recursive-readdir'; +import pLimit from 'p-limit'; +import intel from 'intel'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; -const log = require('intel').getLogger('sitespeedio.plugin.s3'); -const throwIfMissing = require('../../support/util').throwIfMissing; -const { getContentType } = require('./contentType'); +import { throwIfMissing } from '../../support/util'; +import { getContentType } from './contentType'; + +const log = intel.getLogger('sitespeedio.plugin.s3'); + +function ignoreDirectories(file, stats) { + return stats.isDirectory(); +} function createS3(s3Options) { let endpoint = s3Options.endpoint || 's3.amazonaws.com'; const options = { - endpoint: new AWS.Endpoint(endpoint), + endpoint: new Endpoint(endpoint), accessKeyId: s3Options.key, secretAccessKey: s3Options.secret, signatureVersion: 'v4' @@ -21,7 +27,7 @@ function createS3(s3Options) { // You can also set some extra options see // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property Object.assign(options, s3Options.options); - return new AWS.S3(options); + return new S3(options); } async function upload(dir, s3Options, prefix) { @@ -38,12 +44,8 @@ async function upload(dir, s3Options, prefix) { } async function uploadLatestFiles(dir, s3Options, prefix) { - function ignoreDirs(file, stats) { - return stats.isDirectory(); - } - const s3 = createS3(s3Options); - const files = await readdir(dir, [ignoreDirs]); + const files = await readdir(dir, [ignoreDirectories]); // Backward compability naming for old S3 plugin const limit = pLimit(s3Options.maxAsyncS3 || 20); const promises = []; @@ -55,39 +57,42 @@ async function uploadLatestFiles(dir, s3Options, prefix) { } async function uploadFile(file, s3, s3Options, prefix, baseDir) { - const stream = fs.createReadStream(file); + const stream = createReadStream(file); const contentType = getContentType(file); return new Promise((resolve, reject) => { - const onUpload = err => { - if (err) { - reject(err); + const onUpload = error => { + if (error) { + reject(error); } else { resolve(); } }; const options = { partSize: 10 * 1024 * 1024, queueSize: 1 }; // See https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property - const subPath = path.relative(baseDir, file); - const params = { + const subPath = relative(baseDir, file); + const parameters = { Body: stream, Bucket: s3Options.bucketname, ContentType: contentType, - Key: path.join(s3Options.path || prefix, subPath), + Key: join(s3Options.path || prefix, subPath), StorageClass: s3Options.storageClass || 'STANDARD' }; if (s3Options.acl) { - params.ACL = s3Options.acl; + parameters.ACL = s3Options.acl; } // Override/set all the extra options you need // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property - Object.assign(params, s3Options.params); + Object.assign(parameters, s3Options.params); - s3.upload(params, options, onUpload); + s3.upload(parameters, options, onUpload); }); } -module.exports = { +export default class S3Plugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 's3', options, context, queue }); + } open(context, options) { this.s3Options = options.s3; this.options = options; @@ -97,8 +102,7 @@ module.exports = { throwIfMissing(this.s3Options, ['key', 'secret'], 's3'); } this.storageManager = context.storageManager; - }, - + } async processMessage(message, queue) { if (message.type === 'sitespeedio.setup') { // Let other plugins know that the s3 plugin is alive @@ -119,21 +123,21 @@ module.exports = { this.storageManager.getStoragePrefix() ); if (this.options.copyLatestFilesToBase) { - const rootPath = path.resolve(baseDir, '..'); - const dirsAsArray = rootPath.split(path.sep); - const rootName = dirsAsArray.slice(-1)[0]; + const rootPath = _resolve(baseDir, '..'); + const directoriesAsArray = rootPath.split(sep); + const rootName = directoriesAsArray.slice(-1)[0]; await uploadLatestFiles(rootPath, s3Options, rootName); } log.info('Finished upload to s3'); if (s3Options.removeLocalResult) { - await fs.remove(baseDir); + await remove(baseDir); log.debug(`Removed local files and directory ${baseDir}`); } - } catch (e) { - queue.postMessage(make('error', e)); - log.error('Could not upload to S3', e); + } catch (error) { + queue.postMessage(make('error', error)); + log.error('Could not upload to S3', error); } queue.postMessage(make('s3.finished')); } } -}; +} diff --git a/lib/plugins/scp/index.js b/lib/plugins/scp/index.js index dff2f5323..2344dc148 100644 --- a/lib/plugins/scp/index.js +++ b/lib/plugins/scp/index.js @@ -1,11 +1,13 @@ -'use strict'; +import { join, basename, resolve } from 'node:path'; -const fs = require('fs-extra'); -const path = require('path'); -const { Client } = require('node-scp'); -const readdir = require('recursive-readdir'); -const log = require('intel').getLogger('sitespeedio.plugin.scp'); -const throwIfMissing = require('../../support/util').throwIfMissing; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +import { readFileSync, remove } from 'fs-extra'; +import { Client } from 'node-scp'; +import readdir from 'recursive-readdir'; +import intel from 'intel'; +import { throwIfMissing } from '../../support/util'; + +const log = intel.getLogger('sitespeedio.plugin.scp'); async function getClient(scpOptions) { const options = { @@ -19,7 +21,7 @@ async function getClient(scpOptions) { options.password = scpOptions.password; } if (scpOptions.privateKey) { - options.privateKey = fs.readFileSync(scpOptions.privateKey); + options.privateKey = readFileSync(scpOptions.privateKey); } if (scpOptions.passphrase) { options.passphrase = scpOptions.passphrase; @@ -31,21 +33,21 @@ async function upload(dir, scpOptions, prefix) { let client; try { client = await getClient(scpOptions); - const dirs = prefix.split('/'); + const directories = prefix.split('/'); let fullPath = ''; - for (let dir of dirs) { + for (let dir of directories) { fullPath += dir + '/'; const doThePathExist = await client.exists( - path.join(scpOptions.destinationPath, fullPath) + join(scpOptions.destinationPath, fullPath) ); if (!doThePathExist) { - await client.mkdir(path.join(scpOptions.destinationPath, fullPath)); + await client.mkdir(join(scpOptions.destinationPath, fullPath)); } } - await client.uploadDir(dir, path.join(scpOptions.destinationPath, prefix)); - } catch (e) { - log.error(e); - throw e; + await client.uploadDir(dir, join(scpOptions.destinationPath, prefix)); + } catch (error) { + log.error(error); + throw error; } finally { if (client) { client.close(); @@ -60,12 +62,12 @@ async function uploadFiles(files, scpOptions, prefix) { for (let file of files) { await client.uploadFile( file, - path.join(scpOptions.destinationPath, prefix, path.basename(file)) + join(scpOptions.destinationPath, prefix, basename(file)) ); } - } catch (e) { - log.error(e); - throw e; + } catch (error) { + log.error(error); + throw error; } finally { if (client) { client.close(); @@ -73,16 +75,20 @@ async function uploadFiles(files, scpOptions, prefix) { } } +function ignoreDirectories(file, stats) { + return stats.isDirectory(); +} + async function uploadLatestFiles(dir, scpOptions, prefix) { - function ignoreDirs(file, stats) { - return stats.isDirectory(); - } - const files = await readdir(dir, [ignoreDirs]); + const files = await readdir(dir, [ignoreDirectories]); return uploadFiles(files, scpOptions, prefix); } -module.exports = { +export default class ScpPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'scp', options, context, queue }); + } open(context, options) { this.scpOptions = options.scp; this.options = options; @@ -93,8 +99,7 @@ module.exports = { 'scp' ); this.storageManager = context.storageManager; - }, - + } async processMessage(message, queue) { if (message.type === 'sitespeedio.setup') { // Let other plugins know that the scp plugin is alive @@ -114,21 +119,21 @@ module.exports = { this.storageManager.getStoragePrefix() ); if (this.options.copyLatestFilesToBase) { - const rootPath = path.resolve(baseDir, '..'); + const rootPath = resolve(baseDir, '..'); const prefix = this.storageManager.getStoragePrefix(); const firstPart = prefix.split('/')[0]; await uploadLatestFiles(rootPath, this.scpOptions, firstPart); } log.info('Finished upload using scp'); if (this.scpOptions.removeLocalResult) { - await fs.remove(baseDir); + await remove(baseDir); log.debug(`Removed local files and directory ${baseDir}`); } - } catch (e) { - queue.postMessage(make('error', e)); - log.error('Could not upload using scp', e); + } catch (error) { + queue.postMessage(make('error', error)); + log.error('Could not upload using scp', error); } queue.postMessage(make('scp.finished')); } } -}; +} diff --git a/lib/plugins/slack/attachements.js b/lib/plugins/slack/attachements.js index b87d8827d..115d441de 100644 --- a/lib/plugins/slack/attachements.js +++ b/lib/plugins/slack/attachements.js @@ -1,17 +1,13 @@ -'use strict'; - -const get = require('lodash.get'); -const h = require('../../support/helpers'); +import get from 'lodash.get'; +import { time, noop, size } from '../../support/helpers/index.js'; function getMetric(metric, f) { - if (metric.median) { - return f(metric.median) + ' (' + f(metric.max) + ')'; - } else { - return f(metric); - } + return metric.median + ? f(metric.median) + ' (' + f(metric.max) + ')' + : f(metric); } -module.exports = function ( +export function getAttachements( dataCollector, resultUrls, slackOptions, @@ -33,7 +29,7 @@ module.exports = function ( base.browsertime, 'pageSummary.statistics.timings.firstPaint' ), - f: h.time.ms + f: time.ms }, speedIndex: { name: 'Speed Index', @@ -41,7 +37,7 @@ module.exports = function ( base.browsertime, 'pageSummary.statistics.visualMetrics.SpeedIndex' ), - f: h.time.ms + f: time.ms }, firstVisualChange: { name: 'First Visual Change', @@ -49,7 +45,7 @@ module.exports = function ( base.browsertime, 'pageSummary.statistics.visualMetrics.FirstVisualChange' ), - f: h.time.ms + f: time.ms }, visualComplete85: { name: 'Visual Complete 85%', @@ -57,7 +53,7 @@ module.exports = function ( base.browsertime, 'pageSummary.statistics.visualMetrics.VisualComplete85' ), - f: h.time.ms + f: time.ms }, lastVisualChange: { name: 'Last Visual Change', @@ -65,7 +61,7 @@ module.exports = function ( base.browsertime, 'pageSummary.statistics.visualMetrics.LastVisualChange' ), - f: h.time.ms + f: time.ms }, fullyLoaded: { name: 'Fully Loaded', @@ -73,7 +69,7 @@ module.exports = function ( base.browsertime, 'pageSummary.statistics.timings.fullyLoaded' ), - f: h.time.ms + f: time.ms }, domContentLoadedTime: { name: 'domContentLoadedTime', @@ -81,22 +77,22 @@ module.exports = function ( base.browsertime, 'pageSummary.statistics.timings.pageTimings.domContentLoadedTime' ), - f: h.time.ms + f: time.ms }, coachScore: { name: 'Coach score', metric: get(base.coach, 'pageSummary.advice.performance.score'), - f: h.noop + f: noop }, transferSize: { name: 'Page transfer size', metric: get(base.pagexray, 'pageSummary.transferSize'), - f: h.size.format + f: size.format }, transferRequests: { name: 'Requests', metric: get(base.pagexray, 'pageSummary.requests'), - f: h.noop + f: noop } }; @@ -169,4 +165,4 @@ module.exports = function ( attachments.push(attachement); } return attachments; -}; +} diff --git a/lib/plugins/slack/dataCollector.js b/lib/plugins/slack/dataCollector.js index ae8963aad..d602f965d 100644 --- a/lib/plugins/slack/dataCollector.js +++ b/lib/plugins/slack/dataCollector.js @@ -1,10 +1,8 @@ -'use strict'; +import merge from 'lodash.merge'; +import get from 'lodash.get'; +import set from 'lodash.set'; -const merge = require('lodash.merge'), - get = require('lodash.get'), - set = require('lodash.set'); - -class DataCollector { +export class DataCollector { constructor(context) { this.resultUrls = context.resultUrls; this.urlRunPages = {}; @@ -79,5 +77,3 @@ class DataCollector { merge(this.summaryPage, data); } } - -module.exports = DataCollector; diff --git a/lib/plugins/slack/index.js b/lib/plugins/slack/index.js index aea1156f3..d97c2060a 100644 --- a/lib/plugins/slack/index.js +++ b/lib/plugins/slack/index.js @@ -1,14 +1,17 @@ -'use strict'; +import { promisify } from 'node:util'; -const throwIfMissing = require('../../support/util').throwIfMissing; -const log = require('intel').getLogger('sitespeedio.plugin.slack'); -const Slack = require('node-slack'); -const merge = require('lodash.merge'); -const set = require('lodash.set'); -const DataCollector = require('./dataCollector'); -const getAttachments = require('./attachements'); -const getSummary = require('./summary'); -const { promisify } = require('util'); +import intel from 'intel'; +import Slack from 'node-slack'; +import merge from 'lodash.merge'; +import set from 'lodash.set'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; + +import { DataCollector } from './dataCollector.js'; +import { getAttachements } from './attachements.js'; +import { getSummary } from './summary.js'; +import { throwIfMissing } from '../../support/util.js'; + +const log = intel.getLogger('sitespeedio.plugin.slack'); const defaultConfig = { userName: 'Sitespeed.io', @@ -55,7 +58,7 @@ function send(options, dataCollector, context, screenshotType, alias) { let attachments = []; if (['url', 'all', 'error'].includes(type)) { - attachments = getAttachments( + attachments = getAttachements( dataCollector, context.resultUrls, slackOptions, @@ -77,11 +80,11 @@ function send(options, dataCollector, context, screenshotType, alias) { mrkdwn: true, username: slackOptions.userName, attachments - }).catch(e => { - if (e.errno === 'ETIMEDOUT') { + }).catch(error => { + if (error.errno === 'ETIMEDOUT') { log.warn('Timeout sending Slack message.'); } else { - throw e; + throw error; } }); } @@ -101,7 +104,7 @@ function staticPagesProvider(options) { throwIfMissing(s3Options, ['key', 'secret'], 's3'); } return 's3'; - } catch (err) { + } catch { log.debug('s3 is not configured'); } @@ -109,14 +112,18 @@ function staticPagesProvider(options) { try { throwIfMissing(gcsOptions, ['projectId', 'key', 'bucketname'], 'gcs'); return 'gcs'; - } catch (err) { + } catch { log.debug('gcs is not configured'); } - return null; + return; } -module.exports = { +export default class SlackPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'slack', options, context, queue }); + } + open(context, options = {}) { const slackOptions = options.slack || {}; throwIfMissing(slackOptions, ['hookUrl', 'userName'], 'slack'); @@ -125,7 +132,7 @@ module.exports = { this.options = options; this.screenshotType; this.alias = {}; - }, + } processMessage(message) { const dataCollector = this.dataCollector; @@ -219,6 +226,6 @@ module.exports = { ); } } - }, - config: defaultConfig -}; + } +} +export const config = defaultConfig; diff --git a/lib/plugins/slack/summary.js b/lib/plugins/slack/summary.js index fc0153944..5d07fdbe1 100644 --- a/lib/plugins/slack/summary.js +++ b/lib/plugins/slack/summary.js @@ -1,16 +1,22 @@ -'use strict'; -const util = require('util'); -const get = require('lodash.get'); -const h = require('../../support/helpers'); -const tsdbUtil = require('../../support/tsdbUtil'); +import { format } from 'node:util'; +import get from 'lodash.get'; +import { + time, + noop, + size, + cap, + plural, + short +} from '../../support/helpers/index.js'; +import { getConnectivity } from '../../support/tsdbUtil.js'; -module.exports = function (dataCollector, errors, resultUrls, name, options) { +export function getSummary(dataCollector, errors, resultUrls, name, options) { const base = dataCollector.getSummary() || {}; const metrics = { firstPaint: { name: 'First paint', metric: get(base.browsertime, 'summary.firstPaint.median'), - f: h.time.ms + f: time.ms }, domContentLoadedTime: { name: 'domContentLoadedTime', @@ -18,12 +24,12 @@ module.exports = function (dataCollector, errors, resultUrls, name, options) { base.browsertime, 'summary.pageTimings.domContentLoadedTime.median' ), - f: h.time.ms + f: time.ms }, speedIndex: { name: 'Speed Index', metric: get(base.browsertime, 'summary.visualMetrics.SpeedIndex.median'), - f: h.time.ms + f: time.ms }, firstVisualChange: { name: 'First Visual Change', @@ -31,7 +37,7 @@ module.exports = function (dataCollector, errors, resultUrls, name, options) { base.browsertime, 'summary.visualMetrics.FirstVisualChange.median' ), - f: h.time.ms + f: time.ms }, visualComplete85: { name: 'Visual Complete 85%', @@ -39,7 +45,7 @@ module.exports = function (dataCollector, errors, resultUrls, name, options) { base.browsertime, 'summary.visualMetrics.VisualComplete85.median' ), - f: h.time.ms + f: time.ms }, lastVisualChange: { name: 'Last Visual Change', @@ -47,34 +53,34 @@ module.exports = function (dataCollector, errors, resultUrls, name, options) { base.browsertime, 'summary.visualMetrics.LastVisualChange.median' ), - f: h.time.ms + f: time.ms }, fullyLoaded: { name: 'Fully Loaded', metric: get(base.pagexray, 'summary.fullyLoaded.median'), - f: h.time.ms + f: time.ms }, coachScore: { name: 'Coach score', metric: get(base.coach, 'summary.performance.score.median'), - f: h.noop + f: noop }, transferSize: { name: 'Page transfer weight', metric: get(base.pagexray, 'summary.transferSize.median'), - f: h.size.format + f: size.format } }; const iterations = get(options, 'browsertime.iterations', 0); - const browser = h.cap(get(options, 'browsertime.browser', 'unknown')); - const pages = h.plural(dataCollector.getURLs().length, 'page'); - const testName = h.short(name || '', 30) || 'Unknown'; + const browser = cap(get(options, 'browsertime.browser', 'unknown')); + const pages = plural(dataCollector.getURLs().length, 'page'); + const testName = short(name || '', 30) || 'Unknown'; const device = options.mobile ? 'mobile' : 'desktop'; - const runs = h.plural(iterations, 'run'); + const runs = plural(iterations, 'run'); let summaryText = `${pages} analysed for ${testName} ` + - `(${runs}, ${browser}/${device}/${tsdbUtil.getConnectivity(options)})\n`; + `(${runs}, ${browser}/${device}/${getConnectivity(options)})\n`; let message = ''; if (resultUrls.hasBaseUrl()) { @@ -96,7 +102,7 @@ module.exports = function (dataCollector, errors, resultUrls, name, options) { let errorText = ''; if (errors.length > 0) { - errorText += util.format('%d error(s):\n', errors.length); + errorText += format('%d error(s):\n', errors.length); logo = 'https://www.sitespeed.io/img/slack/sitespeed-logo-slack-hm.png'; } @@ -105,4 +111,4 @@ module.exports = function (dataCollector, errors, resultUrls, name, options) { errorText, logo }; -}; +} diff --git a/lib/plugins/sustainable/aggregator.js b/lib/plugins/sustainable/aggregator.js index fbad17bf6..9beb6f3ec 100644 --- a/lib/plugins/sustainable/aggregator.js +++ b/lib/plugins/sustainable/aggregator.js @@ -1,8 +1,10 @@ -'use strict'; +import { + pushStats, + pushGroupStats, + setStatsSummaryWithOptions +} from '../../support/statsHelpers.js'; -const statsHelpers = require('../../support/statsHelpers'); - -class Aggregator { +export class Aggregator { constructor(options) { this.options = options; this.groups = {}; @@ -24,62 +26,54 @@ class Aggregator { this.hostingInfo[url] = data.hostingInfo; this.urlToGroup[url] = group; - statsHelpers.pushStats(this.total, ['totalCO2'], data.totalCO2); - statsHelpers.pushStats(this.total, ['co2PerPageView'], data.co2PerPageView); - statsHelpers.pushStats( - this.total, - ['co2FirstParty'], - data.co2PerParty.firstParty - ); - statsHelpers.pushStats( - this.total, - ['co2ThirdParty'], - data.co2PerParty.thirdParty - ); + pushStats(this.total, ['totalCO2'], data.totalCO2); + pushStats(this.total, ['co2PerPageView'], data.co2PerPageView); + pushStats(this.total, ['co2FirstParty'], data.co2PerParty.firstParty); + pushStats(this.total, ['co2ThirdParty'], data.co2PerParty.thirdParty); - statsHelpers.pushGroupStats( + pushGroupStats( this.urls[url], this.groups[group], ['totalCO2'], data.totalCO2 ); - statsHelpers.pushGroupStats( + pushGroupStats( this.urls[url], this.groups[group], ['co2PerPageView'], data.co2PerPageView ); - statsHelpers.pushGroupStats( + pushGroupStats( this.urls[url], this.groups[group], ['co2FirstParty'], data.co2PerParty.firstParty ); - statsHelpers.pushGroupStats( + pushGroupStats( this.urls[url], this.groups[group], ['co2ThirdParty'], data.co2PerParty.thirdParty ); - statsHelpers.pushGroupStats( + pushGroupStats( this.urls[url], this.groups[group], ['totalTransfer'], data.transferPerPage ); - statsHelpers.pushGroupStats( + pushGroupStats( this.urls[url], this.groups[group], ['firstPartyTransferPerPage'], data.firstPartyTransferPerPage ); - statsHelpers.pushGroupStats( + pushGroupStats( this.urls[url], this.groups[group], ['thirdPartyTransferPerPage'], @@ -94,7 +88,7 @@ class Aggregator { }; for (let type of Object.keys(this.total)) { - statsHelpers.setStatsSummaryWithOptions( + setStatsSummaryWithOptions( summary, ['groups', ['total'], type], this.total[type], @@ -104,7 +98,7 @@ class Aggregator { for (let url of Object.keys(this.urls)) { for (let type of Object.keys(this.urls[url])) { - statsHelpers.setStatsSummaryWithOptions( + setStatsSummaryWithOptions( summary, ['urls', url, type], this.urls[url][type], @@ -115,7 +109,7 @@ class Aggregator { for (let group of Object.keys(this.groups)) { for (let type of Object.keys(this.groups[group])) { - statsHelpers.setStatsSummaryWithOptions( + setStatsSummaryWithOptions( summary, ['groups', group, type], this.groups[group][type], @@ -127,5 +121,3 @@ class Aggregator { return summary; } } - -module.exports = Aggregator; diff --git a/lib/plugins/sustainable/index.js b/lib/plugins/sustainable/index.js index 95554a619..9cbb0ab97 100644 --- a/lib/plugins/sustainable/index.js +++ b/lib/plugins/sustainable/index.js @@ -1,16 +1,19 @@ -'use strict'; +import { resolve, join } from 'node:path'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import fs from 'node:fs'; +import zlib from 'node:zlib'; +import { promisify } from 'node:util'; -const path = require('path'); -const fs = require('fs'); -const log = require('intel').getLogger('sitespeedio.plugin.sustainable'); -const Aggregator = require('./aggregator'); +import intel from 'intel'; +import { co2, hosting } from '@tgwf/co2'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +import { Aggregator } from './aggregator.js'; -let tgwf; -try { - tgwf = require('@tgwf/co2'); -} catch (e) { - tgwf = null; -} +const readFile = promisify(fs.readFile); +const gunzip = promisify(zlib.gunzip); +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const log = intel.getLogger('sitespeedio.plugin.sustainable'); const DEFAULT_METRICS_PAGE_SUMMARY = [ 'co2PerPageView', @@ -18,209 +21,226 @@ const DEFAULT_METRICS_PAGE_SUMMARY = [ 'co2FirstParty', 'co2ThirdParty' ]; -module.exports = { + +async function streamToString(stream) { + return new Promise((resolve, reject) => { + const chunks = []; + stream.on('error', reject); + stream.on('data', chunk => chunks.push(chunk)); + stream.on('end', () => resolve(Buffer.concat(chunks))); + }); +} + +async function getGzippedFileAsJson(jsonPath) { + const readStream = fs.createReadStream(jsonPath); + const text = await streamToString(readStream); + const unzipped = await gunzip(text); + return unzipped.toString(); +} + +async function loadJSON(jsonPath) { + const jsonBuffer = jsonPath.endsWith('.gz') + ? await getGzippedFileAsJson(jsonPath) + : await readFile(jsonPath); + return JSON.parse(jsonBuffer); +} + +export default class SustainablePlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'sustainable', options, context, queue }); + } + open(context, options) { this.storageManager = context.storageManager; this.options = options; this.sustainableOptions = options.sustainable || {}; this.make = context.messageMaker('sustainable').make; - this.pug = fs.readFileSync( - path.resolve(__dirname, 'pug', 'index.pug'), - 'utf8' - ); + this.pug = readFileSync(resolve(__dirname, 'pug', 'index.pug'), 'utf8'); this.aggregator = new Aggregator(options); this.firstRunsData = {}; context.filterRegistry.registerFilterForType( DEFAULT_METRICS_PAGE_SUMMARY, 'sustainable.pageSummary' ); - }, + } async processMessage(message, queue) { - if (tgwf) { - const make = this.make; - const aggregator = this.aggregator; + const make = this.make; + const aggregator = this.aggregator; - switch (message.type) { - // When sitespeed.io starts, it sends a setup message on the queue - // That way, we can tell other plugins we exist (sustainable.setup) - // so others could build upon our data - // ... and we also register the pug file(s) for the HTML output - case 'sitespeedio.setup': { - queue.postMessage(make('sustainable.setup')); - // Add the HTML pugs - queue.postMessage( - make('html.pug', { - id: 'sustainable', - name: 'Sustainable Web', - pug: this.pug, - type: 'pageSummary' - }) - ); - queue.postMessage( - make('html.pug', { - id: 'sustainable', - name: 'Sustainable Web', - pug: this.pug, - type: 'run' - }) - ); - log.info( - 'Use the sustainable web plugin with model %s', - this.sustainableOptions.model - ); - break; - } - - case 'pagexray.run': { - // We got data for a URL, lets calculate co2, check green servers etc - const listOfDomains = Object.keys(message.data.domains); - const greenDomainJSONpath = path.join( - __dirname, - 'data', - 'url2green.json.gz' - ); - - let hostingGreenCheck; - if (this.sustainableOptions.disableHosting === true) { - hostingGreenCheck = []; - } else { - hostingGreenCheck = - this.sustainableOptions.useGreenWebHostingAPI === true - ? await tgwf.hosting.check(listOfDomains) - : await tgwf.hosting.check( - listOfDomains, - await tgwf.hosting.loadJSON(greenDomainJSONpath) - ); - } - const CO2 = new tgwf.co2(this.sustainableOptions.model); - const co2PerDomain = CO2.perDomain(message.data, hostingGreenCheck); - const baseDomain = message.data.baseDomain; - - const hostingInfo = { - green: false, - url: baseDomain - }; - - // is the base domain in our list of green domains? - if (hostingGreenCheck.indexOf(baseDomain) > -1) { - hostingInfo.green = true; - } - - const co2PerParty = CO2.perParty(message.data, hostingGreenCheck); - // Fetch the resources with the largest CO2 impact. ie, - // the resources to optimise, host somewhere green, or contact - // a supplier about - const dirtiestResources = CO2.dirtiestResources( - message.data, - hostingGreenCheck - ); - - const co2PerContentType = CO2.perContentType( - message.data, - hostingGreenCheck - ); - - const co2PerPageView = CO2.perPage(message.data, hostingGreenCheck); - - const totalCO2 = this.sustainableOptions.pageViews - ? this.sustainableOptions.pageViews * co2PerPageView - : co2PerPageView; - - const transferPerPage = message.data.transferSize; - const firstPartyTransferPerPage = - message.data.firstParty.transferSize; - const thirdPartyTransferPerPage = - message.data.thirdParty.transferSize; - - // adding these to make the link between data and CO2 clearer - // and also so we can give an idea how big this is compared to - // the norm - // let transfer1stParty, transfer3rdParty; - - if (message.iteration === 1) { - this.firstRunsData[message.url] = { - co2PerDomain, - co2PerContentType, - dirtiestResources, - hostingGreenCheck - }; - } - // We get data per run, so we want to aggregate that per page (multiple runs per page) - // and per group/domain - aggregator.addStats( - { - co2PerDomain, - co2PerPageView, - co2PerParty, - hostingInfo, - totalCO2, - transferPerPage, - firstPartyTransferPerPage, - thirdPartyTransferPerPage - }, - message.group, - message.url - ); - - // We pass on the data we have, so the that HTML plugin can generate the HTML tab - // per run - queue.postMessage( - make( - 'sustainable.run', - { - co2PerPageView, - totalCO2, - hostingInfo, - co2PerDomain, - totalTransfer: transferPerPage, - firstPartyTransferPerPage, - thirdPartyTransferPerPage, - co2FirstParty: co2PerParty.firstParty, - co2ThirdParty: co2PerParty.thirdParty, - hostingGreenCheck, - dirtiestResources, - co2PerContentType - }, - { - url: message.url, - group: message.group, - runIndex: message.runIndex - } - ) - ); // Here we put the data that we got from that run - break; - } - - case 'sitespeedio.summarize': { - // All URLs has been tested, now calculate the min/median/max c02 per page - // and push that info on the queue - const summaries = aggregator.summarize(); - - // Send each URL - for (let url of Object.keys(summaries.urls)) { - const extras = this.firstRunsData[url]; - // Attach first run so we can show that extra data that we don't collect stats for - summaries.urls[url].firstRun = extras; - queue.postMessage( - make('sustainable.pageSummary', summaries.urls[url], { - url: url, - group: summaries.urlToGroup[url] - }) - ); - } - - queue.postMessage( - make('sustainable.summary', summaries.groups['total'], { - group: 'total' - }) - ); - break; - } + switch (message.type) { + // When sitespeed.io starts, it sends a setup message on the queue + // That way, we can tell other plugins we exist (sustainable.setup) + // so others could build upon our data + // ... and we also register the pug file(s) for the HTML output + case 'sitespeedio.setup': { + queue.postMessage(make('sustainable.setup')); + // Add the HTML pugs + queue.postMessage( + make('html.pug', { + id: 'sustainable', + name: 'Sustainable Web', + pug: this.pug, + type: 'pageSummary' + }) + ); + queue.postMessage( + make('html.pug', { + id: 'sustainable', + name: 'Sustainable Web', + pug: this.pug, + type: 'run' + }) + ); + log.info( + 'Use the sustainable web plugin with model %s', + this.sustainableOptions.model + ); + break; + } + + case 'pagexray.run': { + // We got data for a URL, lets calculate co2, check green servers etc + const listOfDomains = Object.keys(message.data.domains); + const greenDomainJSONpath = join( + __dirname, + 'data', + 'url2green.json.gz' + ); + + let hostingGreenCheck; + if (this.sustainableOptions.disableHosting === true) { + hostingGreenCheck = []; + } else { + hostingGreenCheck = + this.sustainableOptions.useGreenWebHostingAPI === true + ? await hosting.check(listOfDomains) + : await hosting.check( + listOfDomains, + await loadJSON(greenDomainJSONpath) + ); + } + + const CO2 = new co2(this.sustainableOptions.model); + const co2PerDomain = CO2.perDomain(message.data, hostingGreenCheck); + const baseDomain = message.data.baseDomain; + + const hostingInfo = { + green: false, + url: baseDomain + }; + + // is the base domain in our list of green domains? + if (hostingGreenCheck.includes(baseDomain)) { + hostingInfo.green = true; + } + + const co2PerParty = CO2.perParty(message.data, hostingGreenCheck); + // Fetch the resources with the largest CO2 impact. ie, + // the resources to optimise, host somewhere green, or contact + // a supplier about + const dirtiestResources = CO2.dirtiestResources( + message.data, + hostingGreenCheck + ); + + const co2PerContentType = CO2.perContentType( + message.data, + hostingGreenCheck + ); + + const co2PerPageView = CO2.perPage(message.data, hostingGreenCheck); + + const totalCO2 = this.sustainableOptions.pageViews + ? this.sustainableOptions.pageViews * co2PerPageView + : co2PerPageView; + + const transferPerPage = message.data.transferSize; + const firstPartyTransferPerPage = message.data.firstParty.transferSize; + const thirdPartyTransferPerPage = message.data.thirdParty.transferSize; + + // adding these to make the link between data and CO2 clearer + // and also so we can give an idea how big this is compared to + // the norm + // let transfer1stParty, transfer3rdParty; + if (message.iteration === 1) { + this.firstRunsData[message.url] = { + co2PerDomain, + co2PerContentType, + dirtiestResources, + hostingGreenCheck + }; + } + // We get data per run, so we want to aggregate that per page (multiple runs per page) + // and per group/domain + aggregator.addStats( + { + co2PerDomain, + co2PerPageView, + co2PerParty, + hostingInfo, + totalCO2, + transferPerPage, + firstPartyTransferPerPage, + thirdPartyTransferPerPage + }, + message.group, + message.url + ); + + // We pass on the data we have, so the that HTML plugin can generate the HTML tab + // per run + queue.postMessage( + make( + 'sustainable.run', + { + co2PerPageView, + totalCO2, + hostingInfo, + co2PerDomain, + totalTransfer: transferPerPage, + firstPartyTransferPerPage, + thirdPartyTransferPerPage, + co2FirstParty: co2PerParty.firstParty, + co2ThirdParty: co2PerParty.thirdParty, + hostingGreenCheck, + dirtiestResources, + co2PerContentType + }, + { + url: message.url, + group: message.group, + runIndex: message.runIndex + } + ) + ); // Here we put the data that we got from that run + break; + } + + case 'sitespeedio.summarize': { + // All URLs has been tested, now calculate the min/median/max c02 per page + // and push that info on the queue + const summaries = aggregator.summarize(); + + // Send each URL + for (let url of Object.keys(summaries.urls)) { + const extras = this.firstRunsData[url]; + // Attach first run so we can show that extra data that we don't collect stats for + summaries.urls[url].firstRun = extras; + queue.postMessage( + make('sustainable.pageSummary', summaries.urls[url], { + url: url, + group: summaries.urlToGroup[url] + }) + ); + } + + queue.postMessage( + make('sustainable.summary', summaries.groups['total'], { + group: 'total' + }) + ); + break; } - } else { - log.info( - 'Not using the sustainable web plugin since the dependencies is not installed' - ); } } -}; +} diff --git a/lib/plugins/sustainable/pug/index.pug b/lib/plugins/sustainable/pug/index.pug index 669e6ebf9..b93f4ee0e 100644 --- a/lib/plugins/sustainable/pug/index.pug +++ b/lib/plugins/sustainable/pug/index.pug @@ -44,22 +44,22 @@ table th Transfer Size tr td 1 page view total - td #{h.co2.format(co2PerPageView)} + td #{h.co2(co2PerPageView)} td #{h.size.format(totalTransfer)} tr td 1 page view first party - td #{h.co2.format(co2FirstParty)} (#{Number(co2FirstParty / (co2FirstParty + co2ThirdParty) * 100).toFixed(2)} %) + td #{h.co2(co2FirstParty)} (#{Number(co2FirstParty / (co2FirstParty + co2ThirdParty) * 100).toFixed(2)} %) td #{h.size.format(firstPartyTransferPerPage)} tr td 1 page view third party - td #{h.co2.format(co2ThirdParty)} (#{Number(co2ThirdParty / (co2FirstParty + co2ThirdParty) * 100).toFixed(2)} %) + td #{h.co2(co2ThirdParty)} (#{Number(co2ThirdParty / (co2FirstParty + co2ThirdParty) * 100).toFixed(2)} %) td #{h.size.format(thirdPartyTransferPerPage)} if (options.sustainable && options.sustainable.pageViews) tr td #{options.sustainable.pageViews.toLocaleString()} page views - td #{h.co2.format(totalCO2)} + td #{h.co2(totalCO2)} td #{h.size.format(totalTransfer * options.sustainable.pageViews)} - sustainable = pageInfo.data.sustainable.pageSummary ? pageInfo.data.sustainable.pageSummary.firstRun : sustainable a#sustainable-per-domain @@ -78,7 +78,7 @@ table - if (sustainable.hostingGreenCheck.indexOf(data.domain) > -1) span(class='label normal ok') green - const percentage = (data.co2 / co2PerPageView) * 100 - td.number #{h.co2.format(data.co2)} (#{Number(percentage).toFixed(2)} %) + td.number #{h.co2(data.co2)} (#{Number(percentage).toFixed(2)} %) td.number #{h.size.format(data.transferSize)} a#dirtiest-assets h2 Dirtiest assets @@ -95,7 +95,7 @@ table td.url.assetsurl(data-title='URL') a(href=resource.url)= h.shortAsset(resource.url) - const percentage = (resource.co2 / co2PerPageView) * 100 - td.number #{h.co2.format(resource.co2)} (#{Number(percentage).toFixed(2)} %) + td.number #{h.co2(resource.co2)} (#{Number(percentage).toFixed(2)} %) td.number= h.size.format(resource.transferSize) a#sustainible-per-content-type h2 Per Content Type @@ -111,5 +111,5 @@ table tr td #{t.type} - const percentage = (t.co2 / co2PerPageView) * 100 - td.number #{h.co2.format(t.co2)} (#{Number(percentage).toFixed(2)} %) + td.number #{h.co2(t.co2)} (#{Number(percentage).toFixed(2)} %) td.number= h.size.format(t.transferSize) diff --git a/lib/plugins/text/index.js b/lib/plugins/text/index.js index eb2550076..febfc24f6 100644 --- a/lib/plugins/text/index.js +++ b/lib/plugins/text/index.js @@ -1,28 +1,30 @@ -'use strict'; +import merge from 'lodash.merge'; +import set from 'lodash.set'; -const textBuilder = require('./textBuilder'); -const merge = require('lodash.merge'); -const set = require('lodash.set'); +import { renderSummary, renderBriefSummary } from './textBuilder.js'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; -module.exports = { +export default class TextPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'text', options, context, queue }); + } open(context, options) { this.metrics = {}; this.options = options; this.context = context; - }, - + } processMessage(message) { if (message.type.endsWith('.summary')) { const data = {}; set(data, message.type, message.data); merge(this.metrics, data); } - }, + } close(options) { if (!options.summary) return; const renderer = this.options.summaryDetail - ? textBuilder.renderSummary - : textBuilder.renderBriefSummary; + ? renderSummary + : renderBriefSummary; return renderer(this.metrics, this.context, options); } -}; +} diff --git a/lib/plugins/text/textBuilder.js b/lib/plugins/text/textBuilder.js index b6edf943a..eef51220d 100644 --- a/lib/plugins/text/textBuilder.js +++ b/lib/plugins/text/textBuilder.js @@ -1,15 +1,16 @@ -'use strict'; +import table from 'text-table'; +import flatten from 'lodash.flatten'; +import clic from 'cli-color'; +import chunk from 'lodash.chunk'; -const table = require('text-table'), - flatten = require('lodash.flatten'), - color = require('cli-color'), - chunk = require('lodash.chunk'), - h = require('../../support/helpers'), - summaryBoxesSetup = require('../html/setup/summaryBoxes'), - tableOpts = { - stringLength: color.getStrippedLength - }, - drab = color.blackBright; +import { plural, short, cap } from '../../support/helpers/index.js'; +import summaryBoxesSetup from '../html/setup/summaryBoxes.js'; + +const { getStrippedLength, blackBright, bold } = clic; +const tableOptions = { + stringLength: getStrippedLength +}; +const drab = blackBright; function getMarker(label) { return { ok: '√', warning: '!', error: '✗', info: '' }[label] || ''; @@ -26,9 +27,9 @@ function getHeader(context, options) { const noPages = options.urls.length; return drab( [ - `${h.plural(noPages, 'page')} analysed for ${h.short(context.name, 30)} `, - `(${h.plural(options.browsertime.iterations, 'run')}, `, - `${h.cap(options.browsertime.browser)}/${ + `${plural(noPages, 'page')} analysed for ${short(context.name, 30)} `, + `(${plural(options.browsertime.iterations, 'run')}, `, + `${cap(options.browsertime.browser)}/${ options.mobile ? 'mobile' : 'desktop' }/${options.connectivity})` ].join('') @@ -40,57 +41,51 @@ function getBoxes(metrics, html) { } // foo bar -> fb -function abbr(str) { - if (/total|overall/i.test(str)) return str.trim(); - return str.replace(/\w+\s?/g, a => a[0]); +function abbr(string_) { + if (/total|overall/i.test(string_)) return string_.trim(); + return string_.replace(/\w+\s?/g, a => a[0]); } function noop(a) { return a; } -module.exports = { - renderSummary(metrics, context, options) { - let out = getHeader(context, options); - let rows = getBoxes(metrics, options.html).map(b => { - const marker = getMarker(b.label), - c = getColor(b.label); - // color.xterm(ansi)(label), - return [color[c](marker), color[c](b.name), color.bold(b.median)]; - }); - rows.unshift( - ['', 'Score / Metric', 'Median'], - ['', '-------------', '------'] - ); - options.summary = { out: `${out}\n` + table(rows, tableOpts) }; - }, - - renderBriefSummary(metrics, context, options) { - let out = getHeader(context, options); - const lines = [], - scores = []; - let size = '', - reqs = '', - rum = ''; - getBoxes(metrics, options.html).map(b => { - let c = getColor(b.label), - val = b.median, - name; - c = color[c] || noop; - if (/score$/i.test(b.url)) { - name = abbr(b.name.replace('score', '')); - scores.push(c(`${name}:${val}`)); - } else if ('pageSize' === b.url) { - val = val.replace(' ', ''); // 10 KB -> 10KB - size = `${val}`; - } else if (/total requests/i.test(b.name)) { - reqs = `${val} reqs`; - } - }); - lines.push(drab('Score: ') + scores.reverse().join(', ')); - lines.push(size); - lines.push(reqs); - lines.push(rum); - options.summary = { out: `${out}\n` + lines.join(' / ') }; - } -}; +export function renderSummary(metrics, context, options) { + let out = getHeader(context, options); + let rows = getBoxes(metrics, options.html).map(b => { + const marker = getMarker(b.label), + c = getColor(b.label); + // color.xterm(ansi)(label), + return [clic[c](marker), clic[c](b.name), bold(b.median)]; + }); + rows.unshift( + ['', 'Score / Metric', 'Median'], + ['', '-------------', '------'] + ); + options.summary = { out: `${out}\n` + table(rows, tableOptions) }; +} +export function renderBriefSummary(metrics, context, options) { + let out = getHeader(context, options); + const lines = [], + scores = []; + let size = '', + reqs = '', + rum = ''; + getBoxes(metrics, options.html).map(b => { + let c = getColor(b.label), + value = b.median, + name; + c = clic[c] || noop; + if (/score$/i.test(b.url)) { + name = abbr(b.name.replace('score', '')); + scores.push(c(`${name}:${value}`)); + } else if ('pageSize' === b.url) { + value = value.replace(' ', ''); // 10 KB -> 10KB + size = `${value}`; + } else if (/total requests/i.test(b.name)) { + reqs = `${value} reqs`; + } + }); + lines.push(drab('Score: ') + scores.reverse().join(', '), size, reqs, rum); + options.summary = { out: `${out}\n` + lines.join(' / ') }; +} diff --git a/lib/plugins/thirdparty/aggregator.js b/lib/plugins/thirdparty/aggregator.js index 291cb61f1..d947b7de0 100644 --- a/lib/plugins/thirdparty/aggregator.js +++ b/lib/plugins/thirdparty/aggregator.js @@ -1,14 +1,14 @@ -'use strict'; +import { pushStats, setStatsSummary } from '../../support/statsHelpers.js'; -const statsHelpers = require('../../support/statsHelpers'); - -module.exports = { - urls: {}, - categories: {}, - totalRequests: {}, - thirdPartyPercentage: {}, - cpuPerTool: {}, - cookies: {}, +export class ThirdPartyAggregator { + constructor() { + this.urls = {}; + this.categories = {}; + this.totalRequests = {}; + this.thirdPartyPercentage = {}; + this.cpuPerTool = {}; + this.cookies = {}; + } addToAggregate( url, @@ -28,30 +28,26 @@ module.exports = { this.cookies[url] = {}; } - statsHelpers.pushStats( + pushStats( this.totalRequests[url], 'totalRequests', totalThirdPartyRequests ); - statsHelpers.pushStats(this.cookies[url], 'cookies', cookies); + pushStats(this.cookies[url], 'cookies', cookies); - statsHelpers.pushStats( + pushStats( this.thirdPartyPercentage[url], 'percentage', (totalThirdPartyRequests / totalRequestsForThePage) * 100 ); for (let category of Object.keys(requestsByCategory)) { - statsHelpers.pushStats( - this.urls[url], - category, - requestsByCategory[category] - ); + pushStats(this.urls[url], category, requestsByCategory[category]); } for (let category of Object.keys(toolsByCategory)) { - statsHelpers.pushStats( + pushStats( this.categories[url], category, Object.keys(toolsByCategory[category]).length @@ -59,13 +55,13 @@ module.exports = { } for (let tool of Object.keys(cpuPerTool)) { - statsHelpers.pushStats( + pushStats( this.cpuPerTool[url], tool.replace(/\./g, '_'), cpuPerTool[tool] ); } - }, + } summarize() { const summary = {}; for (let url of Object.keys(this.urls)) { @@ -76,7 +72,7 @@ module.exports = { }; // Collect how many requests that is done per type for (let category of Object.keys(this.urls[url])) { - statsHelpers.setStatsSummary( + setStatsSummary( summary[url], 'category.' + category + '.requests', this.urls[url][category] @@ -84,7 +80,7 @@ module.exports = { } // And also collect per category for (let category of Object.keys(this.categories[url])) { - statsHelpers.setStatsSummary( + setStatsSummary( summary[url], 'category.' + category + '.tools', this.categories[url][category] @@ -92,26 +88,22 @@ module.exports = { } for (let tool of Object.keys(this.cpuPerTool[url])) { - statsHelpers.setStatsSummary( + setStatsSummary( summary[url], 'tool.' + tool + '.cpu', this.cpuPerTool[url][tool] ); } - statsHelpers.setStatsSummary( + setStatsSummary( summary[url], 'requests.total', this.totalRequests[url]['totalRequests'] ); - statsHelpers.setStatsSummary( - summary[url], - 'cookies', - this.cookies[url]['cookies'] - ); + setStatsSummary(summary[url], 'cookies', this.cookies[url]['cookies']); - statsHelpers.setStatsSummary( + setStatsSummary( summary[url], 'requests.percentage', this.thirdPartyPercentage[url]['percentage'] @@ -120,4 +112,4 @@ module.exports = { return summary; } -}; +} diff --git a/lib/plugins/thirdparty/index.js b/lib/plugins/thirdparty/index.js index 5054b925f..d5a067b70 100644 --- a/lib/plugins/thirdparty/index.js +++ b/lib/plugins/thirdparty/index.js @@ -1,8 +1,12 @@ -'use strict'; +import { parse } from 'node:url'; -const { getEntity } = require('coach-core').getThirdPartyWeb(); -const aggregator = require('./aggregator'); -const urlParser = require('url'); +import coach from 'coach-core'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; + +import { ThirdPartyAggregator } from './aggregator.js'; + +const { getThirdPartyWeb } = coach; +const { getEntity } = getThirdPartyWeb(); const DEFAULT_THIRDPARTY_PAGESUMMARY_METRICS = [ 'category.*.requests.*', @@ -13,7 +17,11 @@ const DEFAULT_THIRDPARTY_PAGESUMMARY_METRICS = [ const DEFAULT_THIRDPARTY_RUN_METRICS = []; -module.exports = { +export default class ThirdPartyPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'thirdparty', options, context, queue }); + } + open(context, options) { this.metrics = {}; this.options = options; @@ -22,6 +30,7 @@ module.exports = { this.groups = {}; this.runsPerUrl = {}; this.urlAndGroup = {}; + this.thirdPartyAggregator = new ThirdPartyAggregator(); if (options.thirdParty && options.thirdParty.cpu) { DEFAULT_THIRDPARTY_PAGESUMMARY_METRICS.push('tool.*'); @@ -34,7 +43,7 @@ module.exports = { DEFAULT_THIRDPARTY_RUN_METRICS, 'thirdparty.run' ); - }, + } processMessage(message, queue) { const make = this.make; const thirdPartyAssetsByCategory = {}; @@ -42,38 +51,32 @@ module.exports = { const possibileMissedThirdPartyDomains = []; if (message.type === 'pagexray.run') { const cookies = message.data.thirdParty.cookies; - const firstPartyRegEx = message.data.firstPartyRegEx; + const firstPartyRegEx = new RegExp(message.data.firstPartyRegEx); for (let d of Object.keys(message.data.domains)) { - const entity = d.indexOf('localhost') > -1 ? undefined : getEntity(d); - if (entity !== undefined) { - // Here is a match - } else { - if (!d.match(firstPartyRegEx)) { - possibileMissedThirdPartyDomains.push(d); - } + const entity = d.includes('localhost') ? undefined : getEntity(d); + if (entity === undefined && !firstPartyRegEx.test(d)) { + possibileMissedThirdPartyDomains.push(d); } } const byCategory = {}; this.groups[message.url] = message.group; let company; try { - company = - message.url.indexOf('localhost') > -1 - ? undefined - : getEntity(message.url); - } catch (e) { - // Third party web has problem parsing some URLs + company = message.url.includes('localhost') + ? undefined + : getEntity(message.url); + } catch { + // Do nada } let totalThirdPartyRequests = 0; for (let asset of message.data.assets) { let entity; try { - entity = - asset.url.indexOf('localhost') > -1 - ? undefined - : getEntity(asset.url); - } catch (e) { - // Third party web has problem parsing some URLs + entity = asset.url.includes('localhost') + ? undefined + : getEntity(asset.url); + } catch { + // Do nada } if (entity !== undefined) { if (company && company.name === entity.name) { @@ -83,14 +86,13 @@ module.exports = { } totalThirdPartyRequests++; if ( - entity.name.indexOf('Google') > -1 || - entity.name.indexOf('Facebook') > -1 || - entity.name.indexOf('AMP') > -1 || - entity.name.indexOf('YouTube') > -1 + (entity.name.includes('Google') || + entity.name.includes('Facebook') || + entity.name.includes('AMP') || + entity.name.includes('YouTube')) && + !entity.categories.includes('survelliance') ) { - if (!entity.categories.includes('survelliance')) { - entity.categories.push('survelliance'); - } + entity.categories.push('survelliance'); } for (let category of entity.categories) { if (!toolsByCategory[category]) { @@ -114,12 +116,10 @@ module.exports = { } } else { // We don't have a match for this request, check agains the regex - if (!asset.url.match(firstPartyRegEx)) { - if (byCategory['unknown']) { - byCategory['unknown'] = byCategory['unknown'] + 1; - } else { - byCategory['unknown'] = 1; - } + if (!firstPartyRegEx.test(asset.url)) { + byCategory['unknown'] = byCategory['unknown'] + ? byCategory['unknown'] + 1 + : 1; } } } @@ -133,7 +133,7 @@ module.exports = { // fallback to domain if (!entity) { const hostname = ent.url.startsWith('http') - ? urlParser.parse(ent.url).hostname + ? parse(ent.url).hostname : ent.url; entity = { name: hostname @@ -148,7 +148,7 @@ module.exports = { } } - aggregator.addToAggregate( + this.thirdPartyAggregator.addToAggregate( message.url, byCategory, toolsByCategory, @@ -183,7 +183,7 @@ module.exports = { this.runsPerUrl[message.url] = [runData]; } } else if (message.type === 'sitespeedio.summarize') { - let summary = aggregator.summarize(); + let summary = this.thirdPartyAggregator.summarize(); for (let url of Object.keys(summary)) { summary[url].runs = this.runsPerUrl[url]; queue.postMessage( @@ -195,4 +195,4 @@ module.exports = { } } } -}; +} diff --git a/lib/plugins/tracestorer/index.js b/lib/plugins/tracestorer/index.js index 85b668e95..3a1d63566 100644 --- a/lib/plugins/tracestorer/index.js +++ b/lib/plugins/tracestorer/index.js @@ -1,14 +1,17 @@ -'use strict'; +import { gzip as _gzip } from 'node:zlib'; +import { promisify } from 'node:util'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +const gzip = promisify(_gzip); -const zlib = require('zlib'); -const { promisify } = require('util'); -const gzip = promisify(zlib.gzip); +export default class TraceStorerPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'tracestorer', options, context, queue }); + } -module.exports = { open(context) { this.storageManager = context.storageManager; this.alias = {}; - }, + } processMessage(message) { switch (message.type) { case 'browsertime.alias': { @@ -30,4 +33,4 @@ module.exports = { } } } -}; +} diff --git a/lib/sitespeed.js b/lib/sitespeed.js index 58844db9b..7931cc53a 100644 --- a/lib/sitespeed.js +++ b/lib/sitespeed.js @@ -1,27 +1,28 @@ -'use strict'; +import { platform, release } from 'node:os'; +import { env, version } from 'node:process'; +import { join } from 'node:path'; +import fs from 'node:fs/promises'; -const dayjs = require('dayjs'); -const utc = require('dayjs/plugin/utc'); -const log = require('intel').getLogger('sitespeedio'); -const intel = require('intel'); -const os = require('os'); -const process = require('process'); -const path = require('path'); -const logging = require('./core/logging'); -const toArray = require('./support/util').toArray; -const pullAll = require('lodash.pullall'); -const union = require('lodash.union'); -const messageMaker = require('./support/messageMaker'); -const filterRegistry = require('./support/filterRegistry'); -const statsHelpers = require('./support/statsHelpers'); -const packageInfo = require('../package'); +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc.js'; +import intel from 'intel'; +import pullAll from 'lodash.pullall'; +import union from 'lodash.union'; -const QueueHandler = require('./core/queueHandler'); -const resultsStorage = require('./core/resultsStorage'); -const loader = require('./core/pluginLoader'); -const urlSource = require('./core/url-source'); -const scriptSource = require('./core/script-source'); +import { configure } from './core/logging.js'; +import { toArray } from './support/util.js'; +import { messageMaker } from './support/messageMaker.js'; +import * as filterRegistry from './support/filterRegistry.js'; +import * as statsHelpers from './support/statsHelpers.js'; +import { QueueHandler } from './core/queueHandler.js'; +import { resultsStorage } from './core/resultsStorage/index.js'; +import { parsePluginNames, loadPlugins } from './core/pluginLoader.js'; +import * as urlSource from './core/url-source.js'; +import * as scriptSource from './core/script-source.js'; +const packageJson = JSON.parse(await fs.readFile('./package.json')); + +const log = intel.getLogger('sitespeedio'); dayjs.extend(utc); const budgetResult = { @@ -31,147 +32,145 @@ const budgetResult = { }; function hasFunctionFilter(functionName) { - return obj => typeof obj[functionName] === 'function'; + return object => typeof object[functionName] === 'function'; } function runOptionalFunction(objects, fN) { // NOTE: note slicing due to https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments - let args = new Array(arguments.length - 2); - for (let i = 2; i < arguments.length; i++) { - args[i - 2] = arguments[i]; + + let arguments_ = Array.from({ length: arguments.length - 2 }); + for (let index = 2; index < arguments.length; index++) { + arguments_[index - 2] = arguments[index]; } + return objects .filter(hasFunctionFilter(fN)) - .map(plugin => Promise.resolve(plugin[fN].apply(plugin, args))); + .map(plugin => Promise.resolve(plugin[fN].apply(plugin, arguments_))); } -module.exports = { - async run(options) { - const url = options.urls[0]; - const timestamp = options.utc ? dayjs.utc() : dayjs(); - const { storageManager, resultUrls } = resultsStorage( - url, - timestamp, - options - ); +export async function run(options) { + const url = options.urls[0]; + const timestamp = options.utc ? dayjs.utc() : dayjs(); + const { storageManager, resultUrls } = resultsStorage( + url, + timestamp, + options + ); - // Setup logging - const logDir = await storageManager.createDirectory('logs'); + // Setup logging + const logDir = await storageManager.createDirectory('logs'); - if ( - options.browsertime && - options.browsertime.tcpdump && - !process.env.SSLKEYLOGFILE - ) { - process.env.SSLKEYLOGFILE = path.join( - storageManager.getBaseDir(), - 'SSLKEYLOGFILE.txt' - ); + if ( + options.browsertime && + options.browsertime.tcpdump && + !env.SSLKEYLOGFILE + ) { + env.SSLKEYLOGFILE = join(storageManager.getBaseDir(), 'SSLKEYLOGFILE.txt'); + } + configure(options, logDir); + + // Tell the world what we are using + log.info( + 'Versions OS: %s nodejs: %s sitespeed.io: %s browsertime: %s coach: %s', + platform() + ' ' + release(), + version, + packageJson.version, + packageJson.dependencies.browsertime, + packageJson.dependencies['coach-core'] + ); + if (log.isEnabledFor(log.DEBUG)) { + log.debug('Running with options: %:2j', options); + } + + let pluginNames = await parsePluginNames(options); + + const plugins = options.plugins; + + // Deprecated setup + if (plugins) { + if (plugins.disable) { + log.warn('--plugins.disable is deprecated, use plugins.remove instead.'); + plugins.remove = plugins.disable; } - logging.configure(options, logDir); - - // Tell the world what we are using - log.info( - 'Versions OS: %s nodejs: %s sitespeed.io: %s browsertime: %s coach: %s', - os.platform() + ' ' + os.release(), - process.version, - packageInfo.version, - packageInfo.dependencies.browsertime, - packageInfo.dependencies['coach-core'] - ); - if (log.isEnabledFor(log.DEBUG)) { - log.debug('Running with options: %:2j', options); + if (plugins.load) { + log.warn('--plugins.load is deprecated, use plugins.add instead.'); + plugins.add = plugins.load; } - let pluginNames = await loader.parsePluginNames(options); - - const plugins = options.plugins; - - // Deprecated setup - if (plugins) { - if (plugins.disable) { - log.warn( - '--plugins.disable is deprecated, use plugins.remove instead.' - ); - plugins.remove = plugins.disable; - } - if (plugins.load) { - log.warn('--plugins.load is deprecated, use plugins.add instead.'); - plugins.add = plugins.load; - } - - // Finalize the plugins that we wanna run - // First we add the new ones and then remove, that means remove - // always wins - pluginNames = union(pluginNames, toArray(plugins.add)); - pullAll(pluginNames, toArray(plugins.remove)); - if (plugins.list) { - log.info( - 'The following plugins are enabled: %s', - pluginNames.join(', ') - ); - } - } - - const runningPlugins = await loader.loadPlugins(pluginNames); - const urlSources = [options.multi ? scriptSource : urlSource]; - const allPlugins = urlSources.concat(runningPlugins); - const queueHandler = new QueueHandler(runningPlugins, options); - - // This is the contect where we wanna run our tests - const context = { - storageManager, - resultUrls, - timestamp, - budget: budgetResult, - name: url, - log, - intel, - messageMaker, - statsHelpers, - filterRegistry - }; - - // Open/start each and every plugin - try { - await Promise.all( - runOptionalFunction(allPlugins, 'open', context, options) - ); - - // Pass the URLs - const errors = await queueHandler.run(urlSources); - - // Close the plugins - await Promise.all( - runOptionalFunction(allPlugins, 'close', options, errors) - ); - - if (resultUrls.hasBaseUrl()) { - log.info( - 'Find the result at %s', - resultUrls.reportSummaryUrl() + '/index.html' - ); - } - - if (options.summary && options.summary.out) { - console.log(options.summary.out); // eslint-disable-line no-console - } - - return { - errors, - budgetResult, - resultUrl: resultUrls.hasBaseUrl() - ? resultUrls.reportSummaryUrl() + '/index.html' - : '', - pageSummaryUrl: - url && resultUrls.hasBaseUrl() && !options.multi - ? resultUrls.absoluteSummaryPageUrl(url) + 'index.html' - : '', - localPath: storageManager.getBaseDir() - }; - } catch (err) { - log.error(err); - throw err; + // Finalize the plugins that we wanna run + // First we add the new ones and then remove, that means remove + // always wins + pluginNames = union(pluginNames, toArray(plugins.add)); + pullAll(pluginNames, toArray(plugins.remove)); + if (plugins.list) { + log.info('The following plugins are enabled: %s', pluginNames.join(', ')); } } -}; + + // This is the contect where we wanna run our tests + const context = { + storageManager, + resultUrls, + timestamp, + budget: budgetResult, + name: url, + log, + intel, + messageMaker, + statsHelpers, + filterRegistry + }; + + const queueHandler = new QueueHandler(options); + const runningPlugins = await loadPlugins( + pluginNames, + options, + context, + queueHandler + ); + const urlSources = [options.multi ? scriptSource : urlSource]; + const allPlugins = [...runningPlugins]; + queueHandler.setup(runningPlugins); + + // Open/start each and every plugin + try { + await Promise.all( + runOptionalFunction(allPlugins, 'open', context, options) + ); + + // Pass the URLs + const errors = await queueHandler.run(urlSources); + + // Close the plugins + await Promise.all( + runOptionalFunction(allPlugins, 'close', options, errors) + ); + + if (resultUrls.hasBaseUrl()) { + log.info( + 'Find the result at %s', + resultUrls.reportSummaryUrl() + '/index.html' + ); + } + + if (options.summary && options.summary.out) { + console.log(options.summary.out); // eslint-disable-line no-console + } + + return { + errors, + budgetResult, + resultUrl: resultUrls.hasBaseUrl() + ? resultUrls.reportSummaryUrl() + '/index.html' + : '', + pageSummaryUrl: + url && resultUrls.hasBaseUrl() && !options.multi + ? resultUrls.absoluteSummaryPageUrl(url) + 'index.html' + : '', + localPath: storageManager.getBaseDir() + }; + } catch (error) { + log.error(error); + throw error; + } +} diff --git a/lib/support/annotationsHelper.js b/lib/support/annotationsHelper.js index 8fab4d558..1f2e8b477 100644 --- a/lib/support/annotationsHelper.js +++ b/lib/support/annotationsHelper.js @@ -1,68 +1,63 @@ -'use strict'; - -module.exports = { - getAnnotationMessage( - absolutePagePath, - screenShotsEnabledInBrowsertime, - screenshotType, - webPageTestResultURL, - usingBrowsertime, - options - ) { - const screenshotSize = options.mobile ? 'height=200px' : 'width=100%'; - const resultPageUrl = absolutePagePath + 'index.html'; - let screenshotPath; - if (screenShotsEnabledInBrowsertime) { - screenshotPath = - absolutePagePath + - 'data/screenshots/1/afterPageCompleteCheck.' + - screenshotType; - } else if (webPageTestResultURL) { - screenshotPath = - absolutePagePath + 'data/screenshots/wpt-1-firstView.png'; - } - - const screenshotsEnabledForDatasource = - options.graphite.annotationScreenshot || - options.influxdb.annotationScreenshot || - options.grafana.annotationScreenshot; - const harPath = +export function getAnnotationMessage( + absolutePagePath, + screenShotsEnabledInBrowsertime, + screenshotType, + webPageTestResultURL, + usingBrowsertime, + options +) { + const screenshotSize = options.mobile ? 'height=200px' : 'width=100%'; + const resultPageUrl = absolutePagePath + 'index.html'; + let screenshotPath; + if (screenShotsEnabledInBrowsertime) { + screenshotPath = absolutePagePath + - 'data/' + - (usingBrowsertime ? 'browsertime.har' : 'webpagetest.har') + - (options.gzipHAR ? '.gz' : ''); - - const extraMessage = - options.graphite.annotationMessage || - options.influxdb.annotationMessage || - options.grafana.annotationMessage || - undefined; - - const s = options.browsertime.iterations > 1 ? 's' : ''; - - let message = - (screenShotsEnabledInBrowsertime || webPageTestResultURL) && - screenshotsEnabledForDatasource - ? `

    Result - Download HAR

    ` - : `Result ${options.browsertime.iterations} run${s}`; - - if (webPageTestResultURL) { - message = message + ` WebPageTest`; - } - - if (extraMessage) { - message = message + ' - ' + extraMessage; - } - return message; - }, - getTagsAsString(tags) { - return '"' + tags.join(',') + '"'; - }, - getTagsAsArray(tags) { - const stringTags = []; - for (let tag of tags) { - stringTags.push('"' + tag + '"'); - } - return '[' + stringTags.join(',') + ']'; + 'data/screenshots/1/afterPageCompleteCheck.' + + screenshotType; + } else if (webPageTestResultURL) { + screenshotPath = absolutePagePath + 'data/screenshots/wpt-1-firstView.png'; } -}; + + const screenshotsEnabledForDatasource = + options.graphite.annotationScreenshot || + options.influxdb.annotationScreenshot || + options.grafana.annotationScreenshot; + const harPath = + absolutePagePath + + 'data/' + + (usingBrowsertime ? 'browsertime.har' : 'webpagetest.har') + + (options.gzipHAR ? '.gz' : ''); + + const extraMessage = + options.graphite.annotationMessage || + options.influxdb.annotationMessage || + options.grafana.annotationMessage || + undefined; + + const s = options.browsertime.iterations > 1 ? 's' : ''; + + let message = + (screenShotsEnabledInBrowsertime || webPageTestResultURL) && + screenshotsEnabledForDatasource + ? `

    Result - Download HAR

    ` + : `Result ${options.browsertime.iterations} run${s}`; + + if (webPageTestResultURL) { + message = message + ` WebPageTest`; + } + + if (extraMessage) { + message = message + ' - ' + extraMessage; + } + return message; +} +export function getTagsAsString(tags) { + return '"' + tags.join(',') + '"'; +} +export function getTagsAsArray(tags) { + const stringTags = []; + for (let tag of tags) { + stringTags.push('"' + tag + '"'); + } + return '[' + stringTags.join(',') + ']'; +} diff --git a/lib/support/filterRegistry.js b/lib/support/filterRegistry.js index a2a5f48b9..6af245ebf 100644 --- a/lib/support/filterRegistry.js +++ b/lib/support/filterRegistry.js @@ -1,54 +1,40 @@ -'use strict'; - -const clone = require('lodash.clonedeep'), - metricsFilter = require('./metricsFilter'); +import clone from 'lodash.clonedeep'; +import { filterMetrics } from './metricsFilter.js'; let filterForType = {}; -module.exports = { - registerFilterForType(filter, type) { - filterForType[type] = filter; - }, - - getFilterForType(type) { - return filterForType[type]; - }, - - addFilterForType(filter, type) { - const filters = filterForType[type]; - if (filters.indexOf(filter) === -1) { - filterForType[type].push(filter); - } - }, - - getFilters() { - return filterForType; - }, - - getTypes() { - return Object.keys(filterForType); - }, - - removeFilter(type) { - filterForType[type] = undefined; - }, - - clearAll() { - filterForType = {}; - }, - - filterMessage(message) { - const filterConfig = this.getFilterForType(message.type); - - if (!filterConfig) { - return message; - } - - const filteredMessage = clone(message); - filteredMessage.data = metricsFilter.filterMetrics( - filteredMessage.data, - filterConfig - ); - return filteredMessage; +export function registerFilterForType(filter, type) { + filterForType[type] = filter; +} +export function getFilterForType(type) { + return filterForType[type]; +} +export function addFilterForType(filter, type) { + const filters = filterForType[type]; + if (!filters.includes(filter)) { + filterForType[type].push(filter); } -}; +} +export function getFilters() { + return filterForType; +} +export function getTypes() { + return Object.keys(filterForType); +} +export function removeFilter(type) { + filterForType[type] = undefined; +} +export function clearAll() { + filterForType = {}; +} +export function filterMessage(message) { + const filterConfig = this.getFilterForType(message.type); + + if (!filterConfig) { + return message; + } + + const filteredMessage = clone(message); + filteredMessage.data = filterMetrics(filteredMessage.data, filterConfig); + return filteredMessage; +} diff --git a/lib/support/flattenMessage.js b/lib/support/flattenMessage.js index 15df27443..7b01855c6 100644 --- a/lib/support/flattenMessage.js +++ b/lib/support/flattenMessage.js @@ -1,7 +1,6 @@ -'use strict'; - -const urlParser = require('url'); -const log = require('intel'); +import { parse } from 'node:url'; +import intel from 'intel'; +const log = intel.getLogger('sitespeedio'); function joinNonEmpty(strings, delimeter) { return strings.filter(Boolean).join(delimeter); @@ -9,145 +8,149 @@ function joinNonEmpty(strings, delimeter) { function toSafeKey(key) { // U+2013 : EN DASH – as used on https://en.wikipedia.org/wiki/2019–20_coronavirus_pandemic - return key.replace(/[.~ /+|,:?&%–)(]|%7C/g, '_'); + return key.replace(/[ %&()+,./:?|~–]|%7C/g, '_'); } -module.exports = { - keypathFromUrl(url, includeQueryParams, useHash, group) { - function flattenQueryParams(params) { - return Object.keys(params).reduce( - (result, key) => joinNonEmpty([result, key, params[key]], '_'), - '' - ); +export function keypathFromUrl(url, includeQueryParameters, useHash, group) { + function flattenQueryParameters(parameters) { + return Object.keys(parameters).reduce( + (result, key) => joinNonEmpty([result, key, parameters[key]], '_'), + '' + ); + } + + url = parse(url, !!includeQueryParameters); + + let path = toSafeKey(url.pathname); + + if (includeQueryParameters) { + path = joinNonEmpty( + [path, toSafeKey(flattenQueryParameters(url.query))], + '_' + ); + } + if (useHash && url.hash) { + path = joinNonEmpty([path, toSafeKey(url.hash)], '_'); + } + + const keys = [toSafeKey(group || url.hostname), path]; + + return joinNonEmpty(keys, '.'); +} + +function isNumericString(n) { + // eslint-disable-next-line unicorn/prefer-number-properties + return !isNaN(Number.parseFloat(n)) && isFinite(n); +} + +export function flattenMessageData({ data, type }) { + function recursiveFlatten(target, keyPrefix, value) { + // super simple version to avoid flatten HAR and screenshot data + if (/(screenshots\.|har\.)/.test(keyPrefix)) { + return; } - url = urlParser.parse(url, !!includeQueryParams); - - let path = toSafeKey(url.pathname); - - if (includeQueryParams) { - path = joinNonEmpty( - [path, toSafeKey(flattenQueryParams(url.query))], - '_' - ); - } - if (useHash && url.hash) { - path = joinNonEmpty([path, toSafeKey(url.hash)], '_'); + // Google is overloading User Timing marks + // See https://github.com/sitespeedio/browsertime/issues/257 + if (keyPrefix.includes('userTimings.marks.goog_')) { + return; } - const keys = [toSafeKey(group || url.hostname), path]; - - return joinNonEmpty(keys, '.'); - }, - - flattenMessageData({ data, type }) { - function isNumericString(n) { - return !isNaN(parseFloat(n)) && isFinite(n); + // Google is overloading User Timing marks = the same using WebPageTest + // See https://github.com/sitespeedio/browsertime/issues/257 + if (keyPrefix.includes('userTimes.goog_')) { + return; } - function recursiveFlatten(target, keyPrefix, value) { - // super simple version to avoid flatten HAR and screenshot data - if (keyPrefix.match(/(screenshots\.|har\.)/)) { - return; + // Hack to remove visual progress from default metrics + if (keyPrefix.includes('visualMetrics.VisualProgress')) { + return; + } + + if (keyPrefix.includes('visualMetrics.videoRecordingStart')) { + return; + } + + const valueType = typeof value; + + switch (valueType) { + case 'number': { + { + if (Number.isFinite(value)) { + target[keyPrefix] = value; + } else { + log.warn( + `Non-finite number '${value}' found at path '${keyPrefix}' for '${type}' message (url = ${data.url})` + ); + } + } + break; } + case 'object': { + { + if (value === null) { + break; + } - // Google is overloading User Timing marks - // See https://github.com/sitespeedio/browsertime/issues/257 - if (keyPrefix.indexOf('userTimings.marks.goog_') > -1) { - return; - } - - // Google is overloading User Timing marks = the same using WebPageTest - // See https://github.com/sitespeedio/browsertime/issues/257 - if (keyPrefix.indexOf('userTimes.goog_') > -1) { - return; - } - - // Hack to remove visual progress from default metrics - if (keyPrefix.indexOf('visualMetrics.VisualProgress') > -1) { - return; - } - - if (keyPrefix.indexOf('visualMetrics.videoRecordingStart') > -1) { - return; - } - - const valueType = typeof value; - - switch (valueType) { - case 'number': - { - if (isFinite(value)) { - target[keyPrefix] = value; + for (const key of Object.keys(value)) { + // Hey are you coming to the future from 1980s? Please don't + // look at this code, it's a ugly hack to make sure we can send assets + // to Graphite and don't send them with array position, instead + // use the url to generate the key + if (type === 'pagexray.pageSummary' && keyPrefix === 'assets') { + recursiveFlatten( + target, + joinNonEmpty( + [keyPrefix, toSafeKey(value[key].url || key)], + '.' + ), + value[key] + ); } else { - log.warn( - `Non-finite number '${value}' found at path '${keyPrefix}' for '${type}' message (url = ${data.url})` + recursiveFlatten( + target, + joinNonEmpty([keyPrefix, toSafeKey(key)], '.'), + value[key] ); } } - break; - case 'object': - { - if (value === null) { - break; - } - - Object.keys(value).forEach(key => { - // Hey are you coming to the future from 1980s? Please don't - // look at this code, it's a ugly hack to make sure we can send assets - // to Graphite and don't send them with array position, instead - // use the url to generate the key - - if (type === 'pagexray.pageSummary' && keyPrefix === 'assets') { - recursiveFlatten( - target, - joinNonEmpty( - [keyPrefix, toSafeKey(value[key].url || key)], - '.' - ), - value[key] - ); - } else { - recursiveFlatten( - target, - joinNonEmpty([keyPrefix, toSafeKey(key)], '.'), - value[key] - ); - } - }); + } + break; + } + case 'string': { + { + if (isNumericString(value)) { + target[keyPrefix] = Number.parseFloat(value); } - break; - case 'string': - { - if (isNumericString(value)) { - target[keyPrefix] = parseFloat(value); - } - } - break; - case 'boolean': - { - target[keyPrefix] = value ? 1 : 0; - } - break; - case 'undefined': - { - log.debug( - `Undefined value found at path '${keyPrefix}' for '${type}' message (url = ${data.url})` - ); - } - break; - default: - throw new Error( - 'Unhandled value type ' + - valueType + - ' found when flattening data for prefix ' + - keyPrefix + } + break; + } + case 'boolean': { + { + target[keyPrefix] = value ? 1 : 0; + } + break; + } + case 'undefined': { + { + log.debug( + `Undefined value found at path '${keyPrefix}' for '${type}' message (url = ${data.url})` ); + } + break; + } + default: { + throw new Error( + 'Unhandled value type ' + + valueType + + ' found when flattening data for prefix ' + + keyPrefix + ); } } - - let retVal = {}; - recursiveFlatten(retVal, '', data); - return retVal; } -}; + + let returnValueValue = {}; + recursiveFlatten(returnValueValue, '', data); + return returnValueValue; +} diff --git a/lib/support/friendlynames.js b/lib/support/friendlynames.js index 6683a5ca7..77c0ea71e 100644 --- a/lib/support/friendlynames.js +++ b/lib/support/friendlynames.js @@ -1,7 +1,13 @@ -'use strict'; -const { noop, size, time, co2, httpErrors, decimals } = require('./helpers'); +import { + noop, + size, + time, + co2, + httpErrors, + decimals +} from './helpers/index.js'; -module.exports = { +export default { browsertime: { googleWebVitals: { timeToFirstByte: { diff --git a/lib/support/helpers/cap.js b/lib/support/helpers/cap.js index e0fb8efb8..ee0e1a832 100644 --- a/lib/support/helpers/cap.js +++ b/lib/support/helpers/cap.js @@ -1,5 +1,3 @@ -'use strict'; - -module.exports = function (word) { - return word.substr(0, 1).toUpperCase() + word.substr(1); -}; +export function cap(word) { + return word.slice(0, 1).toUpperCase() + word.slice(1); +} diff --git a/lib/support/helpers/co2.js b/lib/support/helpers/co2.js index 1e2ef77e1..86196f3bb 100644 --- a/lib/support/helpers/co2.js +++ b/lib/support/helpers/co2.js @@ -1,15 +1,11 @@ -'use strict'; - const kg = 1000; -module.exports = { - format(grams) { - if (grams === 0) return '0'; - else if (grams === undefined) return 0; - else if (grams < kg) { - return Number(grams).toFixed(4) + ' grams'; - } else { - return Number(grams / kg).toFixed(3) + ' kg'; - } +export function co2(grams) { + if (grams === 0) return '0'; + else if (grams === undefined) return 0; + else if (grams < kg) { + return Number(grams).toFixed(4) + ' grams'; + } else { + return Number(grams / kg).toFixed(3) + ' kg'; } -}; +} diff --git a/lib/support/helpers/decimals.js b/lib/support/helpers/decimals.js index dbd1e7981..63199a825 100644 --- a/lib/support/helpers/decimals.js +++ b/lib/support/helpers/decimals.js @@ -1,8 +1,4 @@ -'use strict'; - -module.exports = function (decimals) { +export function decimals(decimals) { let number = Number(decimals).toFixed(3); - if (number === '0.000') { - return 0; - } else return number; -}; + return number === '0.000' ? 0 : number; +} diff --git a/lib/support/helpers/get.js b/lib/support/helpers/get.js index 60faed34a..53737b6bb 100644 --- a/lib/support/helpers/get.js +++ b/lib/support/helpers/get.js @@ -1,12 +1,11 @@ -'use strict'; -const get = require('lodash.get'); +import { default as _get } from 'lodash.get'; -module.exports = function (object, property, defaultValue) { +export function get(object, property, defaultValue) { if (arguments.length < 3) { defaultValue = 0; } if (!object) { return defaultValue; } - return get(object, property, defaultValue); -}; + return _get(object, property, defaultValue); +} diff --git a/lib/support/helpers/httpErrors.js b/lib/support/helpers/httpErrors.js index 37d2568a9..52fee387d 100644 --- a/lib/support/helpers/httpErrors.js +++ b/lib/support/helpers/httpErrors.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = function (httpCodes) { +export function httpErrors(httpCodes) { let data = ''; for (let code of Object.keys(httpCodes)) { if (Number(code) > 399) { @@ -8,4 +6,4 @@ module.exports = function (httpCodes) { } } return data === '' ? '0' : data; -}; +} diff --git a/lib/support/helpers/index.js b/lib/support/helpers/index.js index 4221ae756..0e89a4896 100644 --- a/lib/support/helpers/index.js +++ b/lib/support/helpers/index.js @@ -1,18 +1,14 @@ -'use strict'; - -module.exports = { - size: require('./size'), - cap: require('./cap'), - time: require('./time'), - plural: require('./plural'), - scoreLabel: require('./scoreLabel'), - label: require('./label'), - get: require('./get'), - short: require('./short'), - shortAsset: require('./shortAsset'), - co2: require('./co2'), - noop: require('./noop'), - percent: require('./percent'), - httpErrors: require('./httpErrors'), - decimals: require('./decimals') -}; +export * from './size.js'; +export * from './cap.js'; +export * from './time.js'; +export * from './plural.js'; +export * from './scoreLabel.js'; +export * from './label.js'; +export * from './get.js'; +export * from './short.js'; +export * from './shortAsset.js'; +export * from './co2.js'; +export * from './noop.js'; +export * from './percent.js'; +export * from './httpErrors.js'; +export * from './decimals.js'; diff --git a/lib/support/helpers/label.js b/lib/support/helpers/label.js index f0c1e8eea..69bf39fe4 100644 --- a/lib/support/helpers/label.js +++ b/lib/support/helpers/label.js @@ -1,6 +1,4 @@ -'use strict'; - -module.exports = function (value, ok, warning) { +export function label(value, ok, warning) { value = value || 0; if (value > ok) { return 'ok'; @@ -8,4 +6,4 @@ module.exports = function (value, ok, warning) { return 'warning'; } return 'error'; -}; +} diff --git a/lib/support/helpers/noop.js b/lib/support/helpers/noop.js index 2588e989c..81a6c66e3 100644 --- a/lib/support/helpers/noop.js +++ b/lib/support/helpers/noop.js @@ -1,5 +1,3 @@ -'use strict'; - -module.exports = function (value) { +export function noop(value) { return value; -}; +} diff --git a/lib/support/helpers/percent.js b/lib/support/helpers/percent.js index 2fae23d6c..0f7718054 100644 --- a/lib/support/helpers/percent.js +++ b/lib/support/helpers/percent.js @@ -1,5 +1,3 @@ -'use strict'; - -module.exports = function (value) { +export function percent(value) { return `${value}%`; -}; +} diff --git a/lib/support/helpers/plural.js b/lib/support/helpers/plural.js index 6e127bf2c..c5a90f9e9 100644 --- a/lib/support/helpers/plural.js +++ b/lib/support/helpers/plural.js @@ -1,8 +1,6 @@ -'use strict'; - -module.exports = function (number, text) { +export function plural(number, text) { if (number === 0 || number > 1) { text += 's'; } return '' + number + ' ' + text; -}; +} diff --git a/lib/support/helpers/scoreLabel.js b/lib/support/helpers/scoreLabel.js index db9f9f8a3..f1a111f08 100644 --- a/lib/support/helpers/scoreLabel.js +++ b/lib/support/helpers/scoreLabel.js @@ -1,11 +1,8 @@ -'use strict'; - -module.exports = function (value) { - value = value || 0; +export function scoreLabel(value = 0) { if (value > 90) { return 'ok'; } else if (value > 80) { return 'warning'; } return 'error'; -}; +} diff --git a/lib/support/helpers/short.js b/lib/support/helpers/short.js index 0df49c255..b442fc4d7 100644 --- a/lib/support/helpers/short.js +++ b/lib/support/helpers/short.js @@ -1,8 +1,6 @@ -'use strict'; - -module.exports = function (text, number) { +export function short(text, number) { if (text.length > number) { return text.slice(0, number) + '...'; } return text; -}; +} diff --git a/lib/support/helpers/shortAsset.js b/lib/support/helpers/shortAsset.js index 271bf698c..e28fa2b8b 100644 --- a/lib/support/helpers/shortAsset.js +++ b/lib/support/helpers/shortAsset.js @@ -1,16 +1,14 @@ -'use strict'; - -module.exports = function (url, longVersion) { +export function shortAsset(url, longVersion) { if (longVersion) { if (url.length > 100) { let shortUrl = url.replace(/\?.*/, ''); - url = shortUrl.substr(0, 80) + '...' + shortUrl.substr(-17); + url = shortUrl.slice(0, 80) + '...' + shortUrl.slice(-17); } } else { if (url.length > 40) { let shortUrl = url.replace(/\?.*/, ''); - url = shortUrl.substr(0, 20) + '...' + shortUrl.substr(-17); + url = shortUrl.slice(0, 20) + '...' + shortUrl.slice(-17); } } return url; -}; +} diff --git a/lib/support/helpers/size.js b/lib/support/helpers/size.js index c8e0cd056..4322b3f4d 100644 --- a/lib/support/helpers/size.js +++ b/lib/support/helpers/size.js @@ -1,9 +1,6 @@ -'use strict'; - const KB = 1024, MB = 1024 * 1024; - -module.exports = { +export const size = { asKb(bytes) { if (!bytes || bytes < 0) return 0; return Number(bytes / KB).toFixed(1); diff --git a/lib/support/helpers/time.js b/lib/support/helpers/time.js index 977f443ed..3f2111f99 100644 --- a/lib/support/helpers/time.js +++ b/lib/support/helpers/time.js @@ -1,8 +1,6 @@ -'use strict'; +import { plural } from './plural.js'; -const plural = require('./plural'); - -module.exports = { +export const time = { duration(seconds) { if (seconds === undefined) { return ''; @@ -34,10 +32,6 @@ module.exports = { } }, ms(ms) { - if (ms < 1000) { - return ms + ' ms'; - } else { - return Number(ms / 1000).toFixed(3) + ' s'; - } + return ms < 1000 ? ms + ' ms' : Number(ms / 1000).toFixed(3) + ' s'; } }; diff --git a/lib/support/messageMaker.js b/lib/support/messageMaker.js index f9eef8714..09c386c21 100644 --- a/lib/support/messageMaker.js +++ b/lib/support/messageMaker.js @@ -1,16 +1,14 @@ -'use strict'; +import dayjs from 'dayjs'; +import merge from 'lodash.merge'; +import { v4 as makeUuid } from 'uuid'; -const dayjs = require('dayjs'), - merge = require('lodash.merge'), - makeUuid = require('uuid').v4; - -module.exports = function messageMaker(source) { +export function messageMaker(source) { return { make(type, data, extras) { - const timestamp = dayjs().format(), - uuid = makeUuid(); + const timestamp = dayjs().format(); + const uuid = makeUuid(); return merge({ uuid, type, timestamp, source, data }, extras); } }; -}; +} diff --git a/lib/support/metricsFilter.js b/lib/support/metricsFilter.js index ad6134803..f64f15abe 100644 --- a/lib/support/metricsFilter.js +++ b/lib/support/metricsFilter.js @@ -1,12 +1,11 @@ -'use strict'; +/* eslint-disable unicorn/no-array-reduce */ +import isEmpty from 'lodash.isempty'; +import get from 'lodash.get'; +import set from 'lodash.set'; +import merge from 'lodash.merge'; +import reduce from 'lodash.reduce'; -const toArray = require('./util').toArray, - isEmpty = require('lodash.isempty'), - get = require('lodash.get'), - set = require('lodash.set'), - merge = require('lodash.merge'), - log = require('intel'), - reduce = require('lodash.reduce'); +import { toArray } from './util.js'; function normalizePath(path) { if (path.endsWith('.*')) return path.slice(0, -2); @@ -16,81 +15,70 @@ function normalizePath(path) { function mergePath(destination, source, path) { const value = get(source, path); - if (value !== undefined) { - return set(destination, path, value); - } else { - return destination; - } + return value !== undefined ? set(destination, path, value) : destination; } -module.exports = { - /** - * - * @param {Object} json - * @param {Array|string} metricPaths - */ - filterMetrics(json, metricPaths) { - metricPaths = toArray(metricPaths); - if (typeof json !== 'object') return undefined; +export function filterMetrics(json, metricPaths) { + metricPaths = toArray(metricPaths); + if (typeof json !== 'object') return; - return metricPaths.reduce((result, path) => { - path = normalizePath(path); + return metricPaths.reduce((result, path) => { + path = normalizePath(path); - const firstWildcard = path.indexOf('*.'); + const firstWildcard = path.indexOf('*.'); - if (firstWildcard === -1) { - mergePath(result, json, path); - } else if (firstWildcard === 0) { - const leafPath = path.substring(2); + if (firstWildcard === -1) { + mergePath(result, json, path); + } else if (firstWildcard === 0) { + const leafPath = path.slice(2); - reduce( - json, - (result, value, key) => { - if (typeof value === 'object') { - const leaf = this.filterMetrics(value, leafPath); + reduce( + json, + (result, value, key) => { + if (typeof value === 'object') { + const leaf = this.filterMetrics(value, leafPath); - if (!isEmpty(leaf)) { - result[key] = leaf; - } + if (!isEmpty(leaf)) { + result[key] = leaf; } - return result; - }, - result - ); - } else { - let branchPath = path.substring(0, firstWildcard); - if (branchPath.endsWith('.')) branchPath = branchPath.slice(0, -1); - - let branch = get(json, branchPath); - // We have ocurrences where the branch is undefined for WebPageTest data - // https://github.com/sitespeedio/sitespeed.io/issues/1897 - if (!branch) { - log.error( - 'Metricsfilter: The ' + - branchPath + - ' is missing from the metrics ' + - metricPaths - ); - return result; - } - const leafPath = path.substring(firstWildcard + 2); - const leafs = Object.keys(branch).reduce((result, key) => { - const leaf = this.filterMetrics(branch[key], leafPath); - - if (!isEmpty(leaf)) { - result[key] = leaf; } return result; - }, {}); + }, + result + ); + } else { + let branchPath = path.slice(0, Math.max(0, firstWildcard)); + if (branchPath.endsWith('.')) branchPath = branchPath.slice(0, -1); - branch = get(result, branchPath, leafs); - branch = merge(branch, leafs); - if (!isEmpty(branch)) { - return set(result, branchPath, branch); - } + let branch = get(json, branchPath); + // We have ocurrences where the branch is undefined for WebPageTest data + // https://github.com/sitespeedio/sitespeed.io/issues/1897 + if (!branch) { + // error( + // 'Metricsfilter: The ' + + // branchPath + + // ' is missing from the metrics ' + + // metricPaths + //); + return result; } + const leafPath = path.slice(Math.max(0, firstWildcard + 2)); + const leafs = Object.keys(branch).reduce((result, key) => { + const leaf = filterMetrics(branch[key], leafPath); - return result; - }, {}); - } -}; + if (!isEmpty(leaf)) { + result[key] = leaf; + } + return result; + }, {}); + + branch = get(result, branchPath, leafs); + branch = merge(branch, leafs); + if (!isEmpty(branch)) { + return set(result, branchPath, branch); + } + } + + return result; + }, {}); +} diff --git a/lib/support/statsHelpers.js b/lib/support/statsHelpers.js index fbb0e8249..b76209438 100644 --- a/lib/support/statsHelpers.js +++ b/lib/support/statsHelpers.js @@ -1,8 +1,6 @@ -'use strict'; - -const Stats = require('fast-stats').Stats, - get = require('lodash.get'), - set = require('lodash.set'); +import { Stats } from 'fast-stats'; +import get from 'lodash.get'; +import set from 'lodash.set'; function percentileName(percentile) { if (percentile === 0) { @@ -15,72 +13,60 @@ function percentileName(percentile) { } function isFiniteNumber(n) { - return typeof n === 'number' && isFinite(n); + return typeof n === 'number' && Number.isFinite(n); } -module.exports = { - /** - * Create or update a fast-stats#Stats object in target at path. - */ - pushStats(target, path, data) { - if (!isFiniteNumber(data)) - throw new Error(`Tried to add ${data} to stats for path ${path}`); +export function pushStats(target, path, data) { + if (!isFiniteNumber(data)) + throw new Error(`Tried to add ${data} to stats for path ${path}`); - const stats = get(target, path, new Stats()); - stats.push(data); - set(target, path, stats); - }, - - pushGroupStats(target, domainTarget, path, data) { - this.pushStats(target, path, data); - this.pushStats(domainTarget, path, data); - }, - - /** - * Summarize stats and put result in target at path - */ - setStatsSummary(target, path, stats) { - set(target, path, this.summarizeStats(stats)); - }, - - setStatsSummaryWithOptions(target, path, stats, options) { - set(target, path, this.summarizeStats(stats, options)); - }, - - summarizeStats(stats, options) { - if (stats.length === 0) { - return undefined; - } - - options = options || {}; - let percentiles = options.percentiles || [0, 90, 100]; - let decimals = options.decimals || 0; - if (stats.median() < 1 && stats.median() > 0) { - decimals = 4; - } - - let data = { - median: parseFloat(stats.median().toFixed(decimals)), - mean: parseFloat(stats.amean().toFixed(decimals)) - }; - percentiles.forEach(p => { - let name = percentileName(p); - const percentile = stats.percentile(p); - if (Number.isFinite(percentile)) { - data[name] = parseFloat(percentile.toFixed(decimals)); - } else { - throw new Error( - 'Failed to calculate ' + - name + - ' for stats: ' + - JSON.stringify(stats, null, 2) - ); - } - }); - if (options.includeSum) { - data.sum = parseFloat(stats.Σ().toFixed(decimals)); - } - - return data; + const stats = get(target, path, new Stats()); + stats.push(data); + set(target, path, stats); +} +export function pushGroupStats(target, domainTarget, path, data) { + pushStats(target, path, data); + pushStats(domainTarget, path, data); +} +export function setStatsSummary(target, path, stats) { + set(target, path, summarizeStats(stats)); +} +export function setStatsSummaryWithOptions(target, path, stats, options) { + set(target, path, summarizeStats(stats, options)); +} +export function summarizeStats(stats, options) { + if (stats.length === 0) { + return; } -}; + + options = options || {}; + let percentiles = options.percentiles || [0, 90, 100]; + let decimals = options.decimals || 0; + if (stats.median() < 1 && stats.median() > 0) { + decimals = 4; + } + + let data = { + median: Number.parseFloat(stats.median().toFixed(decimals)), + mean: Number.parseFloat(stats.amean().toFixed(decimals)) + }; + for (const p of percentiles) { + let name = percentileName(p); + const percentile = stats.percentile(p); + if (Number.isFinite(percentile)) { + data[name] = Number.parseFloat(percentile.toFixed(decimals)); + } else { + throw new TypeError( + 'Failed to calculate ' + + name + + ' for stats: ' + + JSON.stringify(stats, undefined, 2) + ); + } + } + if (options.includeSum) { + data.sum = Number.parseFloat(stats.Σ().toFixed(decimals)); + } + + return data; +} diff --git a/lib/support/tsdbUtil.js b/lib/support/tsdbUtil.js index f8d0f13f3..ec7835950 100644 --- a/lib/support/tsdbUtil.js +++ b/lib/support/tsdbUtil.js @@ -1,40 +1,35 @@ -'use strict'; +import get from 'lodash.get'; +import { keypathFromUrl } from './flattenMessage.js'; -const get = require('lodash.get'); -const flatten = require('./flattenMessage'); +export function toSafeKey(key, safeChar = '_') { + return key.replace(/[ %&+,./:?|~–]|%7C/g, safeChar); +} +export function getConnectivity(options) { + // if we have a friendly name for your connectivity, use that! + let connectivity = get(options, 'browsertime.connectivity.alias'); + return connectivity + ? this.toSafeKey(connectivity.toString()) + : get(options, 'browsertime.connectivity.profile', 'unknown'); +} -module.exports = { - toSafeKey(key, safeChar = '_') { - return key.replace(/[.~ /+|,:?&%–]|%7C/g, safeChar); - }, - getConnectivity(options) { - // if we have a friendly name for your connectivity, use that! - let connectivity = get(options, 'browsertime.connectivity.alias'); - if (connectivity) { - // If the alias is a number, it is converted to a Number by get. - return this.toSafeKey(connectivity.toString()); - } else { - return get(options, 'browsertime.connectivity.profile', 'unknown'); - } - }, - getURLAndGroup(options, group, url, includeQueryParams, alias) { - if ( - group && - options.urlsMetaData && - options.urlsMetaData[url] && - options.urlsMetaData[url].urlAlias - ) { - let alias = options.urlsMetaData[url].urlAlias; - return this.toSafeKey(group) + '.' + this.toSafeKey(alias); - } else if (alias && alias[url]) { - return this.toSafeKey(group) + '.' + this.toSafeKey(alias[url]); - } else { - return flatten.keypathFromUrl( - url, - includeQueryParams, - options.useHash, - group - ); - } +export function getURLAndGroup( + options, + group, + url, + includeQueryParameters, + alias +) { + if ( + group && + options.urlsMetaData && + options.urlsMetaData[url] && + options.urlsMetaData[url].urlAlias + ) { + let alias = options.urlsMetaData[url].urlAlias; + return this.toSafeKey(group) + '.' + this.toSafeKey(alias); + } else if (alias && alias[url]) { + return this.toSafeKey(group) + '.' + this.toSafeKey(alias[url]); + } else { + return keypathFromUrl(url, includeQueryParameters, options.useHash, group); } -}; +} diff --git a/lib/support/util.js b/lib/support/util.js index e3ae9027b..fc9ee3f71 100644 --- a/lib/support/util.js +++ b/lib/support/util.js @@ -1,27 +1,23 @@ -'use strict'; +import { format } from 'node:util'; -const format = require('util').format; - -module.exports = { - toArray(arrayLike) { - if (arrayLike === undefined || arrayLike === null) { - return []; - } - if (Array.isArray(arrayLike)) { - return arrayLike; - } - return [arrayLike]; - }, - throwIfMissing(options, keys, namespace) { - let missingKeys = keys.filter(key => !options[key]); - if (missingKeys.length > 0) { - throw new Error( - format( - 'Required option(s) %s need to be specified in namespace "%s"', - missingKeys.map(s => '"' + s + '"'), - namespace - ) - ); - } +export function toArray(arrayLike) { + if (arrayLike === undefined || arrayLike === null) { + return []; } -}; + if (Array.isArray(arrayLike)) { + return arrayLike; + } + return [arrayLike]; +} +export function throwIfMissing(options, keys, namespace) { + let missingKeys = keys.filter(key => !options[key]); + if (missingKeys.length > 0) { + throw new Error( + format( + 'Required option(s) %s need to be specified in namespace "%s"', + missingKeys.map(s => '"' + s + '"'), + namespace + ) + ); + } +} diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 4a97f67fe..9659092a6 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -10,19 +10,21 @@ "license": "MIT", "dependencies": { "@google-cloud/storage": "5.19.3", - "@tgwf/co2": "0.11.3", + "@sitespeed.io/plugin": "0.0.4", + "@tgwf/co2": "0.12.1", "aws-sdk": "2.1121.0", "axe-core": "4.4.3", - "browsertime": "17.0.0-beta.4", - "cli-color": "2.0.2", - "coach-core": "7.1.2", + "browsertime": "17.1.0", + "cli-color": "2.0.3", + "coach-core": "7.1.3", "concurrent-queue": "7.0.2", - "dayjs": "1.11.1", + "dayjs": "1.11.7", "fast-crc32c": "2.0.0", "fast-stats": "0.0.6", - "find-up": "5.0.0", - "fs-extra": "10.1.0", + "find-up": "6.3.0", + "fs-extra": "11.1.0", "getos": "3.2.1", + "import-global": "0.1.0", "influx": "5.9.3", "intel": "1.2.0", "jstransformer-markdown-it": "2.1.0", @@ -60,9 +62,10 @@ "ava": "4.3.3", "changelog-parser": "2.8.1", "clean-css-cli": "5.6.0", - "eslint": "8.14.0", + "eslint": "8.31.0", "eslint-config-prettier": "8.5.0", - "eslint-plugin-prettier": "4.0.0", + "eslint-plugin-prettier": "4.2.1", + "eslint-plugin-unicorn": "44.0.2", "feed": "4.2.2", "jsdoc": "^3.6.7", "license-checker": "^25.0.0", @@ -76,10 +79,101 @@ "node": ">=14.19.1" } }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } }, "node_modules/@babel/parser": { "version": "7.14.3", @@ -92,24 +186,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/runtime": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.0.tgz", - "integrity": "sha512-NDYdls71fTXoU8TZHfbBWg7DiZfNzClcKui/+kyi6ppD2L1qnWW3VV6CjtaBXSUGGhiTWJ6ereOIkUvenif66Q==", - "optional": true, - "dependencies": { - "regenerator-runtime": "^0.13.10" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime/node_modules/regenerator-runtime": { - "version": "0.13.10", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", - "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==", - "optional": true - }, "node_modules/@babel/types": { "version": "7.13.14", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", @@ -195,23 +271,26 @@ "integrity": "sha1-L3tOyAIWMoqf3evfacjUlC/v99g=" }, "node_modules/@eslint/eslintrc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", - "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", + "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.1", - "globals": "^13.9.0", + "espree": "^9.4.0", + "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/eslintrc/node_modules/debug": { @@ -231,6 +310,18 @@ } } }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/eslintrc/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -327,14 +418,14 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" }, "engines": { "node": ">=10.10.0" @@ -357,12 +448,37 @@ } } }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/config-array/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -370,13 +486,12 @@ "dev": true }, "node_modules/@jimp/bmp": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.16.2.tgz", - "integrity": "sha512-4g9vW45QfMoGhLVvaFj26h4e7cC+McHUQwyFQmNTLW4FfC1OonN9oUr2m/FEDGkTYKR7aqdXR5XUqqIkHWLaFw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.22.4.tgz", + "integrity": "sha512-ZDwQ/tLihpZuTCFGGa0zcyZIWHfhvHkrdbsoHUY0GG/JpH/y2xzlm2I48/TicCpoujN8oGKLHIJje0HmVX3xaA==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", + "@jimp/utils": "^0.22.4", "bmp-js": "^0.1.0" }, "peerDependencies": { @@ -384,22 +499,20 @@ } }, "node_modules/@jimp/core": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.16.2.tgz", - "integrity": "sha512-dp7HcyUMzjXphXYodI6PaXue+I9PXAavbb+AN+1XqFbotN22Z12DosNPEyy+UhLY/hZiQQqUkEaJHkvV31rs+w==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.22.4.tgz", + "integrity": "sha512-K7guEYpXV44SCLR35QdPyKqF+mFZaEUAqiSL8qQ/F4N4Ws9JkPzFI/qYTjOkDoKxSWkXlKnlsk1sfMzy0yqA5g==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", + "@jimp/utils": "^0.22.4", "any-base": "^1.1.0", "buffer": "^5.2.0", "exif-parser": "^0.1.12", - "file-type": "^9.0.0", - "load-bmfont": "^1.3.1", - "mkdirp": "^0.5.1", - "phin": "^2.9.1", + "file-type": "^16.5.4", + "isomorphic-fetch": "^3.0.0", + "mkdirp": "^2.1.3", "pixelmatch": "^4.0.2", - "tinycolor2": "^1.4.1" + "tinycolor2": "^1.6.0" } }, "node_modules/@jimp/core/node_modules/buffer": { @@ -426,24 +539,37 @@ "ieee754": "^1.1.13" } }, + "node_modules/@jimp/core/node_modules/mkdirp": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.3.tgz", + "integrity": "sha512-sjAkg21peAG9HS+Dkx7hlG9Ztx7HLeKnvB3NQRcu/mltCVmvkF0pisbiTSfDVYTT86XEfZrTUosLdZLStquZUw==", + "optional": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@jimp/custom": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.16.2.tgz", - "integrity": "sha512-GtNwOs4hcVS2GIbqRUf42rUuX07oLB92cj7cqxZb0ZGWwcwhnmSW0TFLAkNafXmqn9ug4VTpNvcJSUdiuECVKg==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.4.tgz", + "integrity": "sha512-k9m/RfxjPjklUsgZ2nszlyNkodUG/4xlxlif70UELhxW8bdqZqqlQGzwA9p+PUiSnlSJYZjL6q+P8cd7yj1ggA==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/core": "^0.16.2" + "@jimp/core": "^0.22.4" } }, "node_modules/@jimp/gif": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.16.2.tgz", - "integrity": "sha512-TMdyT9Q0paIKNtT7c5KzQD29CNCsI/t8ka28jMrBjEK7j5RRTvBfuoOnHv7pDJRCjCIqeUoaUSJ7QcciKic6CA==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.22.4.tgz", + "integrity": "sha512-KmN7GoaQTzLAX4JXLBRkIiZAXthgQdKe+Y7BOw4n6CMe6LAS/XCQqrYCG3Av/GqIO7UAKems6D7kIGAUuhpNlQ==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", + "@jimp/utils": "^0.22.4", "gifwrap": "^0.9.2", "omggif": "^1.0.9" }, @@ -452,80 +578,74 @@ } }, "node_modules/@jimp/jpeg": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.16.2.tgz", - "integrity": "sha512-BW5gZydgq6wdIwHd+3iUNgrTklvoQc/FUKSj9meM6A0FU21lUaansRX5BDdJqHkyXJLnnlDGwDt27J+hQuBAVw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.22.4.tgz", + "integrity": "sha512-mMJNhEtJpne65mxpIXEvT0VIzmsKiZWmaFT/c2eQ2tBLEtWAFpkvoP+F7jEaU+F3Ur4fXKFkJ/xOSXtRr/gGNw==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", - "jpeg-js": "^0.4.2" + "@jimp/utils": "^0.22.4", + "jpeg-js": "^0.4.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-blit": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.16.2.tgz", - "integrity": "sha512-Z31rRfV80gC/r+B/bOPSVVpJEWXUV248j7MdnMOFLu4vr8DMqXVo9jYqvwU/s4LSTMAMXqm4Jg6E/jQfadPKAg==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.4.tgz", + "integrity": "sha512-QQHe+rFarsxJQxWKlyHEMfLyXmUG9AiQky+8WfwjZVBYilIFyiBywOc3sThonOsru+7LOSUDmbN6mvbFk4R+gw==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-blur": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.16.2.tgz", - "integrity": "sha512-ShkJCAzRI+1fAKPuLLgEkixpSpVmKTYaKEFROUcgmrv9AansDXGNCupchqVMTdxf8zPyW8rR1ilvG3OJobufLQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.4.tgz", + "integrity": "sha512-p57Ac5LEQckIogiwf7qyOojGvLOD08eMaQd5ylOhet/fbdwAzD/8flWFhSIKsdAVzvnfGcszuLtrsV07jDutTw==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-circle": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.16.2.tgz", - "integrity": "sha512-6T4z/48F4Z5+YwAVCLOvXQcyGmo0E3WztxCz6XGQf66r4JJK78+zcCDYZFLMx0BGM0091FogNK4QniP8JaOkrA==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.22.4.tgz", + "integrity": "sha512-T+TpG+s+wM9kKHlpIEfCAfOM+QrYVqcMoWjkULddc0KtaDEhqgGYFhN+/SlzJfDbZKw0xUgIuAw89sXuzMIUjw==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-color": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.16.2.tgz", - "integrity": "sha512-6oBV0g0J17/7E+aTquvUsgSc85nUbUi+64tIK5eFIDzvjhlqhjGNJYlc46KJMCWIs61qRJayQoZdL/iT/iQuGQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.4.tgz", + "integrity": "sha512-TZqcqepoCcIlF7VodPPfS3WET+LL5Y/XnXOBk4tWnG5i+lhNrs7/U0HOJY6Iw9o4g267DddnlfKWmunvzBcvOQ==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", - "tinycolor2": "^1.4.1" + "@jimp/utils": "^0.22.4", + "tinycolor2": "^1.6.0" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-contain": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.16.2.tgz", - "integrity": "sha512-pLcxO3hVN3LCEhMNvpZ9B7xILHVlS433Vv16zFFJxLRqZdYvPLsc+ZzJhjAiHHuEjVblQrktHE3LGeQwGJPo0w==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.22.4.tgz", + "integrity": "sha512-Hl+TO4v+EpRfEl3R6k/bEgOGOpm6JqNfEIyCFWLi6yqJQjMGzBQ0vt+VHe2u3WIFaFrDWsGxeuFZBDzgtjTwxw==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", @@ -535,13 +655,12 @@ } }, "node_modules/@jimp/plugin-cover": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.16.2.tgz", - "integrity": "sha512-gzWM7VvYeI8msyiwbUZxH+sGQEgO6Vd6adGxZ0CeKX00uQOe5lDzxb1Wjx7sHcJGz8a/5fmAuwz7rdDtpDUbkw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.22.4.tgz", + "integrity": "sha512-KMTQjN/B7r/RNzoLFwnhqhLrgT0kMqTkBMEZQSopj5vPLPNjIX0ElEYC8AIVFKeZAV+1mYkyss+IDdxq4fyRng==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", @@ -551,65 +670,60 @@ } }, "node_modules/@jimp/plugin-crop": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.16.2.tgz", - "integrity": "sha512-qCd3hfMEE+Z2EuuyXewgXRTtKJGIerWzc1zLEJztsUkPz5i73IGgkOL+mrNutZwGaXZbm+8SwUaGb46sxAO6Tw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.4.tgz", + "integrity": "sha512-8krDt7xzBa1fbtlYCzEMZIgNjTkhgywho0FJpgIMkIUMjaZITS1Ea/Veb3UrWt8EsgQS6hxjGVE/Q1FvP5iPLA==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-displace": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.16.2.tgz", - "integrity": "sha512-6nXdvNNjCdD95v2o3/jPeur903dz08lG4Y8gmr5oL2yVv9LSSbMonoXYrR/ASesdyXqGdXJLU4NL+yZs4zUqbQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.22.4.tgz", + "integrity": "sha512-3gBfwYVFrOjp8SUpb7H0UMgqvsG/sxY1PVBIfRW9MUCosiH1eE/Mo5cbxhQ6/w5f3sh23lBmG8W0WuSrnXLorg==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-dither": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.16.2.tgz", - "integrity": "sha512-DERpIzy21ZanMkVsD0Tdy8HQLbD1E41OuvIzaMRoW4183PA6AgGNlrQoFTyXmzjy6FTy1SxaQgTEdouInAWZ9Q==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.22.4.tgz", + "integrity": "sha512-oOhdZBDJpSGIoTUhPOIvLIVUwILRWgrWdA4Vbzcyz2RHvaPHS8gdBH0EdIPbJ5agNyFnY8sJWFM7YKx/rLNKsw==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-fisheye": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.16.2.tgz", - "integrity": "sha512-Df7PsGIwiIpQu3EygYCnaJyTfOwvwtYV3cmYJS7yFLtdiFUuod+hlSo5GkwEPLAy+QBxhUbDuUqnsWo4NQtbiQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.22.4.tgz", + "integrity": "sha512-2myNZyDrwUOV8MKd4NeULnEOojYF7XRbnRHiUPsNptpmK6g/gI4xt+5k7BallAYZD8ZLfZVjstUogsObprHd/Q==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-flip": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.16.2.tgz", - "integrity": "sha512-+2uC8ioVQUr06mnjSWraskz2L33nJHze35LkQ8ZNsIpoZLkgvfiWatqAs5bj+1jGI/9kxoCFAaT1Is0f+a4/rw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.22.4.tgz", + "integrity": "sha512-9FZ0k5N5leLDefeDjizXXTl17dzo23PYtCD/T4xeSVr96d1pQDwbeIk7pEhhHr1rl98tJe0U/OV2dFXFYauKPw==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", @@ -617,66 +731,61 @@ } }, "node_modules/@jimp/plugin-gaussian": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.16.2.tgz", - "integrity": "sha512-2mnuDSg4ZEH8zcJig7DZZf4st/cYmQ5UYJKP76iGhZ+6JDACk6uejwAgT5xHecNhkVAaXMdCybA2eknH/9OE1w==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.22.4.tgz", + "integrity": "sha512-irOSwLdZ9kTq5Wd5dpkMgIMJVwemYcqgnzd04+P6TJGYmem2HR6JUCDpjbET3Fpbo/snFLm4mZ+2A+SmeCGjKA==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-invert": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.16.2.tgz", - "integrity": "sha512-xFvHbVepTY/nus+6yXiYN1iq+UBRkT0MdnObbiQPstUrAsz0Imn6MWISsnAyMvcNxHGrxaxjuU777JT/esM0gg==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.22.4.tgz", + "integrity": "sha512-/WtZeLrF+H+mzbjqudeGvvSxudlHy1kyiP1gVWDxhYNQOuZJI57Vn20kSTYvHBNjvy31LV4/uestyX8j8tE2Qg==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-mask": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.16.2.tgz", - "integrity": "sha512-AbdO85xxhfgEDdxYKpUotEI9ixiCMaIpfYHD5a5O/VWeimz2kuwhcrzlHGiyq1kKAgRcl0WEneTCZAHVSyvPKA==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.22.4.tgz", + "integrity": "sha512-U0SrOwBNKkMYTNPTz5CXeJdZ4c5easFlq2B9Txy0kPsav2OraTv8cZjpMxrWUejo/AQGVUDbaGtXMm9pE13/6w==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-normalize": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.16.2.tgz", - "integrity": "sha512-+ItBWFwmB0Od7OfOtTYT1gm543PpHUgU8/DN55z83l1JqS0OomDJAe7BmCppo2405TN6YtVm/csXo7p4iWd/SQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.22.4.tgz", + "integrity": "sha512-XJiPBJGCHWmIzUdmL4mWP1Ev5LMp77oMmPXdgQGDty1cxfyo3CbkTjZSsnwF/XLlrQ1yfLW+8JB+ihGKcVEOxA==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-print": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.16.2.tgz", - "integrity": "sha512-ifTGEeJ5UZTCiqC70HMeU3iXk/vsOmhWiwVGOXSFXhFeE8ZpDWvlmBsrMYnRrJGuaaogHOIrrQPI+kCdDBSBIQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.22.4.tgz", + "integrity": "sha512-mayiPhg6c7KYjvq3fYOW9ohhXD1eWdEiseV9dAWqTOEbDbohT8S6eTGhVIiVa2sVySLcpNEKZSk07c5EhJAMcw==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", - "load-bmfont": "^1.4.0" + "@jimp/utils": "^0.22.4", + "load-bmfont": "^1.4.1" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", @@ -684,26 +793,24 @@ } }, "node_modules/@jimp/plugin-resize": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.16.2.tgz", - "integrity": "sha512-gE4N9l6xuwzacFZ2EPCGZCJ/xR+aX2V7GdMndIl/6kYIw5/eib1SFuF9AZLvIPSFuE1FnGo8+vT0pr++SSbhYg==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.4.tgz", + "integrity": "sha512-2wMdpPNGf6Zo2lfJg1QHHQ+ds5baQH75IcFpdjw717dcEISpn1jPG//iClXOGh16OJsRQlwHESaZTgEo/5Dw/g==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/plugin-rotate": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.16.2.tgz", - "integrity": "sha512-/CTEYkR1HrgmnE0VqPhhbBARbDAfFX590LWGIpxcYIYsUUGQCadl+8Qo4UX13FH0Nt8UHEtPA+O2x08uPYg9UA==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.4.tgz", + "integrity": "sha512-g08LBsPENbeA6NVoeq0iuDgAL89+N+aZrvYVKYkiJZIM7vUvueJyAIq4+bjDl4r54OR8XBFX0GsrKsqrULh1eA==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", @@ -713,13 +820,12 @@ } }, "node_modules/@jimp/plugin-scale": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.16.2.tgz", - "integrity": "sha512-3inuxfrlquyLaqFdiiiQNJUurR0WbvN5wAf1qcYX2LubG1AG8grayYD6H7XVoxfUGTZXh1kpmeirEYlqA2zxcw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.4.tgz", + "integrity": "sha512-cJiLQtTcNk6/+j05R23TFGXy+smDV0BdlmzJVDb+7Ye9qcmWpkdjVSioQQqZr0QScIYKhhRCY/lFTepBx67yzw==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", @@ -727,13 +833,12 @@ } }, "node_modules/@jimp/plugin-shadow": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.16.2.tgz", - "integrity": "sha512-Q0aIs2/L6fWMcEh9Ms73u34bT1hyUMw/oxaVoIzOLo6/E8YzCs2Bi63H0/qaPS0MQpEppI++kvosPbblABY79w==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.22.4.tgz", + "integrity": "sha512-a5hdpzGBzBo91DNiKaGvs8iJbs2bYQmDRm/BrCh4NET+h5l5AwXNu/Ak0bWRhN16YQ55XYNGHer2jOwBPrf2WQ==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", @@ -742,13 +847,12 @@ } }, "node_modules/@jimp/plugin-threshold": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.16.2.tgz", - "integrity": "sha512-gyOwmBgjtMPvcuyOhkP6dOGWbQdaTfhcBRN22mYeI/k/Wh/Zh1OI21F6eKLApsVRmg15MoFnkrCz64RROC34sw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.22.4.tgz", + "integrity": "sha512-jTT/+p2zb2NESzd7O0bVRowiQszoaHeBf2OgP7lFde10fHd+fn78m5brUmSmlGAdlMRwm8S8ZcxTj5ZjdQns5w==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" }, "peerDependencies": { "@jimp/custom": ">=0.3.5", @@ -757,33 +861,32 @@ } }, "node_modules/@jimp/plugins": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.16.2.tgz", - "integrity": "sha512-zCvYtCgctmC0tkYEu+y+kSwSIZBsNznqJ3/3vkpzxdyjd6wCfNY5Qc/68MPrLc1lmdeGo4cOOTYHG7Vc6myzRw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.22.4.tgz", + "integrity": "sha512-yAxcA4UR3Bs7j73I7wt4ty52vm5MzPmr+8DYk8jrS/ng2Z2iuXzbcTe4mf9eEqXYVah3rTIggo4dPjW75DRZtA==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/plugin-blit": "^0.16.2", - "@jimp/plugin-blur": "^0.16.2", - "@jimp/plugin-circle": "^0.16.2", - "@jimp/plugin-color": "^0.16.2", - "@jimp/plugin-contain": "^0.16.2", - "@jimp/plugin-cover": "^0.16.2", - "@jimp/plugin-crop": "^0.16.2", - "@jimp/plugin-displace": "^0.16.2", - "@jimp/plugin-dither": "^0.16.2", - "@jimp/plugin-fisheye": "^0.16.2", - "@jimp/plugin-flip": "^0.16.2", - "@jimp/plugin-gaussian": "^0.16.2", - "@jimp/plugin-invert": "^0.16.2", - "@jimp/plugin-mask": "^0.16.2", - "@jimp/plugin-normalize": "^0.16.2", - "@jimp/plugin-print": "^0.16.2", - "@jimp/plugin-resize": "^0.16.2", - "@jimp/plugin-rotate": "^0.16.2", - "@jimp/plugin-scale": "^0.16.2", - "@jimp/plugin-shadow": "^0.16.2", - "@jimp/plugin-threshold": "^0.16.2", + "@jimp/plugin-blit": "^0.22.4", + "@jimp/plugin-blur": "^0.22.4", + "@jimp/plugin-circle": "^0.22.4", + "@jimp/plugin-color": "^0.22.4", + "@jimp/plugin-contain": "^0.22.4", + "@jimp/plugin-cover": "^0.22.4", + "@jimp/plugin-crop": "^0.22.4", + "@jimp/plugin-displace": "^0.22.4", + "@jimp/plugin-dither": "^0.22.4", + "@jimp/plugin-fisheye": "^0.22.4", + "@jimp/plugin-flip": "^0.22.4", + "@jimp/plugin-gaussian": "^0.22.4", + "@jimp/plugin-invert": "^0.22.4", + "@jimp/plugin-mask": "^0.22.4", + "@jimp/plugin-normalize": "^0.22.4", + "@jimp/plugin-print": "^0.22.4", + "@jimp/plugin-resize": "^0.22.4", + "@jimp/plugin-rotate": "^0.22.4", + "@jimp/plugin-scale": "^0.22.4", + "@jimp/plugin-shadow": "^0.22.4", + "@jimp/plugin-threshold": "^0.22.4", "timm": "^1.6.1" }, "peerDependencies": { @@ -791,44 +894,41 @@ } }, "node_modules/@jimp/png": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.16.2.tgz", - "integrity": "sha512-sFOtOSz/tzDwXEChFQ/Nxe+0+vG3Tj0eUxnZVDUG/StXE9dI8Bqmwj3MIa0EgK5s+QG3YlnDOmlPUa4JqmeYeQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.22.4.tgz", + "integrity": "sha512-kDovx9dTyV/TSR40HQHdRyVgNNb7Cny4/0PPEa+xeR7snuDC3dV5hu9s/QJwY0RMGiAkiuKDpiaBuSZuz8dwRQ==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", - "pngjs": "^3.3.3" + "@jimp/utils": "^0.22.4", + "pngjs": "^6.0.0" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/tiff": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.16.2.tgz", - "integrity": "sha512-ADcdqmtZF+U2YoaaHTzFX8D6NFpmN4WZUT0BPMerEuY7Cq8QoLYU22z2h034FrVW+Rbi1b3y04sB9iDiQAlf2w==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.22.4.tgz", + "integrity": "sha512-RStUATRnb+unYzzuGxU+SPZALqh5NxYdcS6UGTBvhCMlijopGiY/iL01wstIOst0ypKIjwbcSVj7mAHn6B7Qbw==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "utif": "^2.0.1" + "utif2": "^4.0.1" }, "peerDependencies": { "@jimp/custom": ">=0.3.5" } }, "node_modules/@jimp/types": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.16.2.tgz", - "integrity": "sha512-0Ue5Sq0XnDF6TirisWv5E+8uOnRcd8vRLuwocJOhF76NIlcQrz+5r2k2XWKcr3d+11n28dHLXW5TKSqrUopxhA==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.22.4.tgz", + "integrity": "sha512-v3hm8LGc3we6P6ML0ticiLX7wtdvywrKthYxqVrJVIu3vRL0Z4q3ngFjwzqDmaIF8wC0neq98s/t7ODWfeIiRQ==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/bmp": "^0.16.2", - "@jimp/gif": "^0.16.2", - "@jimp/jpeg": "^0.16.2", - "@jimp/png": "^0.16.2", - "@jimp/tiff": "^0.16.2", + "@jimp/bmp": "^0.22.4", + "@jimp/gif": "^0.22.4", + "@jimp/jpeg": "^0.22.4", + "@jimp/png": "^0.22.4", + "@jimp/tiff": "^0.22.4", "timm": "^1.6.1" }, "peerDependencies": { @@ -836,19 +936,18 @@ } }, "node_modules/@jimp/utils": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.16.2.tgz", - "integrity": "sha512-XENrPvmigiXZQ8E2nxJqO6UVvWBLzbNwyYi3Y8Q1IECoYhYI3kgOQ0fmy4G269Vz1V0omh1bNmC42r4OfXg1Jg==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.22.4.tgz", + "integrity": "sha512-EPaBMNg4NvVXnMpSFJEsdCQqdSVU2ACreAL+Ipkq19C/FkDEj9Q10t6Mjx8zOe/AAjBQj1vTALS/DykcHOn4bQ==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", "regenerator-runtime": "^0.13.3" } }, "node_modules/@jimp/utils/node_modules/regenerator-runtime": { - "version": "0.13.10", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", - "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==", + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "optional": true }, "node_modules/@nodelib/fs.scandir": { @@ -887,36 +986,52 @@ } }, "node_modules/@sitespeed.io/chromedriver": { - "version": "106.0.5249-61", - "resolved": "https://registry.npmjs.org/@sitespeed.io/chromedriver/-/chromedriver-106.0.5249-61.tgz", - "integrity": "sha512-Cv6Kp2hTR1GEwzZ+xy3EjVUu4lUCMcu9jEwDTONZu3fI1myjtZ96F7KgnDPTV4HRHj2VPK0jDWmSsEREcMBn1A==", + "version": "110.0.5481-77", + "resolved": "https://registry.npmjs.org/@sitespeed.io/chromedriver/-/chromedriver-110.0.5481-77.tgz", + "integrity": "sha512-1T/1KzSya6clqIcbylzrDEA79Sx90fTbBMX7VFuo9T1Qa51lZ28DT+Pm+i76LvIFXBQAieTvw3Ira8/gyfKewA==", "hasInstallScript": true, "dependencies": { - "node-downloader-helper": "2.1.1", + "node-downloader-helper": "2.1.5", "node-stream-zip": "1.15.0" } }, "node_modules/@sitespeed.io/edgedriver": { - "version": "106.0.1370-34", - "resolved": "https://registry.npmjs.org/@sitespeed.io/edgedriver/-/edgedriver-106.0.1370-34.tgz", - "integrity": "sha512-dOtxmGlGTXdx8sLM7hMmG+OQOM4iw6Me75Neu/UHvkEuUY3EtAvZq7mGrpxTTAV5eK9J0FhLcW/S3xmsHdYvVw==", + "version": "109.0.1518-70", + "resolved": "https://registry.npmjs.org/@sitespeed.io/edgedriver/-/edgedriver-109.0.1518-70.tgz", + "integrity": "sha512-ZLSZ54UrWRPODKooNMM3X36GTyQ8Rrwopr9adVsRvoTuBjWWf0+Od/f5OoraYBJJA2OsR4yV92sC/S1lSK+rbw==", "hasInstallScript": true, "dependencies": { - "node-downloader-helper": "2.1.1", + "node-downloader-helper": "2.1.6", "node-stream-zip": "1.15.0" } }, + "node_modules/@sitespeed.io/edgedriver/node_modules/node-downloader-helper": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.6.tgz", + "integrity": "sha512-VkOvAXIopI3xMuM/MC5UL7NqqnizQ/9QXZt28jR8FPZ6fHLQm4xe4+YXJ9FqsWwLho5BLXrF51nfOQ0QcohRkQ==", + "bin": { + "ndh": "bin/ndh" + }, + "engines": { + "node": ">=14.18" + } + }, "node_modules/@sitespeed.io/geckodriver": { - "version": "0.31.0-c", - "resolved": "https://registry.npmjs.org/@sitespeed.io/geckodriver/-/geckodriver-0.31.0-c.tgz", - "integrity": "sha512-VLOM5N9TAZYoigx0M/7OyNhdh7xCNEG3NAuwJ6A+5G966nbYELfGbnoxDdCgI5Yo6zKjjU/F2yPLoyrbOmmgYQ==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@sitespeed.io/geckodriver/-/geckodriver-0.32.0.tgz", + "integrity": "sha512-5h/h+S31/gdlFDWh7GUkYW0fuKPTg1l6BKk0gAH4Nz1bJ7nSex2fV+F0/csoaLU7Z8sQFHi1B4QQiwfl1+6ugw==", "hasInstallScript": true, "dependencies": { - "node-downloader-helper": "2.1.1", + "node-downloader-helper": "2.1.5", "node-stream-zip": "1.15.0", - "tar": "6.1.11" + "tar": "6.1.13" } }, + "node_modules/@sitespeed.io/plugin": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@sitespeed.io/plugin/-/plugin-0.0.4.tgz", + "integrity": "sha512-by3DxWv5t6cH6pUpUFA3vNggdmxUCqHBN9rED1W3G+nQLw1OE/sBE0vGs92mQqHTILN6Y2wbTWxtYPRjEWDsJQ==" + }, "node_modules/@sitespeed.io/throttle": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@sitespeed.io/throttle/-/throttle-5.0.0.tgz", @@ -964,9 +1079,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/@tgwf/co2": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@tgwf/co2/-/co2-0.11.3.tgz", - "integrity": "sha512-aFM52uDV0M/tx/BYu1w3K215pei5J2PHlsnaQKSr9/sJ5k2az1M2x8x/3Q70EJiEB8MtsHE2wWl8y17TacfKRQ==", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@tgwf/co2/-/co2-0.12.1.tgz", + "integrity": "sha512-sg0B0AsdYhhG/hLmFouEG3ePFpdQK0hXJdkI7M9URYZ1pQ+HYtUnzb4H+36+UnCxHqbrGwUJ5wdWtdXU0RwcFA==", "dependencies": { "debug": "^4.3.4" } @@ -992,6 +1107,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "optional": true + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -1043,6 +1164,12 @@ "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", "optional": true }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "dev": true + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1724,21 +1851,21 @@ } }, "node_modules/browsertime": { - "version": "17.0.0-beta.4", - "resolved": "https://registry.npmjs.org/browsertime/-/browsertime-17.0.0-beta.4.tgz", - "integrity": "sha512-erykocFSBCjl51qUAxzcMx1S8EGr66ub+iF8H2ESb+97qP9ZSY5bcLjVQHgAlL/ujb7K25XIdWHBd1f+Z+3vXA==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/browsertime/-/browsertime-17.1.0.tgz", + "integrity": "sha512-JRVr8dI/recSlyyZPB4VvBmb6p8E51DOJ0BBsowxO5Ms+T5j+dk7sY92lNqEsm6vPhP6t3HHUn+gdFte+nhfNw==", "dependencies": { "@cypress/xvfb": "1.2.4", "@devicefarmer/adbkit": "2.11.3", - "@sitespeed.io/chromedriver": "106.0.5249-61", - "@sitespeed.io/edgedriver": "106.0.1370-34", - "@sitespeed.io/geckodriver": "0.31.0-c", + "@sitespeed.io/chromedriver": "110.0.5481-77", + "@sitespeed.io/edgedriver": "109.0.1518-70", + "@sitespeed.io/geckodriver": "0.32.0", "@sitespeed.io/throttle": "5.0.0", "@sitespeed.io/tracium": "0.3.3", "btoa": "1.2.1", - "chrome-har": "0.13.0", - "chrome-remote-interface": "0.31.3", - "dayjs": "1.11.6", + "chrome-har": "0.13.1", + "chrome-remote-interface": "0.32.0", + "dayjs": "1.11.7", "execa": "6.1.0", "fast-stats": "0.0.6", "find-up": "6.3.0", @@ -1751,8 +1878,8 @@ "lodash.merge": "4.6.2", "lodash.pick": "4.4.0", "lodash.set": "4.3.2", - "selenium-webdriver": "4.5.0", - "yargs": "17.6.0" + "selenium-webdriver": "4.8.0", + "yargs": "17.6.2" }, "bin": { "browsertime": "bin/browsertime.js" @@ -1761,7 +1888,7 @@ "node": ">=14.19.1" }, "optionalDependencies": { - "jimp": "0.16.2" + "jimp": "0.22.1" } }, "node_modules/browsertime/node_modules/ansi-regex": { @@ -1785,26 +1912,6 @@ "node": ">=12" } }, - "node_modules/browsertime/node_modules/dayjs": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.6.tgz", - "integrity": "sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ==" - }, - "node_modules/browsertime/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/browsertime/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -1817,9 +1924,9 @@ } }, "node_modules/browsertime/node_modules/yargs": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", - "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -1827,7 +1934,7 @@ "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" + "yargs-parser": "^21.1.1" }, "engines": { "node": ">=12" @@ -1878,12 +1985,15 @@ } }, "node_modules/builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/call-bind": { @@ -2016,68 +2126,71 @@ } }, "node_modules/chrome-har": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/chrome-har/-/chrome-har-0.13.0.tgz", - "integrity": "sha512-wknoUfURyGp+sGvikTrOGpsL+bhzinqciItSGXwklIc9logrfrwZb6Ca/ZTho4twDKsy0f4dAVL79jHyvOG4Qw==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/chrome-har/-/chrome-har-0.13.1.tgz", + "integrity": "sha512-R9OXxVeCOqJYmqO8Oco+nouJ4frwVFQ6rGUgj5dJOfVNxBMmwCannp+DH2IAfqY/K8WJ5VYCQyq7SLTNZtcCXA==", "dependencies": { - "dayjs": "1.8.31", - "debug": "4.1.1", - "tough-cookie": "4.0.0", - "uuid": "8.0.0" + "dayjs": "1.11.7", + "debug": "4.3.4", + "tough-cookie": "4.1.2", + "uuid": "9.0.0" } }, - "node_modules/chrome-har/node_modules/dayjs": { - "version": "1.8.31", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.31.tgz", - "integrity": "sha512-mPh1mslned+5PuIuiUfbw4CikHk6AEAf2Baxih+wP5fssv+wmlVhvgZ7mq+BhLt7Sr/Hc8leWDiwe6YnrpNt3g==" - }, "node_modules/chrome-har/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/chrome-har/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/chrome-har/node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "engines": { "node": ">=6" } }, "node_modules/chrome-har/node_modules/tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", + "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { "node": ">=6" } }, "node_modules/chrome-har/node_modules/uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/chrome-remote-interface": { - "version": "0.31.3", - "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.31.3.tgz", - "integrity": "sha512-NTwb1YNPHXLTus1RjqsLxJmdViKwKJg/lrFEcM6pbyQy04Ow2QKWHXyPpxzwE+dFsJghWuvSAdTy4W0trluz1g==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.32.0.tgz", + "integrity": "sha512-g8xK3lKvAgEs3Hj/masMfYOyIFbDkXsMxD7e55TRUvbL7pAb6X9uo+0mKQFjZqQ7DN3b8DIdBfkKw1nwkeWHhw==", "dependencies": { "commander": "2.11.x", "ws": "^7.2.0" @@ -2098,9 +2211,9 @@ "dev": true }, "node_modules/ci-info": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", - "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", "dev": true }, "node_modules/ci-parallel-vars": { @@ -2110,9 +2223,9 @@ "dev": true }, "node_modules/clean-css": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz", - "integrity": "sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", "dev": true, "dependencies": { "source-map": "~0.6.0" @@ -2148,6 +2261,18 @@ "node": ">= 10" } }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/clean-stack": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", @@ -2185,12 +2310,12 @@ } }, "node_modules/cli-color": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.2.tgz", - "integrity": "sha512-g4JYjrTW9MGtCziFNjkqp3IMpGhnJyeB0lOtRPjQkYhXzKYr6tYnXKyEVnMzITxhpbahsEW9KsxOYIDKwcsIBw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.3.tgz", + "integrity": "sha512-OkoZnxyC4ERN3zLzZaY9Emb7f/MhBOIpePv0Ycok0fJYT+Ouo00UBEIwsVsr0yoow++n5YWlSUgST9GKhNHiRQ==", "dependencies": { "d": "^1.0.1", - "es5-ext": "^0.10.59", + "es5-ext": "^0.10.61", "es6-iterator": "^2.0.3", "memoizee": "^0.4.15", "timers-ext": "^0.1.7" @@ -2289,9 +2414,9 @@ } }, "node_modules/coach-core": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/coach-core/-/coach-core-7.1.2.tgz", - "integrity": "sha512-MuziK7sWmkQPHgkVy7vpegtpEJgt8LE9no2x5TwTQIhSMqSeUeREhzr6ayQ0XCjezRZV3+nB68TblEK7GKTbyQ==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/coach-core/-/coach-core-7.1.3.tgz", + "integrity": "sha512-mKJSg7nOU3H384WjHWzKm2GeERcEDnMTwRP1YIJt3q2kw9f68n6R8o9ETsgDRGIQANSqJtUEJ2na07L5rS71hg==", "dependencies": { "filter-files": "0.4.0", "json-stable-stringify": "1.0.1", @@ -2299,7 +2424,7 @@ "lodash.merge": "4.6.2", "lodash.sortby": "4.7.0", "pagexray": "4.4.2", - "third-party-web": "0.17.1", + "third-party-web": "0.20.2", "wappalyzer-core": "6.6.0" }, "engines": { @@ -2582,9 +2707,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.1.tgz", - "integrity": "sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA==" + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" }, "node_modules/dbug": { "version": "0.4.2", @@ -2879,19 +3004,6 @@ "stream-shift": "^1.0.0" } }, - "node_modules/duplexify/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2951,6 +3063,15 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.5.tgz", @@ -3084,13 +3205,15 @@ } }, "node_modules/eslint": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", - "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.31.0.tgz", + "integrity": "sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.2.2", - "@humanwhocodes/config-array": "^0.9.2", + "@eslint/eslintrc": "^1.4.1", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -3100,30 +3223,32 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", + "espree": "^9.4.0", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" @@ -3148,15 +3273,15 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz", - "integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=12.0.0" }, "peerDependencies": { "eslint": ">=7.28.0", @@ -3168,6 +3293,76 @@ } } }, + "node_modules/eslint-plugin-unicorn": { + "version": "44.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-44.0.2.tgz", + "integrity": "sha512-GLIDX1wmeEqpGaKcnMcqRvMVsoabeF0Ton0EX4Th5u6Kmf7RM9WBl705AXFEsns56ESkEs0uyelLuUTvz9Tr0w==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.19.1", + "ci-info": "^3.4.0", + "clean-regexp": "^1.0.0", + "eslint-utils": "^3.0.0", + "esquery": "^1.4.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.0", + "lodash": "^4.17.21", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.24", + "safe-regex": "^2.1.1", + "semver": "^7.3.7", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=8.23.1" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/is-builtin-module": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.0.tgz", + "integrity": "sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-scope": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", @@ -3286,6 +3481,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3310,12 +3521,78 @@ "node": ">=0.10.0" } }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/eslint/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/eslint/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3353,23 +3630,26 @@ } }, "node_modules/espree": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", - "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", "dev": true, "dependencies": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/espree/node_modules/acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -3712,12 +3992,20 @@ } }, "node_modules/file-type": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz", - "integrity": "sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw==", + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", "optional": true, + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, "node_modules/file-uri-to-path": { @@ -3762,70 +4050,20 @@ "dev": true }, "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" }, "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-up/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-up/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-up/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-up/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -3880,16 +4118,16 @@ } }, "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz", + "integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=14.14" } }, "node_modules/fs-extra/node_modules/universalify": { @@ -3911,6 +4149,17 @@ "node": ">= 8" } }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3935,12 +4184,6 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, "node_modules/functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -4114,10 +4357,21 @@ "process": "^0.11.10" } }, + "node_modules/global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "dependencies": { + "ini": "^1.3.4" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/globals": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", - "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", + "version": "13.19.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", + "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -4206,6 +4460,12 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "node_modules/gtoken": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", @@ -4502,6 +4762,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-global": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/import-global/-/import-global-0.1.0.tgz", + "integrity": "sha512-8+hPJLML+m1ym9NSeZXTXFkY5+ml0fYFAzO5yhZiaFQvk9kO0NkE7vd7e7kCVjkTmAxsDPbrWwLQACMwGTDgIg==", + "dependencies": { + "global-dirs": "^0.1.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4541,6 +4812,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/intel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/intel/-/intel-1.2.0.tgz", @@ -4594,6 +4870,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "node_modules/is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -4632,18 +4914,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true, - "dependencies": { - "builtin-modules": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-callable": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", @@ -4979,28 +5249,37 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "optional": true, + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "node_modules/jimp": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.16.2.tgz", - "integrity": "sha512-UpItBk81a92f8oEyoGYbO3YK4QcM0hoIyuGHmShoF9Ov63P5Qo7Q/X2xsAgnODmSuDJFOtrPtJd5GSWW4LKdOQ==", + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.22.1.tgz", + "integrity": "sha512-TzVcbFgnqEjE6nADwtXSAb5xdXimLLwA0q8MWPfhZyrObvUWBDFFH6Dm9Kr4CRgxd8/Mx+7+ugktY7NchNYUzg==", "optional": true, "dependencies": { - "@babel/runtime": "^7.7.2", - "@jimp/custom": "^0.16.2", - "@jimp/plugins": "^0.16.2", - "@jimp/types": "^0.16.2", + "@jimp/custom": "^0.22.1", + "@jimp/plugins": "^0.22.1", + "@jimp/types": "^0.22.1", "regenerator-runtime": "^0.13.3" } }, "node_modules/jimp/node_modules/regenerator-runtime": { - "version": "0.13.10", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", - "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==", + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "optional": true }, "node_modules/jmespath": { @@ -5017,6 +5296,12 @@ "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "optional": true }, + "node_modules/js-sdsl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", + "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", + "dev": true + }, "node_modules/js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -5031,6 +5316,12 @@ "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -5186,6 +5477,12 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -5287,6 +5584,20 @@ "setimmediate": "^1.0.5" } }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/junit-report-builder": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/junit-report-builder/-/junit-report-builder-3.0.0.tgz", @@ -5473,6 +5784,12 @@ "integrity": "sha1-xDkrWH3qOFgMlnhXDm6OSfzlJiI=", "dev": true }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "node_modules/linkify-it": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz", @@ -5893,6 +6210,15 @@ "dom-walk": "^0.1.0" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -5910,12 +6236,9 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "node_modules/minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "dependencies": { - "yallist": "^4.0.0" - }, + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.3.tgz", + "integrity": "sha512-OW2r4sQ0sI+z5ckEt5c1Tri4xTgZwYDxpE54eqWlQloQRoWtXjqt9udJ5Z4dSv7wK+nfFI7FRXyCpBSft+gpFw==", "engines": { "node": ">=8" } @@ -5932,11 +6255,22 @@ "node": ">= 8" } }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "devOptional": true, + "dev": true, "dependencies": { "minimist": "^1.2.5" }, @@ -5973,9 +6307,9 @@ "optional": true }, "node_modules/node-downloader-helper": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.1.tgz", - "integrity": "sha512-ouk8MGmJj1gYymbJwi1L8Mr6PdyheJLwfsmyx0KtsvyJ+7Fpf0kBBzM8Gmx8Mt/JBfRWP1PQm6dAGV6x7eNedw==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.5.tgz", + "integrity": "sha512-sLedzfv8C4VMAvTdDQcjLFAl3gydNeBXh2bLcCzvZRmd4EK0rkoTxJ8tkxnriUSJO/n13skJzH7l6CzCdBwYGg==", "bin": { "ndh": "bin/ndh" }, @@ -6062,13 +6396,13 @@ } }, "node_modules/normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "dependencies": { "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", + "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } @@ -6429,6 +6763,24 @@ "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==", "optional": true }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse-ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", @@ -6476,6 +6828,19 @@ "node": ">=8" } }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -6519,6 +6884,15 @@ "pixelmatch": "bin/pixelmatch" } }, + "node_modules/pixelmatch/node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/pkg-conf": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-4.0.0.tgz", @@ -6535,22 +6909,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pkg-conf/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/plur": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/plur/-/plur-5.1.0.tgz", @@ -6566,13 +6924,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/pngjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", - "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", "optional": true, "engines": { - "node": ">=4.0.0" + "node": ">=12.13.0" } }, "node_modules/prelude-ls": { @@ -6955,6 +7322,11 @@ "node": ">=0.4.x" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -7007,18 +7379,129 @@ "graceful-fs": "^4.1.2" } }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "optional": true, + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" } }, "node_modules/readdir-scoped-modules": { @@ -7062,6 +7545,15 @@ "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", "dev": true }, + "node_modules/regexp-tree": { + "version": "0.1.24", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", + "integrity": "sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==", + "dev": true, + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", @@ -7155,6 +7647,11 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/requizzle": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", @@ -7312,6 +7809,15 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -7340,28 +7846,28 @@ "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, "node_modules/selenium-webdriver": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.5.0.tgz", - "integrity": "sha512-9mSFii+lRwcnT2KUAB1kqvx6+mMiiQHH60Y0VUtr3kxxi3oZ3CV3B8e2nuJ7T4SPb+Q6VA0swswe7rYpez07Bg==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.8.0.tgz", + "integrity": "sha512-s/HL8WNwy1ggHR244+tAhjhyKMJnZLt1HKJ6Gn7nQgVjB/ybDF+46Uui0qI2J7AjPNJzlUmTncdC/jg/kKkn0A==", "dependencies": { "jszip": "^3.10.0", "tmp": "^0.2.1", - "ws": ">=8.7.0" + "ws": ">=8.11.0" }, "engines": { "node": ">= 14.20.0" } }, "node_modules/selenium-webdriver/node_modules/ws": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", - "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz", + "integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -7855,6 +8361,18 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -7864,6 +8382,23 @@ "node": ">=0.10.0" } }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "optional": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", @@ -7987,19 +8522,19 @@ } }, "node_modules/tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", + "minipass": "^4.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" }, "engines": { - "node": ">= 10" + "node": ">=10" } }, "node_modules/tar/node_modules/mkdirp": { @@ -8043,9 +8578,9 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" }, "node_modules/third-party-web": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.17.1.tgz", - "integrity": "sha512-X9Mha8cVeBwakunlZXkXL6xRzw8VCcDGWqT59EzeTYAJIi8ien3CuufnEGEx4ZUFahumNQdoOwf4H2T9Ca6lBg==" + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.20.2.tgz", + "integrity": "sha512-KFaFBDto+gH2DZW6ooFCGYrR8CGV6b/Ibsc2RTUkKhTPbxOWZuKs0NTftdAMoz0Aivf4bAHgW+kAGKciSQpqFg==" }, "node_modules/through": { "version": "2.3.8", @@ -8077,13 +8612,10 @@ "optional": true }, "node_modules/tinycolor2": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", - "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==", - "optional": true, - "engines": { - "node": "*" - } + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "optional": true }, "node_modules/tmp": { "version": "0.2.1", @@ -8122,6 +8654,43 @@ "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", "integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=" }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "optional": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/token-types/node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, "node_modules/tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -8235,9 +8804,9 @@ } }, "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "engines": { "node": ">= 4.0.0" } @@ -8272,18 +8841,27 @@ "querystring": "0.2.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/utcstring": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/utcstring/-/utcstring-0.1.0.tgz", "integrity": "sha1-Qw/VEKt/yVtdWRDJAteYgMIIQ2s=" }, - "node_modules/utif": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/utif/-/utif-2.0.1.tgz", - "integrity": "sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==", + "node_modules/utif2": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.0.1.tgz", + "integrity": "sha512-KMaD76dbzK1VjbwsckHJiqDjhP3pbpwyV+FdqkY6XFQenc2o/HS6pjPSYdu4+NQMHf2NLTW+nVP/eFP1CvOYQQ==", "optional": true, "dependencies": { - "pako": "^1.0.5" + "pako": "^1.0.11" } }, "node_modules/util-deprecate": { @@ -8305,12 +8883,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "node_modules/validate-npm-package-license": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", @@ -8370,6 +8942,12 @@ "node": ">=6" } }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "optional": true + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -8719,9 +9297,9 @@ } }, "node_modules/yargs-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "engines": { "node": ">=12" } @@ -8739,33 +9317,88 @@ } }, "dependencies": { + "@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + }, "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } }, "@babel/parser": { "version": "7.14.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.3.tgz", "integrity": "sha512-7MpZDIfI7sUC5zWo2+foJ50CSI5lcqDehZ0lVgIhSi4bFEk94fLAKlF3Q0nzSQQ+ca0lm+O6G9ztKVBeu8PMRQ==" }, - "@babel/runtime": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.0.tgz", - "integrity": "sha512-NDYdls71fTXoU8TZHfbBWg7DiZfNzClcKui/+kyi6ppD2L1qnWW3VV6CjtaBXSUGGhiTWJ6ereOIkUvenif66Q==", - "optional": true, - "requires": { - "regenerator-runtime": "^0.13.10" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.13.10", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", - "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==", - "optional": true - } - } - }, "@babel/types": { "version": "7.13.14", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", @@ -8842,19 +9475,19 @@ } }, "@eslint/eslintrc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", - "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", + "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.1", - "globals": "^13.9.0", + "espree": "^9.4.0", + "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "dependencies": { @@ -8867,6 +9500,15 @@ "ms": "2.1.2" } }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -8943,14 +9585,14 @@ } }, "@humanwhocodes/config-array": { - "version": "0.9.5", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", - "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", + "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" }, "dependencies": { "debug": { @@ -8962,6 +9604,15 @@ "ms": "2.1.2" } }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -8970,6 +9621,12 @@ } } }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, "@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", @@ -8977,33 +9634,30 @@ "dev": true }, "@jimp/bmp": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.16.2.tgz", - "integrity": "sha512-4g9vW45QfMoGhLVvaFj26h4e7cC+McHUQwyFQmNTLW4FfC1OonN9oUr2m/FEDGkTYKR7aqdXR5XUqqIkHWLaFw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/bmp/-/bmp-0.22.4.tgz", + "integrity": "sha512-ZDwQ/tLihpZuTCFGGa0zcyZIWHfhvHkrdbsoHUY0GG/JpH/y2xzlm2I48/TicCpoujN8oGKLHIJje0HmVX3xaA==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", + "@jimp/utils": "^0.22.4", "bmp-js": "^0.1.0" } }, "@jimp/core": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.16.2.tgz", - "integrity": "sha512-dp7HcyUMzjXphXYodI6PaXue+I9PXAavbb+AN+1XqFbotN22Z12DosNPEyy+UhLY/hZiQQqUkEaJHkvV31rs+w==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/core/-/core-0.22.4.tgz", + "integrity": "sha512-K7guEYpXV44SCLR35QdPyKqF+mFZaEUAqiSL8qQ/F4N4Ws9JkPzFI/qYTjOkDoKxSWkXlKnlsk1sfMzy0yqA5g==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", + "@jimp/utils": "^0.22.4", "any-base": "^1.1.0", "buffer": "^5.2.0", "exif-parser": "^0.1.12", - "file-type": "^9.0.0", - "load-bmfont": "^1.3.1", - "mkdirp": "^0.5.1", - "phin": "^2.9.1", + "file-type": "^16.5.4", + "isomorphic-fetch": "^3.0.0", + "mkdirp": "^2.1.3", "pixelmatch": "^4.0.2", - "tinycolor2": "^1.4.1" + "tinycolor2": "^1.6.0" }, "dependencies": { "buffer": { @@ -9015,335 +9669,312 @@ "base64-js": "^1.3.1", "ieee754": "^1.1.13" } + }, + "mkdirp": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.3.tgz", + "integrity": "sha512-sjAkg21peAG9HS+Dkx7hlG9Ztx7HLeKnvB3NQRcu/mltCVmvkF0pisbiTSfDVYTT86XEfZrTUosLdZLStquZUw==", + "optional": true } } }, "@jimp/custom": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.16.2.tgz", - "integrity": "sha512-GtNwOs4hcVS2GIbqRUf42rUuX07oLB92cj7cqxZb0ZGWwcwhnmSW0TFLAkNafXmqn9ug4VTpNvcJSUdiuECVKg==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/custom/-/custom-0.22.4.tgz", + "integrity": "sha512-k9m/RfxjPjklUsgZ2nszlyNkodUG/4xlxlif70UELhxW8bdqZqqlQGzwA9p+PUiSnlSJYZjL6q+P8cd7yj1ggA==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/core": "^0.16.2" + "@jimp/core": "^0.22.4" } }, "@jimp/gif": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.16.2.tgz", - "integrity": "sha512-TMdyT9Q0paIKNtT7c5KzQD29CNCsI/t8ka28jMrBjEK7j5RRTvBfuoOnHv7pDJRCjCIqeUoaUSJ7QcciKic6CA==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/gif/-/gif-0.22.4.tgz", + "integrity": "sha512-KmN7GoaQTzLAX4JXLBRkIiZAXthgQdKe+Y7BOw4n6CMe6LAS/XCQqrYCG3Av/GqIO7UAKems6D7kIGAUuhpNlQ==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", + "@jimp/utils": "^0.22.4", "gifwrap": "^0.9.2", "omggif": "^1.0.9" } }, "@jimp/jpeg": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.16.2.tgz", - "integrity": "sha512-BW5gZydgq6wdIwHd+3iUNgrTklvoQc/FUKSj9meM6A0FU21lUaansRX5BDdJqHkyXJLnnlDGwDt27J+hQuBAVw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/jpeg/-/jpeg-0.22.4.tgz", + "integrity": "sha512-mMJNhEtJpne65mxpIXEvT0VIzmsKiZWmaFT/c2eQ2tBLEtWAFpkvoP+F7jEaU+F3Ur4fXKFkJ/xOSXtRr/gGNw==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", - "jpeg-js": "^0.4.2" + "@jimp/utils": "^0.22.4", + "jpeg-js": "^0.4.4" } }, "@jimp/plugin-blit": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.16.2.tgz", - "integrity": "sha512-Z31rRfV80gC/r+B/bOPSVVpJEWXUV248j7MdnMOFLu4vr8DMqXVo9jYqvwU/s4LSTMAMXqm4Jg6E/jQfadPKAg==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-0.22.4.tgz", + "integrity": "sha512-QQHe+rFarsxJQxWKlyHEMfLyXmUG9AiQky+8WfwjZVBYilIFyiBywOc3sThonOsru+7LOSUDmbN6mvbFk4R+gw==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-blur": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.16.2.tgz", - "integrity": "sha512-ShkJCAzRI+1fAKPuLLgEkixpSpVmKTYaKEFROUcgmrv9AansDXGNCupchqVMTdxf8zPyW8rR1ilvG3OJobufLQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-0.22.4.tgz", + "integrity": "sha512-p57Ac5LEQckIogiwf7qyOojGvLOD08eMaQd5ylOhet/fbdwAzD/8flWFhSIKsdAVzvnfGcszuLtrsV07jDutTw==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-circle": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.16.2.tgz", - "integrity": "sha512-6T4z/48F4Z5+YwAVCLOvXQcyGmo0E3WztxCz6XGQf66r4JJK78+zcCDYZFLMx0BGM0091FogNK4QniP8JaOkrA==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-0.22.4.tgz", + "integrity": "sha512-T+TpG+s+wM9kKHlpIEfCAfOM+QrYVqcMoWjkULddc0KtaDEhqgGYFhN+/SlzJfDbZKw0xUgIuAw89sXuzMIUjw==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-color": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.16.2.tgz", - "integrity": "sha512-6oBV0g0J17/7E+aTquvUsgSc85nUbUi+64tIK5eFIDzvjhlqhjGNJYlc46KJMCWIs61qRJayQoZdL/iT/iQuGQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-0.22.4.tgz", + "integrity": "sha512-TZqcqepoCcIlF7VodPPfS3WET+LL5Y/XnXOBk4tWnG5i+lhNrs7/U0HOJY6Iw9o4g267DddnlfKWmunvzBcvOQ==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", - "tinycolor2": "^1.4.1" + "@jimp/utils": "^0.22.4", + "tinycolor2": "^1.6.0" } }, "@jimp/plugin-contain": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.16.2.tgz", - "integrity": "sha512-pLcxO3hVN3LCEhMNvpZ9B7xILHVlS433Vv16zFFJxLRqZdYvPLsc+ZzJhjAiHHuEjVblQrktHE3LGeQwGJPo0w==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-0.22.4.tgz", + "integrity": "sha512-Hl+TO4v+EpRfEl3R6k/bEgOGOpm6JqNfEIyCFWLi6yqJQjMGzBQ0vt+VHe2u3WIFaFrDWsGxeuFZBDzgtjTwxw==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-cover": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.16.2.tgz", - "integrity": "sha512-gzWM7VvYeI8msyiwbUZxH+sGQEgO6Vd6adGxZ0CeKX00uQOe5lDzxb1Wjx7sHcJGz8a/5fmAuwz7rdDtpDUbkw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-0.22.4.tgz", + "integrity": "sha512-KMTQjN/B7r/RNzoLFwnhqhLrgT0kMqTkBMEZQSopj5vPLPNjIX0ElEYC8AIVFKeZAV+1mYkyss+IDdxq4fyRng==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-crop": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.16.2.tgz", - "integrity": "sha512-qCd3hfMEE+Z2EuuyXewgXRTtKJGIerWzc1zLEJztsUkPz5i73IGgkOL+mrNutZwGaXZbm+8SwUaGb46sxAO6Tw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-0.22.4.tgz", + "integrity": "sha512-8krDt7xzBa1fbtlYCzEMZIgNjTkhgywho0FJpgIMkIUMjaZITS1Ea/Veb3UrWt8EsgQS6hxjGVE/Q1FvP5iPLA==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-displace": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.16.2.tgz", - "integrity": "sha512-6nXdvNNjCdD95v2o3/jPeur903dz08lG4Y8gmr5oL2yVv9LSSbMonoXYrR/ASesdyXqGdXJLU4NL+yZs4zUqbQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-0.22.4.tgz", + "integrity": "sha512-3gBfwYVFrOjp8SUpb7H0UMgqvsG/sxY1PVBIfRW9MUCosiH1eE/Mo5cbxhQ6/w5f3sh23lBmG8W0WuSrnXLorg==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-dither": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.16.2.tgz", - "integrity": "sha512-DERpIzy21ZanMkVsD0Tdy8HQLbD1E41OuvIzaMRoW4183PA6AgGNlrQoFTyXmzjy6FTy1SxaQgTEdouInAWZ9Q==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-0.22.4.tgz", + "integrity": "sha512-oOhdZBDJpSGIoTUhPOIvLIVUwILRWgrWdA4Vbzcyz2RHvaPHS8gdBH0EdIPbJ5agNyFnY8sJWFM7YKx/rLNKsw==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-fisheye": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.16.2.tgz", - "integrity": "sha512-Df7PsGIwiIpQu3EygYCnaJyTfOwvwtYV3cmYJS7yFLtdiFUuod+hlSo5GkwEPLAy+QBxhUbDuUqnsWo4NQtbiQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-0.22.4.tgz", + "integrity": "sha512-2myNZyDrwUOV8MKd4NeULnEOojYF7XRbnRHiUPsNptpmK6g/gI4xt+5k7BallAYZD8ZLfZVjstUogsObprHd/Q==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-flip": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.16.2.tgz", - "integrity": "sha512-+2uC8ioVQUr06mnjSWraskz2L33nJHze35LkQ8ZNsIpoZLkgvfiWatqAs5bj+1jGI/9kxoCFAaT1Is0f+a4/rw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-0.22.4.tgz", + "integrity": "sha512-9FZ0k5N5leLDefeDjizXXTl17dzo23PYtCD/T4xeSVr96d1pQDwbeIk7pEhhHr1rl98tJe0U/OV2dFXFYauKPw==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-gaussian": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.16.2.tgz", - "integrity": "sha512-2mnuDSg4ZEH8zcJig7DZZf4st/cYmQ5UYJKP76iGhZ+6JDACk6uejwAgT5xHecNhkVAaXMdCybA2eknH/9OE1w==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-gaussian/-/plugin-gaussian-0.22.4.tgz", + "integrity": "sha512-irOSwLdZ9kTq5Wd5dpkMgIMJVwemYcqgnzd04+P6TJGYmem2HR6JUCDpjbET3Fpbo/snFLm4mZ+2A+SmeCGjKA==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-invert": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.16.2.tgz", - "integrity": "sha512-xFvHbVepTY/nus+6yXiYN1iq+UBRkT0MdnObbiQPstUrAsz0Imn6MWISsnAyMvcNxHGrxaxjuU777JT/esM0gg==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-invert/-/plugin-invert-0.22.4.tgz", + "integrity": "sha512-/WtZeLrF+H+mzbjqudeGvvSxudlHy1kyiP1gVWDxhYNQOuZJI57Vn20kSTYvHBNjvy31LV4/uestyX8j8tE2Qg==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-mask": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.16.2.tgz", - "integrity": "sha512-AbdO85xxhfgEDdxYKpUotEI9ixiCMaIpfYHD5a5O/VWeimz2kuwhcrzlHGiyq1kKAgRcl0WEneTCZAHVSyvPKA==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-0.22.4.tgz", + "integrity": "sha512-U0SrOwBNKkMYTNPTz5CXeJdZ4c5easFlq2B9Txy0kPsav2OraTv8cZjpMxrWUejo/AQGVUDbaGtXMm9pE13/6w==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-normalize": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.16.2.tgz", - "integrity": "sha512-+ItBWFwmB0Od7OfOtTYT1gm543PpHUgU8/DN55z83l1JqS0OomDJAe7BmCppo2405TN6YtVm/csXo7p4iWd/SQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-normalize/-/plugin-normalize-0.22.4.tgz", + "integrity": "sha512-XJiPBJGCHWmIzUdmL4mWP1Ev5LMp77oMmPXdgQGDty1cxfyo3CbkTjZSsnwF/XLlrQ1yfLW+8JB+ihGKcVEOxA==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-print": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.16.2.tgz", - "integrity": "sha512-ifTGEeJ5UZTCiqC70HMeU3iXk/vsOmhWiwVGOXSFXhFeE8ZpDWvlmBsrMYnRrJGuaaogHOIrrQPI+kCdDBSBIQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-0.22.4.tgz", + "integrity": "sha512-mayiPhg6c7KYjvq3fYOW9ohhXD1eWdEiseV9dAWqTOEbDbohT8S6eTGhVIiVa2sVySLcpNEKZSk07c5EhJAMcw==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", - "load-bmfont": "^1.4.0" + "@jimp/utils": "^0.22.4", + "load-bmfont": "^1.4.1" } }, "@jimp/plugin-resize": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.16.2.tgz", - "integrity": "sha512-gE4N9l6xuwzacFZ2EPCGZCJ/xR+aX2V7GdMndIl/6kYIw5/eib1SFuF9AZLvIPSFuE1FnGo8+vT0pr++SSbhYg==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-0.22.4.tgz", + "integrity": "sha512-2wMdpPNGf6Zo2lfJg1QHHQ+ds5baQH75IcFpdjw717dcEISpn1jPG//iClXOGh16OJsRQlwHESaZTgEo/5Dw/g==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-rotate": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.16.2.tgz", - "integrity": "sha512-/CTEYkR1HrgmnE0VqPhhbBARbDAfFX590LWGIpxcYIYsUUGQCadl+8Qo4UX13FH0Nt8UHEtPA+O2x08uPYg9UA==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-0.22.4.tgz", + "integrity": "sha512-g08LBsPENbeA6NVoeq0iuDgAL89+N+aZrvYVKYkiJZIM7vUvueJyAIq4+bjDl4r54OR8XBFX0GsrKsqrULh1eA==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-scale": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.16.2.tgz", - "integrity": "sha512-3inuxfrlquyLaqFdiiiQNJUurR0WbvN5wAf1qcYX2LubG1AG8grayYD6H7XVoxfUGTZXh1kpmeirEYlqA2zxcw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-scale/-/plugin-scale-0.22.4.tgz", + "integrity": "sha512-cJiLQtTcNk6/+j05R23TFGXy+smDV0BdlmzJVDb+7Ye9qcmWpkdjVSioQQqZr0QScIYKhhRCY/lFTepBx67yzw==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-shadow": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.16.2.tgz", - "integrity": "sha512-Q0aIs2/L6fWMcEh9Ms73u34bT1hyUMw/oxaVoIzOLo6/E8YzCs2Bi63H0/qaPS0MQpEppI++kvosPbblABY79w==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-shadow/-/plugin-shadow-0.22.4.tgz", + "integrity": "sha512-a5hdpzGBzBo91DNiKaGvs8iJbs2bYQmDRm/BrCh4NET+h5l5AwXNu/Ak0bWRhN16YQ55XYNGHer2jOwBPrf2WQ==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugin-threshold": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.16.2.tgz", - "integrity": "sha512-gyOwmBgjtMPvcuyOhkP6dOGWbQdaTfhcBRN22mYeI/k/Wh/Zh1OI21F6eKLApsVRmg15MoFnkrCz64RROC34sw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-0.22.4.tgz", + "integrity": "sha512-jTT/+p2zb2NESzd7O0bVRowiQszoaHeBf2OgP7lFde10fHd+fn78m5brUmSmlGAdlMRwm8S8ZcxTj5ZjdQns5w==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2" + "@jimp/utils": "^0.22.4" } }, "@jimp/plugins": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.16.2.tgz", - "integrity": "sha512-zCvYtCgctmC0tkYEu+y+kSwSIZBsNznqJ3/3vkpzxdyjd6wCfNY5Qc/68MPrLc1lmdeGo4cOOTYHG7Vc6myzRw==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/plugins/-/plugins-0.22.4.tgz", + "integrity": "sha512-yAxcA4UR3Bs7j73I7wt4ty52vm5MzPmr+8DYk8jrS/ng2Z2iuXzbcTe4mf9eEqXYVah3rTIggo4dPjW75DRZtA==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/plugin-blit": "^0.16.2", - "@jimp/plugin-blur": "^0.16.2", - "@jimp/plugin-circle": "^0.16.2", - "@jimp/plugin-color": "^0.16.2", - "@jimp/plugin-contain": "^0.16.2", - "@jimp/plugin-cover": "^0.16.2", - "@jimp/plugin-crop": "^0.16.2", - "@jimp/plugin-displace": "^0.16.2", - "@jimp/plugin-dither": "^0.16.2", - "@jimp/plugin-fisheye": "^0.16.2", - "@jimp/plugin-flip": "^0.16.2", - "@jimp/plugin-gaussian": "^0.16.2", - "@jimp/plugin-invert": "^0.16.2", - "@jimp/plugin-mask": "^0.16.2", - "@jimp/plugin-normalize": "^0.16.2", - "@jimp/plugin-print": "^0.16.2", - "@jimp/plugin-resize": "^0.16.2", - "@jimp/plugin-rotate": "^0.16.2", - "@jimp/plugin-scale": "^0.16.2", - "@jimp/plugin-shadow": "^0.16.2", - "@jimp/plugin-threshold": "^0.16.2", + "@jimp/plugin-blit": "^0.22.4", + "@jimp/plugin-blur": "^0.22.4", + "@jimp/plugin-circle": "^0.22.4", + "@jimp/plugin-color": "^0.22.4", + "@jimp/plugin-contain": "^0.22.4", + "@jimp/plugin-cover": "^0.22.4", + "@jimp/plugin-crop": "^0.22.4", + "@jimp/plugin-displace": "^0.22.4", + "@jimp/plugin-dither": "^0.22.4", + "@jimp/plugin-fisheye": "^0.22.4", + "@jimp/plugin-flip": "^0.22.4", + "@jimp/plugin-gaussian": "^0.22.4", + "@jimp/plugin-invert": "^0.22.4", + "@jimp/plugin-mask": "^0.22.4", + "@jimp/plugin-normalize": "^0.22.4", + "@jimp/plugin-print": "^0.22.4", + "@jimp/plugin-resize": "^0.22.4", + "@jimp/plugin-rotate": "^0.22.4", + "@jimp/plugin-scale": "^0.22.4", + "@jimp/plugin-shadow": "^0.22.4", + "@jimp/plugin-threshold": "^0.22.4", "timm": "^1.6.1" } }, "@jimp/png": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.16.2.tgz", - "integrity": "sha512-sFOtOSz/tzDwXEChFQ/Nxe+0+vG3Tj0eUxnZVDUG/StXE9dI8Bqmwj3MIa0EgK5s+QG3YlnDOmlPUa4JqmeYeQ==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/png/-/png-0.22.4.tgz", + "integrity": "sha512-kDovx9dTyV/TSR40HQHdRyVgNNb7Cny4/0PPEa+xeR7snuDC3dV5hu9s/QJwY0RMGiAkiuKDpiaBuSZuz8dwRQ==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/utils": "^0.16.2", - "pngjs": "^3.3.3" + "@jimp/utils": "^0.22.4", + "pngjs": "^6.0.0" } }, "@jimp/tiff": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.16.2.tgz", - "integrity": "sha512-ADcdqmtZF+U2YoaaHTzFX8D6NFpmN4WZUT0BPMerEuY7Cq8QoLYU22z2h034FrVW+Rbi1b3y04sB9iDiQAlf2w==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/tiff/-/tiff-0.22.4.tgz", + "integrity": "sha512-RStUATRnb+unYzzuGxU+SPZALqh5NxYdcS6UGTBvhCMlijopGiY/iL01wstIOst0ypKIjwbcSVj7mAHn6B7Qbw==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "utif": "^2.0.1" + "utif2": "^4.0.1" } }, "@jimp/types": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.16.2.tgz", - "integrity": "sha512-0Ue5Sq0XnDF6TirisWv5E+8uOnRcd8vRLuwocJOhF76NIlcQrz+5r2k2XWKcr3d+11n28dHLXW5TKSqrUopxhA==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/types/-/types-0.22.4.tgz", + "integrity": "sha512-v3hm8LGc3we6P6ML0ticiLX7wtdvywrKthYxqVrJVIu3vRL0Z4q3ngFjwzqDmaIF8wC0neq98s/t7ODWfeIiRQ==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/bmp": "^0.16.2", - "@jimp/gif": "^0.16.2", - "@jimp/jpeg": "^0.16.2", - "@jimp/png": "^0.16.2", - "@jimp/tiff": "^0.16.2", + "@jimp/bmp": "^0.22.4", + "@jimp/gif": "^0.22.4", + "@jimp/jpeg": "^0.22.4", + "@jimp/png": "^0.22.4", + "@jimp/tiff": "^0.22.4", "timm": "^1.6.1" } }, "@jimp/utils": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.16.2.tgz", - "integrity": "sha512-XENrPvmigiXZQ8E2nxJqO6UVvWBLzbNwyYi3Y8Q1IECoYhYI3kgOQ0fmy4G269Vz1V0omh1bNmC42r4OfXg1Jg==", + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-0.22.4.tgz", + "integrity": "sha512-EPaBMNg4NvVXnMpSFJEsdCQqdSVU2ACreAL+Ipkq19C/FkDEj9Q10t6Mjx8zOe/AAjBQj1vTALS/DykcHOn4bQ==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", "regenerator-runtime": "^0.13.3" }, "dependencies": { "regenerator-runtime": { - "version": "0.13.10", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", - "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==", + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "optional": true } } @@ -9375,33 +10006,45 @@ } }, "@sitespeed.io/chromedriver": { - "version": "106.0.5249-61", - "resolved": "https://registry.npmjs.org/@sitespeed.io/chromedriver/-/chromedriver-106.0.5249-61.tgz", - "integrity": "sha512-Cv6Kp2hTR1GEwzZ+xy3EjVUu4lUCMcu9jEwDTONZu3fI1myjtZ96F7KgnDPTV4HRHj2VPK0jDWmSsEREcMBn1A==", + "version": "110.0.5481-77", + "resolved": "https://registry.npmjs.org/@sitespeed.io/chromedriver/-/chromedriver-110.0.5481-77.tgz", + "integrity": "sha512-1T/1KzSya6clqIcbylzrDEA79Sx90fTbBMX7VFuo9T1Qa51lZ28DT+Pm+i76LvIFXBQAieTvw3Ira8/gyfKewA==", "requires": { - "node-downloader-helper": "2.1.1", + "node-downloader-helper": "2.1.5", "node-stream-zip": "1.15.0" } }, "@sitespeed.io/edgedriver": { - "version": "106.0.1370-34", - "resolved": "https://registry.npmjs.org/@sitespeed.io/edgedriver/-/edgedriver-106.0.1370-34.tgz", - "integrity": "sha512-dOtxmGlGTXdx8sLM7hMmG+OQOM4iw6Me75Neu/UHvkEuUY3EtAvZq7mGrpxTTAV5eK9J0FhLcW/S3xmsHdYvVw==", + "version": "109.0.1518-70", + "resolved": "https://registry.npmjs.org/@sitespeed.io/edgedriver/-/edgedriver-109.0.1518-70.tgz", + "integrity": "sha512-ZLSZ54UrWRPODKooNMM3X36GTyQ8Rrwopr9adVsRvoTuBjWWf0+Od/f5OoraYBJJA2OsR4yV92sC/S1lSK+rbw==", "requires": { - "node-downloader-helper": "2.1.1", + "node-downloader-helper": "2.1.6", "node-stream-zip": "1.15.0" + }, + "dependencies": { + "node-downloader-helper": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.6.tgz", + "integrity": "sha512-VkOvAXIopI3xMuM/MC5UL7NqqnizQ/9QXZt28jR8FPZ6fHLQm4xe4+YXJ9FqsWwLho5BLXrF51nfOQ0QcohRkQ==" + } } }, "@sitespeed.io/geckodriver": { - "version": "0.31.0-c", - "resolved": "https://registry.npmjs.org/@sitespeed.io/geckodriver/-/geckodriver-0.31.0-c.tgz", - "integrity": "sha512-VLOM5N9TAZYoigx0M/7OyNhdh7xCNEG3NAuwJ6A+5G966nbYELfGbnoxDdCgI5Yo6zKjjU/F2yPLoyrbOmmgYQ==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@sitespeed.io/geckodriver/-/geckodriver-0.32.0.tgz", + "integrity": "sha512-5h/h+S31/gdlFDWh7GUkYW0fuKPTg1l6BKk0gAH4Nz1bJ7nSex2fV+F0/csoaLU7Z8sQFHi1B4QQiwfl1+6ugw==", "requires": { - "node-downloader-helper": "2.1.1", + "node-downloader-helper": "2.1.5", "node-stream-zip": "1.15.0", - "tar": "6.1.11" + "tar": "6.1.13" } }, + "@sitespeed.io/plugin": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@sitespeed.io/plugin/-/plugin-0.0.4.tgz", + "integrity": "sha512-by3DxWv5t6cH6pUpUFA3vNggdmxUCqHBN9rED1W3G+nQLw1OE/sBE0vGs92mQqHTILN6Y2wbTWxtYPRjEWDsJQ==" + }, "@sitespeed.io/throttle": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@sitespeed.io/throttle/-/throttle-5.0.0.tgz", @@ -9434,9 +10077,9 @@ } }, "@tgwf/co2": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@tgwf/co2/-/co2-0.11.3.tgz", - "integrity": "sha512-aFM52uDV0M/tx/BYu1w3K215pei5J2PHlsnaQKSr9/sJ5k2az1M2x8x/3Q70EJiEB8MtsHE2wWl8y17TacfKRQ==", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@tgwf/co2/-/co2-0.12.1.tgz", + "integrity": "sha512-sg0B0AsdYhhG/hLmFouEG3ePFpdQK0hXJdkI7M9URYZ1pQ+HYtUnzb4H+36+UnCxHqbrGwUJ5wdWtdXU0RwcFA==", "requires": { "debug": "^4.3.4" }, @@ -9456,6 +10099,12 @@ } } }, + "@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "optional": true + }, "@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -9504,6 +10153,12 @@ "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", "optional": true }, + "@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "dev": true + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -10018,36 +10673,36 @@ } }, "browsertime": { - "version": "17.0.0-beta.4", - "resolved": "https://registry.npmjs.org/browsertime/-/browsertime-17.0.0-beta.4.tgz", - "integrity": "sha512-erykocFSBCjl51qUAxzcMx1S8EGr66ub+iF8H2ESb+97qP9ZSY5bcLjVQHgAlL/ujb7K25XIdWHBd1f+Z+3vXA==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/browsertime/-/browsertime-17.1.0.tgz", + "integrity": "sha512-JRVr8dI/recSlyyZPB4VvBmb6p8E51DOJ0BBsowxO5Ms+T5j+dk7sY92lNqEsm6vPhP6t3HHUn+gdFte+nhfNw==", "requires": { "@cypress/xvfb": "1.2.4", "@devicefarmer/adbkit": "2.11.3", - "@sitespeed.io/chromedriver": "106.0.5249-61", - "@sitespeed.io/edgedriver": "106.0.1370-34", - "@sitespeed.io/geckodriver": "0.31.0-c", + "@sitespeed.io/chromedriver": "110.0.5481-77", + "@sitespeed.io/edgedriver": "109.0.1518-70", + "@sitespeed.io/geckodriver": "0.32.0", "@sitespeed.io/throttle": "5.0.0", "@sitespeed.io/tracium": "0.3.3", "btoa": "1.2.1", - "chrome-har": "0.13.0", - "chrome-remote-interface": "0.31.3", - "dayjs": "1.11.6", + "chrome-har": "0.13.1", + "chrome-remote-interface": "0.32.0", + "dayjs": "1.11.7", "execa": "6.1.0", "fast-stats": "0.0.6", "find-up": "6.3.0", "get-port": "6.1.2", "hasbin": "1.2.3", "intel": "1.2.0", - "jimp": "0.16.2", + "jimp": "0.22.1", "lodash.get": "4.4.2", "lodash.groupby": "4.6.0", "lodash.isempty": "4.4.0", "lodash.merge": "4.6.2", "lodash.pick": "4.4.0", "lodash.set": "4.3.2", - "selenium-webdriver": "4.5.0", - "yargs": "17.6.0" + "selenium-webdriver": "4.8.0", + "yargs": "17.6.2" }, "dependencies": { "ansi-regex": { @@ -10065,20 +10720,6 @@ "wrap-ansi": "^7.0.0" } }, - "dayjs": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.6.tgz", - "integrity": "sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ==" - }, - "find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "requires": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - } - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -10088,9 +10729,9 @@ } }, "yargs": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", - "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", "requires": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -10098,7 +10739,7 @@ "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" + "yargs-parser": "^21.1.1" } } } @@ -10136,9 +10777,9 @@ "optional": true }, "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true }, "call-bind": { @@ -10236,60 +10877,56 @@ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" }, "chrome-har": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/chrome-har/-/chrome-har-0.13.0.tgz", - "integrity": "sha512-wknoUfURyGp+sGvikTrOGpsL+bhzinqciItSGXwklIc9logrfrwZb6Ca/ZTho4twDKsy0f4dAVL79jHyvOG4Qw==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/chrome-har/-/chrome-har-0.13.1.tgz", + "integrity": "sha512-R9OXxVeCOqJYmqO8Oco+nouJ4frwVFQ6rGUgj5dJOfVNxBMmwCannp+DH2IAfqY/K8WJ5VYCQyq7SLTNZtcCXA==", "requires": { - "dayjs": "1.8.31", - "debug": "4.1.1", - "tough-cookie": "4.0.0", - "uuid": "8.0.0" + "dayjs": "1.11.7", + "debug": "4.3.4", + "tough-cookie": "4.1.2", + "uuid": "9.0.0" }, "dependencies": { - "dayjs": { - "version": "1.8.31", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.31.tgz", - "integrity": "sha512-mPh1mslned+5PuIuiUfbw4CikHk6AEAf2Baxih+wP5fssv+wmlVhvgZ7mq+BhLt7Sr/Hc8leWDiwe6YnrpNt3g==" - }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" }, "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", + "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", "requires": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" } }, "uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" } } }, "chrome-remote-interface": { - "version": "0.31.3", - "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.31.3.tgz", - "integrity": "sha512-NTwb1YNPHXLTus1RjqsLxJmdViKwKJg/lrFEcM6pbyQy04Ow2QKWHXyPpxzwE+dFsJghWuvSAdTy4W0trluz1g==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.32.0.tgz", + "integrity": "sha512-g8xK3lKvAgEs3Hj/masMfYOyIFbDkXsMxD7e55TRUvbL7pAb6X9uo+0mKQFjZqQ7DN3b8DIdBfkKw1nwkeWHhw==", "requires": { "commander": "2.11.x", "ws": "^7.2.0" @@ -10309,9 +10946,9 @@ "dev": true }, "ci-info": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", - "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", "dev": true }, "ci-parallel-vars": { @@ -10321,9 +10958,9 @@ "dev": true }, "clean-css": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.0.tgz", - "integrity": "sha512-YYuuxv4H/iNb1Z/5IbMRoxgrzjWGhOEFfd+groZ5dMCVkpENiMZmwspdrzBo9286JjM1gZJPAyL7ZIdzuvu2AQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", "dev": true, "requires": { "source-map": "~0.6.0" @@ -10349,6 +10986,15 @@ } } }, + "clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, "clean-stack": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", @@ -10373,12 +11019,12 @@ "dev": true }, "cli-color": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.2.tgz", - "integrity": "sha512-g4JYjrTW9MGtCziFNjkqp3IMpGhnJyeB0lOtRPjQkYhXzKYr6tYnXKyEVnMzITxhpbahsEW9KsxOYIDKwcsIBw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.3.tgz", + "integrity": "sha512-OkoZnxyC4ERN3zLzZaY9Emb7f/MhBOIpePv0Ycok0fJYT+Ouo00UBEIwsVsr0yoow++n5YWlSUgST9GKhNHiRQ==", "requires": { "d": "^1.0.1", - "es5-ext": "^0.10.59", + "es5-ext": "^0.10.61", "es6-iterator": "^2.0.3", "memoizee": "^0.4.15", "timers-ext": "^0.1.7" @@ -10448,9 +11094,9 @@ } }, "coach-core": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/coach-core/-/coach-core-7.1.2.tgz", - "integrity": "sha512-MuziK7sWmkQPHgkVy7vpegtpEJgt8LE9no2x5TwTQIhSMqSeUeREhzr6ayQ0XCjezRZV3+nB68TblEK7GKTbyQ==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/coach-core/-/coach-core-7.1.3.tgz", + "integrity": "sha512-mKJSg7nOU3H384WjHWzKm2GeERcEDnMTwRP1YIJt3q2kw9f68n6R8o9ETsgDRGIQANSqJtUEJ2na07L5rS71hg==", "requires": { "filter-files": "0.4.0", "json-stable-stringify": "1.0.1", @@ -10458,7 +11104,7 @@ "lodash.merge": "4.6.2", "lodash.sortby": "4.7.0", "pagexray": "4.4.2", - "third-party-web": "0.17.1", + "third-party-web": "0.20.2", "wappalyzer-core": "6.6.0" } }, @@ -10691,9 +11337,9 @@ } }, "dayjs": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.1.tgz", - "integrity": "sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA==" + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" }, "dbug": { "version": "0.4.2", @@ -10935,18 +11581,6 @@ "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } } }, "eastasianwidth": { @@ -11002,6 +11636,15 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=" }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, "es-abstract": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.5.tgz", @@ -11112,13 +11755,15 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", - "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", + "version": "8.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.31.0.tgz", + "integrity": "sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.2.2", - "@humanwhocodes/config-array": "^0.9.2", + "@eslint/eslintrc": "^1.4.1", + "@humanwhocodes/config-array": "^0.11.8", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -11128,30 +11773,32 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", + "espree": "^9.4.0", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^6.0.1", - "globals": "^13.6.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "text-table": "^0.2.0" }, "dependencies": { "ansi-regex": { @@ -11194,6 +11841,16 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -11214,12 +11871,54 @@ } } }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -11254,14 +11953,62 @@ "requires": {} }, "eslint-plugin-prettier": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz", - "integrity": "sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", "dev": true, "requires": { "prettier-linter-helpers": "^1.0.0" } }, + "eslint-plugin-unicorn": { + "version": "44.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-44.0.2.tgz", + "integrity": "sha512-GLIDX1wmeEqpGaKcnMcqRvMVsoabeF0Ton0EX4Th5u6Kmf7RM9WBl705AXFEsns56ESkEs0uyelLuUTvz9Tr0w==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.19.1", + "ci-info": "^3.4.0", + "clean-regexp": "^1.0.0", + "eslint-utils": "^3.0.0", + "esquery": "^1.4.0", + "indent-string": "^4.0.0", + "is-builtin-module": "^3.2.0", + "lodash": "^4.17.21", + "pluralize": "^8.0.0", + "read-pkg-up": "^7.0.1", + "regexp-tree": "^0.1.24", + "safe-regex": "^2.1.1", + "semver": "^7.3.7", + "strip-indent": "^3.0.0" + }, + "dependencies": { + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "is-builtin-module": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.0.tgz", + "integrity": "sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==", + "dev": true, + "requires": { + "builtin-modules": "^3.3.0" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "eslint-scope": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", @@ -11296,20 +12043,20 @@ "dev": true }, "espree": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", - "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", "dev": true, "requires": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" }, "dependencies": { "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", "dev": true } } @@ -11565,10 +12312,15 @@ } }, "file-type": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz", - "integrity": "sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw==", - "optional": true + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "optional": true, + "requires": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + } }, "file-uri-to-path": { "version": "1.0.0", @@ -11608,43 +12360,12 @@ "dev": true }, "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "dependencies": { - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "requires": { - "p-locate": "^5.0.0" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "requires": { - "p-limit": "^3.0.2" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - } + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" } }, "flat-cache": { @@ -11692,9 +12413,9 @@ } }, "fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.0.tgz", + "integrity": "sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==", "requires": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -11714,6 +12435,16 @@ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", "requires": { "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } } }, "fs.realpath": { @@ -11733,12 +12464,6 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, "functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -11869,10 +12594,18 @@ "process": "^0.11.10" } }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "requires": { + "ini": "^1.3.4" + } + }, "globals": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", - "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", + "version": "13.19.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", + "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -11935,6 +12668,12 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "gtoken": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", @@ -12153,6 +12892,14 @@ "resolve-from": "^4.0.0" } }, + "import-global": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/import-global/-/import-global-0.1.0.tgz", + "integrity": "sha512-8+hPJLML+m1ym9NSeZXTXFkY5+ml0fYFAzO5yhZiaFQvk9kO0NkE7vd7e7kCVjkTmAxsDPbrWwLQACMwGTDgIg==", + "requires": { + "global-dirs": "^0.1.0" + } + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -12183,6 +12930,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "intel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/intel/-/intel-1.2.0.tgz", @@ -12221,6 +12973,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -12247,15 +13005,6 @@ "has-tostringtag": "^1.0.0" } }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "dev": true, - "requires": { - "builtin-modules": "^1.0.0" - } - }, "is-callable": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", @@ -12482,28 +13231,37 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "optional": true, + "requires": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "jimp": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.16.2.tgz", - "integrity": "sha512-UpItBk81a92f8oEyoGYbO3YK4QcM0hoIyuGHmShoF9Ov63P5Qo7Q/X2xsAgnODmSuDJFOtrPtJd5GSWW4LKdOQ==", + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.22.1.tgz", + "integrity": "sha512-TzVcbFgnqEjE6nADwtXSAb5xdXimLLwA0q8MWPfhZyrObvUWBDFFH6Dm9Kr4CRgxd8/Mx+7+ugktY7NchNYUzg==", "optional": true, "requires": { - "@babel/runtime": "^7.7.2", - "@jimp/custom": "^0.16.2", - "@jimp/plugins": "^0.16.2", - "@jimp/types": "^0.16.2", + "@jimp/custom": "^0.22.1", + "@jimp/plugins": "^0.22.1", + "@jimp/types": "^0.22.1", "regenerator-runtime": "^0.13.3" }, "dependencies": { "regenerator-runtime": { - "version": "0.13.10", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", - "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==", + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "optional": true } } @@ -12519,6 +13277,12 @@ "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "optional": true }, + "js-sdsl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", + "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", + "dev": true + }, "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -12530,6 +13294,12 @@ "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -12659,6 +13429,12 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -12746,6 +13522,22 @@ "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + } } }, "junit-report-builder": { @@ -12908,6 +13700,12 @@ "integrity": "sha1-xDkrWH3qOFgMlnhXDm6OSfzlJiI=", "dev": true }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "linkify-it": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.0.3.tgz", @@ -13231,6 +14029,12 @@ "dom-walk": "^0.1.0" } }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -13245,12 +14049,9 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "requires": { - "yallist": "^4.0.0" - } + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.3.tgz", + "integrity": "sha512-OW2r4sQ0sI+z5ckEt5c1Tri4xTgZwYDxpE54eqWlQloQRoWtXjqt9udJ5Z4dSv7wK+nfFI7FRXyCpBSft+gpFw==" }, "minizlib": { "version": "2.1.2", @@ -13259,13 +14060,23 @@ "requires": { "minipass": "^3.0.0", "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } } }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "devOptional": true, + "dev": true, "requires": { "minimist": "^1.2.5" } @@ -13299,9 +14110,9 @@ "optional": true }, "node-downloader-helper": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.1.tgz", - "integrity": "sha512-ouk8MGmJj1gYymbJwi1L8Mr6PdyheJLwfsmyx0KtsvyJ+7Fpf0kBBzM8Gmx8Mt/JBfRWP1PQm6dAGV6x7eNedw==" + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.5.tgz", + "integrity": "sha512-sLedzfv8C4VMAvTdDQcjLFAl3gydNeBXh2bLcCzvZRmd4EK0rkoTxJ8tkxnriUSJO/n13skJzH7l6CzCdBwYGg==" }, "node-fetch": { "version": "2.6.7", @@ -13355,13 +14166,13 @@ } }, "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "requires": { "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", + "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } @@ -13615,6 +14426,18 @@ "integrity": "sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==", "optional": true }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, "parse-ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", @@ -13647,6 +14470,12 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "optional": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -13676,6 +14505,14 @@ "optional": true, "requires": { "pngjs": "^3.0.0" + }, + "dependencies": { + "pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "optional": true + } } }, "pkg-conf": { @@ -13686,18 +14523,6 @@ "requires": { "find-up": "^6.0.0", "load-json-file": "^7.0.0" - }, - "dependencies": { - "find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dev": true, - "requires": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - } - } } }, "plur": { @@ -13709,10 +14534,16 @@ "irregular-plurals": "^3.3.0" } }, + "pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true + }, "pngjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", - "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", "optional": true }, "prelude-ls": { @@ -14066,6 +14897,11 @@ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -14100,18 +14936,96 @@ "slash": "^1.0.0" } }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "optional": true, + "requires": { + "readable-stream": "^3.6.0" } }, "readdir-scoped-modules": { @@ -14149,6 +15063,12 @@ "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", "dev": true }, + "regexp-tree": { + "version": "0.1.24", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", + "integrity": "sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==", + "dev": true + }, "regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", @@ -14218,6 +15138,11 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "requizzle": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", @@ -14329,6 +15254,15 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "requires": { + "regexp-tree": "~0.1.1" + } + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -14351,19 +15285,19 @@ "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, "selenium-webdriver": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.5.0.tgz", - "integrity": "sha512-9mSFii+lRwcnT2KUAB1kqvx6+mMiiQHH60Y0VUtr3kxxi3oZ3CV3B8e2nuJ7T4SPb+Q6VA0swswe7rYpez07Bg==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.8.0.tgz", + "integrity": "sha512-s/HL8WNwy1ggHR244+tAhjhyKMJnZLt1HKJ6Gn7nQgVjB/ybDF+46Uui0qI2J7AjPNJzlUmTncdC/jg/kKkn0A==", "requires": { "jszip": "^3.10.0", "tmp": "^0.2.1", - "ws": ">=8.7.0" + "ws": ">=8.11.0" }, "dependencies": { "ws": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", - "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz", + "integrity": "sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig==", "requires": {} } } @@ -14736,12 +15670,31 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, + "strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "optional": true, + "requires": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + } + }, "stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", @@ -14842,13 +15795,13 @@ } }, "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", + "minipass": "^4.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" @@ -14885,9 +15838,9 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" }, "third-party-web": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.17.1.tgz", - "integrity": "sha512-X9Mha8cVeBwakunlZXkXL6xRzw8VCcDGWqT59EzeTYAJIi8ien3CuufnEGEx4ZUFahumNQdoOwf4H2T9Ca6lBg==" + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.20.2.tgz", + "integrity": "sha512-KFaFBDto+gH2DZW6ooFCGYrR8CGV6b/Ibsc2RTUkKhTPbxOWZuKs0NTftdAMoz0Aivf4bAHgW+kAGKciSQpqFg==" }, "through": { "version": "2.3.8", @@ -14916,9 +15869,9 @@ "optional": true }, "tinycolor2": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", - "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", "optional": true }, "tmp": { @@ -14949,6 +15902,24 @@ "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", "integrity": "sha1-zCAOqyYT9BZtJ/+a/HylbUnfbrQ=" }, + "token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "optional": true, + "requires": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "dependencies": { + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "optional": true + } + } + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -15040,9 +16011,9 @@ } }, "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==" }, "uri-js": { "version": "4.4.1", @@ -15073,18 +16044,27 @@ "querystring": "0.2.0" } }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "utcstring": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/utcstring/-/utcstring-0.1.0.tgz", "integrity": "sha1-Qw/VEKt/yVtdWRDJAteYgMIIQ2s=" }, - "utif": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/utif/-/utif-2.0.1.tgz", - "integrity": "sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==", + "utif2": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/utif2/-/utif2-4.0.1.tgz", + "integrity": "sha512-KMaD76dbzK1VjbwsckHJiqDjhP3pbpwyV+FdqkY6XFQenc2o/HS6pjPSYdu4+NQMHf2NLTW+nVP/eFP1CvOYQQ==", "optional": true, "requires": { - "pako": "^1.0.5" + "pako": "^1.0.11" } }, "util-deprecate": { @@ -15103,12 +16083,6 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, "validate-npm-package-license": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", @@ -15151,6 +16125,12 @@ "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", "dev": true }, + "whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==", + "optional": true + }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -15408,9 +16388,9 @@ } }, "yargs-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==" + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" }, "yocto-queue": { "version": "0.1.0", diff --git a/package.json b/package.json index 04f8cdc59..95a6a4270 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "sitespeed.io", + "type": "module", "bin": { "sitespeed.io": "./bin/sitespeed.js", "sitespeed.io-wpr": "./bin/browsertimeWebPageReplay.js" @@ -62,9 +63,10 @@ "ava": "4.3.3", "changelog-parser": "2.8.1", "clean-css-cli": "5.6.0", - "eslint": "8.14.0", + "eslint": "8.31.0", "eslint-config-prettier": "8.5.0", - "eslint-plugin-prettier": "4.0.0", + "eslint-plugin-prettier": "4.2.1", + "eslint-plugin-unicorn": "44.0.2", "feed": "4.2.2", "jsdoc": "^3.6.7", "license-checker": "^25.0.0", @@ -74,22 +76,23 @@ "pug-lint": "^2.6.0", "pug-lint-config-clock": "^2.0.0" }, - "main": "./lib/sitespeed.js", + "exports": "./lib/sitespeed.js", "dependencies": { "@google-cloud/storage": "5.19.3", - "@tgwf/co2": "0.11.3", + "@tgwf/co2": "0.12.1", "aws-sdk": "2.1121.0", "axe-core": "4.4.3", - "browsertime": "17.0.0-beta.4", - "coach-core": "7.1.2", - "cli-color": "2.0.2", + "browsertime": "17.1.0", + "coach-core": "7.1.3", + "cli-color": "2.0.3", "concurrent-queue": "7.0.2", - "dayjs": "1.11.1", + "dayjs": "1.11.7", "fast-crc32c": "2.0.0", "fast-stats": "0.0.6", - "find-up": "5.0.0", - "fs-extra": "10.1.0", + "find-up": "6.3.0", + "fs-extra": "11.1.0", "getos": "3.2.1", + "import-global":"0.1.0", "influx": "5.9.3", "intel": "1.2.0", "jstransformer-markdown-it": "2.1.0", @@ -114,6 +117,7 @@ "pug": "3.0.2", "recursive-readdir": "2.2.2", "simplecrawler": "1.1.9", + "@sitespeed.io/plugin": "0.0.4", "tape": "5.5.3", "text-table": "0.2.0", "uuid": "8.3.2", diff --git a/release/feed.js b/release/feed.js index 81c9ad630..212435dad 100644 --- a/release/feed.js +++ b/release/feed.js @@ -1,8 +1,8 @@ -const Feed = require('feed').Feed; -const fs = require('fs'); -const path = require('path'); -const parseChangelog = require('changelog-parser'); -const { marked } = require('marked'); +import { Feed } from 'feed'; +import { readdirSync, statSync, readFileSync, writeFileSync } from 'node:fs'; +import { parse, join } from 'node:path'; +import parseChangelog from 'changelog-parser'; +import { marked } from 'marked'; const allFeeds = []; @@ -20,14 +20,14 @@ const images = { }; const getSortedFiles = dir => { - const files = fs.readdirSync(dir); + const files = readdirSync(dir); return files .map(fileName => ({ fileName: fileName, - name: path.parse(fileName).name, - time: fs.statSync(`${dir}/${fileName}`).mtime.getTime(), - version: fs.readFileSync(`${dir}/${fileName}`, 'utf8').trim() + name: parse(fileName).name, + time: statSync(`${dir}/${fileName}`).mtime.getTime(), + version: readFileSync(`${dir}/${fileName}`, 'utf8').trim() })) .sort((a, b) => b.time - a.time); }; @@ -142,11 +142,11 @@ const getContent = async tool => { removeMarkdown: false }); - for (let i = 0; i < 10; i++) { + for (let index = 0; index < 10; index++) { // It's not unreleased - if (result.versions[i] && result.versions[i].date !== null) { - content.push(result.versions[i]); - allFeeds.push({ tool, item: result.versions[i] }); + if (result.versions[index] && result.versions[index].date !== null) { + content.push(result.versions[index]); + allFeeds.push({ tool, item: result.versions[index] }); } } return content; @@ -163,14 +163,11 @@ async function generateFeed() { addItemToFeed(feed, item, tool.name); } - const docPath = './docs/'; + const documentPath = './docs/'; - fs.writeFileSync( - path.join(docPath, 'feed', `${tool.name}.rss`), - feed.rss2() - ); - fs.writeFileSync( - path.join(docPath, 'feed', `${tool.name}.atom`), + writeFileSync(join(documentPath, 'feed', `${tool.name}.rss`), feed.rss2()); + writeFileSync( + join(documentPath, 'feed', `${tool.name}.atom`), feed.atom1() ); } @@ -184,10 +181,10 @@ async function generateFeed() { addItemToFeed(allFeed, item.item, item.tool); } - const docPath = './docs/'; + const documentPath = './docs/'; - fs.writeFileSync(path.join(docPath, 'feed', `rss.xml`), allFeed.rss2()); - fs.writeFileSync(path.join(docPath, 'feed', `atom.xml`), allFeed.atom1()); + writeFileSync(join(documentPath, 'feed', `rss.xml`), allFeed.rss2()); + writeFileSync(join(documentPath, 'feed', `atom.xml`), allFeed.atom1()); } -generateFeed(); +await generateFeed(); diff --git a/release/friendlyNames.js b/release/friendlyNames.js index ef793dbe4..e2d998b08 100644 --- a/release/friendlyNames.js +++ b/release/friendlyNames.js @@ -1,6 +1,4 @@ -'use strict'; - -const friendly = require('../lib/support/friendlynames'); +import friendly from '../lib/support/friendlynames'; for (let key of Object.keys(friendly)) { for (let tool of Object.keys(friendly[key])) { diff --git a/release/friendlyNamesBudget.js b/release/friendlyNamesBudget.js index f4e6dc5f1..350e044aa 100644 --- a/release/friendlyNamesBudget.js +++ b/release/friendlyNamesBudget.js @@ -1,14 +1,12 @@ -'use strict'; - -const friendly = require('../lib/support/friendlynames'); +import friendly from '../lib/support/friendlynames'; console.log('{'); console.log(' "budget": {'); for (let key of Object.keys(friendly)) { for (let tool of Object.keys(friendly[key])) { - console.log(' "' + tool + '": {'); + console.log(' "' + tool + '": {'); for (let metric of Object.keys(friendly[key][tool])) { - console.log(' "' + metric + '": limit,'); + console.log(' "' + metric + '": limit,'); } console.log(' },'); } diff --git a/test/cliTests.js b/test/cliTests.js index cf393aeb8..ecbb63448 100644 --- a/test/cliTests.js +++ b/test/cliTests.js @@ -1,12 +1,15 @@ -'use strict'; +import test from 'ava'; +import { join, resolve } from 'node:path'; +import { promisify } from 'node:util'; +import { execFile as _execFile } from 'node:child_process'; +const execFile = promisify(_execFile); -const test = require('ava'); -const path = require('path'); -const util = require('util'); -const execFile = util.promisify(require('child_process').execFile); +import { fileURLToPath } from 'node:url'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); function runSitespeed(options = []) { - const cli = path.join(path.resolve(__dirname), '../bin/sitespeed.js'); + const cli = join(resolve(__dirname), '../bin/sitespeed.js'); return execFile('node', [cli].concat(options)); } @@ -15,9 +18,9 @@ test(`Test cli without any arguments`, async t => { let exitCode = 0; try { await runSitespeed(); - } catch (err) { - stderr = err.stderr; - exitCode = err.code; + } catch (error) { + stderr = error.stderr; + exitCode = error.code; } t.not(stderr, undefined, 'Should output in standard error'); t.not(exitCode, 0, 'Should exit with error code'); diff --git a/test/cliUtilTests.js b/test/cliUtilTests.js index c1a6f0432..67f7d3393 100644 --- a/test/cliUtilTests.js +++ b/test/cliUtilTests.js @@ -1,27 +1,41 @@ -'use strict'; +import { + getURLs, + getAliases, + pluginDefaults, + registerPluginOptions +} from '../lib/cli/util.js'; +import intel from 'intel'; +import { SitespeedioPlugin } from '@sitespeed.io/plugin'; +import { messageMaker } from '../lib/support/messageMaker.js'; -const cliUtil = require('../lib/cli/util'); -const test = require('ava'); +import test from 'ava'; + +const mockYargs = () => ({ + calls: [], + option() { + this.calls.push([...arguments]); + } +}); test(`getURLs should extract urls`, t => { - let urls = cliUtil.getURLs(['test/fixtures/sitespeed-urls.txt']); + let urls = getURLs(['test/fixtures/sitespeed-urls.txt']); t.is(urls[0], 'https://www.sitespeed.io'); t.is(urls[3], 'https://www.sitespeed.io/faq'); - urls = cliUtil.getURLs(['test/fixtures/sitespeed-urls-aliases.txt']); + urls = getURLs(['test/fixtures/sitespeed-urls-aliases.txt']); t.is(urls[0], 'https://www.sitespeed.io'); t.is(urls[3], 'https://www.sitespeed.io/faq'); }); test(`getAliases should extract aliases`, t => { - let aliases = cliUtil.getAliases(['test/fixtures/sitespeed-urls.txt']); + let aliases = getAliases(['test/fixtures/sitespeed-urls.txt']); t.is(aliases['https://www.sitespeed.io'], undefined); t.is( aliases['https://www.sitespeed.io/documentation/sitespeed.io/webpagetest/'], undefined ); - aliases = cliUtil.getAliases(['test/fixtures/sitespeed-urls-aliases.txt']); + aliases = getAliases(['test/fixtures/sitespeed-urls-aliases.txt']); t.is(aliases['https://www.sitespeed.io'].urlAlias, 'Home_Page'); t.is( @@ -31,10 +45,10 @@ test(`getAliases should extract aliases`, t => { }); test(`pluginDefaults should yield an empty object for invalid values`, t => { - t.deepEqual(cliUtil.pluginDefaults(), {}); - t.deepEqual(cliUtil.pluginDefaults(null), {}); - t.deepEqual(cliUtil.pluginDefaults(1), {}); - t.deepEqual(cliUtil.pluginDefaults(true), {}); + t.deepEqual(pluginDefaults(), {}); + t.deepEqual(pluginDefaults(), {}); + t.deepEqual(pluginDefaults(1), {}); + t.deepEqual(pluginDefaults(true), {}); }); test(`pluginDefaults should yield a map of defaults based on config names and its defaults`, t => { @@ -44,7 +58,7 @@ test(`pluginDefaults should yield a map of defaults based on config names and it } }; - t.is(cliUtil.pluginDefaults(cliOptions).propName, 'value'); + t.is(pluginDefaults(cliOptions).propName, 'value'); }); test(`pluginDefaults should not include options without an explicit default set`, t => { @@ -58,68 +72,49 @@ test(`pluginDefaults should not include options without an explicit default set` } }; - t.is(cliUtil.pluginDefaults(cliOptions).otherProp, undefined); + t.is(pluginDefaults(cliOptions).otherProp, undefined); }); test(`registerPluginOptions should not setup options with invalid values`, t => { - const mockYargs = () => ({ - calls: [], - option() { - this.calls.push(Array.from(arguments)); + class TestPlugin extends SitespeedioPlugin { + constructor(options, context, queue) { + super({ name: 'test', options, context, queue }); } - }); + } + const plugin = new TestPlugin({}, { messageMaker, intel }); - const plugin = { - name() { - return 'test'; - } - }; - - const codeUnderTest = () => - cliUtil.registerPluginOptions(mockYargs(), plugin); + const codeUnderTest = () => registerPluginOptions(mockYargs(), plugin); t.throws(codeUnderTest); - plugin.cliOptions = null; + plugin.cliOptions = undefined; t.throws(codeUnderTest); plugin.cliOptions = true; t.throws(codeUnderTest); }); -test(`registerPluginOptions must have an explicit name defined in the plugin`, t => { - const mockYargs = () => ({ - calls: [], - option() { - this.calls.push(Array.from(arguments)); +function codeUnderTest() { + registerPluginOptions(mockYargs(), { + processMessage() { + // Apparently a safe plugin definition + return; } }); +} - function codeUnderTest() { - cliUtil.registerPluginOptions(mockYargs(), { - processMessage() { - // Apparently a safe plugin definition - return undefined; - } - }); - } - +test(`registerPluginOptions must have an explicit name defined in the plugin`, t => { t.throws(codeUnderTest); }); +/* test(`registerPluginOptions should call yargs.options() when cliOptions is defined as method`, t => { - const mockYargs = () => ({ - calls: [], - option() { - this.calls.push(Array.from(arguments)); - } - }); - const fakeYargs = mockYargs(); - const plugin = { - name() { - return 'test'; - }, + + class TestPlugin extends Plugin { + constructor(options, context, queue) { + super({ name: 'test', options, context, queue }); + } cliOptions() { return { prop1: { @@ -131,13 +126,14 @@ test(`registerPluginOptions should call yargs.options() when cliOptions is defin } }; } - }; - cliUtil.registerPluginOptions(fakeYargs, plugin); - const expectedProp1 = ['test.prop1', { default: 80 }]; + } + const plugin = new TestPlugin(); + registerPluginOptions(fakeYargs, plugin); + const expectedProperty1 = ['test.prop1', { default: 80 }]; t.deepEqual( fakeYargs.calls.find(call => call[0] === 'test.prop1'), - expectedProp1 + expectedProperty1 ); const expectedNested = [ @@ -151,15 +147,10 @@ test(`registerPluginOptions should call yargs.options() when cliOptions is defin }); test(`registerPluginOptions should call yargs.options() when cliOptions is defined as computed property`, t => { - const mockYargs = () => ({ - calls: [], - option() { - this.calls.push(Array.from(arguments)); - } - }); const fakeYargs = mockYargs(); + const plugin = { - name() { + getName() { return 'test'; }, get cliOptions() { @@ -175,13 +166,13 @@ test(`registerPluginOptions should call yargs.options() when cliOptions is defin } }; - cliUtil.registerPluginOptions(fakeYargs, plugin); + registerPluginOptions(fakeYargs, plugin); - const expectedProp1 = ['test.prop1', { default: 80 }]; + const expectedProperty1 = ['test.prop1', { default: 80 }]; t.deepEqual( fakeYargs.calls.find(call => call[0] === 'test.prop1'), - expectedProp1 + expectedProperty1 ); const expectedNested = [ @@ -195,15 +186,9 @@ test(`registerPluginOptions should call yargs.options() when cliOptions is defin }); test(`registerPluginOptions should call yargs.options() when cliOptions is defined as regular property`, t => { - const mockYargs = () => ({ - calls: [], - option() { - this.calls.push(Array.from(arguments)); - } - }); const fakeYargs = mockYargs(); const plugin = { - name() { + getName() { return 'test'; }, cliOptions: { @@ -217,13 +202,13 @@ test(`registerPluginOptions should call yargs.options() when cliOptions is defin } }; - cliUtil.registerPluginOptions(fakeYargs, plugin); + registerPluginOptions(fakeYargs, plugin); - const expectedProp1 = ['test.prop1', { default: 80 }]; + const expectedProperty1 = ['test.prop1', { default: 80 }]; t.deepEqual( fakeYargs.calls.find(call => call[0] === 'test.prop1'), - expectedProp1 + expectedProperty1 ); const expectedNested = [ @@ -235,3 +220,4 @@ test(`registerPluginOptions should call yargs.options() when cliOptions is defin expectedNested ); }); +*/ diff --git a/test/coachTests.js b/test/coachTests.js index 4c7fe1ec2..5d7f6cfe1 100644 --- a/test/coachTests.js +++ b/test/coachTests.js @@ -1,15 +1,17 @@ -'use strict'; +import test from 'ava'; +import { CoachAggregator } from '../lib/plugins/coach/aggregator.js'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; -const test = require('ava'); -const aggregator = require('../lib/plugins/coach/aggregator'); -const fs = require('fs'); -const path = require('path'); +import { fileURLToPath } from 'node:url'; +const __dirname = fileURLToPath(new URL('.', import.meta.url)); -const coachRunPath = path.resolve(__dirname, 'fixtures', 'coach.run-0.json'); -const coachRun = JSON.parse(fs.readFileSync(coachRunPath, 'utf8')); +const coachRunPath = resolve(__dirname, 'fixtures', 'coach.run-0.json'); +const coachRun = JSON.parse(readFileSync(coachRunPath, 'utf8')); test(`Should summarize Coach data`, t => { - aggregator.addToAggregate(coachRun, 'www.sitespeed.io'); - const data = aggregator.summarize(); + const coachAggregator = new CoachAggregator(); + coachAggregator.addToAggregate(coachRun, 'www.sitespeed.io'); + const data = coachAggregator.summarize(); t.not(data, undefined); }); diff --git a/test/domainTests.js b/test/domainTests.js index 88e8afa5c..4b49e3673 100644 --- a/test/domainTests.js +++ b/test/domainTests.js @@ -1,18 +1,19 @@ -'use strict'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; -const test = require('ava'); -const aggregator = require('../lib/plugins/domains/aggregator'); -const fs = require('fs'); -const path = require('path'); +import test from 'ava'; + +import { DomainsAggregator } from '../lib/plugins/domains/aggregator.js'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); const har = JSON.parse( - fs.readFileSync( - path.resolve(__dirname, 'fixtures', 'www-theverge-com.har'), - 'utf8' - ) + readFileSync(resolve(__dirname, 'fixtures', 'www-theverge-com.har'), 'utf8') ); test(`Should summarize data per domain`, t => { + const aggregator = new DomainsAggregator(); aggregator.addToAggregate(har, 'http://www.vox.com'); const summary = aggregator.summarize(); const voxDomain = summary.groups.total['cdn1.vox-cdn.com']; diff --git a/test/grafanaTests.js b/test/grafanaTests.js deleted file mode 100644 index e0cbbbba3..000000000 --- a/test/grafanaTests.js +++ /dev/null @@ -1,13 +0,0 @@ -const test = require('ava'); - -function plugin() { - return require('../lib/plugins/grafana'); -} - -test(`Grafana plugin should have a .config property with default host port value`, t => { - t.is(plugin().config.port, 80); -}); - -test(`Grafana plugin should include missing annotationScreenshot default value`, t => { - t.is(plugin().config.annotationScreenshot, false); -}); diff --git a/test/graphiteTests.js b/test/graphiteTests.js index 1e8aff5da..8b4f329f8 100644 --- a/test/graphiteTests.js +++ b/test/graphiteTests.js @@ -1,8 +1,9 @@ -'use strict'; +import test from 'ava'; +import dayjs from 'dayjs'; +import intel from 'intel'; +import { default as GraphitePlugin } from '../lib/plugins/graphite/index.js'; -const DataGenerator = require('../lib/plugins/graphite/data-generator'); -const test = require('ava'); -const dayjs = require('dayjs'); +import { GraphiteDataGenerator as DataGenerator } from '../lib/plugins/graphite/data-generator.js'; test(`Test dataGenerator`, t => { const message = { @@ -73,29 +74,26 @@ test(`Test generate data in statsD format`, t => { graphite: { statsd: true } }); const data = generator.dataFromMessage(message, dayjs()); - const statsDFormat = new RegExp( - /ns.summary.sub_domain_com.chrome.cable.domains.www.sitespeed.io.dns.(median|mean|min|p10|p90|p99|max):[\d]{1,}\|ms$/ + /ns.summary.sub_domain_com.chrome.cable.domains.www.sitespeed.io.dns.(median|mean|min|p10|p90|p99|max):\d+\|ms$/ ); for (let line of data) { t.true(statsDFormat.test(line)); } }); -test(`Use graphite interface by default`, t => { - const plugin = require('../lib/plugins/graphite'); +test(`Use graphite interface by default`, async t => { const options = { graphite: { host: '127.0.0.1' } }; - const messageMaker = require('../lib/support/messageMaker'); - const filterRegistry = require('../lib/support/filterRegistry'); - const intel = require('intel'); - const statsHelpers = require('../lib/support/statsHelpers'); - const context = { messageMaker, filterRegistry, intel, statsHelpers }; + const { messageMaker } = await import('../lib/support/messageMaker.js'); + const filterRegistry = await import('../lib/support/filterRegistry.js'); + const statsHelpers = await import('../lib/support/statsHelpers.js'); + const context = { messageMaker, filterRegistry, intel, statsHelpers }; + const plugin = new GraphitePlugin(options, context); plugin.open(context, options); - console.log(plugin.sender.facility); t.is(plugin.sender.facility, 'Graphite'); }); diff --git a/test/influxdbTests.js b/test/influxdbTests.js index 59eeadd04..d44951f32 100644 --- a/test/influxdbTests.js +++ b/test/influxdbTests.js @@ -1,8 +1,7 @@ -'use strict'; +import dayjs from 'dayjs'; +import test from 'ava'; -const DataGenerator = require('../lib/plugins/influxdb/data-generator'); -const dayjs = require('dayjs'); -const test = require('ava'); +import { InfluxDBDataGenerator as DataGenerator } from '../lib/plugins/influxdb/data-generator.js'; test(`Test influxdb dataGenerator`, t => { const message = { diff --git a/test/pathToFolderTests.js b/test/pathToFolderTests.js index 6634e729e..5a7de58e0 100644 --- a/test/pathToFolderTests.js +++ b/test/pathToFolderTests.js @@ -1,20 +1,19 @@ -'use strict'; +import test from 'ava'; -const pathFromRootToPageDir = require('../lib/core/resultsStorage/pathToFolder'); -const test = require('ava'); +import { pathToFolder } from '../lib/core/resultsStorage/pathToFolder.js'; test(`Test pathFromRootToPageDir should create path from site root`, t => { - const path = pathFromRootToPageDir('http://www.foo.bar', {}); + const path = pathToFolder('http://www.foo.bar', {}); t.is(path, 'pages/www_foo_bar/', 'should create path from site root'); }); test(`Test pathFromRootToPageDir should create path from url`, t => { - const path = pathFromRootToPageDir('http://www.foo.bar/x/y/z.html', {}); + const path = pathToFolder('http://www.foo.bar/x/y/z.html', {}); t.is(path, 'pages/www_foo_bar/x/y/z.html/', 'should create path from url'); }); test(`Test pathFromRootToPageDir should create path from url with sanitized characters`, t => { - const path = pathFromRootToPageDir('http://www.foo.bar/x/y/z:200.html', {}); + const path = pathToFolder('http://www.foo.bar/x/y/z:200.html', {}); t.is( path, 'pages/www_foo_bar/x/y/z-200.html/', @@ -23,7 +22,7 @@ test(`Test pathFromRootToPageDir should create path from url with sanitized char }); test(`Test pathFromRootToPageDir should create path from url with query string`, t => { - const path = pathFromRootToPageDir('http://www.foo.bar/x/y/z?foo=bar', {}); + const path = pathToFolder('http://www.foo.bar/x/y/z?foo=bar', {}); t.is( path, 'pages/www_foo_bar/x/y/z/query-115ffe20/', diff --git a/test/resultUrlTests.js b/test/resultUrlTests.js index c30f544be..656830a14 100644 --- a/test/resultUrlTests.js +++ b/test/resultUrlTests.js @@ -1,8 +1,7 @@ -'use strict'; +import dayjs from 'dayjs'; +import test from 'ava'; -const resultsStorage = require('../lib/core/resultsStorage'); -const dayjs = require('dayjs'); -const test = require('ava'); +import { resultsStorage } from '../lib/core/resultsStorage/index.js'; const timestamp = dayjs(); const timestampString = timestamp.format('YYYY-MM-DD-HH-mm-ss'); @@ -13,11 +12,7 @@ function createResultUrls(url, outputFolder, resultBaseURL) { } test(`Test hasBaseUrl should be false if base url is missing`, t => { - const resultUrls = createResultUrls( - 'http://www.foo.bar', - undefined, - undefined - ); + const resultUrls = createResultUrls('http://www.foo.bar'); t.is( resultUrls.hasBaseUrl(), false, diff --git a/test/runWithoutCli.js b/test/runWithoutCli.js index c1a79cb97..c7e88bd6c 100644 --- a/test/runWithoutCli.js +++ b/test/runWithoutCli.js @@ -1,9 +1,9 @@ -const sitespeed = require('../lib/sitespeed'); +import { run as _run } from '../lib/sitespeed.js'; const urls = ['http://127.0.0.1:3001/simple/']; async function run() { try { - let result = await sitespeed.run({ + let result = await _run({ urls, browsertime: { iterations: 1, @@ -19,14 +19,16 @@ async function run() { /* eslint-disable no-console */ console.error(result.errors); /* eslint-enable no-console */ + // eslint-disable-next-line unicorn/no-process-exit process.exit(1); } - } catch (e) { + } catch (error) { /* eslint-disable no-console */ - console.error(e); + console.error(error); /* eslint-enable no-console */ + // eslint-disable-next-line unicorn/no-process-exit process.exit(1); } } -run(); +await run(); diff --git a/test/slackTests.js b/test/slackTests.js index 80a4fdaaa..99e155806 100644 --- a/test/slackTests.js +++ b/test/slackTests.js @@ -1,18 +1,22 @@ -'use strict'; -const path = require('path'); -const fs = require('fs'); -const test = require('ava'); +import { resolve } from 'node:path'; +import { readFileSync } from 'node:fs'; -const resultUrls = require('../lib/core/resultsStorage/resultUrls'); -const messageMaker = require('../lib/support/messageMaker'); -const filterRegistry = require('../lib/support/filterRegistry'); -const intel = require('intel'); -const statsHelpers = require('../lib/support/statsHelpers'); +import test from 'ava'; +import intel from 'intel'; -const coachRunPath = path.resolve(__dirname, 'fixtures', 'coach.run-0.json'); -const coachRun = JSON.parse(fs.readFileSync(coachRunPath, 'utf8')); +import { resultUrls } from '../lib/core/resultsStorage/resultUrls.js'; +import { messageMaker } from '../lib/support/messageMaker.js'; +import * as filterRegistry from '../lib/support/filterRegistry.js'; +import * as statsHelpers from '../lib/support/statsHelpers.js'; +import { getSummary } from '../lib/plugins/slack/summary.js'; -const DataCollector = require('../lib/plugins/slack/dataCollector'); +import { fileURLToPath } from 'node:url'; +const __dirname = fileURLToPath(new URL('.', import.meta.url)); + +const coachRunPath = resolve(__dirname, 'fixtures', 'coach.run-0.json'); +const coachRun = JSON.parse(readFileSync(coachRunPath, 'utf8')); + +import { DataCollector } from '../lib/plugins/slack/dataCollector.js'; const defaultContextFactory = (context = {}) => { return Object.assign( @@ -28,7 +32,6 @@ const defaultContextFactory = (context = {}) => { }; test(`should not hard crash without a name`, t => { - const getSummary = require('../lib/plugins/slack/summary'); const dataCollector = new DataCollector(defaultContextFactory()); const options = { browsertime: { @@ -47,12 +50,9 @@ test(`DataCollector add data should add new page URL `, t => { const context = defaultContextFactory(); const collector = new DataCollector(context); - collector.addDataForUrl( - 'https://fake-site.sitespeed.io', - 'coach.run', - { coach: { pageSummary: coachRun } }, - undefined - ); + collector.addDataForUrl('https://fake-site.sitespeed.io', 'coach.run', { + coach: { pageSummary: coachRun } + }); t.deepEqual(collector.getURLs(), ['https://fake-site.sitespeed.io']); }); diff --git a/test/storageManagerTests.js b/test/storageManagerTests.js index 506e6d45f..20a4634db 100644 --- a/test/storageManagerTests.js +++ b/test/storageManagerTests.js @@ -1,9 +1,9 @@ -'use strict'; +import { resolve, join } from 'node:path'; -const resultsStorage = require('../lib/core/resultsStorage'); -const dayjs = require('dayjs'); -const path = require('path'); -const test = require('ava'); +import dayjs from 'dayjs'; +import test from 'ava'; + +import { resultsStorage } from '../lib/core/resultsStorage/index.js'; const timestamp = dayjs(); const timestampString = timestamp.format('YYYY-MM-DD-HH-mm-ss'); @@ -30,7 +30,7 @@ test(`Create base dir with default output folder`, t => { const storageManager = createManager('http://www.foo.bar'); t.is( storageManager.getBaseDir(), - path.resolve('sitespeed-result', 'www.foo.bar', timestampString) + resolve('sitespeed-result', 'www.foo.bar', timestampString) ); }); @@ -44,10 +44,7 @@ test(`Create base dir with custom output folder`, t => { test(`Create prefix with default output folder`, t => { const storageManager = createManager('http://www.foo.bar'); - t.is( - storageManager.getStoragePrefix(), - path.join('www.foo.bar', timestampString) - ); + t.is(storageManager.getStoragePrefix(), join('www.foo.bar', timestampString)); }); test(`Create prefix with custom output folder`, t => { diff --git a/tools/check-licenses.js b/tools/check-licenses.js index e538d137f..2a52923a5 100755 --- a/tools/check-licenses.js +++ b/tools/check-licenses.js @@ -1,17 +1,14 @@ #!/usr/bin/env node /*eslint no-console: 0*/ +import { init } from 'license-checker'; -'use strict'; - -const checker = require('license-checker'); - -checker.init( +init( { start: '.' }, - function (err, json) { - if (err) { - console.error(err.message); + function (error, json) { + if (error) { + console.error(error.message); process.exit(1); } else { const incompatibleDependencies = Object.keys(json).filter(packageName => { @@ -24,12 +21,12 @@ checker.init( .filter( license => !( - license.match(/LGPL/) || - license.match(/MIT/) || - license.match(/BSD/) + /LGPL/.test(license) || + /MIT/.test(license) || + /BSD/.test(license) ) ) - .find(license => license.match(/GPL/)) + .some(license => license.match(/GPL/)) ) return packageName; }); diff --git a/tools/tcp-server.js b/tools/tcp-server.js index a4a916721..cc3d5c5a8 100755 --- a/tools/tcp-server.js +++ b/tools/tcp-server.js @@ -1,15 +1,13 @@ #!/usr/bin/env node /*eslint no-console: 0*/ -const net = require('net'); +import { createServer } from 'node:net'; -const server = net - .createServer(function (sock) { - sock.on('data', function (data) { - console.log(data.toString()); - }); - }) - .listen(process.argv[2] || 0, undefined, undefined, () => { - const address = server.address(); - console.log('Server listening on ' + address.address + ':' + address.port); +const server = createServer(function (sock) { + sock.on('data', function (data) { + console.log(data.toString()); }); +}).listen(process.argv[2] || 0, undefined, undefined, () => { + const address = server.address(); + console.log('Server listening on ' + address.address + ':' + address.port); +}); diff --git a/tools/udp-server.js b/tools/udp-server.js index b71a05ba7..5da3fe52e 100755 --- a/tools/udp-server.js +++ b/tools/udp-server.js @@ -1,11 +1,11 @@ #!/usr/bin/env node /*eslint no-console: 0*/ -const dgram = require('dgram'); +import { createSocket } from 'node:dgram'; -const server = dgram.createSocket('udp4'); -server.on('message', function (msg) { - console.log(msg.toString()); +const server = createSocket('udp4'); +server.on('message', function (message) { + console.log(message.toString()); }); server.on('listening', function () {