diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7c8eb779..a384bd792 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-latest, windows-latest-x64] name: test - ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: diff --git a/package-lock.json b/package-lock.json index ebd7deb65..066159953 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3299,6 +3299,29 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "dev": true, @@ -12016,6 +12039,16 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/yaml": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", @@ -12168,6 +12201,7 @@ "chalk": "^4.1.2", "ci-info": "^4.3.1", "conf": "^10.2.0", + "debug": "^4.4.3", "dotenv": "^16.5.0", "execa": "^9.6.1", "git-repo-info": "^2.1.1", @@ -12196,6 +12230,7 @@ "@playwright/test": "^1.57.0", "@types/archiver": "6.0.3", "@types/config": "^3.3.5", + "@types/debug": "^4.1.12", "@types/glob": "^8.1.0", "@types/luxon": "^3.7.1", "@types/node": "^22.14.1", @@ -12211,6 +12246,7 @@ "oclif": "^4.22.56", "rimraf": "^5.0.10", "simple-git-hooks": "^2.12.1", + "tar": "^7.5.7", "ts-node": "^10.9.2", "typescript": "^5.3.3", "vitest": "3.1.2" @@ -12317,6 +12353,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "packages/cli/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "packages/cli/node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", @@ -12407,6 +12453,29 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/cli/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "packages/cli/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "packages/cli/node_modules/npm-run-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", @@ -12459,6 +12528,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/cli/node_modules/tar": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "packages/create-cli": { "name": "create-checkly", "version": "0.0.1-dev", @@ -14774,6 +14860,23 @@ } } }, + "@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "requires": { + "minipass": "^7.0.4" + }, + "dependencies": { + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + } + } + }, "@jridgewell/resolve-uri": { "version": "3.1.0", "dev": true @@ -16534,6 +16637,7 @@ "@playwright/test": "^1.57.0", "@types/archiver": "6.0.3", "@types/config": "^3.3.5", + "@types/debug": "^4.1.12", "@types/glob": "^8.1.0", "@types/luxon": "^3.7.1", "@types/node": "^22.14.1", @@ -16552,6 +16656,7 @@ "conf": "^10.2.0", "config": "^3.3.12", "cross-env": "^7.0.3", + "debug": "^4.4.3", "dotenv": "^16.5.0", "execa": "^9.6.1", "git-repo-info": "^2.1.1", @@ -16575,6 +16680,7 @@ "rimraf": "^5.0.10", "semver": "^7.7.3", "simple-git-hooks": "^2.12.1", + "tar": "^7.5.7", "ts-node": "^10.9.2", "tunnel": "^0.0.6", "typescript": "^5.3.3", @@ -16636,6 +16742,12 @@ "supports-color": "^7.1.0" } }, + "chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true + }, "eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", @@ -16687,6 +16799,21 @@ "brace-expansion": "^2.0.1" } }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + }, + "minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "requires": { + "minipass": "^7.1.2" + } + }, "npm-run-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", @@ -16710,6 +16837,19 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==" + }, + "tar": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "dev": true, + "requires": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + } } } }, @@ -20674,6 +20814,12 @@ "version": "5.0.8", "dev": true }, + "yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true + }, "yaml": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", diff --git a/packages/cli/assets/runtimes/snapshots/20260130/runtimes.json b/packages/cli/assets/runtimes/snapshots/20260130/runtimes.json new file mode 100644 index 000000000..c611a9d69 --- /dev/null +++ b/packages/cli/assets/runtimes/snapshots/20260130/runtimes.json @@ -0,0 +1,349 @@ +[ + { + "name": "2025.04", + "stage": "CURRENT", + "multiStepSupport": true, + "nodeJsVersion": "v22.11.0", + "description": "The main update is Playwright 1.51.1. The Node.js version is v22.11.0.", + "dependencies": { + "@playwright/test": "1.51.1", + "@axe-core/playwright": "4.10.1", + "@azure/identity": "4.9.1", + "@azure/keyvault-secrets": "4.9.0", + "@checkly/playwright-helpers": "1.0.3", + "@faker-js/faker": "9.7.0", + "@google-cloud/local-auth": "3.0.1", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "@t3-oss/env-nextjs": "0.11.1", + "@xmldom/xmldom": "0.9.2", + "aws4": "1.13.2", + "axios": "0.28.0", + "btoa": "1.2.1", + "http": "22.11.0", + "https": "22.11.0", + "crypto-js": "4.2.0", + "date-fns": "3.3.1", + "date-fns-tz": "3.1.3", + "dotenv": "16.4.5", + "ethers": "6.13.2", + "expect": "29.7.0", + "form-data": "4.0.4", + "gmail-api-parse-message-ts": "2.2.33", + "google-auth-library": "9.14.1", + "googleapis": "144.0.0", + "graphql": "16.9.0", + "graphql-tag": "2.12.6", + "jose": "5.9.2", + "jsdom": "25.0.0", + "jsonwebtoken": "9.0.2", + "lodash": "4.17.21", + "long": "5.2.3", + "moment": "2.30.1", + "nice-grpc": "2.1.12", + "nice-grpc-client-middleware-deadline": "2.0.15", + "nice-grpc-client-middleware-devtools": "1.0.7", + "nice-grpc-client-middleware-retry": "3.1.11", + "nice-grpc-common": "2.0.2", + "nice-grpc-error-details": "0.2.9", + "nice-grpc-opentelemetry": "0.1.18", + "nice-grpc-prometheus": "0.2.7", + "nice-grpc-server-health": "2.0.14", + "nice-grpc-server-middleware-terminator": "2.0.14", + "nice-grpc-server-reflection": "2.0.14", + "nice-grpc-web": "3.3.7", + "node-pop3": "0.9.1", + "otpauth": "9.4.0", + "playwright": "1.51.1", + "pdf2json": "3.1.4", + "prisma": "6.6.0", + "protobufjs": "7.5.0", + "tedious": "18.6.1", + "twilio": "5.3.0", + "uuid": "11.1.0", + "ws": "8.18.1", + "xml-crypto": "6.1.1", + "xml-encryption": "3.1.0", + "zod": "3.24.3", + "@clerk/testing": "1.5.1", + "mailosaur": "8.6.1", + "gaxios": "6.7.1", + "@kubernetes/client-node": "1.1.2", + "mysql": "2.18.1" + } + }, + { + "name": "2024.09", + "stage": "STABLE", + "multiStepSupport": true, + "nodeJsVersion": "v18.20.3", + "description": "The main update is Playwright 1.48.2. The Node.js version is v18.20.3. New dependencies are `@clerk/testing`, `mailosaur`, `gaxios`, `@kubernetes/client-node` and `mysql`.", + "dependencies": { + "@playwright/test": "1.48.2", + "@axe-core/playwright": "4.10.0", + "@azure/identity": "4.4.1", + "@azure/keyvault-secrets": "4.8.0", + "@checkly/playwright-helpers": "1.0.3", + "@faker-js/faker": "9.0.1", + "@google-cloud/local-auth": "3.0.1", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.53.0", + "@opentelemetry/sdk-metrics": "1.26.0", + "@opentelemetry/sdk-trace-base": "1.26.0", + "@t3-oss/env-nextjs": "0.11.1", + "@xmldom/xmldom": "0.9.2", + "aws4": "1.13.2", + "axios": "0.28.0", + "btoa": "1.2.1", + "http": "18.20.3", + "https": "18.20.3", + "crypto-js": "4.2.0", + "date-fns": "3.3.1", + "date-fns-tz": "3.1.3", + "dotenv": "16.4.5", + "ethers": "6.13.2", + "expect": "29.7.0", + "form-data": "4.0.4", + "gmail-api-parse-message-ts": "2.2.33", + "google-auth-library": "9.14.1", + "googleapis": "144.0.0", + "graphql": "16.9.0", + "graphql-tag": "2.12.6", + "jose": "5.9.2", + "jsdom": "25.0.0", + "jsonwebtoken": "9.0.2", + "lodash": "4.17.21", + "long": "5.2.3", + "moment": "2.30.1", + "nice-grpc": "2.1.9", + "nice-grpc-client-middleware-deadline": "2.0.12", + "nice-grpc-client-middleware-devtools": "1.0.4", + "nice-grpc-client-middleware-retry": "3.1.8", + "nice-grpc-common": "2.0.2", + "nice-grpc-error-details": "0.2.6", + "nice-grpc-opentelemetry": "0.1.15", + "nice-grpc-prometheus": "0.2.4", + "nice-grpc-server-health": "2.0.11", + "nice-grpc-server-middleware-terminator": "2.0.11", + "nice-grpc-server-reflection": "2.0.11", + "nice-grpc-web": "3.3.4", + "otpauth": "9.3.2", + "playwright": "1.48.2", + "pdf2json": "3.1.4", + "prisma": "5.19.1", + "protobufjs": "7.4.0", + "tedious": "18.6.1", + "twilio": "5.3.0", + "uuid": "10.0.0", + "ws": "8.18.0", + "xml-crypto": "4.0.1", + "xml-encryption": "3.0.2", + "zod": "3.23.8", + "@clerk/testing": "1.3.0", + "mailosaur": "8.6.1", + "gaxios": "6.7.1", + "@kubernetes/client-node": "0.22.3", + "mysql": "2.18.1" + } + }, + { + "name": "2024.02", + "stage": "STABLE", + "multiStepSupport": true, + "nodeJsVersion": "v18.20.3", + "description": "The main update is Playwright 1.42.1. The Node.js version is v18.20.3. New dependencies are `@opentelemetry/exporter-metrics-otlp-grpc`, `@opentelemetry/sdk-metrics`, `chai-json-schema`, `pdf2json`, `protobufjs`, `long` and the `nice-grpc` family.", + "dependencies": { + "@axe-core/playwright": "4.8.5", + "@checkly/playwright-helpers": "1.0.2", + "@faker-js/faker": "8.4.1", + "@google-cloud/local-auth": "3.0.1", + "@opentelemetry/api": "1.7.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "0.48.0", + "@opentelemetry/sdk-metrics": "1.22.0", + "@opentelemetry/sdk-trace-base": "1.22.0", + "@playwright/test": "1.42.1", + "@t3-oss/env-nextjs": "0.9.2", + "@xmldom/xmldom": "0.8.10", + "aws4": "1.12.0", + "axios": "0.28.0", + "btoa": "1.2.1", + "http": "18.20.3", + "https": "18.20.3", + "chai": "4.4.1", + "chai-json-schema": "1.5.1", + "chai-string": "1.5.0", + "crypto-js": "4.2.0", + "date-fns": "3.3.1", + "date-fns-tz": "3.1.3", + "dotenv": "16.4.5", + "ethers": "6.11.1", + "expect": "29.7.0", + "form-data": "4.0.0", + "gmail-api-parse-message-ts": "2.2.32", + "google-auth-library": "9.6.3", + "googleapis": "133.0.0", + "graphql": "16.9.0", + "graphql-tag": "2.12.6", + "jose": "5.2.2", + "jsdom": "24.0.0", + "jsonwebtoken": "9.0.2", + "lodash": "4.17.21", + "long": "5.2.3", + "moment": "2.30.1", + "nice-grpc": "2.1.8", + "nice-grpc-client-middleware-deadline": "2.0.11", + "nice-grpc-client-middleware-devtools": "1.0.3", + "nice-grpc-client-middleware-retry": "3.1.7", + "nice-grpc-common": "2.0.2", + "nice-grpc-error-details": "0.2.5", + "nice-grpc-opentelemetry": "0.1.14", + "nice-grpc-prometheus": "0.2.3", + "nice-grpc-server-health": "2.0.10", + "nice-grpc-server-middleware-terminator": "2.0.10", + "nice-grpc-server-reflection": "2.0.10", + "nice-grpc-web": "3.3.3", + "otpauth": "9.2.2", + "pdf2json": "3.0.5", + "playwright": "1.42.1", + "prisma": "5.10.2", + "protobufjs": "7.2.6", + "protobufjs/minimal": "7.2.6", + "protobufjs/light": "7.2.6", + "twilio": "4.23.0", + "uuid": "9.0.1", + "ws": "8.16.0", + "xml-crypto": "4.0.1", + "xml-encryption": "3.0.2", + "zod": "3.22.4", + "@azure/identity": "4.3.0", + "@azure/keyvault-secrets": "4.8.0", + "tedious": "18.2.0", + "allure-playwright": "latest", + "@dvag/playwright-utils": "latest" + } + }, + { + "name": "2023.09", + "stage": "STABLE", + "multiStepSupport": true, + "nodeJsVersion": "v18.20.3", + "description": "The main updates are Playwright 1.38.1 and the addition of ethers 6.7.1, prisma 5.1.1, zod 3.22.2, @t3-oss/env-nextjs 0.6.1 and @xmldom/xmldom 0.8.10. The Node.js version is v18.20.3.", + "dependencies": { + "@checkly/playwright-helpers": "1.0.2", + "@faker-js/faker": "8.0.2", + "@google-cloud/local-auth": "3.0.0", + "@opentelemetry/api": "1.4.1", + "@opentelemetry/sdk-trace-base": "1.15.2", + "@playwright/test": "1.38.1", + "@t3-oss/env-nextjs": "0.6.1", + "@xmldom/xmldom": "0.8.10", + "aws4": "1.12.0", + "axios": "0.27.2", + "btoa": "1.2.1", + "http": "18.20.3", + "https": "18.20.3", + "chai": "4.3.7", + "chai-string": "1.5.0", + "crypto-js": "4.1.1", + "date-fns": "2.30.0", + "date-fns-tz": "2.0.0", + "dotenv": "16.3.1", + "ethers": "6.7.1", + "expect": "29.6.2", + "form-data": "4.0.0", + "gmail-api-parse-message-ts": "2.2.32", + "google-auth-library": "9.0.0", + "googleapis": "126.0.0", + "jose": "4.14.4", + "jsdom": "22.1.0", + "jsonwebtoken": "9.0.1", + "lodash": "4.17.21", + "moment": "2.29.4", + "otpauth": "9.1.4", + "playwright": "1.38.1", + "prisma": "5.1.1", + "twilio": "4.15.0", + "uuid": "9.0.0", + "ws": "8.13.0", + "xml-crypto": "4.1.0", + "xml-encryption": "3.0.2", + "zod": "3.22.2" + } + }, + { + "name": "2023.02", + "stage": "STABLE", + "multiStepSupport": false, + "nodeJsVersion": "v16.20.2", + "description": "The main updates are Playwright 1.32.1, faker 7.6.0 and the addition of date-fns 2.29.3 and ws 8.13.0. We are dropping support for Mocha.", + "dependencies": { + "@faker-js/faker": "7.6.0", + "@google-cloud/local-auth": "2.1.1", + "@opentelemetry/api": "1.0.4", + "@opentelemetry/sdk-trace-base": "1.0.1", + "@playwright/test": "1.32.3", + "@xmldom/xmldom": "0.8.10", + "aws4": "1.11.0", + "axios": "0.27.2", + "btoa": "1.2.1", + "http": "16.20.2", + "https": "16.20.2", + "chai": "4.3.7", + "chai-string": "1.5.0", + "crypto-js": "4.1.1", + "date-fns": "2.29.3", + "date-fns-tz": "2.0.0", + "expect": "29.3.1", + "form-data": "4.0.0", + "gmail-api-parse-message-ts": "2.2.32", + "google-auth-library": "8.8.0", + "googleapis": "118.0.0", + "jose": "4.14.1", + "jsdom": "21.1.2", + "jsonwebtoken": "9.0.0", + "lodash": "4.17.21", + "moment": "2.29.2", + "otpauth": "9.0.2", + "twilio": "4.11.1", + "playwright": "1.32.3", + "typescript": "4.8.4", + "uuid": "9.0.0", + "ws": "8.13.0", + "xml-crypto": "4.0.1", + "xml-encryption": "3.0.2" + } + }, + { + "name": "2022.10", + "stage": "STABLE", + "multiStepSupport": false, + "nodeJsVersion": "v16.20.2", + "description": "The main updates are Playwright 1.28.0, Node.js v16.20.2 and Typescript support. We are dropping support for Puppeteer.", + "dependencies": { + "@faker-js/faker": "5.5.3", + "@opentelemetry/api": "1.0.4", + "@opentelemetry/sdk-trace-base": "1.0.1", + "@playwright/test": "1.28.0", + "aws4": "1.11.0", + "axios": "0.27.2", + "btoa": "1.2.1", + "http": "16.20.2", + "https": "16.20.2", + "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", + "otpauth": "9.0.2", + "playwright": "1.28.0", + "typescript": "4.8.4", + "uuid": "9.0.0" + } + } +] diff --git a/packages/cli/e2e/__tests__/deploy.spec.ts b/packages/cli/e2e/__tests__/deploy.spec.ts index f3585d34d..70a65f61d 100644 --- a/packages/cli/e2e/__tests__/deploy.spec.ts +++ b/packages/cli/e2e/__tests__/deploy.spec.ts @@ -8,8 +8,9 @@ import { DateTime, Duration } from 'luxon' import { describe, it, expect, beforeAll, beforeEach, afterAll, afterEach } from 'vitest' import Projects from '../../src/rest/projects' -import { runChecklyCli } from '../run-checkly' import CheckTypes from '../../src/constants' +import { FixtureSandbox, RunOptions } from '../../src/testing/fixture-sandbox' +import { ExecaError } from 'execa' async function cleanupProjects (projectLogicalId?: string) { const baseURL: string = config.get('baseURL') @@ -75,7 +76,27 @@ async function getAllResources (type: 'checks' | 'check-groups' | 'private-locat return entries } -describe('deploy', () => { +async function runDeploy (fixt: FixtureSandbox, args: string[], options?: RunOptions) { + const result = await fixt.run('npx', [ + 'checkly', + 'deploy', + ...args, + ], { + timeout: 120_000, + ...options, + }) + + if (result.exitCode !== 0) { + console.error('stderr', result.stderr) + console.error('stdout', result.stdout) + } + + expect(result.exitCode).toBe(0) + + return result +} + +describe('deploy', { timeout: 45_000 }, () => { // Create a unique ID suffix to support parallel test executions let projectLogicalId: string let privateLocationSlugname: string @@ -94,164 +115,93 @@ describe('deploy', () => { afterEach(() => cleanupProjects(projectLogicalId)) afterAll(() => cleanupProjects()) - it('Simple project should deploy successfully (version v4.0.8)', async () => { - const { status, stdout, stderr } = await runChecklyCli({ - args: ['deploy', '--force'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'deploy-project'), - env: { PROJECT_LOGICAL_ID: projectLogicalId, PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname }, - cliVersion: '4.0.8', - }) - expect(stderr).toBe('') - // expect not to change version since the version is specified - expect(stdout).not.toContain('Notice: replacing version') - expect(status).toBe(0) - - const checks = await getAllResources('checks') - const checkGroups = await getAllResources('check-groups') - const privateLocations = await getAllResources('private-locations') - - // Check that all assignments were applied - // Filter out heartbeat checks as they don't have the privateLocations property - expect(checks.filter(({ checkType }: { checkType: string }) => checkType !== CheckTypes.HEARTBEAT) - .filter(({ privateLocations }: { privateLocations: string[] }) => - privateLocations.some(slugName => slugName.startsWith(privateLocationSlugname))).length).toEqual(1) - expect(checkGroups.filter(({ privateLocations }: { privateLocations: string[] }) => - privateLocations.some(slugName => slugName.startsWith(privateLocationSlugname))).length).toEqual(2) - expect(privateLocations - .filter(({ slugName }: { slugName: string }) => slugName.startsWith(privateLocationSlugname)).length).toEqual(1) - }) + describe('deploy-project', () => { + let fixt: FixtureSandbox - it('Simple project should deploy successfully', async () => { - const { status, stdout, stderr } = await runChecklyCli({ - args: ['deploy', '--force'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'deploy-project'), - env: { - PROJECT_LOGICAL_ID: projectLogicalId, - PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname, - CHECKLY_CLI_VERSION: undefined, - }, - }) - expect(stderr).toBe('') - // expect the version to be overriden with latest from NPM - expect(stdout).toContain(`Notice: replacing version '0.0.1-dev' with latest '${latestVersion}'`) - expect(status).toBe(0) - - const checks = await getAllResources('checks') - const checkGroups = await getAllResources('check-groups') - const privateLocations = await getAllResources('private-locations') - - // Check that all assignments were applied - // Filter out heartbeat checks as they don't have the privateLocations property - expect(checks.filter(({ checkType }: { checkType: string }) => checkType !== CheckTypes.HEARTBEAT) - .filter(({ privateLocations }: { privateLocations: string[] }) => - privateLocations.some(slugName => slugName.startsWith(privateLocationSlugname))).length).toEqual(1) - expect(checkGroups.filter(({ privateLocations }: { privateLocations: string[] }) => - privateLocations.some(slugName => slugName.startsWith(privateLocationSlugname))).length).toEqual(2) - expect(privateLocations - .filter(({ slugName }: { slugName: string }) => slugName.startsWith(privateLocationSlugname)).length).toEqual(1) - }) + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'deploy-project'), + }) + }, 180_000) - it('Simple esm project should deploy successfully', async () => { - const { status, stderr } = await runChecklyCli({ - args: ['deploy', '--force'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'deploy-esm-project'), - env: { PROJECT_LOGICAL_ID: projectLogicalId, PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname }, + afterAll(async () => { + await fixt?.destroy() }) - expect(stderr).toBe('') - expect(status).toBe(0) - }) - it('Should mark testOnly check as skipped', async () => { - const { status, stdout } = await runChecklyCli({ - args: ['deploy', '--preview'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'test-only-project'), - env: { - PROJECT_LOGICAL_ID: projectLogicalId, - PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname, - TEST_ONLY: 'true', - }, - }) - expect(stdout).toContain( - `Create: - ApiCheck: not-testonly-default-check - ApiCheck: not-testonly-false-check + it('Simple project should deploy successfully (version v4.0.8)', async () => { + const { stderr, stdout } = await runDeploy(fixt, ['--force'], { + env: { + PROJECT_LOGICAL_ID: projectLogicalId, + PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname, + CHECKLY_CLI_VERSION: '4.0.8', + }, + }) -Skip (testOnly): - ApiCheck: testonly-true-check -`) - expect(status).toBe(0) - }) + expect(stderr).toBe('') + // expect not to change version since the version is specified + expect(stdout).not.toContain('Notice: replacing version') - it('Should mark testOnly check as deleted if there is a deletion', async () => { - // Deploy a check (testOnly=false) - await runChecklyCli({ - args: ['deploy', '--force'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'test-only-project'), - env: { TEST_ONLY: 'false', PROJECT_LOGICAL_ID: projectLogicalId, PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname }, - }) - // Deploy a check (testOnly=true) - const { status, stdout } = await runChecklyCli({ - args: ['deploy', '--force', '--output'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'test-only-project'), - env: { TEST_ONLY: 'true', PROJECT_LOGICAL_ID: projectLogicalId, PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname }, + const checks = await getAllResources('checks') + const checkGroups = await getAllResources('check-groups') + const privateLocations = await getAllResources('private-locations') + + // Check that all assignments were applied + // Filter out heartbeat checks as they don't have the privateLocations property + expect(checks.filter(({ checkType }: { checkType: string }) => checkType !== CheckTypes.HEARTBEAT) + .filter(({ privateLocations }: { privateLocations: string[] }) => + privateLocations.some(slugName => slugName.startsWith(privateLocationSlugname))).length).toEqual(1) + expect(checkGroups.filter(({ privateLocations }: { privateLocations: string[] }) => + privateLocations.some(slugName => slugName.startsWith(privateLocationSlugname))).length).toEqual(2) + expect(privateLocations + .filter(({ slugName }: { slugName: string }) => slugName.startsWith(privateLocationSlugname)).length).toEqual(1) }) - // Moving the check to testOnly causes it to be deleted. - // The check should only be listed under "Delete" and not "Skip". - expect(stdout).toContain( - `Delete: - Check: testonly-true-check -Update and Unchanged: - ApiCheck: not-testonly-default-check - ApiCheck: not-testonly-false-check`) - expect(status).toBe(0) - }) + it('Simple project should deploy successfully', async () => { + const { stderr, stdout } = await runDeploy(fixt, ['--force'], { + env: { + PROJECT_LOGICAL_ID: projectLogicalId, + PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname, + CHECKLY_CLI_VERSION: undefined, + }, + }) + expect(stderr).toBe('') + // expect the version to be overriden with latest from NPM + expect(stdout).toContain(`Notice: replacing version '0.0.1-dev' with latest '${latestVersion}'`) - it('Should deploy with different config file', async () => { - const resultOne = await runChecklyCli({ - args: ['deploy', '--preview'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'deploy-project'), - env: { PROJECT_LOGICAL_ID: projectLogicalId, PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname }, - timeout: 10000, - }) - const resultTwo = await runChecklyCli({ - args: ['deploy', '--preview', '--config', 'checkly.staging.config.ts'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'deploy-project'), - env: { PROJECT_LOGICAL_ID: projectLogicalId, PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname }, - timeout: 10000, + const checks = await getAllResources('checks') + const checkGroups = await getAllResources('check-groups') + const privateLocations = await getAllResources('private-locations') + + // Check that all assignments were applied + // Filter out heartbeat checks as they don't have the privateLocations property + expect(checks.filter(({ checkType }: { checkType: string }) => checkType !== CheckTypes.HEARTBEAT) + .filter(({ privateLocations }: { privateLocations: string[] }) => + privateLocations.some(slugName => slugName.startsWith(privateLocationSlugname))).length).toEqual(1) + expect(checkGroups.filter(({ privateLocations }: { privateLocations: string[] }) => + privateLocations.some(slugName => slugName.startsWith(privateLocationSlugname))).length).toEqual(2) + expect(privateLocations + .filter(({ slugName }: { slugName: string }) => slugName.startsWith(privateLocationSlugname)).length).toEqual(1) }) - if (resultOne.status !== 0) { - console.group('resultOne') - console.warn(`stdout=${resultOne.stdout}`) - console.warn(`stderr=${resultOne.stderr}`) - console.groupEnd() - } - expect(resultOne.status).toBe(0) - if (resultOne.status !== 0) { - console.group('resultTwo') - console.warn(`stdout=${resultTwo.stdout}`) - console.warn(`stderr=${resultTwo.stderr}`) - console.groupEnd() - } - expect(resultTwo.status).toBe(0) - expect(resultOne.stdout).toContain( - `Create: + + it('Should deploy with different config file', async () => { + const resultOne = await runDeploy(fixt, ['--preview'], { + env: { + PROJECT_LOGICAL_ID: projectLogicalId, + PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname, + CHECKLY_CLI_VERSION: '4.8.0', + }, + timeout: 10000, + }) + const resultTwo = await runDeploy(fixt, ['--preview', '--config', 'checkly.staging.config.ts'], { + env: { + PROJECT_LOGICAL_ID: projectLogicalId, + PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname, + CHECKLY_CLI_VERSION: '4.8.0', + }, + timeout: 10000, + }) + expect(resultOne.stdout).toContain( + `Create: ApiCheck: api-check ApiCheck: api-check-high-freq ApiCheck: api-check-incident-trigger @@ -271,8 +221,8 @@ Update and Unchanged: StatusPageService: bar-service StatusPageService: foo-service `) - expect(resultTwo.stdout).toContain( - `Create: + expect(resultTwo.stdout).toContain( + `Create: ApiCheck: api-check ApiCheck: api-check-high-freq ApiCheck: api-check-incident-trigger @@ -290,29 +240,152 @@ Update and Unchanged: MaintenanceWindow: maintenance-window-1 PrivateLocation: private-location-1 `) + }) + }) + + describe('deploy-esm-project', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'deploy-esm-project'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + + it('Simple esm project should deploy successfully', async () => { + const { stderr } = await runDeploy(fixt, ['--force'], { + env: { + PROJECT_LOGICAL_ID: projectLogicalId, + PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname, + CHECKLY_CLI_VERSION: '4.8.0', + }, + }) + + expect(stderr).toBe('') + }) + }) + + describe('test-only-project', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'test-only-project'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + + it('Should mark testOnly check as skipped', async () => { + const { stdout } = await runDeploy(fixt, ['--preview'], { + env: { + PROJECT_LOGICAL_ID: projectLogicalId, + PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname, + TEST_ONLY: 'true', + CHECKLY_CLI_VERSION: '4.8.0', + }, + }) + expect(stdout).toContain( + `Create: + ApiCheck: not-testonly-default-check + ApiCheck: not-testonly-false-check + +Skip (testOnly): + ApiCheck: testonly-true-check +`) + }) + + it('Should mark testOnly check as deleted if there is a deletion', async () => { + // Deploy a check (testOnly=false) + await runDeploy(fixt, ['--force'], { + env: { + PROJECT_LOGICAL_ID: projectLogicalId, + PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname, + TEST_ONLY: 'false', + CHECKLY_CLI_VERSION: '4.8.0', + }, + }) + // Deploy a check (testOnly=true) + const { stdout } = await runDeploy(fixt, ['--force', '--output'], { + env: { + PROJECT_LOGICAL_ID: projectLogicalId, + PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname, + TEST_ONLY: 'true', + CHECKLY_CLI_VERSION: '4.8.0', + }, + }) + // Moving the check to testOnly causes it to be deleted. + // The check should only be listed under "Delete" and not "Skip". + expect(stdout).toContain( + `Delete: + Check: testonly-true-check + +Update and Unchanged: + ApiCheck: not-testonly-default-check + ApiCheck: not-testonly-false-check`) + }) }) - it('Should terminate when no resources are found', async () => { - const result = await runChecklyCli({ - args: ['deploy'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'empty-project'), - env: { PROJECT_LOGICAL_ID: projectLogicalId, PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname }, + describe('empty-project', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'empty-project'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + + it('Should terminate when no resources are found', async () => { + expect.assertions(1) + try { + await runDeploy(fixt, [], { + env: { + PROJECT_LOGICAL_ID: projectLogicalId, + PRIVATE_LOCATION_SLUG_NAME: privateLocationSlugname, + CHECKLY_CLI_VERSION: '4.8.0', + }, + }) + } catch (err: any) { + if (err instanceof ExecaError) { + expect(err.stderr).toContain('Failed to deploy your project. Unable to find constructs to deploy.') + } else { + throw err + } + } }) - 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 }, + describe('snapshot-project', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'snapshot-project'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + + it('Should deploy a project with snapshots', async () => { + await runDeploy(fixt, ['--force'], { + env: { + PROJECT_LOGICAL_ID: projectLogicalId, + CHECKLY_CLI_VERSION: '4.8.0', + }, + }) + // TODO: Add assertions that the snapshots are successfully uploaded. }) - expect(result.status).toBe(0) - // TODO: Add assertions that the snapshots are successfully uploaded. }) }) diff --git a/packages/cli/e2e/__tests__/fixtures/deploy-esm-project/package.json b/packages/cli/e2e/__tests__/fixtures/deploy-esm-project/package.json new file mode 100644 index 000000000..4d6e99586 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/deploy-esm-project/package.json @@ -0,0 +1,8 @@ +{ + "name": "project", + "version": "1.0.0", + "dependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/e2e/__tests__/fixtures/deploy-project/package.json b/packages/cli/e2e/__tests__/fixtures/deploy-project/package.json new file mode 100644 index 000000000..4d6e99586 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/deploy-project/package.json @@ -0,0 +1,8 @@ +{ + "name": "project", + "version": "1.0.0", + "dependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/e2e/__tests__/fixtures/empty-project/package.json b/packages/cli/e2e/__tests__/fixtures/empty-project/package.json new file mode 100644 index 000000000..4d6e99586 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/empty-project/package.json @@ -0,0 +1,8 @@ +{ + "name": "project", + "version": "1.0.0", + "dependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/e2e/__tests__/fixtures/esm-module/package.json b/packages/cli/e2e/__tests__/fixtures/esm-module/package.json index 5f9ccd694..4d6e99586 100644 --- a/packages/cli/e2e/__tests__/fixtures/esm-module/package.json +++ b/packages/cli/e2e/__tests__/fixtures/esm-module/package.json @@ -1,12 +1,8 @@ { - "name": "Test ECMAScript Module Project", + "name": "project", "version": "1.0.0", - "description": "", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "devDependencies": {}, - "type": "module" -} \ No newline at end of file + "dependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/e2e/__tests__/fixtures/retry-project/package.json b/packages/cli/e2e/__tests__/fixtures/retry-project/package.json new file mode 100644 index 000000000..4d6e99586 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/retry-project/package.json @@ -0,0 +1,8 @@ +{ + "name": "project", + "version": "1.0.0", + "dependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/e2e/__tests__/fixtures/snapshot-project-missing-snapshots/package.json b/packages/cli/e2e/__tests__/fixtures/snapshot-project-missing-snapshots/package.json new file mode 100644 index 000000000..42375f136 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/snapshot-project-missing-snapshots/package.json @@ -0,0 +1,15 @@ +{ + "name": "snapshot-test", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@playwright/test": "^1.51.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/e2e/__tests__/fixtures/snapshot-project/package.json b/packages/cli/e2e/__tests__/fixtures/snapshot-project/package.json new file mode 100644 index 000000000..58e446843 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/snapshot-project/package.json @@ -0,0 +1,15 @@ +{ + "name": "snapshot-test", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/e2e/__tests__/fixtures/snapshot-project/playwright.config.ts b/packages/cli/e2e/__tests__/fixtures/snapshot-project/playwright.config.ts new file mode 100644 index 000000000..83f578828 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/snapshot-project/playwright.config.ts @@ -0,0 +1,10 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/package.json b/packages/cli/e2e/__tests__/fixtures/test-duplicated-groups/package.json similarity index 60% rename from packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/package.json rename to packages/cli/e2e/__tests__/fixtures/test-duplicated-groups/package.json index f48b51333..30ce59b6f 100644 --- a/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/package.json +++ b/packages/cli/e2e/__tests__/fixtures/test-duplicated-groups/package.json @@ -2,6 +2,7 @@ "name": "playwright-bundle-test", "version": "1.0.0", "dependencies": { - "@playwright/test": "^1.40.0" + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" } } diff --git a/packages/cli/e2e/__tests__/fixtures/test-only-project/package.json b/packages/cli/e2e/__tests__/fixtures/test-only-project/package.json new file mode 100644 index 000000000..4d6e99586 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/test-only-project/package.json @@ -0,0 +1,8 @@ +{ + "name": "project", + "version": "1.0.0", + "dependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/e2e/__tests__/fixtures/test-parse-error/package.json b/packages/cli/e2e/__tests__/fixtures/test-parse-error/package.json new file mode 100644 index 000000000..30ce59b6f --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/test-parse-error/package.json @@ -0,0 +1,8 @@ +{ + "name": "playwright-bundle-test", + "version": "1.0.0", + "dependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/e2e/__tests__/fixtures/test-project/package.json b/packages/cli/e2e/__tests__/fixtures/test-project/package.json new file mode 100644 index 000000000..30ce59b6f --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/test-project/package.json @@ -0,0 +1,8 @@ +{ + "name": "playwright-bundle-test", + "version": "1.0.0", + "dependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/e2e/__tests__/fixtures/test-pwt-native/checkly.config.original.ts b/packages/cli/e2e/__tests__/fixtures/test-pwt-native/checkly.config.original.ts deleted file mode 100644 index 25efa265f..000000000 --- a/packages/cli/e2e/__tests__/fixtures/test-pwt-native/checkly.config.original.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { defineConfig } from 'checkly' - -const config = defineConfig({ - projectName: 'Test Project', - logicalId: 'test-project', - repoUrl: 'https://github.com/checkly/checkly-cli', - checks: { - locations: ['us-east-1', 'eu-west-1'], - tags: ['mac'], - runtimeId: '2022.10', - checkMatch: '**/*.check.ts', - browserChecks: { - // using .test.ts suffix (no .spec.ts) to avoid '@playwright/test not found error' when Jest transpile the spec.ts - testMatch: '**/__checks__/*.test.ts', - }, - }, - cli: { - runLocation: 'us-east-1', - }, -}) - -export default config diff --git a/packages/cli/e2e/__tests__/fixtures/test-pwt-native/package-lock.json b/packages/cli/e2e/__tests__/fixtures/test-pwt-native/package-lock.json deleted file mode 100644 index 35e27a0c8..000000000 --- a/packages/cli/e2e/__tests__/fixtures/test-pwt-native/package-lock.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "name": "test-pwt-native", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "test-pwt-native", - "version": "0.0.1", - "devDependencies": { - "@playwright/test": "1.53.1" - } - }, - "node_modules/@playwright/test": { - "version": "1.53.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz", - "integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==", - "dev": true, - "dependencies": { - "playwright": "1.53.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.53.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz", - "integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==", - "dev": true, - "dependencies": { - "playwright-core": "1.53.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.53.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz", - "integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==", - "dev": true, - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - } - } -} diff --git a/packages/cli/e2e/__tests__/fixtures/test-pwt-native/package.json b/packages/cli/e2e/__tests__/fixtures/test-pwt-native/package.json index 78a102b14..1772945f3 100644 --- a/packages/cli/e2e/__tests__/fixtures/test-pwt-native/package.json +++ b/packages/cli/e2e/__tests__/fixtures/test-pwt-native/package.json @@ -2,6 +2,7 @@ "name": "test-pwt-native", "version": "0.0.1", "devDependencies": { - "@playwright/test": "1.53.1" + "@playwright/test": "1.53.1", + "jiti": "^2.6.1" } } diff --git a/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/package.json b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/package.json new file mode 100644 index 000000000..d589ca764 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/package.json @@ -0,0 +1,13 @@ +{ + "name": "workspace-basic-pnpm", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.28.1" +} diff --git a/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/.gitignore b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/.gitignore new file mode 100644 index 000000000..335bd46df --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/.gitignore @@ -0,0 +1,8 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/checkly.config.ts b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/checkly.config.ts new file mode 100644 index 000000000..b7e754594 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/checkly.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + logicalId: 'my-app', + projectName: 'my-app', + checks: { + playwrightConfigPath: './playwright.config.ts', + playwrightChecks: [ + { + logicalId: 'my-app-tests', + name: 'my-app-tests', + frequency: 10, + locations: [ + 'us-east-1', + ], + installCommand: 'pnpm i', + }, + ], + frequency: 10, + locations: [ + 'us-east-1', + ], + }, + cli: { + runLocation: 'us-east-1', + }, +}) + +export default config diff --git a/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/package.json b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/package.json new file mode 100644 index 000000000..ce16e5d58 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/package.json @@ -0,0 +1,20 @@ +{ + "name": "a", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.28.1", + "dependencies": { + "b": "workspace:*" + }, + "devDependencies": { + "@playwright/test": "^1.57.0", + "@types/node": "^25.0.3", + "checkly": "^6.9.8", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/playwright.config.ts b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/playwright.config.ts new file mode 100644 index 000000000..6dfc0d9bc --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/tests/example.spec.ts b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/tests/example.spec.ts new file mode 100644 index 000000000..54a906a4e --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/a/tests/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); diff --git a/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/b/package.json b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/b/package.json new file mode 100644 index 000000000..b3c4c0019 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/b/package.json @@ -0,0 +1,16 @@ +{ + "name": "b", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.28.1", + "dependencies": { + "c": "workspace:*" + } +} diff --git a/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/c/package.json b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/c/package.json new file mode 100644 index 000000000..d17353963 --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/packages/c/package.json @@ -0,0 +1,13 @@ +{ + "name": "c", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.28.1" +} diff --git a/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/pnpm-lock.yaml b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/pnpm-lock.yaml new file mode 100644 index 000000000..5c43c293d --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/pnpm-lock.yaml @@ -0,0 +1,2569 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + packages/a: + dependencies: + b: + specifier: workspace:* + version: link:../b + devDependencies: + '@playwright/test': + specifier: ^1.57.0 + version: 1.57.0 + '@types/node': + specifier: ^25.0.3 + version: 25.0.3 + checkly: + specifier: ^6.9.8 + version: 6.9.8(@types/node@25.0.3)(jiti@2.6.1)(typescript@5.9.3) + jiti: + specifier: ^2.6.1 + version: 2.6.1 + + packages/b: + dependencies: + c: + specifier: workspace:* + version: link:../c + + packages/c: {} + +packages: + + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@oclif/core@4.8.0': + resolution: {integrity: sha512-jteNUQKgJHLHFbbz806aGZqf+RJJ7t4gwF4MYa8fCwCxQ8/klJNWc0MvaJiBebk7Mc+J39mdlsB4XraaCKznFw==} + engines: {node: '>=18.0.0'} + + '@oclif/plugin-help@6.2.36': + resolution: {integrity: sha512-NBQIg5hEMhvdbi4mSrdqRGl5XJ0bqTAHq6vDCCCDXUcfVtdk3ZJbSxtRVWyVvo9E28vwqu6MZyHOJylevqcHbA==} + engines: {node: '>=18.0.0'} + + '@oclif/plugin-not-found@3.2.73': + resolution: {integrity: sha512-2bQieTGI9XNFe9hKmXQjJmHV5rZw+yn7Rud1+C5uLEo8GaT89KZbiLTJgL35tGILahy/cB6+WAs812wjw7TK6w==} + engines: {node: '>=18.0.0'} + + '@oclif/plugin-plugins@5.4.54': + resolution: {integrity: sha512-yzdukEfvvyXx31AhN+YhxLhuQdx2SrZDcRtPl5CNkuqh/uNSB2BuA3xpurdv2qotpaw/Z9InRl+Sa9bLp/4aLA==} + engines: {node: '>=18.0.0'} + + '@oclif/plugin-warn-if-update-available@3.1.53': + resolution: {integrity: sha512-ALxKMNFFJQJV1Z2OMVTV+q7EbKHhnTAPcTgkgHeXCNdW5nFExoXuwusZLS4Zv2o83j9UoDx1R/CSX7QZVgEHTA==} + engines: {node: '>=18.0.0'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@playwright/test@1.57.0': + resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} + engines: {node: '>=18'} + hasBin: true + + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@2.3.1': + resolution: {integrity: sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==} + engines: {node: '>=12'} + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@types/node@25.0.3': + resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + + '@types/readable-stream@4.0.23': + resolution: {integrity: sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@typescript-eslint/project-service@8.52.0': + resolution: {integrity: sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/tsconfig-utils@8.52.0': + resolution: {integrity: sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.52.0': + resolution: {integrity: sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.52.0': + resolution: {integrity: sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.52.0': + resolution: {integrity: sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@3.17.0: + resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} + engines: {node: '>=14'} + + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atomically@1.7.0: + resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} + engines: {node: '>=10.12.0'} + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + b4a@1.7.3: + resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bare-events@2.8.2: + resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bl@6.1.6: + resolution: {integrity: sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + broker-factory@3.1.11: + resolution: {integrity: sha512-ex4RuEI0AJOdaIcXe1lu9EqRAVkoYvdcvwLvNcE5UZQzYNqzPY+z0frnlxT4+cUwNVpE//9MwGx4lKiLH+pEcw==} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + checkly@6.9.8: + resolution: {integrity: sha512-7CzBfjp7kVx9Rh+K6a8DLUSsIT84yV8uicZYRjxbGsETdpnawYOopt5RkfxCV73eClECAJoGIHlRrMt9dDSb5A==} + engines: {node: ^18.19.0 || >=20.5.0} + hasBin: true + peerDependencies: + jiti: '>=2' + peerDependenciesMeta: + jiti: + optional: true + + ci-info@4.3.1: + resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} + engines: {node: '>=8'} + + clean-stack@3.0.1: + resolution: {integrity: sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==} + engines: {node: '>=10'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commist@3.2.0: + resolution: {integrity: sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==} + + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + conf@10.2.0: + resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==} + engines: {node: '>=12'} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debounce-fn@4.0.0: + resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==} + engines: {node: '>=10'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@9.6.1: + resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} + engines: {node: ^18.19.0 || >=20.5.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-levenshtein@3.0.0: + resolution: {integrity: sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==} + + fast-unique-numbers@9.0.24: + resolution: {integrity: sha512-Dv0BYn4waOWse94j16rsZ5w/0zoaCa74O3q6IZjMqaXbtT92Q+Sb6pPk+phGzD8Xh+nueQmSRI3tSCaHKidzKw==} + engines: {node: '>=18.2.0'} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + git-repo-info@2.1.1: + resolution: {integrity: sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg==} + engines: {node: '>= 4.0'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + hasBin: true + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + + hosted-git-info@7.0.2: + resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} + engines: {node: ^16.14.0 || >=18.0.0} + + http-call@5.3.0: + resolution: {integrity: sha512-ahwimsC23ICE4kPl9xTBjKB4inbRaeLyZeRunC/1Jy/Z6X8tv22MEAjK+KBOMSVLaqXPTTmd8638waVIKLGx2w==} + engines: {node: '>=8.0.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + iconv-lite@0.7.1: + resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-retry-allowed@1.2.0: + resolution: {integrity: sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==} + engines: {node: '>=0.10.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-sdsl@4.3.0: + resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==} + + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@7.0.3: + resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==} + + json-stream-stringify@3.1.6: + resolution: {integrity: sha512-x7fpwxOkbhFCaJDJ8vb1fBY3DdSa4AlITaz+HHILQJzdPMnHEFjxPwVUi1ALIbcIxDE0PNe/0i7frnY8QnBQog==} + engines: {node: '>=7.10.1'} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jwt-decode@3.1.2: + resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@3.1.0: + resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} + engines: {node: '>=8'} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mqtt-packet@9.0.2: + resolution: {integrity: sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==} + + mqtt@5.14.1: + resolution: {integrity: sha512-NxkPxE70Uq3Ph7goefQa7ggSsVzHrayCD0OyxlJgITN/EbzlZN+JEPmaAZdxP1LsIT5FamDyILoQTF72W7Nnbw==} + engines: {node: '>=16.0.0'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-package-arg@11.0.3: + resolution: {integrity: sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==} + engines: {node: ^16.14.0 || >=18.0.0} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + npm@10.9.4: + resolution: {integrity: sha512-OnUG836FwboQIbqtefDNlyR0gTHzIfwRfE3DuiNewBvnMnWEpB0VEXwBlFVgqpNzIgYo/MHh3d2Hel/pszapAA==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + bundledDependencies: + - '@isaacs/string-locale-compare' + - '@npmcli/arborist' + - '@npmcli/config' + - '@npmcli/fs' + - '@npmcli/map-workspaces' + - '@npmcli/package-json' + - '@npmcli/promise-spawn' + - '@npmcli/redact' + - '@npmcli/run-script' + - '@sigstore/tuf' + - abbrev + - archy + - cacache + - chalk + - ci-info + - cli-columns + - fastest-levenshtein + - fs-minipass + - glob + - graceful-fs + - hosted-git-info + - ini + - init-package-json + - is-cidr + - json-parse-even-better-errors + - libnpmaccess + - libnpmdiff + - libnpmexec + - libnpmfund + - libnpmhook + - libnpmorg + - libnpmpack + - libnpmpublish + - libnpmsearch + - libnpmteam + - libnpmversion + - make-fetch-happen + - minimatch + - minipass + - minipass-pipeline + - ms + - node-gyp + - nopt + - normalize-package-data + - npm-audit-report + - npm-install-checks + - npm-package-arg + - npm-pick-manifest + - npm-profile + - npm-registry-fetch + - npm-user-validate + - p-map + - pacote + - parse-conflict-json + - proc-log + - qrcode-terminal + - read + - semver + - spdx-expression-parse + - ssri + - supports-color + - tar + - text-table + - tiny-relative-date + - treeverse + - validate-npm-package-name + - which + - write-file-atomic + + number-allocator@1.0.14: + resolution: {integrity: sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==} + + object-treeify@4.0.1: + resolution: {integrity: sha512-Y6tg5rHfsefSkfKujv2SwHulInROy/rCL5F4w0QOWxut8AnxYxf0YmNhTh95Zfyxpsudo66uqkux0ACFnyMSgQ==} + engines: {node: '>= 16'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + + playwright-core@1.57.0: + resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.57.0: + resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + engines: {node: '>=18'} + hasBin: true + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + proc-log@4.2.0: + resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + recast@0.23.11: + resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} + engines: {node: '>= 4'} + + registry-auth-token@5.1.0: + resolution: {integrity: sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==} + engines: {node: '>=14'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + streamx@2.23.0: + resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + worker-factory@7.0.46: + resolution: {integrity: sha512-Sr1hq2FMgNa04UVhYQacsw+i58BtMimzDb4+CqYphZ97OfefRpURu0UZ+JxMr/H36VVJBfuVkxTK7MytsanC3w==} + + worker-timers-broker@8.0.13: + resolution: {integrity: sha512-PZnHHmqOY5oMKQPyfJhqPI9cb3QFmwD3lCIc/Zip6sShpfG2rvvCVDl0xeabGIspiEpP5exNNIlTUHjgP5VAcg==} + + worker-timers-worker@9.0.11: + resolution: {integrity: sha512-pArb5xtgHWImYpXhjg1OFv7JFG0ubmccb73TFoXHXjG830fFj+16N57q9YeBnZX52dn+itRrMoJZ9HaZBVzDaA==} + + worker-timers@8.0.27: + resolution: {integrity: sha512-+7ptDduAWj6Wd09Ga0weRFRx/MUwLhExazn+zu3IrwF0N2U2FPqFRR5W3Qz4scnI3cOILzdIEEytIJ2vbeD9Gw==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yarn@1.22.22: + resolution: {integrity: sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==} + engines: {node: '>=4.0.0'} + hasBin: true + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + +snapshots: + + '@babel/runtime@7.28.4': {} + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@25.0.3)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@25.0.3) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@25.0.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 25.0.3 + + '@inquirer/confirm@5.1.21(@types/node@25.0.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@25.0.3) + '@inquirer/type': 3.0.10(@types/node@25.0.3) + optionalDependencies: + '@types/node': 25.0.3 + + '@inquirer/core@10.3.2(@types/node@25.0.3)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@25.0.3) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 25.0.3 + + '@inquirer/editor@4.2.23(@types/node@25.0.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@25.0.3) + '@inquirer/external-editor': 1.0.3(@types/node@25.0.3) + '@inquirer/type': 3.0.10(@types/node@25.0.3) + optionalDependencies: + '@types/node': 25.0.3 + + '@inquirer/expand@4.0.23(@types/node@25.0.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@25.0.3) + '@inquirer/type': 3.0.10(@types/node@25.0.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 25.0.3 + + '@inquirer/external-editor@1.0.3(@types/node@25.0.3)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.1 + optionalDependencies: + '@types/node': 25.0.3 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@25.0.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@25.0.3) + '@inquirer/type': 3.0.10(@types/node@25.0.3) + optionalDependencies: + '@types/node': 25.0.3 + + '@inquirer/number@3.0.23(@types/node@25.0.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@25.0.3) + '@inquirer/type': 3.0.10(@types/node@25.0.3) + optionalDependencies: + '@types/node': 25.0.3 + + '@inquirer/password@4.0.23(@types/node@25.0.3)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@25.0.3) + '@inquirer/type': 3.0.10(@types/node@25.0.3) + optionalDependencies: + '@types/node': 25.0.3 + + '@inquirer/prompts@7.10.1(@types/node@25.0.3)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@25.0.3) + '@inquirer/confirm': 5.1.21(@types/node@25.0.3) + '@inquirer/editor': 4.2.23(@types/node@25.0.3) + '@inquirer/expand': 4.0.23(@types/node@25.0.3) + '@inquirer/input': 4.3.1(@types/node@25.0.3) + '@inquirer/number': 3.0.23(@types/node@25.0.3) + '@inquirer/password': 4.0.23(@types/node@25.0.3) + '@inquirer/rawlist': 4.1.11(@types/node@25.0.3) + '@inquirer/search': 3.2.2(@types/node@25.0.3) + '@inquirer/select': 4.4.2(@types/node@25.0.3) + optionalDependencies: + '@types/node': 25.0.3 + + '@inquirer/rawlist@4.1.11(@types/node@25.0.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@25.0.3) + '@inquirer/type': 3.0.10(@types/node@25.0.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 25.0.3 + + '@inquirer/search@3.2.2(@types/node@25.0.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@25.0.3) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@25.0.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 25.0.3 + + '@inquirer/select@4.4.2(@types/node@25.0.3)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@25.0.3) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@25.0.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 25.0.3 + + '@inquirer/type@3.0.10(@types/node@25.0.3)': + optionalDependencies: + '@types/node': 25.0.3 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@oclif/core@4.8.0': + dependencies: + ansi-escapes: 4.3.2 + ansis: 3.17.0 + clean-stack: 3.0.1 + cli-spinners: 2.9.2 + debug: 4.4.3(supports-color@8.1.1) + ejs: 3.1.10 + get-package-type: 0.1.0 + indent-string: 4.0.0 + is-wsl: 2.2.0 + lilconfig: 3.1.3 + minimatch: 9.0.5 + semver: 7.7.3 + string-width: 4.2.3 + supports-color: 8.1.1 + tinyglobby: 0.2.15 + widest-line: 3.1.0 + wordwrap: 1.0.0 + wrap-ansi: 7.0.0 + + '@oclif/plugin-help@6.2.36': + dependencies: + '@oclif/core': 4.8.0 + + '@oclif/plugin-not-found@3.2.73(@types/node@25.0.3)': + dependencies: + '@inquirer/prompts': 7.10.1(@types/node@25.0.3) + '@oclif/core': 4.8.0 + ansis: 3.17.0 + fast-levenshtein: 3.0.0 + transitivePeerDependencies: + - '@types/node' + + '@oclif/plugin-plugins@5.4.54': + dependencies: + '@oclif/core': 4.8.0 + ansis: 3.17.0 + debug: 4.4.3(supports-color@8.1.1) + npm: 10.9.4 + npm-package-arg: 11.0.3 + npm-run-path: 5.3.0 + object-treeify: 4.0.1 + semver: 7.7.3 + validate-npm-package-name: 5.0.1 + which: 4.0.0 + yarn: 1.22.22 + transitivePeerDependencies: + - supports-color + + '@oclif/plugin-warn-if-update-available@3.1.53': + dependencies: + '@oclif/core': 4.8.0 + ansis: 3.17.0 + debug: 4.4.3(supports-color@8.1.1) + http-call: 5.3.0 + lodash: 4.17.21 + registry-auth-token: 5.1.0 + transitivePeerDependencies: + - supports-color + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@playwright/test@1.57.0': + dependencies: + playwright: 1.57.0 + + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/network.ca-file@1.0.2': + dependencies: + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@2.3.1': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@types/node@25.0.3': + dependencies: + undici-types: 7.16.0 + + '@types/readable-stream@4.0.23': + dependencies: + '@types/node': 25.0.3 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.0.3 + + '@typescript-eslint/project-service@8.52.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) + '@typescript-eslint/types': 8.52.0 + debug: 4.4.3(supports-color@8.1.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/tsconfig-utils@8.52.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/types@8.52.0': {} + + '@typescript-eslint/typescript-estree@8.52.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.52.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) + '@typescript-eslint/types': 8.52.0 + '@typescript-eslint/visitor-keys': 8.52.0 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.4.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.52.0': + dependencies: + '@typescript-eslint/types': 8.52.0 + eslint-visitor-keys: 4.2.1 + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv-formats@2.1.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ansis@3.17.0: {} + + archiver-utils@5.0.2: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.17.21 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.1.7 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + async@3.2.6: {} + + asynckit@0.4.0: {} + + atomically@1.7.0: {} + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + b4a@1.7.3: {} + + balanced-match@1.0.2: {} + + bare-events@2.8.2: {} + + base64-js@1.5.1: {} + + bl@6.1.6: + dependencies: + '@types/readable-stream': 4.0.23 + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 4.7.0 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + broker-factory@3.1.11: + dependencies: + '@babel/runtime': 7.28.4 + fast-unique-numbers: 9.0.24 + tslib: 2.8.1 + worker-factory: 7.0.46 + + buffer-crc32@1.0.0: {} + + buffer-from@1.1.2: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chardet@2.1.1: {} + + checkly@6.9.8(@types/node@25.0.3)(jiti@2.6.1)(typescript@5.9.3): + dependencies: + '@oclif/core': 4.8.0 + '@oclif/plugin-help': 6.2.36 + '@oclif/plugin-not-found': 3.2.73(@types/node@25.0.3) + '@oclif/plugin-plugins': 5.4.54 + '@oclif/plugin-warn-if-update-available': 3.1.53 + '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) + acorn: 8.15.0 + acorn-walk: 8.3.4 + archiver: 7.0.1 + axios: 1.13.2 + chalk: 4.1.2 + ci-info: 4.3.1 + conf: 10.2.0 + dotenv: 16.6.1 + execa: 9.6.1 + git-repo-info: 2.1.1 + glob: 10.5.0 + indent-string: 4.0.0 + json-stream-stringify: 3.1.6 + json5: 2.2.3 + jwt-decode: 3.1.2 + log-symbols: 4.1.0 + luxon: 3.7.2 + minimatch: 9.0.5 + mqtt: 5.14.1 + open: 8.4.2 + p-queue: 6.6.2 + prompts: 2.4.2 + proxy-from-env: 1.1.0 + recast: 0.23.11 + semver: 7.7.3 + tunnel: 0.0.6 + uuid: 11.1.0 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - '@types/node' + - bare-abort-controller + - bufferutil + - debug + - react-native-b4a + - supports-color + - typescript + - utf-8-validate + + ci-info@4.3.1: {} + + clean-stack@3.0.1: + dependencies: + escape-string-regexp: 4.0.0 + + cli-spinners@2.9.2: {} + + cli-width@4.1.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commist@3.2.0: {} + + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + conf@10.2.0: + dependencies: + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + atomically: 1.7.0 + debounce-fn: 4.0.0 + dot-prop: 6.0.1 + env-paths: 2.2.1 + json-schema-typed: 7.0.3 + onetime: 5.1.2 + pkg-up: 3.1.0 + semver: 7.7.3 + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + content-type@1.0.5: {} + + core-util-is@1.0.3: {} + + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debounce-fn@4.0.0: + dependencies: + mimic-fn: 3.1.0 + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + define-lazy-prop@2.0.0: {} + + delayed-stream@1.0.0: {} + + dot-prop@6.0.1: + dependencies: + is-obj: 2.0.0 + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + escape-string-regexp@4.0.0: {} + + eslint-visitor-keys@4.2.1: {} + + esprima@4.0.1: {} + + event-target-shim@5.0.1: {} + + eventemitter3@4.0.7: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + + events@3.3.0: {} + + execa@9.6.1: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + fast-deep-equal@3.1.3: {} + + fast-fifo@1.3.2: {} + + fast-levenshtein@3.0.0: + dependencies: + fastest-levenshtein: 1.0.16 + + fast-unique-numbers@9.0.24: + dependencies: + '@babel/runtime': 7.28.4 + tslib: 2.8.1 + + fast-uri@3.1.0: {} + + fastest-levenshtein@1.0.16: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + + follow-redirects@1.15.11: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.2: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + git-repo-info@2.1.1: {} + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + gopd@1.2.0: {} + + graceful-fs@4.2.10: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + help-me@5.0.0: {} + + hosted-git-info@7.0.2: + dependencies: + lru-cache: 10.4.3 + + http-call@5.3.0: + dependencies: + content-type: 1.0.5 + debug: 4.4.3(supports-color@8.1.1) + is-retry-allowed: 1.2.0 + is-stream: 2.0.1 + parse-json: 4.0.0 + tunnel-agent: 0.6.0 + transitivePeerDependencies: + - supports-color + + human-signals@8.0.1: {} + + iconv-lite@0.7.1: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + indent-string@4.0.0: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ip-address@10.1.0: {} + + is-arrayish@0.2.1: {} + + is-docker@2.2.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-obj@2.0.0: {} + + is-plain-obj@4.1.0: {} + + is-retry-allowed@1.2.0: {} + + is-stream@2.0.1: {} + + is-stream@4.0.1: {} + + is-unicode-supported@0.1.0: {} + + is-unicode-supported@2.1.0: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + isexe@3.1.1: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.4 + picocolors: 1.1.1 + + jiti@2.6.1: {} + + js-sdsl@4.3.0: {} + + json-parse-better-errors@1.0.2: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@7.0.3: {} + + json-stream-stringify@3.1.6: {} + + json5@2.2.3: {} + + jwt-decode@3.1.2: {} + + kleur@3.0.3: {} + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + lilconfig@3.1.3: {} + + locate-path@3.0.0: + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + lru-cache@10.4.3: {} + + luxon@3.7.2: {} + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@2.1.0: {} + + mimic-fn@3.1.0: {} + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.2 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + mqtt-packet@9.0.2: + dependencies: + bl: 6.1.6 + debug: 4.4.3(supports-color@8.1.1) + process-nextick-args: 2.0.1 + transitivePeerDependencies: + - supports-color + + mqtt@5.14.1: + dependencies: + '@types/readable-stream': 4.0.23 + '@types/ws': 8.18.1 + commist: 3.2.0 + concat-stream: 2.0.0 + debug: 4.4.3(supports-color@8.1.1) + help-me: 5.0.0 + lru-cache: 10.4.3 + minimist: 1.2.8 + mqtt-packet: 9.0.2 + number-allocator: 1.0.14 + readable-stream: 4.7.0 + rfdc: 1.4.1 + socks: 2.8.7 + split2: 4.2.0 + worker-timers: 8.0.27 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + ms@2.1.3: {} + + mute-stream@2.0.0: {} + + normalize-path@3.0.0: {} + + npm-package-arg@11.0.3: + dependencies: + hosted-git-info: 7.0.2 + proc-log: 4.2.0 + semver: 7.7.3 + validate-npm-package-name: 5.0.1 + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + npm@10.9.4: {} + + number-allocator@1.0.14: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + js-sdsl: 4.3.0 + transitivePeerDependencies: + - supports-color + + object-treeify@4.0.1: {} + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + p-finally@1.0.0: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + parse-json@4.0.0: + dependencies: + error-ex: 1.3.4 + json-parse-better-errors: 1.0.2 + + parse-ms@4.0.0: {} + + path-exists@3.0.0: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pkg-up@3.1.0: + dependencies: + find-up: 3.0.0 + + playwright-core@1.57.0: {} + + playwright@1.57.0: + dependencies: + playwright-core: 1.57.0 + optionalDependencies: + fsevents: 2.3.2 + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + proc-log@4.2.0: {} + + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + proto-list@1.2.4: {} + + proxy-from-env@1.1.0: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + + recast@0.23.11: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + registry-auth-token@5.1.0: + dependencies: + '@pnpm/npm-conf': 2.3.1 + + require-from-string@2.0.2: {} + + rfdc@1.4.1: {} + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + semver@7.7.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + sisteransi@1.0.5: {} + + smart-buffer@4.2.0: {} + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + streamx@2.23.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-final-newline@4.0.0: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + tar-stream@3.1.7: + dependencies: + b4a: 1.7.3 + fast-fifo: 1.3.2 + streamx: 2.23.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + text-decoder@1.2.3: + dependencies: + b4a: 1.7.3 + transitivePeerDependencies: + - react-native-b4a + + tiny-invariant@1.3.3: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tunnel@0.0.6: {} + + type-fest@0.21.3: {} + + typedarray@0.0.6: {} + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + unicorn-magic@0.3.0: {} + + util-deprecate@1.0.2: {} + + uuid@11.1.0: {} + + validate-npm-package-name@5.0.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@4.0.0: + dependencies: + isexe: 3.1.1 + + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + + wordwrap@1.0.0: {} + + worker-factory@7.0.46: + dependencies: + '@babel/runtime': 7.28.4 + fast-unique-numbers: 9.0.24 + tslib: 2.8.1 + + worker-timers-broker@8.0.13: + dependencies: + '@babel/runtime': 7.28.4 + broker-factory: 3.1.11 + fast-unique-numbers: 9.0.24 + tslib: 2.8.1 + worker-timers-worker: 9.0.11 + + worker-timers-worker@9.0.11: + dependencies: + '@babel/runtime': 7.28.4 + tslib: 2.8.1 + worker-factory: 7.0.46 + + worker-timers@8.0.27: + dependencies: + '@babel/runtime': 7.28.4 + tslib: 2.8.1 + worker-timers-broker: 8.0.13 + worker-timers-worker: 9.0.11 + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + ws@8.19.0: {} + + yarn@1.22.22: {} + + yoctocolors-cjs@2.1.3: {} + + yoctocolors@2.1.2: {} + + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 diff --git a/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/pnpm-workspace.yaml b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/pnpm-workspace.yaml new file mode 100644 index 000000000..18ec407ef --- /dev/null +++ b/packages/cli/e2e/__tests__/fixtures/workspace-basic-pnpm/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'packages/*' diff --git a/packages/cli/e2e/__tests__/pw-test.spec.ts b/packages/cli/e2e/__tests__/pw-test.spec.ts index 8cf8f5f09..dced97fb7 100644 --- a/packages/cli/e2e/__tests__/pw-test.spec.ts +++ b/packages/cli/e2e/__tests__/pw-test.spec.ts @@ -1,73 +1,116 @@ import path from 'node:path' -import fs from 'node:fs' -import config from 'config' -import { describe, it, expect, afterEach, beforeAll } from 'vitest' +import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest' -import { runChecklyCli } from '../run-checkly' import { loadChecklyConfig } from '../../src/services/checkly-config-loader' +import { FixtureSandbox } from '../../src/testing/fixture-sandbox' -const FIXTURE_TEST_PWT_NATIVE = path.join(__dirname, 'fixtures', 'test-pwt-native') +async function runTest (fixt: FixtureSandbox, args: string[]) { + const result = await fixt.run('npx', [ + 'checkly', + 'pw-test', + ...args, + ], { + timeout: 180_000, + }) + + if (result.exitCode !== 0) { + // eslint-disable-next-line no-console + console.error('stderr', result.stderr) + // eslint-disable-next-line no-console + console.error('stdout', result.stdout) + } + + expect(result.exitCode).toBe(0) + + return result +} describe('pw-test', { timeout: 45000 }, () => { - afterEach(() => { - fs.copyFileSync( - path.join(FIXTURE_TEST_PWT_NATIVE, 'checkly.config.original.ts'), - path.join(FIXTURE_TEST_PWT_NATIVE, 'checkly.config.ts'), - ) - }) + let fixt: FixtureSandbox beforeAll(async () => { - // Install fixture dependencies or they will not resolve correctly. - const { execa } = await import('execa') - await execa('npm', ['install'], { cwd: FIXTURE_TEST_PWT_NATIVE }) + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'test-pwt-native'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() }) it('Playwright test should run successfully', async () => { - const result = await runChecklyCli({ - args: ['pw-test', '--', `--grep`, '@TAG-B'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: FIXTURE_TEST_PWT_NATIVE, - timeout: 120000, // 2 minutes - }) - if (result.status !== 0) { - // eslint-disable-next-line no-console - console.log(result) - } - expect(result.status).toBe(0) + await runTest(fixt, [ + '--', + '--grep', + '@TAG-B', + ]) }, 130000) - it('Should add a Playwright test to the config', async () => { - const result = await runChecklyCli({ - args: ['pw-test', '--create-check', '--', `--grep`, '@TAG-B'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: FIXTURE_TEST_PWT_NATIVE, - timeout: 120000, // 2 minutes + describe('create-check', () => { + let fixt: FixtureSandbox + + beforeEach(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'test-pwt-native'), + }) + }, 180_000) + + afterEach(async () => { + await fixt?.destroy() }) - expect(result.status).toBe(0) - const checklyConfig = await loadChecklyConfig(FIXTURE_TEST_PWT_NATIVE) - expect(checklyConfig.config?.checks).toBeDefined() - expect(checklyConfig.config?.checks?.playwrightConfigPath).toBe('./playwright.config.ts') - expect(checklyConfig.config?.checks?.playwrightChecks).toBeDefined() - expect(checklyConfig.config?.checks?.playwrightChecks.length).toBe(1) - expect(checklyConfig.config?.checks?.playwrightChecks[0].name).toBe('Playwright Test: --grep @TAG-B') - expect(checklyConfig.config?.checks?.playwrightChecks[0].testCommand).toBe('npx playwright test --grep @TAG-B') - expect(checklyConfig.config?.checks?.playwrightChecks[0].frequency).toBe(10) - }) - it('Should add a Playwright test with custom frequency', async () => { - const result = await runChecklyCli({ - args: ['pw-test', '--create-check', '--frequency', '5', '--', `--grep`, '@TAG-B'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: FIXTURE_TEST_PWT_NATIVE, - timeout: 120000, // 2 minutes + it('Should add a Playwright test to the config', async () => { + await runTest(fixt, [ + '--create-check', + '--', + '--grep', + '@TAG-B', + ]) + + const checklyConfig = await loadChecklyConfig(fixt.root) + + expect(checklyConfig).toEqual(expect.objectContaining({ + config: expect.objectContaining({ + checks: expect.objectContaining({ + playwrightConfigPath: './playwright.config.ts', + playwrightChecks: expect.arrayContaining([ + expect.objectContaining({ + name: 'Playwright Test: --grep @TAG-B', + testCommand: 'npx playwright test --grep @TAG-B', + frequency: 10, + }), + ]), + }), + }), + })) + }) + + it('Should add a Playwright test with custom frequency', async () => { + await runTest(fixt, [ + '--create-check', + '--frequency', + '5', + '--', + '--grep', + '@TAG-B', + ]) + + const checklyConfig = await loadChecklyConfig(fixt.root) + + expect(checklyConfig).toEqual(expect.objectContaining({ + config: expect.objectContaining({ + checks: expect.objectContaining({ + playwrightChecks: expect.arrayContaining([ + expect.objectContaining({ + name: 'Playwright Test: --grep @TAG-B', + testCommand: 'npx playwright test --grep @TAG-B', + frequency: 5, + }), + ]), + }), + }), + })) }) - expect(result.status).toBe(0) - const configContent = fs.readFileSync( - path.join(FIXTURE_TEST_PWT_NATIVE, 'checkly.config.ts'), 'utf-8') - expect(configContent).toContain('frequency: 5') }) }) diff --git a/packages/cli/e2e/__tests__/test.spec.ts b/packages/cli/e2e/__tests__/test.spec.ts index 03a67b768..4a35fba04 100644 --- a/packages/cli/e2e/__tests__/test.spec.ts +++ b/packages/cli/e2e/__tests__/test.spec.ts @@ -2,210 +2,305 @@ import path from 'node:path' import fs from 'node:fs' import * as uuid from 'uuid' -import config from 'config' -import { describe, it, expect } from 'vitest' +import { describe, it, expect, afterAll, beforeAll } from 'vitest' -import { runChecklyCli } from '../run-checkly' +import { FixtureSandbox, RunOptions } from '../../src/testing/fixture-sandbox' +import { ExecaError } from 'execa' -describe('test', { timeout: 45000 }, () => { - it('Test project should run successfully', 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', 'test-project'), - timeout: 120000, // 2 minutes - }) - expect(result.stdout).not.toContain('File extension type example') - expect(result.stdout).toContain(secretEnv) - expect(result.status).toBe(0) - }, 130000) - - it('Should include only one check', async () => { - const result = await runChecklyCli({ - args: ['test', 'secret.check.ts'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'test-project'), - }) - expect(result.stdout).toContain('Show SECRET_ENV value') - expect(result.stdout).toContain('1 passed, 1 total') - expect(result.status).toBe(0) +async function runTest (fixt: FixtureSandbox, args: string[], options?: RunOptions) { + const result = await fixt.run('npx', [ + 'checkly', + 'test', + ...args, + ], { + timeout: 120_000, + ...options, + env: { + CHECKLY_CLI_VERSION: '4.8.0', + ...options?.env, + }, }) - it('Should use different config file', async () => { - const result = await runChecklyCli({ - args: ['test', 'secret.check.ts', '--config', 'checkly.staging.config.ts'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'test-project'), + if (result.exitCode !== 0) { + // eslint-disable-next-line no-console + console.error('stderr', result.stderr) + // eslint-disable-next-line no-console + console.error('stdout', result.stdout) + } + + expect(result.exitCode).toBe(0) + + return result +} + +describe('test', { timeout: 45000 }, () => { + describe('test-project', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'test-project'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() }) - expect(result.stdout).toContain('Show SECRET_ENV value') - expect(result.stdout).toContain('1 passed, 1 total') - expect(result.status).toBe(0) - }) - it('Should fail with config file not found', async () => { - const result = await runChecklyCli({ - args: ['test', 'secret.check.ts', '--config', 'checkly.notfound.config.ts'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'test-project'), + it('Test project should run successfully', async () => { + const secretEnv = uuid.v4() + const result = await runTest(fixt, ['-e', `SECRET_ENV=${secretEnv}`, '--verbose']) + expect(result.stdout).not.toContain('File extension type example') + expect(result.stdout).toContain(secretEnv) + }, 130_000) + + it('Should include only one check', async () => { + const result = await runTest(fixt, ['secret.check.ts']) + expect(result.stdout).toContain('Show SECRET_ENV value') + expect(result.stdout).toContain('1 passed, 1 total') }) - expect(result.status).toBe(1) - }) - it('Should terminate when no checks are found', async () => { - const result = await runChecklyCli({ - args: ['test', 'does-not-exist.js'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'test-project'), + it('Should use different config file', async () => { + const result = await runTest(fixt, ['secret.check.ts', '--config', 'checkly.staging.config.ts']) + expect(result.stdout).toContain('Show SECRET_ENV value') + expect(result.stdout).toContain('1 passed, 1 total') }) - expect(result.stdout).toContain('Unable to find checks to run using [FILEARGS]=\'') - expect(result.status).toBe(0) - }) - it('Should list checks and not execute them with `--list`', async () => { - const result = await runChecklyCli({ - args: ['test', '--list'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'test-project'), + it('Should fail with config file not found', async () => { + expect.assertions(1) + try { + await runTest(fixt, ['secret.check.ts', '--config', 'checkly.notfound.config.ts']) + } catch (err) { + if (err instanceof ExecaError) { + expect(err.exitCode).toBe(1) + } else { + throw err + } + } }) - expect(result.stdout).toContain('Listing all checks') - expect(result.status).toBe(0) - }) - it('Should terminate with error when JS/TS throws error', async () => { - const result = await runChecklyCli({ - args: ['test'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'test-parse-error'), + it('Should terminate when no checks are found', async () => { + const result = await runTest(fixt, ['does-not-exist.js']) + expect(result.stdout).toContain('Unable to find checks to run using [FILEARGS]=\'') }) - expect(result.stderr).toContain('Error loading file') - expect(result.stderr).toContain('Error: Big bang!') - expect(result.status).toBe(1) - }) - it('Should terminate with error when duplicated logicalId', async () => { - const result = await runChecklyCli({ - args: ['test'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'test-duplicated-groups'), + it('Should list checks and not execute them with `--list`', async () => { + const result = await runTest(fixt, ['--list']) + expect(result.stdout).toContain('Listing all checks') + }) + + it('Should use Github reporter and show test-session link', async () => { + const reportFilename = fixt.abspath('./reports/checkly-summary.md') + try { + fs.unlinkSync(reportFilename) + } catch { + // No-op + } + const result = await runTest(fixt, ['--record', '--reporter', 'github'], { + env: { + CHECKLY_REPORTER_GITHUB_OUTPUT: reportFilename, + }, + }) + expect(result.stdout).toContain('Github summary saved in') + expect(result.stdout).toContain('Detailed session summary at') + expect(result.stdout).toContain('https://chkly.link/l') + expect(fs.existsSync(reportFilename)).toBe(true) + }) + + it('Should report timeouts correctly', async () => { + expect.assertions(2) + try { + await runTest(fixt, ['homepage.test.ts', '--timeout', '0']) + } catch (err) { + if (err instanceof ExecaError) { + expect(err.stdout).toContain('Reached timeout of 0 seconds waiting for check result.') + expect(err.exitCode).toEqual(1) + } + } }) - expect(result.stderr.replace(/(\n {4})/gm, '')) - .toContain('Error: Resource of type \'check-group\' with logical id \'my-check-group\' already exists.') - expect(result.status).toBe(1) }) - it('Should include a testOnly check', async () => { - const result = await runChecklyCli({ - args: ['test'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'test-only-project'), - env: { PROJECT_LOGICAL_ID: 'test-only-project', TEST_ONLY: 'true' }, - }) - expect(result.stdout).toContain('TestOnly=false (default) Check') - expect(result.stdout).toContain('TestOnly=false Check') - expect(result.stdout).toContain('TestOnly=true Check') - expect(result.status).toBe(0) + describe('test-parse-error', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'test-parse-error'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + + it('Should terminate with error when JS/TS throws error', async () => { + expect.assertions(3) + try { + await runTest(fixt, []) + } catch (err) { + if (err instanceof ExecaError) { + expect(err.stderr).toContain('Error loading file') + expect(err.stderr).toContain('Error: Big bang!') + expect(err.exitCode).toBe(1) + } else { + throw err + } + } + }) }) - it('Should use Github reporter and show test-session link', async () => { - const reportFilename = './reports/checkly-summary.md' - try { - fs.unlinkSync(reportFilename) - } catch { - // No-op - } - const result = await runChecklyCli({ - args: ['test', '--record', '--reporter', 'github'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'test-project'), - env: { CHECKLY_REPORTER_GITHUB_OUTPUT: reportFilename }, - timeout: 120000, // 2 minutes - }) - expect(result.stdout).toContain('Github summary saved in') - expect(result.stdout).toContain('Detailed session summary at') - expect(result.stdout).toContain('https://chkly.link/l') - expect(fs.existsSync(path.join(__dirname, 'fixtures', 'test-project', reportFilename))).toBe(true) - expect(result.status).toBe(0) + describe('test-duplicated-groups', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'test-duplicated-groups'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + + it('Should terminate with error when duplicated logicalId', async () => { + expect.assertions(2) + try { + await runTest(fixt, []) + } catch (err) { + if (err instanceof ExecaError) { + expect((err.stderr as unknown as string).replace(/(\n {4})/gm, '')) + .toContain('Error: Resource of type \'check-group\' with logical id \'my-check-group\' already exists.') + expect(err.exitCode).toBe(1) + } else { + throw err + } + } + }) }) - it('Should report timeouts correctly', async () => { - const result = await runChecklyCli({ - args: ['test', 'homepage.test.ts', '--timeout', '0'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'test-project'), + describe('test-only-project', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'test-only-project'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + + it('Should include a testOnly check', async () => { + const result = await runTest(fixt, [], { + env: { + PROJECT_LOGICAL_ID: 'test-only-project', + TEST_ONLY: 'true', + }, + }) + expect(result.stdout).toContain('TestOnly=false (default) Check') + expect(result.stdout).toContain('TestOnly=false Check') + expect(result.stdout).toContain('TestOnly=true Check') }) - expect(result.status).toBe(1) - expect(result.stdout).toContain('Reached timeout of 0 seconds waiting for check result.') }) - it('ESModule project should run successfully', 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', 'esm-module'), - timeout: 120000, // 2 minutes - }) - expect(result.stdout).not.toContain('File extension type example') - expect(result.stdout).toContain(secretEnv) - expect(result.status).toBe(0) + describe('esm-module', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'esm-module'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + + it('ESModule project should run successfully', async () => { + const secretEnv = uuid.v4() + const result = await runTest(fixt, ['-e', `SECRET_ENV=${secretEnv}`, '--verbose']) + expect(result.stdout).not.toContain('File extension type example') + expect(result.stdout).toContain(secretEnv) + }) }) - it('Should run snapshot tests', async () => { - const secretEnv = uuid.v4() - const result = await runChecklyCli({ - args: ['test', '-e', `SECRET_ENV=${secretEnv}`, '--verbose', '--record'], - 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) + describe('snapshot-project', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'snapshot-project'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + + it('Should run snapshot tests', async () => { + const secretEnv = uuid.v4() + const result = await runTest(fixt, ['-e', `SECRET_ENV=${secretEnv}`, '--verbose', '--record'], { + env: { + PROJECT_LOGICAL_ID: `snapshot-project-${uuid.v4()}`, + }, + }) + expect(result.stdout).toContain(secretEnv) + }) }) - 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()}` }, + describe('snapshot-project-missing-snapshots', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'snapshot-project-missing-snapshots'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + + it('Should create snapshots when using --update-snapshots', async () => { + const snapshotDir = fixt.abspath('snapshot-test.spec.ts-snapshots') + await runTest(fixt, ['--update-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 }) - } + }) }) - it('Should execute retries', async () => { - const result = await runChecklyCli({ - args: ['test', '--retries=3'], - apiKey: config.get('apiKey'), - accountId: config.get('accountId'), - directory: path.join(__dirname, 'fixtures', 'retry-project'), - timeout: 120000, // 2 minutes - }) - // The failing check result will have "Failing Check Result" in the output. - // We expect the check to be run 4 times. - expect(result.stdout.match(/Failing Check Result/g)).toHaveLength(4) + describe('retry-project', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'retry-project'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + + it('Should execute retries', async () => { + expect.assertions(1) + try { + await runTest(fixt, ['--retries=3']) + } catch (err) { + if (err instanceof ExecaError) { + // The failing check result will have "Failing Check Result" in the output. + // We expect the check to be run 4 times. + expect((err.stdout as unknown as string).match(/Failing Check Result/g)).toHaveLength(4) + } else { + throw err + } + } + }, 120_000) }) }) diff --git a/packages/cli/e2e/__tests__/trigger.spec.ts b/packages/cli/e2e/__tests__/trigger.spec.ts index 6615f8339..125d0fa2a 100644 --- a/packages/cli/e2e/__tests__/trigger.spec.ts +++ b/packages/cli/e2e/__tests__/trigger.spec.ts @@ -51,7 +51,7 @@ describe('trigger', () => { expect(result.stdout).toContain('Prod Frontend Check') expect(result.stdout).not.toContain('Staging Backend Check') expect(result.status).toBe(0) - }) + }, 45_000) test('Should return code 1 when no checks match', async () => { const result = await runChecklyCli({ diff --git a/packages/cli/package.json b/packages/cli/package.json index 065770a5c..e743a32c0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -21,8 +21,8 @@ "prepare:ai-context": "cross-env CHECKLY_SKIP_AUTH=1 CHECKLY_CLI_VERSION=99.0.0 ./bin/run import plan --root gen --debug-import-plan-input-file ./src/ai-context/context.fixtures.json && ts-node ./scripts/prepare-ai-context.ts", "prepare:dist": "tsc --build", "prepare": "npm run clean && npm run prepare:dist && npm run prepare:ai-context", - "test": "vitest", - "test:e2e": "npm run prepare && cross-env NODE_CONFIG_DIR=./e2e/config vitest -c ./vitest.config.e2e.mts", + "test": "npm pack && vitest --run", + "test:e2e": "npm pack && cross-env NODE_CONFIG_DIR=./e2e/config vitest --run -c ./vitest.config.e2e.mts", "test:e2e:local": "cross-env CHECKLY_BASE_URL=http://localhost:3000 CHECKLY_ENV=local npm run test:e2e", "watch": "tsc --watch" }, @@ -38,6 +38,7 @@ "files": [ "/bin", "/dist", + "/assets", "/constructs.js", "/constructs.d.ts", "/oclif.manifest.json" @@ -89,6 +90,7 @@ "chalk": "^4.1.2", "ci-info": "^4.3.1", "conf": "^10.2.0", + "debug": "^4.4.3", "dotenv": "^16.5.0", "execa": "^9.6.1", "git-repo-info": "^2.1.1", @@ -114,6 +116,7 @@ "@playwright/test": "^1.57.0", "@types/archiver": "6.0.3", "@types/config": "^3.3.5", + "@types/debug": "^4.1.12", "@types/glob": "^8.1.0", "@types/luxon": "^3.7.1", "@types/node": "^22.14.1", @@ -129,6 +132,7 @@ "oclif": "^4.22.56", "rimraf": "^5.0.10", "simple-git-hooks": "^2.12.1", + "tar": "^7.5.7", "ts-node": "^10.9.2", "typescript": "^5.3.3", "vitest": "3.1.2" @@ -141,4 +145,4 @@ "optional": true } } -} +} \ No newline at end of file diff --git a/packages/cli/src/auth/index.ts b/packages/cli/src/auth/index.ts index 32d202f85..5ffa2e629 100644 --- a/packages/cli/src/auth/index.ts +++ b/packages/cli/src/auth/index.ts @@ -4,7 +4,7 @@ import * as http from 'http' import * as crypto from 'crypto' import jwtDecode from 'jwt-decode' import { getDefaults as getApiDefaults } from '../rest/api' -import { assignProxy } from '../services/util' +import { assignProxy } from '../services/proxy' import * as fs from 'fs' import * as path from 'path' diff --git a/packages/cli/src/commands/baseCommand.ts b/packages/cli/src/commands/baseCommand.ts index 9346b14b2..c6d60ede0 100644 --- a/packages/cli/src/commands/baseCommand.ts +++ b/packages/cli/src/commands/baseCommand.ts @@ -3,8 +3,8 @@ import prompts from 'prompts' import { Command } from '@oclif/core' import { api } from '../rest/api' import { CommandStyle } from '../helpers/command-style' -import { PackageFilesResolver } from '../services/check-parser/package-files/resolver' import { PackageJsonFile } from '../services/check-parser/package-files/package-json-file' +import { detectNearestPackageJson } from '../services/check-parser/package-files/package-manager' export type BaseCommandClass = typeof Command & { coreCommand: boolean @@ -18,12 +18,15 @@ export abstract class BaseCommand extends Command { #packageJsonLoader?: Promise async loadPackageJsonOfSelf (): Promise { - if (!this.#packageJsonLoader) { - const resolver = new PackageFilesResolver() - this.#packageJsonLoader = resolver.loadPackageJsonFile(__filename) - } + try { + if (!this.#packageJsonLoader) { + this.#packageJsonLoader = detectNearestPackageJson(__dirname) + } - return await this.#packageJsonLoader + return await this.#packageJsonLoader + } catch { + return + } } async checkEngineCompatibility (): Promise { diff --git a/packages/cli/src/commands/debug/parse-file.ts b/packages/cli/src/commands/debug/parse-file.ts new file mode 100644 index 000000000..602b9e51e --- /dev/null +++ b/packages/cli/src/commands/debug/parse-file.ts @@ -0,0 +1,89 @@ +import path from 'node:path' + +import { Command, Flags } from '@oclif/core' + +import { Parser } from '../../services/check-parser/parser' +import { detectPackageManager } from '../../services/check-parser/package-files/package-manager' +import { DependencyParseError } from '../../services/check-parser/errors' + +export default class ParseFileCommand extends Command { + static hidden = true + static description = 'Parses and outputs relevant details of a code file.' + + static flags = { + 'file': Flags.string({ + required: true, + }), + 'restricted': Flags.boolean({ + default: false, + }), + 'detect-workspace': Flags.boolean({ + default: true, + allowNo: true, + }), + 'supported-module': Flags.string({ + multiple: true, + default: [], + }), + 'check-unsupported-modules': Flags.boolean({ + default: false, + }), + } + + async run (): Promise { + const { flags } = await this.parse(ParseFileCommand) + const { + file: codeFile, + restricted, + 'detect-workspace': detectWorkspace, + 'supported-module': supportedNpmModules, + 'check-unsupported-modules': checkUnsupportedModules, + } = flags + + const codeDir = path.dirname(codeFile) + + const packageManager = await detectPackageManager(codeDir) + + const workspace = detectWorkspace + ? await packageManager.lookupWorkspace(codeDir) + : undefined + + const errors = [] + let result + + try { + const parser = new Parser({ + workspace, + restricted, + supportedNpmModules, + checkUnsupportedModules, + }) + + result = await parser.parse(codeFile) + } catch (err: any) { + if (err instanceof DependencyParseError) { + errors.push({ + name: err.name, + message: err.message, + entrypoint: err.entrypoint, + missingFiles: err.missingFiles, + unsupportedNpmDependencies: err.unsupportedNpmDependencies, + parseErrors: err.parseErrors, + }) + } else { + errors.push({ + name: err.name, + message: err.message, + }) + } + } + + const output = { + errors, + result, + } + + // eslint-disable-next-line no-console + console.log(JSON.stringify(output, null, 2)) + } +} diff --git a/packages/cli/src/commands/debug/parse-playwright-config.ts b/packages/cli/src/commands/debug/parse-playwright-config.ts new file mode 100644 index 000000000..1ac8e9bb1 --- /dev/null +++ b/packages/cli/src/commands/debug/parse-playwright-config.ts @@ -0,0 +1,37 @@ +import path from 'node:path' + +import { Command, Flags } from '@oclif/core' + +import { PlaywrightConfig } from '../../services/playwright-config' +import { Session } from '../../constructs/project' +import { Parser } from '../../services/check-parser/parser' + +export default class ParsePlaywrightConfigCommand extends Command { + static hidden = true + static description = 'Parses and outputs relevant details of a Playwright configuration file.' + + static flags = { + file: Flags.string({ + env: 'CHECKLY_PLAYWRIGHT_CONFIG_FILE', + default: path.join(process.cwd(), 'playwright.config.ts'), + }), + } + + async run (): Promise { + const { flags } = await this.parse(ParsePlaywrightConfigCommand) + const { + file: playwrightConfigFile, + } = flags + + const playwrightConfig = new PlaywrightConfig( + playwrightConfigFile, + await Session.loadFile(playwrightConfigFile), + ) + + const parser = new Parser({}) + const output = await parser.getFilesAndDependencies(playwrightConfig) + + // eslint-disable-next-line no-console + console.log(JSON.stringify(output, null, 2)) + } +} diff --git a/packages/cli/src/commands/debug/parse-project.ts b/packages/cli/src/commands/debug/parse-project.ts new file mode 100644 index 000000000..d0c554947 --- /dev/null +++ b/packages/cli/src/commands/debug/parse-project.ts @@ -0,0 +1,152 @@ +import { Command, Flags } from '@oclif/core' + +import { parseProject } from '../../services/project-parser' +import { loadChecklyConfig } from '../../services/checkly-config-loader' +import { + Diagnostics, +} from '../../constructs' +import { splitConfigFilePath } from '../../services/util' +import commonMessages from '../../messages/common-messages' +import { loadSnapshot, Runtime } from '../../runtimes' + +export type ParseProjectOutput = { + diagnostics: { + fatal: boolean + benign: boolean + observations: { + title: string + message: string + fatal: boolean + benign: boolean + }[] + } + payload: { + project: { + logicalId: string + name: string + } + sharedFiles: { + path: string + content: string + }[] + resources: { + logicalId: string + type: string + member: boolean + payload: unknown + }[] + } +} + +export default class ParseProjectCommand extends Command { + static hidden = true + static description = 'Parses a Checkly project.' + + static flags = { + 'config': Flags.string({ + char: 'c', + description: commonMessages.configFile, + env: 'CHECKLY_CONFIG_FILE', + }), + 'default-runtime': Flags.string({ + description: 'The default runtime to use if none is specified.', + default: '2025.04', + env: 'CHECKLY_DEFAULT_RUNTIME', + }), + 'verify-runtime-dependencies': Flags.boolean({ + description: '[default: true] Return an error if checks import dependencies that are not supported by the selected runtime.', + default: true, + allowNo: true, + env: 'CHECKLY_VERIFY_RUNTIME_DEPENDENCIES', + }), + 'emulate-pw-test': Flags.boolean({ + description: 'Pretend to be the pw-test command. Affects validation.', + env: 'CHECKLY_EMULATE_PW_TEST', + }), + 'include': Flags.string({ + description: 'File patterns to include when bundling the test project (e.g., "utils/**/*").', + multiple: true, + default: [], + }), + } + + async run (): Promise { + const { flags } = await this.parse(ParseProjectCommand) + const { + config: configFilename, + 'default-runtime': defaultRuntime, + 'verify-runtime-dependencies': verifyRuntimeDependencies, + 'emulate-pw-test': emulatePwTest, + 'include': includeFlag, + } = flags + const { configDirectory, configFilenames } = splitConfigFilePath(configFilename) + const { + config: checklyConfig, + constructs: checklyConfigConstructs, + } = await loadChecklyConfig(configDirectory, configFilenames) + const availableRuntimes = await loadSnapshot() + + try { + const project = await parseProject({ + directory: configDirectory, + projectLogicalId: checklyConfig.logicalId, + projectName: checklyConfig.projectName, + repoUrl: checklyConfig.repoUrl, + checkMatch: checklyConfig.checks?.checkMatch, + browserCheckMatch: checklyConfig.checks?.browserChecks?.testMatch, + multiStepCheckMatch: checklyConfig.checks?.multiStepChecks?.testMatch, + ignoreDirectoriesMatch: checklyConfig.checks?.ignoreDirectoriesMatch, + checkDefaults: checklyConfig.checks, + browserCheckDefaults: checklyConfig.checks?.browserChecks, + availableRuntimes: availableRuntimes.reduce((acc, runtime) => { + acc[runtime.name] = runtime + return acc + }, > {}), + defaultRuntimeId: defaultRuntime, + verifyRuntimeDependencies, + checklyConfigConstructs, + playwrightConfigPath: checklyConfig.checks?.playwrightConfigPath, + include: includeFlag.length ? includeFlag : checklyConfig.checks?.include, + includeFlagProvided: includeFlag.length > 0, + playwrightChecks: checklyConfig.checks?.playwrightChecks, + currentCommand: emulatePwTest ? 'pw-test' : undefined, + }) + + const diagnostics = new Diagnostics() + await project.validate(diagnostics) + + const bundle = await project.bundle() + + const payload = diagnostics.isFatal() ? null : bundle.synthesize() + + const output = { + diagnostics: { + fatal: diagnostics.isFatal(), + benign: diagnostics.isBenign(), + observations: diagnostics.observations.map(diag => { + return { + title: diag.title, + message: diag.message, + fatal: diag.isFatal(), + benign: diag.isBenign(), + } + }), + }, + payload, + } + + // eslint-disable-next-line no-console + console.log(JSON.stringify(output, null, 2)) + } catch (err: any) { + const output = { + errors: [{ + name: err.name, + message: err.message, + }], + } + + // eslint-disable-next-line no-console + console.log(JSON.stringify(output, null, 2)) + } + } +} diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 1bef80f7f..f7c344667 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -6,7 +6,6 @@ import { Flags } from '@oclif/core' import { AuthCommand } from './authCommand' import { parseProject } from '../services/project-parser' import { loadChecklyConfig } from '../services/checkly-config-loader' -import type { Runtime } from '../rest/runtimes' import { Check, AlertChannelSubscription, AlertChannel, CheckGroup, Dashboard, MaintenanceWindow, PrivateLocation, PrivateLocationCheckAssignment, PrivateLocationGroupAssignment, @@ -18,6 +17,8 @@ import commonMessages from '../messages/common-messages' import { ProjectDeployResponse } from '../rest/projects' import { uploadSnapshots } from '../services/snapshot-service' import { BrowserCheckBundle } from '../constructs/browser-check-bundle' +import { PlaywrightCheckLocalBundle } from '../constructs/playwright-check-bundle' +import { Runtime } from '../runtimes' // eslint-disable-next-line no-restricted-syntax enum ResourceDeployStatus { @@ -93,7 +94,7 @@ export default class Deploy extends AuthCommand { constructs: checklyConfigConstructs, } = await loadChecklyConfig(configDirectory, configFilenames) const account = this.account - const { data: avilableRuntimes } = await api.runtimes.getAll() + const availableRuntimes = await api.runtimes.getAll() const project = await parseProject({ directory: configDirectory, projectLogicalId: checklyConfig.logicalId, @@ -105,7 +106,7 @@ export default class Deploy extends AuthCommand { ignoreDirectoriesMatch: checklyConfig.checks?.ignoreDirectoriesMatch, checkDefaults: checklyConfig.checks, browserCheckDefaults: checklyConfig.checks?.browserChecks, - availableRuntimes: avilableRuntimes.reduce((acc, runtime) => { + availableRuntimes: availableRuntimes.reduce((acc, runtime) => { acc[runtime.name] = runtime return acc }, > {}), @@ -155,12 +156,45 @@ export default class Deploy extends AuthCommand { } })() - if (!preview) { - for (const { bundle: check } of Object.values(projectBundle.data.check)) { - if (!(check instanceof BrowserCheckBundle)) { - continue + const bundledChecksByType = { + playwright: [] as string[], + browser: [] as string[], + } + + for (const [logicalId, { bundle }] of Object.entries(projectBundle.data.check)) { + if (bundle instanceof BrowserCheckBundle) { + bundledChecksByType.browser.push(logicalId) + } else if (bundle instanceof PlaywrightCheckLocalBundle) { + bundledChecksByType.playwright.push(logicalId) + } + } + + if (!preview && bundledChecksByType.browser.length) { + this.style.actionStart('Uploading Playwright snapshots') + try { + for (const logicalId of bundledChecksByType.browser) { + const bundle = projectBundle.data.check[logicalId].bundle as BrowserCheckBundle + bundle.snapshots = await uploadSnapshots(bundle.rawSnapshots) } - check.snapshots = await uploadSnapshots(check.rawSnapshots) + this.style.actionSuccess() + } catch (err) { + this.style.actionFailure() + throw err + } + } + + if (bundledChecksByType.playwright.length) { + this.style.actionStart('Uploading Playwright code bundles') + try { + for (const logicalId of bundledChecksByType.playwright) { + const resourceData = projectBundle.data.check[logicalId] + const bundle = resourceData.bundle as PlaywrightCheckLocalBundle + resourceData.bundle = await bundle.store() + } + this.style.actionSuccess() + } catch (err) { + this.style.actionFailure() + throw err } } diff --git a/packages/cli/src/commands/import/plan.ts b/packages/cli/src/commands/import/plan.ts index a81304f0a..0e912fd19 100644 --- a/packages/cli/src/commands/import/plan.ts +++ b/packages/cli/src/commands/import/plan.ts @@ -26,13 +26,12 @@ import { ExitError } from '@oclif/core/errors' import { confirmCommit, performCommitAction } from './commit' import { confirmApply, performApplyAction } from './apply' import { generateChecklyConfig } from '../../services/checkly-config-codegen' -import { PackageFilesResolver } from '../../services/check-parser/package-files/resolver' import { PackageJsonFile } from '../../services/check-parser/package-files/package-json-file' -import { detectPackageManager, knownPackageManagers, PackageManager } from '../../services/check-parser/package-files/package-manager' +import { detectNearestPackageJson, detectPackageManager, knownPackageManagers, PackageManager } from '../../services/check-parser/package-files/package-manager' import { parseProject } from '../../services/project-parser' -import { Runtime } from '../../rest/runtimes' import { ConstructExport, Project, Session } from '../../constructs/project' import { Diagnostics } from '../../constructs' +import { Runtime } from '../../runtimes' type FriendExports = { [type in FriendResourceSync['type']]: Map @@ -704,10 +703,11 @@ ${chalk.cyan('For safety, resources are not deletable until the plan has been co } async #loadPackageJson (): Promise { - const resolver = new PackageFilesResolver() - return await resolver.loadPackageJsonFile(process.cwd(), { - isDir: true, - }) + try { + return await detectNearestPackageJson(process.cwd()) + } catch { + return + } } #createPackageJson (logicalId: string): PackageJsonFile { @@ -942,7 +942,7 @@ ${chalk.cyan('For safety, resources are not deletable until the plan has been co let project: Project try { const account = this.account - const { data: availableRuntimes } = await api.runtimes.getAll() + const availableRuntimes = await api.runtimes.getAll() project = await parseProject({ directory: configDirectory, diff --git a/packages/cli/src/commands/pw-test.ts b/packages/cli/src/commands/pw-test.ts index 90675a0ee..b43877ca1 100644 --- a/packages/cli/src/commands/pw-test.ts +++ b/packages/cli/src/commands/pw-test.ts @@ -12,7 +12,6 @@ import { prepareReportersTypes, prepareRunLocation, splitChecklyAndPlaywrightFla import * as api from '../rest/api' import config from '../services/config' import { parseProject } from '../services/project-parser' -import type { Runtime } from '../rest/runtimes' import { Diagnostics, PlaywrightCheck, RuntimeCheck, Session } from '../constructs' import { Flags } from '@oclif/core' import { createReporters, ReporterType } from '../reporters/reporter' @@ -38,6 +37,8 @@ import { detectPackageManager } from '../services/check-parser/package-files/pac import { DEFAULT_REGION } from '../helpers/constants' import { cased } from '../sourcegen' import { shellQuote } from '../services/shell' +import { Runtime } from '../runtimes' +import { PlaywrightCheckLocalBundle } from '../constructs/playwright-check-bundle' export default class PwTestCommand extends AuthCommand { static coreCommand = true @@ -171,7 +172,7 @@ export default class PwTestCommand extends AuthCommand { const reporterTypes = prepareReportersTypes(reporterFlag as ReporterType, checklyConfig.cli?.reporters) const account = this.account - const { data: availableRuntimes } = await api.runtimes.getAll() + const availableRuntimes = await api.runtimes.getAll() const testEnvVars = await getEnvs(envFile, env) const project = await parseProject({ @@ -251,6 +252,31 @@ export default class PwTestCommand extends AuthCommand { } })() + const bundledChecksByType = { + playwright: [] as string[], + } + + for (const [logicalId, { bundle }] of Object.entries(projectBundle.data.check)) { + if (bundle instanceof PlaywrightCheckLocalBundle) { + bundledChecksByType.playwright.push(logicalId) + } + } + + if (bundledChecksByType.playwright.length) { + this.style.actionStart('Uploading Playwright code bundles') + try { + for (const logicalId of bundledChecksByType.playwright) { + const resourceData = projectBundle.data.check[logicalId] + const bundle = resourceData.bundle as PlaywrightCheckLocalBundle + resourceData.bundle = await bundle.store() + } + this.style.actionSuccess() + } catch (err) { + this.style.actionFailure() + throw err + } + } + const checkBundles = Object.values(projectBundle.data.check) if (!checkBundles.length) { diff --git a/packages/cli/src/commands/runtimes.ts b/packages/cli/src/commands/runtimes.ts index 1b217bb73..395adf7cf 100644 --- a/packages/cli/src/commands/runtimes.ts +++ b/packages/cli/src/commands/runtimes.ts @@ -5,7 +5,7 @@ export default class Runtimes extends BaseCommand { static hidden = false static description = 'List all supported runtimes and dependencies.' async run (): Promise { - const { data: runtimes } = await api.runtimes.getAll() + const runtimes = await api.runtimes.getAll() const output = runtimes .sort((a, b) => b.name.localeCompare(a.name)) .map(r => { diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index ed898b4c0..e7b65a0a1 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -11,7 +11,6 @@ import { import TestRunner from '../services/test-runner' 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, Diagnostics, HeartbeatMonitor, MultiStepCheck, Project, RetryStrategyBuilder, RuntimeCheck, Session } from '../constructs' import type { Region } from '..' @@ -24,6 +23,8 @@ import { uploadSnapshots } from '../services/snapshot-service' import { isEntrypoint } from '../constructs/construct' import { BrowserCheckBundle } from '../constructs/browser-check-bundle' import { prepareReportersTypes, prepareRunLocation } from '../helpers/test-helper' +import { PlaywrightCheckLocalBundle } from '../constructs/playwright-check-bundle' +import { Runtime } from '../runtimes' const MAX_RETRIES = 3 @@ -162,7 +163,7 @@ export default class Test extends AuthCommand { const verbose = this.prepareVerboseFlag(verboseFlag, checklyConfig.cli?.verbose) const reporterTypes = prepareReportersTypes(reporterFlag as ReporterType, checklyConfig.cli?.reporters) const account = this.account - const { data: availableRuntimes } = await api.runtimes.getAll() + const availableRuntimes = await api.runtimes.getAll() const project = await parseProject({ directory: configDirectory, @@ -280,19 +281,54 @@ export default class Test extends AuthCommand { } })() - const checkBundles = Object.values(projectBundle.data.check) + const bundledChecksByType = { + playwright: [] as string[], + browser: [] as string[], + } + + for (const [logicalId, { bundle }] of Object.entries(projectBundle.data.check)) { + if (bundle instanceof BrowserCheckBundle) { + bundledChecksByType.browser.push(logicalId) + } else if (bundle instanceof PlaywrightCheckLocalBundle) { + bundledChecksByType.playwright.push(logicalId) + } + } + + if (bundledChecksByType.browser.length) { + this.style.actionStart('Uploading Playwright snapshots') + try { + for (const logicalId of bundledChecksByType.browser) { + const bundle = projectBundle.data.check[logicalId].bundle as BrowserCheckBundle + bundle.snapshots = await uploadSnapshots(bundle.rawSnapshots) + } + this.style.actionSuccess() + } catch (err) { + this.style.actionFailure() + throw err + } + } - for (const { bundle: check } of checkBundles) { - if (!(check instanceof BrowserCheckBundle)) { - continue + if (bundledChecksByType.playwright.length) { + this.style.actionStart('Uploading Playwright code bundles') + try { + for (const logicalId of bundledChecksByType.playwright) { + const resourceData = projectBundle.data.check[logicalId] + const bundle = resourceData.bundle as PlaywrightCheckLocalBundle + resourceData.bundle = await bundle.store() + } + this.style.actionSuccess() + } catch (err) { + this.style.actionFailure() + throw err } - check.snapshots = await uploadSnapshots(check.rawSnapshots) } if (this.fancy) { ux.action.stop() } + const checkBundles = Object.values(projectBundle.data.check) + if (!checkBundles.length) { this.log(`Unable to find checks to run${filePatterns[0] !== '.*' ? ' using [FILEARGS]=\'' + filePatterns + '\'' : ''}.`) return diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index 8a730ef2f..f9648fba9 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -3,10 +3,10 @@ import { Flags } from '@oclif/core' import { AuthCommand } from './authCommand' import { parseProject } from '../services/project-parser' import { loadChecklyConfig } from '../services/checkly-config-loader' -import type { Runtime } from '../rest/runtimes' import { Diagnostics } from '../constructs' import { splitConfigFilePath } from '../services/util' import commonMessages from '../messages/common-messages' +import { Runtime } from '../runtimes' export default class Validate extends AuthCommand { static coreCommand = true @@ -39,7 +39,7 @@ export default class Validate extends AuthCommand { constructs: checklyConfigConstructs, } = await loadChecklyConfig(configDirectory, configFilenames) const account = this.account - const { data: avilableRuntimes } = await api.runtimes.getAll() + const availableRuntimes = await api.runtimes.getAll() const project = await parseProject({ directory: configDirectory, projectLogicalId: checklyConfig.logicalId, @@ -51,7 +51,7 @@ export default class Validate extends AuthCommand { ignoreDirectoriesMatch: checklyConfig.checks?.ignoreDirectoriesMatch, checkDefaults: checklyConfig.checks, browserCheckDefaults: checklyConfig.checks?.browserChecks, - availableRuntimes: avilableRuntimes.reduce((acc, runtime) => { + availableRuntimes: availableRuntimes.reduce((acc, runtime) => { acc[runtime.name] = runtime return acc }, > {}), diff --git a/packages/cli/src/constructs/__tests__/api-check.spec.ts b/packages/cli/src/constructs/__tests__/api-check.spec.ts index 7d1377b46..b89831b99 100644 --- a/packages/cli/src/constructs/__tests__/api-check.spec.ts +++ b/packages/cli/src/constructs/__tests__/api-check.spec.ts @@ -1,214 +1,317 @@ import fs from 'node:fs' import path from 'node:path' -import { describe, it, expect, beforeEach, afterAll } from 'vitest' +import { describe, it, expect, afterAll, beforeAll } from 'vitest' -import { ApiCheck, CheckGroup, Request } from '../index' -import { Project, Session } from '../project' +import { FixtureSandbox } from '../../testing/fixture-sandbox' +import { ParseProjectOutput } from '../../commands/debug/parse-project' -const runtimes = { - '2022.10': { name: '2022.10', 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' } }, -} +async function parseProject (fixt: FixtureSandbox, ...args: string[]): Promise { + const result = await fixt.run('npx', [ + 'checkly', + 'debug', + 'parse-project', + ...args, + ]) + + if (result.exitCode !== 0) { + // eslint-disable-next-line no-console + console.error('stderr', result.stderr) + // eslint-disable-next-line no-console + console.error('stdout', result.stdout) + } + + expect(result.exitCode).toBe(0) -const request: Request = { - method: 'GET', - url: 'https://acme.com', + const output: ParseProjectOutput = JSON.parse(result.stdout) + + return output } -describe('ApiCheck', () => { - beforeEach(() => { - Session.resetSharedFiles() - }) +const DEFAULT_TEST_TIMEOUT = 30_000 - afterAll(() => { - Session.resetSharedFiles() - }) +describe('ApiCheck', () => { + let fixt: FixtureSandbox - it('should correctly load file script dependencies', async () => { - Session.basePath = __dirname - Session.availableRuntimes = runtimes - const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) - const bundle = await ApiCheck.bundle(getFilePath('entrypoint.js'), '2022.10') - Session.basePath = undefined - - expect(bundle).toEqual({ - script: fs.readFileSync(getFilePath('entrypoint.js')).toString(), - scriptPath: 'fixtures/api-check/entrypoint.js', - dependencies: [ - 0, - 1, - ], + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'api-check'), }) + }, 180_000) - expect(Session.sharedFiles).toEqual([ - { - path: 'fixtures/api-check/dep1.js', - content: fs.readFileSync(getFilePath('dep1.js')).toString(), - }, - { - path: 'fixtures/api-check/dep2.js', - content: fs.readFileSync(getFilePath('dep2.js')).toString(), - }, - ]) + afterAll(async () => { + await fixt?.destroy() }) - it('should fail to bundle if runtime is not specified and default runtime is not set', async () => { - const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) - const bundle = async () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _bundle = await ApiCheck.bundle(getFilePath('entrypoint.js'), undefined) - } - - Session.basePath = __dirname - Session.availableRuntimes = runtimes - Session.defaultRuntimeId = undefined - await expect(bundle()).rejects.toThrow('runtime is not set') - Session.basePath = undefined - Session.defaultRuntimeId = undefined - }) + it('should correctly load file script dependencies', async () => { + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-script-dependencies/checkly.config.js'), + ) - it('should successfully bundle if runtime is not specified but default runtime is set', async () => { - const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) - const bundle = async () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _bundle = await ApiCheck.bundle(getFilePath('entrypoint.js'), undefined) - } - - Session.basePath = __dirname - Session.availableRuntimes = runtimes - Session.defaultRuntimeId = '2022.10' - await expect(bundle()).resolves.not.toThrow() - Session.basePath = undefined - Session.defaultRuntimeId = undefined - }) + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + sharedFiles: [ + expect.objectContaining({ + path: 'package.json', + content: fs.readFileSync(fixt.abspath('package.json'), 'utf8'), + }), + expect.objectContaining({ + path: 'package-lock.json', + content: fs.readFileSync(fixt.abspath('package-lock.json'), 'utf8'), + }), + expect.objectContaining({ + path: 'test-cases/test-script-dependencies/dep1.js', + content: fs.readFileSync(fixt.abspath('test-cases/test-script-dependencies/dep1.js'), 'utf8'), + }), + expect.objectContaining({ + path: 'test-cases/test-script-dependencies/dep2.js', + content: fs.readFileSync(fixt.abspath('test-cases/test-script-dependencies/dep2.js'), 'utf8'), + }), + ], + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check-setupScript', + type: 'check', + member: true, + payload: expect.objectContaining({ + setupScriptPath: 'test-cases/test-script-dependencies/entrypoint.js', + setupScriptDependencies: [ + 0, + 1, + 2, + 3, + ], + }), + }), + expect.objectContaining({ + logicalId: 'check-tearDownScript', + type: 'check', + member: true, + payload: expect.objectContaining({ + tearDownScriptPath: 'test-cases/test-script-dependencies/entrypoint.js', + tearDownScriptDependencies: [ + 0, + 1, + 2, + 3, + ], + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) it('should fail to bundle if runtime is not supported even if default runtime is set', async () => { - const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) - const bundle = async () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _bundle = await ApiCheck.bundle(getFilePath('entrypoint.js'), '9999.99') - } - - Session.basePath = __dirname - Session.availableRuntimes = runtimes - Session.defaultRuntimeId = '2022.02' - await expect(bundle()).rejects.toThrow('9999.99 is not supported') - Session.basePath = undefined - Session.defaultRuntimeId = undefined - }) + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-invalid-runtime/checkly.config.js'), + ) - it('should not synthesize runtime if not specified even if default runtime is set', async () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - Session.availableRuntimes = runtimes - Session.defaultRuntimeId = '2022.02' - const apiCheck = new ApiCheck('test-check', { - name: 'Test Check', - request, - }) - const bundle = await apiCheck.bundle() - const payload = bundle.synthesize() - expect(payload.runtimeId).toBeUndefined() - Session.defaultRuntimeId = undefined - }) + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: true, + benign: false, + observations: expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('is not a known runtime.'), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) it('should synthesize runtime if specified', async () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - Session.availableRuntimes = runtimes - Session.defaultRuntimeId = '2022.02' - const apiCheck = new ApiCheck('test-check', { - name: 'Test Check', - runtimeId: '2022.02', - request, - }) - const bundle = await apiCheck.bundle() - const payload = bundle.synthesize() - expect(payload.runtimeId).toEqual('2022.02') - Session.defaultRuntimeId = undefined - }) + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-runtime/checkly.config.js'), + ) - it('should apply default check settings', () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - Session.checkDefaults = { tags: ['default tags'] } - const apiCheck = new ApiCheck('test-check', { - name: 'Test Check', - request, - }) - Session.checkDefaults = undefined - expect(apiCheck).toMatchObject({ tags: ['default tags'] }) - }) + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check-with-runtime', + type: 'check', + member: true, + payload: expect.objectContaining({ + runtimeId: '2025.04', + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) - it('should overwrite default check settings with check-specific config', () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - Session.checkDefaults = { tags: ['default tags'] } - const apiCheck = new ApiCheck('test-check', { - name: 'Test Check', - tags: ['test check'], - request, - }) - Session.checkDefaults = undefined - expect(apiCheck).toMatchObject({ tags: ['test check'] }) - }) + it('should not synthesize default runtime', async () => { + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-runtime/checkly.config.js'), + '--default-runtime', + '2024.09', + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check-without-runtime', + type: 'check', + member: true, + // The default runtime is used for sanity checks but is not + // serialized. This is done so that all checks can be changed + // at once by changing the account default runtime. + payload: expect.not.objectContaining({ + runtimeId: expect.anything(), + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) + + it('should apply default check settings', async () => { + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-check-defaults/checkly.config.js'), + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check-should-have-defaults', + type: 'check', + member: true, + payload: expect.objectContaining({ + tags: ['default tags'], + }), + }), + expect.objectContaining({ + logicalId: 'check-should-not-have-defaults', + type: 'check', + member: true, + payload: expect.objectContaining({ + tags: ['not default tags'], + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) it('should support setting groups with `groupId`', async () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - const group = new CheckGroup('main-group', { name: 'Main Group', locations: [] }) - const check = new ApiCheck('main-check', { - name: 'Main Check', - request, - groupId: group.ref(), - }) - const bundle = await check.bundle() - expect(bundle.synthesize()).toMatchObject({ groupId: { ref: 'main-group' } }) - }) + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-groupId-mapping/checkly.config.js'), + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'test-group', + type: 'check-group', + member: true, + }), + expect.objectContaining({ + logicalId: 'check', + type: 'check', + member: true, + payload: expect.objectContaining({ + groupId: { + ref: 'test-group', + }, + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) it('should support setting groups with `group`', async () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - const group = new CheckGroup('main-group', { name: 'Main Group', locations: [] }) - const check = new ApiCheck('main-check', { - name: 'Main Check', - request, - group, - }) - const bundle = await check.bundle() - expect(bundle.synthesize()).toMatchObject({ groupId: { ref: 'main-group' } }) - }) + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-group-mapping/checkly.config.js'), + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'test-group', + type: 'check-group', + member: true, + }), + expect.objectContaining({ + logicalId: 'check', + type: 'check', + member: true, + payload: expect.objectContaining({ + groupId: { + ref: 'test-group', + }, + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) describe('retryStrategy', () => { it('should synthesize `onlyOn`', async () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - const apiCheck = new ApiCheck('test-check', { - name: 'Test Check', - runtimeId: '2022.02', - request, - retryStrategy: { - type: 'LINEAR', - onlyOn: 'NETWORK_ERROR', - }, - }) - const bundle = await apiCheck.bundle() - const payload = bundle.synthesize() - expect(payload.retryStrategy?.onlyOn).toEqual('NETWORK_ERROR') - }) + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-retryStrategy-onlyOn/checkly.config.js'), + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check', + type: 'check', + member: true, + payload: expect.objectContaining({ + retryStrategy: expect.objectContaining({ + type: 'LINEAR', + onlyOn: 'NETWORK_ERROR', + }), + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) }) }) diff --git a/packages/cli/src/constructs/__tests__/browser-check.spec.ts b/packages/cli/src/constructs/__tests__/browser-check.spec.ts index 1dc11ec9f..accff28c9 100644 --- a/packages/cli/src/constructs/__tests__/browser-check.spec.ts +++ b/packages/cli/src/constructs/__tests__/browser-check.spec.ts @@ -1,291 +1,423 @@ import fs from 'node:fs' import path from 'node:path' -import { describe, it, expect, beforeEach, afterAll } from 'vitest' +import { describe, it, expect, afterAll, beforeAll } from 'vitest' -import { BrowserCheck, CheckGroup } from '../index' -import { Project, Session } from '../project' +import { FixtureSandbox } from '../../testing/fixture-sandbox' +import { ParseProjectOutput } from '../../commands/debug/parse-project' -const runtimes = { - '2022.10': { name: '2022.10', 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' } }, +async function parseProject (fixt: FixtureSandbox, ...args: string[]): Promise { + const result = await fixt.run('npx', [ + 'checkly', + 'debug', + 'parse-project', + ...args, + ]) + + if (result.exitCode !== 0) { + // eslint-disable-next-line no-console + console.error('stderr', result.stderr) + // eslint-disable-next-line no-console + console.error('stdout', result.stdout) + } + + expect(result.exitCode).toBe(0) + + const output: ParseProjectOutput = JSON.parse(result.stdout) + + return output } +const DEFAULT_TEST_TIMEOUT = 30_000 + describe('BrowserCheck', () => { - beforeEach(() => { - Session.resetSharedFiles() - }) + let fixt: FixtureSandbox - afterAll(() => { - Session.resetSharedFiles() + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'browser-check'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() }) it('should correctly load file dependencies', async () => { - Session.basePath = __dirname - Session.availableRuntimes = runtimes - const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'browser-check', filename) - const bundle = await BrowserCheck.bundle(getFilePath('entrypoint.js'), '2022.10') - Session.basePath = undefined - - expect(bundle).toMatchObject({ - script: fs.readFileSync(getFilePath('entrypoint.js')).toString(), - scriptPath: 'fixtures/browser-check/entrypoint.js', - dependencies: [ - 0, - 1, - ], - }) + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-code-dependencies/checkly.config.js'), + ) - expect(Session.sharedFiles).toEqual([ - { - path: 'fixtures/browser-check/dep1.js', - content: fs.readFileSync(getFilePath('dep1.js')).toString(), - }, - { - path: 'fixtures/browser-check/dep2.js', - content: fs.readFileSync(getFilePath('dep2.js')).toString(), - }, - ]) - }) + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + sharedFiles: [ + expect.objectContaining({ + path: 'package.json', + content: fs.readFileSync(fixt.abspath('package.json'), 'utf8'), + }), + expect.objectContaining({ + path: 'package-lock.json', + content: fs.readFileSync(fixt.abspath('package-lock.json'), 'utf8'), + }), + expect.objectContaining({ + path: 'test-cases/test-code-dependencies/dep1.js', + content: fs.readFileSync(fixt.abspath('test-cases/test-code-dependencies/dep1.js'), 'utf8'), + }), + expect.objectContaining({ + path: 'test-cases/test-code-dependencies/dep2.js', + content: fs.readFileSync(fixt.abspath('test-cases/test-code-dependencies/dep2.js'), 'utf8'), + }), + ], + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check', + type: 'check', + member: true, + payload: expect.objectContaining({ + scriptPath: 'test-cases/test-code-dependencies/entrypoint.js', + script: fs.readFileSync(fixt.abspath('test-cases/test-code-dependencies/entrypoint.js'), 'utf8'), + dependencies: [ + 0, + 1, + 2, + 3, + ], + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) - it('should fail to bundle if runtime is not specified and default runtime is not set', async () => { - const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) - const bundle = async () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _bundle = await BrowserCheck.bundle(getFilePath('entrypoint.js'), undefined) - } - - Session.basePath = __dirname - Session.availableRuntimes = runtimes - Session.defaultRuntimeId = undefined - await expect(bundle()).rejects.toThrow('runtime is not set') - Session.basePath = undefined - Session.defaultRuntimeId = undefined - }) + // it('should fail to bundle if runtime is not specified and default runtime is not set', async () => { + // const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) + // const bundle = async () => { + // // eslint-disable-next-line @typescript-eslint/no-unused-vars + // const _bundle = await BrowserCheck.bundle(getFilePath('entrypoint.js'), undefined) + // } - it('should successfully bundle if runtime is not specified but default runtime is set', async () => { - const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) - const bundle = async () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _bundle = await BrowserCheck.bundle(getFilePath('entrypoint.js'), undefined) - } - - Session.basePath = __dirname - Session.availableRuntimes = runtimes - Session.defaultRuntimeId = '2022.10' - await expect(bundle()).resolves.not.toThrow() - Session.basePath = undefined - Session.defaultRuntimeId = undefined - }) + // Session.basePath = __dirname + // Session.availableRuntimes = runtimes + // Session.defaultRuntimeId = undefined + // await expect(bundle()).rejects.toThrow('runtime is not set') + // Session.basePath = undefined + // Session.defaultRuntimeId = undefined + // }, DEFAULT_TEST_TIMEOUT) + + // it('should successfully bundle if runtime is not specified but default runtime is set', async () => { + // const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) + // const bundle = async () => { + // // eslint-disable-next-line @typescript-eslint/no-unused-vars + // const _bundle = await BrowserCheck.bundle(getFilePath('entrypoint.js'), undefined) + // } + + // Session.basePath = __dirname + // Session.availableRuntimes = runtimes + // Session.defaultRuntimeId = '2022.10' + // await expect(bundle()).resolves.not.toThrow() + // Session.basePath = undefined + // Session.defaultRuntimeId = undefined + // }, DEFAULT_TEST_TIMEOUT) it('should fail to bundle if runtime is not supported even if default runtime is set', async () => { - const getFilePath = (filename: string) => path.join(__dirname, 'fixtures', 'api-check', filename) - const bundle = async () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const _bundle = await BrowserCheck.bundle(getFilePath('entrypoint.js'), '9999.99') - } - - Session.basePath = __dirname - Session.availableRuntimes = runtimes - Session.defaultRuntimeId = '2022.02' - await expect(bundle()).rejects.toThrow('9999.99 is not supported') - Session.basePath = undefined - Session.defaultRuntimeId = undefined - }) + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-invalid-runtime/checkly.config.js'), + ) - it('should not synthesize runtime if not specified even if default runtime is set', async () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - Session.availableRuntimes = runtimes - Session.defaultRuntimeId = '2022.02' - const browserCheck = new BrowserCheck('test-check', { - name: 'Test Check', - code: { content: 'console.log("test check")' }, - }) - const bundle = await browserCheck.bundle() - const payload = bundle.synthesize() - expect(payload.runtimeId).toBeUndefined() - Session.defaultRuntimeId = undefined - }) + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: true, + benign: false, + observations: expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('is not a known runtime.'), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) it('should synthesize runtime if specified', async () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - Session.availableRuntimes = runtimes - Session.defaultRuntimeId = '2022.02' - const browserCheck = new BrowserCheck('test-check', { - name: 'Test Check', - runtimeId: '2022.02', - code: { content: 'console.log("test check")' }, - }) - const bundle = await browserCheck.bundle() - const payload = bundle.synthesize() - expect(payload.runtimeId).toEqual('2022.02') - Session.defaultRuntimeId = undefined - }) + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-runtime/checkly.config.js'), + ) - it('should apply default check settings', () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - Session.checkDefaults = { tags: ['default tags'] } - const browserCheck = new BrowserCheck('test-check', { - name: 'Test Check', - code: { content: 'console.log("test check")' }, - }) - Session.checkDefaults = undefined - expect(browserCheck).toMatchObject({ tags: ['default tags'] }) - }) + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check-with-runtime', + type: 'check', + member: true, + payload: expect.objectContaining({ + runtimeId: '2025.04', + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) - it('should overwrite default check settings with check-specific config', () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - Session.checkDefaults = { tags: ['default tags'] } - const browserCheck = new BrowserCheck('test-check', { - name: 'Test Check', - tags: ['test check'], - code: { content: 'console.log("test check")' }, - }) - Session.checkDefaults = undefined - expect(browserCheck).toMatchObject({ tags: ['test check'] }) - }) + it('should not synthesize default runtime', async () => { + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-runtime/checkly.config.js'), + '--default-runtime', + '2024.09', + ) - it('should apply browser check defaults instead of generic check defaults', () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - Session.checkDefaults = { tags: ['check default'] } - Session.browserCheckDefaults = { tags: ['browser check default'] } - const browserCheck = new BrowserCheck('test-check', { - name: 'Test Check', - code: { content: 'console.log("test check")' }, - }) - Session.checkDefaults = undefined - Session.browserCheckDefaults = undefined - expect(browserCheck).toMatchObject({ tags: ['browser check default'] }) - }) + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check-without-runtime', + type: 'check', + member: true, + // The default runtime is used for sanity checks but is not + // serialized. This is done so that all checks can be changed + // at once by changing the account default runtime. + payload: expect.not.objectContaining({ + runtimeId: expect.anything(), + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) + + it('should apply default check settings', async () => { + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-check-defaults/checkly.config.js'), + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check-should-have-defaults', + type: 'check', + member: true, + payload: expect.objectContaining({ + tags: ['default tags'], + }), + }), + expect.objectContaining({ + logicalId: 'check-should-not-have-defaults', + type: 'check', + member: true, + payload: expect.objectContaining({ + tags: ['not default tags'], + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) + + it('should apply browser check defaults instead of generic check defaults', async () => { + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-browser-check-defaults/checkly.config.js'), + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check-should-have-defaults', + type: 'check', + member: true, + payload: expect.objectContaining({ + tags: ['browser default tags'], + }), + }), + expect.objectContaining({ + logicalId: 'check-should-not-have-defaults', + type: 'check', + member: true, + payload: expect.objectContaining({ + tags: ['not default tags'], + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) it('should support setting groups with `groupId`', async () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - const group = new CheckGroup('main-group', { name: 'Main Group', locations: [] }) - const check = new BrowserCheck('main-check', { - name: 'Main Check', - code: { content: '' }, - groupId: group.ref(), - }) - const bundle = await check.bundle() - expect(bundle.synthesize()).toMatchObject({ groupId: { ref: 'main-group' } }) - }) + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-groupId-mapping/checkly.config.js'), + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'test-group', + type: 'check-group', + member: true, + }), + expect.objectContaining({ + logicalId: 'check', + type: 'check', + member: true, + payload: expect.objectContaining({ + groupId: { + ref: 'test-group', + }, + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) it('should support setting groups with `group`', async () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - const group = new CheckGroup('main-group', { name: 'Main Group', locations: [] }) - const check = new BrowserCheck('main-check', { - name: 'Main Check', - code: { content: '' }, - group, - }) - const bundle = await check.bundle() - expect(bundle.synthesize()).toMatchObject({ groupId: { ref: 'main-group' } }) - }) + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-group-mapping/checkly.config.js'), + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'test-group', + type: 'check-group', + member: true, + }), + expect.objectContaining({ + logicalId: 'check', + type: 'check', + member: true, + payload: expect.objectContaining({ + groupId: { + ref: 'test-group', + }, + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) describe('playwrightConfig', () => { - it('should set from check defaults', () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - const defaults = { - playwrightConfig: { - use: { - baseURL: 'https://example.org', - }, - }, - } - Session.checkDefaults = { - ...defaults, - } - Session.browserCheckDefaults = {} - const browserCheck = new BrowserCheck('test-check', { - name: 'Test Check', - code: { content: 'console.log("test check")' }, - }) - Session.checkDefaults = undefined - Session.browserCheckDefaults = undefined - expect(browserCheck).toMatchObject(defaults) - }) + it('should set from check defaults', async () => { + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-check-defaults-playwright-config/checkly.config.js'), + ) - it('should set from browser check defaults', () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - const defaults = { - playwrightConfig: { - use: { - baseURL: 'https://example.org', - }, - }, - } - Session.checkDefaults = {} - Session.browserCheckDefaults = { - ...defaults, - } - const browserCheck = new BrowserCheck('test-check', { - name: 'Test Check', - code: { content: 'console.log("test check")' }, - }) - Session.checkDefaults = undefined - Session.browserCheckDefaults = undefined - expect(browserCheck).toMatchObject(defaults) - }) + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check-should-have-defaults', + type: 'check', + member: true, + payload: expect.objectContaining({ + playwrightConfig: { + use: { + baseURL: 'https://example.org/check-defaults', + }, + }, + }), + }), + expect.objectContaining({ + logicalId: 'check-should-not-have-defaults', + type: 'check', + member: true, + payload: expect.objectContaining({ + playwrightConfig: { + use: { + baseURL: 'https://example.org/self', + }, + }, + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) - it('should not override with default if set', () => { - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - const defaults = { - playwrightConfig: { - use: { - baseURL: 'https://example.org/foo', - }, - }, - } - Session.checkDefaults = { - ...defaults, - } - Session.browserCheckDefaults = { - ...defaults, - } - const custom = { - playwrightConfig: { - use: { - baseURL: 'https://example.org/bar', - }, - }, - } - const browserCheck = new BrowserCheck('test-check', { - name: 'Test Check', - code: { content: 'console.log("test check")' }, - ...custom, - }) - Session.checkDefaults = undefined - Session.browserCheckDefaults = undefined - expect(browserCheck).toMatchObject(custom) - }) + it('should set from browser check defaults', async () => { + const output = await parseProject( + fixt, + '--config', + fixt.abspath('test-cases/test-browser-check-defaults-playwright-config/checkly.config.js'), + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check-should-have-defaults', + type: 'check', + member: true, + payload: expect.objectContaining({ + playwrightConfig: { + use: { + baseURL: 'https://example.org/browser-defaults', + }, + }, + }), + }), + expect.objectContaining({ + logicalId: 'check-should-not-have-defaults', + type: 'check', + member: true, + payload: expect.objectContaining({ + playwrightConfig: { + use: { + baseURL: 'https://example.org/self', + }, + }, + }), + }), + ]), + }), + })) + }, DEFAULT_TEST_TIMEOUT) }) }) diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/package.json b/packages/cli/src/constructs/__tests__/fixtures/api-check/package.json new file mode 100644 index 000000000..69ac5c067 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/package.json @@ -0,0 +1,5 @@ +{ + "name": "api-check-fixture", + "type": "module", + "devDependencies": {} +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-check-defaults/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-check-defaults/checkly.config.js new file mode 100644 index 000000000..209e706cb --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-check-defaults/checkly.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + tags: ['default tags'], + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-check-defaults/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-check-defaults/test.check.js new file mode 100644 index 000000000..079afc5a6 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-check-defaults/test.check.js @@ -0,0 +1,18 @@ +import { ApiCheck } from 'checkly/constructs' + +new ApiCheck('check-should-have-defaults', { + name: 'Foo', + request: { + method: 'GET', + url: 'https://example.org', + }, +}) + +new ApiCheck('check-should-not-have-defaults', { + name: 'Foo', + request: { + method: 'GET', + url: 'https://example.org', + }, + tags: ['not default tags'], +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-group-mapping/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-group-mapping/checkly.config.js new file mode 100644 index 000000000..19a81a24e --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-group-mapping/checkly.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-group-mapping/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-group-mapping/test.check.js new file mode 100644 index 000000000..03ca99da0 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-group-mapping/test.check.js @@ -0,0 +1,14 @@ +import { ApiCheck, CheckGroupV2 } from 'checkly/constructs' + +const group = new CheckGroupV2('test-group', { + name: 'Group', +}) + +new ApiCheck('check', { + name: 'Foo', + request: { + method: 'GET', + url: 'https://example.org', + }, + group, +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-groupId-mapping/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-groupId-mapping/checkly.config.js new file mode 100644 index 000000000..19a81a24e --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-groupId-mapping/checkly.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-groupId-mapping/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-groupId-mapping/test.check.js new file mode 100644 index 000000000..d27d8e4cb --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-groupId-mapping/test.check.js @@ -0,0 +1,14 @@ +import { ApiCheck, CheckGroupV2 } from 'checkly/constructs' + +const group = new CheckGroupV2('test-group', { + name: 'Group', +}) + +new ApiCheck('check', { + name: 'Foo', + request: { + method: 'GET', + url: 'https://example.org', + }, + groupId: group.ref(), +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-invalid-runtime/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-invalid-runtime/checkly.config.js new file mode 100644 index 000000000..17ca5c791 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-invalid-runtime/checkly.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + runtimeId: '2025.04', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-invalid-runtime/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-invalid-runtime/test.check.js new file mode 100644 index 000000000..eeafed993 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-invalid-runtime/test.check.js @@ -0,0 +1,13 @@ +import { ApiCheck } from 'checkly/constructs' + +new ApiCheck('check', { + name: 'Foo', + request: { + method: 'GET', + url: 'https://example.org', + }, + runtimeId: '9999.99', + setupScript: { + content: 'console.log()', + }, +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-retryStrategy-onlyOn/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-retryStrategy-onlyOn/checkly.config.js new file mode 100644 index 000000000..19a81a24e --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-retryStrategy-onlyOn/checkly.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-retryStrategy-onlyOn/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-retryStrategy-onlyOn/test.check.js new file mode 100644 index 000000000..fc119af15 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-retryStrategy-onlyOn/test.check.js @@ -0,0 +1,12 @@ +import { ApiCheck, RetryStrategyBuilder } from 'checkly/constructs' + +new ApiCheck('check', { + name: 'Foo', + request: { + method: 'GET', + url: 'https://example.org', + }, + retryStrategy: RetryStrategyBuilder.linearStrategy({ + onlyOn: 'NETWORK_ERROR', + }) +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-runtime/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-runtime/checkly.config.js new file mode 100644 index 000000000..19a81a24e --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-runtime/checkly.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-runtime/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-runtime/test.check.js new file mode 100644 index 000000000..1f8e3d6d2 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-runtime/test.check.js @@ -0,0 +1,24 @@ +import { ApiCheck } from 'checkly/constructs' + +new ApiCheck('check-with-runtime', { + name: 'Foo', + request: { + method: 'GET', + url: 'https://example.org', + }, + runtimeId: '2025.04', + setupScript: { + content: 'console.log()', + }, +}) + +new ApiCheck('check-without-runtime', { + name: 'Foo', + request: { + method: 'GET', + url: 'https://example.org', + }, + setupScript: { + content: 'console.log()', + }, +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-script-dependencies/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-script-dependencies/checkly.config.js new file mode 100644 index 000000000..19a81a24e --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-script-dependencies/checkly.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/dep1.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-script-dependencies/dep1.js similarity index 100% rename from packages/cli/src/constructs/__tests__/fixtures/api-check/dep1.js rename to packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-script-dependencies/dep1.js diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/dep2.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-script-dependencies/dep2.js similarity index 100% rename from packages/cli/src/constructs/__tests__/fixtures/api-check/dep2.js rename to packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-script-dependencies/dep2.js diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/entrypoint.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-script-dependencies/entrypoint.js similarity index 100% rename from packages/cli/src/constructs/__tests__/fixtures/api-check/entrypoint.js rename to packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-script-dependencies/entrypoint.js diff --git a/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-script-dependencies/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-script-dependencies/test.check.js new file mode 100644 index 000000000..25c91eaa3 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/api-check/test-cases/test-script-dependencies/test.check.js @@ -0,0 +1,23 @@ +import { ApiCheck } from 'checkly/constructs' + +new ApiCheck('check-setupScript', { + name: 'Foo', + request: { + method: 'GET', + url: 'https://example.org', + }, + setupScript: { + entrypoint: './entrypoint.js' + } +}) + +new ApiCheck('check-tearDownScript', { + name: 'Foo', + request: { + method: 'GET', + url: 'https://example.org', + }, + tearDownScript: { + entrypoint: './entrypoint.js' + } +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/package.json b/packages/cli/src/constructs/__tests__/fixtures/browser-check/package.json new file mode 100644 index 000000000..69ac5c067 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/package.json @@ -0,0 +1,5 @@ +{ + "name": "api-check-fixture", + "type": "module", + "devDependencies": {} +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-browser-check-defaults-playwright-config/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-browser-check-defaults-playwright-config/checkly.config.js new file mode 100644 index 000000000..69686800d --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-browser-check-defaults-playwright-config/checkly.config.js @@ -0,0 +1,25 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + tags: ['default tags'], + playwrightConfig: { + use: { + baseURL: 'https://example.org/check-defaults', + }, + }, + browserChecks: { + tags: ['browser default tags'], + playwrightConfig: { + use: { + baseURL: 'https://example.org/browser-defaults', + }, + }, + }, + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-browser-check-defaults-playwright-config/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-browser-check-defaults-playwright-config/test.check.js new file mode 100644 index 000000000..6f7bbcae2 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-browser-check-defaults-playwright-config/test.check.js @@ -0,0 +1,21 @@ +import { BrowserCheck } from 'checkly/constructs' + +new BrowserCheck('check-should-have-defaults', { + name: 'Foo', + code: { + content: 'console.log()', + }, +}) + +new BrowserCheck('check-should-not-have-defaults', { + name: 'Foo', + code: { + content: 'console.log()', + }, + tags: ['not default tags'], + playwrightConfig: { + use: { + baseURL: 'https://example.org/self', + }, + }, +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-browser-check-defaults/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-browser-check-defaults/checkly.config.js new file mode 100644 index 000000000..276911668 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-browser-check-defaults/checkly.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + tags: ['default tags'], + browserChecks: { + tags: ['browser default tags'], + }, + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-browser-check-defaults/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-browser-check-defaults/test.check.js new file mode 100644 index 000000000..e94625f2f --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-browser-check-defaults/test.check.js @@ -0,0 +1,16 @@ +import { BrowserCheck } from 'checkly/constructs' + +new BrowserCheck('check-should-have-defaults', { + name: 'Foo', + code: { + content: 'console.log()', + }, +}) + +new BrowserCheck('check-should-not-have-defaults', { + name: 'Foo', + code: { + content: 'console.log()', + }, + tags: ['not default tags'], +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-check-defaults-playwright-config/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-check-defaults-playwright-config/checkly.config.js new file mode 100644 index 000000000..1dcd61462 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-check-defaults-playwright-config/checkly.config.js @@ -0,0 +1,17 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + tags: ['default tags'], + playwrightConfig: { + use: { + baseURL: 'https://example.org/check-defaults', + }, + }, + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-check-defaults-playwright-config/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-check-defaults-playwright-config/test.check.js new file mode 100644 index 000000000..6f7bbcae2 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-check-defaults-playwright-config/test.check.js @@ -0,0 +1,21 @@ +import { BrowserCheck } from 'checkly/constructs' + +new BrowserCheck('check-should-have-defaults', { + name: 'Foo', + code: { + content: 'console.log()', + }, +}) + +new BrowserCheck('check-should-not-have-defaults', { + name: 'Foo', + code: { + content: 'console.log()', + }, + tags: ['not default tags'], + playwrightConfig: { + use: { + baseURL: 'https://example.org/self', + }, + }, +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-check-defaults/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-check-defaults/checkly.config.js new file mode 100644 index 000000000..209e706cb --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-check-defaults/checkly.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + tags: ['default tags'], + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-check-defaults/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-check-defaults/test.check.js new file mode 100644 index 000000000..e94625f2f --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-check-defaults/test.check.js @@ -0,0 +1,16 @@ +import { BrowserCheck } from 'checkly/constructs' + +new BrowserCheck('check-should-have-defaults', { + name: 'Foo', + code: { + content: 'console.log()', + }, +}) + +new BrowserCheck('check-should-not-have-defaults', { + name: 'Foo', + code: { + content: 'console.log()', + }, + tags: ['not default tags'], +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-code-dependencies/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-code-dependencies/checkly.config.js new file mode 100644 index 000000000..19a81a24e --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-code-dependencies/checkly.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/dep1.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-code-dependencies/dep1.js similarity index 100% rename from packages/cli/src/constructs/__tests__/fixtures/browser-check/dep1.js rename to packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-code-dependencies/dep1.js diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/dep2.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-code-dependencies/dep2.js similarity index 100% rename from packages/cli/src/constructs/__tests__/fixtures/browser-check/dep2.js rename to packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-code-dependencies/dep2.js diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/entrypoint.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-code-dependencies/entrypoint.js similarity index 100% rename from packages/cli/src/constructs/__tests__/fixtures/browser-check/entrypoint.js rename to packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-code-dependencies/entrypoint.js diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-code-dependencies/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-code-dependencies/test.check.js new file mode 100644 index 000000000..896573102 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-code-dependencies/test.check.js @@ -0,0 +1,8 @@ +import { BrowserCheck } from 'checkly/constructs' + +new BrowserCheck('check', { + name: 'Foo', + code: { + entrypoint: './entrypoint.js' + } +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-group-mapping/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-group-mapping/checkly.config.js new file mode 100644 index 000000000..19a81a24e --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-group-mapping/checkly.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-group-mapping/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-group-mapping/test.check.js new file mode 100644 index 000000000..ca512143d --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-group-mapping/test.check.js @@ -0,0 +1,13 @@ +import { BrowserCheck, CheckGroupV2 } from 'checkly/constructs' + +const group = new CheckGroupV2('test-group', { + name: 'Group', +}) + +new BrowserCheck('check', { + name: 'Foo', + code: { + content: 'console.log()', + }, + group, +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-groupId-mapping/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-groupId-mapping/checkly.config.js new file mode 100644 index 000000000..19a81a24e --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-groupId-mapping/checkly.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-groupId-mapping/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-groupId-mapping/test.check.js new file mode 100644 index 000000000..d165ecdb9 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-groupId-mapping/test.check.js @@ -0,0 +1,13 @@ +import { BrowserCheck, CheckGroupV2 } from 'checkly/constructs' + +const group = new CheckGroupV2('test-group', { + name: 'Group', +}) + +new BrowserCheck('check', { + name: 'Foo', + code: { + content: 'console.log()', + }, + groupId: group.ref(), +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-invalid-runtime/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-invalid-runtime/checkly.config.js new file mode 100644 index 000000000..17ca5c791 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-invalid-runtime/checkly.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + runtimeId: '2025.04', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-invalid-runtime/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-invalid-runtime/test.check.js new file mode 100644 index 000000000..5d4d3e933 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-invalid-runtime/test.check.js @@ -0,0 +1,9 @@ +import { BrowserCheck } from 'checkly/constructs' + +new BrowserCheck('check', { + name: 'Foo', + runtimeId: '9999.99', + code: { + content: 'console.log()', + }, +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-runtime/checkly.config.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-runtime/checkly.config.js new file mode 100644 index 000000000..19a81a24e --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-runtime/checkly.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.js', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-runtime/test.check.js b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-runtime/test.check.js new file mode 100644 index 000000000..f735b9bf8 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/browser-check/test-cases/test-runtime/test.check.js @@ -0,0 +1,16 @@ +import { BrowserCheck } from 'checkly/constructs' + +new BrowserCheck('check-with-runtime', { + name: 'Foo', + code: { + content: 'console.log()', + }, + runtimeId: '2025.04', +}) + +new BrowserCheck('check-without-runtime', { + name: 'Foo', + code: { + content: 'console.log()', + }, +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check-webserver/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check-webserver/package.json deleted file mode 100644 index f418b40c2..000000000 --- a/packages/cli/src/constructs/__tests__/fixtures/playwright-check-webserver/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "playwright-check-webserver-fixture", - "devDependencies": { - "@playwright/test": "^1.55.1" - } -} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/.gitignore b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/.gitignore deleted file mode 100644 index 2ccbe4656..000000000 --- a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/node_modules/ diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/package-lock.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/package-lock.json deleted file mode 100644 index 70b1cdb82..000000000 --- a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/package-lock.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "name": "playwright-check-fixture", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "playwright-check-fixture", - "devDependencies": { - "@playwright/test": "^1.55.1" - } - }, - "node_modules/@playwright/test": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.1.tgz", - "integrity": "sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.55.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.1.tgz", - "integrity": "sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.55.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.55.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.1.tgz", - "integrity": "sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - } - } -} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/package.json deleted file mode 100644 index 56be25d50..000000000 --- a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "playwright-check-fixture", - "devDependencies": { - "@playwright/test": "^1.55.1" - } -} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.exclude-fixtures.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.exclude-fixtures.config.ts new file mode 100644 index 000000000..d13832e93 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.exclude-fixtures.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.ts', + ignoreDirectoriesMatch: ['**/fixtures/**'], + playwrightConfigPath: './playwright.config.ts', + playwrightChecks: [ + { + logicalId: 'playwright-check-suite', + name: 'Playwright Check Suite', + } + ], + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.exclude-node-modules-if-include-not-explicit.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.exclude-node-modules-if-include-not-explicit.config.ts new file mode 100644 index 000000000..dd78853cc --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.exclude-node-modules-if-include-not-explicit.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.ts', + ignoreDirectoriesMatch: [], + include: ['**/*.js'], + playwrightConfigPath: './playwright.config.ts', + playwrightChecks: [ + { + logicalId: 'playwright-check-suite', + name: 'Playwright Check Suite', + } + ], + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.exclude-nothing.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.exclude-nothing.config.ts new file mode 100644 index 000000000..916f96306 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.exclude-nothing.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.ts', + ignoreDirectoriesMatch: [], + include: ['fixtures/**/*'], + playwrightConfigPath: './playwright.config.ts', + playwrightChecks: [ + { + logicalId: 'playwright-check-suite', + name: 'Playwright Check Suite', + } + ], + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.exclude-prefer-over-include.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.exclude-prefer-over-include.config.ts new file mode 100644 index 000000000..97c524d57 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.exclude-prefer-over-include.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.ts', + ignoreDirectoriesMatch: ['**/checkly/**'], + include: ['node_modules/checkly/**'], + playwrightConfigPath: './playwright.config.ts', + playwrightChecks: [ + { + logicalId: 'playwright-check-suite', + name: 'Playwright Check Suite', + } + ], + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.include-node-modules-if-explicit.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.include-node-modules-if-explicit.config.ts new file mode 100644 index 000000000..fbce080fb --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/checkly.include-node-modules-if-explicit.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Check Fixture', + logicalId: 'check-fixture', + checks: { + checkMatch: '**/*.check.ts', + ignoreDirectoriesMatch: [], + include: ['node_modules/checkly/**'], + playwrightConfigPath: './playwright.config.ts', + playwrightChecks: [ + { + logicalId: 'playwright-check-suite', + name: 'Playwright Check Suite', + } + ], + }, +}) + +export default config diff --git a/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/fixtures/mock-data.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/fixtures/mock-data.json similarity index 100% rename from packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/fixtures/mock-data.json rename to packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/fixtures/mock-data.json diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/package.json new file mode 100644 index 000000000..30ce59b6f --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/package.json @@ -0,0 +1,8 @@ +{ + "name": "playwright-bundle-test", + "version": "1.0.0", + "dependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/playwright.config.ts similarity index 100% rename from packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/playwright.config.ts rename to packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/playwright.config.ts diff --git a/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/tests/example.spec.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/tests/example.spec.ts similarity index 100% rename from packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/tests/example.spec.ts rename to packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-bundling/tests/example.spec.ts diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-default-ignored/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-default-ignored/checkly.config.ts new file mode 100644 index 000000000..ed287be4d --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-default-ignored/checkly.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + doubleCheck: true, + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-default-ignored/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-default-ignored/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-default-ignored/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-default-ignored/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-default-ignored/playwright.config.ts new file mode 100644 index 000000000..63897b611 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-default-ignored/playwright.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-default-ignored/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-default-ignored/test.check.ts new file mode 100644 index 000000000..e7ba23133 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-default-ignored/test.check.ts @@ -0,0 +1,6 @@ +import { PlaywrightCheck } from 'checkly/constructs' + +const check = new PlaywrightCheck('check', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-not-allowed/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-not-allowed/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-not-allowed/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-not-allowed/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-not-allowed/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-not-allowed/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-not-allowed/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-not-allowed/playwright.config.ts new file mode 100644 index 000000000..63897b611 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-not-allowed/playwright.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-not-allowed/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-not-allowed/test.check.ts new file mode 100644 index 000000000..07a80489e --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-doubleCheck-not-allowed/test.check.ts @@ -0,0 +1,10 @@ +import { PlaywrightCheck } from 'checkly/constructs' + +const check = new PlaywrightCheck('check', { + name: 'Check', + + playwrightConfigPath: './playwright.config.ts', + + // @ts-expect-error Testing a property that isn't part of the type. + doubleCheck: true, +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-group-mapping/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-group-mapping/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-group-mapping/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-group-mapping/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-group-mapping/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-group-mapping/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-group-mapping/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-group-mapping/playwright.config.ts new file mode 100644 index 000000000..63897b611 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-group-mapping/playwright.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-group-mapping/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-group-mapping/test.check.ts new file mode 100644 index 000000000..b473bc313 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-group-mapping/test.check.ts @@ -0,0 +1,11 @@ +import { CheckGroupV2, PlaywrightCheck } from 'checkly/constructs' + +const group = new CheckGroupV2('group', { + name: 'Group', +}) + +const check = new PlaywrightCheck('check', { + name: 'Check', + group, + playwrightConfigPath: './playwright.config.ts', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupId-mapping/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupId-mapping/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupId-mapping/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupId-mapping/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupId-mapping/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupId-mapping/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupId-mapping/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupId-mapping/playwright.config.ts new file mode 100644 index 000000000..63897b611 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupId-mapping/playwright.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupId-mapping/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupId-mapping/test.check.ts new file mode 100644 index 000000000..d6b1ba290 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupId-mapping/test.check.ts @@ -0,0 +1,11 @@ +import { CheckGroupV2, PlaywrightCheck } from 'checkly/constructs' + +const group = new CheckGroupV2('group', { + name: 'Group', +}) + +const check = new PlaywrightCheck('check', { + name: 'Check', + groupId: group.ref(), + playwrightConfigPath: './playwright.config.ts', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-mapping/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-mapping/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-mapping/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-mapping/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-mapping/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-mapping/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-mapping/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-mapping/playwright.config.ts new file mode 100644 index 000000000..63897b611 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-mapping/playwright.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-mapping/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-mapping/test.check.ts new file mode 100644 index 000000000..0461bee0b --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-mapping/test.check.ts @@ -0,0 +1,11 @@ +import { CheckGroupV2, PlaywrightCheck } from 'checkly/constructs' + +const group = new CheckGroupV2('group', { + name: 'b801a908-8d3c-4a94-92ab-cf15f58a59b4', +}) + +const check = new PlaywrightCheck('check', { + name: 'Check', + groupName: 'b801a908-8d3c-4a94-92ab-cf15f58a59b4', + playwrightConfigPath: './playwright.config.ts', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-not-found/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-not-found/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-not-found/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-not-found/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-not-found/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-not-found/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-not-found/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-not-found/playwright.config.ts new file mode 100644 index 000000000..63897b611 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-not-found/playwright.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-not-found/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-not-found/test.check.ts new file mode 100644 index 000000000..a66747d7a --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-not-found/test.check.ts @@ -0,0 +1,11 @@ +import { CheckGroupV2, PlaywrightCheck } from 'checkly/constructs' + +const group = new CheckGroupV2('group', { + name: 'foo', +}) + +const check = new PlaywrightCheck('check', { + name: 'Check', + groupName: 'bar', + playwrightConfigPath: './playwright.config.ts', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-group-conflict/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-group-conflict/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-group-conflict/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-group-conflict/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-group-conflict/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-group-conflict/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-group-conflict/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-group-conflict/playwright.config.ts new file mode 100644 index 000000000..63897b611 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-group-conflict/playwright.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-group-conflict/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-group-conflict/test.check.ts new file mode 100644 index 000000000..f63e99be2 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-group-conflict/test.check.ts @@ -0,0 +1,12 @@ +import { CheckGroupV2, PlaywrightCheck } from 'checkly/constructs' + +const group = new CheckGroupV2('group', { + name: 'foo', +}) + +const check = new PlaywrightCheck('check', { + name: 'Check', + groupName: 'foo', + group, + playwrightConfigPath: './playwright.config.ts', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-groupId-conflict/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-groupId-conflict/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-groupId-conflict/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-groupId-conflict/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-groupId-conflict/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-groupId-conflict/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-groupId-conflict/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-groupId-conflict/playwright.config.ts new file mode 100644 index 000000000..63897b611 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-groupId-conflict/playwright.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-groupId-conflict/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-groupId-conflict/test.check.ts new file mode 100644 index 000000000..f63e99be2 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-groupName-with-groupId-conflict/test.check.ts @@ -0,0 +1,12 @@ +import { CheckGroupV2, PlaywrightCheck } from 'checkly/constructs' + +const group = new CheckGroupV2('group', { + name: 'foo', +}) + +const check = new PlaywrightCheck('check', { + name: 'Check', + groupName: 'foo', + group, + playwrightConfigPath: './playwright.config.ts', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-in-project-not-allowed/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-in-project-not-allowed/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-in-project-not-allowed/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-in-project-not-allowed/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-in-project-not-allowed/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-in-project-not-allowed/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/playwright.config.headless-false-project.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-in-project-not-allowed/playwright.config.ts similarity index 100% rename from packages/cli/src/constructs/__tests__/fixtures/playwright-check/playwright.config.headless-false-project.ts rename to packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-in-project-not-allowed/playwright.config.ts diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-in-project-not-allowed/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-in-project-not-allowed/test.check.ts new file mode 100644 index 000000000..e7ba23133 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-in-project-not-allowed/test.check.ts @@ -0,0 +1,6 @@ +import { PlaywrightCheck } from 'checkly/constructs' + +const check = new PlaywrightCheck('check', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-not-allowed/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-not-allowed/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-not-allowed/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-not-allowed/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-not-allowed/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-not-allowed/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/playwright.config.headless-false.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-not-allowed/playwright.config.ts similarity index 100% rename from packages/cli/src/constructs/__tests__/fixtures/playwright-check/playwright.config.headless-false.ts rename to packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-not-allowed/playwright.config.ts diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-not-allowed/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-not-allowed/test.check.ts new file mode 100644 index 000000000..e7ba23133 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-false-not-allowed/test.check.ts @@ -0,0 +1,6 @@ +import { PlaywrightCheck } from 'checkly/constructs' + +const check = new PlaywrightCheck('check', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-true-allowed/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-true-allowed/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-true-allowed/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-true-allowed/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-true-allowed/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-true-allowed/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/playwright.config.headless-true.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-true-allowed/playwright.config.ts similarity index 100% rename from packages/cli/src/constructs/__tests__/fixtures/playwright-check/playwright.config.headless-true.ts rename to packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-true-allowed/playwright.config.ts diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-true-allowed/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-true-allowed/test.check.ts new file mode 100644 index 000000000..e7ba23133 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-true-allowed/test.check.ts @@ -0,0 +1,6 @@ +import { PlaywrightCheck } from 'checkly/constructs' + +const check = new PlaywrightCheck('check', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-unset-allowed/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-unset-allowed/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-unset-allowed/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-unset-allowed/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-unset-allowed/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-unset-allowed/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-unset-allowed/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-unset-allowed/playwright.config.ts new file mode 100644 index 000000000..0c054a8a7 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-unset-allowed/playwright.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './tests', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-unset-allowed/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-unset-allowed/test.check.ts new file mode 100644 index 000000000..e7ba23133 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-headless-unset-allowed/test.check.ts @@ -0,0 +1,6 @@ +import { PlaywrightCheck } from 'checkly/constructs' + +const check = new PlaywrightCheck('check', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-allowed/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-allowed/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-allowed/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-allowed/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-allowed/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-allowed/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-allowed/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-allowed/playwright.config.ts new file mode 100644 index 000000000..0c054a8a7 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-allowed/playwright.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './tests', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-allowed/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-allowed/test.check.ts new file mode 100644 index 000000000..43223adb1 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-allowed/test.check.ts @@ -0,0 +1,13 @@ +import { PlaywrightCheck } from 'checkly/constructs' + +const check1 = new PlaywrightCheck('check1', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', + installCommand: 'npm ci', +}) + +const check2 = new PlaywrightCheck('check2', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', + installCommand: 'pnpm i', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-unnecessary-playwright-install-warn/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-unnecessary-playwright-install-warn/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-unnecessary-playwright-install-warn/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-unnecessary-playwright-install-warn/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-unnecessary-playwright-install-warn/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-unnecessary-playwright-install-warn/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-unnecessary-playwright-install-warn/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-unnecessary-playwright-install-warn/playwright.config.ts new file mode 100644 index 000000000..0c054a8a7 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-unnecessary-playwright-install-warn/playwright.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './tests', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-unnecessary-playwright-install-warn/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-unnecessary-playwright-install-warn/test.check.ts new file mode 100644 index 000000000..e00405a9e --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-installCommand-unnecessary-playwright-install-warn/test.check.ts @@ -0,0 +1,7 @@ +import { PlaywrightCheck } from 'checkly/constructs' + +const check = new PlaywrightCheck('check', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', + installCommand: 'npm ci && npx playwright install chromium', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-default-ignored/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-default-ignored/checkly.config.ts new file mode 100644 index 000000000..794c32bd0 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-default-ignored/checkly.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'checkly' +import { RetryStrategyBuilder } from 'checkly/constructs' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + retryStrategy: RetryStrategyBuilder.fixedStrategy({ maxRetries: 3 }), + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-default-ignored/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-default-ignored/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-default-ignored/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-default-ignored/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-default-ignored/playwright.config.ts new file mode 100644 index 000000000..63897b611 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-default-ignored/playwright.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-default-ignored/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-default-ignored/test.check.ts new file mode 100644 index 000000000..15200c1e9 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-default-ignored/test.check.ts @@ -0,0 +1,6 @@ +import { PlaywrightCheck, RetryStrategyBuilder } from 'checkly/constructs' + +const check = new PlaywrightCheck('check', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-not-allowed/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-not-allowed/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-not-allowed/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-not-allowed/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-not-allowed/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-not-allowed/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-not-allowed/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-not-allowed/playwright.config.ts new file mode 100644 index 000000000..63897b611 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-not-allowed/playwright.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-not-allowed/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-not-allowed/test.check.ts new file mode 100644 index 000000000..50fdd5c6e --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-retryStrategy-not-allowed/test.check.ts @@ -0,0 +1,10 @@ +import { PlaywrightCheck, RetryStrategyBuilder } from 'checkly/constructs' + +const check = new PlaywrightCheck('check', { + name: 'Check', + + playwrightConfigPath: './playwright.config.ts', + + // @ts-expect-error - Testing runtime validation. TypeScript should prevent this at compile time. + retryStrategy: RetryStrategyBuilder.fixedStrategy({ maxRetries: 3 }), +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-allowed/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-allowed/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-allowed/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-allowed/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-allowed/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-allowed/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-allowed/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-allowed/playwright.config.ts new file mode 100644 index 000000000..0c054a8a7 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-allowed/playwright.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './tests', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-allowed/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-allowed/test.check.ts new file mode 100644 index 000000000..e75c6177d --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-allowed/test.check.ts @@ -0,0 +1,13 @@ +import { PlaywrightCheck } from 'checkly/constructs' + +const check1 = new PlaywrightCheck('check1', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', + testCommand: 'npx playwright test', +}) + +const check2 = new PlaywrightCheck('check2', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', + testCommand: 'pnpm playwright test', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-unnecessary-playwright-install-warn/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-unnecessary-playwright-install-warn/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-unnecessary-playwright-install-warn/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-unnecessary-playwright-install-warn/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-unnecessary-playwright-install-warn/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-unnecessary-playwright-install-warn/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-unnecessary-playwright-install-warn/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-unnecessary-playwright-install-warn/playwright.config.ts new file mode 100644 index 000000000..0c054a8a7 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-unnecessary-playwright-install-warn/playwright.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './tests', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-unnecessary-playwright-install-warn/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-unnecessary-playwright-install-warn/test.check.ts new file mode 100644 index 000000000..eeb15fac6 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-testCommand-unnecessary-playwright-install-warn/test.check.ts @@ -0,0 +1,7 @@ +import { PlaywrightCheck } from 'checkly/constructs' + +const check = new PlaywrightCheck('check', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', + testCommand: 'npx playwright install && npx playwright test', +}) diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-webServer/checkly.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-webServer/checkly.config.ts new file mode 100644 index 000000000..bae253372 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-webServer/checkly.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Playwright Check Fixture', + logicalId: 'playwright-check-fixture', + checks: { + checkMatch: '**/*.check.ts', + }, +}) + +export default config diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-webServer/package.json b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-webServer/package.json new file mode 100644 index 000000000..6e1cc95f5 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-webServer/package.json @@ -0,0 +1,7 @@ +{ + "name": "playwright-check-fixture", + "devDependencies": { + "@playwright/test": "^1.55.1", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check-webserver/playwright.config.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-webServer/playwright.config.ts similarity index 100% rename from packages/cli/src/constructs/__tests__/fixtures/playwright-check-webserver/playwright.config.ts rename to packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-webServer/playwright.config.ts diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-webServer/test.check.ts b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-webServer/test.check.ts new file mode 100644 index 000000000..43223adb1 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/fixtures/playwright-check/test-cases/test-webServer/test.check.ts @@ -0,0 +1,13 @@ +import { PlaywrightCheck } from 'checkly/constructs' + +const check1 = new PlaywrightCheck('check1', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', + installCommand: 'npm ci', +}) + +const check2 = new PlaywrightCheck('check2', { + name: 'Check', + playwrightConfigPath: './playwright.config.ts', + installCommand: 'pnpm i', +}) diff --git a/packages/cli/src/constructs/__tests__/playwright-check.spec.ts b/packages/cli/src/constructs/__tests__/playwright-check.spec.ts index e4b3eb8b3..5a4315802 100644 --- a/packages/cli/src/constructs/__tests__/playwright-check.spec.ts +++ b/packages/cli/src/constructs/__tests__/playwright-check.spec.ts @@ -1,563 +1,864 @@ import path from 'node:path' -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' -import { AxiosHeaders } from 'axios' - -import { CheckGroupV2, Diagnostics, PlaywrightCheck, RetryStrategyBuilder } from '../index' -import { Project, Session } from '../project' -import { checklyStorage } from '../../rest/api' - -describe('PlaywrightCheck', () => { - beforeEach(() => { - vi.resetAllMocks() - vi.spyOn(checklyStorage, 'uploadCodeBundle').mockResolvedValue({ - data: { key: 'mock-upload-key' }, - status: 201, - statusText: 'Created', - headers: new AxiosHeaders(), - config: { - headers: new AxiosHeaders(), - }, - }) - Session.currentCommand = undefined - Session.includeFlagProvided = undefined +import { describe, it, expect, beforeAll, afterAll } from 'vitest' +import { list } from 'tar' + +import { FixtureSandbox } from '../../testing/fixture-sandbox' +import { ParseProjectOutput } from '../../commands/debug/parse-project' + +async function parseProject (fixt: FixtureSandbox, ...args: string[]): Promise { + const result = await fixt.run('npx', [ + 'checkly', + 'debug', + 'parse-project', + ...args, + ]) + + if (result.exitCode !== 0) { + // eslint-disable-next-line no-console + console.error('stderr', result.stderr) + // eslint-disable-next-line no-console + console.error('stdout', result.stdout) + } + + expect(result.exitCode).toBe(0) + + const output: ParseProjectOutput = JSON.parse(result.stdout) + + return output +} + +async function listTarFiles (filePath: string): Promise { + const filenames: string[] = [] + await list({ + file: filePath, + onReadEntry: entry => filenames.push(entry.path), }) + return filenames +} - afterEach(() => { - Session.currentCommand = undefined - Session.includeFlagProvided = undefined - }) +const DEFAULT_TEST_TIMEOUT = 180_000 +describe('PlaywrightCheck', () => { it('should synthesize groupName', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-groupName-mapping'), }) - const group = new CheckGroupV2('group', { - name: 'Test Group', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - groupName: 'Test Group', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.isFatal()).toEqual(false) - - const bundle = await check.bundle() - const payload = bundle.synthesize() + try { + const output = await parseProject(fixt) - expect(payload.groupId).toEqual(group.ref()) - }) + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'group', + type: 'check-group', + member: true, + payload: expect.objectContaining({ + name: 'b801a908-8d3c-4a94-92ab-cf15f58a59b4', + }), + }), + expect.objectContaining({ + logicalId: 'check', + type: 'check', + member: true, + payload: expect.objectContaining({ + groupId: { + ref: 'group', + }, + }), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) it('should synthesize group', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - - const group = new CheckGroupV2('group', { - name: 'Test Group', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - group, - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-group-mapping'), }) - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.isFatal()).toEqual(false) - - const bundle = await check.bundle() - const payload = bundle.synthesize() + try { + const output = await parseProject(fixt) - expect(payload.groupId).toEqual(group.ref()) - }) + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'group', + type: 'check-group', + member: true, + }), + expect.objectContaining({ + logicalId: 'check', + type: 'check', + member: true, + payload: expect.objectContaining({ + groupId: { + ref: 'group', + }, + }), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) it('should synthesize groupId', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-groupId-mapping'), }) - const group = new CheckGroupV2('group', { - name: 'Test Group', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - groupId: group.ref(), - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.isFatal()).toEqual(false) - - const bundle = await check.bundle() - const payload = bundle.synthesize() + try { + const output = await parseProject(fixt) - expect(payload.groupId).toEqual(group.ref()) - }) + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'group', + type: 'check-group', + member: true, + }), + expect.objectContaining({ + logicalId: 'check', + type: 'check', + member: true, + payload: expect.objectContaining({ + groupId: { + ref: 'group', + }, + }), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) describe('validation', () => { it('should warn that groupName is deprecated', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const group = new CheckGroupV2('group', { - name: 'Test Group', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - groupName: 'Test Group', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.isFatal()).toEqual(false) - expect(diags.observations).toEqual(expect.arrayContaining([ - expect.objectContaining({ - message: expect.stringContaining('Property "groupName" is deprecated and will eventually be removed.'), - }), - ])) - }) + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-groupName-mapping'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + benign: false, + observations: expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('Property "groupName" is deprecated and will eventually be removed.'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) it('should error if groupName is not found', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const group = new CheckGroupV2('group', { - name: 'Test Group', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - groupName: 'Missing Group', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.isFatal()).toEqual(true) - expect(diags.observations).toEqual(expect.arrayContaining([ - expect.objectContaining({ - message: expect.stringContaining('The value provided for property "groupName" is not valid.'), - }), - ])) - }) + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-groupName-not-found'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: true, + benign: false, + observations: expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('The value provided for property "groupName" is not valid.'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) it('should error if both group and groupName are set', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - - const group = new CheckGroupV2('group', { - name: 'Test Group', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - group, - groupName: 'Test Group', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.isFatal()).toEqual(true) - expect(diags.observations).toEqual(expect.arrayContaining([ - expect.objectContaining({ - message: expect.stringContaining('Property "groupName" cannot be set when "group" is set.'), - }), - ])) - }) + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-groupName-with-group-conflict'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: true, + benign: false, + observations: expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('Property "groupName" cannot be set when "group" is set.'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) it('should error if both groupId and groupName are set', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - - const group = new CheckGroupV2('group', { - name: 'Test Group', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - groupId: group.ref(), - groupName: 'Test Group', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.isFatal()).toEqual(true) - expect(diags.observations).toEqual(expect.arrayContaining([ - expect.objectContaining({ - message: expect.stringContaining('Property "groupName" cannot be set when "group" is set.'), - }), - ])) - }) + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-groupName-with-groupId-conflict'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: true, + benign: false, + observations: expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('Property "groupName" cannot be set when "group" is set.'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) it('should error if retryStrategy is set', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - // @ts-expect-error - Testing runtime validation. TypeScript should prevent this at compile time. - retryStrategy: RetryStrategyBuilder.fixedStrategy({ maxRetries: 3 }), - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.isFatal()).toEqual(true) - expect(diags.observations).toEqual(expect.arrayContaining([ - expect.objectContaining({ - message: expect.stringContaining('Property "retryStrategy" is not supported.'), - }), - ])) - }) + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-retryStrategy-not-allowed'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: true, + benign: false, + observations: expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('Property "retryStrategy" is not supported.'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) it('should error if doubleCheck is set', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - // @ts-expect-error Testing a property that isn't part of the type. - doubleCheck: true, - }) - - const diags = new Diagnostics() - await check.validate(diags) + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-doubleCheck-not-allowed'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: true, + benign: false, + observations: expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('Property "doubleCheck" is not supported.'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) + + describe('headless', () => { + it('should error if headless: false is set globally', async () => { + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-headless-false-not-allowed'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: true, + benign: false, + observations: expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('The value provided for property "headless" is not valid.'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) + + it('should error if headless: false is set in a project', async () => { + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-headless-false-in-project-not-allowed'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: true, + benign: false, + observations: expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('The value provided for property "headless" is not valid.'), + }), + expect.objectContaining({ + message: expect.stringContaining('in project "chromium"'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) + + it('should not error if headless: true is set', async () => { + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-headless-true-allowed'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + benign: true, + observations: expect.not.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('The value provided for property "headless" is not valid.'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) + + it('should not error if headless is not set', async () => { + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-headless-unset-allowed'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + benign: true, + observations: expect.not.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining('The value provided for property "headless" is not valid.'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) + }) - expect(diags.isFatal()).toEqual(true) - expect(diags.observations).toEqual(expect.arrayContaining([ - expect.objectContaining({ - message: expect.stringContaining('Property "doubleCheck" is not supported.'), - }), - ])) + describe('webServer', () => { + it('should warn if webServer is configured in playwright config when running pw-test', async () => { + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-webServer'), + }) + + try { + const output = await parseProject( + fixt, + '--emulate-pw-test', + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + benign: false, + observations: expect.arrayContaining([ + expect.objectContaining({ + title: expect.stringContaining('webServer configuration detected'), + message: expect.stringContaining('webServer configuration requires additional files'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) + + it('should not warn about webServer when not running pw-test command', async () => { + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-webServer'), + }) + + try { + const output = await parseProject( + fixt, + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + benign: true, + observations: expect.not.arrayContaining([ + expect.objectContaining({ + title: expect.stringContaining('webServer configuration detected'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) + + it('should not warn about webServer when --include flag is provided', async () => { + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-webServer'), + }) + + try { + const output = await parseProject( + fixt, + '--emulate-pw-test', + '--include', 'foobar', + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + benign: true, + observations: expect.not.arrayContaining([ + expect.objectContaining({ + title: expect.stringContaining('webServer configuration detected'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) }) - it('should error if headless: false is set globally', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) + describe('installCommand', () => { + it('should warn when installCommand contains playwright install', async () => { + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-installCommand-unnecessary-playwright-install-warn'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + benign: false, + observations: expect.arrayContaining([ + expect.objectContaining({ + title: expect.stringContaining('Unnecessary browser installation detected'), + message: expect.stringContaining('installCommand contains "playwright install"'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) + + it('should not warn when installCommand does not contain playwright install', async () => { + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-installCommand-allowed'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + benign: true, + observations: expect.not.arrayContaining([ + expect.objectContaining({ + title: expect.stringContaining('Unnecessary browser installation detected'), + message: expect.stringContaining('installCommand contains "playwright install"'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) + }) - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.headless-false.ts'), - }) + describe('testCommand', () => { + it('should warn when testCommand contains playwright install', async () => { + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-testCommand-unnecessary-playwright-install-warn'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + benign: false, + observations: expect.arrayContaining([ + expect.objectContaining({ + title: expect.stringContaining('Unnecessary browser installation detected'), + message: expect.stringContaining('testCommand contains "playwright install"'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) + + it('should not warn when testCommand does not playwright install', async () => { + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-testCommand-allowed'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + benign: true, + observations: expect.not.arrayContaining([ + expect.objectContaining({ + title: expect.stringContaining('Unnecessary browser installation detected'), + message: expect.stringContaining('testCommand contains "playwright install"'), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) + }) + }) - const diags = new Diagnostics() - await check.validate(diags) + describe('defaults', () => { + it('should ignore retryStrategy from session check defaults', async () => { + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-retryStrategy-default-ignored'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check', + type: 'check', + member: true, + payload: expect.not.objectContaining({ + retryStrategy: expect.anything(), + }), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) + + it('should ignore doubleCheck from session check defaults', async () => { + const fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-doubleCheck-default-ignored'), + }) + + try { + const output = await parseProject(fixt) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, + }), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'check', + type: 'check', + member: true, + payload: expect.not.objectContaining({ + doubleCheck: true, + }), + }), + ]), + }), + })) + } finally { + await fixt.destroy() + } + }, DEFAULT_TEST_TIMEOUT) + }) - expect(diags.isFatal()).toEqual(true) - expect(diags.observations).toEqual(expect.arrayContaining([ - expect.objectContaining({ - message: expect.stringContaining('The value provided for property "headless" is not valid.'), - }), - ])) - }) + describe('bundling', () => { + let fixt: FixtureSandbox - it('should error if headless: false is set in a project', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'fixtures', 'playwright-check', 'test-cases', 'test-bundling'), }) + }, DEFAULT_TEST_TIMEOUT) - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.headless-false-project.ts'), - }) + afterAll(async () => { + await fixt?.destroy() + }) - const diags = new Diagnostics() - await check.validate(diags) + it('should exclude directories matching ignoreDirectoriesMatch pattern', async () => { + const output = await parseProject( + fixt, + '--config', + 'checkly.exclude-fixtures.config.ts', + ) - expect(diags.isFatal()).toEqual(true) - expect(diags.observations).toEqual(expect.arrayContaining([ - expect.objectContaining({ - message: expect.stringContaining('The value provided for property "headless" is not valid.'), + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, }), - expect.objectContaining({ - message: expect.stringContaining('in project "chromium"'), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'playwright-check-suite', + type: 'check', + member: true, + payload: expect.objectContaining({ + codeBundlePath: expect.stringMatching(/.tar.gz$/), + }), + }), + ]), }), - ])) - }) - - it('should not error if headless: true is set', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.headless-true.ts'), - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.isFatal()).toEqual(false) - expect(diags.observations).not.toEqual(expect.arrayContaining([ - expect.objectContaining({ - message: expect.stringContaining('The value provided for property "headless" is not valid.'), + })) + + const { + codeBundlePath, + } = output.payload.resources[0].payload as any + + const files = await listTarFiles(codeBundlePath) + + expect(files.sort()).toEqual([ + 'package-lock.json', + 'package.json', + 'playwright.config.ts', + 'tests/example.spec.ts', + ]) + }, DEFAULT_TEST_TIMEOUT) + + it('should include all directories when ignoreDirectoriesMatch is empty', async () => { + const output = await parseProject( + fixt, + '--config', + 'checkly.exclude-nothing.config.ts', + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, }), - ])) - }) - - it('should not error if headless is not set', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.isFatal()).toEqual(false) - expect(diags.observations).not.toEqual(expect.arrayContaining([ - expect.objectContaining({ - message: expect.stringContaining('The value provided for property "headless" is not valid.'), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'playwright-check-suite', + type: 'check', + member: true, + payload: expect.objectContaining({ + codeBundlePath: expect.stringMatching(/.tar.gz$/), + }), + }), + ]), }), - ])) - }) - - it('should warn if webServer is configured in playwright config when running pw-test', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check-webserver') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - Session.currentCommand = 'pw-test' - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check-webserver/playwright.config.ts'), - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.isFatal()).toEqual(false) - expect(diags.observations).toEqual(expect.arrayContaining([ - expect.objectContaining({ - title: expect.stringContaining('webServer configuration detected'), - message: expect.stringContaining('webServer configuration requires additional files'), + })) + + const { + codeBundlePath, + } = output.payload.resources[0].payload as any + + const files = await listTarFiles(codeBundlePath) + + expect(files.sort()).toEqual([ + 'fixtures/mock-data.json', + 'package-lock.json', + 'package.json', + 'playwright.config.ts', + 'tests/example.spec.ts', + ]) + }, DEFAULT_TEST_TIMEOUT) + + it('should include explicit node_modules patterns bypassing default ignores', async () => { + const output = await parseProject( + fixt, + '--config', + 'checkly.include-node-modules-if-explicit.config.ts', + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, }), - ])) - }) - - it('should not warn about webServer when not running pw-test command', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check-webserver') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - Session.currentCommand = 'test' - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check-webserver/playwright.config.ts'), - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.observations).not.toEqual(expect.arrayContaining([ - expect.objectContaining({ - title: expect.stringContaining('webServer configuration detected'), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'playwright-check-suite', + type: 'check', + member: true, + payload: expect.objectContaining({ + codeBundlePath: expect.stringMatching(/.tar.gz$/), + }), + }), + ]), }), - ])) - }) - - it('should not warn about webServer when --include flag is provided', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check-webserver') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - Session.currentCommand = 'pw-test' - Session.includeFlagProvided = true + })) - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check-webserver/playwright.config.ts'), - }) + const { + codeBundlePath, + } = output.payload.resources[0].payload as any - const diags = new Diagnostics() - await check.validate(diags) + const files = await listTarFiles(codeBundlePath) - expect(diags.observations).not.toEqual(expect.arrayContaining([ - expect.objectContaining({ - title: expect.stringContaining('webServer configuration detected'), - }), + expect(files.sort()).toEqual(expect.arrayContaining([ + 'node_modules/checkly/package.json', + 'package-lock.json', + 'package.json', + 'playwright.config.ts', + 'tests/example.spec.ts', ])) - }) - - it('should warn when installCommand contains playwright install', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - installCommand: 'npm ci && npx playwright install chromium', - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.isFatal()).toEqual(false) - expect(diags.observations).toEqual(expect.arrayContaining([ - expect.objectContaining({ - title: 'Unnecessary browser installation detected', - message: expect.stringContaining('installCommand contains "playwright install"'), + }, DEFAULT_TEST_TIMEOUT) + + it('should still respect custom ignoreDirectoriesMatch for explicit patterns', async () => { + const output = await parseProject( + fixt, + '--config', + 'checkly.exclude-node-modules-if-include-not-explicit.config.ts', + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, }), - ])) - }) - - it('should warn when testCommand contains playwright install', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - testCommand: 'npx playwright install && npx playwright test', - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.isFatal()).toEqual(false) - expect(diags.observations).toEqual(expect.arrayContaining([ - expect.objectContaining({ - title: 'Unnecessary browser installation detected', - message: expect.stringContaining('testCommand contains "playwright install"'), + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'playwright-check-suite', + type: 'check', + member: true, + payload: expect.objectContaining({ + codeBundlePath: expect.stringMatching(/.tar.gz$/), + }), + }), + ]), }), - ])) - }) - - it('should not warn when commands do not contain playwright install', async () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - installCommand: 'npm ci', - testCommand: 'npx playwright test', - }) - - const diags = new Diagnostics() - await check.validate(diags) - - expect(diags.observations).not.toEqual(expect.arrayContaining([ - expect.objectContaining({ - title: 'Unnecessary browser installation detected', + })) + + const { + codeBundlePath, + } = output.payload.resources[0].payload as any + + const files = await listTarFiles(codeBundlePath) + + expect(files.sort()).toEqual([ + 'package-lock.json', + 'package.json', + 'playwright.config.ts', + 'tests/example.spec.ts', + ]) + }, DEFAULT_TEST_TIMEOUT) + + it('should exclude node_modules with broad patterns despite include', async () => { + const output = await parseProject( + fixt, + '--config', + 'checkly.exclude-prefer-over-include.config.ts', + ) + + expect(output).toEqual(expect.objectContaining({ + diagnostics: expect.objectContaining({ + fatal: false, }), - ])) - }) - }) - - describe('defaults', () => { - it('should ignore retryStrategy from session check defaults', () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) - - Session.checkDefaults = { - retryStrategy: RetryStrategyBuilder.fixedStrategy({ maxRetries: 3 }), - } - - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - }) - - expect(check.retryStrategy).toBeUndefined() - }) - - it('should ignore doubleCheck from session check defaults', () => { - Session.basePath = path.resolve(__dirname, './fixtures/playwright-check') - Session.project = new Project('project-id', { - name: 'Test Project', - repoUrl: 'https://github.com/checkly/checkly-cli', - }) + payload: expect.objectContaining({ + resources: expect.arrayContaining([ + expect.objectContaining({ + logicalId: 'playwright-check-suite', + type: 'check', + member: true, + payload: expect.objectContaining({ + codeBundlePath: expect.stringMatching(/.tar.gz$/), + }), + }), + ]), + }), + })) - Session.checkDefaults = { - doubleCheck: true, - } + const { + codeBundlePath, + } = output.payload.resources[0].payload as any - const check = new PlaywrightCheck('foo', { - name: 'Test Check', - playwrightConfigPath: path.resolve(__dirname, './fixtures/playwright-check/playwright.config.ts'), - }) + const files = await listTarFiles(codeBundlePath) - expect(check.doubleCheck).toBeUndefined() - }) + expect(files.sort()).toEqual([ + 'package-lock.json', + 'package.json', + 'playwright.config.ts', + 'tests/example.spec.ts', + ]) + }, DEFAULT_TEST_TIMEOUT) }) }) diff --git a/packages/cli/src/constructs/api-check.ts b/packages/cli/src/constructs/api-check.ts index 311f592ee..23e9f95dc 100644 --- a/packages/cli/src/constructs/api-check.ts +++ b/packages/cli/src/constructs/api-check.ts @@ -11,6 +11,7 @@ import { DeprecatedPropertyDiagnostic, InvalidPropertyValueDiagnostic } from './ import { ApiCheckBundle, ApiCheckBundleProps } from './api-check-bundle' import { Assertion } from './api-assertion' import { validateResponseTimes } from './internal/common-diagnostics' +import { Runtime } from '../runtimes' /** * Default configuration that can be applied to API checks. @@ -246,6 +247,16 @@ export class ApiCheck extends RuntimeCheck { )) } + if (this.runtimeId) { + const runtime = Session.getRuntime(this.runtimeId) + if (!runtime) { + diagnostics.add(new InvalidPropertyValueDiagnostic( + 'runtimeId', + new Error(`"${this.runtimeId}" is not a known runtime.`), + )) + } + } + await validateResponseTimes(diagnostics, this, { degradedResponseTime: 30_000, maxResponseTime: 30_000, diff --git a/packages/cli/src/constructs/browser-check.ts b/packages/cli/src/constructs/browser-check.ts index 53963d290..ce1729363 100644 --- a/packages/cli/src/constructs/browser-check.ts +++ b/packages/cli/src/constructs/browser-check.ts @@ -1,9 +1,7 @@ import fs from 'node:fs/promises' -import path from 'node:path' import { CheckProps, RuntimeCheck, RuntimeCheckProps } from './check' import { Session, SharedFileRef } from './project' -import { pathToPosix } from '../services/util' import { Content, Entrypoint, isContent, isEntrypoint } from './construct' import { detectSnapshots } from '../services/snapshot-service' import { PlaywrightConfig } from './playwright-config' @@ -11,6 +9,7 @@ import { Diagnostics } from './diagnostics' import { InvalidPropertyValueDiagnostic } from './construct-diagnostics' import { BrowserCheckBundle } from './browser-check-bundle' import { ConfigDefaultsGetter, makeConfigDefaultsGetter } from './check-config' +import { CheckConfigDefaults } from '../services/checkly-config-loader' export interface BrowserCheckProps extends RuntimeCheckProps { /** @@ -106,7 +105,7 @@ export class BrowserCheck extends RuntimeCheck { return `BrowserCheck:${this.logicalId}` } - protected configDefaultsGetter (props: CheckProps): ConfigDefaultsGetter { + protected configDefaultsGetter (props: CheckProps): ConfigDefaultsGetter { return makeConfigDefaultsGetter( props.group?.getBrowserCheckDefaults(), Session.browserCheckDefaults, @@ -154,6 +153,16 @@ export class BrowserCheck extends RuntimeCheck { )) } } + + if (this.runtimeId) { + const runtime = Session.getRuntime(this.runtimeId) + if (!runtime) { + diagnostics.add(new InvalidPropertyValueDiagnostic( + 'runtimeId', + new Error(`"${this.runtimeId}" is not a known runtime.`), + )) + } + } } static async bundle (entry: string, runtimeId?: string) { @@ -168,7 +177,7 @@ export class BrowserCheck extends RuntimeCheck { const deps: SharedFileRef[] = [] for (const { filePath, content } of parsed.dependencies) { deps.push(Session.registerSharedFile({ - path: pathToPosix(path.relative(Session.basePath!, filePath)), + path: Session.relativePosixPath(filePath), content, })) } diff --git a/packages/cli/src/constructs/check-config.ts b/packages/cli/src/constructs/check-config.ts index 7f4595555..53ad43440 100644 --- a/packages/cli/src/constructs/check-config.ts +++ b/packages/cli/src/constructs/check-config.ts @@ -1,13 +1,13 @@ -import { CheckConfigDefaults } from '../services/checkly-config-loader' +export type ConfigDefaultsGetter< + T extends object, +> = (key: K) => T[K] | undefined -export type ConfigDefaultsGetter = (key: K) => CheckConfigDefaults[K] - -export function makeConfigDefaultsGetter ( - ...defaults: (Partial | undefined)[] -): ConfigDefaultsGetter { +export function makeConfigDefaultsGetter ( + ...defaults: (Partial | undefined)[] +): ConfigDefaultsGetter { const ok = defaults.filter(value => value !== undefined) - function get (key: K): CheckConfigDefaults[K] { + function get (key: K): T[K] | undefined { for (const config of ok) { // Older TS seems to need this check. if (config === undefined) { diff --git a/packages/cli/src/constructs/check-group-v1.ts b/packages/cli/src/constructs/check-group-v1.ts index 213c5d62b..31b539b1d 100644 --- a/packages/cli/src/constructs/check-group-v1.ts +++ b/packages/cli/src/constructs/check-group-v1.ts @@ -17,7 +17,6 @@ import { Diagnostics } from './diagnostics' import { DeprecatedConstructDiagnostic, DeprecatedPropertyDiagnostic, InvalidPropertyValueDiagnostic } from './construct-diagnostics' import CheckTypes from '../constants' import { CheckConfigDefaults } from '../services/checkly-config-loader' -import { pathToPosix } from '../services/util' import { AlertChannelSubscription } from './alert-channel-subscription' import { BrowserCheck } from './browser-check' import { CheckGroupRef } from './check-group-ref' @@ -438,7 +437,7 @@ export class CheckGroupV1 extends Construct { }, // the browserChecks props inherited from the group are applied in BrowserCheck.constructor() } - const checkLogicalId = pathToPosix(path.relative(Session.basePath!, filepath)) + const checkLogicalId = Session.relativePosixPath(filepath) if (checkType === CheckTypes.BROWSER) { new BrowserCheck(checkLogicalId, props) } else { diff --git a/packages/cli/src/constructs/check.ts b/packages/cli/src/constructs/check.ts index 83d95aef4..23af8ccea 100644 --- a/packages/cli/src/constructs/check.ts +++ b/packages/cli/src/constructs/check.ts @@ -23,6 +23,7 @@ import { ConfigDefaultsGetter, makeConfigDefaultsGetter } from './check-config' import { Diagnostics } from './diagnostics' import { validateDeprecatedDoubleCheck } from './internal/common-diagnostics' import { InvalidPropertyValueDiagnostic } from './construct-diagnostics' +import { CheckConfigDefaults } from '../services/checkly-config-loader' /** * Retry strategies supported by checks. @@ -329,7 +330,7 @@ export abstract class Check extends Construct { await this.validateRetryStrategyOnlyOn(diagnostics) } - protected configDefaultsGetter (props: CheckProps): ConfigDefaultsGetter { + protected configDefaultsGetter (props: CheckProps): ConfigDefaultsGetter { return makeConfigDefaultsGetter( props.group?.getCheckDefaults(), Session.checkDefaults, diff --git a/packages/cli/src/constructs/construct-diagnostics.ts b/packages/cli/src/constructs/construct-diagnostics.ts index 2bbd9a91e..335cbdd49 100644 --- a/packages/cli/src/constructs/construct-diagnostics.ts +++ b/packages/cli/src/constructs/construct-diagnostics.ts @@ -137,6 +137,20 @@ export class UnsupportedRuntimeFeatureDiagnostic extends ErrorDiagnostic { } } +export class UnsatisfiedLocalPrerequisitesDiagnostic extends ErrorDiagnostic { + constructor (error: Error) { + super({ + title: `Unsatisfied local prerequisites`, + message: + `Local environment does not satisfy the prerequisites for this ` + + `functionality.` + + `\n\n` + + `Cause: ${error.message}`, + error, + }) + } +} + export class ConstructDiagnostic extends Diagnostic { underlying: Diagnostic diff --git a/packages/cli/src/constructs/internal/codegen/snippet.ts b/packages/cli/src/constructs/internal/codegen/snippet.ts index 1d87c81fd..a875a9840 100644 --- a/packages/cli/src/constructs/internal/codegen/snippet.ts +++ b/packages/cli/src/constructs/internal/codegen/snippet.ts @@ -54,6 +54,6 @@ export function parseSnippetDependencies (content: string): string[] { } return dependencies - .filter(value => value.startsWith(SNIPPET_PATH_PREFIX)) - .map(value => value.slice(SNIPPET_PATH_PREFIX.length)) + .filter(({ importPath }) => importPath.startsWith(SNIPPET_PATH_PREFIX)) + .map(({ importPath }) => importPath.slice(SNIPPET_PATH_PREFIX.length)) } diff --git a/packages/cli/src/constructs/multi-step-check.ts b/packages/cli/src/constructs/multi-step-check.ts index 56c86cf12..cb2066de8 100644 --- a/packages/cli/src/constructs/multi-step-check.ts +++ b/packages/cli/src/constructs/multi-step-check.ts @@ -9,6 +9,7 @@ import { Diagnostics } from './diagnostics' import { InvalidPropertyValueDiagnostic, UnsupportedRuntimeFeatureDiagnostic } from './construct-diagnostics' import { MultiStepCheckBundle } from './multi-step-check-bundle' import { ConfigDefaultsGetter, makeConfigDefaultsGetter } from './check-config' +import { CheckConfigDefaults } from '../services/checkly-config-loader' export interface MultiStepCheckProps extends RuntimeCheckProps { /** @@ -96,7 +97,7 @@ export class MultiStepCheck extends RuntimeCheck { } } - protected configDefaultsGetter (props: CheckProps): ConfigDefaultsGetter { + protected configDefaultsGetter (props: CheckProps): ConfigDefaultsGetter { return makeConfigDefaultsGetter( props.group?.getMultiStepCheckDefaults(), Session.multiStepCheckDefaults, diff --git a/packages/cli/src/constructs/playwright-check-bundle.ts b/packages/cli/src/constructs/playwright-check-bundle.ts index 7237cf2d8..1ff947b0d 100644 --- a/packages/cli/src/constructs/playwright-check-bundle.ts +++ b/packages/cli/src/constructs/playwright-check-bundle.ts @@ -1,48 +1,107 @@ +import { createReadStream } from 'node:fs' +import fs from 'node:fs/promises' + +import { AxiosResponse } from 'axios' + import { Bundle } from './construct' import { PlaywrightCheck } from './playwright-check' import { Ref } from './ref' +import { checklyStorage } from '../rest/api' -export interface PlaywrightCheckBundleProps { +export interface PlaywrightCheckLocalBundleProps { groupId?: Ref - codeBundlePath?: string + localCodeBundlePath: string browsers?: string[] cacheHash?: string playwrightVersion?: string installCommand?: string testCommand: string + workingDir?: string } -export class PlaywrightCheckBundle implements Bundle { +export class PlaywrightCheckLocalBundle implements Bundle { playwrightCheck: PlaywrightCheck groupId?: Ref - codeBundlePath?: string + localCodeBundlePath: string browsers?: string[] cacheHash?: string playwrightVersion?: string installCommand?: string testCommand: string + workingDir?: string - constructor (playwrightCheck: PlaywrightCheck, props: PlaywrightCheckBundleProps) { + constructor (playwrightCheck: PlaywrightCheck, props: PlaywrightCheckLocalBundleProps) { this.playwrightCheck = playwrightCheck this.groupId = props.groupId - this.codeBundlePath = props.codeBundlePath + this.localCodeBundlePath = props.localCodeBundlePath this.browsers = props.browsers this.cacheHash = props.cacheHash this.playwrightVersion = props.playwrightVersion this.installCommand = props.installCommand this.testCommand = props.testCommand + this.workingDir = props.workingDir + } + + async store (): Promise { + const { + data: { + key: codeBundlePath, + }, + } = await this.#uploadCodeBundle(this.localCodeBundlePath) + + return new PlaywrightCheckStoredBundle(this.playwrightCheck, { + groupId: this.groupId, + localCodeBundlePath: this.localCodeBundlePath, + codeBundlePath, + browsers: this.browsers, + cacheHash: this.cacheHash, + playwrightVersion: this.playwrightVersion, + testCommand: this.testCommand, + installCommand: this.installCommand, + workingDir: this.workingDir, + }) + } + + async #uploadCodeBundle (filePath: string): Promise { + const { size } = await fs.stat(filePath) + const stream = createReadStream(filePath) + stream.on('error', err => { + throw new Error(`Failed to read Playwright project file: ${err.message}`) + }) + return checklyStorage.uploadCodeBundle(stream, size) } synthesize () { return { ...this.playwrightCheck.synthesize(), groupId: this.groupId, - codeBundlePath: this.codeBundlePath, + codeBundlePath: this.localCodeBundlePath, browsers: this.browsers, cacheHash: this.cacheHash, playwrightVersion: this.playwrightVersion, installCommand: this.installCommand, testCommand: this.testCommand, + workingDir: this.workingDir, + } + } +} + +export interface PlaywrightCheckStoredBundleProps extends PlaywrightCheckLocalBundleProps { + codeBundlePath: string +} + +export class PlaywrightCheckStoredBundle extends PlaywrightCheckLocalBundle { + codeBundlePath: string + + constructor (playwrightCheck: PlaywrightCheck, props: PlaywrightCheckStoredBundleProps) { + super(playwrightCheck, props) + this.codeBundlePath = props.codeBundlePath + } + + synthesize () { + return { + ...super.synthesize(), + codeBundlePath: this.codeBundlePath, } } } diff --git a/packages/cli/src/constructs/playwright-check.ts b/packages/cli/src/constructs/playwright-check.ts index 784012975..b32c416f9 100644 --- a/packages/cli/src/constructs/playwright-check.ts +++ b/packages/cli/src/constructs/playwright-check.ts @@ -1,10 +1,7 @@ -import { createReadStream } from 'node:fs' import fs from 'node:fs/promises' -import type { AxiosResponse } from 'axios' -import { checklyStorage } from '../rest/api' import { - bundlePlayWrightProject, cleanup, + bundlePlayWrightProject, } from '../services/util' import { shellQuote } from '../services/shell' import { RuntimeCheck, RuntimeCheckProps } from './check' @@ -12,14 +9,15 @@ import { ConflictingPropertyDiagnostic, DeprecatedPropertyDiagnostic, InvalidPropertyValueDiagnostic, + UnsatisfiedLocalPrerequisitesDiagnostic, UnsupportedPropertyDiagnostic, } from './construct-diagnostics' -import { WarningDiagnostic } from './diagnostics' -import { Diagnostics } from './diagnostics' -import { PlaywrightCheckBundle } from './playwright-check-bundle' +import { Diagnostics, WarningDiagnostic } from './diagnostics' +import { PlaywrightCheckLocalBundle } from './playwright-check-bundle' import { Session } from './project' import { Ref } from './ref' import { ConfigDefaultsGetter, makeConfigDefaultsGetter } from './check-config' +import { CheckConfigDefaults } from '../services/checkly-config-loader' export interface PlaywrightCheckProps extends Omit { /** @@ -127,7 +125,7 @@ export interface PlaywrightCheckProps extends Omit { const group = PlaywrightCheck.#resolveGroupFromProps(props) return makeConfigDefaultsGetter( @@ -337,6 +335,7 @@ export class PlaywrightCheck extends RuntimeCheck { async validate (diagnostics: Diagnostics): Promise { await super.validate(diagnostics) + await this.#validateWorkspace(diagnostics) await this.validateRetryStrategy(diagnostics) try { @@ -355,6 +354,29 @@ export class PlaywrightCheck extends RuntimeCheck { this.#validateGroupReferences(diagnostics) } + // eslint-disable-next-line require-await + async #validateWorkspace (diagnostics: Diagnostics): Promise { + const workspace = Session.workspace + if (workspace.isOk()) { + const lockfile = workspace.ok().lockfile + if (lockfile.isErr()) { + diagnostics.add(new UnsatisfiedLocalPrerequisitesDiagnostic(new Error( + `A lockfile is required for Playwright checks, but none could be ` + + `detected.` + + '\n\n' + + `Cause: ${lockfile.err().message}`, + ))) + } + } else if (workspace.isErr()) { + diagnostics.add(new UnsatisfiedLocalPrerequisitesDiagnostic(new Error( + `A workspace is required for Playwright checks, but none could be ` + + `detected.` + + '\n\n' + + `Cause: ${workspace.err().message}`, + ))) + } + } + #validateGroupReferences (diagnostics: Diagnostics): void { if (this.groupName) { diagnostics.add(new DeprecatedPropertyDiagnostic( @@ -396,30 +418,7 @@ export class PlaywrightCheck extends RuntimeCheck { return `${testCommand} --config ${quotedPath}${projectArg}${tagArg}` } - static async bundleProject (playwrightConfigPath: string, include: string[]) { - let dir = '' - try { - const { - outputFile, browsers, relativePlaywrightConfigPath, cacheHash, playwrightVersion, - } = await bundlePlayWrightProject(playwrightConfigPath, include) - dir = outputFile - const { data: { key } } = await PlaywrightCheck.uploadPlaywrightProject(dir) - return { key, browsers, relativePlaywrightConfigPath, cacheHash, playwrightVersion } - } finally { - await cleanup(dir) - } - } - - static async uploadPlaywrightProject (dir: string): Promise { - const { size } = await fs.stat(dir) - const stream = createReadStream(dir) - stream.on('error', err => { - throw new Error(`Failed to read Playwright project file: ${err.message}`) - }) - return checklyStorage.uploadCodeBundle(stream, size) - } - - async bundle (): Promise { + async bundle (): Promise { // Prefer the standard groupId but fall back to the deprecated groupName // if available. const groupId = this.groupName && !this.groupId @@ -427,31 +426,37 @@ export class PlaywrightCheck extends RuntimeCheck { : this.groupId const { - key: codeBundlePath, + outputFile: codeBundleLocalFilePath, browsers, cacheHash, playwrightVersion, relativePlaywrightConfigPath, - } = await PlaywrightCheck.bundleProject(this.playwrightConfigPath, this.include ?? []) + workingDir, + } = await bundlePlayWrightProject(this.playwrightConfigPath, this.include ?? []) const testCommand = PlaywrightCheck.buildTestCommand( - this.testCommand, + this.testCommand ?? this.#defaultTestCommand(), relativePlaywrightConfigPath, this.pwProjects, this.pwTags, ) - return new PlaywrightCheckBundle(this, { + return new PlaywrightCheckLocalBundle(this, { groupId, - codeBundlePath, + localCodeBundlePath: codeBundleLocalFilePath, browsers, cacheHash, playwrightVersion, testCommand, installCommand: this.installCommand, + workingDir, }) } + #defaultTestCommand (): string { + return Session.packageManager.execCommand(['playwright', 'test']).unsafeDisplayCommand + } + synthesize () { return { ...super.synthesize(), diff --git a/packages/cli/src/constructs/project.ts b/packages/cli/src/constructs/project.ts index 935b2528f..fb7d624ca 100644 --- a/packages/cli/src/constructs/project.ts +++ b/packages/cli/src/constructs/project.ts @@ -6,7 +6,6 @@ import { Parser } from '../services/check-parser/parser' import { Construct } from './construct' import { ValidationError } from './validator-error' -import type { Runtime } from '../rest/runtimes' import { Check, AlertChannelSubscription, AlertChannel, CheckGroup, MaintenanceWindow, Dashboard, PrivateLocation, HeartbeatMonitor, PrivateLocationCheckAssignment, PrivateLocationGroupAssignment, @@ -24,6 +23,10 @@ import { Diagnostics } from './diagnostics' import { ConstructDiagnostics, InvalidPropertyValueDiagnostic } from './construct-diagnostics' import { ProjectBundle, ProjectDataBundle } from './project-bundle' import { pathToPosix } from '../services/util' +import { Workspace } from '../services/check-parser/package-files/workspace' +import { npmPackageManager, PackageManager } from '../services/check-parser/package-files/package-manager' +import { Err, Result } from '../services/check-parser/package-files/result' +import { Runtime } from '../runtimes' export interface ProjectProps { /** @@ -229,6 +232,7 @@ export class Session { static project?: Project static basePath?: string + static contextPath?: string static checkDefaults?: CheckConfigDefaults static checkFilter?: CheckFilter static browserCheckDefaults?: CheckConfigDefaults @@ -246,6 +250,32 @@ export class Session { static ignoreDirectoriesMatch: string[] = [] static currentCommand?: 'pw-test' | 'test' | 'deploy' static includeFlagProvided?: boolean + static packageManager: PackageManager = npmPackageManager + static workspace: Result = Err(new Error(`Workspace support not initialized`)) + + static reset () { + this.project = undefined + this.basePath = undefined + this.contextPath = undefined + this.checkDefaults = undefined + this.checkFilter = undefined + this.browserCheckDefaults = undefined + this.multiStepCheckDefaults = undefined + this.checkFilePath = undefined + this.checkFileAbsolutePath = undefined + this.availableRuntimes = {} + this.defaultRuntimeId = undefined + this.verifyRuntimeDependencies = true + this.loadingChecklyConfigFile = false + this.checklyConfigFileConstructs = undefined + this.privateLocations = [] + this.parsers = new Map() + this.constructExports = [] + this.ignoreDirectoriesMatch = [] + this.packageManager = npmPackageManager + this.workspace = Err(new Error(`Workspace support not initialized`)) + this.resetSharedFiles() + } static async loadFile (filePath: string): Promise { const loader = this.loader @@ -340,6 +370,7 @@ export class Session { const parser = new Parser({ supportedNpmModules: Object.keys(runtime.dependencies), checkUnsupportedModules: Session.verifyRuntimeDependencies, + workspace: Session.workspace.ok(), }) Session.parsers.set(runtime.name, parser) @@ -351,6 +382,10 @@ export class Session { return pathToPosix(path.relative(Session.basePath!, filePath)) } + static contextRelativePosixPath (filePath: string): string { + return pathToPosix(path.relative(Session.contextPath!, filePath)) + } + static sharedFileRefs = new Map() static sharedFiles: SharedFile[] = [] diff --git a/packages/cli/src/loader/jiti.ts b/packages/cli/src/loader/jiti.ts index 5e74f65f8..218c216c0 100644 --- a/packages/cli/src/loader/jiti.ts +++ b/packages/cli/src/loader/jiti.ts @@ -1,7 +1,11 @@ +import Debug from 'debug' + import { FileLoader, FileLoaderOptions, UnsupportedFileLoaderError } from './loader' import { FileMatch } from './match' import { preferenceDelta } from './config' +const debug = Debug('checkly:cli:loader:jiti') + interface JitiExports { createJiti (id: string, userOptions?: any): Jiti } @@ -18,11 +22,16 @@ export class UninitializedJitiFileLoaderState extends FileLoader { async loadFile (filePath: string): Promise { UninitializedJitiFileLoaderState.init ??= (async () => { + debug('Initializing loader') try { const jitiExports: JitiExports = await import('jiti') - const jiti = jitiExports.createJiti(__filename) + const jiti = jitiExports.createJiti(__filename, { + tsx: true, + }) + debug(`Successfully initialized loader`) JitiFileLoader.state = new InitializedJitiFileLoaderState(jiti) } catch (err) { + debug(`Failed to initialize loader: ${err}`) JitiFileLoader.state = new FailedJitiFileLoaderState(err as Error) } })() @@ -58,8 +67,14 @@ export class InitializedJitiFileLoaderState extends FileLoader { } async loadFile (filePath: string): Promise { - const moduleExports = await this.jiti.import(filePath) - return moduleExports + debug(`Loading file ${filePath}`) + try { + const moduleExports = await this.jiti.import(filePath) + return moduleExports + } catch (err) { + debug(`Failed to load file ${filePath}: ${err}`) + throw err + } } } diff --git a/packages/cli/src/loader/ts-node.ts b/packages/cli/src/loader/ts-node.ts index 4acd32b42..326954935 100644 --- a/packages/cli/src/loader/ts-node.ts +++ b/packages/cli/src/loader/ts-node.ts @@ -1,7 +1,11 @@ +import Debug from 'debug' + import { preferenceDelta } from './config' import { FileLoader, FileLoaderOptions, UnsupportedFileLoaderError } from './loader' import { FileMatch } from './match' +const debug = Debug('checkly:cli:loader:ts-node') + interface TSNodeExports { register (opts?: any): TSNodeService } @@ -15,6 +19,7 @@ export class UninitializedTSNodeFileLoaderState extends FileLoader { async loadFile (filePath: string): Promise { UninitializedTSNodeFileLoaderState.init ??= (async () => { + debug('Initializing loader') try { const tsNodeExports: TSNodeExports = await import('ts-node') const service = tsNodeExports.register({ @@ -25,8 +30,10 @@ export class UninitializedTSNodeFileLoaderState extends FileLoader { module: 'CommonJS', }, }) + debug(`Successfully initialized loader`) TSNodeFileLoader.state = new InitializedTSNodeFileLoaderState(service) } catch (err) { + debug(`Failed to initialize loader: ${err}`) TSNodeFileLoader.state = new FailedTSNodeFileLoaderState(err as Error) } })() @@ -63,6 +70,7 @@ export class InitializedTSNodeFileLoaderState extends FileLoader { // eslint-disable-next-line require-await async loadFile (filePath: string): Promise { + debug(`Loading file ${filePath}`) try { this.service.enabled(true) @@ -70,6 +78,8 @@ export class InitializedTSNodeFileLoaderState extends FileLoader { const moduleExports = require(filePath) return moduleExports } catch (err: any) { + debug(`Failed to load file ${filePath}: ${err}`) + if (err.message?.includes('Unable to compile TypeScript')) { throw new Error(`Unable to load file '${filePath}' with 'ts-node' (hint: consider installing 'jiti' for improved TypeScript support)\n${err.stack}`, { cause: err as Error, diff --git a/packages/cli/src/rest/api.ts b/packages/cli/src/rest/api.ts index 03fcf6c75..0ed58f103 100644 --- a/packages/cli/src/rest/api.ts +++ b/packages/cli/src/rest/api.ts @@ -1,7 +1,7 @@ import axios, { AxiosInstance, InternalAxiosRequestConfig } from 'axios' import { name as CIname } from 'ci-info' import config from '../services/config' -import { assignProxy } from '../services/util' +import { assignProxy } from '../services/proxy' import Accounts, { Account } from './accounts' import Users from './users' import Projects from './projects' diff --git a/packages/cli/src/rest/runtimes.ts b/packages/cli/src/rest/runtimes.ts index 8330f41ed..9f80d2ca6 100644 --- a/packages/cli/src/rest/runtimes.ts +++ b/packages/cli/src/rest/runtimes.ts @@ -1,13 +1,6 @@ import type { AxiosInstance } from 'axios' -export interface Runtime { - name: string - stage?: string - runtimeEndOfLife?: string - description?: string - dependencies: Record - multiStepSupport?: boolean -} +import { Runtime } from '../runtimes' class Runtimes { api: AxiosInstance @@ -15,12 +8,14 @@ class Runtimes { this.api = api } - getAll () { - return this.api.get>('/v1/runtimes') + async getAll (): Promise { + const resp = await this.api.get>('/v1/runtimes') + return resp.data } - get (runtimeId: string) { - return this.api.get(`/v1/runtimes/${runtimeId}`) + async get (runtimeId: string): Promise { + const resp = await this.api.get(`/v1/runtimes/${runtimeId}`) + return resp.data } } diff --git a/packages/cli/src/runtimes/index.ts b/packages/cli/src/runtimes/index.ts new file mode 100644 index 000000000..96144156b --- /dev/null +++ b/packages/cli/src/runtimes/index.ts @@ -0,0 +1,2 @@ +export { Runtime } from './runtime' +export { loadSnapshot } from './snapshot' diff --git a/packages/cli/src/runtimes/runtime.ts b/packages/cli/src/runtimes/runtime.ts new file mode 100644 index 000000000..1198d64f8 --- /dev/null +++ b/packages/cli/src/runtimes/runtime.ts @@ -0,0 +1,8 @@ +export interface Runtime { + name: string + stage?: string + runtimeEndOfLife?: string + description?: string + dependencies: Record + multiStepSupport?: boolean +} diff --git a/packages/cli/src/runtimes/snapshot.ts b/packages/cli/src/runtimes/snapshot.ts new file mode 100644 index 000000000..772634a7e --- /dev/null +++ b/packages/cli/src/runtimes/snapshot.ts @@ -0,0 +1,20 @@ +import fs from 'node:fs/promises' +import path from 'node:path' + +import { Runtime } from './runtime' + +const SNAPSHOT_DATE = '20260130' + +const ASSETS_PATH = path.join(__dirname, '..', '..', 'assets') + +export async function loadSnapshot (): Promise { + const data = await fs.readFile(path.join( + ASSETS_PATH, + 'runtimes', + 'snapshots', + SNAPSHOT_DATE, + 'runtimes.json', + )) + + return JSON.parse(data.toString()) +} diff --git a/packages/cli/src/services/__tests__/checkly-config-loader.spec.ts b/packages/cli/src/services/__tests__/checkly-config-loader.spec.ts index cf88fd8d6..2f1146754 100644 --- a/packages/cli/src/services/__tests__/checkly-config-loader.spec.ts +++ b/packages/cli/src/services/__tests__/checkly-config-loader.spec.ts @@ -2,7 +2,7 @@ import path from 'node:path' import { describe, it, expect } from 'vitest' -import { loadChecklyConfig } from '../checkly-config-loader' +import { loadChecklyConfig, defaultFilenames } from '../checkly-config-loader' import { splitConfigFilePath } from '../util' describe('loadChecklyConfig()', () => { @@ -25,8 +25,10 @@ describe('loadChecklyConfig()', () => { try { await loadChecklyConfig(configDir) } catch (e: any) { - expect(e.message).toContain(`Unable to locate a config at ${configDir} with ${ - ['checkly.config.ts', 'checkly.config.mts', 'checkly.config.cts', 'checkly.config.js', 'checkly.config.mjs', 'checkly.config.cjs'].join(', ')}.`) + expect(e.message).toContain(`Unable to detect a Checkly configuration file`) + for (const filename of defaultFilenames) { + expect(e.message).toContain(filename) + } } }) it('config TS file should export an object', async () => { diff --git a/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/.gitignore b/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/.gitignore deleted file mode 100644 index 796317591..000000000 --- a/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Allow node_modules in test fixtures -!node_modules diff --git a/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/node_modules/@internal/test-helpers/helper.js b/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/node_modules/@internal/test-helpers/helper.js deleted file mode 100644 index 7fce01642..000000000 --- a/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/node_modules/@internal/test-helpers/helper.js +++ /dev/null @@ -1,4 +0,0 @@ -// Internal test helper -module.exports = { - helperFunction: () => 'helper' -} diff --git a/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/package-lock.json b/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/package-lock.json deleted file mode 100644 index 2942f5b2c..000000000 --- a/packages/cli/src/services/__tests__/fixtures/playwright-bundle-test/package-lock.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "playwright-bundle-test", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "playwright-bundle-test", - "version": "1.0.0", - "dependencies": { - "@playwright/test": "^1.40.0" - } - }, - "node_modules/@playwright/test": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.0.tgz", - "integrity": "sha512-example" - } - } -} diff --git a/packages/cli/src/services/__tests__/util.spec.ts b/packages/cli/src/services/__tests__/util.spec.ts index 53daa566a..b39cecb89 100644 --- a/packages/cli/src/services/__tests__/util.spec.ts +++ b/packages/cli/src/services/__tests__/util.spec.ts @@ -1,15 +1,11 @@ import path from 'node:path' -import fs from 'node:fs/promises' -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { extract } from 'tar' +import { describe, it, expect } from 'vitest' import { pathToPosix, isFileSync, getPlaywrightVersionFromPackage, - bundlePlayWrightProject, } from '../util' -import { Session } from '../../constructs/project' describe('util', () => { describe('pathToPosix()', () => { @@ -50,156 +46,4 @@ describe('util', () => { expect(version).toMatch(/^\d+\.\d+\.\d+/) }) }) - - describe('bundlePlayWrightProject()', () => { - let originalBasePath: string | undefined - let extractDir: string - - beforeEach(async () => { - // Save original Session state - originalBasePath = Session.basePath - - // Set up Session for bundling - const fixtureDir = path.join(__dirname, 'fixtures', 'playwright-bundle-test') - Session.basePath = fixtureDir - - // Create temp directory for extraction - extractDir = await fs.mkdtemp(path.join(__dirname, 'temp-extract-')) - }) - - afterEach(async () => { - // Restore Session state - Session.basePath = originalBasePath - Session.ignoreDirectoriesMatch = [] - - // Clean up extraction directory - try { - await fs.rm(extractDir, { recursive: true, force: true }) - } catch { - // Ignore cleanup errors - } - }) - - it('should exclude directories matching ignoreDirectoriesMatch pattern', async () => { - const fixtureDir = path.join(__dirname, 'fixtures', 'playwright-bundle-test') - const playwrightConfigPath = path.join(fixtureDir, 'playwright.config.ts') - - // Set ignoreDirectoriesMatch to exclude fixtures directory - Session.ignoreDirectoriesMatch = ['**/fixtures/**'] - - // Bundle the project - const result = await bundlePlayWrightProject(playwrightConfigPath, []) - - // Extract the bundle - await extract({ - file: result.outputFile, - cwd: extractDir, - }) - - // Check that test files are included - const testsDir = path.join(extractDir, 'tests') - const testFiles = await fs.readdir(testsDir) - expect(testFiles).toContain('example.spec.ts') - - // Check that fixtures directory is NOT included - const fixturesPath = path.join(extractDir, 'fixtures') - await expect(fs.access(fixturesPath)).rejects.toThrow() - }, 30000) - - it('should include all directories when ignoreDirectoriesMatch is empty', async () => { - const fixtureDir = path.join(__dirname, 'fixtures', 'playwright-bundle-test') - const playwrightConfigPath = path.join(fixtureDir, 'playwright.config.ts') - - // Set empty ignoreDirectoriesMatch - Session.ignoreDirectoriesMatch = [] - - // Bundle the project with include pattern that matches fixtures - const result = await bundlePlayWrightProject(playwrightConfigPath, ['fixtures/**/*']) - - // Extract the bundle - await extract({ - file: result.outputFile, - cwd: extractDir, - }) - - // Check that fixtures directory IS included when explicitly in include - const fixturesPath = path.join(extractDir, 'fixtures') - const fixturesExists = await fs.access(fixturesPath).then(() => true).catch(() => false) - expect(fixturesExists).toBe(true) - - if (fixturesExists) { - const fixtureFiles = await fs.readdir(fixturesPath) - expect(fixtureFiles).toContain('mock-data.json') - } - }, 30000) - - it('should include explicit node_modules patterns bypassing default ignores', async () => { - const fixtureDir = path.join(__dirname, 'fixtures', 'playwright-bundle-test') - const playwrightConfigPath = path.join(fixtureDir, 'playwright.config.ts') - - // Set empty ignoreDirectoriesMatch - Session.ignoreDirectoriesMatch = [] - - // Bundle the project with explicit node_modules pattern - const result = await bundlePlayWrightProject(playwrightConfigPath, ['node_modules/@internal/test-helpers/**']) - - // Extract the bundle - await extract({ - file: result.outputFile, - cwd: extractDir, - }) - - // Check that node_modules directory IS included when explicitly specified - const nodeModulesPath = path.join(extractDir, 'node_modules', '@internal', 'test-helpers') - const nodeModulesExists = await fs.access(nodeModulesPath).then(() => true).catch(() => false) - expect(nodeModulesExists).toBe(true) - - if (nodeModulesExists) { - const helperFiles = await fs.readdir(nodeModulesPath) - expect(helperFiles).toContain('helper.js') - } - }, 30000) - - it('should still respect custom ignoreDirectoriesMatch for explicit patterns', async () => { - const fixtureDir = path.join(__dirname, 'fixtures', 'playwright-bundle-test') - const playwrightConfigPath = path.join(fixtureDir, 'playwright.config.ts') - - // Set custom ignoreDirectoriesMatch to exclude @internal - Session.ignoreDirectoriesMatch = ['**/@internal/**'] - - // Bundle the project with explicit node_modules pattern - const result = await bundlePlayWrightProject(playwrightConfigPath, ['node_modules/@internal/test-helpers/**']) - - // Extract the bundle - await extract({ - file: result.outputFile, - cwd: extractDir, - }) - - // Check that @internal is NOT included (custom ignore still applies) - const nodeModulesPath = path.join(extractDir, 'node_modules', '@internal') - await expect(fs.access(nodeModulesPath)).rejects.toThrow() - }, 30000) - - it('should exclude node_modules with broad patterns despite include', async () => { - const fixtureDir = path.join(__dirname, 'fixtures', 'playwright-bundle-test') - const playwrightConfigPath = path.join(fixtureDir, 'playwright.config.ts') - - // Set empty ignoreDirectoriesMatch - Session.ignoreDirectoriesMatch = [] - - // Bundle with a broad pattern that would match node_modules but doesn't explicitly target it - const result = await bundlePlayWrightProject(playwrightConfigPath, ['**/*.js']) - - // Extract the bundle - await extract({ - file: result.outputFile, - cwd: extractDir, - }) - - // Check that node_modules is NOT included (default ignore still applies for broad patterns) - const nodeModulesPath = path.join(extractDir, 'node_modules') - await expect(fs.access(nodeModulesPath)).rejects.toThrow() - }, 30000) - }) }) diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/playwright-project-snapshots/package.json b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/playwright-project-snapshots/package.json new file mode 100644 index 000000000..430943643 --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/playwright-project-snapshots/package.json @@ -0,0 +1,8 @@ +{ + "name": "playwright-project-snapshots", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.57.0", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/playwright-project/package.json b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/playwright-project/package.json new file mode 100644 index 000000000..c263ade6b --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/playwright-project/package.json @@ -0,0 +1,8 @@ +{ + "name": "playwright-project", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.57.0", + "jiti": "^2.6.1" + } +} diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/package-lock.json b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/package-lock.json new file mode 100644 index 000000000..ed7a7f891 --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/package-lock.json @@ -0,0 +1,40 @@ +{ + "name": "workspace-example", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "workspace-example", + "version": "1.0.0", + "workspaces": [ + "packages/*" + ] + }, + "node_modules/bar": { + "resolved": "packages/bar", + "link": true + }, + "node_modules/baz": { + "resolved": "packages/baz", + "link": true + }, + "node_modules/foo": { + "resolved": "packages/foo", + "link": true + }, + "packages/bar": { + "version": "1.0.0", + "dependencies": { + "baz": "*", + "foo": "*" + } + }, + "packages/baz": { + "version": "1.0.0" + }, + "packages/foo": { + "version": "1.0.0" + } + } +} diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/package.json b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/package.json new file mode 100644 index 000000000..6f6c36ca4 --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/package.json @@ -0,0 +1,7 @@ +{ + "name": "workspace-example", + "version": "1.0.0", + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/checkly.config.ts b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/checkly.config.ts new file mode 100644 index 000000000..fffb104f3 --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/checkly.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'checkly' + +const config = defineConfig({ + projectName: 'Workspace Example Project', + logicalId: '56fbaf4d-fc2c-418c-868a-3f461809ed37', + checks: { + checkMatch: '**/__checks__/**/*.check.ts', + tags: [ + 'mac', + ], + }, +}) + +export default config diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/package.json b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/package.json new file mode 100644 index 000000000..f4d29576a --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/package.json @@ -0,0 +1,12 @@ +{ + "name": "bar", + "version": "1.0.0", + "main": "dist/index.js", + "exports": { + ".": "./dist/index.js" + }, + "dependencies": { + "foo": "*", + "baz": "*" + } +} \ No newline at end of file diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/src/__checks__/api-check-1/api.check.ts b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/src/__checks__/api-check-1/api.check.ts new file mode 100644 index 000000000..23491303b --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/src/__checks__/api-check-1/api.check.ts @@ -0,0 +1,12 @@ +import { ApiCheck } from 'checkly/constructs' + +new ApiCheck('api-check-1', { + name: 'API Check #1', + request: { + url: 'https://api.checklyhq.com', + method: 'GET', + }, + setupScript: { + entrypoint: './setup.ts' + } +}) diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/src/__checks__/api-check-1/setup.ts b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/src/__checks__/api-check-1/setup.ts new file mode 100644 index 000000000..9e9a34d15 --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/src/__checks__/api-check-1/setup.ts @@ -0,0 +1,3 @@ +import { value } from '@lib/helper' + +console.log(value) diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/src/index.ts b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/src/index.ts new file mode 100644 index 000000000..5ff6d2ff1 --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/src/index.ts @@ -0,0 +1 @@ +export { value } from '@lib/helper' diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/src/lib/helper.ts b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/src/lib/helper.ts new file mode 100644 index 000000000..3e02cd133 --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/src/lib/helper.ts @@ -0,0 +1,2 @@ +export { value } from 'foo' +export { value as value2 } from 'baz/lib/dep2' diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/tsconfig.json b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/tsconfig.json new file mode 100644 index 000000000..6d5cf4dfe --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/bar/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "declaration": true, + "module": "nodenext", + "esModuleInterop": true, + "outDir": "dist", + "rootDirs": [ + "src", + ], + "baseUrl": "./src", + "strict": true, + "target": "esnext", + "sourceMap": true, + "paths": { + "@lib/*": [ + "./lib/*" + ] + } + }, + "include": [ + "src/**/*", + ], +} \ No newline at end of file diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/package.json b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/package.json new file mode 100644 index 000000000..cb030520a --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/package.json @@ -0,0 +1,9 @@ +{ + "name": "baz", + "version": "1.0.0", + "main": "dist/index.js", + "exports": { + ".": "./dist/index.js", + "./lib/dep2": "./dist/lib/dep2.js" + } +} \ No newline at end of file diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/src/index.ts b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/src/index.ts new file mode 100644 index 000000000..435db7ccb --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/src/index.ts @@ -0,0 +1 @@ +export { value } from './lib/dep1' diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/src/lib/dep1.ts b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/src/lib/dep1.ts new file mode 100644 index 000000000..1d772ee2c --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/src/lib/dep1.ts @@ -0,0 +1 @@ +export const value = 1 diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/src/lib/dep2.ts b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/src/lib/dep2.ts new file mode 100644 index 000000000..3aee44a1e --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/src/lib/dep2.ts @@ -0,0 +1 @@ +export const value = 2 diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/tsconfig.json b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/tsconfig.json new file mode 100644 index 000000000..8c751e160 --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/baz/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "declaration": true, + "module": "nodenext", + "esModuleInterop": true, + "outDir": "dist", + "rootDirs": [ + "src" + ], + "strict": true, + "target": "esnext", + "sourceMap": true + }, + "include": [ + "src/**/*" + ], +} \ No newline at end of file diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/foo/package.json b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/foo/package.json new file mode 100644 index 000000000..81e70e9bb --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/foo/package.json @@ -0,0 +1,8 @@ +{ + "name": "foo", + "version": "1.0.0", + "main": "dist/index.js", + "exports": { + ".": "./dist/index.js" + } +} \ No newline at end of file diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/foo/src/index.ts b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/foo/src/index.ts new file mode 100644 index 000000000..435db7ccb --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/foo/src/index.ts @@ -0,0 +1 @@ +export { value } from './lib/dep1' diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/foo/src/lib/dep1.ts b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/foo/src/lib/dep1.ts new file mode 100644 index 000000000..1d772ee2c --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/foo/src/lib/dep1.ts @@ -0,0 +1 @@ +export const value = 1 diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/foo/tsconfig.json b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/foo/tsconfig.json new file mode 100644 index 000000000..8c751e160 --- /dev/null +++ b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/packages/foo/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "declaration": true, + "module": "nodenext", + "esModuleInterop": true, + "outDir": "dist", + "rootDirs": [ + "src" + ], + "strict": true, + "target": "esnext", + "sourceMap": true + }, + "include": [ + "src/**/*" + ], +} \ No newline at end of file diff --git a/packages/cli/src/constructs/__tests__/fixtures/playwright-check/playwright.config.ts b/packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/tsconfig.json similarity index 100% rename from packages/cli/src/constructs/__tests__/fixtures/playwright-check/playwright.config.ts rename to packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/workspace-example/tsconfig.json diff --git a/packages/cli/src/services/check-parser/__tests__/check-parser.spec.ts b/packages/cli/src/services/check-parser/__tests__/check-parser.spec.ts index c331e475d..9131063b8 100644 --- a/packages/cli/src/services/check-parser/__tests__/check-parser.spec.ts +++ b/packages/cli/src/services/check-parser/__tests__/check-parser.spec.ts @@ -1,8 +1,9 @@ import path from 'node:path' -import { describe, it, expect } from 'vitest' +import { describe, it, expect, beforeAll, afterAll } from 'vitest' import { Parser } from '../parser' +import { FixtureSandbox } from '../../../testing/fixture-sandbox' const defaultNpmModules = [ 'timers', 'tls', 'url', 'util', 'zlib', '@faker-js/faker', '@opentelemetry/api', '@opentelemetry/sd-trace-base', @@ -11,16 +12,31 @@ const defaultNpmModules = [ ] describe('dependency-parser - parser()', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + // These fixtures do not need packages as they are not executed and + // therefore do not import anything. + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'check-parser-fixtures'), + installPackages: false, + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + it('should handle JS file with no dependencies', async () => { const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) - const { dependencies } = await parser.parse(path.join(__dirname, 'check-parser-fixtures', 'no-dependencies.js')) + const { dependencies } = await parser.parse(fixt.abspath('no-dependencies.js')) expect(dependencies.map(d => d.filePath)).toHaveLength(0) }) it('should handle JS file with dependencies', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'simple-example', ...filepath) + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('simple-example', ...filepath) const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) @@ -36,7 +52,7 @@ describe('dependency-parser - parser()', () => { }) it('should report a missing entrypoint file', async () => { - const missingEntrypoint = path.join(__dirname, 'check-parser-fixtures', 'does-not-exist.js') + const missingEntrypoint = fixt.abspath('does-not-exist.js') expect.assertions(1) try { const parser = new Parser({ @@ -49,20 +65,24 @@ describe('dependency-parser - parser()', () => { }) it('should report missing check dependencies', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', ...filepath) expect.assertions(1) try { const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) - await parser.parse(toAbsolutePath('missing-dependencies.js')) + await parser.parse(fixt.abspath('missing-dependencies.js')) } catch (err) { - expect(err).toMatchObject({ missingFiles: [toAbsolutePath('does-not-exist.js'), toAbsolutePath('does-not-exist2.js')] }) + expect(err).toMatchObject({ + missingFiles: [ + fixt.abspath('does-not-exist.js'), + fixt.abspath('does-not-exist2.js'), + ], + }) } }) it('should report syntax errors', async () => { - const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'syntax-error.js') + const entrypoint = fixt.abspath('syntax-error.js') expect.assertions(1) try { const parser = new Parser({ @@ -70,12 +90,16 @@ describe('dependency-parser - parser()', () => { }) await parser.parse(entrypoint) } catch (err) { - expect(err).toMatchObject({ parseErrors: [{ file: entrypoint, error: 'Unexpected token (4:70)' }] }) + expect(err).toMatchObject({ + parseErrors: [ + { file: entrypoint, error: 'Unexpected token (4:70)' }, + ], + }) } }) it('should report unsupported dependencies', async () => { - const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'unsupported-dependencies.js') + const entrypoint = fixt.abspath('unsupported-dependencies.js') expect.assertions(1) try { const parser = new Parser({ @@ -83,12 +107,17 @@ describe('dependency-parser - parser()', () => { }) await parser.parse(entrypoint) } catch (err) { - expect(err).toMatchObject({ unsupportedNpmDependencies: [{ file: entrypoint, unsupportedDependencies: ['left-pad', 'right-pad'] }] }) + expect(err).toMatchObject({ + unsupportedNpmDependencies: [{ + file: entrypoint, + unsupportedDependencies: ['left-pad', 'right-pad'], + }], + }) } }) it('should allow unsupported dependencies if configured to do so', async () => { - const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'unsupported-dependencies.js') + const entrypoint = fixt.abspath('unsupported-dependencies.js') const parser = new Parser({ supportedNpmModules: defaultNpmModules, checkUnsupportedModules: false, @@ -97,7 +126,7 @@ describe('dependency-parser - parser()', () => { }) it('should handle circular dependencies', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'circular-dependencies', ...filepath) + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('circular-dependencies', ...filepath) const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) @@ -113,7 +142,7 @@ describe('dependency-parser - parser()', () => { }) it('should parse typescript dependencies', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'typescript-example', ...filepath) + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('typescript-example', ...filepath) const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) @@ -135,7 +164,7 @@ describe('dependency-parser - parser()', () => { }) it('should parse typescript dependencies relying on tsconfig when tsconfig has comments', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'tsconfig-json-text', ...filepath) + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('tsconfig-json-text', ...filepath) const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) @@ -147,7 +176,7 @@ describe('dependency-parser - parser()', () => { }) it('should parse typescript dependencies using tsconfig paths', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'tsconfig-paths-sample-project', ...filepath) + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('tsconfig-paths-sample-project', ...filepath) const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) @@ -162,17 +191,20 @@ describe('dependency-parser - parser()', () => { toAbsolutePath('lib1', 'tsconfig.json'), toAbsolutePath('lib2', 'index.ts'), toAbsolutePath('lib3', 'foo', 'bar.ts'), + toAbsolutePath('lib3', 'jsconfig.json'), + toAbsolutePath('package.json'), toAbsolutePath('tsconfig.json'), ]) }) it('should parse typescript dependencies using tsconfig paths relative to baseUrl', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'tsconfig-paths-baseurl-relative', ...filepath) + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('tsconfig-paths-baseurl-relative', ...filepath) const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) const { dependencies } = await parser.parse(toAbsolutePath('src', 'entrypoint.ts')) expect(dependencies.map(d => d.filePath).sort()).toEqual([ + toAbsolutePath('package.json'), toAbsolutePath('src', 'lib1', 'file1.ts'), toAbsolutePath('src', 'lib1', 'file2.ts'), toAbsolutePath('src', 'lib1', 'folder', 'file1.ts'), @@ -182,21 +214,24 @@ describe('dependency-parser - parser()', () => { toAbsolutePath('src', 'lib1', 'tsconfig.json'), toAbsolutePath('src', 'lib2', 'index.ts'), toAbsolutePath('src', 'lib3', 'foo', 'bar.ts'), + toAbsolutePath('src', 'lib3', 'jsconfig.json'), toAbsolutePath('tsconfig.json'), ]) }) - it('should not include tsconfig if not needed', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'tsconfig-paths-unused', ...filepath) + it('should always include tsconfig even if not needed', async () => { + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('tsconfig-paths-unused', ...filepath) const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) const { dependencies } = await parser.parse(toAbsolutePath('src', 'entrypoint.ts')) - expect(dependencies.map(d => d.filePath).sort()).toEqual([]) + expect(dependencies.map(d => d.filePath).sort()).toEqual([ + toAbsolutePath('tsconfig.json'), + ]) }) it('should support importing ts extensions if allowed', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'tsconfig-allow-importing-ts-extensions', ...filepath) + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('tsconfig-allow-importing-ts-extensions', ...filepath) const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) @@ -205,11 +240,12 @@ describe('dependency-parser - parser()', () => { toAbsolutePath('src', 'dep1.ts'), toAbsolutePath('src', 'dep2.ts'), toAbsolutePath('src', 'dep3.ts'), + toAbsolutePath('tsconfig.json'), ]) }) it('should not import TS files from a JS file', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'no-import-ts-from-js', ...filepath) + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('no-import-ts-from-js', ...filepath) const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) @@ -229,7 +265,7 @@ describe('dependency-parser - parser()', () => { }) it('should import JS files from a TS file', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'import-js-from-ts', ...filepath) + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('import-js-from-ts', ...filepath) const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) @@ -242,7 +278,7 @@ describe('dependency-parser - parser()', () => { }) it('should handle ES Modules', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'esmodules-example', ...filepath) + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('esmodules-example', ...filepath) const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) @@ -257,7 +293,7 @@ describe('dependency-parser - parser()', () => { }) it('should handle Common JS and ES Modules', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'common-esm-example', ...filepath) + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('common-esm-example', ...filepath) const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) @@ -273,7 +309,7 @@ describe('dependency-parser - parser()', () => { }) it('should handle node: prefix for built-ins', async () => { - const toAbsolutePath = (...filepath: string[]) => path.join(__dirname, 'check-parser-fixtures', 'builtin-with-node-prefix', ...filepath) + const toAbsolutePath = (...filepath: string[]) => fixt.abspath('builtin-with-node-prefix', ...filepath) const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) @@ -286,7 +322,7 @@ describe('dependency-parser - parser()', () => { * We could address this by keeping track of assignments as we walk the AST. */ it.skip('should ignore cases where require is reassigned', async () => { - const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'reassign-require.js') + const entrypoint = fixt.abspath('reassign-require.js') const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) @@ -296,7 +332,7 @@ describe('dependency-parser - parser()', () => { // Checks run on Checkly are wrapped to support top level await. // For consistency with checks created via the UI, the CLI should support this as well. it('should allow top-level await', async () => { - const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'top-level-await.js') + const entrypoint = fixt.abspath('top-level-await.js') const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) @@ -304,7 +340,7 @@ describe('dependency-parser - parser()', () => { }) it('should allow top-level await in TypeScript', async () => { - const entrypoint = path.join(__dirname, 'check-parser-fixtures', 'top-level-await.ts') + const entrypoint = fixt.abspath('top-level-await.ts') const parser = new Parser({ supportedNpmModules: defaultNpmModules, }) diff --git a/packages/cli/src/services/check-parser/__tests__/parse-files.spec.ts b/packages/cli/src/services/check-parser/__tests__/parse-files.spec.ts index ecbb0f17f..5654f018c 100644 --- a/packages/cli/src/services/check-parser/__tests__/parse-files.spec.ts +++ b/packages/cli/src/services/check-parser/__tests__/parse-files.spec.ts @@ -1,35 +1,102 @@ import path from 'node:path' -import { describe, it, expect } from 'vitest' - -import { Parser } from '../parser' -import { PlaywrightConfig } from '../../playwright-config' -import { Session } from '../../../constructs' - -const fixturePath = path.join(__dirname, 'check-parser-fixtures') - -describe('project parser - getFilesAndDependencies()', () => { - it('should handle spec file', async () => { - const projectPath = path.join(fixturePath, 'playwright-project') - - const playwrightConfig = new PlaywrightConfig( - path.join(projectPath, 'playwright.config.ts'), - await Session.loadFile(path.join(projectPath, 'playwright.config.ts')), - ) - const parser = new Parser({}) - const res = await parser.getFilesAndDependencies(playwrightConfig) - expect(res.files).toHaveLength(2) - expect(res.errors).toHaveLength(0) +import { describe, test, expect, afterAll, beforeAll } from 'vitest' + +import { File } from '../parser' +import { FixtureSandbox } from '../../../testing/fixture-sandbox' +import { pathToPosix } from '../../util' + +describe('project parser - getFilesAndDependencies()', { timeout: 45_000 }, () => { + describe('playwright-project', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'check-parser-fixtures', 'playwright-project'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + + test('should handle spec file', async () => { + const result = await fixt.run('npx', [ + 'checkly', + 'debug', + 'parse-playwright-config', + '--file', + fixt.abspath('playwright.config.ts'), + ]) + + if (result.exitCode !== 0) { + // eslint-disable-next-line no-console + console.error('stderr', result.stderr) + // eslint-disable-next-line no-console + console.error('stdout', result.stdout) + } + + expect(result.exitCode).toBe(0) + + const output: { + files: File[] + errors: string[] + } = JSON.parse(result.stdout) + + expect(output.files).toEqual(expect.arrayContaining([ + { physical: true, filePath: pathToPosix(fixt.abspath('package.json')) }, + { physical: true, filePath: pathToPosix(fixt.abspath('playwright.config.ts')) }, + { physical: true, filePath: pathToPosix(fixt.abspath('tests', 'example.spec.ts')) }, + ])) + expect(output.files).toHaveLength(3) + expect(output.errors).toHaveLength(0) + }) }) - it('should handle a spec file with snapshots', async () => { - const projectPath = path.join(fixturePath, 'playwright-project-snapshots') - const playwrightConfig = new PlaywrightConfig( - path.join(projectPath, 'playwright.config.ts'), - await Session.loadFile(path.join(projectPath, 'playwright.config.ts')), - ) - const parser = new Parser({}) - const res = await parser.getFilesAndDependencies(playwrightConfig) - expect(res.files).toHaveLength(3) - expect(res.errors).toHaveLength(0) + + describe('playwright-project-snapshots', () => { + let fixt: FixtureSandbox + + beforeAll(async () => { + fixt = await FixtureSandbox.create({ + source: path.join(__dirname, 'check-parser-fixtures', 'playwright-project-snapshots'), + }) + }, 180_000) + + afterAll(async () => { + await fixt?.destroy() + }) + + test('should handle a spec file with snapshots', async () => { + const result = await fixt.run('npx', [ + 'checkly', + 'debug', + 'parse-playwright-config', + '--file', + fixt.abspath('playwright.config.ts'), + ]) + + if (result.exitCode !== 0) { + // eslint-disable-next-line no-console + console.error('stderr', result.stderr) + // eslint-disable-next-line no-console + console.error('stdout', result.stdout) + } + + expect(result.exitCode).toBe(0) + + const output: { + files: File[] + errors: string[] + } = JSON.parse(result.stdout) + + expect(output.files).toEqual(expect.arrayContaining([ + { physical: true, filePath: pathToPosix(fixt.abspath('package.json')) }, + { physical: true, filePath: pathToPosix(fixt.abspath('playwright.config.ts')) }, + { physical: true, filePath: pathToPosix(fixt.abspath('tests/example.spec.ts')) }, + { physical: true, filePath: expect.stringContaining('example.spec.ts-snapshots') }, + ])) + expect(output.files).toHaveLength(4) + expect(output.errors).toHaveLength(0) + }) }) }) diff --git a/packages/cli/src/services/check-parser/errors.ts b/packages/cli/src/services/check-parser/errors.ts index 8afd92db0..eca8aead7 100644 --- a/packages/cli/src/services/check-parser/errors.ts +++ b/packages/cli/src/services/check-parser/errors.ts @@ -42,6 +42,7 @@ export class DependencyParseError extends Error { } } super(message) + this.name = 'DependencyParseError' this.entrypoint = entrypoint this.missingFiles = missingFiles this.unsupportedNpmDependencies = unsupportedNpmDependencies diff --git a/packages/cli/src/services/check-parser/faux-package.ts b/packages/cli/src/services/check-parser/faux-package.ts new file mode 100644 index 000000000..d43365a4f --- /dev/null +++ b/packages/cli/src/services/check-parser/faux-package.ts @@ -0,0 +1,22 @@ +import { Package } from './package-files/workspace' +import { VirtualFile } from './parser' + +const DESCRIPTION = `This is a placeholder for an otherwise unused package ` + + `that Checkly determined to be needed during the installation step.` + +export function createFauxPackageFiles (pkg: Package): VirtualFile[] { + return [{ + filePath: pkg.packageJsonPath, + physical: false, + content: JSON.stringify( + { + name: pkg.name, + version: '0.0.0', + description: DESCRIPTION, + private: true, + }, + undefined, + 2, + ), + }] +} diff --git a/packages/cli/src/services/check-parser/package-files/extension.ts b/packages/cli/src/services/check-parser/package-files/extension.ts index 6f28c9a8c..d1937d9ef 100644 --- a/packages/cli/src/services/check-parser/package-files/extension.ts +++ b/packages/cli/src/services/check-parser/package-files/extension.ts @@ -4,6 +4,10 @@ export type CoreExtension = '.js' | '.mjs' | '.cjs' | '.json' export const CoreExtensions: CoreExtension[] = ['.js', '.mjs', '.cjs', '.json'] +export function isCoreExtension (value: string): value is CoreExtension { + return CoreExtensions.includes(value as CoreExtension) +} + type CoreExtensionMapping = { [key in CoreExtension]: string[] } @@ -26,9 +30,13 @@ export const tsCoreExtensionLookupOrder: CoreExtensionMapping = { '.json': ['.json'], } -export type TSExtension = '.ts' | '.mts' | '.tsx' +export type TSExtension = '.ts' | '.mts' | '.cts' | '.tsx' -export const TSExtensions: TSExtension[] = ['.ts', '.mts', '.tsx'] +export const TSExtensions: TSExtension[] = ['.ts', '.mts', '.cts', '.tsx'] + +export function isTSExtension (value: string): value is TSExtension { + return TSExtensions.includes(value as TSExtension) +} type TSExtensionMapping = { [key in TSExtension]: string[] @@ -37,6 +45,7 @@ type TSExtensionMapping = { export const tsExtensionLookupOrder: TSExtensionMapping = { '.ts': ['.ts'], '.mts': ['.mts'], + '.cts': ['.cts'], '.tsx': ['.tsx'], } diff --git a/packages/cli/src/services/check-parser/package-files/json-source-file.ts b/packages/cli/src/services/check-parser/package-files/json-source-file.ts index 521da653d..0b274c586 100644 --- a/packages/cli/src/services/check-parser/package-files/json-source-file.ts +++ b/packages/cli/src/services/check-parser/package-files/json-source-file.ts @@ -26,4 +26,13 @@ export class JsonSourceFile { // Ignore. } } + + static async loadFromFilePath (filePath: string): Promise | undefined> { + const sourceFile = await SourceFile.loadFromFilePath(filePath) + if (!sourceFile) { + return + } + + return JsonSourceFile.loadFromSourceFile(sourceFile) + } } diff --git a/packages/cli/src/services/check-parser/package-files/json-text-source-file-parser.ts b/packages/cli/src/services/check-parser/package-files/json-text-source-file-parser.ts index 08c0858f3..3d357c84b 100644 --- a/packages/cli/src/services/check-parser/package-files/json-text-source-file-parser.ts +++ b/packages/cli/src/services/check-parser/package-files/json-text-source-file-parser.ts @@ -6,7 +6,8 @@ class UninitializedJsonTextSourceFileParserState extends SourceFileParser { async #parser (): Promise { try { - const { parseJsonText, convertToObject } = await import('typescript') + const typescriptExports = await import('typescript') + const { parseJsonText, convertToObject } = typescriptExports.default const parser = new SourceFileParserFuncState((sourceFile: SourceFile) => { const errors: any[] = [] diff --git a/packages/cli/src/services/check-parser/package-files/package-json-file.ts b/packages/cli/src/services/check-parser/package-files/package-json-file.ts index e2fd55e3f..92c052244 100644 --- a/packages/cli/src/services/check-parser/package-files/package-json-file.ts +++ b/packages/cli/src/services/check-parser/package-files/package-json-file.ts @@ -4,9 +4,24 @@ import semver from 'semver' import { JsonSourceFile } from './json-source-file' import { FileMeta, SourceFile } from './source-file' +import { PathResolver, ResolveResult } from './paths' -type ExportCondition = - 'node-addons' | 'node' | 'import' | 'require' | 'module-sync' | 'default' +type ConditionKey = + | 'node-addons' + | 'node' + | 'import' + | 'require' + | 'module-sync' + | 'default' + // Allow any string value, but keep auto complete for known values. + | (string & Record) + +type Exports = + | string + | Record> + +type Imports = + | Record> type Schema = { name?: string @@ -14,10 +29,12 @@ type Schema = { license?: string main?: string engines?: Record - exports?: string | string[] | Record | Record> + exports?: Exports + imports?: Imports dependencies?: Record devDependencies?: Record private?: boolean + workspaces?: string[] } export interface EngineSupportResult { @@ -47,10 +64,66 @@ export class PackageJsonFile { : [fallbackMainPath] } + hasExports (): boolean { + return !!this.jsonFile.data.exports + } + + resolveExportPath (exportPath: string, conditions: ConditionKey[]): ResolveResult { + const resolver = PathResolver.createFromPaths( + this.basePath, + this.#resolveExports(this.jsonFile.data.exports ?? {}, conditions), + ) + + // Exports must always start with "./" - make sure that the path we're + // matching against also starts with that prefix. + if (!exportPath.startsWith('./')) { + exportPath = `./${exportPath}` + } + + return resolver.resolve(exportPath) + } + + #resolveExports (exports: Exports, conditions: ConditionKey[]): Record { + if (typeof exports === 'string') { + return { + '.': [exports], + } + } + + const resolved: Record = {} + + Resolve: + for (const [from, rules] of Object.entries(exports)) { + if (typeof rules === 'string') { + resolved[from] = [rules] + continue Resolve + } + + for (const [condition, to] of Object.entries(rules)) { + if (conditions.includes(condition)) { + resolved[from] = [to] + continue Resolve + } + } + + const fallback = rules['default'] + if (fallback) { + resolved[from] = [fallback] + continue Resolve + } + } + + return resolved + } + public get meta () { return this.jsonFile.meta } + public get name () { + return this.jsonFile.data.name + } + public get version () { return this.jsonFile.data.version } @@ -67,6 +140,10 @@ export class PackageJsonFile { return this.jsonFile.data.engines } + public get workspaces () { + return this.jsonFile.data.workspaces + } + supportsEngine (engine: string, version: string): EngineSupportResult { const requirements = this.engines?.[engine] if (requirements === undefined) { @@ -105,6 +182,24 @@ export class PackageJsonFile { return new PackageJsonFile(jsonFile) } + static async loadFromSourceFile (sourceFile: SourceFile): Promise { + const jsonSourceFile = await JsonSourceFile.loadFromSourceFile(sourceFile) + if (!jsonSourceFile) { + return + } + + return PackageJsonFile.loadFromJsonSourceFile(jsonSourceFile) + } + + static async loadFromFilePath (filePath: string): Promise { + const sourceFile = await SourceFile.loadFromFilePath(filePath) + if (!sourceFile) { + return + } + + return PackageJsonFile.loadFromSourceFile(sourceFile) + } + static filePath (dirPath: string) { return path.join(dirPath, PackageJsonFile.FILENAME) } diff --git a/packages/cli/src/services/check-parser/package-files/package-manager.ts b/packages/cli/src/services/check-parser/package-files/package-manager.ts index 5594a26e4..3f13ed596 100644 --- a/packages/cli/src/services/check-parser/package-files/package-manager.ts +++ b/packages/cli/src/services/check-parser/package-files/package-manager.ts @@ -3,6 +3,10 @@ import path from 'node:path' import { lineage } from './walk' import { shellQuote } from '../../../services/shell' +import { PackageJsonFile } from './package-json-file' +import { JsonSourceFile } from './json-source-file' +import { OptionalWorkspaceFile, Package, Workspace, WorkspaceOptions } from './workspace' +import { Err, Ok } from './result' export class Runnable { executable: string @@ -20,8 +24,12 @@ export class Runnable { export interface PackageManager { get name (): string + get representativeLockfile (): string | undefined + get representativeConfigFile (): string | undefined installCommand (): Runnable execCommand (args: string[]): Runnable + lookupWorkspace (dir: string): Promise + detector (): PackageManagerDetector } class NotDetectedError extends Error {} @@ -31,10 +39,16 @@ export abstract class PackageManagerDetector { abstract detectUserAgent (userAgent: string): boolean abstract detectRuntime (): boolean abstract get representativeLockfile (): string | undefined + abstract get representativeConfigFile (): string | undefined abstract detectLockfile (dir: string): Promise + abstract detectConfigFile (dir: string): Promise abstract detectExecutable (lookup: PathLookup): Promise abstract installCommand (): Runnable abstract execCommand (args: string[]): Runnable + abstract lookupWorkspace (dir: string): Promise + detector (): PackageManagerDetector { + return this + } } export class NpmDetector extends PackageManagerDetector implements PackageManager { @@ -54,10 +68,19 @@ export class NpmDetector extends PackageManagerDetector implements PackageManage return 'package-lock.json' } + get representativeConfigFile (): undefined { + return + } + async detectLockfile (dir: string): Promise { return await accessR(path.join(dir, this.representativeLockfile)) } + // eslint-disable-next-line require-await, @typescript-eslint/no-unused-vars + async detectConfigFile (dir: string): Promise { + throw new NotDetectedError() + } + async detectExecutable (lookup: PathLookup): Promise { await lookup.detectPresence('npm') } @@ -69,6 +92,10 @@ export class NpmDetector extends PackageManagerDetector implements PackageManage execCommand (args: string[]): Runnable { return new Runnable('npx', args) } + + async lookupWorkspace (dir: string): Promise { + return await lookupNearestPackageJsonWorkspace(this, dir) + } } export class CNpmDetector extends PackageManagerDetector implements PackageManager { @@ -88,11 +115,20 @@ export class CNpmDetector extends PackageManagerDetector implements PackageManag return } + get representativeConfigFile (): undefined { + return + } + // eslint-disable-next-line require-await async detectLockfile (): Promise { throw new NotDetectedError() } + // eslint-disable-next-line require-await, @typescript-eslint/no-unused-vars + async detectConfigFile (dir: string): Promise { + throw new NotDetectedError() + } + async detectExecutable (lookup: PathLookup): Promise { await lookup.detectPresence('cnpm') } @@ -104,6 +140,10 @@ export class CNpmDetector extends PackageManagerDetector implements PackageManag execCommand (args: string[]): Runnable { return new Runnable('npx', args) } + + async lookupWorkspace (dir: string): Promise { + return await lookupNearestPackageJsonWorkspace(this, dir) + } } export class PNpmDetector extends PackageManagerDetector implements PackageManager { @@ -123,10 +163,18 @@ export class PNpmDetector extends PackageManagerDetector implements PackageManag return 'pnpm-lock.yaml' } + get representativeConfigFile (): string { + return 'pnpm-workspace.yaml' + } + async detectLockfile (dir: string): Promise { return await accessR(path.join(dir, this.representativeLockfile)) } + async detectConfigFile (dir: string): Promise { + return await accessR(path.join(dir, this.representativeConfigFile)) + } + async detectExecutable (lookup: PathLookup): Promise { await lookup.detectPresence('pnpm') } @@ -138,6 +186,81 @@ export class PNpmDetector extends PackageManagerDetector implements PackageManag execCommand (args: string[]): Runnable { return new Runnable('pnpm', args) } + + async lookupWorkspace (dir: string): Promise { + // To avoid having to bring in a yaml parser, just call pnpm directly. + // However, to avoid calling pnpm if it's likely not installed, detect + // the presence of the workspace file first. + for (const searchPath of lineage(dir)) { + try { + await this.detectConfigFile(searchPath) + } catch { + continue + } + + const { execa } = await import('execa') + + const pnpmArgs = [ + 'list', + '--json', + '--recursive', + '--depth', + '1', + ] + + const result = await execa('pnpm', pnpmArgs, { + cwd: searchPath, + }) + + type PnpmProjectOutput = { + name: string + path: string + } + + const output: PnpmProjectOutput[] = JSON.parse(result.stdout) + if (!Array.isArray(output)) { + throw new Error(`The output of 'pnpm list' was not an array (stdout=${result.stdout}, stderr=${result.stderr})`) + } + + const [root, dependencies] = output.reduce( + ([root, dependencies]: [PnpmProjectOutput | undefined, PnpmProjectOutput[]], project) => { + if (root === undefined) { + return [project, dependencies] + } + + // The project with the shortest path should be the workspace root. + if (root.path.length > project.path.length) { + return [project, [...dependencies, root]] + } + + return [root, [...dependencies, project]] + }, + [undefined, []], + ) + + if (root === undefined) { + return + } + + const rootPackage = new Package({ + name: root.name, + path: root.path, + workspaces: dependencies.map(dep => dep.path), + }) + + const packages = dependencies.map(({ name, path }) => { + return new Package({ + name, + path, + }) + }) + + return await initWorkspace(this, { + root: rootPackage, + packages, + }) + } + } } export class YarnDetector extends PackageManagerDetector implements PackageManager { @@ -157,10 +280,19 @@ export class YarnDetector extends PackageManagerDetector implements PackageManag return 'yarn.lock' } + get representativeConfigFile (): undefined { + return + } + async detectLockfile (dir: string): Promise { return await accessR(path.join(dir, this.representativeLockfile)) } + // eslint-disable-next-line require-await, @typescript-eslint/no-unused-vars + async detectConfigFile (dir: string): Promise { + throw new NotDetectedError() + } + async detectExecutable (lookup: PathLookup): Promise { await lookup.detectPresence('yarn') } @@ -172,6 +304,10 @@ export class YarnDetector extends PackageManagerDetector implements PackageManag execCommand (args: string[]): Runnable { return new Runnable('yarn', args) } + + async lookupWorkspace (dir: string): Promise { + return await lookupNearestPackageJsonWorkspace(this, dir) + } } export class DenoDetector extends PackageManagerDetector implements PackageManager { @@ -191,10 +327,18 @@ export class DenoDetector extends PackageManagerDetector implements PackageManag return 'deno.lock' } + get representativeConfigFile (): string { + return 'deno.json' + } + async detectLockfile (dir: string): Promise { return await accessR(path.join(dir, this.representativeLockfile)) } + async detectConfigFile (dir: string): Promise { + return await accessR(path.join(dir, this.representativeConfigFile)) + } + async detectExecutable (lookup: PathLookup): Promise { await lookup.detectPresence('deno') } @@ -206,6 +350,54 @@ export class DenoDetector extends PackageManagerDetector implements PackageManag execCommand (args: string[]): Runnable { return new Runnable('deno', ['run', '-A', `npm:${args[0]}`, ...args.slice(1)]) } + + async lookupWorkspace (dir: string): Promise { + for (const searchPath of lineage(dir)) { + try { + const configFile = await this.detectConfigFile(searchPath) + + type Schema = { + workspace?: string[] + } + + const jsonFile = await JsonSourceFile.loadFromFilePath(configFile) + if (!jsonFile) { + continue + } + + const rootPackage = await Package.loadFromDirPath(searchPath) + if (rootPackage === undefined) { + continue + } + + const workspaces = jsonFile.data.workspace?.map(packagePath => { + return path.resolve(searchPath, packagePath) + }) + + if (!workspaces) { + continue + } + + const packages: Package[] = [] + + for (const workspace of workspaces) { + const workspacePackage = await Package.loadFromDirPath(workspace) + if (workspacePackage === undefined) { + continue + } + + packages.push(workspacePackage) + } + + return await initWorkspace(this, { + root: rootPackage, + packages, + }) + } catch { + continue + } + } + } } export class BunDetector extends PackageManagerDetector implements PackageManager { @@ -225,10 +417,19 @@ export class BunDetector extends PackageManagerDetector implements PackageManage return 'bun.lockb' } + get representativeConfigFile (): undefined { + return + } + async detectLockfile (dir: string): Promise { return await accessR(path.join(dir, this.representativeLockfile)) } + // eslint-disable-next-line require-await, @typescript-eslint/no-unused-vars + async detectConfigFile (dir: string): Promise { + throw new NotDetectedError() + } + async detectExecutable (lookup: PathLookup): Promise { await lookup.detectPresence('bun') } @@ -240,6 +441,10 @@ export class BunDetector extends PackageManagerDetector implements PackageManage execCommand (args: string[]): Runnable { return new Runnable('bunx', args) } + + async lookupWorkspace (dir: string): Promise { + return await lookupNearestPackageJsonWorkspace(this, dir) + } } async function accessR (filePath: string): Promise { @@ -334,7 +539,7 @@ export class PathLookup { } } -const npmDetector = new NpmDetector() +export const npmPackageManager = new NpmDetector() // The order of the detectors is relevant to the lookup order. export const knownPackageManagers: PackageManagerDetector[] = [ @@ -343,7 +548,7 @@ export const knownPackageManagers: PackageManagerDetector[] = [ new DenoDetector(), new YarnDetector(), new CNpmDetector(), - npmDetector, + npmPackageManager, ] export interface DetectOptions { @@ -386,6 +591,18 @@ export async function detectPackageManager ( // Nothing detected. } + // Next, try to find a config file. + try { + const { packageManager } = await detectNearestConfigFile(dir, { + detectors, + root: options?.root, + }) + + return packageManager + } catch { + // Nothing detected. + } + // Finally, try to find a relevant executable. // // This can generate a whole bunch of path lookups. Try one by one despite @@ -401,7 +618,7 @@ export async function detectPackageManager ( } // If all else fails, just assume npm. - return npmDetector + return npmPackageManager } export interface NearestLockFile { @@ -461,3 +678,163 @@ export async function detectNearestLockfile ( throw new NoLockfileFoundError(searchPaths, lockfiles) } + +export interface NearestConfigFile { + packageManager: PackageManager + configFile: string +} + +export class NoConfigFileFoundError extends Error { + searchPaths: string[] + configFiles: string[] + + constructor (searchPaths: string[], configFiles: string[], options?: ErrorOptions) { + const message = `Unable to detect a config file in any of the following paths:` + + `\n\n` + + `${searchPaths.map(searchPath => ` ${searchPath}`).join('\n')}` + + `\n\n` + + `Config files we looked for:` + + `\n\n` + + `${configFiles.map(lockfile => ` ${lockfile}`).join('\n')}` + super(message, options) + this.name = 'NoConfigFileFoundError' + this.searchPaths = searchPaths + this.configFiles = configFiles + } +} + +export async function detectNearestConfigFile ( + dir: string, + options?: DetectOptions, +): Promise { + const detectors = options?.detectors ?? knownPackageManagers + + const searchPaths: string[] = [] + + for (const searchPath of lineage(dir, { root: options?.root })) { + try { + searchPaths.push(searchPath) + + // Assume that only a single kind of config file exists, which means + // the resolve order does not matter. + return await Promise.any(detectors.map(async detector => { + const configFile = await detector.detectConfigFile(searchPath) + return { + packageManager: detector, + configFile, + } + })) + } catch { + // Nothing detected. + } + } + + const configFiles = detectors.reduce((acc, detector) => { + return acc.concat(detector.representativeConfigFile ?? []) + }, []) + + throw new NoConfigFileFoundError(searchPaths, configFiles) +} + +export class NoPackageJsonFoundError extends Error { + searchPaths: string[] + + constructor (searchPaths: string[], options?: ErrorOptions) { + const message = `Unable to detect a package.json in any of the following paths:` + + `\n\n` + + `${searchPaths.map(searchPath => ` ${searchPath}`).join('\n')}` + super(message, options) + this.name = 'NoPackageJsonFoundError' + this.searchPaths = searchPaths + } +} + +export interface DetectNearestPackageJsonOptions { + root?: string +} + +export async function detectNearestPackageJson ( + dir: string, + options?: DetectNearestPackageJsonOptions, +): Promise { + const searchPaths: string[] = [] + + for (const searchPath of lineage(dir, { root: options?.root })) { + searchPaths.push(searchPath) + + const packageJson = await PackageJsonFile.loadFromFilePath( + PackageJsonFile.filePath(searchPath), + ) + + if (packageJson) { + return packageJson + } + } + + throw new NoPackageJsonFoundError(searchPaths) +} + +export async function fauxWorkspaceFromPackageJson ( + packageManager: PackageManager, + packageJsonFile: PackageJsonFile, +): Promise { + const rootPackage = new Package({ + name: packageJsonFile.name!, + path: packageJsonFile.basePath, + }) + + return await initWorkspace(packageManager.detector(), { + root: rootPackage, + packages: [], + }) +} + +async function lookupNearestPackageJsonWorkspace ( + detector: PackageManagerDetector, + dir: string, +): Promise { + for (const searchPath of lineage(dir)) { + const rootPackage = await Package.loadFromDirPath(searchPath) + if (!rootPackage) { + continue + } + + if (rootPackage.workspaces === undefined || rootPackage.workspaces.length === 0) { + continue + } + + const packages = await Workspace.resolvePatterns(searchPath, rootPackage.workspaces) + + return await initWorkspace(detector, { + root: rootPackage, + packages, + }) + } +} + +async function initWorkspace ( + detector: PackageManagerDetector, + options: Pick, +) { + const lockfile: OptionalWorkspaceFile = await detectNearestLockfile(options.root.path, { + root: options.root.path, + detectors: [detector], + }).then( + ({ lockfile }) => Ok(lockfile), + reason => Err(reason), + ) + + const configFile: OptionalWorkspaceFile = await detectNearestConfigFile(options.root.path, { + root: options.root.path, + detectors: [detector], + }).then( + ({ configFile }) => Ok(configFile), + reason => Err(reason), + ) + + return new Workspace({ + ...options, + lockfile, + configFile, + }) +} diff --git a/packages/cli/src/services/check-parser/package-files/paths.ts b/packages/cli/src/services/check-parser/package-files/paths.ts index 818d60451..5f9bc6f6f 100644 --- a/packages/cli/src/services/check-parser/package-files/paths.ts +++ b/packages/cli/src/services/check-parser/package-files/paths.ts @@ -275,3 +275,33 @@ export function isBuiltinPath (importPath: string) { return false } + +export function isImportsPath (importPath: string) { + if (importPath.startsWith('#')) { + return true + } + + return false +} + +export interface ExternalPath { + name: string + path: string +} + +export function splitExternalPath (importPath: string): ExternalPath { + if (importPath.startsWith('@')) { + const [namespace, pkg, ...rest] = importPath.split('/') + return { + name: pkg ? `${namespace}/${pkg}` : namespace, + path: rest.join('/'), + } + } + + const [pkg, ...rest] = importPath.split('/') + + return { + name: pkg, + path: rest.join('/'), + } +} diff --git a/packages/cli/src/services/check-parser/package-files/resolver.ts b/packages/cli/src/services/check-parser/package-files/resolver.ts index 899e04373..cc3cd0ca8 100644 --- a/packages/cli/src/services/check-parser/package-files/resolver.ts +++ b/packages/cli/src/services/check-parser/package-files/resolver.ts @@ -4,12 +4,13 @@ import { SourceFile } from './source-file' import { PackageJsonFile } from './package-json-file' import { TSConfigFile } from './tsconfig-json-file' import { JSConfigFile } from './jsconfig-json-file' -import { isBuiltinPath, isLocalPath, PathResult } from './paths' +import { isBuiltinPath, isImportsPath, isLocalPath, PathResult, splitExternalPath } from './paths' import { FileLoader, LoadFile } from './loader' import { JsonSourceFile } from './json-source-file' import { JsonTextSourceFile } from './json-text-source-file' import { LookupContext } from './lookup' -import { walkUp, WalkUpOptions } from './walk' +import { lineage, LineageOptions } from './walk' +import { Package, Workspace } from './workspace' class PackageFilesCache { #sourceFileCache = new FileLoader(SourceFile.loadFromFilePath) @@ -51,9 +52,13 @@ class PackageFilesCache { #tsconfigJsonCache = new FileLoader(this.#jsonTextFileLoader(TSConfigFile.loadFromJsonTextSourceFile)) #jsconfigJsonCache = new FileLoader(this.#jsonFileLoader(JSConfigFile.loadFromJsonSourceFile)) + async exactSourceFile (filePath: string): Promise { + return await this.#sourceFileCache.load(filePath) + } + async sourceFile (filePath: string, context: LookupContext): Promise { for (const lookupPath of context.collectLookupPaths(filePath)) { - const sourceFile = await this.#sourceFileCache.load(lookupPath) + const sourceFile = await this.exactSourceFile(lookupPath) if (sourceFile === undefined) { continue } @@ -112,23 +117,70 @@ class PackageFiles { } } -type TSConfigFileLocalDependency = { - kind: 'tsconfig-file' +export type RawDependencySource = 'require' | 'import' + +export type RawDependency = { + importPath: string + source: RawDependencySource +} + +type WorkspaceRootPackageJsonFileLocalDependency = { + kind: 'workspace-root-package-json-file' + importPath: string + sourceFile: SourceFile + packageJsonFile: PackageJsonFile +} + +type WorkspaceRootTSConfigFileLocalDependency = { + kind: 'workspace-root-tsconfig-file' importPath: string sourceFile: SourceFile configFile: TSConfigFile } -type TSConfigResolvedPathLocalDependency = { - kind: 'tsconfig-resolved-path' +type WorkspaceRootLockfileLocalDependency = { + kind: 'workspace-root-lockfile' + importPath: string + sourceFile: SourceFile +} + +type WorkspaceRootConfigFileLocalDependency = { + kind: 'workspace-root-config-file' + importPath: string + sourceFile: SourceFile +} + +type NearestPackageJsonFileLocalDependency = { + kind: 'nearest-package-json-file' + importPath: string + sourceFile: SourceFile + packageJsonFile: PackageJsonFile +} + +type NearestTSConfigFileLocalDependency = { + kind: 'nearest-tsconfig-file' + importPath: string + sourceFile: SourceFile + configFile: TSConfigFile +} + +type SupportingTSConfigFileLocalDependency = { + kind: 'supporting-tsconfig-file' + importPath: string + sourceFile: SourceFile + configFile: TSConfigFile +} + +type SupportingTSConfigResolvedPathLocalDependency = { + kind: 'supporting-tsconfig-resolved-path' importPath: string sourceFile: SourceFile configFile: TSConfigFile pathResult: PathResult } -type TSConfigBaseUrlRelativePathLocalDependency = { - kind: 'tsconfig-baseurl-relative-path' +type SupportingTSConfigBaseUrlRelativePathLocalDependency = { + kind: 'supporting-tsconfig-baseurl-relative-path' importPath: string configFile: TSConfigFile sourceFile: SourceFile @@ -140,11 +192,25 @@ type RelativePathLocalDependency = { sourceFile: SourceFile } +type WorkspaceNeighborLocalDependency = { + kind: 'workspace-neighbor' + neighbor: Package + importPath: string + sourceFile: SourceFile +} + type LocalDependency = - TSConfigFileLocalDependency - | TSConfigResolvedPathLocalDependency - | TSConfigBaseUrlRelativePathLocalDependency + | WorkspaceRootPackageJsonFileLocalDependency + | WorkspaceRootTSConfigFileLocalDependency + | WorkspaceRootLockfileLocalDependency + | WorkspaceRootConfigFileLocalDependency + | NearestPackageJsonFileLocalDependency + | NearestTSConfigFileLocalDependency + | SupportingTSConfigFileLocalDependency + | SupportingTSConfigResolvedPathLocalDependency + | SupportingTSConfigBaseUrlRelativePathLocalDependency | RelativePathLocalDependency + | WorkspaceNeighborLocalDependency type MissingDependency = { importPath: string @@ -155,38 +221,53 @@ type ExternalDependency = { importPath: string } +type NeighborDependencies = { + depends: Package[] + references: Package[] +} + export type Dependencies = { external: ExternalDependency[] missing: MissingDependency[] local: LocalDependency[] + neighbors: NeighborDependencies } -export class PackageFilesResolver { - cache = new PackageFilesCache() +interface ResolveSourceFileOptions { + exportPath?: string + source?: RawDependencySource +} - async loadPackageJsonFile (filePath: string, options?: WalkUpOptions): Promise { - let packageJson: PackageJsonFile | undefined +export interface PackageFilesResolverOptions { + workspace?: Workspace +} - await walkUp(filePath, async dirPath => { - packageJson = await this.cache.packageJson(PackageJsonFile.filePath(dirPath)) - return packageJson !== undefined - }, options) +export class PackageFilesResolver { + cache = new PackageFilesCache() + workspace?: Workspace - return packageJson + constructor (options?: PackageFilesResolverOptions) { + this.workspace = options?.workspace } - async loadPackageFiles (filePath: string, options?: WalkUpOptions): Promise { + async loadPackageFiles (filePath: string, options?: LineageOptions): Promise { const files = new PackageFiles() - await walkUp(filePath, async dirPath => { - const found = await files.satisfyFromDirPath(dirPath, this.cache) - return found - }, options) + for (const searchPath of lineage(path.dirname(filePath), options)) { + const found = await files.satisfyFromDirPath(searchPath, this.cache) + if (found) { + break + } + } return files } - private async resolveSourceFile (sourceFile: SourceFile, context: LookupContext): Promise { + private async resolveSourceFile ( + sourceFile: SourceFile, + context: LookupContext, + options?: ResolveSourceFileOptions, + ): Promise { if (sourceFile.meta.basename === PackageJsonFile.FILENAME) { const packageJson = await this.cache.packageJson(sourceFile.meta.filePath) if (packageJson === undefined) { @@ -195,11 +276,35 @@ export class PackageFilesResolver { return [sourceFile] } + const { + exportPath = '', + source = 'import', + } = options ?? {} + + const searchPaths: string[] = [] + + if (packageJson.hasExports()) { + const { root, paths } = packageJson.resolveExportPath(exportPath, [ + source, + 'node', + 'module-sync', + 'default', + ]) + + for (const { target: targetPath } of paths) { + searchPaths.push(path.resolve(root, targetPath.path)) + } + } + + if (searchPaths.length === 0 && exportPath === '') { + searchPaths.push(...packageJson.mainPaths) + } + // Go through each main path. A fallback path is included. If we can // find a tsconfig for the main file, look it up and attempt to find // the original TypeScript sources roughly the same way tsc does it. - for (const mainPath of packageJson.mainPaths) { - const { tsconfigJson, jsconfigJson } = await this.loadPackageFiles(mainPath, { + for (const searchPath of searchPaths) { + const { tsconfigJson, jsconfigJson } = await this.loadPackageFiles(searchPath, { root: packageJson.basePath, }) @@ -209,7 +314,7 @@ export class PackageFilesResolver { continue } - const candidatePaths = configJson.collectLookupPaths(mainPath).flatMap(filePath => { + const candidatePaths = configJson.collectLookupPaths(searchPath).flatMap(filePath => { return context.collectLookupPaths(filePath) }) for (const candidatePath of candidatePaths) { @@ -224,7 +329,7 @@ export class PackageFilesResolver { } } - const mainSourceFile = await this.cache.sourceFile(mainPath, context) + const mainSourceFile = await this.cache.sourceFile(searchPath, context) if (mainSourceFile === undefined) { continue } @@ -241,22 +346,124 @@ export class PackageFilesResolver { async resolveDependenciesForFilePath ( filePath: string, - dependencies: string[], + dependencies: RawDependency[], ): Promise { const resolved: Dependencies = { external: [], missing: [], local: [], + neighbors: { + depends: [], + references: [], + }, } const dirname = path.dirname(filePath) - const { tsconfigJson, jsconfigJson } = await this.loadPackageFiles(filePath) + const { + packageJson, + tsconfigJson, + jsconfigJson, + } = await this.loadPackageFiles(filePath, { + root: this.workspace?.root.path, + }) + + if (this.workspace) { + const { + packageJson, + tsconfigJson, + jsconfigJson, + } = await this.loadPackageFiles(PackageJsonFile.filePath(this.workspace.root.path), { + root: this.workspace.root.path, + }) + + if (packageJson) { + resolved.local.push({ + kind: 'workspace-root-package-json-file', + importPath: filePath, + sourceFile: packageJson.jsonFile.sourceFile, + packageJsonFile: packageJson, + }) + } + + if (tsconfigJson) { + resolved.local.push({ + kind: 'workspace-root-tsconfig-file', + importPath: filePath, + sourceFile: tsconfigJson.jsonFile.sourceFile, + configFile: tsconfigJson, + }) + } + + if (jsconfigJson) { + resolved.local.push({ + kind: 'workspace-root-tsconfig-file', + importPath: filePath, + sourceFile: jsconfigJson.jsonFile.sourceFile, + configFile: jsconfigJson, + }) + } + + if (this.workspace.lockfile.isOk()) { + const lockfile = await this.cache.exactSourceFile( + this.workspace.lockfile.ok(), + ) + if (lockfile !== undefined) { + resolved.local.push({ + kind: 'workspace-root-lockfile', + importPath: filePath, + sourceFile: lockfile, + }) + } + } + + if (this.workspace.configFile.isOk()) { + const configFile = await this.cache.exactSourceFile( + this.workspace.configFile.ok(), + ) + if (configFile !== undefined) { + resolved.local.push({ + kind: 'workspace-root-config-file', + importPath: filePath, + sourceFile: configFile, + }) + } + } + } + + if (packageJson) { + resolved.local.push({ + kind: 'nearest-package-json-file', + importPath: filePath, + sourceFile: packageJson.jsonFile.sourceFile, + packageJsonFile: packageJson, + }) + } + + if (tsconfigJson) { + resolved.local.push({ + kind: 'nearest-tsconfig-file', + importPath: filePath, + sourceFile: tsconfigJson.jsonFile.sourceFile, + configFile: tsconfigJson, + }) + } + + if (jsconfigJson) { + resolved.local.push({ + kind: 'nearest-tsconfig-file', + importPath: filePath, + sourceFile: jsconfigJson.jsonFile.sourceFile, + configFile: jsconfigJson, + }) + } + + const usedNeighbors = new Set() const context = LookupContext.forFilePath(filePath) resolve: - for (const importPath of dependencies) { + for (const { importPath, source } of dependencies) { if (isBuiltinPath(importPath)) { resolved.external.push({ importPath, @@ -305,7 +512,7 @@ export class PackageFilesResolver { for (const resolvedFile of resolvedFiles) { configJson.registerRelatedSourceFile(resolvedFile) resolved.local.push({ - kind: 'tsconfig-resolved-path', + kind: 'supporting-tsconfig-resolved-path', importPath, sourceFile: resolvedFile, configFile: configJson, @@ -315,7 +522,7 @@ export class PackageFilesResolver { }, }) resolved.local.push({ - kind: 'tsconfig-file', + kind: 'supporting-tsconfig-file', importPath, sourceFile: configJson.jsonFile.sourceFile, configFile: configJson, @@ -343,13 +550,13 @@ export class PackageFilesResolver { for (const resolvedFile of resolvedFiles) { configJson.registerRelatedSourceFile(resolvedFile) resolved.local.push({ - kind: 'tsconfig-baseurl-relative-path', + kind: 'supporting-tsconfig-baseurl-relative-path', importPath, sourceFile: resolvedFile, configFile: configJson, }) resolved.local.push({ - kind: 'tsconfig-file', + kind: 'supporting-tsconfig-file', importPath, sourceFile: configJson.jsonFile.sourceFile, configFile: configJson, @@ -363,11 +570,65 @@ export class PackageFilesResolver { } } + if (isImportsPath(importPath)) { + // TODO + continue resolve + } + + if (this.workspace) { + const { name, path: exportPath } = splitExternalPath(importPath) + const neighbor = this.workspace.memberByName(name) + if (neighbor) { + const sourceFile = await this.cache.sourceFile(neighbor.path, context) + if (sourceFile !== undefined) { + const resolvedFiles = await this.resolveSourceFile(sourceFile, context, { + exportPath, + source, + }) + let found = false + for (const resolvedFile of resolvedFiles) { + resolved.local.push({ + kind: 'workspace-neighbor', + neighbor, + importPath, + sourceFile: resolvedFile, + }) + found = true + } + if (found) { + usedNeighbors.add(neighbor) + continue resolve + } + } + } + } + resolved.external.push({ importPath, }) } + const requiredNeighbors = new Set() + + if (packageJson && this.workspace) { + const combinedDependencies = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + } + + for (const dep of Object.keys(combinedDependencies)) { + const neighbor = this.workspace.memberByName(dep) + if (neighbor) { + requiredNeighbors.add(neighbor) + } + } + } + + resolved.neighbors = { + depends: Array.from(requiredNeighbors), + references: Array.from(usedNeighbors), + } + return resolved } } diff --git a/packages/cli/src/services/check-parser/package-files/result.ts b/packages/cli/src/services/check-parser/package-files/result.ts new file mode 100644 index 000000000..94fb2886e --- /dev/null +++ b/packages/cli/src/services/check-parser/package-files/result.ts @@ -0,0 +1,116 @@ +export interface Result { + /** + * Returns `true` if the {@link Result} is {@link Ok}, or `false` otherwise. + */ + isOk (): this is Ok + + /** + * Returns `true` if the {@link Result} is {@link Err}, or `false` otherwise. + */ + isErr (): this is Err + + /** + * Returns `T` if the {@link Result} is {@link Ok}, or throws `E` otherwise. + */ + unwrap (): T + + /** + * Returns `T` if the {@link Result} is {@link Ok}, or `undefined` otherwise. + */ + ok (): T | undefined + + /** + * Returns `E` if the {@link Result} is {@link Err}, or `undefined` otherwise. + */ + err (): E | undefined +} + +export interface Ok extends Result { + isOk (): this is Ok + isErr (): this is Err + unwrap (): T + ok (): T + err (): undefined +} + +export interface Err extends Result { + isOk (): this is Ok + isErr (): this is Err + unwrap (): never + ok (): undefined + err (): E +} + +export function Ok (value: T): Result { + return new _Ok(value) +} + +export function Err (error: E): Result { + return new _Err(error) +} + +abstract class _Result implements Result { + abstract isOk (): this is Ok + abstract isErr (): this is Err + abstract unwrap (): T + abstract ok (): T | undefined + abstract err (): E | undefined +} + +class _Ok extends _Result implements Ok { + #value: T + + constructor (value: T) { + super() + this.#value = value + } + + isOk (): this is Ok { + return true + } + + isErr (): this is Err { + return false + } + + unwrap (): T { + return this.#value + } + + ok (): T { + return this.#value + } + + err (): undefined { + return + } +} + +class _Err extends _Result implements Err { + #error: E + + constructor (error: E) { + super() + this.#error = error + } + + isOk (): this is Ok { + return false + } + + isErr (): this is Err { + return true + } + + unwrap (): never { + throw this.#error + } + + ok (): undefined { + return + } + + err (): E { + return this.#error + } +} diff --git a/packages/cli/src/services/check-parser/package-files/workspace.ts b/packages/cli/src/services/check-parser/package-files/workspace.ts new file mode 100644 index 000000000..45c6d3f71 --- /dev/null +++ b/packages/cli/src/services/check-parser/package-files/workspace.ts @@ -0,0 +1,156 @@ +import path from 'node:path' + +import { glob } from 'glob' + +import { PackageJsonFile } from './package-json-file' +import { Result } from './result' + +export interface PackageOptions { + /** + * The name of the package. + */ + name: string + + /** + * An absolute path to the package directory. + */ + path: string + + /** + * Whether the package is a workspace. + */ + workspaces?: string[] +} + +export class Package { + name: string + path: string + workspaces?: string[] + + constructor ({ name, path, workspaces }: PackageOptions) { + this.name = name + this.path = path + this.workspaces = workspaces + } + + get packageJsonPath (): string { + return PackageJsonFile.filePath(this.path) + } + + // eslint-disable-next-line require-await + static async loadFromPackageJsonFile (packageJson: PackageJsonFile): Promise { + const { name, workspaces } = packageJson + if (name === undefined) { + return + } + + return new Package({ + name, + path: packageJson.meta.dirname, + workspaces, + }) + } + + static async loadFromDirPath (dirPath: string): Promise { + const packageJson = await PackageJsonFile.loadFromFilePath(PackageJsonFile.filePath(dirPath)) + if (!packageJson) { + return + } + + return await Package.loadFromPackageJsonFile(packageJson) + } +} + +export type OptionalWorkspaceFile = Result + +export interface WorkspaceOptions { + root: Package + packages: Package[] + lockfile: OptionalWorkspaceFile + configFile: OptionalWorkspaceFile +} + +export class Workspace { + /** + * The workspace root package. + */ + root: Package + + /** + * Packages that are a part of the workspace, excluding the root package. + */ + packages: Package[] + + /** + * The package manager specific lockfile of the workspace. + */ + lockfile: OptionalWorkspaceFile + + /** + * The package manager specific config file of the workspace. + */ + configFile: OptionalWorkspaceFile + + #membersByName = new Map() + #membersByPath = new Map() + + constructor (options: WorkspaceOptions) { + this.root = options.root + this.packages = options.packages + this.#membersByName = [options.root, ...options.packages].reduce( + (map, pkg) => map.set(pkg.name, pkg), + new Map(), + ) + this.#membersByPath = [options.root, ...options.packages].reduce( + (map, pkg) => map.set(pkg.path, pkg), + new Map(), + ) + this.lockfile = options.lockfile + this.configFile = options.configFile + } + + memberByName (name: string): Package | undefined { + return this.#membersByName.get(name) + } + + memberByPath (path: string): Package | undefined { + return this.#membersByPath.get(path) + } + + /** + * @param root An absolute path to the workspace root. + * @param patterns Relative workspace patterns. + * @returns Absolute paths to every package in the workspace. + */ + static async resolvePatterns ( + root: string, + patterns: string[], + ): Promise { + const lookup = patterns.map(pattern => + path.join(pattern, PackageJsonFile.FILENAME), + ) + + const results = await glob(lookup, { + cwd: root, + absolute: true, + }) + + const packages: Package[] = [] + + for (const result of results) { + const packageJson = await PackageJsonFile.loadFromFilePath(result) + if (packageJson === undefined) { + continue + } + + const workspacePackage = await Package.loadFromPackageJsonFile(packageJson) + if (workspacePackage === undefined) { + continue + } + + packages.push(workspacePackage) + } + + return packages + } +} diff --git a/packages/cli/src/services/check-parser/parser.ts b/packages/cli/src/services/check-parser/parser.ts index 28425ce7e..6e0b382ec 100644 --- a/packages/cli/src/services/check-parser/parser.ts +++ b/packages/cli/src/services/check-parser/parser.ts @@ -7,12 +7,18 @@ import * as walk from 'acorn-walk' import { minimatch } from 'minimatch' // Only import types given this is an optional dependency import type { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree' +import Debug from 'debug' import { Collector } from './collector' import { DependencyParseError } from './errors' -import { PackageFilesResolver, Dependencies } from './package-files/resolver' +import { PackageFilesResolver, Dependencies, RawDependency, RawDependencySource } from './package-files/resolver' import type { PlaywrightConfig } from '../playwright-config' import { findFilesWithPattern, pathToPosix } from '../util' +import { Package, Workspace } from './package-files/workspace' +import { isCoreExtension, isTSExtension } from './package-files/extension' +import { createFauxPackageFiles } from './faux-package' + +const debug = Debug('checkly:cli:services:check-parser:parser') // Our custom configuration to handle walking errors @@ -20,13 +26,23 @@ import { findFilesWithPattern, pathToPosix } from '../util' const ignore = (_node: any, _st: any, _c: any) => {} type Module = { - dependencies: Array + dependencies: Array } -type SupportedFileExtension = '.js' | '.mjs' | '.ts' +type LegacySupportedFileExtension = '.js' | '.mjs' | '.ts' + +function isLegacySupportedFileExtension (value: string): value is LegacySupportedFileExtension { + switch (value) { + case '.js': + case '.mjs': + case '.ts': + return true + default: + return false + } +} const PACKAGE_EXTENSION = `${path.sep}package.json` -const STATIC_FILE_EXTENSION = ['.json', '.txt', '.jpeg', '.jpg', '.png', '.yml'] const supportedBuiltinModules = [ 'node:assert', @@ -46,24 +62,6 @@ const supportedBuiltinModules = [ 'node:zlib', ] -async function validateEntrypoint ( - entrypoint: string, -): Promise<{ - extension: SupportedFileExtension - content: string -}> { - const extension = path.extname(entrypoint) - if (extension !== '.js' && extension !== '.ts' && extension !== '.mjs') { - throw new Error(`Unsupported file extension for ${entrypoint}`) - } - try { - const content = await fs.readFile(entrypoint, { encoding: 'utf-8' }) - return { extension, content } - } catch { - throw new DependencyParseError(entrypoint, [entrypoint], [], []) - } -} - let tsParser: any function getTsParser (): any { if (tsParser) { @@ -77,12 +75,11 @@ function getTsParser (): any { // Our custom configuration to handle walking errors Object.values(AST_NODE_TYPES).forEach(astType => { - // Only handle the TS specific ones - if (!astType.startsWith('TS')) { - return + // Only handle the TS/JSX specific ones + if (astType.startsWith('TS') || astType.startsWith('JSX')) { + const base: any = walk.base + base[astType] = base[astType] ?? ignore } - const base: any = walk.base - base[astType] = base[astType] ?? ignore }) return tsParser } catch (err: any) { @@ -93,15 +90,65 @@ function getTsParser (): any { } } +class RawDependencyCollector { + #dependencies: RawDependency[] = [] + + add (dependency: RawDependency) { + this.#dependencies.push(dependency) + } + + state (): RawDependency[] { + return this.#dependencies + } +} + +class NeighborDependencyCollector { + #depended = new Set() + #referenced = new Set() + + markDepended (...pkg: Package[]): void { + for (const p of pkg) { + this.#depended.add(p) + } + } + + markReferenced (...pkg: Package[]): void { + for (const p of pkg) { + this.markDepended(p) + this.#referenced.add(p) + } + } + + unreferencedDependedPackages (): Package[] { + return Array.from(this.#depended).filter(pkg => !this.#referenced.has(pkg)) + } +} + +type FileEntry = { + filePath: string + content: string +} + type ParserOptions = { supportedNpmModules?: Array checkUnsupportedModules?: boolean + workspace?: Workspace + restricted?: boolean } +export type VirtualFile = { filePath: string, physical: false, content: string } +export type PhysicalFile = { filePath: string, physical: true } + +export type File = + | VirtualFile + | PhysicalFile + export class Parser { supportedModules: Set checkUnsupportedModules: boolean - resolver = new PackageFilesResolver() + workspace?: Workspace + resolver: PackageFilesResolver + restricted: boolean cache = new Map { + private async validateFile ( + filePath: string, + ): Promise { + debug(`Validating file ${filePath}`) const extension = path.extname(filePath) - if (extension !== '.js' && extension !== '.ts' && extension !== '.mjs') { + if (!this.isProcessableExtension(extension)) { throw new Error(`Unsupported file extension for ${filePath}`) } try { const content = await fs.readFile(filePath, { encoding: 'utf-8' }) return { filePath, content } - } catch { + } catch (err) { + debug(`Failed to validate file ${filePath}: ${err}`) throw new DependencyParseError(filePath, [filePath], [], []) } } - async getFilesAndDependencies (playwrightConfig: PlaywrightConfig): - Promise<{ files: string[], errors: string[] }> { + private isProcessableExtension (extension: string): boolean { + if (this.restricted) { + return isLegacySupportedFileExtension(extension) + } + + if (isCoreExtension(extension) && extension !== '.json') { + return true + } + + if (isTSExtension(extension)) { + return true + } + + return false + } + + async getFilesAndDependencies ( + playwrightConfig: PlaywrightConfig, + ): Promise<{ + files: File[] + errors: string[] + }> { const files = new Set(await this.getFilesFromPaths(playwrightConfig)) files.add(playwrightConfig.configFilePath) const errors = new Set() const missingFiles = new Set() const resultFileSet = new Set() + const neighbors = new NeighborDependencyCollector() for (const file of files) { if (resultFileSet.has(file)) { continue } - if (STATIC_FILE_EXTENSION.includes(path.extname(file))) { - // Holds info about the main file and doesn't need to be parsed + const extension = path.extname(file) + if (!this.isProcessableExtension(extension)) { resultFileSet.add(file) continue } @@ -172,9 +249,12 @@ export class Parser { ?? await this.resolver.resolveDependenciesForFilePath(item.filePath, module.dependencies) for (const dep of resolvedDependencies.missing) { - missingFiles.add(pathToPosix(dep.filePath)) + missingFiles.add(dep.filePath) } + neighbors.markDepended(...resolvedDependencies.neighbors.depends) + neighbors.markReferenced(...resolvedDependencies.neighbors.references) + this.cache.set(item.filePath, { module, resolvedDependencies }) for (const dep of resolvedDependencies.local) { @@ -184,12 +264,35 @@ export class Parser { const filePath = dep.sourceFile.meta.filePath files.add(filePath) } - resultFileSet.add(pathToPosix(item.filePath)) + resultFileSet.add(item.filePath) } + if (missingFiles.size) { throw new DependencyParseError([].join(', '), Array.from(missingFiles), [], []) } - return { files: Array.from(resultFileSet), errors: Array.from(errors) } + + const outputFiles: File[] = [] + + for (const filePath of resultFileSet) { + outputFiles.push({ + filePath: pathToPosix(filePath), + physical: true, + }) + } + + const neighborFiles = neighbors.unreferencedDependedPackages() + .flatMap(pkg => createFauxPackageFiles(pkg)) + + outputFiles.push(...neighborFiles) + + outputFiles.sort((a, b) => { + return a.filePath.localeCompare(b.filePath) + }) + + return { + files: outputFiles, + errors: Array.from(errors), + } } private async collectFiles (cache: Map, testDir: string, ignoredFiles: string[]) { @@ -267,7 +370,7 @@ export class Parser { } async parse (entrypoint: string) { - const { content } = await validateEntrypoint(entrypoint) + const { content } = await this.validateFile(entrypoint) /* * The importing of files forms a directed graph. @@ -276,7 +379,8 @@ export class Parser { * In this implementation, we use breadth first search. */ const collector = new Collector(entrypoint, content) - const bfsQueue: [{ filePath: string, content: string }] = [{ filePath: entrypoint, content }] + const neighbors = new NeighborDependencyCollector() + const bfsQueue: [FileEntry] = [{ filePath: entrypoint, content }] while (bfsQueue.length > 0) { // Since we just checked the length, shift() will never return undefined. // We can add a not-null assertion operator (!). @@ -332,6 +436,15 @@ export class Parser { collector.addDependency(filePath, dep.sourceFile.contents) bfsQueue.push({ filePath, content: dep.sourceFile.contents }) } + + neighbors.markDepended(...resolvedDependencies.neighbors.depends) + neighbors.markReferenced(...resolvedDependencies.neighbors.references) + } + + for (const pkg of neighbors.unreferencedDependedPackages()) { + for (const f of createFauxPackageFiles(pkg)) { + collector.addDependency(f.filePath, f.content) + } } collector.validate() @@ -341,11 +454,12 @@ export class Parser { static parseDependencies (filePath: string, contents: string): { module: Module, error?: any } { - const dependencies = new Set() + debug(`Parsing dependencies of ${filePath}`) + const dependencies = new RawDependencyCollector() const extension = path.extname(filePath) try { - if (extension === '.js' || extension === '.mjs') { + if (isCoreExtension(extension) && extension !== '.json') { const ast = acorn.parse(contents, { allowReturnOutsideFunction: true, ecmaVersion: 'latest', @@ -353,22 +467,24 @@ export class Parser { allowAwaitOutsideFunction: true, }) walk.simple(ast, Parser.jsNodeVisitor(dependencies)) - } else if (extension === '.ts') { + } else if (isTSExtension(extension)) { const tsParser = getTsParser() - const ast = tsParser.parse(contents, {}) + const ast = tsParser.parse(contents, { + // We must only enable jsx for tsx/jsx files. Otherwise type brackets + // may confuse the parser and cause an error. + jsx: extension.endsWith('x'), + }) // The AST from typescript-estree is slightly different from the type used by acorn-walk. // This doesn't actually cause problems (both are "ESTree's"), but we need to ignore type errors here. // @ts-ignore walk.simple(ast, Parser.tsNodeVisitor(tsParser, dependencies)) - } else if (extension === '.json') { - // No dependencies to check. - } else { - throw new Error(`Unsupported file extension for ${filePath}`) } } catch (err) { + debug(`Failed to parse dependencies of ${filePath}: ${err}`) + return { module: { - dependencies: Array.from(dependencies), + dependencies: dependencies.state(), }, error: err, } @@ -376,60 +492,60 @@ export class Parser { return { module: { - dependencies: Array.from(dependencies), + dependencies: dependencies.state(), }, } } - static jsNodeVisitor (dependencies: Set): any { + static jsNodeVisitor (dependencies: RawDependencyCollector): any { return { CallExpression (node: Node) { if (!Parser.isRequireExpression(node)) return const requireStringArg = Parser.getRequireStringArg(node) - Parser.registerDependency(requireStringArg, dependencies) + Parser.registerDependency(requireStringArg, 'require', dependencies) }, ImportDeclaration (node: any) { if (node.source.type !== 'Literal') return - Parser.registerDependency(node.source.value, dependencies) + Parser.registerDependency(node.source.value, 'import', dependencies) }, ExportNamedDeclaration (node: any) { if (node.source === null) return if (node.source.type !== 'Literal') return - Parser.registerDependency(node.source.value, dependencies) + Parser.registerDependency(node.source.value, 'import', dependencies) }, ExportAllDeclaration (node: any) { if (node.source === null) return if (node.source.type !== 'Literal') return - Parser.registerDependency(node.source.value, dependencies) + Parser.registerDependency(node.source.value, 'import', dependencies) }, } } - static tsNodeVisitor (tsParser: any, dependencies: Set): any { + static tsNodeVisitor (tsParser: any, dependencies: RawDependencyCollector): any { return { // While rare, TypeScript files may also use require. CallExpression (node: Node) { if (!Parser.isRequireExpression(node)) return const requireStringArg = Parser.getRequireStringArg(node) - Parser.registerDependency(requireStringArg, dependencies) + Parser.registerDependency(requireStringArg, 'require', dependencies) }, ImportDeclaration (node: TSESTree.ImportDeclaration) { // For now, we only support literal strings in the import statement if (node.source.type !== tsParser.TSESTree.AST_NODE_TYPES.Literal) return - Parser.registerDependency(node.source.value, dependencies) + Parser.registerDependency(node.source.value, 'import', dependencies) }, ExportNamedDeclaration (node: TSESTree.ExportNamedDeclaration) { // The statement isn't importing another dependency if (node.source === null) return // For now, we only support literal strings in the export statement if (node.source.type !== tsParser.TSESTree.AST_NODE_TYPES.Literal) return - Parser.registerDependency(node.source.value, dependencies) + Parser.registerDependency(node.source.value, 'import', dependencies) }, ExportAllDeclaration (node: TSESTree.ExportAllDeclaration) { if (node.source === null) return // For now, we only support literal strings in the export statement if (node.source.type !== tsParser.TSESTree.AST_NODE_TYPES.Literal) return - Parser.registerDependency(node.source.value, dependencies) + Parser.registerDependency(node.source.value, 'import', dependencies) }, } } @@ -471,12 +587,19 @@ export class Parser { } } - static registerDependency (importArg: string | null, dependencies: Set) { - // TODO: We currently don't support import path aliases, f.ex: `import { Something } from '@services/my-service'` - if (!importArg) { - // If there's no importArg, don't register a dependency - } else { - dependencies.add(importArg) + static registerDependency ( + importPath: string | null, + source: RawDependencySource, + dependencies: RawDependencyCollector, + ) { + if (!importPath) { + // If there's no importPath, don't register a dependency. + return } + + dependencies.add({ + importPath, + source, + }) } } diff --git a/packages/cli/src/services/checkly-config-loader.ts b/packages/cli/src/services/checkly-config-loader.ts index a85ceb8a3..7b0100b23 100644 --- a/packages/cli/src/services/checkly-config-loader.ts +++ b/packages/cli/src/services/checkly-config-loader.ts @@ -143,9 +143,40 @@ export async function getChecklyConfigFile (): Promise<{ checklyConfig: string, return config } -export class ConfigNotFoundError extends Error {} +export class ConfigNotFoundError extends Error { + searchPaths: string[] + configFiles: string[] -export async function loadChecklyConfig (dir: string, filenames = ['checkly.config.ts', 'checkly.config.mts', 'checkly.config.cts', 'checkly.config.js', 'checkly.config.mjs', 'checkly.config.cjs'], writeChecklyConfig: boolean = true, playwrightConfigPath?: string): Promise<{ config: ChecklyConfig, constructs: Construct[] }> { + constructor (searchPaths: string[], configFiles: string[], options?: ErrorOptions) { + const message = `Unable to detect a Checkly configuration file in any of the following paths:` + + `\n\n` + + `${searchPaths.map(searchPath => ` ${searchPath}`).join('\n')}` + + `\n\n` + + `Configuration files we looked for:` + + `\n\n` + + `${configFiles.map(lockfile => ` ${lockfile}`).join('\n')}` + super(message, options) + this.name = 'ConfigNotFoundError' + this.searchPaths = searchPaths + this.configFiles = configFiles + } +} + +export const defaultFilenames = [ + 'checkly.config.ts', + 'checkly.config.mts', + 'checkly.config.cts', + 'checkly.config.js', + 'checkly.config.mjs', + 'checkly.config.cjs', +] + +export async function loadChecklyConfig ( + dir: string, + filenames = defaultFilenames, + writeChecklyConfig: boolean = true, + playwrightConfigPath?: string, +): Promise<{ config: ChecklyConfig, constructs: Construct[] }> { Session.loadingChecklyConfigFile = true try { let config: ChecklyConfig | undefined @@ -192,7 +223,7 @@ async function handleMissingConfig ( } return checklyConfig } - throw new ConfigNotFoundError(`Unable to locate a config at ${dir} with ${filenames.join(', ')}.`) + throw new ConfigNotFoundError([dir], filenames) } function validateConfigFields (config: ChecklyConfig, fields: (keyof ChecklyConfig)[]): void { diff --git a/packages/cli/src/services/playwright-config.ts b/packages/cli/src/services/playwright-config.ts index de5702e4a..82505ca54 100644 --- a/packages/cli/src/services/playwright-config.ts +++ b/packages/cli/src/services/playwright-config.ts @@ -65,9 +65,9 @@ export class PlaywrightConfig { continue } if (Array.isArray(definition)) { - definition.forEach((file: string) => this.files.add(file)) + definition.forEach((file: string) => this.files.add(toAbsolutePath(dir, file))) } else { - this.files.add(definition) + this.files.add(toAbsolutePath(dir, definition)) } } diff --git a/packages/cli/src/services/project-parser.ts b/packages/cli/src/services/project-parser.ts index ffe3973cd..e20180cfb 100644 --- a/packages/cli/src/services/project-parser.ts +++ b/packages/cli/src/services/project-parser.ts @@ -1,4 +1,5 @@ import * as path from 'path' +import Debug from 'debug' import { findFilesWithPattern, getPlaywrightConfigPath, pathToPosix, @@ -10,9 +11,20 @@ import { } from '../constructs' import { Ref } from '../constructs/ref' import { CheckConfigDefaults, PlaywrightSlimmedProp } from './checkly-config-loader' -import type { Runtime } from '../rest/runtimes' import { isEntrypoint, type Construct } from '../constructs/construct' import { PlaywrightCheck } from '../constructs/playwright-check' +import { + detectNearestPackageJson, + detectPackageManager, + fauxWorkspaceFromPackageJson, + NoPackageJsonFoundError, + PackageManager, +} from './check-parser/package-files/package-manager' +import { Err, Ok, Result } from './check-parser/package-files/result' +import { Workspace } from './check-parser/package-files/workspace' +import { Runtime } from '../runtimes' + +const debug = Debug('checkly:cli:services:project-parser') type ProjectParseOpts = { directory: string @@ -36,11 +48,82 @@ type ProjectParseOpts = { includeFlagProvided?: boolean playwrightChecks?: PlaywrightSlimmedProp[] currentCommand?: 'pw-test' | 'test' | 'deploy' + enableWorkspaces?: boolean } const BASE_CHECK_DEFAULTS = { } +async function findBasePath ( + packageManager: PackageManager, + directory: string, + { ignoreWorkspaces = false }, +): Promise<{ + basePath: string + contextPath: string + workspace: Result +}> { + try { + if (ignoreWorkspaces) { + const nearestPackageJson = await detectNearestPackageJson(directory) + + const workspace = await fauxWorkspaceFromPackageJson( + packageManager, + nearestPackageJson, + ) + + return { + basePath: workspace.root.path, + contextPath: workspace.root.path, + workspace: Ok(workspace), + } + } + + const workspace = await packageManager.lookupWorkspace(directory) + + // If we can't locate a real workspace, set up a faux workspace instead. + // Makes usage easier since we can rely on a workspace being available. + if (!workspace) { + return await findBasePath(packageManager, directory, { + ignoreWorkspaces: true, + }) + } + + // We've found a workspace. Let's try to figure out whether we're a part + // of it. + const nearestPackageJson = await detectNearestPackageJson(directory, { + root: workspace.root.path, + }) + + const contextPath = nearestPackageJson.basePath + + // If the nearest workspace includes the nearest package, then use the + // workspace root as the project root. + if (workspace.memberByPath(contextPath) !== undefined) { + return { + basePath: workspace.root.path, + contextPath, + workspace: Ok(workspace), + } + } + + // Otherwise, use the config dir as the project root. + return await findBasePath(packageManager, directory, { + ignoreWorkspaces: true, + }) + } catch (err) { + if (err instanceof NoPackageJsonFoundError) { + return { + basePath: directory, + contextPath: directory, + workspace: Err(err), + } + } + + throw err + } +} + export async function parseProject (opts: ProjectParseOpts): Promise { const { directory, @@ -64,6 +147,7 @@ export async function parseProject (opts: ProjectParseOpts): Promise { includeFlagProvided, playwrightChecks, currentCommand, + enableWorkspaces = true, } = opts const project = new Project(projectLogicalId, { name: projectName, @@ -74,11 +158,19 @@ export async function parseProject (opts: ProjectParseOpts): Promise { project.allowTestOnly(true) } + const packageManager = await detectPackageManager(directory) + debug(`Detected package manager ${packageManager.name}`) + + const { basePath, contextPath, workspace } = await findBasePath(packageManager, directory, { + ignoreWorkspaces: !enableWorkspaces, + }) + checklyConfigConstructs?.forEach( construct => project.addResource(construct.type, construct.logicalId, construct), ) Session.project = project - Session.basePath = directory + Session.basePath = basePath + Session.contextPath = contextPath Session.checkDefaults = Object.assign({}, BASE_CHECK_DEFAULTS, checkDefaults) Session.checkFilter = checkFilter Session.browserCheckDefaults = browserCheckDefaults @@ -88,6 +180,8 @@ export async function parseProject (opts: ProjectParseOpts): Promise { Session.ignoreDirectoriesMatch = ignoreDirectoriesMatch Session.currentCommand = currentCommand Session.includeFlagProvided = includeFlagProvided + Session.packageManager = packageManager + Session.workspace = workspace // TODO: Do we really need all of the ** globs, or could we just put node_modules? const ignoreDirectories = ['**/node_modules/**', '**/.git/**', ...ignoreDirectoriesMatch] diff --git a/packages/cli/src/services/proxy.ts b/packages/cli/src/services/proxy.ts new file mode 100644 index 000000000..5e75b1e58 --- /dev/null +++ b/packages/cli/src/services/proxy.ts @@ -0,0 +1,37 @@ +import type { CreateAxiosDefaults } from 'axios' + +// @ts-ignore +import { getProxyForUrl } from 'proxy-from-env' +import { httpOverHttp, httpsOverHttp, httpOverHttps, httpsOverHttps } from 'tunnel' + +const isHttps = (protocol: string) => protocol.startsWith('https') + +export function assignProxy (baseURL: string, axiosConfig: CreateAxiosDefaults) { + const proxyUrlEnv = getProxyForUrl(baseURL) + if (!proxyUrlEnv) { + return axiosConfig + } + + const parsedProxyUrl = new URL(proxyUrlEnv) + const isProxyHttps = isHttps(parsedProxyUrl.protocol) + const isEndpointHttps = isHttps(baseURL) + const proxy: any = { + host: parsedProxyUrl.hostname, + port: parsedProxyUrl.port, + protocol: parsedProxyUrl.protocol, + } + if (parsedProxyUrl.username && parsedProxyUrl.password) { + proxy.proxyAuth = `${parsedProxyUrl.username}:${parsedProxyUrl.password}` + } + if (isProxyHttps && isEndpointHttps) { + axiosConfig.httpsAgent = httpsOverHttps({ proxy }) + } else if (isProxyHttps && !isEndpointHttps) { + axiosConfig.httpAgent = httpOverHttps({ proxy }) + } else if (!isProxyHttps && isEndpointHttps) { + axiosConfig.httpsAgent = httpsOverHttp({ proxy }) + } else { + axiosConfig.httpAgent = httpOverHttp({ proxy }) + } + axiosConfig.proxy = false + return axiosConfig +} diff --git a/packages/cli/src/services/test-runner.ts b/packages/cli/src/services/test-runner.ts index b00e1ca42..4a03e3cdd 100644 --- a/packages/cli/src/services/test-runner.ts +++ b/packages/cli/src/services/test-runner.ts @@ -7,7 +7,7 @@ import { Check } from '../constructs/check' import { RetryStrategy, SharedFile } from '../constructs' import { ProjectBundle, ResourceDataBundle } from '../constructs/project-bundle' import { pullSnapshots } from '../services/snapshot-service' -import { PlaywrightCheckBundle } from '../constructs/playwright-check-bundle' +import { PlaywrightCheckLocalBundle } from '../constructs/playwright-check-bundle' export default class TestRunner extends AbstractCheckRunner { projectBundle: ProjectBundle @@ -61,7 +61,7 @@ export default class TestRunner extends AbstractCheckRunner { const checkRunJobs = this.checkBundles.map(({ construct: check, bundle }) => { // Get the group reference - Playwright checks store it in the bundle, // other checks store it in the check construct (as groupId) - const groupId = bundle instanceof PlaywrightCheckBundle + const groupId = bundle instanceof PlaywrightCheckLocalBundle ? bundle.groupId : check.groupId diff --git a/packages/cli/src/services/util.ts b/packages/cli/src/services/util.ts index b1e9f3347..b902ef4b0 100644 --- a/packages/cli/src/services/util.ts +++ b/packages/cli/src/services/util.ts @@ -1,13 +1,9 @@ -import type { CreateAxiosDefaults } from 'axios' import * as path from 'path' import * as fs from 'fs/promises' import * as fsSync from 'fs' import gitRepoInfo from 'git-repo-info' import { parse } from 'dotenv' -// @ts-ignore -import { getProxyForUrl } from 'proxy-from-env' -import { httpOverHttp, httpsOverHttp, httpOverHttps, httpsOverHttps } from 'tunnel' import type { Archiver } from 'archiver' import { glob } from 'glob' import os from 'node:os' @@ -17,14 +13,8 @@ import * as JSON5 from 'json5' import { PlaywrightConfig } from './playwright-config' import { readFile } from 'fs/promises' import { createHash } from 'crypto' -import { Session } from '../constructs' +import { Session } from '../constructs/project' import semver from 'semver' -import { - detectNearestLockfile, - NpmDetector, - PNpmDetector, - YarnDetector, -} from './check-parser/package-files/package-manager' import { existsSync } from 'fs' export interface GitInformation { @@ -149,38 +139,6 @@ export async function getEnvs (envFile: string | undefined, envArgs: Array protocol.startsWith('https') - -export function assignProxy (baseURL: string, axiosConfig: CreateAxiosDefaults) { - const proxyUrlEnv = getProxyForUrl(baseURL) - if (!proxyUrlEnv) { - return axiosConfig - } - - const parsedProxyUrl = new URL(proxyUrlEnv) - const isProxyHttps = isHttps(parsedProxyUrl.protocol) - const isEndpointHttps = isHttps(baseURL) - const proxy: any = { - host: parsedProxyUrl.hostname, - port: parsedProxyUrl.port, - protocol: parsedProxyUrl.protocol, - } - if (parsedProxyUrl.username && parsedProxyUrl.password) { - proxy.proxyAuth = `${parsedProxyUrl.username}:${parsedProxyUrl.password}` - } - if (isProxyHttps && isEndpointHttps) { - axiosConfig.httpsAgent = httpsOverHttps({ proxy }) - } else if (isProxyHttps && !isEndpointHttps) { - axiosConfig.httpAgent = httpOverHttps({ proxy }) - } else if (!isProxyHttps && isEndpointHttps) { - axiosConfig.httpsAgent = httpsOverHttp({ proxy }) - } else { - axiosConfig.httpAgent = httpOverHttp({ proxy }) - } - axiosConfig.proxy = false - return axiosConfig -} - export function normalizeVersion (v?: string | undefined): string | undefined { const cleaned = semver.valid(semver.clean(v ?? '') || '') @@ -197,11 +155,11 @@ export async function bundlePlayWrightProject ( relativePlaywrightConfigPath: string cacheHash: string playwrightVersion: string + workingDir?: string }> { const dir = path.resolve(path.dirname(playwrightConfig)) const filePath = path.resolve(dir, playwrightConfig) - const supportedDetectors = [new NpmDetector(), new YarnDetector(), new PNpmDetector()] - const { lockfile } = await detectNearestLockfile(dir, { detectors: supportedDetectors, root: Session.basePath }) + const lockfile = Session.workspace.unwrap().lockfile.unwrap() // No need of loading everything if there is no lockfile const pwtConfig = await Session.loadFile(filePath) @@ -230,7 +188,7 @@ export async function bundlePlayWrightProject ( const [cacheHash] = await Promise.all([ getCacheHash(lockfile), - loadPlaywrightProjectFiles(dir, pwConfigParsed, include, archive, lockfile), + loadPlaywrightProjectFiles(dir, pwConfigParsed, include, archive), ]) await archive.finalize() @@ -240,8 +198,9 @@ export async function bundlePlayWrightProject ( outputFile, browsers: pwConfigParsed.getBrowsers(), playwrightVersion, - relativePlaywrightConfigPath: Session.relativePosixPath(filePath), + relativePlaywrightConfigPath: Session.contextRelativePosixPath(filePath), cacheHash, + workingDir: Session.relativePosixPath(Session.contextPath!), }) }) @@ -279,51 +238,35 @@ export function getPlaywrightVersionFromPackage (cwd: string): string { } } -// Temporarily always include these extra files (if present) until they can -// be properly supported. -const extraFiles = [ - 'pnpm-workspace.yaml', -] - export async function loadPlaywrightProjectFiles ( dir: string, pwConfigParsed: PlaywrightConfig, include: string[], archive: Archiver, - lockFile: string, ) { const ignoredFiles = ['**/node_modules/**', '.git/**', ...Session.ignoreDirectoriesMatch] - const parser = new Parser({}) + const parser = new Parser({ + workspace: Session.workspace.ok(), + restricted: false, + }) const { files, errors } = await parser.getFilesAndDependencies(pwConfigParsed) if (errors.length) { throw new Error(`Error loading playwright project files: ${errors.map((e: string) => e).join(', ')}`) } const root = Session.basePath! - const prefix = Session.relativePosixPath(dir) const entryDefaults = { mode: 0o755, // Default mode for files in the archive } for (const file of files) { - archive.file(file, { - ...entryDefaults, - name: Session.relativePosixPath(file), - }) + if (file.physical) { + archive.file(file.filePath, { + ...entryDefaults, + name: Session.relativePosixPath(file.filePath), + }) + } else { + archive.append(file.content, { + ...entryDefaults, + name: Session.relativePosixPath(file.filePath), + }) + } } - const lockFileDirName = path.dirname(lockFile) - const packageJsonFile = path.join(lockFileDirName, 'package.json') - archive.file(lockFile, { - ...entryDefaults, - name: Session.relativePosixPath(lockFile), - }) - archive.file(packageJsonFile, { - ...entryDefaults, - name: Session.relativePosixPath(packageJsonFile), - }) - // handle workspaces - archive.glob('**/package.json', { - cwd: dir, - ignore: ignoredFiles, - }, { - ...entryDefaults, - prefix, - }) for (const includePattern of include) { // If pattern explicitly targets an ignored directory, only apply custom ignores const explicitlyTargetsIgnored = @@ -337,18 +280,6 @@ export async function loadPlaywrightProjectFiles ( ...entryDefaults, }) } - for (const filePath of extraFiles) { - archive.file(path.resolve(root, filePath), { - ...entryDefaults, - name: Session.relativePosixPath(filePath), - }) - } -} - -export async function findRegexFiles (directory: string, regex: RegExp, ignorePattern: string[]): -Promise { - const files = await findFilesWithPattern(directory, '**/*.{js,ts,mjs}', ignorePattern) - return files.filter(file => regex.test(file)).map(file => pathToPosix(path.relative(directory, file))) } export async function findFilesWithPattern ( diff --git a/packages/cli/src/testing/fixture-sandbox.ts b/packages/cli/src/testing/fixture-sandbox.ts new file mode 100644 index 000000000..f3aa42431 --- /dev/null +++ b/packages/cli/src/testing/fixture-sandbox.ts @@ -0,0 +1,202 @@ +import path from 'node:path' +import fs from 'node:fs/promises' +import { tmpdir } from 'node:os' + +import Debug from 'debug' + +const debug = Debug('checkly:cli:testing:fixture-sandbox') + +import { detectNearestPackageJson, detectPackageManager, PackageManager } from '../services/check-parser/package-files/package-manager' + +export interface CreateFixtureSandboxOptions { + /** + * The fixture source directory. + */ + source: string + + /** + * The full, absolute path to the sandbox directory. The path will be + * created if it does not exist. + * + * When the sandbox is destroyed, the path is deleted. + * + * If not provided, a temporary directory is created automatically. + */ + root?: string + + /** + * The package manager used to manage the fixture. + * + * If not provided, the package manager is detected automatically. + */ + packageManager?: PackageManager + + /** + * Whether to install packages using the package manager. + * + * @default true + */ + installPackages?: boolean + + /** + * If true, installs the packed containing package into the sandbox. Done + * only if packages are also installed. + * + * @default true + */ + injectPackedSelf?: boolean +} + +interface FixtureSandboxOptions { + packageManager: PackageManager + root: string +} + +export class FixtureSandbox { + #root: string + #packageManager: PackageManager + + private constructor ({ root, packageManager }: FixtureSandboxOptions) { + this.#root = root + this.#packageManager = packageManager + } + + get root (): string { + return this.#root + } + + get packageManager (): PackageManager { + return this.#packageManager + } + + static async create (options: CreateFixtureSandboxOptions): Promise { + const { execa } = await import('execa') + + const { + source, + root: maybeRoot, + packageManager: maybePackageManager, + installPackages = true, + injectPackedSelf = true, + } = options + + const root = maybeRoot + ? await fs.mkdir(maybeRoot, { recursive: true }).then(() => maybeRoot) + : await fs.mkdtemp(path.join(tmpdir(), 'fixture-sandbox-')) + + debug(`Using root ${root}`) + + debug(`Copying sources from ${source}`) + + await fs.cp(source, root, { + recursive: true, + }) + + const packageManager = maybePackageManager + ?? await detectPackageManager(root) + + debug(`Detected package manager ${packageManager.name}`) + + if (installPackages) { + const { executable, args, unsafeDisplayCommand } = packageManager.installCommand() + + debug(`Installing packages via ${unsafeDisplayCommand}`) + + await execa(executable, args, { + cwd: root, + }) + } + + if (installPackages && injectPackedSelf) { + debug('Injecting containing package') + + const lockfile = packageManager.representativeLockfile + + // Take a backup of the original package.json so that we can restore + // it later. + await fs.cp( + path.join(root, 'package.json'), + path.join(root, 'package.json.backup'), + ) + + // Same for the lockfile. + if (lockfile) { + await fs.cp( + path.join(root, lockfile), + path.join(root, `${lockfile}.backup`), + ) + } + + const packageJson = await detectNearestPackageJson(__dirname) + + const sourcePath = path.join( + packageJson.basePath, + `${packageJson.name}-${packageJson.version}.tgz`, + ) + + // Make sure the archive exists. + await fs.access(sourcePath, fs.constants.R_OK) + + const { executable, args } = packageManager.installCommand() + + await execa(executable, [...args, '--save-dev', `file:${sourcePath}`], { + cwd: root, + }) + + // Restore original package.json. + await fs.rename( + path.join(root, 'package.json.backup'), + path.join(root, 'package.json'), + ) + + // Restore original lockfile. + if (lockfile) { + await fs.rename( + path.join(root, `${lockfile}.backup`), + path.join(root, lockfile), + ) + } + } + + return new FixtureSandbox({ + root, + packageManager, + }) + } + + async destroy (): Promise { + debug(`Destroying root ${this.#root}`) + + await fs.rm(this.#root, { + recursive: true, + force: true, + }) + } + + async run (executable: string, args: string[], options?: RunOptions) { + return await run(executable, args, { + cwd: this.#root, + ...options, + }) + } + + abspath (...to: string[]): string { + return path.join(this.#root, ...to) + } +} + +async function run (executable: string, args: string[], options?: RunOptions) { + const { execa } = await import('execa') + + const result = await execa(executable, args, { + ...options, + }) + + return result +} + +export interface RunOptions { + env?: Record + timeout?: number + cwd?: string +}