From 1b25f8cdccdd5fb9d5ed1dc445119e28ff20920c Mon Sep 17 00:00:00 2001 From: Mythic Date: Tue, 27 Aug 2024 17:02:43 +0300 Subject: [PATCH] Implement visual diffing CI pipeline Implement a CI pipeline for generating visual screencaps of the cyberstorm-remix site and uploading them to percy for review --- .editorconfig | 3 + .github/workflows/visual-diff.yml | 75 + apps/cyberstorm-remix/.gitignore | 1 + tools/cyberstorm-playwright/.gitignore | 6 + tools/cyberstorm-playwright/.percy.yml | 3 + tools/cyberstorm-playwright/package-lock.json | 1301 +++++++++++++++++ tools/cyberstorm-playwright/package.json | 16 + .../playwright.config.ts | 87 ++ .../tests/communitie.spec.ts | 10 + {apps => tools}/ts-proxy/Dockerfile | 0 {apps => tools}/ts-proxy/docker-compose.yml | 0 {apps => tools}/ts-proxy/nginx.conf | 0 tools/visual-diff-backend/docker-compose.yml | 82 ++ tools/visual-diff-backend/fix_migration.py | 47 + tools/visual-diff-backend/run_test_backend.py | 89 ++ tools/visual-diff-ci/helpers/__init__.py | 0 tools/visual-diff-ci/poetry.lock | 67 + tools/visual-diff-ci/pyproject.toml | 15 + tools/visual-diff-ci/run_ci_script.py | 221 +++ 19 files changed, 2023 insertions(+) create mode 100644 .github/workflows/visual-diff.yml create mode 100644 tools/cyberstorm-playwright/.gitignore create mode 100644 tools/cyberstorm-playwright/.percy.yml create mode 100644 tools/cyberstorm-playwright/package-lock.json create mode 100644 tools/cyberstorm-playwright/package.json create mode 100644 tools/cyberstorm-playwright/playwright.config.ts create mode 100644 tools/cyberstorm-playwright/tests/communitie.spec.ts rename {apps => tools}/ts-proxy/Dockerfile (100%) rename {apps => tools}/ts-proxy/docker-compose.yml (100%) rename {apps => tools}/ts-proxy/nginx.conf (100%) create mode 100644 tools/visual-diff-backend/docker-compose.yml create mode 100644 tools/visual-diff-backend/fix_migration.py create mode 100644 tools/visual-diff-backend/run_test_backend.py create mode 100644 tools/visual-diff-ci/helpers/__init__.py create mode 100644 tools/visual-diff-ci/poetry.lock create mode 100644 tools/visual-diff-ci/pyproject.toml create mode 100644 tools/visual-diff-ci/run_ci_script.py diff --git a/.editorconfig b/.editorconfig index fdd320ba8..e822bd2bd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,3 +8,6 @@ trim_trailing_whitespace = true end_of_line = lf charset = utf-8 max_line_length = 88 + +[*.py] +indent_size = 4 diff --git a/.github/workflows/visual-diff.yml b/.github/workflows/visual-diff.yml new file mode 100644 index 000000000..f64da87a3 --- /dev/null +++ b/.github/workflows/visual-diff.yml @@ -0,0 +1,75 @@ +name: "Visual diff" + +on: + push: + branches: + - "master" + pull_request: + branches: + - "**" + +jobs: + get-node-version: + name: Get Node version + if: ( + github.event_name != 'pull_request' && ( + github.event_name != 'push' || + github.actor != 'dependabot[bot]' + ) + ) || ( + github.event_name == 'pull_request' && ( + github.event.base.repo.id != github.event.head.repo.id || + github.actor == 'dependabot[bot]' + ) + ) + runs-on: ubuntu-latest + outputs: + node-version: ${{ steps.get-node-version.outputs.node-version }} + steps: + - uses: actions/checkout@v4 + - name: Check get-node-version version + uses: ./.github/actions/get-node-version + with: + package-json: .github/actions/get-node-version/package.json + - name: Get Node version + id: get-node-version + uses: ./.github/actions/get-node-version + visual-diff: + name: "Generate visual diffs" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout" + uses: actions/checkout@v4 + - name: "Setup Python 3.10" + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: "Setup Node" + uses: actions/setup-node@v4 + with: + node-version: ${{ needs.get-node-version.outputs.node-version }} + - uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + - name: Cache poetry venv + id: cache-poetry + uses: actions/cache@v4 + with: + path: tools/visual-diff-ci/.venv + key: "poetry-${{ runner.os }}-\ + ${{ steps.setup-python.outputs.python-version }}-\ + ${{ hashFiles('tools/visual-diff-ci/poetry.lock') }}" + restore-keys: | + poetry-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}- + - name: Install poetry dependencies + run: | + cd tools/visual-diff-ci/ + poetry install + - name: Run visual diffing script + run: | + cd tools/visual-diff-ci/ + poetry run python run_ci_script.py + env: + PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} diff --git a/apps/cyberstorm-remix/.gitignore b/apps/cyberstorm-remix/.gitignore index d0b9a35dd..64a44e7f8 100644 --- a/apps/cyberstorm-remix/.gitignore +++ b/apps/cyberstorm-remix/.gitignore @@ -3,3 +3,4 @@ node_modules /.cache /build .env.development +.env.production diff --git a/tools/cyberstorm-playwright/.gitignore b/tools/cyberstorm-playwright/.gitignore new file mode 100644 index 000000000..1d48be2ba --- /dev/null +++ b/tools/cyberstorm-playwright/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +build-storybook.log diff --git a/tools/cyberstorm-playwright/.percy.yml b/tools/cyberstorm-playwright/.percy.yml new file mode 100644 index 000000000..933b5d561 --- /dev/null +++ b/tools/cyberstorm-playwright/.percy.yml @@ -0,0 +1,3 @@ +version: 1 +snapshot: + widths: [375, 1200, 1920] diff --git a/tools/cyberstorm-playwright/package-lock.json b/tools/cyberstorm-playwright/package-lock.json new file mode 100644 index 000000000..bfdf95b33 --- /dev/null +++ b/tools/cyberstorm-playwright/package-lock.json @@ -0,0 +1,1301 @@ +{ + "name": "cyberstorm-playwright", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cyberstorm-playwright", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@percy/cli": "^1.29.2", + "@percy/playwright": "^1.0.6", + "@playwright/test": "^1.46.1", + "@types/node": "^22.5.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@percy/cli": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/cli/-/cli-1.29.2.tgz", + "integrity": "sha512-3wQC6mZH1nkpM0m8KQu3DGX4mGyxom8AJdz9IN4Uv8avJDCgsQHBe2h+OCNbZwlBUqduTvkUqYJEQ/qsltgSbw==", + "dev": true, + "dependencies": { + "@percy/cli-app": "1.29.2", + "@percy/cli-build": "1.29.2", + "@percy/cli-command": "1.29.2", + "@percy/cli-config": "1.29.2", + "@percy/cli-exec": "1.29.2", + "@percy/cli-snapshot": "1.29.2", + "@percy/cli-upload": "1.29.2", + "@percy/client": "1.29.2", + "@percy/logger": "1.29.2" + }, + "bin": { + "percy": "bin/run.cjs" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-app": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/cli-app/-/cli-app-1.29.2.tgz", + "integrity": "sha512-OpPeyRmSht0twb/Sw+XzdIcACPPD2SWI/ckSe+o1VxDgRvmaN7qJkyUhgk7LBF6z0qbJTeEnHK1amU02dCsu0A==", + "dev": true, + "dependencies": { + "@percy/cli-command": "1.29.2", + "@percy/cli-exec": "1.29.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-build": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/cli-build/-/cli-build-1.29.2.tgz", + "integrity": "sha512-Bauu4To6Zn7otj0ialc8pT/KptyES9xTgEMT/uCGvZ3J4s7D9za5DPQSot+YLin2NqMdYmp6pBc/IpYGJWO1gQ==", + "dev": true, + "dependencies": { + "@percy/cli-command": "1.29.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-command": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/cli-command/-/cli-command-1.29.2.tgz", + "integrity": "sha512-uW9vRTxy60Ktqda28eWEWQ3x6UtChU4H8E0epLwxeqgO/fpYnS1/llzx4g2QD+H3lSORltrSwXnUzpfcik911g==", + "dev": true, + "dependencies": { + "@percy/config": "1.29.2", + "@percy/core": "1.29.2", + "@percy/logger": "1.29.2" + }, + "bin": { + "percy-cli-readme": "bin/readme.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-config": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/cli-config/-/cli-config-1.29.2.tgz", + "integrity": "sha512-OUtUTSXuQ7uJSXiRqyOda8ZOLPXD/gUQNW/PkTHFFN2Lv3CgWD+Wilf2No7xM7tz8h9HTLauqUMK0SXZa6a33Q==", + "dev": true, + "dependencies": { + "@percy/cli-command": "1.29.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-exec": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/cli-exec/-/cli-exec-1.29.2.tgz", + "integrity": "sha512-lVdP1D4FgehpBMhJ6fGcKjRUOvQ56r9AkhCi5wWjZO5Lt/qIWXGJneoJBRNU7gClAOcJAD1AEypY6SNDnQlkow==", + "dev": true, + "dependencies": { + "@percy/cli-command": "1.29.2", + "@percy/logger": "1.29.2", + "cross-spawn": "^7.0.3", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-snapshot": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/cli-snapshot/-/cli-snapshot-1.29.2.tgz", + "integrity": "sha512-slub1k+1uM+C5ie+6ab+4bpxc22hBSrJUy2CIzfATGAfDiQn6dLgqiZrCm988GjZiw5MS1G8IgQcqlU7HmxV2w==", + "dev": true, + "dependencies": { + "@percy/cli-command": "1.29.2", + "yaml": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/cli-upload": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/cli-upload/-/cli-upload-1.29.2.tgz", + "integrity": "sha512-7uLF3ESslnv9r/vU/gMnUstaZnfcuPN39ADldgX/NBWgKVibfo0g1QGXiWf4RBBvixSKpIbidh5YbfyErLmpFQ==", + "dev": true, + "dependencies": { + "@percy/cli-command": "1.29.2", + "fast-glob": "^3.2.11", + "image-size": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/client": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/client/-/client-1.29.2.tgz", + "integrity": "sha512-2urvUo8YzizyvXWi/ber83DJ6j1wZ8e32BySUwra8OFpUYrRr0/9qHB/xaJJPJ3lwgUuJCR8+pXrEN925BM6Wg==", + "dev": true, + "dependencies": { + "@percy/env": "1.29.2", + "@percy/logger": "1.29.2", + "pako": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/config": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/config/-/config-1.29.2.tgz", + "integrity": "sha512-A0ySzyY5KLYvfV8sltHtksDviCTDt0p5f7/KKoVCoIlIM0kwOQ2Xs4u6/Den+VvGhub0eNvn4YHUVY9SVxs2lg==", + "dev": true, + "dependencies": { + "@percy/logger": "1.29.2", + "ajv": "^8.6.2", + "cosmiconfig": "^8.0.0", + "yaml": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/config/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@percy/config/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@percy/config/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@percy/core": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/core/-/core-1.29.2.tgz", + "integrity": "sha512-VOosiIRyXRMTQw2uaR7PdB+g7AP0vgl/I0I7ebh6a7qqTRGynVnQ4enHSDVj3nYlevsiz1SnRVvZZG4Dx4x9sA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@percy/client": "1.29.2", + "@percy/config": "1.29.2", + "@percy/dom": "1.29.2", + "@percy/logger": "1.29.2", + "@percy/webdriver-utils": "1.29.2", + "content-disposition": "^0.5.4", + "cross-spawn": "^7.0.3", + "extract-zip": "^2.0.1", + "fast-glob": "^3.2.11", + "micromatch": "^4.0.6", + "mime-types": "^2.1.34", + "pako": "^2.1.0", + "path-to-regexp": "^6.2.0", + "rimraf": "^3.0.2", + "ws": "^8.17.1", + "yaml": "^2.4.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/core/node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, + "node_modules/@percy/core/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@percy/dom": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/dom/-/dom-1.29.2.tgz", + "integrity": "sha512-zxgvTb1EbCydtu0X6Pks/def8IR27Llk/4hrhgO79kuGg7Ydkl/16/QBLH11yr/NSA4kM9DEIKcV+st0fb2UOw==", + "dev": true + }, + "node_modules/@percy/env": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/env/-/env-1.29.2.tgz", + "integrity": "sha512-Lhl9NcrAD8xYWXkHj7kuWSJuOmoPLmAbm2tuLAeaMM8zqU/BDYE0zok92sRZMzfIAig5MtxURMqN3XzIaMs9nA==", + "dev": true, + "dependencies": { + "@percy/logger": "1.29.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/logger": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/logger/-/logger-1.29.2.tgz", + "integrity": "sha512-gcsDKnnV8Mg12KferGhoDlng4pdDWDqt0bqPAPexUDMOTAzBgOdm1ba0NDIYPwlFTVSyWbvWaUapmhDrnszOdA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/playwright": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@percy/playwright/-/playwright-1.0.6.tgz", + "integrity": "sha512-oi98dnsTp/QG0NUFTUHsyqoWvgDOuzBmSzCwZCjxu52k7F8hzaOazqRqqdRHc7GVRUL561RUdkzZi0xhiW+Jfg==", + "dev": true, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "playwright-core": ">=1" + } + }, + "node_modules/@percy/sdk-utils": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/sdk-utils/-/sdk-utils-1.29.2.tgz", + "integrity": "sha512-oeeopqb7n1voWJ2t5PdkgihsYurcVtkp4HyqOV+o7/kXk0/mqDkUJ+6BVaSJjkonQ9FCZ3dr+Qq1r9au6BAS8g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@percy/webdriver-utils": { + "version": "1.29.2", + "resolved": "https://registry.npmjs.org/@percy/webdriver-utils/-/webdriver-utils-1.29.2.tgz", + "integrity": "sha512-hVo+0ymbFEDhCcrb8iy9xfNzaT5fBa+toMVN9o6wmU68KbjpSwOOyVo0FOSvN+O0/AZfMcArPhz+959/Tdh3ig==", + "dev": true, + "dependencies": { + "@percy/config": "1.29.2", + "@percy/sdk-utils": "1.29.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz", + "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==", + "dev": true, + "dependencies": { + "playwright": "1.46.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "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/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/image-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", + "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", + "dev": true, + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", + "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", + "dev": true, + "dependencies": { + "playwright-core": "1.46.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", + "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dev": true, + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "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 + } + } + }, + "node_modules/yaml": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/tools/cyberstorm-playwright/package.json b/tools/cyberstorm-playwright/package.json new file mode 100644 index 000000000..727980757 --- /dev/null +++ b/tools/cyberstorm-playwright/package.json @@ -0,0 +1,16 @@ +{ + "name": "cyberstorm-playwright", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@percy/cli": "^1.29.2", + "@percy/playwright": "^1.0.6", + "@playwright/test": "^1.46.1", + "@types/node": "^22.5.0" + } +} diff --git a/tools/cyberstorm-playwright/playwright.config.ts b/tools/cyberstorm-playwright/playwright.config.ts new file mode 100644 index 000000000..83b9258e7 --- /dev/null +++ b/tools/cyberstorm-playwright/playwright.config.ts @@ -0,0 +1,87 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// 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://127.0.0.1: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"], + viewport: { width: 1920, height: 1080 }, + }, + }, + + { + name: "firefox", + use: { + ...devices["Desktop Firefox"], + viewport: { width: 1920, height: 1080 }, + }, + }, + + { + name: "webkit", + use: { + ...devices["Desktop Safari"], + viewport: { width: 1920, height: 1080 }, + }, + }, + + /* 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://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/tools/cyberstorm-playwright/tests/communitie.spec.ts b/tools/cyberstorm-playwright/tests/communitie.spec.ts new file mode 100644 index 000000000..e163e3a14 --- /dev/null +++ b/tools/cyberstorm-playwright/tests/communitie.spec.ts @@ -0,0 +1,10 @@ +// import { test, expect } from "@chromatic-com/playwright"; +import { test, expect } from "@playwright/test"; + +const percySnapshot = require("@percy/playwright"); + +test("communities page", async ({ page }) => { + await page.goto("http://localhost:3000/communities"); + // await expect(page).toHaveScreenshot({ fullPage: true }); + await percySnapshot(page, "Communities"); +}); diff --git a/apps/ts-proxy/Dockerfile b/tools/ts-proxy/Dockerfile similarity index 100% rename from apps/ts-proxy/Dockerfile rename to tools/ts-proxy/Dockerfile diff --git a/apps/ts-proxy/docker-compose.yml b/tools/ts-proxy/docker-compose.yml similarity index 100% rename from apps/ts-proxy/docker-compose.yml rename to tools/ts-proxy/docker-compose.yml diff --git a/apps/ts-proxy/nginx.conf b/tools/ts-proxy/nginx.conf similarity index 100% rename from apps/ts-proxy/nginx.conf rename to tools/ts-proxy/nginx.conf diff --git a/tools/visual-diff-backend/docker-compose.yml b/tools/visual-diff-backend/docker-compose.yml new file mode 100644 index 000000000..8efe1300d --- /dev/null +++ b/tools/visual-diff-backend/docker-compose.yml @@ -0,0 +1,82 @@ +version: "3.8" + +x-django-service: &django-service + image: ${DJANGO_IMAGE:-thunderstore/thunderstore:release-0.130.0} + environment: + CORS_ALLOWED_ORIGINS: "*" + CELERY_BROKER_URL: "pyamqp://django:django@rabbitmq/django" + DATABASE_URL: "psql://django:django@db/django" + REDIS_URL: "redis://redis:6379/0" + SECRET_KEY: "hunter2" + AWS_ACCESS_KEY_ID: "thunderstore" + AWS_SECRET_ACCESS_KEY: "thunderstore" + AWS_S3_REGION_NAME: "" + AWS_S3_ENDPOINT_URL: "http://minio:9000/" + AWS_S3_CUSTOM_DOMAIN: "localhost:9000/thunderstore" + AWS_STORAGE_BUCKET_NAME: "thunderstore" + AWS_LOCATION: "development" + AWS_S3_SECURE_URLS: "False" + USERMEDIA_S3_ENDPOINT_URL: "http://minio:9000/" + USERMEDIA_S3_SIGNING_ENDPOINT_URL: "http://localhost:9000/" + USERMEDIA_S3_ACCESS_KEY_ID: "thunderstore" + USERMEDIA_S3_SECRET_ACCESS_KEY: "thunderstore" + USERMEDIA_S3_REGION_NAME: "" + USERMEDIA_S3_STORAGE_BUCKET_NAME: "thunderstore" + USERMEDIA_S3_LOCATION: "development" + CACHE_S3_ENDPOINT_URL: "http://minio:9000/" + CACHE_S3_SIGNING_ENDPOINT_URL: "http://localhost:9000/" + CACHE_S3_ACCESS_KEY_ID: "thunderstore" + CACHE_S3_SECRET_ACCESS_KEY: "thunderstore" + CACHE_S3_REGION_NAME: "" + CACHE_S3_STORAGE_BUCKET_NAME: "thunderstore" + CACHE_S3_LOCATION: "development/cache" + IS_CYBERSTORM_ENABLED: "True" + DEBUG: "True" + PROTOCOL: "http://" + SOURCE_VIEW_ENABLE_HIGHLIGHTING: "True" + USE_ASYNC_PACKAGE_SUBMISSION_FLOW: "True" + USE_TIME_SERIES_PACKAGE_DOWNLOAD_METRICS: "True" + ALLOWED_HOSTS: "*" + PRIMARY_HOST: "127.0.0.1:8000" + depends_on: + - db + - redis + - rabbitmq + - minio + +services: + db: + image: postgres:13.12-alpine + environment: + POSTGRES_PASSWORD: django + POSTGRES_USER: django + POSTGRES_DB: django + + redis: + image: redis:6.2.1-alpine@sha256:daa795ebec4c206cc7f76e19c4bb26a98dfca671d7b5bedcb38862de078bdaa2 + command: --maxmemory 200mb --maxmemory-policy allkeys-lru + + rabbitmq: + image: rabbitmq:3.8.14-alpine@sha256:11d8cd02ac340e99de628053f76bb1182ce3df3b41d3e8f399377276ba489409 + hostname: rabbitmq + environment: + RABBITMQ_DEFAULT_USER: django + RABBITMQ_DEFAULT_PASS: django + RABBITMQ_DEFAULT_VHOST: django + + minio: + image: thunderstore/minio:latest + environment: + MINIO_ROOT_USER: thunderstore + MINIO_ROOT_PASSWORD: thunderstore + command: server /data + + django: + <<: *django-service + entrypoint: "python" + command: "run_test_backend.py" + volumes: + - ./run_test_backend.py:/app/run_test_backend.py + - ./fix_migration.py:/app/fix_migration.py + ports: + - "127.0.0.1:8000:8000" diff --git a/tools/visual-diff-backend/fix_migration.py b/tools/visual-diff-backend/fix_migration.py new file mode 100644 index 000000000..56fb10a7f --- /dev/null +++ b/tools/visual-diff-backend/fix_migration.py @@ -0,0 +1,47 @@ +import os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "thunderstore.core.settings") + +from django import setup + +setup() + +import thunderstore.monkeypatch # noqa + + +from thunderstore.community.models import Community, CommunitySite +from django.contrib.sites.models import Site + + +def setup_default_community(): + primary_host = os.environ.get("PRIMARY_HOST", "127.0.0.1:8000") + default_community = Community.objects.filter(identifier="riskofrain2").first() + if not default_community: + default_community = Community.objects.create( + identifier="riskofrain2", name="Risk of Rain 2" + ) + community_site = default_community.sites.first() + + if not community_site: + site = Site.objects.filter(community=None).first() + if not site: + site = Site.objects.create( + domain=primary_host, name=default_community.name + ) + site.domain = primary_host + site.save() + CommunitySite.objects.create( + site=site, + community=default_community, + ) + else: + site = community_site.site + site.domain = primary_host + site.save() + + print("Created default community") + print(f"{site=}") + print(f"{primary_host=}") + + +setup_default_community() diff --git a/tools/visual-diff-backend/run_test_backend.py b/tools/visual-diff-backend/run_test_backend.py new file mode 100644 index 000000000..65bfbec97 --- /dev/null +++ b/tools/visual-diff-backend/run_test_backend.py @@ -0,0 +1,89 @@ +import os +import subprocess +import sys +from distutils.util import strtobool +from typing import List, Union + + +class EnvironmentVariable: + def __init__(self, cast, name, default): + self.cast = cast + self.name = name + self.default = default + + @property + def value(self): + val = os.environ.get(self.name, self.default) + if self.cast is not None and val is not None and isinstance(val, str): + val = self.cast(val) + return val + + @value.setter + def value(self, val): + if val is None and self.name in os.environ: + del os.environ[self.name] + else: + os.environ[self.name] = str(val) + + def __str__(self): + if self.value is None: + return "" + return str(self.value) + + def __bool__(self): + return bool(self.value) + + +VARIABLES = {} + + +def run_command(command: Union[List[Union[EnvironmentVariable, str]], str]) -> int: + environment = { + **os.environ, + **({k: str(v) for k, v in VARIABLES.items() if v is not None}), + } + if isinstance(command, str): + command = command.split(" ") + else: + command = [str(x) for x in command] + print(" ".join(command)) + result = subprocess.call(command, env=environment) + if result != 0: + sys.exit(result) + return result + + +def register_variable(cast, name, default): + var = EnvironmentVariable(cast, name, default) + VARIABLES[name] = var + return var + + +def to_bool(val) -> bool: + if not val: + return False + return bool(strtobool(val)) + + +def run_migrations(): + run_command("python manage.py migrate community") + run_command("python fix_migration.py") + run_command("python manage.py migrate") + + +def create_data(): + run_command("python manage.py create_test_data") + + +def launch_server(): + run_command("python docker_entrypoint.py django") + + +def main(): + run_migrations() + create_data() + launch_server() + + +if __name__ == "__main__": + main() diff --git a/tools/visual-diff-ci/helpers/__init__.py b/tools/visual-diff-ci/helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tools/visual-diff-ci/poetry.lock b/tools/visual-diff-ci/poetry.lock new file mode 100644 index 000000000..ce0b51d9e --- /dev/null +++ b/tools/visual-diff-ci/poetry.lock @@ -0,0 +1,67 @@ +[[package]] +name = "certifi" +version = "2024.7.4" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.7.0" + +[[package]] +name = "idna" +version = "3.8" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=3.8" + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "b27186d4114ddf6af8c83239646e26a3a002f3f3c9217a05db042d6820be1400" + +[metadata.files] +certifi = [] +charset-normalizer = [] +idna = [] +requests = [] +urllib3 = [] diff --git a/tools/visual-diff-ci/pyproject.toml b/tools/visual-diff-ci/pyproject.toml new file mode 100644 index 000000000..51059a5bb --- /dev/null +++ b/tools/visual-diff-ci/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "visual-diffs" +version = "0.1.0" +description = "" +authors = ["Mythic "] + +[tool.poetry.dependencies] +python = "^3.10" +requests = "^2.32.3" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tools/visual-diff-ci/run_ci_script.py b/tools/visual-diff-ci/run_ci_script.py new file mode 100644 index 000000000..8a18db2ac --- /dev/null +++ b/tools/visual-diff-ci/run_ci_script.py @@ -0,0 +1,221 @@ +import os +import shutil +import subprocess +import sys +import time +from distutils.util import strtobool +from pathlib import Path +from typing import List, Union, Dict, Optional + +import requests + + +CURRENT_DIR = Path(__file__).parent +REPO_ROOT = CURRENT_DIR / ".." / ".." +YARN_PATH = shutil.which("yarn") +NPX_PATH = shutil.which("npx") +NPM_PATH = shutil.which("npm") + + +class EarlyExit(Exception): + def __init__(self, status_code: int): + self.status_code = status_code + + def exit(self): + sys.exit(self.status_code) + + +class EnvironmentVariable: + def __init__(self, cast, name, default): + self.cast = cast + self.name = name + self.default = default + + @property + def value(self): + val = os.environ.get(self.name, self.default) + if self.cast is not None and val is not None and isinstance(val, str): + val = self.cast(val) + return val + + @value.setter + def value(self, val): + if val is None and self.name in os.environ: + del os.environ[self.name] + else: + os.environ[self.name] = str(val) + + def __str__(self): + if self.value is None: + return "" + return str(self.value) + + def __bool__(self): + return bool(self.value) + + +VARIABLES = {} + + +def build_command(parts: Union[List[Union[EnvironmentVariable, str]], str]) -> List[str]: + if isinstance(parts, str): + command = parts.split(" ") + else: + command = [str(x) for x in parts] + return command + + +def build_env() -> Dict[str, str]: + environment = { + **os.environ, + **({k: str(v) for k, v in VARIABLES.items() if v is not None}), + } + return environment + + +def run_command( + command: Union[List[Union[EnvironmentVariable, str]], str], + cwd: Optional[Path] = None, +) -> int: + environment = build_env() + command = build_command(command) + cwd = cwd or CURRENT_DIR + print(str(cwd.resolve()) + ": " + " ".join(command)) + result = subprocess.call(command, env=environment, cwd=cwd) + if result != 0: + raise EarlyExit(result) + return result + + +def register_variable(cast, name, default) -> EnvironmentVariable: + var = EnvironmentVariable(cast, name, default) + VARIABLES[name] = var + return var + + +def to_bool(val) -> bool: + if not val: + return False + return bool(strtobool(val)) + + +PERCY_TOKEN = register_variable(str, "PERCY_TOKEN", None) + + +class BgProcess: + process: subprocess.Popen + + def __init__( + self, + command: Union[List[Union[EnvironmentVariable, str]], str], + cwd: Optional[Path] = None, + env: Optional[Dict[str, str]] = None, + ): + command = build_command(command) + cwd = cwd or CURRENT_DIR + print(str(cwd.resolve()) + ": " + " ".join(command)) + env = { + **build_env(), + **(env or {}), + } + self.process = subprocess.Popen( + command, + cwd=cwd, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + def kill(self): + self.process.kill() + + +def setup_frontend(): + run_command([YARN_PATH, "install", "--frozen-lockfile"], cwd=REPO_ROOT) + run_command([YARN_PATH, "workspace", "@thunderstore/cyberstorm-remix", "build"], cwd=REPO_ROOT) + + +def start_frontend() -> BgProcess: + configs = { + "PUBLIC_SITE_URL": "http://127.0.0.1:8000", + "PUBLIC_API_URL": "http://127.0.0.1:8000", + } + process = BgProcess( + [YARN_PATH, "workspace", "@thunderstore/cyberstorm-remix", "start", "--host", "0.0.0.0", "--port", "3000"], + cwd=REPO_ROOT, + env=configs, + ) + return process + + +def wait_for_url(url: str) -> bool: + def poll_backend() -> bool: + try: + resp = requests.get(url, timeout=3) + return resp.status_code == 200 + except Exception: + return False + + timeout_threshold = time.time() + 60 + while (result := poll_backend()) is False and time.time() < timeout_threshold: + print( + "Polling failed, " + f"retrying for {timeout_threshold - time.time():.2f} seconds" + ) + time.sleep(1) + + return result + + +BACKEND_DIR = REPO_ROOT / "tools" / "visual-diff-backend" + + +def start_backend() -> bool: + run_command("docker compose up -d", cwd=BACKEND_DIR) + return wait_for_url("http://127.0.0.1:8000/") + + +def stop_backend(): + run_command("docker compose down", cwd=BACKEND_DIR) + + +PLAYWRIGHT_DIR = (REPO_ROOT / "tools" / "cyberstorm-playwright").resolve() + + +def run_playwright(): + # try: + run_command([NPM_PATH, "ci"], cwd=PLAYWRIGHT_DIR) + run_command([NPX_PATH, "playwright", "install", "--with-deps"], cwd=PLAYWRIGHT_DIR) + run_command([NPX_PATH, "percy", "exec", "--", "playwright", "test", "--reporter=list"], cwd=PLAYWRIGHT_DIR) + + +def main(): + print("Setting up frontend") + setup_frontend() + backend_success = start_backend() + + if backend_success: + print("Successfully launched backend!") + else: + print("Failed to launch backend!") + + print("Starting frontend") + process = start_frontend() + wait_for_url("http://127.0.0.1:3000") + + try: + run_playwright() + except Exception as e: + print(f"Exception: {e}") + process.kill() + stop_backend() + raise e + + print("Done") + + +if __name__ == "__main__": + try: + main() + except EarlyExit as e: + e.exit()