diff --git a/commitlint.config.js b/commitlint.config.js index 5094bc7d..08879233 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -2,11 +2,5 @@ module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'header-max-length': [2, 'always', 100], - 'references-empty': [1, 'never'], - }, - parserPreset: { - parserOpts: { - issuePrefixes: ['gh', 'sc'], - }, }, } diff --git a/examples/advanced-project-js/README.md b/examples/advanced-project-js/README.md index 8b561e5f..955b9a12 100644 --- a/examples/advanced-project-js/README.md +++ b/examples/advanced-project-js/README.md @@ -15,34 +15,6 @@ npm create checkly -- --template advanced-project This project mimics a typical app where you organize code with top-level defaults and per page, service or component checks. -``` -. -├── .github -│   └── workflow.yml -├── README.md -├── checkly.config.js -├── package.json -└── src - ├── __checks__ - │   ├── api.check.js - │   ├── home.check.js - │   ├── homepage.spec.js - │   ├── login.spec.js - │   ├── utils - │   │   ├── auth-client.js - │   │   └── setup.js - │   └── website-group.check.js - ├── alert-channels.js - ├── defaults.js - └── services - ├── api - │   └── __checks__ - │   └── api.check.js - └── top-sellers - └── __checks__ - └── top-sellers.spec.js -``` - - Running `npx checkly test` will look for `.check.js` files and `.spec.js` in `__checks__` directories and execute them in a dry run. - Running `npx check deploy` will deploy your checks to Checkly, attach alert channels, and run them on a 10m schedule in the diff --git a/examples/advanced-project-js/checkly.config.js b/examples/advanced-project-js/checkly.config.js index a89877a2..77c49fda 100644 --- a/examples/advanced-project-js/checkly.config.js +++ b/examples/advanced-project-js/checkly.config.js @@ -1,4 +1,5 @@ const { defineConfig } = require('checkly'); +const { RetryStrategyBuilder } = require('checkly/constructs'); /** * See https://www.checklyhq.com/docs/cli/project-structure/ @@ -10,8 +11,8 @@ const config = defineConfig({ * See https://www.checklyhq.com/docs/cli/constructs/ to learn more about logical IDs. */ logicalId: 'advanced-example-project', - /* An optional URL to your Git repo */ - repoUrl: 'https://github.com/checkly/checkly-cli', + /* An optional URL to your Git repo to be shown in your test sessions and resource activity log */ + /* repoUrl: 'https://github.com/checkly/checkly-cli', */ /* Sets default values for Checks */ checks: { /* A default for how often your Check should run in minutes */ @@ -24,6 +25,8 @@ const config = defineConfig({ * See https://www.checklyhq.com/docs/cli/npm-packages/ */ runtimeId: '2023.02', + /* Failed check runs will be retried before triggering alerts */ + retryStrategy: RetryStrategyBuilder.fixedStrategy({ baseBackoffSeconds: 60, maxRetries: 4, sameRegion: true }), /* A glob pattern that matches the Checks inside your repo, see https://www.checklyhq.com/docs/cli/using-check-test-match/ */ checkMatch: '**/__checks__/**/*.check.js', browserChecks: { diff --git a/examples/advanced-project-js/src/__checks__/heartbeat.check.js b/examples/advanced-project-js/src/__checks__/heartbeat.check.js index 0977ef38..1df1638b 100644 --- a/examples/advanced-project-js/src/__checks__/heartbeat.check.js +++ b/examples/advanced-project-js/src/__checks__/heartbeat.check.js @@ -7,9 +7,9 @@ const { HeartbeatCheck } = require('checkly/constructs') /* new HeartbeatCheck('heartbeat-check-1', { name: 'Send weekly newsletter job', - period: 30, - periodUnit: 'minutes', - grace: 10, + period: 1, + periodUnit: 'hours', + grace: 30, graceUnit: 'minutes', }) */ diff --git a/examples/advanced-project-js/src/__checks__/multi-step-spacex.check.js b/examples/advanced-project-js/src/__checks__/multi-step-spacex.check.js new file mode 100644 index 00000000..ea17fb62 --- /dev/null +++ b/examples/advanced-project-js/src/__checks__/multi-step-spacex.check.js @@ -0,0 +1,21 @@ +import * as path from 'path' +import { MultiStepCheck } from 'checkly/constructs' +import { smsChannel, emailChannel } from '../alert-channels' + +const alertChannels = [smsChannel, emailChannel] + +/* +* In this example, we utilize the SpaceX public API to construct a series of chained requests, with the goal of confirming +* that the capsules retrieved from the main endpoint match those obtained from the individual capsule endpoint. +* Read more in our documentation https://www.checklyhq.com/docs/multistep-checks/ +*/ + +// We can define multiple checks in a single *.check.js file. +new MultiStepCheck('spacex-multistep-check', { + name: 'SpaceX MS', + runtimeId: '2023.09', + alertChannels, + code: { + entrypoint: path.join(__dirname, 'spacex-requests.spec.js') + }, +}) diff --git a/examples/advanced-project-js/src/__checks__/spacex-requests.spec.js b/examples/advanced-project-js/src/__checks__/spacex-requests.spec.js new file mode 100644 index 00000000..f329fcc9 --- /dev/null +++ b/examples/advanced-project-js/src/__checks__/spacex-requests.spec.js @@ -0,0 +1,29 @@ +import { test, expect } from '@playwright/test' + +const baseUrl = 'https://api.spacexdata.com/v3' + +test('space-x dragon capsules', async ({ request }) => { + /** + * Get all SpaceX Dragon Capsules + */ + const [first] = await test.step('get all capsules', async () => { + const response = await request.get(`${baseUrl}/dragons`) + expect(response).toBeOK() + + const data = await response.json() + expect(data.length).toBeGreaterThan(0) + + return data + }) + + /** + * Get a single Dragon Capsule + */ + await test.step('get single dragon capsule', async () => { + const response = await request.get(`${baseUrl}/dragons/${first.id}`) + expect(response).toBeOK() + + const dragonCapsule = await response.json() + expect(dragonCapsule.name).toEqual(first.name) + }) +}) diff --git a/examples/advanced-project-js/src/__checks__/website-group.check.js b/examples/advanced-project-js/src/__checks__/website-group.check.js index e41838d7..7f49ebe0 100644 --- a/examples/advanced-project-js/src/__checks__/website-group.check.js +++ b/examples/advanced-project-js/src/__checks__/website-group.check.js @@ -1,4 +1,4 @@ -const { CheckGroup } = require('checkly/constructs'); +const { CheckGroup, RetryStrategyBuilder } = require('checkly/constructs'); const { smsChannel, emailChannel } = require('../alert-channels'); const alertChannels = [smsChannel, emailChannel]; /* @@ -24,6 +24,11 @@ const websiteGroup = new CheckGroup('website-check-group-1', { apiCheckDefaults: {}, concurrency: 100, alertChannels, + /* + * Failed check runs in this group will be retried before triggering alerts. + * The wait time between retries will increase linearly: 30 seconds, 60 seconds, and then 90 seconds between the retries. + */ + retryStrategy: RetryStrategyBuilder.linearStrategy({ baseBackoffSeconds: 30, maxRetries: 3, sameRegion: false }), }); -module.exports = websiteGroup; +module.exports = { websiteGroup }; diff --git a/examples/advanced-project/README.md b/examples/advanced-project/README.md index 12ae0876..01a2f244 100644 --- a/examples/advanced-project/README.md +++ b/examples/advanced-project/README.md @@ -15,34 +15,6 @@ npm create checkly -- --template advanced-project This project mimics a typical app where you organize code with top-level defaults and per page, service or component checks. -``` -. -├── .github -│   └── workflow.yml -├── README.md -├── checkly.config.ts -├── package.json -└── src - ├── __checks__ - │   ├── api.check.ts - │   ├── home.check.ts - │   ├── homepage.spec.ts - │   ├── login.spec.ts - │   ├── utils - │   │   ├── auth-client.ts - │   │   └── setup.ts - │   └── website-group.check.ts - ├── alert-channels.ts - ├── defaults.ts - └── services - ├── api - │   └── __checks__ - │   └── api.check.ts - └── top-sellers - └── __checks__ - └── top-sellers.spec.ts -``` - - Running `npx checkly test` will look for `.check.ts` files and `.spec.ts` in `__checks__` directories and execute them in a dry run. - Running `npx check deploy` will deploy your checks to Checkly, attach alert channels, and run them on a 10m schedule in the diff --git a/examples/advanced-project/checkly.config.ts b/examples/advanced-project/checkly.config.ts index eda6c322..8035dcb6 100644 --- a/examples/advanced-project/checkly.config.ts +++ b/examples/advanced-project/checkly.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from 'checkly' +import { RetryStrategyBuilder } from 'checkly/constructs' /** * See https://www.checklyhq.com/docs/cli/project-structure/ @@ -10,8 +11,8 @@ const config = defineConfig({ * See https://www.checklyhq.com/docs/cli/constructs/ to learn more about logical IDs. */ logicalId: 'advanced-example-project', - /* An optional URL to your Git repo */ - repoUrl: 'https://github.com/checkly/checkly-cli', + /* An optional URL to your Git repo to be shown in your test sessions and resource activity log */ + /* repoUrl: 'https://github.com/checkly/checkly-cli', */ /* Sets default values for Checks */ checks: { /* A default for how often your Check should run in minutes */ @@ -24,6 +25,8 @@ const config = defineConfig({ * See https://www.checklyhq.com/docs/cli/npm-packages/ */ runtimeId: '2023.02', + /* Failed check runs will be retried before triggering alerts */ + retryStrategy: RetryStrategyBuilder.fixedStrategy({ baseBackoffSeconds: 60, maxRetries: 4, sameRegion: true }), /* A glob pattern that matches the Checks inside your repo, see https://www.checklyhq.com/docs/cli/using-check-test-match/ */ checkMatch: '**/__checks__/**/*.check.ts', browserChecks: { diff --git a/examples/advanced-project/src/__checks__/heartbeat.check.ts b/examples/advanced-project/src/__checks__/heartbeat.check.ts index dddda52c..f8cf7a0a 100644 --- a/examples/advanced-project/src/__checks__/heartbeat.check.ts +++ b/examples/advanced-project/src/__checks__/heartbeat.check.ts @@ -7,9 +7,9 @@ import { HeartbeatCheck } from 'checkly/constructs' /* new HeartbeatCheck('heartbeat-check-1', { name: 'Send weekly newsletter job', - period: 30, - periodUnit: 'minutes', - grace: 10, + period: 1, + periodUnit: 'hours', + grace: 30, graceUnit: 'minutes', }) */ diff --git a/examples/advanced-project/src/__checks__/multi-step-spacex.check.ts b/examples/advanced-project/src/__checks__/multi-step-spacex.check.ts new file mode 100644 index 00000000..908460c3 --- /dev/null +++ b/examples/advanced-project/src/__checks__/multi-step-spacex.check.ts @@ -0,0 +1,21 @@ +import * as path from 'path' +import { MultiStepCheck } from 'checkly/constructs' +import { smsChannel, emailChannel } from '../alert-channels' + +const alertChannels = [smsChannel, emailChannel] + +/* +* In this example, we utilize the SpaceX public API to construct a series of chained requests, with the goal of confirming +* that the capsules retrieved from the main endpoint match those obtained from the individual capsule endpoint. +* Read more in our documentation https://www.checklyhq.com/docs/multistep-checks/ +*/ + +// We can define multiple checks in a single *.check.ts file. +new MultiStepCheck('spacex-multistep-check', { + name: 'SpaceX MS', + runtimeId: '2023.09', + alertChannels, + code: { + entrypoint: path.join(__dirname, 'spacex-requests.spec.ts') + }, +}) diff --git a/examples/advanced-project/src/__checks__/spacex-requests.spec.ts b/examples/advanced-project/src/__checks__/spacex-requests.spec.ts new file mode 100644 index 00000000..f329fcc9 --- /dev/null +++ b/examples/advanced-project/src/__checks__/spacex-requests.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@playwright/test' + +const baseUrl = 'https://api.spacexdata.com/v3' + +test('space-x dragon capsules', async ({ request }) => { + /** + * Get all SpaceX Dragon Capsules + */ + const [first] = await test.step('get all capsules', async () => { + const response = await request.get(`${baseUrl}/dragons`) + expect(response).toBeOK() + + const data = await response.json() + expect(data.length).toBeGreaterThan(0) + + return data + }) + + /** + * Get a single Dragon Capsule + */ + await test.step('get single dragon capsule', async () => { + const response = await request.get(`${baseUrl}/dragons/${first.id}`) + expect(response).toBeOK() + + const dragonCapsule = await response.json() + expect(dragonCapsule.name).toEqual(first.name) + }) +}) diff --git a/examples/advanced-project/src/__checks__/website-group.check.ts b/examples/advanced-project/src/__checks__/website-group.check.ts index 1d8c0b35..22b193c5 100644 --- a/examples/advanced-project/src/__checks__/website-group.check.ts +++ b/examples/advanced-project/src/__checks__/website-group.check.ts @@ -1,4 +1,4 @@ -import { CheckGroup } from 'checkly/constructs' +import { CheckGroup, RetryStrategyBuilder } from 'checkly/constructs' import { smsChannel, emailChannel } from '../alert-channels' const alertChannels = [smsChannel, emailChannel] /* @@ -24,4 +24,9 @@ export const websiteGroup = new CheckGroup('website-check-group-1', { apiCheckDefaults: {}, concurrency: 100, alertChannels, + /* + * Failed check runs in this group will be retried before triggering alerts. + * The wait time between retries will increase linearly: 30 seconds, 60 seconds, and then 90 seconds between the retries. + */ + retryStrategy: RetryStrategyBuilder.linearStrategy({ baseBackoffSeconds: 30, maxRetries: 3, sameRegion: false }), }) diff --git a/examples/boilerplate-project-js/__checks__/heartbeat.check.js b/examples/boilerplate-project-js/__checks__/heartbeat.check.js index 0977ef38..1df1638b 100644 --- a/examples/boilerplate-project-js/__checks__/heartbeat.check.js +++ b/examples/boilerplate-project-js/__checks__/heartbeat.check.js @@ -7,9 +7,9 @@ const { HeartbeatCheck } = require('checkly/constructs') /* new HeartbeatCheck('heartbeat-check-1', { name: 'Send weekly newsletter job', - period: 30, - periodUnit: 'minutes', - grace: 10, + period: 1, + periodUnit: 'hours', + grace: 30, graceUnit: 'minutes', }) */ diff --git a/examples/boilerplate-project/__checks__/heartbeat.check.ts b/examples/boilerplate-project/__checks__/heartbeat.check.ts index dddda52c..f8cf7a0a 100644 --- a/examples/boilerplate-project/__checks__/heartbeat.check.ts +++ b/examples/boilerplate-project/__checks__/heartbeat.check.ts @@ -7,9 +7,9 @@ import { HeartbeatCheck } from 'checkly/constructs' /* new HeartbeatCheck('heartbeat-check-1', { name: 'Send weekly newsletter job', - period: 30, - periodUnit: 'minutes', - grace: 10, + period: 1, + periodUnit: 'hours', + grace: 30, graceUnit: 'minutes', }) */ diff --git a/package-lock.json b/package-lock.json index fad3fa2d..b50654e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,20 @@ "@commitlint/config-conventional": "17.6.5", "@typescript-eslint/eslint-plugin": "5.59.8", "@typescript-eslint/parser": "5.61.0", - "eslint": "8.41.0", + "eslint": "8.48.0", "lint-staged": "13.2.3", "simple-git-hooks": "2.8.1" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "dev": true, @@ -1269,21 +1278,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.1", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", + "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", "dev": true, - "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.3", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.2", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -1299,9 +1310,10 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, - "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -1314,8 +1326,9 @@ }, "node_modules/@eslint/eslintrc/node_modules/type-fest": { "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -1324,9 +1337,10 @@ } }, "node_modules/@eslint/js": { - "version": "8.41.0", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", + "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", "dev": true, - "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -1337,9 +1351,10 @@ "license": "MIT" }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", @@ -1349,11 +1364,6 @@ "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "dev": true, @@ -1366,6 +1376,12 @@ "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", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "license": "ISC", @@ -4165,10 +4181,11 @@ "license": "ISC" }, "node_modules/@typescript-eslint/types": { - "version": "5.46.1", - "license": "MIT", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.2.tgz", + "integrity": "sha512-flJYwMYgnUNDAN9/GAI3l8+wTmvTYdv64fcH8aoJK76Y+1FCZ08RtI5zDerM/FYT5DMkAc+19E4aLmd5KqdFyg==", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4176,19 +4193,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.46.1", - "license": "BSD-2-Clause", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.2.tgz", + "integrity": "sha512-kiJKVMLkoSciGyFU0TOY0fRxnp9qq1AzVOHNeN1+B9erKFCJ4Z8WdjAkKQPP+b1pWStGFqezMLltxO+308dJTQ==", "dependencies": { - "@typescript-eslint/types": "5.46.1", - "@typescript-eslint/visitor-keys": "5.46.1", + "@typescript-eslint/types": "6.7.2", + "@typescript-eslint/visitor-keys": "6.7.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4202,7 +4220,8 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { "version": "6.0.0", - "license": "ISC", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { "yallist": "^4.0.0" }, @@ -4211,8 +4230,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.3.8", - "license": "ISC", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -4225,7 +4245,8 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { "version": "4.0.0", - "license": "ISC" + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/@typescript-eslint/utils": { "version": "5.59.7", @@ -4348,14 +4369,15 @@ "license": "ISC" }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.46.1", - "license": "MIT", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.2.tgz", + "integrity": "sha512-uVw9VIMFBUTz8rIeaUT3fFe8xIUx8r4ywAdlQv1ifH+6acn/XF8Y6rwJ7XNmkNMDrTW+7+vxFFPIF40nJCVsMQ==", "dependencies": { - "@typescript-eslint/types": "5.46.1", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.7.2", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -4363,10 +4385,14 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "license": "Apache-2.0", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/abbrev": { @@ -4444,8 +4470,9 @@ }, "node_modules/ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5413,14 +5440,15 @@ } }, "node_modules/ci-info": { - "version": "3.7.1", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/sibiraj-s" } ], - "license": "MIT", "engines": { "node": ">=8" } @@ -6277,8 +6305,9 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, "node_modules/deepmerge": { "version": "4.3.1", @@ -6735,26 +6764,27 @@ } }, "node_modules/eslint": { - "version": "8.41.0", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", + "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.41.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.48.0", + "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -6764,7 +6794,6 @@ "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", @@ -6774,9 +6803,8 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -7297,9 +7325,10 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.0", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -7312,9 +7341,10 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "3.4.1", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -7324,8 +7354,9 @@ }, "node_modules/eslint/node_modules/estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -7432,11 +7463,12 @@ } }, "node_modules/espree": { - "version": "9.5.2", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" }, @@ -7447,6 +7479,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/espree/node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/espree/node_modules/eslint-visitor-keys": { "version": "3.4.1", "dev": true, @@ -7621,8 +7665,9 @@ }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "node_modules/fastest-levenshtein": { "version": "1.0.16", @@ -10901,8 +10946,9 @@ }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/json-schema-typed": { "version": "7.0.3", @@ -11066,8 +11112,9 @@ }, "node_modules/levn": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, - "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -13151,16 +13198,17 @@ } }, "node_modules/optionator": { - "version": "0.9.1", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, - "license": "MIT", "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -13686,8 +13734,9 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -14970,6 +15019,17 @@ "node": ">=8" } }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-jest": { "version": "29.0.3", "dev": true, @@ -15111,6 +15171,7 @@ }, "node_modules/tsutils": { "version": "3.21.0", + "dev": true, "license": "MIT", "dependencies": { "tslib": "^1.8.1" @@ -15124,6 +15185,7 @@ }, "node_modules/tsutils/node_modules/tslib": { "version": "1.14.1", + "dev": true, "license": "0BSD" }, "node_modules/tunnel": { @@ -15146,8 +15208,9 @@ }, "node_modules/type-check": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -15621,14 +15684,6 @@ "node": ">=8" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wordwrap": { "version": "1.0.0", "license": "MIT" @@ -16371,13 +16426,13 @@ "@oclif/plugin-not-found": "2.3.23", "@oclif/plugin-plugins": "2.3.0", "@oclif/plugin-warn-if-update-available": "2.0.24", - "@typescript-eslint/typescript-estree": "5.46.1", + "@typescript-eslint/typescript-estree": "6.7.2", "acorn": "8.8.1", "acorn-walk": "8.2.0", "async-mqtt": "2.6.3", "axios": "1.4.0", "chalk": "4.1.2", - "ci-info": "3.7.1", + "ci-info": "3.8.0", "conf": "10.2.0", "dotenv": "16.3.1", "git-repo-info": "2.1.1", @@ -16876,6 +16931,12 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, "@ampproject/remapping": { "version": "2.2.0", "dev": true, @@ -17694,16 +17755,20 @@ } }, "@eslint-community/regexpp": { - "version": "4.5.1", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", + "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", "dev": true }, "@eslint/eslintrc": { - "version": "2.0.3", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.2", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -17713,7 +17778,9 @@ }, "dependencies": { "globals": { - "version": "13.20.0", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -17721,12 +17788,16 @@ }, "type-fest": { "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true } } }, "@eslint/js": { - "version": "8.41.0", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", + "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", "dev": true }, "@gar/promisify": { @@ -17734,24 +17805,26 @@ "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.8", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", "minimatch": "^3.0.5" - }, - "dependencies": { - "@humanwhocodes/object-schema": { - "version": "1.2.1", - "dev": true - } } }, "@humanwhocodes/module-importer": { "version": "1.0.1", "dev": true }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, "@isaacs/cliui": { "version": "8.0.2", "requires": { @@ -19687,34 +19760,44 @@ } }, "@typescript-eslint/types": { - "version": "5.46.1" + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.2.tgz", + "integrity": "sha512-flJYwMYgnUNDAN9/GAI3l8+wTmvTYdv64fcH8aoJK76Y+1FCZ08RtI5zDerM/FYT5DMkAc+19E4aLmd5KqdFyg==" }, "@typescript-eslint/typescript-estree": { - "version": "5.46.1", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.2.tgz", + "integrity": "sha512-kiJKVMLkoSciGyFU0TOY0fRxnp9qq1AzVOHNeN1+B9erKFCJ4Z8WdjAkKQPP+b1pWStGFqezMLltxO+308dJTQ==", "requires": { - "@typescript-eslint/types": "5.46.1", - "@typescript-eslint/visitor-keys": "5.46.1", + "@typescript-eslint/types": "6.7.2", + "@typescript-eslint/visitor-keys": "6.7.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "dependencies": { "lru-cache": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "requires": { "yallist": "^4.0.0" } }, "semver": { - "version": "7.3.8", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } }, "yallist": { - "version": "4.0.0" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, @@ -19782,14 +19865,18 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.46.1", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.2.tgz", + "integrity": "sha512-uVw9VIMFBUTz8rIeaUT3fFe8xIUx8r4ywAdlQv1ifH+6acn/XF8Y6rwJ7XNmkNMDrTW+7+vxFFPIF40nJCVsMQ==", "requires": { - "@typescript-eslint/types": "5.46.1", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.7.2", + "eslint-visitor-keys": "^3.4.1" }, "dependencies": { "eslint-visitor-keys": { - "version": "3.3.0" + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" } } }, @@ -19839,6 +19926,8 @@ }, "ajv": { "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -20491,13 +20580,13 @@ "@types/tunnel": "0.0.3", "@types/uuid": "9.0.1", "@types/ws": "8.5.5", - "@typescript-eslint/typescript-estree": "5.46.1", + "@typescript-eslint/typescript-estree": "6.7.2", "acorn": "8.8.1", "acorn-walk": "8.2.0", "async-mqtt": "2.6.3", "axios": "1.4.0", "chalk": "4.1.2", - "ci-info": "3.7.1", + "ci-info": "3.8.0", "conf": "10.2.0", "config": "3.3.8", "cross-env": "7.0.3", @@ -20593,7 +20682,9 @@ "version": "2.0.0" }, "ci-info": { - "version": "3.7.1" + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==" }, "cjs-module-lexer": { "version": "1.2.3", @@ -21348,6 +21439,8 @@ }, "deep-is": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, "deepmerge": { @@ -21650,25 +21743,27 @@ "dev": true }, "eslint": { - "version": "8.41.0", + "version": "8.48.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", + "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.41.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.48.0", + "@humanwhocodes/config-array": "^0.11.10", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -21678,7 +21773,6 @@ "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", @@ -21688,9 +21782,8 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "dependencies": { @@ -21725,7 +21818,9 @@ "dev": true }, "eslint-scope": { - "version": "7.2.0", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -21733,11 +21828,15 @@ } }, "eslint-visitor-keys": { - "version": "3.4.1", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, "estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, "find-up": { @@ -22052,14 +22151,22 @@ "dev": true }, "espree": { - "version": "9.5.2", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" }, "dependencies": { + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + }, "eslint-visitor-keys": { "version": "3.4.1", "dev": true @@ -22175,6 +22282,8 @@ }, "fast-levenshtein": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "fastest-levenshtein": { @@ -24371,6 +24480,8 @@ }, "json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "json-schema-typed": { @@ -24471,6 +24582,8 @@ }, "levn": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "requires": { "prelude-ls": "^1.2.1", @@ -25843,15 +25956,17 @@ } }, "optionator": { - "version": "0.9.1", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" } }, "os-tmpdir": { @@ -26165,6 +26280,8 @@ }, "prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, "prettier": { @@ -26948,6 +27065,12 @@ "version": "3.0.1", "dev": true }, + "ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "requires": {} + }, "ts-jest": { "version": "29.0.3", "dev": true, @@ -27024,12 +27147,14 @@ }, "tsutils": { "version": "3.21.0", + "dev": true, "requires": { "tslib": "^1.8.1" }, "dependencies": { "tslib": { - "version": "1.14.1" + "version": "1.14.1", + "dev": true } } }, @@ -27046,6 +27171,8 @@ }, "type-check": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "requires": { "prelude-ls": "^1.2.1" @@ -27369,10 +27496,6 @@ "string-width": "^4.0.0" } }, - "word-wrap": { - "version": "1.2.3", - "dev": true - }, "wordwrap": { "version": "1.0.0" }, diff --git a/package.json b/package.json index 88c4f4ee..0b4285c6 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@commitlint/config-conventional": "17.6.5", "@typescript-eslint/eslint-plugin": "5.59.8", "@typescript-eslint/parser": "5.61.0", - "eslint": "8.41.0", + "eslint": "8.48.0", "lint-staged": "13.2.3", "simple-git-hooks": "2.8.1" }, diff --git a/packages/cli/e2e/__tests__/check-parse-error.spec.ts b/packages/cli/e2e/__tests__/check-parse-error.spec.ts index 1557ddb8..c9cf5946 100644 --- a/packages/cli/e2e/__tests__/check-parse-error.spec.ts +++ b/packages/cli/e2e/__tests__/check-parse-error.spec.ts @@ -1,5 +1,5 @@ import * as path from 'path' -import * as config from 'config' +import config from 'config' import { runChecklyCli } from '../run-checkly' describe('check parse error', () => { diff --git a/packages/cli/e2e/__tests__/deploy.spec.ts b/packages/cli/e2e/__tests__/deploy.spec.ts index 6481b5b9..8db6ed57 100644 --- a/packages/cli/e2e/__tests__/deploy.spec.ts +++ b/packages/cli/e2e/__tests__/deploy.spec.ts @@ -1,5 +1,5 @@ import * as path from 'path' -import * as config from 'config' +import config from 'config' import { v4 as uuidv4 } from 'uuid' import axios from 'axios' import { runChecklyCli } from '../run-checkly' @@ -244,4 +244,16 @@ Update and Unchanged: expect(result.stderr).toContain('Failed to deploy your project. Unable to find constructs to deploy.') expect(result.status).toBe(1) }) + + it('Should deploy a project with snapshots', async () => { + const result = await runChecklyCli({ + args: ['deploy', '--force'], + apiKey: config.get('apiKey'), + accountId: config.get('accountId'), + directory: path.join(__dirname, 'fixtures', 'snapshot-project'), + env: { PROJECT_LOGICAL_ID: projectLogicalId }, + }) + expect(result.status).toBe(0) + // TODO: Add assertions that the snapshots are successfully uploaded. + }) }) diff --git a/packages/cli/e2e/__tests__/destroy.spec.ts b/packages/cli/e2e/__tests__/destroy.spec.ts index d73e2552..8679f3d6 100644 --- a/packages/cli/e2e/__tests__/destroy.spec.ts +++ b/packages/cli/e2e/__tests__/destroy.spec.ts @@ -1,5 +1,5 @@ import * as path from 'path' -import * as config from 'config' +import config from 'config' import { v4 as uuidv4 } from 'uuid' import { runChecklyCli } from '../run-checkly' diff --git a/packages/cli/e2e/__tests__/env/env.add.spec.ts b/packages/cli/e2e/__tests__/env/env.add.spec.ts index eb2166eb..320c5bc6 100644 --- a/packages/cli/e2e/__tests__/env/env.add.spec.ts +++ b/packages/cli/e2e/__tests__/env/env.add.spec.ts @@ -1,6 +1,6 @@ // create test for checkly env add import * as path from 'path' -import * as config from 'config' +import config from 'config' import { nanoid } from 'nanoid' import { runChecklyCli } from '../../run-checkly' diff --git a/packages/cli/e2e/__tests__/env/env.ls.spec.ts b/packages/cli/e2e/__tests__/env/env.ls.spec.ts index f2e925e8..c1741dfa 100644 --- a/packages/cli/e2e/__tests__/env/env.ls.spec.ts +++ b/packages/cli/e2e/__tests__/env/env.ls.spec.ts @@ -1,6 +1,6 @@ // create test for checkly env ls import * as path from 'path' -import * as config from 'config' +import config from 'config' import { nanoid } from 'nanoid' import { runChecklyCli } from '../../run-checkly' diff --git a/packages/cli/e2e/__tests__/env/env.pull.spec.ts b/packages/cli/e2e/__tests__/env/env.pull.spec.ts index 0b78534f..20525c6c 100644 --- a/packages/cli/e2e/__tests__/env/env.pull.spec.ts +++ b/packages/cli/e2e/__tests__/env/env.pull.spec.ts @@ -1,6 +1,6 @@ // create test for checkly env pull import * as path from 'path' -import * as config from 'config' +import config from 'config' import { nanoid } from 'nanoid' import * as fs from 'fs' diff --git a/packages/cli/e2e/__tests__/env/env.rm.spec.ts b/packages/cli/e2e/__tests__/env/env.rm.spec.ts index 8558a0c3..3ac245cf 100644 --- a/packages/cli/e2e/__tests__/env/env.rm.spec.ts +++ b/packages/cli/e2e/__tests__/env/env.rm.spec.ts @@ -1,6 +1,6 @@ // create test for checkly env add import * as path from 'path' -import * as config from 'config' +import config from 'config' import { nanoid } from 'nanoid' import { runChecklyCli } from '../../run-checkly' diff --git a/packages/cli/e2e/__tests__/env/env.upate.spec.ts b/packages/cli/e2e/__tests__/env/env.upate.spec.ts index d605ebb0..c0f3da38 100644 --- a/packages/cli/e2e/__tests__/env/env.upate.spec.ts +++ b/packages/cli/e2e/__tests__/env/env.upate.spec.ts @@ -1,6 +1,6 @@ // create test for checkly env update import * as path from 'path' -import * as config from 'config' +import config from 'config' import { nanoid } from 'nanoid' import { runChecklyCli } from '../../run-checkly' diff --git a/packages/cli/e2e/__tests__/fixtures/snapshot-project-missing-snapshots/.gitignore b/packages/cli/e2e/__tests__/fixtures/snapshot-project-missing-snapshots/.gitignore new file mode 100644 index 00000000..ed56a91c --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/snapshot-project-missing-snapshots/.gitignore @@ -0,0 +1 @@ +*-snapshots diff --git a/packages/cli/e2e/__tests__/fixtures/snapshot-project-missing-snapshots/checkly.config.ts b/packages/cli/e2e/__tests__/fixtures/snapshot-project-missing-snapshots/checkly.config.ts new file mode 100644 index 00000000..9b69c8ae --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/snapshot-project-missing-snapshots/checkly.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Snapshot Project (Missing Snapshots)', + logicalId: process.env.PROJECT_LOGICAL_ID!, + checks: { + browserChecks: { testMatch: '**/*.spec.ts' }, + }, + cli: { + runLocation: 'us-east-1', + }, +}) + +export default config diff --git a/packages/cli/e2e/__tests__/fixtures/snapshot-project-missing-snapshots/snapshot-test.spec.ts b/packages/cli/e2e/__tests__/fixtures/snapshot-project-missing-snapshots/snapshot-test.spec.ts new file mode 100644 index 00000000..ccfb2999 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/snapshot-project-missing-snapshots/snapshot-test.spec.ts @@ -0,0 +1,8 @@ +import { expect, test } from '@playwright/test' + +test.use({ actionTimeout: 10000 }) + +test('Danube Snapshot Test', async ({ page }) => { + await page.goto('https://danube-web.shop') + await expect(page).toHaveScreenshot({ maxDiffPixels: 10000 }) +}) diff --git a/packages/cli/e2e/__tests__/fixtures/snapshot-project/checkly.config.ts b/packages/cli/e2e/__tests__/fixtures/snapshot-project/checkly.config.ts new file mode 100644 index 00000000..bb3a7869 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/snapshot-project/checkly.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Snapshot Project', + logicalId: process.env.PROJECT_LOGICAL_ID!, + checks: { + browserChecks: { testMatch: '**/*.spec.ts' }, + }, + cli: { + runLocation: 'us-east-1', + }, +}) + +export default config diff --git a/packages/cli/e2e/__tests__/fixtures/snapshot-project/snapshot-test.spec.ts b/packages/cli/e2e/__tests__/fixtures/snapshot-project/snapshot-test.spec.ts new file mode 100644 index 00000000..db918f51 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/snapshot-project/snapshot-test.spec.ts @@ -0,0 +1,10 @@ +/* eslint-disable no-console */ +import { expect, test } from '@playwright/test' + +test.use({ actionTimeout: 10000 }) + +test('Danube Snapshot Test', async ({ page }) => { + await page.goto('https://danube-web.shop') + await expect(page).toHaveScreenshot({ maxDiffPixels: 10000 }) + console.log(process.env.SECRET_ENV) +}) diff --git a/packages/cli/e2e/__tests__/fixtures/snapshot-project/snapshot-test.spec.ts-snapshots/Danube-Snapshot-Test-1-chromium-linux.png b/packages/cli/e2e/__tests__/fixtures/snapshot-project/snapshot-test.spec.ts-snapshots/Danube-Snapshot-Test-1-chromium-linux.png new file mode 100644 index 00000000..0199a3d0 Binary files /dev/null and b/packages/cli/e2e/__tests__/fixtures/snapshot-project/snapshot-test.spec.ts-snapshots/Danube-Snapshot-Test-1-chromium-linux.png differ diff --git a/packages/cli/e2e/__tests__/login.spec.ts b/packages/cli/e2e/__tests__/login.spec.ts index 8fd994a3..4734b1d5 100644 --- a/packages/cli/e2e/__tests__/login.spec.ts +++ b/packages/cli/e2e/__tests__/login.spec.ts @@ -1,4 +1,4 @@ -import * as config from 'config' +import config from 'config' import { runChecklyCli } from '../run-checkly' describe('login', () => { diff --git a/packages/cli/e2e/__tests__/switch.spec.ts b/packages/cli/e2e/__tests__/switch.spec.ts index 3654fb76..953435d7 100644 --- a/packages/cli/e2e/__tests__/switch.spec.ts +++ b/packages/cli/e2e/__tests__/switch.spec.ts @@ -1,5 +1,5 @@ import { runChecklyCli } from '../run-checkly' -import * as config from 'config' +import config from 'config' describe('switch', () => { it('should switch between user accounts', async () => { diff --git a/packages/cli/e2e/__tests__/test.spec.ts b/packages/cli/e2e/__tests__/test.spec.ts index 652a3c40..0ff9a371 100644 --- a/packages/cli/e2e/__tests__/test.spec.ts +++ b/packages/cli/e2e/__tests__/test.spec.ts @@ -1,6 +1,6 @@ import * as path from 'path' import * as uuid from 'uuid' -import * as config from 'config' +import config from 'config' import * as fs from 'fs' import { runChecklyCli } from '../run-checkly' @@ -171,4 +171,37 @@ describe('test', () => { expect(result.stdout).toContain(secretEnv) expect(result.status).toBe(0) }) + + it('Should run snapshot tests', async () => { + const secretEnv = uuid.v4() + const result = await runChecklyCli({ + args: ['test', '-e', `SECRET_ENV=${secretEnv}`, '--verbose'], + apiKey: config.get('apiKey'), + accountId: config.get('accountId'), + directory: path.join(__dirname, 'fixtures', 'snapshot-project'), + env: { PROJECT_LOGICAL_ID: `snapshot-project-${uuid.v4()}` }, + }) + expect(result.stdout).toContain(secretEnv) + expect(result.status).toBe(0) + }) + + it('Should create snapshots when using --update-snapshots', async () => { + const snapshotDir = path.join(__dirname, 'fixtures', 'snapshot-project-missing-snapshots', 'snapshot-test.spec.ts-snapshots') + try { + const result = await runChecklyCli({ + args: ['test', '--update-snapshots'], + apiKey: config.get('apiKey'), + accountId: config.get('accountId'), + directory: path.join(__dirname, 'fixtures', 'snapshot-project-missing-snapshots'), + env: { PROJECT_LOGICAL_ID: `snapshot-project-${uuid.v4()}` }, + }) + expect(result.status).toBe(0) + expect(fs.readdirSync(snapshotDir)).toEqual([ + 'Danube-Snapshot-Test-1-chromium-linux.png', + ]) + } finally { + // Clean up the snapshots for future runs + fs.rmSync(snapshotDir, { recursive: true }) + } + }) }) diff --git a/packages/cli/e2e/__tests__/trigger.spec.ts b/packages/cli/e2e/__tests__/trigger.spec.ts index d433096f..780755d4 100644 --- a/packages/cli/e2e/__tests__/trigger.spec.ts +++ b/packages/cli/e2e/__tests__/trigger.spec.ts @@ -1,6 +1,6 @@ import * as path from 'path' import * as uuid from 'uuid' -import * as config from 'config' +import config from 'config' import { runChecklyCli } from '../run-checkly' diff --git a/packages/cli/e2e/__tests__/whoami.spec.ts b/packages/cli/e2e/__tests__/whoami.spec.ts index 9385cfda..1cdf69d8 100644 --- a/packages/cli/e2e/__tests__/whoami.spec.ts +++ b/packages/cli/e2e/__tests__/whoami.spec.ts @@ -1,5 +1,5 @@ import { runChecklyCli } from '../run-checkly' -import * as config from 'config' +import config from 'config' describe('whomai', () => { it('should give correct user', async () => { diff --git a/packages/cli/package.json b/packages/cli/package.json index b827aa2d..cc1c9663 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -73,13 +73,13 @@ "@oclif/plugin-not-found": "2.3.23", "@oclif/plugin-plugins": "2.3.0", "@oclif/plugin-warn-if-update-available": "2.0.24", - "@typescript-eslint/typescript-estree": "5.46.1", + "@typescript-eslint/typescript-estree": "6.7.2", "acorn": "8.8.1", "acorn-walk": "8.2.0", "async-mqtt": "2.6.3", "axios": "1.4.0", "chalk": "4.1.2", - "ci-info": "3.7.1", + "ci-info": "3.8.0", "conf": "10.2.0", "dotenv": "16.3.1", "git-repo-info": "2.1.1", @@ -131,6 +131,10 @@ "testMatch": [ "/e2e/__tests__/**/*.spec.ts" ], + "testPathIgnorePatterns": [ + "/node_modules/", + "/e2e/__tests__/fixtures/" + ], "preset": "ts-jest", "testEnvironment": "node" } diff --git a/packages/cli/src/commands/baseCommand.ts b/packages/cli/src/commands/baseCommand.ts index 14eeef38..3289f58d 100644 --- a/packages/cli/src/commands/baseCommand.ts +++ b/packages/cli/src/commands/baseCommand.ts @@ -1,5 +1,5 @@ import axios from 'axios' -import * as prompts from 'prompts' +import prompts from 'prompts' import { Command } from '@oclif/core' import { api } from '../rest/api' diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 66ea3a66..4d1eb9ca 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -1,6 +1,6 @@ import * as api from '../rest/api' import config from '../services/config' -import * as prompts from 'prompts' +import prompts from 'prompts' import { Flags, ux } from '@oclif/core' import { AuthCommand } from './authCommand' import { parseProject } from '../services/project-parser' @@ -10,12 +10,13 @@ import type { Runtime } from '../rest/runtimes' import { Check, AlertChannelSubscription, AlertChannel, CheckGroup, Dashboard, MaintenanceWindow, PrivateLocation, PrivateLocationCheckAssignment, PrivateLocationGroupAssignment, - Project, ProjectData, + Project, ProjectData, BrowserCheck, } from '../constructs' -import * as chalk from 'chalk' +import chalk from 'chalk' import { splitConfigFilePath, getGitInformation } from '../services/util' import commonMessages from '../messages/common-messages' import { ProjectDeployResponse } from '../rest/projects' +import { uploadSnapshots } from '../services/snapshot-service' // eslint-disable-next-line no-restricted-syntax enum ResourceDeployStatus { @@ -59,7 +60,13 @@ export default class Deploy extends AuthCommand { async run (): Promise { ux.action.start('Parsing your project', undefined, { stdout: true }) const { flags } = await this.parse(Deploy) - const { force, preview, 'schedule-on-deploy': scheduleOnDeploy, output, config: configFilename } = flags + const { + force, + preview, + 'schedule-on-deploy': scheduleOnDeploy, + output, + config: configFilename, + } = flags const { configDirectory, configFilenames } = splitConfigFilePath(configFilename) const { config: checklyConfig, @@ -85,6 +92,15 @@ export default class Deploy extends AuthCommand { const repoInfo = getGitInformation(project.repoUrl) ux.action.stop() + if (!preview) { + for (const check of Object.values(project.data.check)) { + if (!(check instanceof BrowserCheck)) { + continue + } + check.snapshots = await uploadSnapshots(check.rawSnapshots) + } + } + const projectPayload = project.synthesize(false) if (!projectPayload.resources.length) { if (preview) { diff --git a/packages/cli/src/commands/destroy.ts b/packages/cli/src/commands/destroy.ts index 7df90441..97e76e82 100644 --- a/packages/cli/src/commands/destroy.ts +++ b/packages/cli/src/commands/destroy.ts @@ -2,7 +2,7 @@ import { Flags } from '@oclif/core' import * as api from '../rest/api' import { loadChecklyConfig } from '../services/checkly-config-loader' import { AuthCommand } from './authCommand' -import * as prompts from 'prompts' +import prompts from 'prompts' import config from '../services/config' import { splitConfigFilePath } from '../services/util' import commonMessages from '../messages/common-messages' diff --git a/packages/cli/src/commands/env/pull.ts b/packages/cli/src/commands/env/pull.ts index bfb5cfa5..9249dffd 100644 --- a/packages/cli/src/commands/env/pull.ts +++ b/packages/cli/src/commands/env/pull.ts @@ -1,4 +1,4 @@ -import * as prompts from 'prompts' +import prompts from 'prompts' import * as path from 'path' import * as api from '../../rest/api' import { Flags, Args } from '@oclif/core' diff --git a/packages/cli/src/commands/env/rm.ts b/packages/cli/src/commands/env/rm.ts index 7dc1aa92..a5456786 100644 --- a/packages/cli/src/commands/env/rm.ts +++ b/packages/cli/src/commands/env/rm.ts @@ -1,4 +1,4 @@ -import * as prompts from 'prompts' +import prompts from 'prompts' import * as api from '../../rest/api' import { Flags, Args } from '@oclif/core' import { AuthCommand } from '../authCommand' diff --git a/packages/cli/src/commands/login.ts b/packages/cli/src/commands/login.ts index 2b22755e..b423703d 100644 --- a/packages/cli/src/commands/login.ts +++ b/packages/cli/src/commands/login.ts @@ -1,7 +1,7 @@ -import * as open from 'open' -import * as chalk from 'chalk' +import open from 'open' +import chalk from 'chalk' import { BaseCommand } from './baseCommand' -import * as prompts from 'prompts' +import prompts from 'prompts' import config from '../services/config' import * as api from '../rest/api' import type { Account } from '../rest/accounts' diff --git a/packages/cli/src/commands/logout.ts b/packages/cli/src/commands/logout.ts index 0686f52f..4d760e31 100644 --- a/packages/cli/src/commands/logout.ts +++ b/packages/cli/src/commands/logout.ts @@ -1,4 +1,4 @@ -import * as prompts from 'prompts' +import prompts from 'prompts' import { Flags } from '@oclif/core' import config from '../services/config' import { BaseCommand } from './baseCommand' diff --git a/packages/cli/src/commands/switch.ts b/packages/cli/src/commands/switch.ts index 6ab8df45..b17e4bb4 100644 --- a/packages/cli/src/commands/switch.ts +++ b/packages/cli/src/commands/switch.ts @@ -1,4 +1,4 @@ -import * as chalk from 'chalk' +import chalk from 'chalk' import { Flags } from '@oclif/core' import config from '../services/config' import * as api from '../rest/api' diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index a481a2b1..bff0c871 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -1,5 +1,5 @@ import { Flags, Args, ux } from '@oclif/core' -import * as indentString from 'indent-string' +import indentString from 'indent-string' import { isCI } from 'ci-info' import * as api from '../rest/api' import config from '../services/config' @@ -16,13 +16,14 @@ import { loadChecklyConfig } from '../services/checkly-config-loader' import { filterByFileNamePattern, filterByCheckNamePattern, filterByTags } from '../services/test-filters' import type { Runtime } from '../rest/runtimes' import { AuthCommand } from './authCommand' -import { BrowserCheck, Check, HeartbeatCheck, Session } from '../constructs' +import { BrowserCheck, Check, HeartbeatCheck, Project, Session } from '../constructs' import type { Region } from '..' import { splitConfigFilePath, getGitInformation, getCiInformation, getEnvs } from '../services/util' import { createReporters, ReporterType } from '../reporters/reporter' import commonMessages from '../messages/common-messages' import { TestResultsShortLinks } from '../rest/test-sessions' import { printLn, formatCheckTitle, CheckStatus } from '../reporters/util' +import { uploadSnapshots } from '../services/snapshot-service' const DEFAULT_REGION = 'eu-central-1' @@ -94,6 +95,11 @@ export default class Test extends AuthCommand { char: 'n', description: 'A name to use when storing results in Checkly with --record.', }), + 'update-snapshots': Flags.boolean({ + char: 'u', + description: 'Update any snapshots using the actual result of this test run.', + default: false, + }), } static args = { @@ -125,6 +131,7 @@ export default class Test extends AuthCommand { config: configFilename, record: shouldRecord, 'test-session-name': testSessionName, + 'update-snapshots': updateSnapshots, } = flags const filePatterns = argv as string[] @@ -174,7 +181,13 @@ export default class Test extends AuthCommand { return filterByCheckNamePattern(grep, check.name) }) .filter(([, check]) => { - return filterByTags(targetTags?.map((tags: string) => tags.split(',')) ?? [], check.tags) + const tags = check.tags ?? [] + const checkGroup = this.getCheckGroup(project, check) + if (checkGroup) { + const checkGroupTags = checkGroup.tags ?? [] + tags.concat(checkGroupTags) + } + return filterByTags(targetTags?.map((tags: string) => tags.split(',')) ?? [], tags) }) .map(([key, check]) => { check.logicalId = key @@ -192,6 +205,13 @@ export default class Test extends AuthCommand { return check }) + for (const check of checks) { + if (!(check instanceof BrowserCheck)) { + continue + } + check.snapshots = await uploadSnapshots(check.rawSnapshots) + } + ux.action.stop() if (!checks.length) { @@ -218,6 +238,8 @@ export default class Test extends AuthCommand { shouldRecord, repoInfo, ciInfo.environment, + updateSnapshots, + configDirectory, ) runner.on(Events.RUN_STARTED, @@ -237,6 +259,7 @@ export default class Test extends AuthCommand { if (result.hasFailures) { process.exitCode = 1 } + reporters.forEach(r => r.onCheckEnd(checkRunId, { logicalId: check.logicalId, sourceFile: check.getSourceFile(), @@ -329,4 +352,12 @@ export default class Test extends AuthCommand { } } } + + private getCheckGroup (project: Project, check: Check) { + if (!check.groupId) { + return + } + const ref = check.groupId.ref.toString() + return project.data['check-group'][ref] + } } diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index cd8e9bcb..327bf843 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -2,6 +2,7 @@ const CheckTypes = { API: 'API', BROWSER: 'BROWSER', HEARTBEAT: 'HEARTBEAT', + MULTI_STEP: 'MULTI_STEP', } export default CheckTypes diff --git a/packages/cli/src/constructs/__tests__/browser-check.spec.ts b/packages/cli/src/constructs/__tests__/browser-check.spec.ts index 57a0870c..dc782333 100644 --- a/packages/cli/src/constructs/__tests__/browser-check.spec.ts +++ b/packages/cli/src/constructs/__tests__/browser-check.spec.ts @@ -15,7 +15,7 @@ describe('BrowserCheck', () => { const bundle = BrowserCheck.bundle(getFilePath('entrypoint.js'), '2022.10') delete Session.basePath - expect(bundle).toEqual({ + expect(bundle).toMatchObject({ script: fs.readFileSync(getFilePath('entrypoint.js')).toString(), scriptPath: 'fixtures/browser-check/entrypoint.js', dependencies: [ diff --git a/packages/cli/src/constructs/__tests__/multi-step-check.spec.ts b/packages/cli/src/constructs/__tests__/multi-step-check.spec.ts new file mode 100644 index 00000000..e24513d9 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/multi-step-check.spec.ts @@ -0,0 +1,39 @@ +import { Project, Session } from '../project' +import { MultiStepCheck } from '../multi-step-check' + +const runtimesWithMultiStepSupport = { + 2023.09: { name: '2023.09', multiStepSupport: true, default: false, stage: 'CURRENT', description: 'Main updates are Playwright 1.28.0, Node.js 16.x and Typescript support. We are also dropping support for Puppeteer', dependencies: { '@playwright/test': '1.28.0', '@opentelemetry/api': '1.0.4', '@opentelemetry/sdk-trace-base': '1.0.1', '@faker-js/faker': '5.5.3', aws4: '1.11.0', axios: '0.27.2', btoa: '1.2.1', chai: '4.3.7', 'chai-string': '1.5.0', 'crypto-js': '4.1.1', expect: '29.3.1', 'form-data': '4.0.0', jsonwebtoken: '8.5.1', lodash: '4.17.21', mocha: '10.1.0', moment: '2.29.2', node: '16.x', otpauth: '9.0.2', playwright: '1.28.0', typescript: '4.8.4', uuid: '9.0.0' } }, +} + +describe('MultistepCheck', () => { + it('should report multistep as check type', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.availableRuntimes = runtimesWithMultiStepSupport + const check = new MultiStepCheck('main-check', { + name: 'Main Check', + runtimeId: '2023.09', + code: { content: '' }, + }) + expect(check.synthesize()).toMatchObject({ checkType: 'MULTI_STEP' }) + }) + it('should throw if runtime does not support multi step check type', () => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.availableRuntimes = { + ...runtimesWithMultiStepSupport, + 2023.09: { + ...runtimesWithMultiStepSupport['2023.09'], + multiStepSupport: false, + }, + } + expect(() => new MultiStepCheck('main-check', { + name: 'Main Check', + code: { content: '' }, + })).toThrowError('This runtime does not support multi step checks.') + }) +}) diff --git a/packages/cli/src/constructs/browser-check.ts b/packages/cli/src/constructs/browser-check.ts index bc3ed773..cf1e69c9 100644 --- a/packages/cli/src/constructs/browser-check.ts +++ b/packages/cli/src/constructs/browser-check.ts @@ -5,6 +5,7 @@ import { Parser } from '../services/check-parser/parser' import { CheckConfigDefaults } from '../services/checkly-config-loader' import { pathToPosix } from '../services/util' import { Content, Entrypoint } from './construct' +import { detectSnapshots, Snapshot } from '../services/snapshot-service' export interface CheckDependency { path: string @@ -37,6 +38,11 @@ export class BrowserCheck extends Check { dependencies?: Array sslCheckDomain?: string + // For snapshots, we first store `rawSnapshots` with the path to the file. + // The `snapshots` field is set later (with a `key`) after these are uploaded to storage. + rawSnapshots?: Array<{ absolutePath: string, path: string }> + snapshots?: Array + /** * Constructs the Browser Check instance * @@ -73,6 +79,7 @@ export class BrowserCheck extends Check { this.script = bundle.script this.scriptPath = bundle.scriptPath this.dependencies = bundle.dependencies + this.rawSnapshots = bundle.snapshots } else { throw new Error('Unrecognized type for the "code" property. The "code" property should be a string of JS/TS code.') } @@ -124,6 +131,7 @@ export class BrowserCheck extends Check { script: parsed.entrypoint.content, scriptPath: pathToPosix(path.relative(Session.basePath!, parsed.entrypoint.filePath)), dependencies: deps, + snapshots: detectSnapshots(Session.basePath!, parsed.entrypoint.filePath), } } @@ -139,6 +147,7 @@ export class BrowserCheck extends Check { scriptPath: this.scriptPath, dependencies: this.dependencies, sslCheckDomain: this.sslCheckDomain || null, // empty string is converted to null + snapshots: this.snapshots, } } } diff --git a/packages/cli/src/constructs/check-group.ts b/packages/cli/src/constructs/check-group.ts index 88e1d294..0b8b1026 100644 --- a/packages/cli/src/constructs/check-group.ts +++ b/packages/cli/src/constructs/check-group.ts @@ -14,6 +14,7 @@ import { ApiCheckDefaultConfig } from './api-check' import { pathToPosix } from '../services/util' import type { Region } from '..' import type { Frequency } from './frequency' +import type { RetryStrategy } from './retry-strategy' const defaultApiCheckDefaults: ApiCheckDefaultConfig = { headers: [], @@ -32,6 +33,13 @@ type BrowserCheckConfig = CheckConfigDefaults & { testMatch: string, } +type MultiStepCheckConfig = CheckConfigDefaults & { + /** + * Glob pattern to include multiple files, i.e. all `.spec.ts` files + */ + testMatch: string, +} + export interface CheckGroupProps { /** * The name of the check group. @@ -48,6 +56,7 @@ export interface CheckGroupProps { /** * Setting this to "true" will trigger a retry when a check fails from the failing region and another, * randomly selected region before marking the check as failed. + * @deprecated Use {@link CheckGroupProps.retryStrategy} instead. */ doubleCheck?: boolean /** @@ -80,6 +89,7 @@ export interface CheckGroupProps { */ alertChannels?: Array browserChecks?: BrowserCheckConfig, + multiStepChecks?: MultiStepCheckConfig, /** * A valid piece of Node.js code to run in the setup phase of an API check in this group. * @deprecated use the "ApiCheck.setupScript" property instead and use common JS/TS code @@ -93,6 +103,10 @@ export interface CheckGroupProps { */ localTearDownScript?: string apiCheckDefaults?: ApiCheckDefaultConfig + /** + * Sets a retry policy for the group. Use RetryStrategyBuilder to create a retry policy. + */ + retryStrategy?: RetryStrategy } /** @@ -119,6 +133,8 @@ export class CheckGroup extends Construct { localTearDownScript?: string apiCheckDefaults: ApiCheckDefaultConfig browserChecks?: BrowserCheckConfig + multiStepChecks?: MultiStepCheckConfig + retryStrategy?: RetryStrategy static readonly __checklyType = 'check-group' @@ -156,6 +172,7 @@ export class CheckGroup extends Construct { this.alertChannels = props.alertChannels ?? [] this.localSetupScript = props.localSetupScript this.localTearDownScript = props.localTearDownScript + this.retryStrategy = props.retryStrategy // `browserChecks` is not a CheckGroup resource property. Not present in synthesize() this.browserChecks = props.browserChecks const fileAbsolutePath = Session.checkFileAbsolutePath! @@ -230,6 +247,12 @@ export class CheckGroup extends Construct { } } + public getMultiStepCheckDefaults (): CheckConfigDefaults { + return { + frequency: this.browserChecks?.frequency, + } + } + synthesize () { return { name: this.name, @@ -247,6 +270,7 @@ export class CheckGroup extends Construct { localTearDownScript: this.localTearDownScript, apiCheckDefaults: this.apiCheckDefaults, environmentVariables: this.environmentVariables, + retryStrategy: this.retryStrategy, } } } diff --git a/packages/cli/src/constructs/check.ts b/packages/cli/src/constructs/check.ts index 689f3032..8dec2f2a 100644 --- a/packages/cli/src/constructs/check.ts +++ b/packages/cli/src/constructs/check.ts @@ -10,6 +10,7 @@ import type { Region } from '..' import type { CheckGroup } from './check-group' import { PrivateLocation } from './private-location' import { PrivateLocationCheckAssignment } from './private-location-check-assignment' +import { RetryStrategy } from './retry-strategy' export interface CheckProps { /** @@ -27,6 +28,7 @@ export interface CheckProps { /** * Setting this to "true" will trigger a retry when a check fails from the failing region and another, * randomly selected region before marking the check as failed. + * @deprecated Use {@link CheckProps.retryStrategy} instead. */ doubleCheck?: boolean /** @@ -80,6 +82,10 @@ export interface CheckProps { * Determines if the check is available only when 'test' runs (not included when 'deploy' is executed). */ testOnly?: boolean + /** + * Sets a retry policy for the check. Use RetryStrategyBuilder to create a retry policy. + */ + retryStrategy?: RetryStrategy } // This is an abstract class. It shouldn't be used directly. @@ -99,6 +105,7 @@ export abstract class Check extends Construct { groupId?: Ref alertChannels?: Array testOnly?: boolean + retryStrategy?: RetryStrategy __checkFilePath?: string // internal variable to filter by check file name from the CLI static readonly __checklyType = 'check' @@ -134,6 +141,7 @@ export abstract class Check extends Construct { // alertSettings, useGlobalAlertSettings, groupId, groupOrder this.testOnly = props.testOnly ?? false + this.retryStrategy = props.retryStrategy this.__checkFilePath = Session.checkFilePath } @@ -209,6 +217,7 @@ export abstract class Check extends Construct { frequencyOffset: this.frequencyOffset, groupId: this.groupId, environmentVariables: this.environmentVariables, + retryStrategy: this.retryStrategy, } } } diff --git a/packages/cli/src/constructs/frequency.ts b/packages/cli/src/constructs/frequency.ts index 903c867d..3b233fac 100644 --- a/packages/cli/src/constructs/frequency.ts +++ b/packages/cli/src/constructs/frequency.ts @@ -12,7 +12,7 @@ export class Frequency { static EVERY_1H = new Frequency(1 * 60) static EVERY_2H = new Frequency(2 * 60) - static EVERY_3M = new Frequency(3 * 60) + static EVERY_3H = new Frequency(3 * 60) static EVERY_6H = new Frequency(6 * 60) static EVERY_12H = new Frequency(12 * 60) static EVERY_24H = new Frequency(24 * 60) diff --git a/packages/cli/src/constructs/heartbeat-check.ts b/packages/cli/src/constructs/heartbeat-check.ts index 0571030c..accc894e 100644 --- a/packages/cli/src/constructs/heartbeat-check.ts +++ b/packages/cli/src/constructs/heartbeat-check.ts @@ -80,6 +80,7 @@ export class HeartbeatCheck extends Check { } Session.registerConstruct(this) + this.addSubscriptions() } synthesize (): any | null { diff --git a/packages/cli/src/constructs/index.ts b/packages/cli/src/constructs/index.ts index cbeb9097..9e667d22 100644 --- a/packages/cli/src/constructs/index.ts +++ b/packages/cli/src/constructs/index.ts @@ -22,3 +22,5 @@ export * from './private-location-group-assignment' export * from './check' export * from './dashboard' export * from './phone-call-alert-channel' +export * from './retry-strategy' +export * from './multi-step-check' diff --git a/packages/cli/src/constructs/multi-step-check.ts b/packages/cli/src/constructs/multi-step-check.ts new file mode 100644 index 00000000..ec7a4193 --- /dev/null +++ b/packages/cli/src/constructs/multi-step-check.ts @@ -0,0 +1,133 @@ +import * as path from 'path' +import { Check, CheckProps } from './check' +import { Session } from './project' +import { Parser } from '../services/check-parser/parser' +import { CheckConfigDefaults } from '../services/checkly-config-loader' +import { pathToPosix } from '../services/util' +import { Content, Entrypoint } from './construct' +import CheckTypes from '../constants' +import { CheckDependency } from './browser-check' + +export interface MultiStepCheckProps extends CheckProps { + /** + * A valid piece of Node.js javascript code describing a multi-step interaction + * with the Puppeteer or Playwright frameworks. + */ + code: Content|Entrypoint +} + +/** + * Creates a multi-step Check + * + * @remarks + * + * This class make use of the multi-step checks endpoints. + */ +export class MultiStepCheck extends Check { + script: string + scriptPath?: string + dependencies?: Array + + /** + * Constructs the multi-step instance + * + * @param logicalId unique project-scoped resource name identification + * @param props check configuration properties + * {@link https://checklyhq.com/docs/cli/constructs/#multistepcheck Read more in the docs} + */ + constructor (logicalId: string, props: MultiStepCheckProps) { + if (props.group) { + MultiStepCheck.applyDefaultMultiStepCheckGroupConfig(props, props.group.getMultiStepCheckDefaults()) + } + MultiStepCheck.applyDefaultMultiStepCheckConfig(props) + super(logicalId, props) + + if (!Session.availableRuntimes[this.runtimeId!]?.multiStepSupport) { + throw new Error('This runtime does not support multi step checks.') + } + if ('content' in props.code) { + const script = props.code.content + this.script = script + } else if ('entrypoint' in props.code) { + const entrypoint = props.code.entrypoint + let absoluteEntrypoint = null + if (path.isAbsolute(entrypoint)) { + absoluteEntrypoint = entrypoint + } else { + if (!Session.checkFileAbsolutePath) { + throw new Error('You cannot use relative paths without the checkFileAbsolutePath in session') + } + absoluteEntrypoint = path.join(path.dirname(Session.checkFileAbsolutePath), entrypoint) + } + // runtimeId will always be set by check or multi-step check defaults so it is safe to use ! operator + const bundle = MultiStepCheck.bundle(absoluteEntrypoint, this.runtimeId!) + if (!bundle.script) { + throw new Error(`Multi-Step check "${logicalId}" is not allowed to be empty`) + } + this.script = bundle.script + this.scriptPath = bundle.scriptPath + this.dependencies = bundle.dependencies + } else { + throw new Error('Unrecognized type for the "code" property. The "code" property should be a string of JS/TS code.') + } + Session.registerConstruct(this) + this.addSubscriptions() + this.addPrivateLocationCheckAssignments() + } + + private static applyDefaultMultiStepCheckGroupConfig (props: CheckConfigDefaults, groupProps: CheckConfigDefaults) { + let configKey: keyof CheckConfigDefaults + for (configKey in groupProps) { + const newVal: any = props[configKey] ?? groupProps[configKey] + props[configKey] = newVal + } + } + + private static applyDefaultMultiStepCheckConfig (props: CheckConfigDefaults) { + if (!Session.multiStepCheckDefaults) { + return + } + let configKey: keyof CheckConfigDefaults + for (configKey in Session.multiStepCheckDefaults) { + const newVal: any = props[configKey] ?? Session.multiStepCheckDefaults[configKey] + props[configKey] = newVal + } + } + + static bundle (entry: string, runtimeId: string) { + const runtime = Session.availableRuntimes[runtimeId] + if (!runtime) { + throw new Error(`${runtimeId} is not supported`) + } + const parser = new Parser(Object.keys(runtime.dependencies)) + const parsed = parser.parse(entry) + // Maybe we can get the parsed deps with the content immediately + + const deps: CheckDependency[] = [] + for (const { filePath, content } of parsed.dependencies) { + deps.push({ + path: pathToPosix(path.relative(Session.basePath!, filePath)), + content, + }) + } + return { + script: parsed.entrypoint.content, + scriptPath: pathToPosix(path.relative(Session.basePath!, parsed.entrypoint.filePath)), + dependencies: deps, + } + } + + getSourceFile () { + return this.__checkFilePath ?? this.scriptPath + } + + synthesize () { + return { + ...super.synthesize(), + checkType: CheckTypes.MULTI_STEP, + script: this.script, + scriptPath: this.scriptPath, + dependencies: this.dependencies, + } + } +} diff --git a/packages/cli/src/constructs/project.ts b/packages/cli/src/constructs/project.ts index 0fd067f6..1d472771 100644 --- a/packages/cli/src/constructs/project.ts +++ b/packages/cli/src/constructs/project.ts @@ -138,6 +138,7 @@ export class Session { static basePath?: string static checkDefaults?: CheckConfigDefaults static browserCheckDefaults?: CheckConfigDefaults + static multiStepCheckDefaults?: CheckConfigDefaults static checkFilePath?: string static checkFileAbsolutePath?: string static availableRuntimes: Record diff --git a/packages/cli/src/constructs/retry-strategy.ts b/packages/cli/src/constructs/retry-strategy.ts new file mode 100644 index 00000000..731e29f2 --- /dev/null +++ b/packages/cli/src/constructs/retry-strategy.ts @@ -0,0 +1,67 @@ +export type RetryStrategyType = 'LINEAR' | 'EXPONENTIAL' | 'FIXED' + +export interface RetryStrategy { + type: RetryStrategyType, + /** + * The number of seconds to wait before the first retry attempt. + */ + baseBackoffSeconds?: number, + /** + * The maximum number of attempts to retry the check. Value must be between 1 and 10. + */ + maxRetries?: number, + /** + * The total amount of time to continue retrying the check (maximum 600 seconds). + */ + maxDurationSeconds?: number, + /** + * Whether retries should be run in the same region as the initial check run. + */ + sameRegion?: boolean, +} + +export type RetryStrategyOptions = Pick + +export class RetryStrategyBuilder { + private static readonly DEFAULT_BASE_BACKOFF_SECONDS = 60 + private static readonly DEFAULT_MAX_RETRIES = 2 + private static readonly DEFAULT_MAX_DURATION_SECONDS = 60 * 10 + private static readonly DEFAULT_SAME_REGION = true + + /** + * Each retry is run with the same backoff between attempts. + */ + static fixedStrategy (options?: RetryStrategyOptions): RetryStrategy { + return RetryStrategyBuilder.retryStrategy('FIXED', options) + } + + /** + * The delay between retries increases linearly + * + * The delay between retries is calculated using `baseBackoffSeconds * attempt`. + * For example, retries will be run with a backoff of 10s, 20s, 30s, and so on. + */ + static linearStrategy (options?: RetryStrategyOptions): RetryStrategy { + return RetryStrategyBuilder.retryStrategy('LINEAR', options) + } + + /** + * The delay between retries increases exponentially + * + * The delay between retries is calculated using `baseBackoffSeconds ^ attempt`. + * For example, retries will be run with a backoff of 10s, 100s, 1000s, and so on. + */ + static exponentialStrategy (options?: RetryStrategyOptions): RetryStrategy { + return RetryStrategyBuilder.retryStrategy('EXPONENTIAL', options) + } + + private static retryStrategy (type: RetryStrategyType, options?: RetryStrategyOptions): RetryStrategy { + return { + type, + baseBackoffSeconds: options?.baseBackoffSeconds ?? RetryStrategyBuilder.DEFAULT_BASE_BACKOFF_SECONDS, + maxRetries: options?.maxRetries ?? RetryStrategyBuilder.DEFAULT_MAX_RETRIES, + maxDurationSeconds: options?.maxDurationSeconds ?? RetryStrategyBuilder.DEFAULT_MAX_DURATION_SECONDS, + sameRegion: options?.sameRegion ?? RetryStrategyBuilder.DEFAULT_SAME_REGION, + } + } +} diff --git a/packages/cli/src/reporters/abstract-list.ts b/packages/cli/src/reporters/abstract-list.ts index a41e1c22..bca22d98 100644 --- a/packages/cli/src/reporters/abstract-list.ts +++ b/packages/cli/src/reporters/abstract-list.ts @@ -1,5 +1,5 @@ -import * as chalk from 'chalk' -import * as indentString from 'indent-string' +import chalk from 'chalk' +import indentString from 'indent-string' import { Reporter } from './reporter' import { CheckStatus, formatCheckTitle, getTestSessionUrl, printLn } from './util' diff --git a/packages/cli/src/reporters/ci.ts b/packages/cli/src/reporters/ci.ts index 46e81fbe..a0b34064 100644 --- a/packages/cli/src/reporters/ci.ts +++ b/packages/cli/src/reporters/ci.ts @@ -1,4 +1,4 @@ -import * as indentString from 'indent-string' +import indentString from 'indent-string' import AbstractListReporter from './abstract-list' import { formatCheckTitle, formatCheckResult, CheckStatus, printLn } from './util' diff --git a/packages/cli/src/reporters/dot.ts b/packages/cli/src/reporters/dot.ts index ce116dff..72b25a7e 100644 --- a/packages/cli/src/reporters/dot.ts +++ b/packages/cli/src/reporters/dot.ts @@ -1,4 +1,4 @@ -import * as chalk from 'chalk' +import chalk from 'chalk' import AbstractListReporter from './abstract-list' import { CheckRunId } from '../services/abstract-check-runner' import { print, printLn } from './util' diff --git a/packages/cli/src/reporters/list.ts b/packages/cli/src/reporters/list.ts index ada01a1e..a4e8eb3b 100644 --- a/packages/cli/src/reporters/list.ts +++ b/packages/cli/src/reporters/list.ts @@ -1,5 +1,5 @@ -import * as indentString from 'indent-string' -import * as chalk from 'chalk' +import indentString from 'indent-string' +import chalk from 'chalk' import AbstractListReporter from './abstract-list' import { CheckRunId } from '../services/abstract-check-runner' diff --git a/packages/cli/src/reporters/util.ts b/packages/cli/src/reporters/util.ts index 3666521e..f57780d5 100644 --- a/packages/cli/src/reporters/util.ts +++ b/packages/cli/src/reporters/util.ts @@ -1,5 +1,5 @@ -import * as chalk from 'chalk' -import * as indentString from 'indent-string' +import chalk from 'chalk' +import indentString from 'indent-string' import { DateTime } from 'luxon' import * as logSymbols from 'log-symbols' diff --git a/packages/cli/src/rest/api.ts b/packages/cli/src/rest/api.ts index 7e49eb52..3f6cd398 100644 --- a/packages/cli/src/rest/api.ts +++ b/packages/cli/src/rest/api.ts @@ -1,4 +1,5 @@ import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios' +import { name as CIname } from 'ci-info' import config from '../services/config' import { assignProxy } from '../services/util' import Accounts from './accounts' @@ -11,6 +12,7 @@ import Locations from './locations' import TestSessions from './test-sessions' import EnvironmentVariables from './environment-variables' import HeartbeatChecks from './heartbeat-checks' +import ChecklyStorage from './checkly-storage' export function getDefaults () { const apiKey = config.getApiKey() @@ -56,6 +58,8 @@ export function requestInterceptor (config: InternalAxiosRequestConfig) { config.headers['x-checkly-account'] = accountId } + config.headers['x-checkly-ci-name'] = CIname + return config } @@ -95,3 +99,4 @@ export const privateLocations = new PrivateLocations(api) export const testSessions = new TestSessions(api) export const environmentVariables = new EnvironmentVariables(api) export const heartbeatCheck = new HeartbeatChecks(api) +export const checklyStorage = new ChecklyStorage(api) diff --git a/packages/cli/src/rest/checkly-storage.ts b/packages/cli/src/rest/checkly-storage.ts new file mode 100644 index 00000000..114ab3d7 --- /dev/null +++ b/packages/cli/src/rest/checkly-storage.ts @@ -0,0 +1,23 @@ +import type { AxiosInstance } from 'axios' +import type { Readable } from 'node:stream' + +class ChecklyStorage { + api: AxiosInstance + constructor (api: AxiosInstance) { + this.api = api + } + + upload (stream: Readable) { + return this.api.post<{ key: string }>( + '/next/checkly-storage/upload', + stream, + { headers: { 'Content-Type': 'application/octet-stream' } }, + ) + } + + download (key: string) { + return this.api.post('/next/checkly-storage/download', { key }, { responseType: 'stream' }) + } +} + +export default ChecklyStorage diff --git a/packages/cli/src/rest/runtimes.ts b/packages/cli/src/rest/runtimes.ts index ad4e0a1c..8330f41e 100644 --- a/packages/cli/src/rest/runtimes.ts +++ b/packages/cli/src/rest/runtimes.ts @@ -6,6 +6,7 @@ export interface Runtime { runtimeEndOfLife?: string description?: string dependencies: Record + multiStepSupport?: boolean } class Runtimes { diff --git a/packages/cli/src/services/abstract-check-runner.ts b/packages/cli/src/services/abstract-check-runner.ts index 3d3568cd..24777548 100644 --- a/packages/cli/src/services/abstract-check-runner.ts +++ b/packages/cli/src/services/abstract-check-runner.ts @@ -141,20 +141,8 @@ export default abstract class AbstractCheckRunner extends EventEmitter { } else if (subtopic === 'run-end') { this.disableTimeout(checkRunId) const { result } = message - const { - region, - logPath, - checkRunDataPath, - } = result.assets - if (logPath && (this.verbose || result.hasFailures)) { - result.logs = await assets.getLogs(region, logPath) - } - if (checkRunDataPath && (this.verbose || result.hasFailures)) { - result.checkRunData = await assets.getCheckRunData(region, checkRunDataPath) - } - + await this.processCheckResult(result) const links = testResultId && result.hasFailures && await this.getShortLinks(testResultId) - this.emit(Events.CHECK_SUCCESSFUL, checkRunId, check, result, links) this.emit(Events.CHECK_FINISHED, check) } else if (subtopic === 'error') { @@ -164,6 +152,20 @@ export default abstract class AbstractCheckRunner extends EventEmitter { } } + async processCheckResult (result: any) { + const { + region, + logPath, + checkRunDataPath, + } = result.assets + if (logPath && (this.verbose || result.hasFailures)) { + result.logs = await assets.getLogs(region, logPath) + } + if (checkRunDataPath && (this.verbose || result.hasFailures)) { + result.checkRunData = await assets.getCheckRunData(region, checkRunDataPath) + } + } + private allChecksFinished (): Promise { let finishedCheckCount = 0 const numChecks = this.checks.size diff --git a/packages/cli/src/services/checkly-config-loader.ts b/packages/cli/src/services/checkly-config-loader.ts index 9ef0ea3d..eafc2cd0 100644 --- a/packages/cli/src/services/checkly-config-loader.ts +++ b/packages/cli/src/services/checkly-config-loader.ts @@ -9,7 +9,7 @@ import { ReporterType } from '../reporters/reporter' export type CheckConfigDefaults = Pick + | 'alertChannels' | 'privateLocations' | 'retryStrategy'> export type ChecklyConfig = { /** diff --git a/packages/cli/src/services/project-parser.ts b/packages/cli/src/services/project-parser.ts index ab781b6e..65e9ea73 100644 --- a/packages/cli/src/services/project-parser.ts +++ b/packages/cli/src/services/project-parser.ts @@ -3,7 +3,7 @@ import * as path from 'path' import { loadJsFile, loadTsFile, pathToPosix } from './util' import { Check, BrowserCheck, CheckGroup, Project, Session, - PrivateLocation, PrivateLocationCheckAssignment, PrivateLocationGroupAssignment, + PrivateLocation, PrivateLocationCheckAssignment, PrivateLocationGroupAssignment, MultiStepCheck, } from '../constructs' import { Ref } from '../constructs/ref' import { CheckConfigDefaults } from './checkly-config-loader' @@ -104,7 +104,7 @@ async function loadAllBrowserChecks ( const checkFiles = await findFilesWithPattern(directory, browserCheckFilePattern, ignorePattern) const preexistingCheckFiles = new Set() Object.values(project.data.check).forEach((check) => { - if (check instanceof BrowserCheck && check.scriptPath) { + if ((check instanceof BrowserCheck || check instanceof MultiStepCheck) && check.scriptPath) { preexistingCheckFiles.add(check.scriptPath) } }) diff --git a/packages/cli/src/services/snapshot-service.ts b/packages/cli/src/services/snapshot-service.ts new file mode 100644 index 00000000..d4ba238c --- /dev/null +++ b/packages/cli/src/services/snapshot-service.ts @@ -0,0 +1,64 @@ +import * as fsAsync from 'node:fs/promises' +import * as fs from 'node:fs' +import * as path from 'node:path' +import * as stream from 'node:stream/promises' + +import { checklyStorage } from '../rest/api' +import { findFilesRecursively, pathToPosix } from './util' + +export interface Snapshot { + key: string, + path: string, +} + +export async function pullSnapshots (basePath: string, snapshots?: Snapshot[] | null) { + if (!snapshots?.length) { + return + } + + try { + for (const snapshot of snapshots) { + const fullPath = path.resolve(basePath, snapshot.path) + if (!fullPath.startsWith(basePath)) { + // The snapshot file should always be within the project, but we validate this just in case. + throw new Error(`Detected invalid snapshot file ${fullPath}`) + } + await fsAsync.mkdir(path.dirname(fullPath), { recursive: true }) + const fileStream = fs.createWriteStream(fullPath) + const { data: contentStream } = await checklyStorage.download(snapshot.key) + contentStream.pipe(fileStream) + await stream.finished(contentStream) + } + } catch (err: any) { + throw new Error(`Error downloading snapshots: ${err.message}`) + } +} + +export function detectSnapshots (projectBasePath: string, scriptFilePath: string) { + // By default, PWT will store snapshots in the `script.spec.js-snapshots` directory. + // Other paths can be configured, though, and we should add support for those as well. + // https://playwright.dev/docs/api/class-testconfig#test-config-snapshot-path-template + const snapshotFiles = findFilesRecursively(`${scriptFilePath}-snapshots`) + return snapshotFiles.map(absolutePath => ({ + absolutePath, + path: pathToPosix(path.relative(projectBasePath, absolutePath)), + })) +} + +export async function uploadSnapshots (rawSnapshots?: Array<{ absolutePath: string, path: string }>) { + if (!rawSnapshots?.length) { + return [] + } + + try { + const snapshots: Array = [] + for (const rawSnapshot of rawSnapshots) { + const snapshotStream = fs.createReadStream(rawSnapshot.absolutePath) + const { data: { key } } = await checklyStorage.upload(snapshotStream) + snapshots.push({ key, path: rawSnapshot.path }) + } + return snapshots + } catch (err: any) { + throw new Error(`Error uploading snapshots: ${err.message}`) + } +} diff --git a/packages/cli/src/services/test-runner.ts b/packages/cli/src/services/test-runner.ts index 0dfd83a7..2e81627b 100644 --- a/packages/cli/src/services/test-runner.ts +++ b/packages/cli/src/services/test-runner.ts @@ -3,6 +3,7 @@ import AbstractCheckRunner, { RunLocation, CheckRunId } from './abstract-check-r import { GitInformation } from './util' import { Check } from '../constructs/check' import { Project } from '../constructs' +import { pullSnapshots } from '../services/snapshot-service' import * as uuid from 'uuid' @@ -13,6 +14,8 @@ export default class TestRunner extends AbstractCheckRunner { shouldRecord: boolean repoInfo: GitInformation | null environment: string | null + updateSnapshots: boolean + baseDirectory: string constructor ( accountId: string, project: Project, @@ -23,6 +26,8 @@ export default class TestRunner extends AbstractCheckRunner { shouldRecord: boolean, repoInfo: GitInformation | null, environment: string | null, + updateSnapshots: boolean, + baseDirectory: string, ) { super(accountId, timeout, verbose) this.project = project @@ -31,6 +36,8 @@ export default class TestRunner extends AbstractCheckRunner { this.shouldRecord = shouldRecord this.repoInfo = repoInfo this.environment = environment + this.updateSnapshots = updateSnapshots + this.baseDirectory = baseDirectory } async scheduleChecks ( @@ -46,7 +53,7 @@ export default class TestRunner extends AbstractCheckRunner { ...check.synthesize(), group: check.groupId ? this.project.data['check-group'][check.groupId.ref].synthesize() : undefined, groupId: undefined, - sourceInfo: { checkRunSuiteId, checkRunId }, + sourceInfo: { checkRunSuiteId, checkRunId, updateSnapshots: this.updateSnapshots }, logicalId: check.logicalId, filePath: check.getSourceFile(), })) @@ -71,4 +78,11 @@ export default class TestRunner extends AbstractCheckRunner { throw new Error(err.response?.data?.message ?? err.message) } } + + async processCheckResult (result: any) { + await super.processCheckResult(result) + if (this.updateSnapshots) { + await pullSnapshots(this.baseDirectory, result.assets?.snapshots) + } + } } diff --git a/packages/cli/src/services/util.ts b/packages/cli/src/services/util.ts index 669bded7..ded7e1b7 100644 --- a/packages/cli/src/services/util.ts +++ b/packages/cli/src/services/util.ts @@ -3,7 +3,7 @@ import * as path from 'path' import * as fs from 'fs/promises' import * as fsSync from 'fs' import { Service } from 'ts-node' -import * as gitRepoInfo from 'git-repo-info' +import gitRepoInfo from 'git-repo-info' import { parse } from 'dotenv' // @ts-ignore import { getProxyForUrl } from 'proxy-from-env' @@ -25,22 +25,33 @@ export interface CiInformation { environment: string | null } -// TODO: Remove this in favor of glob? It's unused. -export async function walkDirectory ( - directory: string, - ignoreDirectories: Set, - callback: (filepath: string) => Promise, -): Promise { - const files = await fs.readdir(directory) - for (const file of files.sort()) { - const filepath = path.join(directory, file) - const stats = await fs.stat(filepath) - if (stats.isDirectory() && !ignoreDirectories.has(file)) { - await walkDirectory(filepath, ignoreDirectories, callback) - } else { - await callback(filepath) +export function findFilesRecursively (directory: string, ignoredPaths: Array = []) { + if (!fsSync.statSync(directory, { throwIfNoEntry: false })?.isDirectory()) { + return [] + } + + const files = [] + const directoriesToVisit = [directory] + const ignoredPathsSet = new Set(ignoredPaths) + while (directoriesToVisit.length > 0) { + const currentDirectory = directoriesToVisit.shift()! + const contents = fsSync.readdirSync(currentDirectory, { withFileTypes: true }) + for (const content of contents) { + if (content.isSymbolicLink()) { + continue + } + const fullPath = path.resolve(currentDirectory, content.name) + if (ignoredPathsSet.has(fullPath)) { + continue + } + if (content.isDirectory()) { + directoriesToVisit.push(fullPath) + } else { + files.push(fullPath) + } } } + return files } export async function loadJsFile (filepath: string): Promise { @@ -66,7 +77,7 @@ export async function loadTsFile (filepath: string): Promise { try { const tsCompiler = await getTsCompiler() tsCompiler.enabled(true) - let { default: exported } = await import(filepath) + let { default: exported } = await require(filepath) if (exported instanceof Function) { exported = await exported() } diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 3bb342bc..56a143b3 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -3,7 +3,8 @@ "incremental": true, "declaration": true, "importHelpers": false, - "module": "commonjs", + "module": "node16", + "esModuleInterop": true, "outDir": "dist", "rootDirs": ["src", "e2e"], "strict": true,