From d476f8cbe10d5981ef4dd72045ce0ff917c89075 Mon Sep 17 00:00:00 2001 From: Martin <2026226+martin-mfg@users.noreply.github.com> Date: Sat, 18 Apr 2026 10:37:26 +0200 Subject: [PATCH] extract "core" package, small improvements (#137) This PR mainly refactors the code by extracting a "core" package which is then used by the "backend" and "frontend" apps. Apart from this the PR also contains several smaller changes like fixing dependabot, upgrading dependencies, aligned "package.json" files, added tests, ... addresses step 1 of #32 closes #136 --------- Co-authored-by: Marco Pasqualetti --- .github/CONTRIBUTING.md | 3 +- .../actions/install-dependencies/action.yml | 6 +- .github/dependabot.yml | 15 +- .github/workflows/ci.yml | 59 +- .github/workflows/generate-theme-doc.yml | 8 +- .github/workflows/release.yml | 18 + .github/workflows/repeat-recent-requests.yml | 2 +- .github/workflows/update-langs.yml | 6 +- .gitignore | 17 +- .husky/pre-commit | 3 +- .../output/functions/api.func/router.js | 76 - apps/backend/api-renamed/authenticate.js | 3 +- apps/backend/api-renamed/delete-user.js | 3 +- apps/backend/api-renamed/downgrade.js | 2 +- apps/backend/api-renamed/gist.js | 129 - apps/backend/api-renamed/status/pat-info.js | 24 +- apps/backend/api-renamed/status/up.js | 10 +- apps/backend/api-renamed/user-access.js | 3 +- apps/backend/api-renamed/wakatime-proxy.js | 6 +- apps/backend/api-renamed/wakatime.js | 148 - apps/backend/express.js | 15 +- apps/backend/index.js | 1 + apps/backend/package.json | 49 +- apps/backend/powered-by-vercel.svg | 6 - apps/backend/router.js | 225 ++ apps/backend/src/cards/index.js | 4 - apps/backend/src/common/access.js | 50 +- apps/backend/src/common/cache.js | 9 +- apps/backend/src/common/database.js | 18 +- apps/backend/src/common/envs.js | 15 - apps/backend/src/common/index.js | 13 - apps/backend/src/index.js | 2 - apps/backend/src/users.js | 1 + .../tests/__snapshots__/wakatime.test.js.snap | 115 - apps/backend/tests/_setup.private-instance.js | 2 - apps/backend/tests/api.test.js | 433 +-- apps/backend/tests/bench/api.bench.js | 91 +- .../tests/bench/calculateRank.bench.js | 22 - apps/backend/tests/bench/gist.bench.js | 66 +- apps/backend/tests/bench/pin.bench.js | 63 +- apps/backend/tests/e2e/e2e.test.js | 366 ++- apps/backend/tests/gist.test.js | 259 +- apps/backend/tests/pat-info.test.js | 27 +- apps/backend/tests/pin.test.js | 232 +- .../tests/private-instance/api.test.js | 41 - .../tests/private-instance/gist.test.js | 41 - .../tests/private-instance/pin.test.js | 64 - .../tests/private-instance/top-langs.test.js | 41 - .../__snapshots__/api.test.js.snap | 514 ++++ .../__snapshots__/gist.test.js.snap | 252 ++ .../__snapshots__/pin.test.js.snap | 406 +++ .../__snapshots__/top-langs.test.js.snap | 460 +++ .../__snapshots__/wakatime.test.js.snap | 546 ++++ .../backend/tests/public-instance/api.test.js | 180 +- .../tests/public-instance/gist.test.js | 125 +- .../backend/tests/public-instance/pin.test.js | 235 +- .../tests/public-instance/top-langs.test.js | 194 +- .../tests/public-instance/wakatime.test.js | 212 ++ apps/backend/tests/test-data/api-data.js | 51 - apps/backend/tests/test-data/gist-data.js | 28 - apps/backend/tests/test-data/langs-data.js | 36 - apps/backend/tests/test-data/pin-data.js | 26 - apps/backend/tests/top-langs.test.js | 239 +- apps/backend/tests/utils.js | 315 +- apps/backend/tests/wakatime.test.js | 246 +- apps/backend/tsconfig.typecheck.json | 7 + apps/backend/vercel.json | 2 +- apps/backend/vitest.config.bench.ts | 3 + apps/backend/vitest.config.e2e.ts | 3 + apps/backend/vitest.config.ts | 37 +- apps/frontend/package.json | 49 +- .../src/components/Card/SvgInline.tsx | 4 +- apps/frontend/src/constants.ts | 8 +- apps/frontend/src/pages/Home/Home.tsx | 2 +- apps/frontend/src/pages/Home/stages/Theme.tsx | 22 +- apps/frontend/tsconfig.typecheck.json | 7 + apps/frontend/vercel.json | 2 +- apps/frontend/vite.config.ts | 54 +- docs/advanced_documentation.md | 6 +- docs/deploy.md | 1 + eslint.config.js | 20 + knip.jsonc | 15 +- package.json | 41 +- {apps/backend => packages/core}/codecov.yml | 0 packages/core/package.json | 94 + .../core}/scripts/generate-langs-json.js | 0 .../core}/scripts/generate-theme-doc.js | 4 +- packages/core/src/api/gist.js | 97 + .../core/src/api}/index.js | 93 +- .../core/src/api}/pin.js | 95 +- .../core/src/api}/top-langs.js | 100 +- packages/core/src/api/wakatime.js | 113 + .../core}/src/calculateRank.js | 0 .../core}/src/cards/gist.js | 0 .../core}/src/cards/repo.js | 0 .../core}/src/cards/stats.js | 2 +- .../core}/src/cards/top-languages.js | 4 +- .../core}/src/cards/types.d.ts | 16 +- .../core}/src/cards/wakatime.js | 2 +- .../core}/src/common/Card.js | 2 +- .../core}/src/common/I18n.js | 0 .../core}/src/common/color.js | 2 +- packages/core/src/common/config.js | 78 + .../core}/src/common/constants.js | 0 .../core}/src/common/error.js | 0 .../core}/src/common/fmt.js | 2 +- .../core}/src/common/html.js | 0 .../core}/src/common/http.js | 0 .../core}/src/common/icons.js | 0 .../core}/src/common/languageColors.json | 3 + .../core}/src/common/log.js | 6 +- .../core}/src/common/ops.js | 0 .../core}/src/common/render.js | 1 - .../core}/src/common/retryer.js | 21 +- .../core}/src/fetchers/gist.js | 5 +- .../core}/src/fetchers/repo.js | 5 +- .../core}/src/fetchers/stats.js | 67 +- .../core}/src/fetchers/top-languages.js | 21 +- .../core}/src/fetchers/types.d.ts | 40 +- .../core}/src/fetchers/wakatime.js | 0 packages/core/src/index.ts | 25 + .../core/src}/themes/README.md | 0 .../core/src/themes/index.ts | 13 +- .../core}/src/translations.js | 0 .../renderWakatimeCard.test.js.snap | 72 + packages/core/tests/_setup.js | 7 + .../core}/tests/calculateRank.test.js | 2 - .../core}/tests/card.test.js | 2 - .../core}/tests/color.test.js | 0 .../core}/tests/fetchGist.test.js | 2 - .../core}/tests/fetchRepo.test.js | 2 - .../core}/tests/fetchStats.test.js | 7 +- .../core}/tests/fetchTopLanguages.test.js | 2 - .../core}/tests/fetchWakatime.test.js | 2 - .../core}/tests/flexLayout.test.js | 0 .../core}/tests/fmt.test.js | 0 .../core}/tests/html.test.js | 0 .../core}/tests/i18n.test.js | 0 .../core}/tests/ops.test.js | 0 .../core}/tests/render.test.js | 2 - .../core}/tests/renderGistCard.test.js | 4 +- .../core}/tests/renderRepoCard.test.js | 4 +- .../core}/tests/renderStatsCard.test.js | 4 +- .../tests/renderTopLanguagesCard.test.js | 4 +- .../core}/tests/renderWakatimeCard.test.js | 2 - .../core}/tests/retryer.test.js | 15 +- packages/core/tests/utils.js | 41 + packages/core/tsconfig.build.json | 9 + packages/core/tsconfig.json | 21 + packages/core/tsconfig.typecheck.json | 7 + packages/core/vitest.config.ts | 9 + pnpm-lock.yaml | 2582 +++++++---------- pnpm-workspace.yaml | 5 +- tsconfig.base.json | 9 + tsconfig.json | 11 +- turbo.json | 16 + vercel-preparation.sh | 1 - vitest.config.coverage.ts | 13 + vitest.config.ts | 11 + 159 files changed, 6515 insertions(+), 4424 deletions(-) create mode 100644 .github/workflows/release.yml delete mode 100644 apps/backend/_dot_vercel_copy/output/functions/api.func/router.js delete mode 100644 apps/backend/api-renamed/gist.js delete mode 100644 apps/backend/api-renamed/wakatime.js create mode 100644 apps/backend/index.js delete mode 100644 apps/backend/powered-by-vercel.svg create mode 100644 apps/backend/router.js delete mode 100644 apps/backend/src/cards/index.js delete mode 100644 apps/backend/src/common/envs.js delete mode 100644 apps/backend/src/common/index.js delete mode 100644 apps/backend/src/index.js delete mode 100644 apps/backend/tests/__snapshots__/wakatime.test.js.snap delete mode 100644 apps/backend/tests/_setup.private-instance.js delete mode 100644 apps/backend/tests/bench/calculateRank.bench.js delete mode 100644 apps/backend/tests/private-instance/api.test.js delete mode 100644 apps/backend/tests/private-instance/gist.test.js delete mode 100644 apps/backend/tests/private-instance/pin.test.js delete mode 100644 apps/backend/tests/private-instance/top-langs.test.js create mode 100644 apps/backend/tests/public-instance/__snapshots__/api.test.js.snap create mode 100644 apps/backend/tests/public-instance/__snapshots__/gist.test.js.snap create mode 100644 apps/backend/tests/public-instance/__snapshots__/pin.test.js.snap create mode 100644 apps/backend/tests/public-instance/__snapshots__/top-langs.test.js.snap create mode 100644 apps/backend/tests/public-instance/__snapshots__/wakatime.test.js.snap create mode 100644 apps/backend/tests/public-instance/wakatime.test.js delete mode 100644 apps/backend/tests/test-data/api-data.js delete mode 100644 apps/backend/tests/test-data/gist-data.js delete mode 100644 apps/backend/tests/test-data/langs-data.js delete mode 100644 apps/backend/tests/test-data/pin-data.js create mode 100644 apps/backend/tsconfig.typecheck.json create mode 100644 apps/frontend/tsconfig.typecheck.json rename {apps/backend => packages/core}/codecov.yml (100%) create mode 100644 packages/core/package.json rename {apps/backend => packages/core}/scripts/generate-langs-json.js (100%) rename {apps/backend => packages/core}/scripts/generate-theme-doc.js (96%) create mode 100644 packages/core/src/api/gist.js rename {apps/backend/api-renamed => packages/core/src/api}/index.js (71%) rename {apps/backend/api-renamed => packages/core/src/api}/pin.js (63%) rename {apps/backend/api-renamed => packages/core/src/api}/top-langs.js (64%) create mode 100644 packages/core/src/api/wakatime.js rename {apps/backend => packages/core}/src/calculateRank.js (100%) rename {apps/backend => packages/core}/src/cards/gist.js (100%) rename {apps/backend => packages/core}/src/cards/repo.js (100%) rename {apps/backend => packages/core}/src/cards/stats.js (99%) rename {apps/backend => packages/core}/src/cards/top-languages.js (99%) rename {apps/backend => packages/core}/src/cards/types.d.ts (88%) rename {apps/backend => packages/core}/src/cards/wakatime.js (99%) rename {apps/backend => packages/core}/src/common/Card.js (98%) rename {apps/backend => packages/core}/src/common/I18n.js (100%) rename {apps/backend => packages/core}/src/common/color.js (98%) create mode 100644 packages/core/src/common/config.js rename {apps/backend => packages/core}/src/common/constants.js (100%) rename {apps/backend => packages/core}/src/common/error.js (100%) rename {apps/backend => packages/core}/src/common/fmt.js (99%) rename {apps/backend => packages/core}/src/common/html.js (100%) rename {apps/backend => packages/core}/src/common/http.js (100%) rename {apps/backend => packages/core}/src/common/icons.js (100%) rename {apps/backend => packages/core}/src/common/languageColors.json (99%) rename {apps/backend => packages/core}/src/common/log.js (57%) rename {apps/backend => packages/core}/src/common/ops.js (100%) rename {apps/backend => packages/core}/src/common/render.js (99%) rename {apps/backend => packages/core}/src/common/retryer.js (83%) rename {apps/backend => packages/core}/src/fetchers/gist.js (94%) rename {apps/backend => packages/core}/src/fetchers/repo.js (97%) rename {apps/backend => packages/core}/src/fetchers/stats.js (91%) rename {apps/backend => packages/core}/src/fetchers/top-languages.js (93%) rename {apps/backend => packages/core}/src/fetchers/types.d.ts (89%) rename {apps/backend => packages/core}/src/fetchers/wakatime.js (100%) create mode 100644 packages/core/src/index.ts rename {apps/backend => packages/core/src}/themes/README.md (100%) rename apps/backend/themes/index.js => packages/core/src/themes/index.ts (97%) rename {apps/backend => packages/core}/src/translations.js (100%) rename {apps/backend => packages/core}/tests/__snapshots__/renderWakatimeCard.test.js.snap (91%) create mode 100644 packages/core/tests/_setup.js rename {apps/backend => packages/core}/tests/calculateRank.test.js (98%) rename {apps/backend => packages/core}/tests/card.test.js (99%) rename {apps/backend => packages/core}/tests/color.test.js (100%) rename {apps/backend => packages/core}/tests/fetchGist.test.js (98%) rename {apps/backend => packages/core}/tests/fetchRepo.test.js (98%) rename {apps/backend => packages/core}/tests/fetchStats.test.js (98%) rename {apps/backend => packages/core}/tests/fetchTopLanguages.test.js (99%) rename {apps/backend => packages/core}/tests/fetchWakatime.test.js (98%) rename {apps/backend => packages/core}/tests/flexLayout.test.js (100%) rename {apps/backend => packages/core}/tests/fmt.test.js (100%) rename {apps/backend => packages/core}/tests/html.test.js (100%) rename {apps/backend => packages/core}/tests/i18n.test.js (100%) rename {apps/backend => packages/core}/tests/ops.test.js (100%) rename {apps/backend => packages/core}/tests/render.test.js (95%) rename {apps/backend => packages/core}/tests/renderGistCard.test.js (98%) rename {apps/backend => packages/core}/tests/renderRepoCard.test.js (99%) rename {apps/backend => packages/core}/tests/renderStatsCard.test.js (99%) rename {apps/backend => packages/core}/tests/renderTopLanguagesCard.test.js (99%) rename {apps/backend => packages/core}/tests/renderWakatimeCard.test.js (98%) rename {apps/backend => packages/core}/tests/retryer.test.js (83%) create mode 100644 packages/core/tests/utils.js create mode 100644 packages/core/tsconfig.build.json create mode 100644 packages/core/tsconfig.json create mode 100644 packages/core/tsconfig.typecheck.json create mode 100644 packages/core/vitest.config.ts create mode 100644 turbo.json create mode 100644 vitest.config.coverage.ts create mode 100644 vitest.config.ts diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a304b2f9..002e4576 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -7,7 +7,8 @@ To set up the project GitHub-Stats-Extended locally, run the following commands: ```bash ./vercel-preparation.sh pnpm install -pnpm --filter frontend run build +pnpm run build:packages +pnpm run dev:frontend ``` The easiest way to run and test the project is to deploy it to Vercel as described in the [deployment guide](../docs/deploy.md). diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index 901d2f63..ba1ccbae 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -14,11 +14,11 @@ runs: steps: - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5 - name: Setup Node.js (via input) if: ${{ inputs.node-version }} - uses: actions/setup-node@v6 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: ${{ inputs.node-version }} cache: "pnpm" @@ -26,7 +26,7 @@ runs: - name: Setup Node.js (via .nvmrc) if: ${{ !inputs.node-version }} - uses: actions/setup-node@v6 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version-file: ".nvmrc" cache: "pnpm" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e0eeed90..6bb49064 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,9 +4,8 @@ updates: - package-ecosystem: npm directory: "/" schedule: - interval: cron - cronjob: every day at 5am - open-pull-requests-limit: 10 + interval: daily + open-pull-requests-limit: 20 commit-message: prefix: "build(deps)" prefix-development: "build(deps-dev)" @@ -20,9 +19,8 @@ updates: - package-ecosystem: github-actions directory: "/" schedule: - interval: cron - cronjob: every day at 5am - open-pull-requests-limit: 10 + interval: daily + open-pull-requests-limit: 20 commit-message: prefix: "ci(deps)" prefix-development: "ci(deps-dev)" @@ -33,9 +31,8 @@ updates: - package-ecosystem: devcontainers directory: "/" schedule: - interval: cron - cronjob: every day at 5am - open-pull-requests-limit: 10 + interval: daily + open-pull-requests-limit: 20 commit-message: prefix: "build(deps)" prefix-development: "build(deps-dev)" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67d0bad2..9f27b1fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run vercel-preparation.sh run: | @@ -43,14 +43,14 @@ jobs: with: node-version: ${{ matrix.node }} + - name: Build packages + run: pnpm run build:packages + - name: Build frontend - run: pnpm --filter frontend run build + run: pnpm run build:frontend - - name: Run frontend tests - run: pnpm --filter frontend run test - - - name: Run backend tests - run: pnpm --filter github-readme-stats run test + - name: Run tests + run: pnpm run test --silent frontend-test-e2e: name: Frontend E2E test @@ -64,21 +64,42 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Dependencies uses: ./.github/actions/install-dependencies - - name: Run vercel-preparation.sh - run: | - chmod +x ./vercel-preparation.sh - ./vercel-preparation.sh + - name: Build packages + run: pnpm run build:packages - name: Install Playwright Browsers run: pnpm exec playwright install --with-deps - name: Run Playwright tests - run: pnpm --filter frontend run test:e2e + run: pnpm --filter ./apps/frontend/ run test:e2e + + backend-test-e2e: + name: Backend E2E test + + runs-on: ubuntu-latest + + permissions: + contents: read + + continue-on-error: true + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + + - name: Build packages + run: pnpm run build:packages + + - name: Run backend end-to-end tests + run: pnpm --filter ./apps/backend/ run test:e2e code-checks: name: Code checks @@ -90,17 +111,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 - - # Heads up! - # - # 1. Execution of this script is needed to resolve `.vercel` folder from `apps/frontend/src/components/Card/SVG.js` - # 2. This scripts removes `apps/backend/node_modules` breaking ESLint’s module resolution. - # Dependency installation must occur after running ./vercel-preparation.sh. - - name: Run vercel-preparation.sh - run: | - chmod +x ./vercel-preparation.sh - ./vercel-preparation.sh + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Dependencies uses: ./.github/actions/install-dependencies diff --git a/.github/workflows/generate-theme-doc.yml b/.github/workflows/generate-theme-doc.yml index c985fded..92cce540 100644 --- a/.github/workflows/generate-theme-doc.yml +++ b/.github/workflows/generate-theme-doc.yml @@ -4,7 +4,7 @@ on: branches: - master paths: - - "apps/backend/themes/index.js" + - "packages/core/src/themes/index.js" workflow_dispatch: permissions: {} @@ -30,17 +30,17 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Dependencies uses: ./.github/actions/install-dependencies - name: Generate readme run: | - pnpm --filter github-readme-stats run theme-readme-gen + pnpm --filter ./packages/core/ run theme-readme-gen - name: Create Pull Request if themes README has changed - uses: peter-evans/create-pull-request@v8 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8 with: commit-message: "feat(backend): update themes README" branch: "update_themes_readme/patch" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..f8ba92bc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +name: Update version tags +on: + push: + branches-ignore: + - "**" + tags: + - "v*.*.*" + +permissions: {} + +jobs: + update-semver: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: haya14busa/action-update-semver@7d2c558640ea49e798d46539536190aff8c18715 # v1.5.1 diff --git a/.github/workflows/repeat-recent-requests.yml b/.github/workflows/repeat-recent-requests.yml index ebf23adf..6e4d6801 100644 --- a/.github/workflows/repeat-recent-requests.yml +++ b/.github/workflows/repeat-recent-requests.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Make Request id: myRequest - uses: fjogeleit/http-request-action@v2 + uses: fjogeleit/http-request-action@551353b829c3646756b2ec2b3694f819d7957495 # v2 with: url: "https://github-stats-extended.vercel.app/api/repeat-recent" method: "POST" diff --git a/.github/workflows/update-langs.yml b/.github/workflows/update-langs.yml index 8ead85e2..b720eecb 100644 --- a/.github/workflows/update-langs.yml +++ b/.github/workflows/update-langs.yml @@ -38,16 +38,16 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Dependencies uses: ./.github/actions/install-dependencies - name: Run update-languages-json.js script - run: pnpm --filter github-readme-stats run generate-langs-json + run: pnpm --filter ./packages/core/ run generate-langs-json - name: Create Pull Request if upstream language file is changed - uses: peter-evans/create-pull-request@v8 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8 with: commit-message: "feat(backend): update languages JSON" branch: "update_langs/patch" diff --git a/.gitignore b/.gitignore index bd17695b..2890613a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,8 @@ node_modules -.env.local -.env.development.local -.env.test.local -.env.production.local - # OS .DS_Store -# Logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* - # Project coverage @@ -26,11 +16,12 @@ apps/backend/vercel_token apps/backend-copy apps/frontend/.env -apps/frontend/src/backend -apps/frontend/build + +.turbo +build build-ts -tsconfig.tsbuildinfo +*.tsbuildinfo # IDE .idea/ diff --git a/.husky/pre-commit b/.husky/pre-commit index b4c15f46..e742101e 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,3 @@ pnpm lint-staged pnpm run lint -# TODO enable -# npm test +pnpm test diff --git a/apps/backend/_dot_vercel_copy/output/functions/api.func/router.js b/apps/backend/_dot_vercel_copy/output/functions/api.func/router.js deleted file mode 100644 index b9a98e1e..00000000 --- a/apps/backend/_dot_vercel_copy/output/functions/api.func/router.js +++ /dev/null @@ -1,76 +0,0 @@ -/* eslint-disable import-x/no-unresolved */ -import { default as authenticate } from "./api-renamed/authenticate.js"; -import { default as deleteUser } from "./api-renamed/delete-user.js"; -import { default as downgrade } from "./api-renamed/downgrade.js"; -import { default as gist } from "./api-renamed/gist.js"; -import { default as api } from "./api-renamed/index.js"; -import { default as pin } from "./api-renamed/pin.js"; -import { default as repeatRecent } from "./api-renamed/repeat-recent.js"; -import { default as patInfo } from "./api-renamed/status/pat-info.js"; -import { default as statusUp } from "./api-renamed/status/up.js"; -import { default as topLangs } from "./api-renamed/top-langs.js"; -import { default as userAccess } from "./api-renamed/user-access.js"; -import { default as wakatimeProxy } from "./api-renamed/wakatime-proxy.js"; -import { default as wakatime } from "./api-renamed/wakatime.js"; - -export default async (req, res) => { - // remaining code expects express.js-like request and response objects - res.send = function (data) { - if (typeof data === "object") { - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify(data)); - } else if (typeof data === "string") { - res.end(data); - } else { - res.end(String(data)); - } - }; - const url = new URL(req.url, "https://localhost"); - req.query = Object.fromEntries(url.searchParams.entries()); - - switch (url.pathname) { - case "/api": - await api(req, res); - break; - case "/api/gist": - await gist(req, res); - break; - case "/api/pin": - await pin(req, res); - break; - case "/api/top-langs": - await topLangs(req, res); - break; - case "/api/wakatime": - await wakatime(req, res); - break; - case "/api/wakatime-proxy": - await wakatimeProxy(req, res); - break; - case "/api/repeat-recent": - await repeatRecent(req, res); - break; - case "/api/status/pat-info": - await patInfo(req, res); - break; - case "/api/status/up": - await statusUp(req, res); - break; - case "/api/authenticate": - await authenticate(req, res); - break; - case "/api/delete-user": - await deleteUser(req, res); - break; - case "/api/user-access": - await userAccess(req, res); - break; - case "/api/downgrade": - await downgrade(req, res); - break; - default: - res.statusCode = 404; - res.end("Not Found"); - break; - } -}; diff --git a/apps/backend/api-renamed/authenticate.js b/apps/backend/api-renamed/authenticate.js index 18511a22..bcfaa3ec 100644 --- a/apps/backend/api-renamed/authenticate.js +++ b/apps/backend/api-renamed/authenticate.js @@ -1,4 +1,5 @@ -import { logger } from "../src/common/log.js"; +import { logger } from "@stats-organization/github-readme-stats-core"; + import { authenticate } from "../src/users.js"; /** diff --git a/apps/backend/api-renamed/delete-user.js b/apps/backend/api-renamed/delete-user.js index 83c073e9..f19c3672 100644 --- a/apps/backend/api-renamed/delete-user.js +++ b/apps/backend/api-renamed/delete-user.js @@ -1,5 +1,6 @@ +import { logger } from "@stats-organization/github-readme-stats-core"; + import { deleteUser } from "../src/common/database.js"; -import { logger } from "../src/common/log.js"; /** * @param {any} req The request. diff --git a/apps/backend/api-renamed/downgrade.js b/apps/backend/api-renamed/downgrade.js index 519d3f9f..e52600d7 100644 --- a/apps/backend/api-renamed/downgrade.js +++ b/apps/backend/api-renamed/downgrade.js @@ -1,7 +1,7 @@ +import { logger } from "@stats-organization/github-readme-stats-core"; import axios from "axios"; import { deleteUser, getUserAccessByKey } from "../src/common/database.js"; -import { logger } from "../src/common/log.js"; export default async (req, res) => { // We could optimize this method by doing both database operations in one statement, using "DELETE ... RETURNING ..." diff --git a/apps/backend/api-renamed/gist.js b/apps/backend/api-renamed/gist.js deleted file mode 100644 index 26e973ee..00000000 --- a/apps/backend/api-renamed/gist.js +++ /dev/null @@ -1,129 +0,0 @@ -// @ts-check - -import { renderGistCard } from "../src/cards/gist.js"; -import { guardAccess } from "../src/common/access.js"; -import { - CACHE_TTL, - resolveCacheSeconds, - setCacheHeaders, - setErrorCacheHeaders, -} from "../src/common/cache.js"; -import { storeRequest } from "../src/common/database.js"; -import { - MissingParamError, - retrieveSecondaryMessage, -} from "../src/common/error.js"; -import { parseBoolean } from "../src/common/ops.js"; -import { renderError } from "../src/common/render.js"; -import { fetchGist } from "../src/fetchers/gist.js"; -import { isLocaleAvailable } from "../src/translations.js"; - -// @ts-ignore -export default async (req, res) => { - const { - id, - title_color, - icon_color, - text_color, - bg_color, - theme, - cache_seconds, - locale, - border_radius, - border_color, - show_owner, - hide_border, - } = req.query; - - res.setHeader("Content-Type", "image/svg+xml"); - - const access = guardAccess({ - res, - id, - type: "gist", - colors: { - title_color, - text_color, - bg_color, - border_color, - theme, - }, - }); - if (!access.isPassed) { - return access.result; - } - - if (locale && !isLocaleAvailable(locale)) { - return res.send( - renderError({ - message: "Something went wrong", - secondaryMessage: "Language not found", - renderOptions: { - title_color, - text_color, - bg_color, - border_color, - theme, - }, - }), - ); - } - - try { - await storeRequest(req); - const gistData = await fetchGist(id); - const cacheSeconds = resolveCacheSeconds({ - requested: parseInt(cache_seconds, 10), - def: CACHE_TTL.GIST_CARD.DEFAULT, - min: CACHE_TTL.GIST_CARD.MIN, - max: CACHE_TTL.GIST_CARD.MAX, - }); - - setCacheHeaders(res, cacheSeconds); - - return res.send( - renderGistCard(gistData, { - title_color, - icon_color, - text_color, - bg_color, - theme, - border_radius, - border_color, - locale: locale ? locale.toLowerCase() : null, - show_owner: parseBoolean(show_owner), - hide_border: parseBoolean(hide_border), - }), - ); - } catch (err) { - setErrorCacheHeaders(res); - if (err instanceof Error) { - return res.send( - renderError({ - message: err.message, - secondaryMessage: retrieveSecondaryMessage(err), - renderOptions: { - title_color, - text_color, - bg_color, - border_color, - theme, - show_repo_link: !(err instanceof MissingParamError), - }, - }), - ); - } - return res.send( - renderError({ - message: "An unknown error occurred", - renderOptions: { - title_color, - text_color, - bg_color, - border_color, - theme, - }, - }), - ); - } -}; diff --git a/apps/backend/api-renamed/status/pat-info.js b/apps/backend/api-renamed/status/pat-info.js index 998d7cbc..5725fc12 100644 --- a/apps/backend/api-renamed/status/pat-info.js +++ b/apps/backend/api-renamed/status/pat-info.js @@ -6,10 +6,12 @@ * * @description This function is currently rate limited to 1 request per 3 minutes. */ - -import { request } from "../../src/common/http.js"; -import { logger } from "../../src/common/log.js"; -import { dateDiff } from "../../src/common/ops.js"; +import { + dateDiff, + getConfig, + logger, + request, +} from "@stats-organization/github-readme-stats-core"; export const RATE_LIMIT_SECONDS = 60 * 3; // 1 request per 3 minutes @@ -39,7 +41,7 @@ const uptimeFetcher = (variables, token) => { }; const getAllPATs = () => { - return Object.keys(process.env).filter((key) => /PAT_\d*$/.exec(key)); + return getConfig().pats; }; /** @@ -61,7 +63,7 @@ const getPATInfo = async (fetcher, variables) => { for (const pat of PATs) { try { - const response = await fetcher(variables, process.env[pat]); + const response = await fetcher(variables, pat.value); const errors = response.data.errors; const hasErrors = Boolean(errors); const errorType = errors?.[0]?.type; @@ -71,7 +73,7 @@ const getPATInfo = async (fetcher, variables) => { // Store PATs with errors. if (hasErrors && errorType !== "RATE_LIMITED") { - details[pat] = { + details[pat.name] = { status: "error", error: { type: errors[0].type, @@ -82,13 +84,13 @@ const getPATInfo = async (fetcher, variables) => { } else if (isRateLimited) { const date1 = new Date(); const date2 = new Date(response.data?.data?.rateLimit?.resetAt); - details[pat] = { + details[pat.name] = { status: "exhausted", remaining: 0, resetIn: dateDiff(date2, date1) + " minutes", }; } else { - details[pat] = { + details[pat.name] = { status: "valid", remaining: response.data.data.rateLimit.remaining, }; @@ -97,11 +99,11 @@ const getPATInfo = async (fetcher, variables) => { // Store the PAT if it is expired. const errorMessage = err.response?.data?.message?.toLowerCase(); if (errorMessage === "bad credentials") { - details[pat] = { + details[pat.name] = { status: "expired", }; } else if (errorMessage === "sorry. your account was suspended.") { - details[pat] = { + details[pat.name] = { status: "suspended", }; } else { diff --git a/apps/backend/api-renamed/status/up.js b/apps/backend/api-renamed/status/up.js index 3afe5853..66162b67 100644 --- a/apps/backend/api-renamed/status/up.js +++ b/apps/backend/api-renamed/status/up.js @@ -7,9 +7,11 @@ * @description This function is currently rate limited to 1 request per 3 minutes. */ -import { request } from "../../src/common/http.js"; -import { logger } from "../../src/common/log.js"; -import { default as retryer } from "../../src/common/retryer.js"; +import { + logger, + request, + retryer, +} from "@stats-organization/github-readme-stats-core"; export const RATE_LIMIT_SECONDS = 60 * 3; // 1 request per 3 minutes @@ -87,7 +89,7 @@ export default async (req, res) => { try { let PATsValid = true; try { - await retryer(uptimeFetcher, null, {}); + await retryer(uptimeFetcher, {}); } catch (err) { // Resolve eslint no-unused-vars err; diff --git a/apps/backend/api-renamed/user-access.js b/apps/backend/api-renamed/user-access.js index 218ceac0..0e868216 100644 --- a/apps/backend/api-renamed/user-access.js +++ b/apps/backend/api-renamed/user-access.js @@ -1,5 +1,6 @@ +import { logger } from "@stats-organization/github-readme-stats-core"; + import { getUserAccessByKey } from "../src/common/database.js"; -import { logger } from "../src/common/log.js"; /** * @param {any} req The request. diff --git a/apps/backend/api-renamed/wakatime-proxy.js b/apps/backend/api-renamed/wakatime-proxy.js index 34dc4611..d370ed0e 100644 --- a/apps/backend/api-renamed/wakatime-proxy.js +++ b/apps/backend/api-renamed/wakatime-proxy.js @@ -1,5 +1,7 @@ -import { logger } from "../src/common/log.js"; -import { fetchWakatimeStats } from "../src/fetchers/wakatime.js"; +import { + fetchWakatimeStats, + logger, +} from "@stats-organization/github-readme-stats-core"; /** * @param {any} req The request. diff --git a/apps/backend/api-renamed/wakatime.js b/apps/backend/api-renamed/wakatime.js deleted file mode 100644 index dd1c3ead..00000000 --- a/apps/backend/api-renamed/wakatime.js +++ /dev/null @@ -1,148 +0,0 @@ -// @ts-check - -import { renderWakatimeCard } from "../src/cards/wakatime.js"; -import { guardAccess } from "../src/common/access.js"; -import { - CACHE_TTL, - resolveCacheSeconds, - setCacheHeaders, - setErrorCacheHeaders, -} from "../src/common/cache.js"; -import { storeRequest } from "../src/common/database.js"; -import { - MissingParamError, - retrieveSecondaryMessage, -} from "../src/common/error.js"; -import { parseArray, parseBoolean } from "../src/common/ops.js"; -import { renderError } from "../src/common/render.js"; -import { fetchWakatimeStats } from "../src/fetchers/wakatime.js"; -import { isLocaleAvailable } from "../src/translations.js"; - -// @ts-ignore -export default async (req, res) => { - const { - username, - title_color, - icon_color, - hide_border, - card_width, - line_height, - text_color, - bg_color, - theme, - cache_seconds, - hide_title, - hide_progress, - custom_title, - locale, - layout, - langs_count, - hide, - api_domain, - border_radius, - border_color, - display_format, - disable_animations, - } = req.query; - - res.setHeader("Content-Type", "image/svg+xml"); - - const access = guardAccess({ - res, - id: username, - type: "wakatime", - colors: { - title_color, - text_color, - bg_color, - border_color, - theme, - }, - }); - if (!access.isPassed) { - return access.result; - } - - if (locale && !isLocaleAvailable(locale)) { - return res.send( - renderError({ - message: "Something went wrong", - secondaryMessage: "Language not found", - renderOptions: { - title_color, - text_color, - bg_color, - border_color, - theme, - }, - }), - ); - } - - try { - await storeRequest(req); - const stats = await fetchWakatimeStats({ username, api_domain }); - const cacheSeconds = resolveCacheSeconds({ - requested: parseInt(cache_seconds, 10), - def: CACHE_TTL.WAKATIME_CARD.DEFAULT, - min: CACHE_TTL.WAKATIME_CARD.MIN, - max: CACHE_TTL.WAKATIME_CARD.MAX, - }); - - setCacheHeaders(res, cacheSeconds); - - return res.send( - renderWakatimeCard(stats, { - custom_title, - hide_title: parseBoolean(hide_title), - hide_border: parseBoolean(hide_border), - card_width: parseInt(card_width, 10), - hide: parseArray(hide), - line_height, - title_color, - icon_color, - text_color, - bg_color, - theme, - hide_progress, - border_radius, - border_color, - locale: locale ? locale.toLowerCase() : null, - layout, - langs_count, - display_format, - disable_animations: parseBoolean(disable_animations), - }), - ); - } catch (err) { - setErrorCacheHeaders(res); - if (err instanceof Error) { - return res.send( - renderError({ - message: err.message, - secondaryMessage: retrieveSecondaryMessage(err), - renderOptions: { - title_color, - text_color, - bg_color, - border_color, - theme, - show_repo_link: !(err instanceof MissingParamError), - }, - }), - ); - } - return res.send( - renderError({ - message: "An unknown error occurred", - renderOptions: { - title_color, - text_color, - bg_color, - border_color, - theme, - }, - }), - ); - } -}; diff --git a/apps/backend/express.js b/apps/backend/express.js index 35a24a36..099c7254 100644 --- a/apps/backend/express.js +++ b/apps/backend/express.js @@ -1,21 +1,10 @@ import express from "express"; -import gistCard from "./api-renamed/gist.js"; -import statsCard from "./api-renamed/index.js"; -import repoCard from "./api-renamed/pin.js"; -import langCard from "./api-renamed/top-langs.js"; -import wakatimeCard from "./api-renamed/wakatime.js"; +import router from "./router.js"; const app = express(); -const router = express.Router(); -router.get("/", statsCard); -router.get("/pin", repoCard); -router.get("/top-langs", langCard); -router.get("/wakatime", wakatimeCard); -router.get("/gist", gistCard); - -app.use("/api", router); +app.use(router); const port = process.env.PORT || process.env.port || 9000; app.listen(port, "0.0.0.0", () => { diff --git a/apps/backend/index.js b/apps/backend/index.js new file mode 100644 index 00000000..5dce81e4 --- /dev/null +++ b/apps/backend/index.js @@ -0,0 +1 @@ +export { default as router } from "./router.js"; diff --git a/apps/backend/package.json b/apps/backend/package.json index e6ef6ed0..8d5a8adf 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,52 +1,29 @@ { - "name": "github-readme-stats", - "version": "1.0.0", - "description": "Dynamically generate stats for your GitHub readme", - "keywords": [ - "github-readme-stats", - "readme-stats", - "cards", - "card-generator" - ], - "main": "src/index.js", + "name": "@stats-organization/github-readme-stats-backend", "type": "module", - "homepage": "https://github-stats-extended.vercel.app/frontend", - "bugs": { - "url": "https://github.com/stats-organization/github-stats-extended/issues" - }, - "repository": { - "type": "git", - "url": "https://github.com/stats-organization/github-stats-extended.git" + "license": "MIT", + "engines": { + "node": "24.x" }, + "private": true, + "main": "index.js", "scripts": { "test": "vitest", "test:update:snapshot": "vitest -u", "test:e2e": "vitest --config vitest.config.e2e.ts", - "theme-readme-gen": "node scripts/generate-theme-doc", - "generate-langs-json": "node scripts/generate-langs-json", - "bench": "vitest bench --run --config vitest.config.bench.ts" + "bench": "vitest bench --run --config vitest.config.bench.ts", + "lint": "eslint", + "typecheck": "tsc -p tsconfig.typecheck.json" }, - "author": "Anurag Hazra", - "license": "MIT", "devDependencies": { - "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", - "@uppercod/css-to-object": "^1.1.1", - "@vitest/coverage-v8": "catalog:default", - "axios-mock-adapter": "^2.1.0", - "express": "^5.2.1", - "js-yaml": "^4.1.1", + "axios-mock-adapter": "2.1.0", + "express": "5.2.1", "jsdom": "28.1.0", "vitest": "catalog:default" }, "dependencies": { "axios": "^1.13.5", - "emoji-name-map": "^2.0.3", - "github-username-regex": "^1.0.0", - "pg": "^8.18.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": "24.x" + "@stats-organization/github-readme-stats-core": "workspace:^", + "pg": "^8.18.0" } } diff --git a/apps/backend/powered-by-vercel.svg b/apps/backend/powered-by-vercel.svg deleted file mode 100644 index 87782868..00000000 --- a/apps/backend/powered-by-vercel.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/backend/router.js b/apps/backend/router.js new file mode 100644 index 00000000..3da27ad5 --- /dev/null +++ b/apps/backend/router.js @@ -0,0 +1,225 @@ +import { + api, + gist, + pin, + topLangs, + wakatime, +} from "@stats-organization/github-readme-stats-core"; + +import { default as authenticate } from "./api-renamed/authenticate.js"; +import { default as deleteUser } from "./api-renamed/delete-user.js"; +import { default as downgrade } from "./api-renamed/downgrade.js"; +import { default as repeatRecent } from "./api-renamed/repeat-recent.js"; +import { default as patInfo } from "./api-renamed/status/pat-info.js"; +import { default as statusUp } from "./api-renamed/status/up.js"; +import { default as userAccess } from "./api-renamed/user-access.js"; +import { default as wakatimeProxy } from "./api-renamed/wakatime-proxy.js"; +import { guardAccess } from "./src/common/access.js"; +import { + CACHE_TTL, + resolveCacheSeconds, + setCacheHeaders, + setErrorCacheHeaders, +} from "./src/common/cache.js"; +import { getUserAccessByName, storeRequest } from "./src/common/database.js"; + +const getGuardResult = (query, type, id) => { + const access = guardAccess({ + id, + type, + colors: { + title_color: query.title_color, + text_color: query.text_color, + bg_color: query.bg_color, + border_color: query.border_color, + theme: query.theme, + }, + }); + + if (access.isPassed) { + return null; + } + + return { + status: "error - permanent", + content: access.result, + }; +}; + +const getUserPat = async (username) => { + if (!username) { + return null; + } + + const userAccess = await getUserAccessByName(username); + if (!userAccess?.token) { + return null; + } + + return userAccess.token; +}; + +export default async (req, res) => { + const url = new URL(req.url, "https://localhost"); + if (res.send === undefined) { + // remaining code expects express.js-like request and response objects + res.send = function (data) { + if (typeof data === "object") { + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify(data)); + } else if (typeof data === "string") { + res.end(data); + } else { + res.end(String(data)); + } + }; + req.query = Object.fromEntries(url.searchParams.entries()); + } + + let result; + + switch (url.pathname) { + case "/api": { + result = getGuardResult(req.query, "username", req.query.username); + if (!result) { + const userPat = await getUserPat(req.query.username); + result = await api(req.query, userPat); + } + if (result.status === "error - temporary") { + setErrorCacheHeaders(res); + } else { + const cacheSeconds = resolveCacheSeconds({ + requested: parseInt(req.query.cache_seconds, 10), + def: CACHE_TTL.STATS_CARD.DEFAULT, + min: CACHE_TTL.STATS_CARD.MIN, + max: CACHE_TTL.STATS_CARD.MAX, + }); + setCacheHeaders(res, cacheSeconds); + } + res.setHeader("Content-Type", "image/svg+xml"); + res.end(result.content); + if (result.status !== "error - permanent") { + await storeRequest(req); + } + break; + } + case "/api/gist": + result = + getGuardResult(req.query, "gist", req.query.id) ?? + (await gist(req.query)); + if (result.status === "error - temporary") { + setErrorCacheHeaders(res); + } else { + const cacheSeconds = resolveCacheSeconds({ + requested: parseInt(req.query.cache_seconds, 10), + def: CACHE_TTL.GIST_CARD.DEFAULT, + min: CACHE_TTL.GIST_CARD.MIN, + max: CACHE_TTL.GIST_CARD.MAX, + }); + setCacheHeaders(res, cacheSeconds); + } + res.setHeader("Content-Type", "image/svg+xml"); + res.end(result.content); + if (result.status !== "error - permanent") { + await storeRequest(req); + } + break; + case "/api/pin": { + result = getGuardResult(req.query, "username", req.query.username); + if (!result) { + const userPat = await getUserPat(req.query.username); + result = await pin(req.query, userPat); + } + if (result.status === "error - temporary") { + setErrorCacheHeaders(res); + } else { + const cacheSeconds = resolveCacheSeconds({ + requested: parseInt(req.query.cache_seconds, 10), + def: CACHE_TTL.PIN_CARD.DEFAULT, + min: CACHE_TTL.PIN_CARD.MIN, + max: CACHE_TTL.PIN_CARD.MAX, + }); + setCacheHeaders(res, cacheSeconds); + } + res.setHeader("Content-Type", "image/svg+xml"); + res.end(result.content); + if (result.status !== "error - permanent") { + await storeRequest(req); + } + break; + } + case "/api/top-langs": { + result = getGuardResult(req.query, "username", req.query.username); + if (!result) { + const userPat = await getUserPat(req.query.username); + result = await topLangs(req.query, userPat); + } + if (result.status === "error - temporary") { + setErrorCacheHeaders(res); + } else { + const cacheSeconds = resolveCacheSeconds({ + requested: parseInt(req.query.cache_seconds, 10), + def: CACHE_TTL.TOP_LANGS_CARD.DEFAULT, + min: CACHE_TTL.TOP_LANGS_CARD.MIN, + max: CACHE_TTL.TOP_LANGS_CARD.MAX, + }); + setCacheHeaders(res, cacheSeconds); + } + res.setHeader("Content-Type", "image/svg+xml"); + res.end(result.content); + if (result.status !== "error - permanent") { + await storeRequest(req); + } + break; + } + case "/api/wakatime": + result = + getGuardResult(req.query, "wakatime", req.query.username) ?? + (await wakatime(req.query)); + if (result.status === "error - temporary") { + setErrorCacheHeaders(res); + } else { + const cacheSeconds = resolveCacheSeconds({ + requested: parseInt(req.query.cache_seconds, 10), + def: CACHE_TTL.WAKATIME_CARD.DEFAULT, + min: CACHE_TTL.WAKATIME_CARD.MIN, + max: CACHE_TTL.WAKATIME_CARD.MAX, + }); + setCacheHeaders(res, cacheSeconds); + } + res.setHeader("Content-Type", "image/svg+xml"); + res.end(result.content); + if (result.status !== "error - permanent") { + await storeRequest(req); + } + break; + case "/api/wakatime-proxy": + await wakatimeProxy(req, res); + break; + case "/api/repeat-recent": + await repeatRecent(req, res); + break; + case "/api/status/pat-info": + await patInfo(req, res); + break; + case "/api/status/up": + await statusUp(req, res); + break; + case "/api/authenticate": + await authenticate(req, res); + break; + case "/api/delete-user": + await deleteUser(req, res); + break; + case "/api/user-access": + await userAccess(req, res); + break; + case "/api/downgrade": + await downgrade(req, res); + break; + default: + res.statusCode = 404; + res.end("Not Found"); + break; + } +}; diff --git a/apps/backend/src/cards/index.js b/apps/backend/src/cards/index.js deleted file mode 100644 index 5ca3a97a..00000000 --- a/apps/backend/src/cards/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export { renderRepoCard } from "./repo.js"; -export { renderStatsCard } from "./stats.js"; -export { renderTopLanguages } from "./top-languages.js"; -export { renderWakatimeCard } from "./wakatime.js"; diff --git a/apps/backend/src/common/access.js b/apps/backend/src/common/access.js index 05a5dbe5..09cf5b30 100644 --- a/apps/backend/src/common/access.js +++ b/apps/backend/src/common/access.js @@ -1,8 +1,11 @@ // @ts-check +import { + getConfig, + renderError, +} from "@stats-organization/github-readme-stats-core"; + import { blacklist } from "./blacklist.js"; -import { gistWhitelist, whitelist } from "./envs.js"; -import { renderError } from "./render.js"; const NOT_WHITELISTED_USERNAME_MESSAGE = "This username is not whitelisted"; const NOT_WHITELISTED_GIST_MESSAGE = "This gist ID is not whitelisted"; @@ -12,36 +15,35 @@ const BLACKLISTED_MESSAGE = "This username is blacklisted"; * Guards access using whitelist/blacklist. * * @param {Object} args The parameters object. - * @param {any} args.res The response object. * @param {string} args.id Resource identifier (username or gist id). * @param {"username"|"gist"|"wakatime"} args.type The type of identifier. * @param {{ title_color?: string, text_color?: string, bg_color?: string, border_color?: string, theme?: string }} args.colors Color options for the error card. * @returns {{ isPassed: boolean, result?: any }} The result object indicating success or failure. */ -const guardAccess = ({ res, id, type, colors }) => { +const guardAccess = ({ id, type, colors }) => { if (!["username", "gist", "wakatime"].includes(type)) { throw new Error( 'Invalid type. Expected "username", "gist", or "wakatime".', ); } - const currentWhitelist = type === "gist" ? gistWhitelist : whitelist; + const config = getConfig(); + const currentWhitelist = + type === "gist" ? config.gistWhitelist : config.whitelist; const notWhitelistedMsg = type === "gist" ? NOT_WHITELISTED_GIST_MESSAGE : NOT_WHITELISTED_USERNAME_MESSAGE; if (Array.isArray(currentWhitelist) && !currentWhitelist.includes(id)) { - const result = res.send( - renderError({ - message: notWhitelistedMsg, - secondaryMessage: "Please deploy your own instance", - renderOptions: { - ...colors, - show_repo_link: false, - }, - }), - ); + const result = renderError({ + message: notWhitelistedMsg, + secondaryMessage: "Please deploy your own instance", + renderOptions: { + ...colors, + show_repo_link: false, + }, + }); return { isPassed: false, result }; } @@ -50,16 +52,14 @@ const guardAccess = ({ res, id, type, colors }) => { currentWhitelist === undefined && blacklist.includes(id) ) { - const result = res.send( - renderError({ - message: BLACKLISTED_MESSAGE, - secondaryMessage: "Please deploy your own instance", - renderOptions: { - ...colors, - show_repo_link: false, - }, - }), - ); + const result = renderError({ + message: BLACKLISTED_MESSAGE, + secondaryMessage: "Please deploy your own instance", + renderOptions: { + ...colors, + show_repo_link: false, + }, + }); return { isPassed: false, result }; } diff --git a/apps/backend/src/common/cache.js b/apps/backend/src/common/cache.js index 6fe2ea59..f71f300b 100644 --- a/apps/backend/src/common/cache.js +++ b/apps/backend/src/common/cache.js @@ -1,6 +1,6 @@ // @ts-check -import { clampValue } from "./ops.js"; +import { clampValue } from "@stats-organization/github-readme-stats-core"; const MIN = 60; const HOUR = 60 * MIN; @@ -106,7 +106,7 @@ const disableCaching = (res) => { * @param {number} cacheSeconds The cache seconds to set in the headers. */ const setCacheHeaders = (res, cacheSeconds) => { - if (cacheSeconds < 1 || process.env.NODE_ENV === "development") { + if (cacheSeconds < 1) { disableCaching(res); return; } @@ -128,10 +128,7 @@ const setErrorCacheHeaders = (res) => { const envCacheSeconds = process.env.CACHE_SECONDS ? parseInt(process.env.CACHE_SECONDS, 10) : NaN; - if ( - (!isNaN(envCacheSeconds) && envCacheSeconds < 1) || - process.env.NODE_ENV === "development" - ) { + if (!isNaN(envCacheSeconds) && envCacheSeconds < 1) { disableCaching(res); return; } diff --git a/apps/backend/src/common/database.js b/apps/backend/src/common/database.js index e76c3109..8bf2e83f 100644 --- a/apps/backend/src/common/database.js +++ b/apps/backend/src/common/database.js @@ -1,14 +1,10 @@ -/** - * In the browser this has to be mocked to avoid runtime errors - * @see apps/frontend/vite.config.ts - */ -import { Pool } from "pg"; - -export const pool = process.env.POSTGRES_URL - ? new Pool({ - connectionString: process.env.POSTGRES_URL, - }) - : null; +export let pool = null; +if (process.env.POSTGRES_URL) { + const { Pool } = await import("pg"); + pool = new Pool({ + connectionString: process.env.POSTGRES_URL, + }); +} /** * Creates all required tables if they do not exist. diff --git a/apps/backend/src/common/envs.js b/apps/backend/src/common/envs.js deleted file mode 100644 index 5f131966..00000000 --- a/apps/backend/src/common/envs.js +++ /dev/null @@ -1,15 +0,0 @@ -// @ts-check - -const whitelist = process.env.WHITELIST - ? process.env.WHITELIST.split(",") - : undefined; - -const gistWhitelist = process.env.GIST_WHITELIST - ? process.env.GIST_WHITELIST.split(",") - : undefined; - -const excludeRepositories = process.env.EXCLUDE_REPO - ? process.env.EXCLUDE_REPO.split(",") - : []; - -export { whitelist, gistWhitelist, excludeRepositories }; diff --git a/apps/backend/src/common/index.js b/apps/backend/src/common/index.js deleted file mode 100644 index db914b86..00000000 --- a/apps/backend/src/common/index.js +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-check - -export { blacklist } from "./blacklist.js"; -export { Card } from "./Card.js"; -export { I18n } from "./I18n.js"; -export { icons } from "./icons.js"; -export { retryer } from "./retryer.js"; -export { - ERROR_CARD_LENGTH, - renderError, - flexLayout, - measureText, -} from "./render.js"; diff --git a/apps/backend/src/index.js b/apps/backend/src/index.js deleted file mode 100644 index ca8d586d..00000000 --- a/apps/backend/src/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./common/index.js"; -export * from "./cards/index.js"; diff --git a/apps/backend/src/users.js b/apps/backend/src/users.js index 12e87457..3f40c8f8 100644 --- a/apps/backend/src/users.js +++ b/apps/backend/src/users.js @@ -80,6 +80,7 @@ async function githubAuthenticate(code, privateAccess) { return { userId, accessToken, needDowngrade }; } catch (err) { if (err.response) { + // eslint-disable-next-line preserve-caught-error throw new Error(`OAuth Error: ${err.response.status}`); } throw err; diff --git a/apps/backend/tests/__snapshots__/wakatime.test.js.snap b/apps/backend/tests/__snapshots__/wakatime.test.js.snap deleted file mode 100644 index 34f060a5..00000000 --- a/apps/backend/tests/__snapshots__/wakatime.test.js.snap +++ /dev/null @@ -1,115 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Test /api/wakatime > should render error if user data is not accessible 1`] = ` -" - - - - - - - - - - - - - WakaTime Stats - - - - - - - - - WakaTime user profile not public - - - - - - " -`; diff --git a/apps/backend/tests/_setup.private-instance.js b/apps/backend/tests/_setup.private-instance.js deleted file mode 100644 index f4f094e5..00000000 --- a/apps/backend/tests/_setup.private-instance.js +++ /dev/null @@ -1,2 +0,0 @@ -process.env.GIST_WHITELIST = "bbfce31e0217a3689c8d961a356cb10d"; -process.env.WHITELIST = "anuraghazra"; diff --git a/apps/backend/tests/api.test.js b/apps/backend/tests/api.test.js index 7aeef4ca..e086abb5 100644 --- a/apps/backend/tests/api.test.js +++ b/apps/backend/tests/api.test.js @@ -1,345 +1,182 @@ // @ts-check -import axios from "axios"; -import MockAdapter from "axios-mock-adapter"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; -import api from "../api-renamed/index.js"; -import { calculateRank } from "../src/calculateRank.js"; -import { renderStatsCard } from "../src/cards/stats.js"; -import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; -import { renderError } from "../src/common/render.js"; +const mocks = vi.hoisted(() => ({ + api: vi.fn(), + storeRequest: vi.fn(), + getUserAccessByName: vi.fn(), + config: {}, +})); -import { data_stats, stats } from "./test-data/api-data.js"; - -stats.rank = calculateRank({ - all_commits: false, - commits: stats.totalCommits, - prs: stats.totalPRs, - reviews: stats.totalReviews, - issues: stats.totalIssues, - repos: 1, - stars: stats.totalStars, - followers: 0, +vi.mock("@stats-organization/github-readme-stats-core", async () => { + const { mockCore } = await import("./utils.js"); + return mockCore({ api: mocks.api, getConfig: () => mocks.config }); }); -const error = { - errors: [ - { - type: "NOT_FOUND", - path: ["user"], - locations: [], - message: "Could not fetch user", - }, - ], -}; +vi.mock("../src/common/database.js", () => ({ + storeRequest: mocks.storeRequest, + getUserAccessByName: mocks.getUserAccessByName, +})); -const mock = new MockAdapter(axios); +import router from "../router.js"; +import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; -// @ts-ignore -const faker = (query, data) => { - const req = { - query: { - username: "anuraghazra", - ...query, - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").replyOnce(200, data); +const createRequest = (search = "") => ({ + headers: {}, + url: `/api?${search}`, +}); - return { req, res }; -}; +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); + +const defaultCacheHeader = + `max-age=${CACHE_TTL.STATS_CARD.DEFAULT}, ` + + `s-maxage=${CACHE_TTL.STATS_CARD.DEFAULT}, ` + + `stale-while-revalidate=${DURATIONS.ONE_DAY}`; + +const errorCacheHeader = + `max-age=${CACHE_TTL.ERROR}, ` + + `s-maxage=${CACHE_TTL.ERROR}, ` + + `stale-while-revalidate=${DURATIONS.ONE_DAY}`; beforeEach(() => { - process.env.CACHE_SECONDS = undefined; + mocks.api.mockReset(); + mocks.storeRequest.mockReset().mockResolvedValue(undefined); + mocks.getUserAccessByName.mockReset().mockResolvedValue(null); + mocks.config = {}; + // CACHE_SECONDS is not set here, this is just to safeguard against CACHE_SECONDS being set externally + delete process.env.CACHE_SECONDS; }); -afterEach(() => { - mock.reset(); -}); +describe("Test /api backend routing", () => { + it("happy path should pass query params and user PAT, respond with stats content and persist request", async () => { + mocks.getUserAccessByName.mockResolvedValue({ token: "user-pat" }); + mocks.api.mockResolvedValue({ + status: "success", + content: "mock-stats-svg", + }); -describe("Test /api/", () => { - it("should test the request", async () => { - const { req, res } = faker({}, data_stats); - - await api(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderStatsCard(stats, { ...req.query }), + const req = createRequest( + "username=anuraghazra&theme=dark&hide=issues,prs,contribs", ); - }); + const res = createResponse(); - it("should render error card on error", async () => { - const { req, res } = faker({}, error); + await router(req, res); - await api(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: error.errors[0].message, - secondaryMessage: - "Make sure the provided username is not an organization", - }), - ); - }); - - it("should render error card in same theme as requested card", async () => { - const { req, res } = faker({ theme: "merko" }, error); - - await api(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: error.errors[0].message, - secondaryMessage: - "Make sure the provided username is not an organization", - renderOptions: { theme: "merko" }, - }), - ); - }); - - it("should get the query options", async () => { - const { req, res } = faker( + expect(mocks.getUserAccessByName).toHaveBeenCalledWith("anuraghazra"); + expect(mocks.api).toHaveBeenCalledWith( { username: "anuraghazra", + theme: "dark", hide: "issues,prs,contribs", - show_icons: true, - hide_border: true, - line_height: 100, - title_color: "fff", - icon_color: "fff", - text_color: "fff", - bg_color: "fff", }, - data_stats, + "user-pat", ); - - await api(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderStatsCard(stats, { - hide: ["issues", "prs", "contribs"], - show_icons: true, - hide_border: true, - line_height: 100, - title_color: "fff", - icon_color: "fff", - text_color: "fff", - bg_color: "fff", - }), - ); - }); - - it("should have proper cache", async () => { - const { req, res } = faker({}, data_stats); - - await api(req, res); - + expect(req.query).toEqual({ + username: "anuraghazra", + theme: "dark", + hide: "issues,prs,contribs", + }); expect(res.setHeader.mock.calls).toEqual([ + ["Cache-Control", defaultCacheHeader], ["Content-Type", "image/svg+xml"], - [ - "Cache-Control", - `max-age=${CACHE_TTL.STATS_CARD.DEFAULT}, ` + - `s-maxage=${CACHE_TTL.STATS_CARD.DEFAULT}, ` + - `stale-while-revalidate=${DURATIONS.ONE_DAY}`, - ], ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-stats-svg"); + expect(mocks.storeRequest).toHaveBeenCalledExactlyOnceWith(req); }); - it("should set proper cache", async () => { - const cache_seconds = DURATIONS.TWELVE_HOURS; - const { req, res } = faker({ cache_seconds }, data_stats); - await api(req, res); + it("should use the shorter error cache for temporary stats errors", async () => { + mocks.api.mockResolvedValue({ + status: "error - temporary", + content: "temporary-error-svg", + }); - expect(res.setHeader.mock.calls).toEqual([ - ["Content-Type", "image/svg+xml"], - [ - "Cache-Control", - `max-age=${cache_seconds}, ` + - `s-maxage=${cache_seconds}, ` + - `stale-while-revalidate=${DURATIONS.ONE_DAY}`, - ], - ]); - }); + const req = createRequest("username=anuraghazra"); + const res = createResponse(); - it("should set shorter cache when error", async () => { - const { req, res } = faker({}, error); - await api(req, res); + await router(req, res); - expect(res.setHeader.mock.calls).toEqual([ - ["Content-Type", "image/svg+xml"], - [ - "Cache-Control", - `max-age=${CACHE_TTL.ERROR}, ` + - `s-maxage=${CACHE_TTL.ERROR}, ` + - `stale-while-revalidate=${DURATIONS.ONE_DAY}`, - ], - ]); - }); - - it("should properly set cache using CACHE_SECONDS env variable", async () => { - const cacheSeconds = "10000"; - process.env.CACHE_SECONDS = cacheSeconds; - - const { req, res } = faker({}, data_stats); - await api(req, res); - - expect(res.setHeader.mock.calls).toEqual([ - ["Content-Type", "image/svg+xml"], - [ - "Cache-Control", - `max-age=${cacheSeconds}, ` + - `s-maxage=${cacheSeconds}, ` + - `stale-while-revalidate=${DURATIONS.ONE_DAY}`, - ], - ]); - }); - - it("should disable cache when CACHE_SECONDS is set to 0", async () => { - process.env.CACHE_SECONDS = "0"; - - const { req, res } = faker({}, data_stats); - await api(req, res); - - expect(res.setHeader.mock.calls).toEqual([ - ["Content-Type", "image/svg+xml"], - [ - "Cache-Control", - "no-cache, no-store, must-revalidate, max-age=0, s-maxage=0", - ], - ["Pragma", "no-cache"], - ["Expires", "0"], - ]); - }); - - it("should set proper cache with clamped values", async () => { - { - let { req, res } = faker({ cache_seconds: 200_000 }, data_stats); - await api(req, res); - - expect(res.setHeader.mock.calls).toEqual([ - ["Content-Type", "image/svg+xml"], - [ - "Cache-Control", - `max-age=${CACHE_TTL.STATS_CARD.MAX}, ` + - `s-maxage=${CACHE_TTL.STATS_CARD.MAX}, ` + - `stale-while-revalidate=${DURATIONS.ONE_DAY}`, - ], - ]); - } - - // note i'm using block scoped vars - { - let { req, res } = faker({ cache_seconds: 0 }, data_stats); - await api(req, res); - - expect(res.setHeader.mock.calls).toEqual([ - ["Content-Type", "image/svg+xml"], - [ - "Cache-Control", - `max-age=${CACHE_TTL.STATS_CARD.MIN}, ` + - `s-maxage=${CACHE_TTL.STATS_CARD.MIN}, ` + - `stale-while-revalidate=${DURATIONS.ONE_DAY}`, - ], - ]); - } - - { - let { req, res } = faker({ cache_seconds: -10_000 }, data_stats); - await api(req, res); - - expect(res.setHeader.mock.calls).toEqual([ - ["Content-Type", "image/svg+xml"], - [ - "Cache-Control", - `max-age=${CACHE_TTL.STATS_CARD.MIN}, ` + - `s-maxage=${CACHE_TTL.STATS_CARD.MIN}, ` + - `stale-while-revalidate=${DURATIONS.ONE_DAY}`, - ], - ]); - } - }); - - it("should allow changing ring_color", async () => { - const { req, res } = faker( + expect(mocks.getUserAccessByName).toHaveBeenCalledWith("anuraghazra"); + expect(mocks.api).toHaveBeenCalledWith( { username: "anuraghazra", - hide: "issues,prs,contribs", - show_icons: true, - hide_border: true, - line_height: 100, - title_color: "fff", - ring_color: "0000ff", - icon_color: "fff", - text_color: "fff", - bg_color: "fff", }, - data_stats, - ); - - await api(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderStatsCard(stats, { - hide: ["issues", "prs", "contribs"], - show_icons: true, - hide_border: true, - line_height: 100, - title_color: "fff", - ring_color: "0000ff", - icon_color: "fff", - text_color: "fff", - bg_color: "fff", - }), + null, ); + expect(res.setHeader.mock.calls).toEqual([ + ["Cache-Control", errorCacheHeader], + ["Content-Type", "image/svg+xml"], + ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith("temporary-error-svg"); + expect(mocks.storeRequest).toHaveBeenCalledExactlyOnceWith(req); }); - it("should render error card when wrong locale is provided", async () => { - const { req, res } = faker({ locale: "asdf" }, data_stats); + it("should not persist permanent stats errors returned by core", async () => { + mocks.api.mockResolvedValue({ + status: "error - permanent", + content: "permanent-error-svg", + }); - await api(req, res); + const req = createRequest("username=anuraghazra"); + const res = createResponse(); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "Something went wrong", - secondaryMessage: "Language not found", - }), + await router(req, res); + + expect(mocks.getUserAccessByName).toHaveBeenCalledWith("anuraghazra"); + expect(mocks.api).toHaveBeenCalledWith( + { + username: "anuraghazra", + }, + null, ); + expect(res.setHeader.mock.calls).toEqual([ + ["Cache-Control", defaultCacheHeader], + ["Content-Type", "image/svg+xml"], + ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith("permanent-error-svg"); + expect(mocks.storeRequest).not.toHaveBeenCalled(); }); - it("should render error card when include_all_commits true and upstream API fails", async () => { - mock - .onGet( - "https://api.github.com/search/commits?per_page=1&q=author:anuraghazra", - ) - .reply(200, { error: "Some test error message" }); + it("should reject blacklisted usernames before calling core logic", async () => { + const req = createRequest("username=renovate-bot"); + const res = createResponse(); - const { req, res } = faker( - { username: "anuraghazra", include_all_commits: true }, - data_stats, - ); + await router(req, res); - await api(req, res); + expect(mocks.api).not.toHaveBeenCalled(); + expect(mocks.getUserAccessByName).not.toHaveBeenCalled(); + expect(res.setHeader.mock.calls).toEqual([ + ["Cache-Control", defaultCacheHeader], + ["Content-Type", "image/svg+xml"], + ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith( + "render-error:This username is blacklisted", + ); + expect(mocks.storeRequest).not.toHaveBeenCalled(); + }); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "Could not fetch data from GitHub REST API.", - secondaryMessage: "Please try again later", - }), - ); - // Received SVG output should not contain string "https://tiny.one/readme-stats" - expect(res.send.mock.calls[0][0]).not.toContain( - "https://tiny.one/readme-stats", + it("should reject non-whitelisted usernames before calling core logic", async () => { + mocks.config = { + whitelist: ["allowed-user"], + }; + + const req = createRequest("username=blocked-user"); + const res = createResponse(); + + await router(req, res); + + expect(mocks.api).not.toHaveBeenCalled(); + expect(mocks.getUserAccessByName).not.toHaveBeenCalled(); + expect(res.setHeader.mock.calls).toEqual([ + ["Cache-Control", defaultCacheHeader], + ["Content-Type", "image/svg+xml"], + ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith( + "render-error:This username is not whitelisted", ); + expect(mocks.storeRequest).not.toHaveBeenCalled(); }); }); diff --git a/apps/backend/tests/bench/api.bench.js b/apps/backend/tests/bench/api.bench.js index a7326840..6b4a47ae 100644 --- a/apps/backend/tests/bench/api.bench.js +++ b/apps/backend/tests/bench/api.bench.js @@ -1,81 +1,40 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; -import { bench, describe, vi } from "vitest"; +import { beforeAll, bench, describe, vi } from "vitest"; -import api from "../../api-renamed/index.js"; - -const stats = { - name: "Anurag Hazra", - totalStars: 100, - totalCommits: 200, - totalIssues: 300, - totalPRs: 400, - totalPRsMerged: 320, - mergedPRsPercentage: 80, - totalReviews: 50, - totalDiscussionsStarted: 10, - totalDiscussionsAnswered: 40, - contributedTo: 50, - rank: null, -}; - -const data_stats = { - data: { - user: { - name: stats.name, - repositoriesContributedTo: { totalCount: stats.contributedTo }, - commits: { - totalCommitContributions: stats.totalCommits, - }, - reviews: { - totalPullRequestReviewContributions: stats.totalReviews, - }, - pullRequests: { totalCount: stats.totalPRs }, - mergedPullRequests: { totalCount: stats.totalPRsMerged }, - openIssues: { totalCount: stats.totalIssues }, - closedIssues: { totalCount: 0 }, - followers: { totalCount: 0 }, - repositoryDiscussions: { totalCount: stats.totalDiscussionsStarted }, - repositoryDiscussionComments: { - totalCount: stats.totalDiscussionsAnswered, - }, - repositories: { - totalCount: 1, - nodes: [{ stargazers: { totalCount: 100 } }], - pageInfo: { - hasNextPage: false, - endCursor: "cursor", - }, - }, - }, - }, -}; +import { data_stats } from "../utils.js"; const mock = new MockAdapter(axios); -const faker = (query, data) => { - const req = { - query: { - username: "anuraghazra", - ...query, - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").replyOnce(200, data); +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); - return { req, res }; -}; +let router; -describe("/api", () => { +beforeAll(async () => { + vi.stubEnv("CACHE_SECONDS", ""); + vi.stubEnv("GIST_WHITELIST", ""); + vi.stubEnv("POSTGRES_URL", ""); + vi.stubEnv("WHITELIST", ""); + + ({ default: router } = await import("../../router.js")); + + mock.onPost("https://api.github.com/graphql").reply(200, data_stats); +}); + +describe("bench /api", () => { bench( "base", async () => { - const { req, res } = faker({}, data_stats); + const req = { + headers: {}, + url: "/api?username=anuraghazra", + }; + const res = createResponse(); - await api(req, res); + await router(req, res); }, { warmupIterations: 50 }, ); diff --git a/apps/backend/tests/bench/calculateRank.bench.js b/apps/backend/tests/bench/calculateRank.bench.js deleted file mode 100644 index a9e52d09..00000000 --- a/apps/backend/tests/bench/calculateRank.bench.js +++ /dev/null @@ -1,22 +0,0 @@ -import { bench, describe } from "vitest"; - -import { calculateRank } from "../../src/calculateRank.js"; - -describe("calculateRank", () => { - bench( - "base", - async () => { - calculateRank({ - all_commits: false, - commits: 1300, - prs: 1500, - issues: 4500, - reviews: 1000, - repos: 0, - stars: 600000, - followers: 50000, - }); - }, - { warmupIterations: 50 }, - ); -}); diff --git a/apps/backend/tests/bench/gist.bench.js b/apps/backend/tests/bench/gist.bench.js index 9b15c47c..997a28f9 100644 --- a/apps/backend/tests/bench/gist.bench.js +++ b/apps/backend/tests/bench/gist.bench.js @@ -1,54 +1,42 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; -import { bench, describe, vi } from "vitest"; +import { beforeAll, bench, describe, vi } from "vitest"; -import gist from "../../api-renamed/gist.js"; - -const gist_data = { - data: { - viewer: { - gist: { - description: - "List of countries and territories in English and Spanish: name, continent, capital, dial code, country codes, TLD, and area in sq km. Lista de países y territorios en Inglés y Español: nombre, continente, capital, código de teléfono, códigos de país, dominio y área en km cuadrados. Updated 2023", - owner: { - login: "Yizack", - }, - stargazerCount: 33, - forks: { - totalCount: 11, - }, - files: [ - { - name: "countries.json", - language: { - name: "JSON", - }, - size: 85858, - }, - ], - }, - }, - }, -}; +import { happy_path_gist_data } from "../utils.js"; const mock = new MockAdapter(axios); -mock.onPost("https://api.github.com/graphql").reply(200, gist_data); -describe("test /api/gist", () => { +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); + +let router; + +beforeAll(async () => { + vi.stubEnv("CACHE_SECONDS", ""); + vi.stubEnv("GIST_WHITELIST", ""); + vi.stubEnv("POSTGRES_URL", ""); + vi.stubEnv("WHITELIST", ""); + + ({ default: router } = await import("../../router.js")); + + mock + .onPost("https://api.github.com/graphql") + .reply(200, happy_path_gist_data); +}); + +describe("bench /api/gist", () => { bench( "base", async () => { const req = { - query: { - id: "bbfce31e0217a3689c8d961a356cb10d", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), + headers: {}, + url: "/api/gist?id=happy-gist-id", }; + const res = createResponse(); - await gist(req, res); + await router(req, res); }, { warmupIterations: 50 }, ); diff --git a/apps/backend/tests/bench/pin.bench.js b/apps/backend/tests/bench/pin.bench.js index 790c3a1b..f57acdda 100644 --- a/apps/backend/tests/bench/pin.bench.js +++ b/apps/backend/tests/bench/pin.bench.js @@ -1,53 +1,40 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; -import { bench, describe, vi } from "vitest"; +import { beforeAll, bench, describe, vi } from "vitest"; -import pin from "../../api-renamed/pin.js"; - -const data_repo = { - repository: { - username: "anuraghazra", - name: "convoychat", - stargazers: { - totalCount: 38000, - }, - description: "Help us take over the world! React + TS + GraphQL Chat App", - primaryLanguage: { - color: "#2b7489", - id: "MDg6TGFuZ3VhZ2UyODc=", - name: "TypeScript", - }, - forkCount: 100, - isTemplate: false, - }, -}; - -const data_user = { - data: { - user: { repository: data_repo.repository }, - organization: null, - }, -}; +import { data_user } from "../utils.js"; const mock = new MockAdapter(axios); -mock.onPost("https://api.github.com/graphql").reply(200, data_user); -describe("/api/pin", () => { +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); + +let router; + +beforeAll(async () => { + vi.stubEnv("CACHE_SECONDS", ""); + vi.stubEnv("GIST_WHITELIST", ""); + vi.stubEnv("POSTGRES_URL", ""); + vi.stubEnv("WHITELIST", ""); + + ({ default: router } = await import("../../router.js")); + + mock.onPost("https://api.github.com/graphql").reply(200, data_user); +}); + +describe("bench /api/pin", () => { bench( "base", async () => { const req = { - query: { - username: "anuraghazra", - repo: "convoychat", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), + headers: {}, + url: "/api/pin?username=anuraghazra&repo=convoychat", }; + const res = createResponse(); - await pin(req, res); + await router(req, res); }, { warmupIterations: 50 }, ); diff --git a/apps/backend/tests/e2e/e2e.test.js b/apps/backend/tests/e2e/e2e.test.js index a923922a..3b13143f 100644 --- a/apps/backend/tests/e2e/e2e.test.js +++ b/apps/backend/tests/e2e/e2e.test.js @@ -3,221 +3,343 @@ */ import axios from "axios"; -import { beforeAll, describe, expect, test } from "vitest"; - -import { renderGistCard } from "../../src/cards/gist.js"; -import { renderRepoCard } from "../../src/cards/repo.js"; -import { renderStatsCard } from "../../src/cards/stats.js"; -import { renderTopLanguages } from "../../src/cards/top-languages.js"; -import { renderWakatimeCard } from "../../src/cards/wakatime.js"; +import MockAdapter from "axios-mock-adapter"; +import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; const REPO = "curly-fiesta"; const USER = "catelinemnemosyne"; const STATS_CARD_USER = "e2eninja"; const GIST_ID = "372cef55fd897b31909fdeb3a7262758"; -const STATS_DATA = { - name: "CodeNinja", - totalPRs: 1, - totalReviews: 0, - totalCommits: 3, - totalIssues: 1, - totalStars: 1, - contributedTo: 0, - rank: { - level: "C", - percentile: 98.73972605284538, +const STATS_MOCK_RESPONSE = { + data: { + user: { + name: "CodeNinja", + login: STATS_CARD_USER, + repositoriesContributedTo: { totalCount: 0 }, + commits: { + totalCommitContributions: 3, + }, + reviews: { + totalPullRequestReviewContributions: 0, + }, + pullRequests: { totalCount: 1 }, + openIssues: { totalCount: 1 }, + closedIssues: { totalCount: 0 }, + followers: { totalCount: 0 }, + repositories: { + totalCount: 1, + nodes: [{ name: REPO, stargazers: { totalCount: 1 } }], + pageInfo: { + hasNextPage: false, + endCursor: "cursor", + }, + }, + }, }, }; -const LANGS_DATA = { - HTML: { - color: "#e34c26", - name: "HTML", - size: 1721, - }, - CSS: { - color: "#663399", - name: "CSS", - size: 930, - }, - JavaScript: { - color: "#f1e05a", - name: "JavaScript", - size: 1912, +const COMMITS_SEARCH_MOCK_RESPONSE = { + total_count: 3, +}; + +const TOP_LANGS_MOCK_RESPONSE = { + data: { + user: { + repositories: { + nodes: [ + { + name: REPO, + languages: { + edges: [ + { + size: 1721, + node: { + color: "#e34c26", + name: "HTML", + }, + }, + { + size: 930, + node: { + color: "#663399", + name: "CSS", + }, + }, + { + size: 1912, + node: { + color: "#f1e05a", + name: "JavaScript", + }, + }, + ], + }, + }, + ], + }, + }, }, }; -const WAKATIME_DATA = { - human_readable_range: "last week", - is_already_updating: false, - is_coding_activity_visible: true, - is_including_today: false, - is_other_usage_visible: false, - is_stuck: false, - is_up_to_date: false, - is_up_to_date_pending_future: false, - percent_calculated: 0, - range: "all_time", - status: "pending_update", - timeout: 15, - username: USER, - writes_only: false, +const WAKATIME_MOCK_RESPONSE = { + data: { + human_readable_range: "last week", + is_already_updating: false, + is_coding_activity_visible: true, + is_including_today: false, + is_other_usage_visible: false, + is_stuck: false, + is_up_to_date: false, + is_up_to_date_pending_future: false, + percent_calculated: 0, + range: "all_time", + status: "pending_update", + timeout: 15, + username: USER, + writes_only: false, + }, }; -const REPOSITORY_DATA = { - name: REPO, - nameWithOwner: `${USER}/cra-test`, - isPrivate: false, - isArchived: false, - isTemplate: false, - stargazers: { - totalCount: 1, +const REPO_MOCK_RESPONSE = { + data: { + user: { + repository: { + name: REPO, + nameWithOwner: `${USER}/cra-test`, + isPrivate: false, + isArchived: false, + isTemplate: false, + stargazers: { + totalCount: 1, + }, + description: "Simple cra test repo.", + primaryLanguage: { + color: "#f1e05a", + id: "MDg6TGFuZ3VhZ2UxNDA=", + name: "JavaScript", + }, + forkCount: 0, + }, + }, + organization: null, }, - description: "Simple cra test repo.", - primaryLanguage: { - color: "#f1e05a", - id: "MDg6TGFuZ3VhZ2UxNDA=", - name: "JavaScript", - }, - forkCount: 0, - starCount: 1, }; -/** - * @typedef {import("../../src/fetchers/types").GistData} GistData Gist data type. - */ - -/** - * @type {GistData} - */ -const GIST_DATA = { - name: "link.txt", - nameWithOwner: "qwerty541/link.txt", - description: - "Trying to access this path on Windows 10 ver. 1803+ will breaks NTFS", - language: "Text", - starsCount: 1, - forksCount: 0, +const GIST_MOCK_RESPONSE = { + data: { + viewer: { + gist: { + description: + "Trying to access this path on Windows 10 ver. 1803+ will breaks NTFS", + owner: { + login: "qwerty541", + }, + stargazerCount: 1, + forks: { + totalCount: 0, + }, + files: [ + { + name: "link.txt", + language: { + name: "Text", + }, + size: 1, + }, + ], + }, + }, + }, }; const CACHE_BURST_STRING = `v=${new Date().getTime()}`; -describe("Fetch Cards", () => { - let VERCEL_PREVIEW_URL = "https://github-stats-extended.vercel.app"; +const mock = new MockAdapter(axios, { onNoMatch: "passthrough" }); - beforeAll(() => { - process.env.NODE_ENV = "development"; +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); + +let router; + +/** + * Renders a card locally through the backend router. + * @param {string} url Card URL to render through the router. + * @returns {Promise} Rendered SVG markup. + */ +async function getLocalSvg(url) { + const req = { + headers: {}, + url, + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + return res.end.mock.calls[0][0]; +} + +beforeAll(async () => { + vi.stubEnv("CACHE_SECONDS", ""); + vi.stubEnv("GIST_WHITELIST", ""); + vi.stubEnv("POSTGRES_URL", ""); + vi.stubEnv("WHITELIST", ""); + + vi.stubEnv("PAT_1", "dummyPAT1"); + vi.stubEnv("PAT_2", "dummyPAT2"); + + ({ default: router } = await import("../../router.js")); + + mock.onPost("https://api.github.com/graphql").reply((config) => { + const { query, variables } = JSON.parse(config.data); + + if ( + query.includes("query userInfo") && + variables?.login === STATS_CARD_USER + ) { + return [200, STATS_MOCK_RESPONSE]; + } + + if (query.includes("query userInfo") && variables?.login === USER) { + return [200, TOP_LANGS_MOCK_RESPONSE]; + } + + if (query.includes("query getRepo")) { + return [200, REPO_MOCK_RESPONSE]; + } + + if (query.includes("query gistInfo")) { + return [200, GIST_MOCK_RESPONSE]; + } + + return [500, { error: "Unhandled GraphQL request in e2e test" }]; }); + mock + .onGet( + `https://api.github.com/search/commits?per_page=1&q=author:${STATS_CARD_USER}`, + ) + .reply(200, COMMITS_SEARCH_MOCK_RESPONSE); + + mock + .onGet( + `https://wakatime.com/api/v1/users/${USER}/stats?is_including_today=true`, + ) + .reply(200, WAKATIME_MOCK_RESPONSE); +}); + +afterAll(() => { + mock.restore(); + vi.unstubAllEnvs(); +}); + +describe("Fetch Cards", () => { + const VERCEL_PREVIEW_URL = "https://github-stats-extended-preview.vercel.app"; + test("retrieve stats card", async () => { expect(VERCEL_PREVIEW_URL).toBeDefined(); + const cardPath = `/api?username=${STATS_CARD_USER}&include_all_commits=true&${CACHE_BURST_STRING}`; + // Check if the Vercel preview instance stats card function is up and running. await expect( - axios.get(`${VERCEL_PREVIEW_URL}/api?username=${STATS_CARD_USER}`), + axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`), ).resolves.not.toThrow(); // Get local stats card. - const localStatsCardSVG = renderStatsCard(STATS_DATA, { - include_all_commits: true, - }); + const localStatsCardSVG = await getLocalSvg(cardPath); // Get the Vercel preview stats card response. - const serverStatsSvg = await axios.get( - `${VERCEL_PREVIEW_URL}/api?username=${STATS_CARD_USER}&include_all_commits=true&${CACHE_BURST_STRING}`, - ); + const serverStatsSvg = await axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`); // Check if stats card from deployment matches the stats card from local. expect(serverStatsSvg.data).toEqual(localStatsCardSVG); - }, 15000); + }, 20000); test("retrieve language card", async () => { expect(VERCEL_PREVIEW_URL).toBeDefined(); + const cardPath = `/api/top-langs?username=${USER}&${CACHE_BURST_STRING}`; + // Check if the Vercel preview instance language card function is up and running. - console.log( - `${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}&${CACHE_BURST_STRING}`, - ); await expect( - axios.get( - `${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}&${CACHE_BURST_STRING}`, - ), + axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`), ).resolves.not.toThrow(); // Get local language card. - const localLanguageCardSVG = renderTopLanguages(LANGS_DATA); + const localLanguageCardSVG = await getLocalSvg(cardPath); // Get the Vercel preview language card response. - const severLanguageSVG = await axios.get( - `${VERCEL_PREVIEW_URL}/api/top-langs/?username=${USER}&${CACHE_BURST_STRING}`, + const serverLanguageSVG = await axios.get( + `${VERCEL_PREVIEW_URL}${cardPath}`, ); // Check if language card from deployment matches the local language card. - expect(severLanguageSVG.data).toEqual(localLanguageCardSVG); - }, 15000); + expect(serverLanguageSVG.data).toEqual(localLanguageCardSVG); + }, 20000); test("retrieve WakaTime card", async () => { expect(VERCEL_PREVIEW_URL).toBeDefined(); + const cardPath = `/api/wakatime?username=${USER}&${CACHE_BURST_STRING}`; + // Check if the Vercel preview instance WakaTime function is up and running. await expect( - axios.get(`${VERCEL_PREVIEW_URL}/api/wakatime?username=${USER}`), + axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`), ).resolves.not.toThrow(); // Get local WakaTime card. - const localWakaCardSVG = renderWakatimeCard(WAKATIME_DATA); + const localWakaCardSVG = await getLocalSvg(cardPath); // Get the Vercel preview WakaTime card response. const serverWakaTimeSvg = await axios.get( - `${VERCEL_PREVIEW_URL}/api/wakatime?username=${USER}&${CACHE_BURST_STRING}`, + `${VERCEL_PREVIEW_URL}${cardPath}`, ); // Check if WakaTime card from deployment matches the local WakaTime card. expect(serverWakaTimeSvg.data).toEqual(localWakaCardSVG); - }, 15000); + }, 20000); test("retrieve repo card", async () => { expect(VERCEL_PREVIEW_URL).toBeDefined(); + const cardPath = `/api/pin?username=${USER}&repo=${REPO}&${CACHE_BURST_STRING}`; + // Check if the Vercel preview instance Repo function is up and running. await expect( - axios.get( - `${VERCEL_PREVIEW_URL}/api/pin/?username=${USER}&repo=${REPO}&${CACHE_BURST_STRING}`, - ), + axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`), ).resolves.not.toThrow(); // Get local repo card. - const localRepoCardSVG = renderRepoCard(REPOSITORY_DATA); + const localRepoCardSVG = await getLocalSvg(cardPath); // Get the Vercel preview repo card response. - const serverRepoSvg = await axios.get( - `${VERCEL_PREVIEW_URL}/api/pin/?username=${USER}&repo=${REPO}&${CACHE_BURST_STRING}`, - ); + const serverRepoSvg = await axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`); // Check if Repo card from deployment matches the local Repo card. expect(serverRepoSvg.data).toEqual(localRepoCardSVG); - }, 15000); + }, 20000); test("retrieve gist card", async () => { expect(VERCEL_PREVIEW_URL).toBeDefined(); + const cardPath = `/api/gist?id=${GIST_ID}&${CACHE_BURST_STRING}`; + // Check if the Vercel preview instance Gist function is up and running. await expect( - axios.get( - `${VERCEL_PREVIEW_URL}/api/gist?id=${GIST_ID}&${CACHE_BURST_STRING}`, - ), + axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`), ).resolves.not.toThrow(); // Get local gist card. - const localGistCardSVG = renderGistCard(GIST_DATA); + const localGistCardSVG = await getLocalSvg(cardPath); // Get the Vercel preview gist card response. - const serverGistSvg = await axios.get( - `${VERCEL_PREVIEW_URL}/api/gist?id=${GIST_ID}&${CACHE_BURST_STRING}`, - ); + const serverGistSvg = await axios.get(`${VERCEL_PREVIEW_URL}${cardPath}`); // Check if Gist card from deployment matches the local Gist card. expect(serverGistSvg.data).toEqual(localGistCardSVG); - }, 15000); + }, 20000); }); diff --git a/apps/backend/tests/gist.test.js b/apps/backend/tests/gist.test.js index 2cb6d4c3..8ee56379 100644 --- a/apps/backend/tests/gist.test.js +++ b/apps/backend/tests/gist.test.js @@ -1,161 +1,150 @@ // @ts-check -import axios from "axios"; -import MockAdapter from "axios-mock-adapter"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; -import gist from "../api-renamed/gist.js"; -import { renderGistCard } from "../src/cards/gist.js"; +const mocks = vi.hoisted(() => ({ + gist: vi.fn(), + storeRequest: vi.fn(), + getUserAccessByName: vi.fn(), + config: {}, +})); + +vi.mock("@stats-organization/github-readme-stats-core", async () => { + const { mockCore } = await import("./utils.js"); + return mockCore({ gist: mocks.gist, getConfig: () => mocks.config }); +}); + +vi.mock("../src/common/database.js", () => ({ + storeRequest: mocks.storeRequest, + getUserAccessByName: mocks.getUserAccessByName, +})); + +import router from "../router.js"; import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; -import { renderError } from "../src/common/render.js"; -import { gist_data } from "./test-data/gist-data.js"; - -import "@testing-library/jest-dom/vitest"; - -const gist_not_found_data = { - data: { - viewer: { - gist: null, - }, - }, -}; - -const mock = new MockAdapter(axios); - -afterEach(() => { - mock.reset(); +const createRequest = (search) => ({ + headers: {}, + url: `/api/gist?${search}`, }); -describe("Test /api/gist", () => { - it("should test the request", async () => { - const req = { - query: { - id: "bbfce31e0217a3689c8d961a356cb10d", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, gist_data); +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); - await gist(req, res); +const defaultCacheHeader = + `max-age=${CACHE_TTL.GIST_CARD.DEFAULT}, ` + + `s-maxage=${CACHE_TTL.GIST_CARD.DEFAULT}, ` + + `stale-while-revalidate=${DURATIONS.ONE_DAY}`; - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderGistCard({ - name: gist_data.data.viewer.gist.files[0].name, - nameWithOwner: `${gist_data.data.viewer.gist.owner.login}/${gist_data.data.viewer.gist.files[0].name}`, - description: gist_data.data.viewer.gist.description, - language: gist_data.data.viewer.gist.files[0].language.name, - starsCount: gist_data.data.viewer.gist.stargazerCount, - forksCount: gist_data.data.viewer.gist.forks.totalCount, - }), - ); +const errorCacheHeader = + `max-age=${CACHE_TTL.ERROR}, ` + + `s-maxage=${CACHE_TTL.ERROR}, ` + + `stale-while-revalidate=${DURATIONS.ONE_DAY}`; + +beforeEach(() => { + mocks.gist.mockReset(); + mocks.storeRequest.mockReset().mockResolvedValue(undefined); + mocks.getUserAccessByName.mockReset().mockResolvedValue(null); + mocks.config = {}; + // CACHE_SECONDS is not set here, this is just to safeguard against CACHE_SECONDS being set externally + delete process.env.CACHE_SECONDS; +}); + +describe("Test /api/gist backend routing", () => { + it("happy path should pass query params, respond with gist content and persist request", async () => { + mocks.gist.mockResolvedValue({ + status: "success", + content: "mock-gist-svg", + }); + + const req = createRequest("id=bbfce31e0217a3689c8d961a356cb10d&theme=dark"); + const res = createResponse(); + + await router(req, res); + + expect(mocks.gist).toHaveBeenCalledWith({ + id: "bbfce31e0217a3689c8d961a356cb10d", + theme: "dark", + }); + expect(mocks.getUserAccessByName).not.toHaveBeenCalled(); + expect(req.query).toEqual({ + id: "bbfce31e0217a3689c8d961a356cb10d", + theme: "dark", + }); + expect(res.setHeader.mock.calls).toEqual([ + ["Cache-Control", defaultCacheHeader], + ["Content-Type", "image/svg+xml"], + ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-gist-svg"); + expect(mocks.storeRequest).toHaveBeenCalledExactlyOnceWith(req); }); - it("should get the query options", async () => { - const req = { - query: { - id: "bbfce31e0217a3689c8d961a356cb10d", - title_color: "fff", - icon_color: "fff", - text_color: "fff", - bg_color: "fff", - show_owner: true, - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, gist_data); + it("should use the shorter error cache for temporary gist errors", async () => { + mocks.gist.mockResolvedValue({ + status: "error - temporary", + content: "temporary-error-svg", + }); - await gist(req, res); + const req = createRequest("id=bbfce31e0217a3689c8d961a356cb10d"); + const res = createResponse(); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderGistCard( - { - name: gist_data.data.viewer.gist.files[0].name, - nameWithOwner: `${gist_data.data.viewer.gist.owner.login}/${gist_data.data.viewer.gist.files[0].name}`, - description: gist_data.data.viewer.gist.description, - language: gist_data.data.viewer.gist.files[0].language.name, - starsCount: gist_data.data.viewer.gist.stargazerCount, - forksCount: gist_data.data.viewer.gist.forks.totalCount, - }, - { ...req.query }, - ), - ); + await router(req, res); + + expect(mocks.gist).toHaveBeenCalledWith({ + id: "bbfce31e0217a3689c8d961a356cb10d", + }); + expect(mocks.getUserAccessByName).not.toHaveBeenCalled(); + expect(res.setHeader.mock.calls).toEqual([ + ["Cache-Control", errorCacheHeader], + ["Content-Type", "image/svg+xml"], + ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith("temporary-error-svg"); + expect(mocks.storeRequest).toHaveBeenCalledExactlyOnceWith(req); }); - it("should render error if gist is not found", async () => { - const req = { - query: { - id: "bbfce31e0217a3689c8d961a356cb10d", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock - .onPost("https://api.github.com/graphql") - .reply(200, gist_not_found_data); + it("should not persist permanent gist errors returned by core", async () => { + mocks.gist.mockResolvedValue({ + status: "error - permanent", + content: "permanent-error-svg", + }); - await gist(req, res); + const req = createRequest("id=bbfce31e0217a3689c8d961a356cb10d"); + const res = createResponse(); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ message: "Gist not found" }), - ); + await router(req, res); + + expect(mocks.gist).toHaveBeenCalledWith({ + id: "bbfce31e0217a3689c8d961a356cb10d", + }); + expect(mocks.getUserAccessByName).not.toHaveBeenCalled(); + expect(res.setHeader.mock.calls).toEqual([ + ["Cache-Control", defaultCacheHeader], + ["Content-Type", "image/svg+xml"], + ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith("permanent-error-svg"); + expect(mocks.storeRequest).not.toHaveBeenCalled(); }); - it("should render error if wrong locale is provided", async () => { - const req = { - query: { - id: "bbfce31e0217a3689c8d961a356cb10d", - locale: "asdf", - }, + it("should reject non-whitelisted gist ids before calling core logic", async () => { + mocks.config = { + gistWhitelist: ["allowed-gist-id"], }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, gist_data); - await gist(req, res); + const req = createRequest("id=blocked-gist-id"); + const res = createResponse(); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "Something went wrong", - secondaryMessage: "Language not found", - }), - ); - }); + await router(req, res); - it("should have proper cache", async () => { - const req = { - query: { - id: "bbfce31e0217a3689c8d961a356cb10d", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, gist_data); - - await gist(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.setHeader).toHaveBeenCalledWith( - "Cache-Control", - `max-age=${CACHE_TTL.GIST_CARD.DEFAULT}, ` + - `s-maxage=${CACHE_TTL.GIST_CARD.DEFAULT}, ` + - `stale-while-revalidate=${DURATIONS.ONE_DAY}`, + expect(mocks.gist).not.toHaveBeenCalled(); + expect(mocks.getUserAccessByName).not.toHaveBeenCalled(); + expect(res.setHeader.mock.calls).toEqual([ + ["Cache-Control", defaultCacheHeader], + ["Content-Type", "image/svg+xml"], + ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith( + "render-error:This gist ID is not whitelisted", ); + expect(mocks.storeRequest).not.toHaveBeenCalled(); }); }); diff --git a/apps/backend/tests/pat-info.test.js b/apps/backend/tests/pat-info.test.js index 3e98fa8b..ef01b9ba 100644 --- a/apps/backend/tests/pat-info.test.js +++ b/apps/backend/tests/pat-info.test.js @@ -6,8 +6,6 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; -import patInfo, { RATE_LIMIT_SECONDS } from "../api-renamed/status/pat-info.js"; - const mock = new MockAdapter(axios); const successData = { @@ -57,20 +55,26 @@ const bad_credentials_error = { message: "Bad credentials", }; +let RATE_LIMIT_SECONDS, patInfo; + +beforeAll(async () => { + vi.stubEnv("PAT_1", "testPAT1"); + vi.stubEnv("PAT_2", "testPAT2"); + vi.stubEnv("PAT_3", "testPAT3"); + vi.stubEnv("PAT_4", "testPAT4"); + + ({ RATE_LIMIT_SECONDS, default: patInfo } = + await import("../api-renamed/status/pat-info.js")); +}); + afterEach(() => { mock.reset(); + vi.unstubAllEnvs(); + // modules may cache environment variables, so we need to reset them + vi.resetModules(); }); describe("Test /api/status/pat-info", () => { - beforeAll(() => { - // reset patenv first so that they are not populated with local envs - process.env = {}; - process.env.PAT_1 = "testPAT1"; - process.env.PAT_2 = "testPAT2"; - process.env.PAT_3 = "testPAT3"; - process.env.PAT_4 = "testPAT4"; - }); - it("should return only 'validPATs' if all PATs are valid", async () => { mock .onPost("https://api.github.com/graphql") @@ -243,7 +247,6 @@ describe("Test /api/status/pat-info", () => { }); it("should have proper cache when error is thrown", async () => { - mock.reset(); mock.onPost("https://api.github.com/graphql").networkError(); const { req, res } = faker({}, {}); diff --git a/apps/backend/tests/pin.test.js b/apps/backend/tests/pin.test.js index b9fa0356..628e68fa 100644 --- a/apps/backend/tests/pin.test.js +++ b/apps/backend/tests/pin.test.js @@ -1,175 +1,83 @@ // @ts-check -import axios from "axios"; -import MockAdapter from "axios-mock-adapter"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; -import pin from "../api-renamed/pin.js"; -import { renderRepoCard } from "../src/cards/repo.js"; +const mocks = vi.hoisted(() => ({ + pin: vi.fn(), + storeRequest: vi.fn(), + getUserAccessByName: vi.fn(), +})); + +vi.mock("@stats-organization/github-readme-stats-core", async () => { + const { mockCore } = await import("./utils.js"); + return mockCore({ pin: mocks.pin }); +}); + +vi.mock("../src/common/database.js", () => ({ + storeRequest: mocks.storeRequest, + getUserAccessByName: mocks.getUserAccessByName, +})); + +import router from "../router.js"; import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; -import { renderError } from "../src/common/render.js"; -import { data_repo, data_user } from "./test-data/pin-data.js"; - -import "@testing-library/jest-dom/vitest"; - -const mock = new MockAdapter(axios); - -afterEach(() => { - mock.reset(); +const createRequest = (search = "") => ({ + headers: {}, + url: `/api/pin?${search}`, }); -describe("Test /api/pin", () => { - it("should test the request", async () => { - const req = { - query: { +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); + +const defaultCacheHeader = + `max-age=${CACHE_TTL.PIN_CARD.DEFAULT}, ` + + `s-maxage=${CACHE_TTL.PIN_CARD.DEFAULT}, ` + + `stale-while-revalidate=${DURATIONS.ONE_DAY}`; + +beforeEach(() => { + mocks.pin.mockReset(); + mocks.storeRequest.mockReset().mockResolvedValue(undefined); + mocks.getUserAccessByName.mockReset().mockResolvedValue(null); + // CACHE_SECONDS is not set here, this is just to safeguard against CACHE_SECONDS being set externally + delete process.env.CACHE_SECONDS; +}); + +describe("Test /api/pin backend routing", () => { + it("happy path should pass query params and user PAT, respond with pin content and persist request", async () => { + mocks.getUserAccessByName.mockResolvedValue({ token: "user-pat" }); + mocks.pin.mockResolvedValue({ + status: "success", + content: "mock-pin-svg", + }); + + const req = createRequest( + "username=anuraghazra&repo=convoychat&theme=dark", + ); + const res = createResponse(); + + await router(req, res); + + expect(mocks.getUserAccessByName).toHaveBeenCalledWith("anuraghazra"); + expect(mocks.pin).toHaveBeenCalledWith( + { username: "anuraghazra", repo: "convoychat", + theme: "dark", }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); - - await pin(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - // @ts-ignore - renderRepoCard({ - ...data_repo.repository, - starCount: data_repo.repository.stargazers.totalCount, - }), - ); - }); - - it("should get the query options", async () => { - const req = { - query: { - username: "anuraghazra", - repo: "convoychat", - title_color: "fff", - icon_color: "fff", - text_color: "fff", - bg_color: "fff", - full_name: "1", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); - - await pin(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderRepoCard( - // @ts-ignore - { - ...data_repo.repository, - starCount: data_repo.repository.stargazers.totalCount, - }, - { ...req.query }, - ), - ); - }); - - it("should render error card if user repo not found", async () => { - const req = { - query: { - username: "anuraghazra", - repo: "convoychat", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock - .onPost("https://api.github.com/graphql") - .reply(200, { data: { user: { repository: null }, organization: null } }); - - await pin(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ message: "User Repository Not found" }), - ); - }); - - it("should render error card if org repo not found", async () => { - const req = { - query: { - username: "anuraghazra", - repo: "convoychat", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock - .onPost("https://api.github.com/graphql") - .reply(200, { data: { user: null, organization: { repository: null } } }); - - await pin(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ message: "Organization Repository Not found" }), - ); - }); - - it("should render error card if wrong locale provided", async () => { - const req = { - query: { - username: "anuraghazra", - repo: "convoychat", - locale: "asdf", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); - - await pin(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "Something went wrong", - secondaryMessage: "Language not found", - }), - ); - }); - - it("should have proper cache", async () => { - const req = { - query: { - username: "anuraghazra", - repo: "convoychat", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); - - await pin(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.setHeader).toHaveBeenCalledWith( - "Cache-Control", - `max-age=${CACHE_TTL.PIN_CARD.DEFAULT}, ` + - `s-maxage=${CACHE_TTL.PIN_CARD.DEFAULT}, ` + - `stale-while-revalidate=${DURATIONS.ONE_DAY}`, + "user-pat", ); + expect(req.query).toEqual({ + username: "anuraghazra", + repo: "convoychat", + theme: "dark", + }); + expect(res.setHeader.mock.calls).toEqual([ + ["Cache-Control", defaultCacheHeader], + ["Content-Type", "image/svg+xml"], + ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-pin-svg"); + expect(mocks.storeRequest).toHaveBeenCalledExactlyOnceWith(req); }); }); diff --git a/apps/backend/tests/private-instance/api.test.js b/apps/backend/tests/private-instance/api.test.js deleted file mode 100644 index b422a3b1..00000000 --- a/apps/backend/tests/private-instance/api.test.js +++ /dev/null @@ -1,41 +0,0 @@ -// @ts-check - -import axios from "axios"; -import MockAdapter from "axios-mock-adapter"; -import { afterEach, describe, expect, it, vi } from "vitest"; - -import api from "../../api-renamed/index.js"; -import { renderError } from "../../src/common/render.js"; -import { data_stats } from "../test-data/api-data.js"; - -const mock = new MockAdapter(axios); - -afterEach(() => { - mock.reset(); -}); - -describe("Test /api/", () => { - it("should render error card if username not in whitelist", async () => { - const req = { - query: { - username: "renovate-bot", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").replyOnce(200, data_stats); - - await api(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "This username is not whitelisted", - secondaryMessage: "Please deploy your own instance", - renderOptions: { show_repo_link: false }, - }), - ); - }); -}); diff --git a/apps/backend/tests/private-instance/gist.test.js b/apps/backend/tests/private-instance/gist.test.js deleted file mode 100644 index 9a23afea..00000000 --- a/apps/backend/tests/private-instance/gist.test.js +++ /dev/null @@ -1,41 +0,0 @@ -// @ts-check - -import axios from "axios"; -import MockAdapter from "axios-mock-adapter"; -import { afterEach, describe, expect, it, vi } from "vitest"; - -import gist from "../../api-renamed/gist.js"; -import { renderError } from "../../src/common/render.js"; -import { gist_data } from "../test-data/gist-data.js"; - -const mock = new MockAdapter(axios); - -afterEach(() => { - mock.reset(); -}); - -describe("Test /api/gist with gist whitelist", () => { - it("should render error card if id not in whitelist", async () => { - const req = { - query: { - id: "9bae0392ee3a26bac5cc388a6c8b1469", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, gist_data); - - await gist(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "This gist ID is not whitelisted", - secondaryMessage: "Please deploy your own instance", - renderOptions: { show_repo_link: false }, - }), - ); - }); -}); diff --git a/apps/backend/tests/private-instance/pin.test.js b/apps/backend/tests/private-instance/pin.test.js deleted file mode 100644 index fb81dc55..00000000 --- a/apps/backend/tests/private-instance/pin.test.js +++ /dev/null @@ -1,64 +0,0 @@ -// @ts-check - -import axios from "axios"; -import MockAdapter from "axios-mock-adapter"; -import { afterEach, describe, expect, it, vi } from "vitest"; - -import pin from "../../api-renamed/pin.js"; -import { renderError } from "../../src/common/render.js"; -import { data_user } from "../test-data/pin-data.js"; - -const mock = new MockAdapter(axios); - -afterEach(() => { - mock.reset(); -}); - -describe("Test /api/pin", () => { - it("should render error card if username not in whitelist", async () => { - const req = { - query: { - username: "renovate-bot", - repo: "convoychat", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); - - await pin(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "This username is not whitelisted", - secondaryMessage: "Please deploy your own instance", - renderOptions: { show_repo_link: false }, - }), - ); - }); - - it("should render error card if missing required parameters", async () => { - const req = { - query: {}, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); - - await pin(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "This username is not whitelisted", - secondaryMessage: "Please deploy your own instance", - renderOptions: { show_repo_link: false }, - }), - ); - }); -}); diff --git a/apps/backend/tests/private-instance/top-langs.test.js b/apps/backend/tests/private-instance/top-langs.test.js deleted file mode 100644 index c72f4d6f..00000000 --- a/apps/backend/tests/private-instance/top-langs.test.js +++ /dev/null @@ -1,41 +0,0 @@ -// @ts-check - -import axios from "axios"; -import MockAdapter from "axios-mock-adapter"; -import { afterEach, describe, expect, it, vi } from "vitest"; - -import topLangs from "../../api-renamed/top-langs.js"; -import { renderError } from "../../src/common/render.js"; -import { data_langs } from "../test-data/langs-data.js"; - -const mock = new MockAdapter(axios); - -afterEach(() => { - mock.reset(); -}); - -describe("Test /api/top-langs", () => { - it("should render error card if username not in whitelist", async () => { - const req = { - query: { - username: "renovate-bot", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_langs); - - await topLangs(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "This username is not whitelisted", - secondaryMessage: "Please deploy your own instance", - renderOptions: { show_repo_link: false }, - }), - ); - }); -}); diff --git a/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap b/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap new file mode 100644 index 00000000..1edbcd22 --- /dev/null +++ b/apps/backend/tests/public-instance/__snapshots__/api.test.js.snap @@ -0,0 +1,514 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test /api contract > should match the private missing-username response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + This username is not whitelisted + Please deploy your own instance + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api contract > should match the public blacklisted-username response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + This username is blacklisted + Please deploy your own instance + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api contract > should match the public happy-path response snapshot 1`] = ` +{ + "content": " + Anurag Hazra's GitHub Stats, Rank: A+ + Total Stars Earned: 14099, Total Commits (last year) : 200, Total PRs: 4000, Total Issues: 340, Contributed to (last year): 51 + + + + + + + + + + Anurag Hazra's GitHub Stats + + + + + + + + + + + + + A+ + + + + + + + + + Total Stars Earned: + 14.1k + + + + + Total Commits (last year): + 200 + + + + + Total PRs: + 4k + + + + + Total Issues: + 340 + + + + + Contributed to (last year): + 51 + + + + + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api contract > should match the public many-params response snapshot 1`] = ` +{ + "content": " + a custom title, Rank: A+ + कुल अर्जित सितारे: 4100, कुल commits (2024) : 200, कुल PR: 4000, कुल PR का विलय: 3200, मर्ज किए गए PRs प्रतिशत: 80.0, कुल PRs की समीक्षा की गई: 1234, कुल चर्चाएँ शुरू हुईं: 222, कुल चर्चाओं के उत्तर: 111, (पिछले वर्ष) में योगदान दिया: 51 + + + + + + + + + + a custom title + + + + + + + + + + + + + + + + + + + + + + + + कुल अर्जित सितारे: + 4100 + + + + + + + + + कुल commits (2024): + 200 + + + + + + + + + कुल PR: + 4000 + + + + + + + + + कुल PR का विलय: + 3200 + + + + + + + + + मर्ज किए गए PRs प्रतिशत: + 80.0 % + + + + + + + + + कुल PRs की समीक्षा की गई: + 1234 + + + + + + + + + कुल चर्चाएँ शुरू हुईं: + 222 + + + + + + + + + कुल चर्चाओं के उत्तर: + 111 + + + + + + + + + (पिछले वर्ष) में योगदान दिया: + 51 + + + + + + ", + "graphqlRequest": "{"query":"\\n query userInfo($login: String!, $after: String, $includeMergedPullRequests: Boolean!, $includeDiscussions: Boolean!, $includeDiscussionsAnswers: Boolean!, $startTime: DateTime = null, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n name\\n login\\n commits: contributionsCollection (from: $startTime) {\\n totalCommitContributions,\\n }\\n reviews: contributionsCollection {\\n totalPullRequestReviewContributions\\n }\\n repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {\\n totalCount\\n }\\n pullRequests(first: 1) {\\n totalCount\\n }\\n mergedPullRequests: pullRequests(states: MERGED) @include(if: $includeMergedPullRequests) {\\n totalCount\\n }\\n openIssues: issues(states: OPEN) {\\n totalCount\\n }\\n closedIssues: issues(states: CLOSED) {\\n totalCount\\n }\\n followers {\\n totalCount\\n }\\n repositoryDiscussions @include(if: $includeDiscussions) {\\n totalCount\\n }\\n repositoryDiscussionComments(onlyAnswers: true) @include(if: $includeDiscussionsAnswers) {\\n totalCount\\n }\\n \\n repositories(first: 100, after: $after, ownerAffiliations: $ownerAffiliations, orderBy: {direction: DESC, field: STARGAZERS}) {\\n totalCount\\n nodes {\\n name\\n stargazers {\\n totalCount\\n }\\n }\\n pageInfo {\\n hasNextPage\\n endCursor\\n }\\n }\\n\\n }\\n }\\n","variables":{"login":"anuraghazra","first":100,"after":null,"includeMergedPullRequests":true,"includeDiscussions":true,"includeDiscussionsAnswers":true,"startTime":"2024-01-01T00:00:00Z","ownerAffiliations":["OWNER","COLLABORATOR"]}}", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api contract > should match the public missing-username response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + Missing params "username" make sure you pass the parameters in URL + + + ", + "headers": [ + [ + "Cache-Control", + "max-age=600, s-maxage=600, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api contract > should render error card in same theme as requested card 1`] = ` +{ + "content": " + + + Something went wrong! + + Missing params "username" make sure you pass the parameters in URL + + + ", + "headers": [ + [ + "Cache-Control", + "max-age=600, s-maxage=600, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; diff --git a/apps/backend/tests/public-instance/__snapshots__/gist.test.js.snap b/apps/backend/tests/public-instance/__snapshots__/gist.test.js.snap new file mode 100644 index 00000000..6e8e2779 --- /dev/null +++ b/apps/backend/tests/public-instance/__snapshots__/gist.test.js.snap @@ -0,0 +1,252 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test /api/gist contract > should match the private missing-id response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + This gist ID is not whitelisted + Please deploy your own instance + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/gist contract > should match the public happy-path response snapshot 1`] = ` +{ + "content": " + + + + + + + + + + + + + + + + countries.json + + + + + + + + List of countries and territories in English and Spanish:name, continent, capital, dial code, country codes, TLD,and area in sq km. Lista de países y territorios enInglés y Español: nombre, continente, capital,código de teléfono, códigos de país,dominio y área en km cuadrados. Updated 2023 + + + + + + + JSON + + + + + + 33 + + + + 11 + + + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/gist contract > should match the public many-params response snapshot 1`] = ` +{ + "content": " + + + + + + + + + + + + + + + + Yizack/countries.json + + + + + + + + List of countries and territories in English and Spanish:name, continent, capital, dial code, country codes, TLD,and area in sq km. Lista de países y territorios enInglés y Español: nombre, continente, capital,código de teléfono, códigos de país,dominio y área en km cuadrados. Updated 2023 + + + + + + + JSON + + + + + + 33 + + + + 11 + + + + ", + "graphqlRequest": "{"query":"\\nquery gistInfo($gistName: String!) {\\n viewer {\\n gist(name: $gistName) {\\n description\\n owner {\\n login\\n }\\n stargazerCount\\n forks {\\n totalCount\\n }\\n files {\\n name\\n language {\\n name\\n }\\n size\\n }\\n }\\n }\\n}\\n","variables":{"gistName":"happy-gist-id"}}", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/gist contract > should match the public missing-id response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + Missing params "id" make sure you pass the parameters in URL + /api/gist?id=GIST_ID + + ", + "headers": [ + [ + "Cache-Control", + "max-age=600, s-maxage=600, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; diff --git a/apps/backend/tests/public-instance/__snapshots__/pin.test.js.snap b/apps/backend/tests/public-instance/__snapshots__/pin.test.js.snap new file mode 100644 index 00000000..959c008a --- /dev/null +++ b/apps/backend/tests/public-instance/__snapshots__/pin.test.js.snap @@ -0,0 +1,406 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test /api/pin contract > should match the private missing-username response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + This username is not whitelisted + Please deploy your own instance + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/pin contract > should match the private non-whitelisted username response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + This username is not whitelisted + Please deploy your own instance + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/pin contract > should match the public blacklisted-username response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + This username is blacklisted + Please deploy your own instance + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/pin contract > should match the public happy-path response snapshot 1`] = ` +{ + "content": " + + + + + + + + + + + + + + + + convoychat + + + + + + + + + + Help us take over the world with a deeply customizableReact, TypeScript and GraphQL chat app that has enough textto wrap across multiple lines in the repository card. + + + + + + + TypeScript + + + + + + 38k + + + + 100 + + + + + + + + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/pin contract > should match the public many-params response snapshot 1`] = ` +{ + "content": " + + + + + + + + + + + + + + + + anuraghazra/convoychat + + + + + + + + + + Help us take over the world with a deeply customizable React, TypeScript and GraphQL... + + + + + + + TypeScript + + + + + + 38k + + + + 100 + + + + + + + my created PRs: + 1234 + + + + + my commented PRs: + 2345 + + + + + my reviewed PRs: + 3456 + + + + + my created issues: + 4567 + + + + + my commented issues: + 5678 + + + + + + + ", + "graphqlRequest": "{"query":"\\n fragment RepoInfo on Repository {\\n name\\n nameWithOwner\\n isPrivate\\n isArchived\\n isTemplate\\n stargazers {\\n totalCount\\n }\\n description\\n primaryLanguage {\\n color\\n id\\n name\\n }\\n forkCount\\n }\\n query getRepo($login: String!, $repo: String!) {\\n user(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n organization(login: $login) {\\n repository(name: $repo) {\\n ...RepoInfo\\n }\\n }\\n }\\n ","variables":{"login":"anuraghazra","repo":"convoychat"}}", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/pin contract > should match the public missing-params response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + Missing params "username", "repo" make sure you pass the parameters in URL + /api/pin?username=USERNAME&repo=REPO_NAME + + ", + "headers": [ + [ + "Cache-Control", + "max-age=600, s-maxage=600, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/pin contract > should render error card in same theme as requested card 1`] = ` +{ + "content": " + + + Something went wrong! + + Missing params "username", "repo" make sure you pass the parameters in URL + /api/pin?username=USERNAME&repo=REPO_NAME + + ", + "headers": [ + [ + "Cache-Control", + "max-age=600, s-maxage=600, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; diff --git a/apps/backend/tests/public-instance/__snapshots__/top-langs.test.js.snap b/apps/backend/tests/public-instance/__snapshots__/top-langs.test.js.snap new file mode 100644 index 00000000..589f315e --- /dev/null +++ b/apps/backend/tests/public-instance/__snapshots__/top-langs.test.js.snap @@ -0,0 +1,460 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test /api/top-langs contract > should match the private missing-username response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + This username is not whitelisted + Please deploy your own instance + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/top-langs contract > should match the private non-whitelisted username response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + This username is not whitelisted + Please deploy your own instance + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/top-langs contract > should match the public blacklisted-username response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + This username is blacklisted + Please deploy your own instance + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/top-langs contract > should match the public happy-path response snapshot 1`] = ` +{ + "content": " + + + + + + + + + + + + Most Used Languages + + + + + + + + + + Rust + 70.92% + + + + + + + + + + + + HTML + 12.77% + + + + + + + + + + + + JavaScript + 8.51% + + + + + + + + + + + + TypeScript + 6.38% + + + + + + + + + + + + Java + 1.42% + + + + + + + + + + + + + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/top-langs contract > should match the public many-params response snapshot 1`] = ` +{ + "content": " + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TypeScript 19.0 B + + + + + + + Java 4.5 B + + + + + + + Python 3.9 B + + + + + + + + + ", + "graphqlRequest": "{"query":"\\n query userInfo($login: String!, $ownerAffiliations: [RepositoryAffiliation]) {\\n user(login: $login) {\\n # do not fetch forks\\n repositories(ownerAffiliations: $ownerAffiliations, isFork: false, first: 100) {\\n nodes {\\n name\\n languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {\\n edges {\\n size\\n node {\\n color\\n name\\n }\\n }\\n }\\n }\\n }\\n }\\n }\\n ","variables":{"login":"anuraghazra","ownerAffiliations":["OWNER","COLLABORATOR"]}}", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/top-langs contract > should match the public missing-username response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + Missing params "username" make sure you pass the parameters in URL + + + ", + "headers": [ + [ + "Cache-Control", + "max-age=600, s-maxage=600, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/top-langs contract > should render error card in same theme as requested card 1`] = ` +{ + "content": " + + + Something went wrong! + + Missing params "username" make sure you pass the parameters in URL + + + ", + "headers": [ + [ + "Cache-Control", + "max-age=600, s-maxage=600, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; diff --git a/apps/backend/tests/public-instance/__snapshots__/wakatime.test.js.snap b/apps/backend/tests/public-instance/__snapshots__/wakatime.test.js.snap new file mode 100644 index 00000000..a1a3075a --- /dev/null +++ b/apps/backend/tests/public-instance/__snapshots__/wakatime.test.js.snap @@ -0,0 +1,546 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test /api/wakatime contract > should match the private missing-username response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + This username is not whitelisted + Please deploy your own instance + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/wakatime contract > should match the private non-whitelisted username response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + This username is not whitelisted + Please deploy your own instance + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/wakatime contract > should match the public happy-path response snapshot 1`] = ` +{ + "content": " + + + + + + + + + + + + WakaTime Stats (last 7 days) + + + + + + + + + + TypeScript: + 12 hrs + + + + + + + + + + + + JavaScript: + 6 hrs 30 mins + + + + + + + + + + + + Other: + 3 hrs + + + + + + + + + + + + YAML: + 1 hr + + + + + + + + + + + + JSON: + 30 mins + + + + + + + + + + + + + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/wakatime contract > should match the public inaccessible-profile response snapshot 1`] = ` +{ + "content": " + + + + + + + + + + + + WakaTime Stats + + + + + + + + + WakaTime user profile not public + + + + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/wakatime contract > should match the public many-params response snapshot 1`] = ` +{ + "content": " + + + + + + + + + + + + a custom title + + + + + + + + + + + + + + + + + + + + + + + TypeScript - 61.88 % + + + + + + + JavaScript - 33.32 % + + + + + + + YAML - 4.76 % + + + + + + + + ", + "headers": [ + [ + "Cache-Control", + "max-age=36000, s-maxage=36000, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/wakatime contract > should match the public missing-username response snapshot 1`] = ` +{ + "content": " + + + Something went wrong! + + Missing params "username" make sure you pass the parameters in URL + + + ", + "headers": [ + [ + "Cache-Control", + "max-age=600, s-maxage=600, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; + +exports[`Test /api/wakatime contract > should render error card in same theme as requested card 1`] = ` +{ + "content": " + + + Something went wrong! + + Missing params "username" make sure you pass the parameters in URL + + + ", + "headers": [ + [ + "Cache-Control", + "max-age=600, s-maxage=600, stale-while-revalidate=86400", + ], + [ + "Content-Type", + "image/svg+xml", + ], + ], +} +`; diff --git a/apps/backend/tests/public-instance/api.test.js b/apps/backend/tests/public-instance/api.test.js index 52080c89..506d9f05 100644 --- a/apps/backend/tests/public-instance/api.test.js +++ b/apps/backend/tests/public-instance/api.test.js @@ -2,40 +2,176 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import api from "../../api-renamed/index.js"; -import { renderError } from "../../src/common/render.js"; -import { data_stats } from "../test-data/api-data.js"; +import { data_stats, normalizeSvg } from "../utils.js"; const mock = new MockAdapter(axios); +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); + +beforeEach(() => { + vi.stubEnv("CACHE_SECONDS", ""); + vi.stubEnv("GIST_WHITELIST", ""); + vi.stubEnv("POSTGRES_URL", ""); + vi.stubEnv("WHITELIST", ""); + + mock.onPost("https://api.github.com/graphql").reply(200, data_stats); +}); + afterEach(() => { mock.reset(); + vi.unstubAllEnvs(); + // modules may cache environment variables, so we need to reset them + vi.resetModules(); }); -describe("Test /api/", () => { - it("should render error card if username in blacklist", async () => { +describe("Test /api contract", () => { + it("should match the public happy-path response snapshot", async () => { + const { default: router } = await import("../../router.js"); + const req = { - query: { - username: "renovate-bot", - }, + headers: {}, + url: "/api?username=anuraghazra", }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the public many-params response snapshot", async () => { + mock.onPost("https://api.github.com/graphql").reply(200, data_stats); + + const { default: router } = await import("../../router.js"); + + const params = new URLSearchParams({ + username: "anuraghazra", + show_icons: "true", + card_width: "540", + line_height: "32", + title_color: "123456", + ring_color: "654321", + icon_color: "ff00aa", + text_color: "abcdef", + text_bold: "false", + bg_color: "0f172a", + exclude_repo: "repo-exclude-me", + custom_title: "a custom title", + locale: "hi", + disable_animations: "true", + border_radius: "12", + number_format: "long", + number_precision: "1", + border_color: "fedcba", + rank_icon: "github", + commits_year: "2024", + hide: "issues", + role: "OWNER,COLLABORATOR", + show: "reviews,prs_merged,prs_merged_percentage,discussions_started,discussions_answered", + }); + + const req = { + headers: {}, + url: `/api?${params.toString()}`, }; - mock.onPost("https://api.github.com/graphql").replyOnce(200, data_stats); + const res = createResponse(); - await api(req, res); + await router(req, res); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "This username is blacklisted", - secondaryMessage: "Please deploy your own instance", - renderOptions: { show_repo_link: false }, - }), - ); + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + graphqlRequest: mock.history.post[0].data, + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the public missing-username response snapshot", async () => { + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should render error card in same theme as requested card", async () => { + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api?theme=merko", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the public blacklisted-username response snapshot", async () => { + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api?username=renovate-bot", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the private missing-username response snapshot", async () => { + vi.stubEnv("WHITELIST", "anuraghazra"); + + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); }); }); diff --git a/apps/backend/tests/public-instance/gist.test.js b/apps/backend/tests/public-instance/gist.test.js index 42154736..0742ab3b 100644 --- a/apps/backend/tests/public-instance/gist.test.js +++ b/apps/backend/tests/public-instance/gist.test.js @@ -2,38 +2,123 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import gist from "../../api-renamed/gist.js"; -import { renderError } from "../../src/common/render.js"; -import { gist_data } from "../test-data/gist-data.js"; +import { happy_path_gist_data, normalizeSvg } from "../utils.js"; const mock = new MockAdapter(axios); +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); + +beforeEach(() => { + vi.stubEnv("CACHE_SECONDS", ""); + vi.stubEnv("GIST_WHITELIST", ""); + vi.stubEnv("POSTGRES_URL", ""); + vi.stubEnv("WHITELIST", ""); + + mock + .onPost("https://api.github.com/graphql") + .reply(200, happy_path_gist_data); +}); + afterEach(() => { mock.reset(); + vi.unstubAllEnvs(); + // modules may cache environment variables, so we need to reset them + vi.resetModules(); }); -describe("Test /api/gist", () => { - it("should render error if id is not provided", async () => { +describe("Test /api/gist contract", () => { + it("should match the public happy-path response snapshot", async () => { + const { default: router } = await import("../../router.js"); + const req = { - query: {}, + headers: {}, + url: "/api/gist?id=happy-gist-id", }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the public many-params response snapshot", async () => { + const { default: router } = await import("../../router.js"); + + const params = new URLSearchParams({ + id: "happy-gist-id", + title_color: "123456", + icon_color: "ff00aa", + text_color: "abcdef", + bg_color: "0f172a", + border_radius: "12", + border_color: "fedcba", + show_owner: "true", + }); + + const req = { + headers: {}, + url: `/api/gist?${params.toString()}`, }; - mock.onPost("https://api.github.com/graphql").reply(200, gist_data); + const res = createResponse(); - await gist(req, res); + await router(req, res); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: 'Missing params "id" make sure you pass the parameters in URL', - secondaryMessage: "/api/gist?id=GIST_ID", - renderOptions: { show_repo_link: false }, - }), - ); + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + graphqlRequest: mock.history.post[0].data, + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the public missing-id response snapshot", async () => { + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/gist", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the private missing-id response snapshot", async () => { + vi.stubEnv("GIST_WHITELIST", "allowed-gist-id"); + + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/gist", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); }); }); diff --git a/apps/backend/tests/public-instance/pin.test.js b/apps/backend/tests/public-instance/pin.test.js index fdbdfe98..12da77f9 100644 --- a/apps/backend/tests/public-instance/pin.test.js +++ b/apps/backend/tests/public-instance/pin.test.js @@ -2,64 +2,215 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import pin from "../../api-renamed/pin.js"; -import { renderError } from "../../src/common/render.js"; -import { data_user } from "../test-data/pin-data.js"; +import { data_user, normalizeSvg } from "../utils.js"; const mock = new MockAdapter(axios); +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); + +beforeEach(() => { + vi.stubEnv("CACHE_SECONDS", ""); + vi.stubEnv("GIST_WHITELIST", ""); + vi.stubEnv("POSTGRES_URL", ""); + vi.stubEnv("WHITELIST", ""); + + mock.onPost("https://api.github.com/graphql").reply(200, data_user); +}); + afterEach(() => { mock.reset(); + vi.unstubAllEnvs(); + // modules may cache environment variables, so we need to reset them + vi.resetModules(); }); -describe("Test /api/pin", () => { - it("should render error card if username in blacklist", async () => { +describe("Test /api/pin contract", () => { + it("should match the public happy-path response snapshot", async () => { + const { default: router } = await import("../../router.js"); + const req = { - query: { - username: "renovate-bot", - repo: "convoychat", - }, + headers: {}, + url: "/api/pin?username=anuraghazra&repo=convoychat", }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); + const res = createResponse(); - await pin(req, res); + await router(req, res); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "This username is blacklisted", - secondaryMessage: "Please deploy your own instance", - renderOptions: { show_repo_link: false }, - }), - ); + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); }); - it("should render error card if missing required parameters", async () => { - const req = { - query: {}, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; + it("should match the public many-params response snapshot", async () => { mock.onPost("https://api.github.com/graphql").reply(200, data_user); + mock + .onGet( + "https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+author:anuraghazra+type:pr", + ) + .reply(200, { total_count: 1234 }); + mock + .onGet( + "https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+commenter:anuraghazra+-author:anuraghazra+type:pr", + ) + .reply(200, { total_count: 2345 }); + mock + .onGet( + "https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+reviewed-by:anuraghazra+-author:anuraghazra+type:pr", + ) + .reply(200, { total_count: 3456 }); + mock + .onGet( + "https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+author:anuraghazra+type:issue", + ) + .reply(200, { total_count: 4567 }); + mock + .onGet( + "https://api.github.com/search/issues?per_page=1&q=repo:anuraghazra/convoychat+commenter:anuraghazra+-author:anuraghazra+type:issue", + ) + .reply(200, { total_count: 5678 }); - await pin(req, res); + const { default: router } = await import("../../router.js"); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: - 'Missing params "username", "repo" make sure you pass the parameters in URL', - secondaryMessage: "/api/pin?username=USERNAME&repo=REPO_NAME", - renderOptions: { show_repo_link: false }, - }), - ); + const params = new URLSearchParams({ + username: "anuraghazra", + repo: "convoychat", + title_color: "123456", + icon_color: "ff00aa", + text_color: "abcdef", + bg_color: "0f172a", + card_width: "560", + show_owner: "true", + show: "prs_authored,prs_commented,prs_reviewed,issues_authored,issues_commented", + show_icons: "false", + number_format: "long", + text_bold: "true", + line_height: "30", + border_radius: "12", + border_color: "fedcba", + description_lines_count: "1", + }); + + const req = { + headers: {}, + url: `/api/pin?${params.toString()}`, + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + graphqlRequest: mock.history.post[0].data, + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the public missing-params response snapshot", async () => { + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/pin", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should render error card in same theme as requested card", async () => { + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/pin?theme=merko", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the public blacklisted-username response snapshot", async () => { + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/pin?username=renovate-bot&repo=convoychat", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the private non-whitelisted username response snapshot", async () => { + vi.stubEnv("WHITELIST", "anuraghazra"); + + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/pin?username=martin-mfg", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the private missing-username response snapshot", async () => { + vi.stubEnv("WHITELIST", "anuraghazra"); + + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/pin", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); }); }); diff --git a/apps/backend/tests/public-instance/top-langs.test.js b/apps/backend/tests/public-instance/top-langs.test.js index 2e180e34..9eaba86c 100644 --- a/apps/backend/tests/public-instance/top-langs.test.js +++ b/apps/backend/tests/public-instance/top-langs.test.js @@ -2,40 +2,190 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import topLangs from "../../api-renamed/top-langs.js"; -import { renderError } from "../../src/common/render.js"; -import { data_langs } from "../test-data/langs-data.js"; +import { data_langs, normalizeSvg } from "../utils.js"; const mock = new MockAdapter(axios); +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); + +beforeEach(() => { + vi.stubEnv("CACHE_SECONDS", ""); + vi.stubEnv("GIST_WHITELIST", ""); + vi.stubEnv("POSTGRES_URL", ""); + vi.stubEnv("WHITELIST", ""); + + mock.onPost("https://api.github.com/graphql").reply(200, data_langs); +}); + afterEach(() => { mock.reset(); + vi.unstubAllEnvs(); + // modules may cache environment variables, so we need to reset them + vi.resetModules(); }); -describe("Test /api/top-langs", () => { - it("should render error card if username in blacklist", async () => { +describe("Test /api/top-langs contract", () => { + it("should match the public happy-path response snapshot", async () => { + const { default: router } = await import("../../router.js"); + const req = { - query: { - username: "renovate-bot", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), + headers: {}, + url: "/api/top-langs?username=anuraghazra", }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the public many-params response snapshot", async () => { mock.onPost("https://api.github.com/graphql").reply(200, data_langs); - await topLangs(req, res); + const { default: router } = await import("../../router.js"); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "This username is blacklisted", - secondaryMessage: "Please deploy your own instance", - renderOptions: { show_repo_link: false }, - }), - ); + const params = new URLSearchParams({ + username: "anuraghazra", + hide: "javascript,HTML", + hide_title: "true", + hide_border: "true", + card_width: "420", + title_color: "123456", + text_color: "abcdef", + bg_color: "0f172a", + layout: "compact", + langs_count: "3", + exclude_repo: "repo-hidden", + size_weight: "0.5", + count_weight: "1", + role: "OWNER,COLLABORATOR", + disable_animations: "true", + stats_format: "bytes", + }); + + const req = { + headers: {}, + url: `/api/top-langs?${params.toString()}`, + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + graphqlRequest: mock.history.post[0].data, + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the public missing-username response snapshot", async () => { + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/top-langs", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should render error card in same theme as requested card", async () => { + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/top-langs?theme=merko", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the public blacklisted-username response snapshot", async () => { + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/top-langs?username=renovate-bot", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the private non-whitelisted username response snapshot", async () => { + vi.stubEnv("WHITELIST", "anuraghazra"); + + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/top-langs?username=martin-mfg", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the private missing-username response snapshot", async () => { + vi.stubEnv("WHITELIST", "anuraghazra"); + + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/top-langs", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); }); }); diff --git a/apps/backend/tests/public-instance/wakatime.test.js b/apps/backend/tests/public-instance/wakatime.test.js new file mode 100644 index 00000000..48a23bf8 --- /dev/null +++ b/apps/backend/tests/public-instance/wakatime.test.js @@ -0,0 +1,212 @@ +// @ts-check + +import axios from "axios"; +import MockAdapter from "axios-mock-adapter"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { normalizeSvg, wakaTimeData } from "../utils.js"; + +const mock = new MockAdapter(axios); + +const wakaTimeProfileNotPublicData = { + data: { + viewer: { + gist: null, + }, + }, +}; + +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); + +beforeEach(() => { + vi.stubEnv("CACHE_SECONDS", ""); + vi.stubEnv("GIST_WHITELIST", ""); + vi.stubEnv("POSTGRES_URL", ""); + vi.stubEnv("WHITELIST", ""); + + mock + .onGet( + "https://wakatime.com/api/v1/users/anuraghazra/stats?is_including_today=true", + ) + .reply(200, wakaTimeData); +}); + +afterEach(() => { + mock.reset(); + vi.unstubAllEnvs(); + // modules may cache environment variables, so we need to reset them + vi.resetModules(); +}); + +describe("Test /api/wakatime contract", () => { + it("should match the public happy-path response snapshot", async () => { + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/wakatime?username=anuraghazra", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the public many-params response snapshot", async () => { + mock.reset(); // to verify api_domain param is working + mock + .onGet( + "https://wakatime.local/api/v1/users/anuraghazra/stats?is_including_today=true", + ) + .reply(200, wakaTimeData); + + const { default: router } = await import("../../router.js"); + + const params = new URLSearchParams({ + username: "anuraghazra", + title_color: "123456", + text_color: "abcdef", + bg_color: "0f172a", + card_width: "620", + custom_title: "a custom title", + layout: "compact", + langs_count: "3", + hide: "other", + api_domain: "wakatime.local", + border_radius: "12", + border_color: "fedcba", + display_format: "percent", + disable_animations: "true", + }); + + const req = { + headers: {}, + url: `/api/wakatime?${params.toString()}`, + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the public missing-username response snapshot", async () => { + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/wakatime", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should render error card in same theme as requested card", async () => { + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/wakatime?theme=merko", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the public inaccessible-profile response snapshot", async () => { + mock.reset(); + mock + .onGet( + "https://wakatime.com/api/v1/users/anuraghazra/stats?is_including_today=true", + ) + .reply(200, wakaTimeProfileNotPublicData); + + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/wakatime?username=anuraghazra", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the private non-whitelisted username response snapshot", async () => { + vi.stubEnv("WHITELIST", "anuraghazra"); + + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/wakatime?username=martin-mfg", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); + + it("should match the private missing-username response snapshot", async () => { + vi.stubEnv("WHITELIST", "anuraghazra"); + + const { default: router } = await import("../../router.js"); + + const req = { + headers: {}, + url: "/api/wakatime", + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); + + expect({ + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); + }); +}); diff --git a/apps/backend/tests/test-data/api-data.js b/apps/backend/tests/test-data/api-data.js deleted file mode 100644 index cd0c1e64..00000000 --- a/apps/backend/tests/test-data/api-data.js +++ /dev/null @@ -1,51 +0,0 @@ -// @ts-check - -/** - * @type {import("../../src/fetchers/stats").StatsData} - */ -export const stats = { - name: "Anurag Hazra", - totalStars: 100, - totalCommits: 200, - totalIssues: 300, - totalPRs: 400, - totalPRsMerged: 320, - mergedPRsPercentage: 80, - totalReviews: 50, - totalDiscussionsStarted: 10, - totalDiscussionsAnswered: 40, - contributedTo: 50, - rank: { level: "DEV", percentile: 0 }, -}; - -export const data_stats = { - data: { - user: { - name: stats.name, - repositoriesContributedTo: { totalCount: stats.contributedTo }, - commits: { - totalCommitContributions: stats.totalCommits, - }, - reviews: { - totalPullRequestReviewContributions: stats.totalReviews, - }, - pullRequests: { totalCount: stats.totalPRs }, - mergedPullRequests: { totalCount: stats.totalPRsMerged }, - openIssues: { totalCount: stats.totalIssues }, - closedIssues: { totalCount: 0 }, - followers: { totalCount: 0 }, - repositoryDiscussions: { totalCount: stats.totalDiscussionsStarted }, - repositoryDiscussionComments: { - totalCount: stats.totalDiscussionsAnswered, - }, - repositories: { - totalCount: 1, - nodes: [{ stargazers: { totalCount: 100 } }], - pageInfo: { - hasNextPage: false, - endCursor: "cursor", - }, - }, - }, - }, -}; diff --git a/apps/backend/tests/test-data/gist-data.js b/apps/backend/tests/test-data/gist-data.js deleted file mode 100644 index 8bab8d2a..00000000 --- a/apps/backend/tests/test-data/gist-data.js +++ /dev/null @@ -1,28 +0,0 @@ -// @ts-check - -export const gist_data = { - data: { - viewer: { - gist: { - description: - "List of countries and territories in English and Spanish: name, continent, capital, dial code, country codes, TLD, and area in sq km. Lista de países y territorios en Inglés y Español: nombre, continente, capital, código de teléfono, códigos de país, dominio y área en km cuadrados. Updated 2023", - owner: { - login: "Yizack", - }, - stargazerCount: 33, - forks: { - totalCount: 11, - }, - files: [ - { - name: "countries.json", - language: { - name: "JSON", - }, - size: 85858, - }, - ], - }, - }, - }, -}; diff --git a/apps/backend/tests/test-data/langs-data.js b/apps/backend/tests/test-data/langs-data.js deleted file mode 100644 index febec1c7..00000000 --- a/apps/backend/tests/test-data/langs-data.js +++ /dev/null @@ -1,36 +0,0 @@ -// @ts-check - -export const data_langs = { - data: { - user: { - repositories: { - nodes: [ - { - languages: { - edges: [{ size: 150, node: { color: "#0f0", name: "HTML" } }], - }, - }, - { - languages: { - edges: [{ size: 100, node: { color: "#0f0", name: "HTML" } }], - }, - }, - { - languages: { - edges: [ - { size: 100, node: { color: "#0ff", name: "javascript" } }, - ], - }, - }, - { - languages: { - edges: [ - { size: 100, node: { color: "#0ff", name: "javascript" } }, - ], - }, - }, - ], - }, - }, - }, -}; diff --git a/apps/backend/tests/test-data/pin-data.js b/apps/backend/tests/test-data/pin-data.js deleted file mode 100644 index 549f2c61..00000000 --- a/apps/backend/tests/test-data/pin-data.js +++ /dev/null @@ -1,26 +0,0 @@ -// @ts-check - -export const data_repo = { - repository: { - username: "anuraghazra", - name: "convoychat", - stargazers: { - totalCount: 38000, - }, - description: "Help us take over the world! React + TS + GraphQL Chat App", - primaryLanguage: { - color: "#2b7489", - id: "MDg6TGFuZ3VhZ2UyODc=", - name: "TypeScript", - }, - forkCount: 100, - isTemplate: false, - }, -}; - -export const data_user = { - data: { - user: { repository: data_repo.repository }, - organization: null, - }, -}; diff --git a/apps/backend/tests/top-langs.test.js b/apps/backend/tests/top-langs.test.js index f0519629..96d424e7 100644 --- a/apps/backend/tests/top-langs.test.js +++ b/apps/backend/tests/top-langs.test.js @@ -1,192 +1,83 @@ // @ts-check -import axios from "axios"; -import MockAdapter from "axios-mock-adapter"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; -import topLangs from "../api-renamed/top-langs.js"; -import { renderTopLanguages } from "../src/cards/top-languages.js"; +const mocks = vi.hoisted(() => ({ + topLangs: vi.fn(), + storeRequest: vi.fn(), + getUserAccessByName: vi.fn(), +})); + +vi.mock("@stats-organization/github-readme-stats-core", async () => { + const { mockCore } = await import("./utils.js"); + return mockCore({ topLangs: mocks.topLangs }); +}); + +vi.mock("../src/common/database.js", () => ({ + storeRequest: mocks.storeRequest, + getUserAccessByName: mocks.getUserAccessByName, +})); + +import router from "../router.js"; import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; -import { renderError } from "../src/common/render.js"; -import { data_langs } from "./test-data/langs-data.js"; - -import "@testing-library/jest-dom/vitest"; - -const error = { - errors: [ - { - type: "NOT_FOUND", - path: ["user"], - locations: [], - message: "Could not fetch user", - }, - ], -}; - -const langs = { - HTML: { - color: "#0f0", - name: "HTML", - size: 250, - }, - javascript: { - color: "#0ff", - name: "javascript", - size: 200, - }, -}; - -const mock = new MockAdapter(axios); - -afterEach(() => { - mock.reset(); +const createRequest = (search = "") => ({ + headers: {}, + url: `/api/top-langs?${search}`, }); -describe("Test /api/top-langs", () => { - it("should test the request", async () => { - const req = { - query: { - username: "anuraghazra", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_langs); +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); - await topLangs(req, res); +const defaultCacheHeader = + `max-age=${CACHE_TTL.TOP_LANGS_CARD.DEFAULT}, ` + + `s-maxage=${CACHE_TTL.TOP_LANGS_CARD.DEFAULT}, ` + + `stale-while-revalidate=${DURATIONS.ONE_DAY}`; - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith(renderTopLanguages(langs)); - }); +beforeEach(() => { + mocks.topLangs.mockReset(); + mocks.storeRequest.mockReset().mockResolvedValue(undefined); + mocks.getUserAccessByName.mockReset().mockResolvedValue(null); + // CACHE_SECONDS is not set here, this is just to safeguard against CACHE_SECONDS being set externally + delete process.env.CACHE_SECONDS; +}); - it("should work with the query options", async () => { - const req = { - query: { - username: "anuraghazra", - hide_title: true, - card_width: 100, - title_color: "fff", - icon_color: "fff", - text_color: "fff", - bg_color: "fff", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_langs); +describe("Test /api/top-langs backend routing", () => { + it("happy path should pass query params and user PAT, respond with top languages content and persist request", async () => { + mocks.getUserAccessByName.mockResolvedValue({ token: "user-pat" }); + mocks.topLangs.mockResolvedValue({ + status: "success", + content: "mock-top-langs-svg", + }); - await topLangs(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderTopLanguages(langs, { - hide_title: true, - card_width: 100, - title_color: "fff", - icon_color: "fff", - text_color: "fff", - bg_color: "fff", - }), + const req = createRequest( + "username=anuraghazra&layout=compact&langs_count=5", ); - }); + const res = createResponse(); - it("should render error card on user data fetch error", async () => { - const req = { - query: { + await router(req, res); + + expect(mocks.getUserAccessByName).toHaveBeenCalledWith("anuraghazra"); + expect(mocks.topLangs).toHaveBeenCalledWith( + { username: "anuraghazra", + layout: "compact", + langs_count: "5", }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, error); - - await topLangs(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: error.errors[0].message, - secondaryMessage: - "Make sure the provided username is not an organization", - }), - ); - }); - - it("should render error card on incorrect layout input", async () => { - const req = { - query: { - username: "anuraghazra", - layout: ["pie"], - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_langs); - - await topLangs(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "Something went wrong", - secondaryMessage: "Incorrect layout input", - }), - ); - }); - - it("should render error card if wrong locale provided", async () => { - const req = { - query: { - username: "anuraghazra", - locale: "asdf", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_langs); - - await topLangs(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "Something went wrong", - secondaryMessage: "Locale not found", - }), - ); - }); - - it("should have proper cache", async () => { - const req = { - query: { - username: "anuraghazra", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, data_langs); - - await topLangs(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.setHeader).toHaveBeenCalledWith( - "Cache-Control", - `max-age=${CACHE_TTL.TOP_LANGS_CARD.DEFAULT}, ` + - `s-maxage=${CACHE_TTL.TOP_LANGS_CARD.DEFAULT}, ` + - `stale-while-revalidate=${DURATIONS.ONE_DAY}`, + "user-pat", ); + expect(req.query).toEqual({ + username: "anuraghazra", + layout: "compact", + langs_count: "5", + }); + expect(res.setHeader.mock.calls).toEqual([ + ["Cache-Control", defaultCacheHeader], + ["Content-Type", "image/svg+xml"], + ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-top-langs-svg"); + expect(mocks.storeRequest).toHaveBeenCalledExactlyOnceWith(req); }); }); diff --git a/apps/backend/tests/utils.js b/apps/backend/tests/utils.js index 21b9f1f8..9108685a 100644 --- a/apps/backend/tests/utils.js +++ b/apps/backend/tests/utils.js @@ -1,41 +1,288 @@ // @ts-check +export const data_stats = { + data: { + user: { + name: "Anurag Hazra", + login: "anuraghazra", + repositoriesContributedTo: { totalCount: 51 }, + commits: { + totalCommitContributions: 200, + }, + reviews: { + totalPullRequestReviewContributions: 1234, + }, + pullRequests: { totalCount: 4000 }, + mergedPullRequests: { totalCount: 3200 }, + openIssues: { totalCount: 300 }, + closedIssues: { totalCount: 40 }, + followers: { totalCount: 150 }, + repositoryDiscussions: { totalCount: 222 }, + repositoryDiscussionComments: { + totalCount: 111, + }, + repositories: { + totalCount: 3, + nodes: [ + { name: "repo-keep-1", stargazers: { totalCount: 1500 } }, + { name: "repo-exclude-me", stargazers: { totalCount: 9999 } }, + { name: "repo-keep-2", stargazers: { totalCount: 2600 } }, + ], + pageInfo: { + hasNextPage: false, + endCursor: "cursor", + }, + }, + }, + }, +}; + +export const happy_path_gist_data = { + data: { + viewer: { + gist: { + description: + "List of countries and territories in English and Spanish: name, continent, capital, dial code, country codes, TLD, and area in sq km. Lista de países y territorios en Inglés y Español: nombre, continente, capital, código de teléfono, códigos de país, dominio y área en km cuadrados. Updated 2023", + owner: { + login: "Yizack", + }, + stargazerCount: 33, + forks: { + totalCount: 11, + }, + files: [ + { + name: "countries.json", + language: { + name: "JSON", + }, + size: 85858, + }, + ], + }, + }, + }, +}; + +export const data_user = { + data: { + user: { + repository: { + username: "anuraghazra", + name: "convoychat", + nameWithOwner: "anuraghazra/convoychat", + stargazers: { + totalCount: 38000, + }, + description: + "Help us take over the world with a deeply customizable React, TypeScript and GraphQL chat app that has enough text to wrap across multiple lines in the repository card.", + primaryLanguage: { + color: "#2b7489", + id: "MDg6TGFuZ3VhZ2UyODc=", + name: "TypeScript", + }, + forkCount: 100, + isTemplate: false, + isArchived: false, + }, + }, + organization: null, + }, +}; + +export const data_langs = { + data: { + user: { + repositories: { + nodes: [ + { + name: "repo-html", + languages: { + edges: [{ size: 180, node: { color: "#0f0", name: "HTML" } }], + }, + }, + { + name: "repo-javascript", + languages: { + edges: [ + { size: 120, node: { color: "#0ff", name: "JavaScript" } }, + ], + }, + }, + { + name: "repo-ts-1", + languages: { + edges: [ + { size: 45, node: { color: "#3178c6", name: "TypeScript" } }, + ], + }, + }, + { + name: "repo-ts-2", + languages: { + edges: [ + { size: 45, node: { color: "#3178c6", name: "TypeScript" } }, + ], + }, + }, + { + name: "repo-other-langs", + languages: { + edges: [ + { size: 20, node: { color: "#f00", name: "Java" } }, + { size: 10, node: { color: "#0f0", name: "Cobol" } }, + { size: 15, node: { color: "#00f", name: "Python" } }, + ], + }, + }, + { + name: "repo-hidden", + languages: { + edges: [{ size: 1000, node: { color: "#dea584", name: "Rust" } }], + }, + }, + ], + }, + }, + }, +}; + +export const wakaTimeData = { + data: { + categories: [ + { + digital: "22:40", + hours: 22, + minutes: 40, + name: "Coding", + percent: 100, + text: "22 hrs 40 mins", + total_seconds: 81643.570077, + }, + ], + daily_average: 16095, + daily_average_including_other_language: 16329, + days_including_holidays: 7, + days_minus_holidays: 5, + editors: [ + { + digital: "22:40", + hours: 22, + minutes: 40, + name: "VS Code", + percent: 100, + text: "22 hrs 40 mins", + total_seconds: 81643.570077, + }, + ], + holidays: 2, + human_readable_daily_average: "4 hrs 28 mins", + human_readable_daily_average_including_other_language: "4 hrs 32 mins", + human_readable_total: "22 hrs 21 mins", + human_readable_total_including_other_language: "22 hrs 40 mins", + id: "random hash", + is_already_updating: false, + is_coding_activity_visible: true, + is_including_today: false, + is_other_usage_visible: true, + is_stuck: false, + is_up_to_date: true, + languages: [ + { + digital: "12:00", + hours: 12, + minutes: 0, + name: "TypeScript", + percent: 52, + text: "12 hrs", + total_seconds: 43200, + }, + { + digital: "6:30", + hours: 6, + minutes: 30, + name: "JavaScript", + percent: 28, + text: "6 hrs 30 mins", + total_seconds: 23400, + }, + { + digital: "3:00", + hours: 3, + minutes: 0, + name: "Other", + percent: 13, + text: "3 hrs", + total_seconds: 10800, + }, + { + digital: "1:00", + hours: 1, + minutes: 0, + name: "YAML", + percent: 4, + text: "1 hr", + total_seconds: 3600, + }, + { + digital: "0:30", + hours: 0, + minutes: 30, + name: "JSON", + percent: 3, + text: "30 mins", + total_seconds: 1800, + }, + ], + operating_systems: [ + { + digital: "22:40", + hours: 22, + minutes: 40, + name: "Mac", + percent: 100, + text: "22 hrs 40 mins", + total_seconds: 81643.570077, + }, + ], + percent_calculated: 100, + range: "last_7_days", + status: "ok", + timeout: 15, + total_seconds: 80473.135716, + total_seconds_including_other_language: 81643.570077, + user_id: "random hash", + username: "anuraghazra", + writes_only: false, + }, +}; + /** - * Creates an asymmetric matcher for approximate numeric equality. - * - * This helper is intended for use in test frameworks (e.g., Jest) where - * values need to be compared within a configurable decimal precision - * instead of strict equality. - * - * The comparison succeeds when: - * - * |actual - expected| < 10^(-precision) - * - * For example, with `precision = 3`, values must be within `0.001`. - * - * @param {number} expected The expected numeric value to compare against. - * - * @param {number} [precision=10] - * The number of decimal places of tolerance. Higher values mean stricter - * comparison. Internally converted to epsilon = 10^-precision. - * - * @returns {{ - * asymmetricMatch(actual: unknown): boolean, - * toAsymmetricMatcher(): string - * }} An object implementing Jest-style asymmetric matcher methods. - * + * Creates a mock module for @stats-organization/github-readme-stats-core. + * @param {any} mocks Mocked functions of the core module. + * @returns {any} Mocked core module. */ -export function approxNumber(expected, precision = 10) { +export function mockCore(mocks) { + const noop = () => undefined; + return { - asymmetricMatch(actual) { - if (typeof actual !== "number" || typeof expected !== "number") { - return false; - } - const epsilon = Math.pow(10, -precision); - return Math.abs(actual - expected) < epsilon; - }, - toAsymmetricMatcher() { - return `≈ ${expected} (precision ${precision})`; - }, + api: mocks.api ?? noop, + gist: mocks.gist ?? noop, + pin: mocks.pin ?? noop, + topLangs: mocks.topLangs ?? noop, + wakatime: mocks.wakatime ?? noop, + getConfig: mocks.getConfig ?? (() => mocks.config ?? {}), + renderError: ({ message }) => `render-error:${message}`, + clampValue: (value, min, max) => Math.min(Math.max(value, min), max), }; } + +/** + * Normalizes SVG code for stable test comparisons. + * @param {string} svg SVG code to normalize. + * @returns {string} Normalized SVG code. + */ +export function normalizeSvg(svg) { + const document = new DOMParser().parseFromString(svg, "image/svg+xml"); + return new XMLSerializer().serializeToString(document); +} diff --git a/apps/backend/tests/wakatime.test.js b/apps/backend/tests/wakatime.test.js index f27eec0b..e1561724 100644 --- a/apps/backend/tests/wakatime.test.js +++ b/apps/backend/tests/wakatime.test.js @@ -1,195 +1,77 @@ -import axios from "axios"; -import MockAdapter from "axios-mock-adapter"; -import { afterEach, describe, expect, it, vi } from "vitest"; +// @ts-check -import wakatime from "../api-renamed/wakatime.js"; -import { renderWakatimeCard } from "../src/cards/wakatime.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + wakatime: vi.fn(), + storeRequest: vi.fn(), + getUserAccessByName: vi.fn(), +})); + +vi.mock("@stats-organization/github-readme-stats-core", async () => { + const { mockCore } = await import("./utils.js"); + return mockCore({ wakatime: mocks.wakatime }); +}); + +vi.mock("../src/common/database.js", () => ({ + storeRequest: mocks.storeRequest, + getUserAccessByName: mocks.getUserAccessByName, +})); + +import router from "../router.js"; import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; -import { renderError } from "../src/index.js"; -import "@testing-library/jest-dom/vitest"; - -const wakaTimeData = { - data: { - categories: [ - { - digital: "22:40", - hours: 22, - minutes: 40, - name: "Coding", - percent: 100, - text: "22 hrs 40 mins", - total_seconds: 81643.570077, - }, - ], - daily_average: 16095, - daily_average_including_other_language: 16329, - days_including_holidays: 7, - days_minus_holidays: 5, - editors: [ - { - digital: "22:40", - hours: 22, - minutes: 40, - name: "VS Code", - percent: 100, - text: "22 hrs 40 mins", - total_seconds: 81643.570077, - }, - ], - holidays: 2, - human_readable_daily_average: "4 hrs 28 mins", - human_readable_daily_average_including_other_language: "4 hrs 32 mins", - human_readable_total: "22 hrs 21 mins", - human_readable_total_including_other_language: "22 hrs 40 mins", - id: "random hash", - is_already_updating: false, - is_coding_activity_visible: true, - is_including_today: false, - is_other_usage_visible: true, - is_stuck: false, - is_up_to_date: true, - languages: [ - { - digital: "0:19", - hours: 0, - minutes: 19, - name: "Other", - percent: 1.43, - text: "19 mins", - total_seconds: 1170.434361, - }, - { - digital: "0:01", - hours: 0, - minutes: 1, - name: "TypeScript", - percent: 0.1, - text: "1 min", - total_seconds: 83.293809, - }, - { - digital: "0:00", - hours: 0, - minutes: 0, - name: "YAML", - percent: 0.07, - text: "0 secs", - total_seconds: 54.975151, - }, - ], - operating_systems: [ - { - digital: "22:40", - hours: 22, - minutes: 40, - name: "Mac", - percent: 100, - text: "22 hrs 40 mins", - total_seconds: 81643.570077, - }, - ], - percent_calculated: 100, - range: "last_7_days", - status: "ok", - timeout: 15, - total_seconds: 80473.135716, - total_seconds_including_other_language: 81643.570077, - user_id: "random hash", - username: "anuraghazra", - writes_only: false, - }, -}; - -const wakaTimeNotFoundData = { - data: { - viewer: { - gist: null, - }, - }, -}; - -const mock = new MockAdapter(axios); - -afterEach(() => { - mock.reset(); +const createRequest = (search = "") => ({ + headers: {}, + url: `/api/wakatime?${search}`, }); -describe("Test /api/wakatime", () => { - it("should test the request", async () => { - const username = "anuraghazra"; - const req = { query: { username } }; - const res = { setHeader: vi.fn(), send: vi.fn() }; - mock - .onGet( - `https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`, - ) - .reply(200, wakaTimeData); +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); - await wakatime(req, res); +const defaultCacheHeader = + `max-age=${CACHE_TTL.WAKATIME_CARD.DEFAULT}, ` + + `s-maxage=${CACHE_TTL.WAKATIME_CARD.DEFAULT}, ` + + `stale-while-revalidate=${DURATIONS.ONE_DAY}`; - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderWakatimeCard(wakaTimeData.data, {}), - ); - }); +beforeEach(() => { + mocks.wakatime.mockReset(); + mocks.storeRequest.mockReset().mockResolvedValue(undefined); + mocks.getUserAccessByName.mockReset().mockResolvedValue(null); + // CACHE_SECONDS is not set here, this is just to safeguard against CACHE_SECONDS being set externally + delete process.env.CACHE_SECONDS; +}); - it("should render error if wrong locale is provided", async () => { - const username = "anuraghazra"; - const req = { query: { username, locale: "asdf" } }; - const res = { setHeader: vi.fn(), send: vi.fn() }; - mock - .onGet( - `https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`, - ) - .reply(200, wakaTimeData); +describe("Test /api/wakatime backend routing", () => { + it("happy path should pass query params, respond with wakatime content and persist request", async () => { + mocks.wakatime.mockResolvedValue({ + status: "success", + content: "mock-wakatime-svg", + }); - await wakatime(req, res); + const req = createRequest("username=anuraghazra&theme=dark&layout=compact"); + const res = createResponse(); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "Something went wrong", - secondaryMessage: "Language not found", - }), - ); - }); + await router(req, res); - it("should render error if user data is not accessible", async () => { - const username = "anuraghazra"; - const req = { query: { username } }; - const res = { setHeader: vi.fn(), send: vi.fn() }; - mock - .onGet( - `https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`, - ) - .reply(200, wakaTimeNotFoundData); - - await wakatime(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledTimes(1); - expect(res.send.mock.calls[0][0]).toMatchSnapshot(); - }); - - it("should have proper cache", async () => { - const username = "anuraghazra"; - const req = { query: { username } }; - const res = { setHeader: vi.fn(), send: vi.fn() }; - mock - .onGet( - `https://wakatime.com/api/v1/users/${username}/stats?is_including_today=true`, - ) - .reply(200, wakaTimeData); - - await wakatime(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.setHeader).toHaveBeenCalledWith( - "Cache-Control", - `max-age=${CACHE_TTL.WAKATIME_CARD.DEFAULT}, ` + - `s-maxage=${CACHE_TTL.WAKATIME_CARD.DEFAULT}, ` + - `stale-while-revalidate=${DURATIONS.ONE_DAY}`, - ); + expect(mocks.wakatime).toHaveBeenCalledWith({ + username: "anuraghazra", + theme: "dark", + layout: "compact", + }); + expect(mocks.getUserAccessByName).not.toHaveBeenCalled(); + expect(req.query).toEqual({ + username: "anuraghazra", + theme: "dark", + layout: "compact", + }); + expect(res.setHeader.mock.calls).toEqual([ + ["Cache-Control", defaultCacheHeader], + ["Content-Type", "image/svg+xml"], + ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith("mock-wakatime-svg"); + expect(mocks.storeRequest).toHaveBeenCalledExactlyOnceWith(req); }); }); diff --git a/apps/backend/tsconfig.typecheck.json b/apps/backend/tsconfig.typecheck.json new file mode 100644 index 00000000..4c13d7b1 --- /dev/null +++ b/apps/backend/tsconfig.typecheck.json @@ -0,0 +1,7 @@ +{ + "extends": ["./tsconfig.json"], + "compilerOptions": { + "noEmit": true, + "customConditions": [] + } +} diff --git a/apps/backend/vercel.json b/apps/backend/vercel.json index 62ccb490..addf7bdd 100644 --- a/apps/backend/vercel.json +++ b/apps/backend/vercel.json @@ -1,6 +1,6 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", - "buildCommand": "cd ../../ && git clean ./apps -fx && pnpm --filter ./apps/backend/ --legacy deploy ./apps/deployment/ && mv ./apps/deployment/node_modules/ ./apps/backend/node_modules/ && ./vercel-preparation.sh", + "buildCommand": "cd ../../ && git clean ./apps -fx && pnpm run build:packages && pnpm --filter ./apps/backend/ --legacy deploy ./apps/deployment/ && mv ./apps/deployment/node_modules/ ./apps/backend/node_modules/ && ./vercel-preparation.sh", "redirects": [ { "source": "/", diff --git a/apps/backend/vitest.config.bench.ts b/apps/backend/vitest.config.bench.ts index 9fe2e73d..1f3eef9d 100644 --- a/apps/backend/vitest.config.bench.ts +++ b/apps/backend/vitest.config.bench.ts @@ -1,6 +1,9 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ + resolve: { + conditions: ["@stats/source"], + }, test: { dir: "./tests/bench", environment: "jsdom", diff --git a/apps/backend/vitest.config.e2e.ts b/apps/backend/vitest.config.e2e.ts index 0e6c6312..f7433de3 100644 --- a/apps/backend/vitest.config.e2e.ts +++ b/apps/backend/vitest.config.e2e.ts @@ -1,6 +1,9 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ + resolve: { + conditions: ["@stats/source"], + }, test: { environment: "node", dir: "tests/e2e", diff --git a/apps/backend/vitest.config.ts b/apps/backend/vitest.config.ts index 5da6c76b..54dcb613 100644 --- a/apps/backend/vitest.config.ts +++ b/apps/backend/vitest.config.ts @@ -1,32 +1,15 @@ -import { defineConfig } from "vitest/config"; +import { defineProject } from "vitest/config"; -export default defineConfig({ +export default defineProject({ + resolve: { + conditions: ["@stats/source"], + }, test: { - coverage: { - enabled: true, - }, - projects: [ - { - test: { - name: "backend/public-instance", - environment: "jsdom", - dir: "tests", - include: ["./*.test.{ts,js}", "./public-instance/*.test.{ts,js}"], - setupFiles: ["./tests/_setup.js"], - }, - }, - { - test: { - name: "backend/private-instance", - environment: "jsdom", - dir: "tests", - include: ["./*.test.{ts,js}", "./private-instance/*.test.{ts,js}"], - setupFiles: [ - "./tests/_setup.js", - "./tests/_setup.private-instance.js", - ], - }, - }, + environment: "jsdom", + include: [ + "./tests/*.test.{ts,js}", + "./tests/public-instance/*.test.{ts,js}", ], + setupFiles: ["./tests/_setup.js"], }, }); diff --git a/apps/frontend/package.json b/apps/frontend/package.json index aa6d33c5..7e3031f4 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -1,27 +1,35 @@ { - "name": "frontend", - "version": "0.1.0", - "private": true, + "name": "@stats-organization/github-readme-stats-frontend", "type": "module", + "license": "MIT", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "test": "vitest", + "test:e2e": "playwright test", + "lint": "eslint", + "typecheck": "tsc -p tsconfig.typecheck.json" + }, "dependencies": { - "@reduxjs/toolkit": "2.11.2", - "@tailwindcss/vite": "4.2.1", + "@reduxjs/toolkit": "^2.11.2", + "@tailwindcss/vite": "^4.2.1", "axios": "^1", "axios-cache-interceptor": "^1", - "daisyui": "5.5.19", - "emoji-name-map": "^2.0.3", - "github-username-regex": "^1.0.0", - "react": "18.3.1", - "react-dom": "18.3.1", + "daisyui": "^5.5.19", + "@stats-organization/github-readme-stats-backend": "workspace:^", + "@stats-organization/github-readme-stats-core": "workspace:^", + "react": "^18.3.1", + "react-dom": "^18.3.1", "react-icons": "^5.5.0", "react-loading-skeleton": "^3.3.1", - "react-redux": "9.2.0", + "react-redux": "^9.2.0", "react-spinners": "^0.17.0", - "react-toastify": "11.0.5", - "redux": "5.0.1", + "react-toastify": "^11.0.5", + "redux": "^5.0.1", "save-svg-as-png": "^1.4.17", - "uuid": "13.0.0", - "word-wrap": "^1.2.5" + "uuid": "^13.0.0" }, "devDependencies": { "@types/react": "18.3.27", @@ -30,19 +38,8 @@ "clsx": "2.1.1", "tailwindcss": "4.2.1", "vite": "catalog:default", - "vite-plugin-node-polyfills": "0.25.0", - "vite-plugin-string-replace": "1.1.5", "vitest": "catalog:default" }, - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "test": "vitest", - "test:e2e": "playwright test", - "typecheck": "tsc --noEmit" - }, - "homepage": "/frontend", "browserslist": { "production": [ ">0.2%", diff --git a/apps/frontend/src/components/Card/SvgInline.tsx b/apps/frontend/src/components/Card/SvgInline.tsx index 7c85a010..2834864e 100644 --- a/apps/frontend/src/components/Card/SvgInline.tsx +++ b/apps/frontend/src/components/Card/SvgInline.tsx @@ -1,3 +1,5 @@ +// @ts-expect-error type info should be added later +import { router } from "@stats-organization/github-readme-stats-backend"; import axios from "axios"; import { useEffect, useRef, useState } from "react"; import type { JSX } from "react"; @@ -5,8 +7,6 @@ import Skeleton from "react-loading-skeleton"; import "react-loading-skeleton/dist/skeleton.css"; import { setShouldMock } from "../../axios-override.js"; -// @ts-expect-error will be solved by npm package -import { default as router } from "../../backend/.vercel/output/functions/api.func/router.js"; import { createMockRequest, createMockResponse } from "../../mock-http.js"; import { useIsAuthenticated, diff --git a/apps/frontend/src/constants.ts b/apps/frontend/src/constants.ts index 79c02da9..e3328ed2 100644 --- a/apps/frontend/src/constants.ts +++ b/apps/frontend/src/constants.ts @@ -13,10 +13,10 @@ const REDIRECT_URI = `https://${HOST}/frontend`; export const GITHUB_PRIVATE_AUTH_URL = `https://github.com/login/oauth/authorize?scope=user,repo&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}?mode=private`; export const GITHUB_PUBLIC_AUTH_URL = `https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}?mode=public`; -export const DEMO_USER = "anuraghazra"; -export const DEMO_REPO = "anuraghazra/github-readme-stats"; -export const DEMO_GIST = "bbfce31e0217a3689c8d961a356cb10d"; -export const DEMO_WAKATIME_USER = "alan"; +export const DEMO_USER = "anuraghazra" as string; +export const DEMO_REPO = "anuraghazra/github-readme-stats" as string; +export const DEMO_GIST = "bbfce31e0217a3689c8d961a356cb10d" as string; +export const DEMO_WAKATIME_USER = "alan" as string; window.process = { env: { diff --git a/apps/frontend/src/pages/Home/Home.tsx b/apps/frontend/src/pages/Home/Home.tsx index e73cb397..a7b0b665 100644 --- a/apps/frontend/src/pages/Home/Home.tsx +++ b/apps/frontend/src/pages/Home/Home.tsx @@ -48,7 +48,7 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element { const dispatch = useDispatch(); // for stage two - const [selectedUserId, setSelectedUserId] = useState(userId); + const [selectedUserId, setSelectedUserId] = useState(userId); const [repo, setRepo] = useState(DEMO_REPO); const [gist, setGist] = useState(DEMO_GIST); const [wakatimeUser, setWakatimeUser] = useState(DEMO_WAKATIME_USER); diff --git a/apps/frontend/src/pages/Home/stages/Theme.tsx b/apps/frontend/src/pages/Home/stages/Theme.tsx index 247d8caf..23797cf3 100644 --- a/apps/frontend/src/pages/Home/stages/Theme.tsx +++ b/apps/frontend/src/pages/Home/stages/Theme.tsx @@ -1,21 +1,8 @@ +import { themes } from "@stats-organization/github-readme-stats-core"; import type { JSX } from "react"; -// @ts-expect-error this will be provided by the npm package -import { themes } from "../../../backend/themes/index"; import { Card } from "../../../components/Card/Card"; -// to be removed once npm package has been created -type ThemeData = Record< - string, - { - title_color: string; - icon_color: string; - text_color: string; - bg_color: string; - border_color: string; - } ->; - const excludedThemes = [ "merko", "blue-green", @@ -25,10 +12,9 @@ const excludedThemes = [ "holi", ]; -const themeList = Object.keys( - /* Needed until themes is typed correctly and retrieved from npm package */ - themes as ThemeData, -).filter((myTheme) => !excludedThemes.includes(myTheme)); +const themeList = Object.keys(themes).filter( + (myTheme) => !excludedThemes.includes(myTheme), +); interface ThemeStageProps { fullSuffix: string; diff --git a/apps/frontend/tsconfig.typecheck.json b/apps/frontend/tsconfig.typecheck.json new file mode 100644 index 00000000..4c13d7b1 --- /dev/null +++ b/apps/frontend/tsconfig.typecheck.json @@ -0,0 +1,7 @@ +{ + "extends": ["./tsconfig.json"], + "compilerOptions": { + "noEmit": true, + "customConditions": [] + } +} diff --git a/apps/frontend/vercel.json b/apps/frontend/vercel.json index 138025f9..6ed94648 100644 --- a/apps/frontend/vercel.json +++ b/apps/frontend/vercel.json @@ -1,6 +1,6 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", - "installCommand": "cd ../../ && git clean ./apps -fx && pnpm install && rm -rf ./apps/frontend/node_modules && ./vercel-preparation.sh && pnpm --filter ./apps/frontend/ --legacy deploy ./apps/deployment/ && mv ./apps/deployment/node_modules/ ./apps/frontend/node_modules/", + "installCommand": "cd ../../ && git clean ./apps -fx && pnpm install && pnpm run build:packages && rm -rf ./apps/frontend/node_modules && pnpm --filter ./apps/frontend/ --legacy deploy ./apps/deployment/ && mv ./apps/deployment/node_modules/ ./apps/frontend/node_modules/", "buildCommand": "pnpm run build", "outputDirectory": "build" } diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index b6c6e152..da01ed72 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -2,57 +2,12 @@ import path from "node:path"; import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react-swc"; -import { nodePolyfills } from "vite-plugin-node-polyfills"; -import StringReplace from "vite-plugin-string-replace"; -import { defineConfig } from "vitest/config"; +import { defineProject } from "vitest/config"; // https://vitejs.dev/config/ -export default defineConfig({ +export default defineProject({ base: "/frontend/", - plugins: [ - StringReplace([ - { - search: "process.env", - replace: "window.process.env", - fileName: ".*backend.*", - }, - ]), - nodePolyfills({ - include: [ - "path", - "querystring", - "url", - "http", - "util", - "stream", - "buffer", - ], - exclude: ["fs", "net"], - }), - - react(), - tailwindcss(), - - /** - * mock pg (postgres) package in the browser to avoid runtime errors - * @see apps/backend/src/common/database.js - */ - { - name: "empty-pg-package", - resolveId(id) { - if (id === "pg") { - return id; - } - return undefined; - }, - load(id) { - if (id === "pg") { - return "export class Pool { constructor(config) {} }"; - } - return undefined; - }, - }, - ], + plugins: [react(), tailwindcss()], build: { outDir: "build", sourcemap: true, @@ -61,6 +16,7 @@ export default defineConfig({ chunkSizeWarningLimit: 800, }, resolve: { + conditions: ["@stats/source"], alias: [ { find: "../src/fetchers/wakatime.js", @@ -72,7 +28,7 @@ export default defineConfig({ ], }, test: { - dir: "./src", + dir: path.join(import.meta.dirname, "./src"), exclude: ["**/backend/**"], }, }); diff --git a/docs/advanced_documentation.md b/docs/advanced_documentation.md index 10965e45..bd2e7877 100644 --- a/docs/advanced_documentation.md +++ b/docs/advanced_documentation.md @@ -76,7 +76,7 @@ GitHub Stats Extended comes with several built-in themes (e.g. `dark`, `radical` GitHub Stats Extended Themes -You can look at a preview for [all available themes](../backend/themes/README.md) or checkout the [theme config file](../backend/themes/index.js). Please note that we paused the addition of new themes to decrease maintenance efforts; all pull requests related to new themes will be closed. +You can look at a preview for [all available themes](../packages/core/src/themes/README.md) or checkout the [theme config file](../packages/core/src/themes/index.js). Please note that we paused the addition of new themes to decrease maintenance efforts; all pull requests related to new themes will be closed. #### Responsive Card Theme @@ -102,7 +102,7 @@ We have included a `transparent` theme that has a transparent background. This t ##### Add transparent alpha channel to a themes bg\_color -You can use the `bg_color` parameter to make any of [the available themes](../backend/themes/README.md) transparent. This is done by setting the `bg_color` to a color with a transparent alpha channel (i.e. `bg_color=00000000`): +You can use the `bg_color` parameter to make any of [the available themes](../packages/core/src/themes/README.md) transparent. This is done by setting the `bg_color` to a color with a transparent alpha channel (i.e. `bg_color=00000000`): ```md ![Anurag's GitHub stats](https://github-stats-extended.vercel.app/api?username=anuraghazra&show_icons=true&bg_color=00000000) @@ -181,7 +181,7 @@ You can customize the appearance of all your cards however you wish with URL par | `border_color` | Card's border color. Does not apply when `hide_border` is enabled. | string (hex color) | `e4e2e2` | | `bg_color` | Card's background color. | string (hex color or a gradient in the form of *angle,start,end*) | `fffefe` | | `hide_border` | Hides the card's border. | boolean | `false` | -| `theme` | Name of the theme, choose from [all available themes](../backend/themes/README.md). | enum | `default` | +| `theme` | Name of the theme, choose from [all available themes](../packages/core/src/themes/README.md). | enum | `default` | | `cache_seconds` | Sets the cache header manually (min: 21600, max: 86400). | integer | `21600` | | `locale` | Sets the language in the card, you can check full list of available locales [here](#available-locales). | enum | `en` | | `border_radius` | Corner rounding on the card. | number | `4.5` | diff --git a/docs/deploy.md b/docs/deploy.md index a837d1ec..637cb46a 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -123,6 +123,7 @@ Click on the deploy button to get started! 13. optional: add an SQL database; by using e.g. the ["Nile" integration](https://vercel.com/marketplace/nile) or by manually setting the environment variable `POSTGRES_URL` 14. optional: [create your own OAuth App](https://github.com/settings/developers) and set environment variables `OAUTH_REDIRECT_URI`, `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET` on Vercel accordingly 15. optional: in addition to the Vercel project based on the `apps/backend` folder, create a second project based on the `apps/frontend` folder. No environment variables needed. +16. optional: set the environment variable `TURBO_PLATFORM_ENV_DISABLED` to `true` to disable the build-time warning from [turbo](https://turborepo.dev/) about environment variables missing from "turbo.json" - This warning is not relevant in our project. diff --git a/eslint.config.js b/eslint.config.js index b1b9de20..270d6cbb 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url"; import { includeIgnoreFile } from "@eslint/compat"; import js from "@eslint/js"; import { defineConfig } from "eslint/config"; +import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript"; import { importX } from "eslint-plugin-import-x"; import { default as jsdoc } from "eslint-plugin-jsdoc"; import react from "eslint-plugin-react"; @@ -18,6 +19,25 @@ export default defineConfig( { extends: [importX.flatConfigs.recommended, importX.flatConfigs.typescript], + settings: { + "import-x/resolver-next": [ + createTypeScriptImportResolver({ + conditionNames: [ + /** Keep in sync with `tsconfig.base.json#customConditions` */ + "@stats/source", + + "types", + "import", + + "require", + "node", + "node-addons", + "browser", + "default", + ], + }), + ], + }, rules: { "import-x/consistent-type-specifier-style": ["error", "prefer-top-level"], "import-x/order": [ diff --git a/knip.jsonc b/knip.jsonc index 5de96712..1980b45b 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -2,21 +2,10 @@ "$schema": "./node_modules/knip/schema.json", "workspaces": { "apps/backend": { - "entry": ["api-renamed/*.js", "express.js", "tests/bench/*.bench.js"], - "ignoreFiles": [ - "_dot_vercel_copy/**/*" // Hopefully by using a npm package this file will be removed - ] + "entry": ["api-renamed/*.js", "express.js", "tests/bench/*.bench.js"] }, "apps/frontend": { - "entry": ["src/wakatime-override.ts"], - - "ignoreDependencies": [ - // below dependencies are added because backend folder is copied inside frontend folder, - // so some of his dependencies must be present here - "github-username-regex", - "emoji-name-map", - "word-wrap" - ] + "entry": ["src/wakatime-override.ts"] } } } diff --git a/package.json b/package.json index b0ed2b1c..53e0d770 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,44 @@ { - "name": "root", + "name": "@stats-organization/root", "private": true, "type": "module", - "packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017", + "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319", "devDependencies": { - "@eslint/compat": "2.0.2", - "@eslint/js": "9.39.3", - "@playwright/test": "1.58.2", - "@types/node": "24.10.13", - "eslint": "9.39.2", + "@eslint/compat": "2.0.3", + "@eslint/js": "10.0.1", + "@playwright/test": "1.59.1", + "@types/node": "24.12.0", + "@vitest/coverage-v8": "catalog:default", + "eslint": "10.1.0", "eslint-import-resolver-typescript": "4.4.4", - "eslint-plugin-import-x": "4.16.1", - "eslint-plugin-jsdoc": "62.7.1", + "eslint-plugin-import-x": "4.16.2", + "eslint-plugin-jsdoc": "62.9.0", "eslint-plugin-react": "7.37.5", "eslint-plugin-react-hooks": "7.0.1", - "globals": "17.3.0", + "globals": "17.4.0", "husky": "9.1.7", - "knip": "5.85.0", - "lint-staged": "16.2.7", + "knip": "6.2.0", + "lint-staged": "16.4.0", "prettier": "3.8.1", + "turbo": "2.9.3", "typescript": "5.9.3", - "typescript-eslint": "8.56.1" + "typescript-eslint": "8.58.0", + "vitest": "catalog:default" }, "scripts": { "prepare": "husky", + "build:packages": "turbo run build --filter=./packages/*", + "build:frontend": "turbo run build --filter=./apps/frontend", + "dev:frontend": "pnpm run --filter=./apps/frontend dev", + "test": "vitest", + "test:coverage": "vitest --config vitest.config.coverage.ts", "format": "prettier --write .", "format:check": "prettier --check .", - "lint": "eslint", - "lint:fix": "eslint --fix", + "lint": "turbo run lint", + "lint:eslint": "eslint", + "lint:eslint:fix": "eslint --fix", "lint:knip": "knip", - "typecheck": "tsc --build --noEmit" + "typecheck": "turbo run typecheck" }, "lint-staged": { "*.{js,jsx,ts,tsx,css,json,jsonc,yaml,yml}": "prettier --write" diff --git a/apps/backend/codecov.yml b/packages/core/codecov.yml similarity index 100% rename from apps/backend/codecov.yml rename to packages/core/codecov.yml diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..e9e10e70 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,94 @@ +{ + "name": "@stats-organization/github-readme-stats-core", + "version": "2.0.0", + "type": "module", + "homepage": "https://github-stats-extended.vercel.app/frontend", + "bugs": { + "url": "https://github.com/stats-organization/github-stats-extended/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/stats-organization/github-stats-extended.git", + "directory": "packages/core" + }, + "author": { + "name": "Anurag Hazra", + "url": "https://github.com/anuraghazra/" + }, + "contributors": [ + { + "name": "Rick Staa", + "url": "https://github.com/rickstaa" + }, + { + "name": "Alexandr Garbuzov", + "url": "https://github.com/qwerty541" + }, + { + "name": "Abhijit Gupta", + "url": "https://github.com/avgupta456" + }, + { + "name": "Marco Pasqualetti", + "url": "https://github.com/marcalexiei" + }, + { + "name": "martin-mfg", + "url": "https://github.com/martin-mfg" + } + ], + "license": "MIT", + "engines": { + "node": "24.x" + }, + "description": "Dynamically generate stats for your GitHub readme", + "keywords": [ + "github-readme-stats", + "readme-stats", + "cards", + "card-generator", + "github-stats", + "github-stats-extended", + "github-readme-stats-extended" + ], + "main": "./build/index.js", + "exports": { + ".": { + "@stats/source": "./src/index.ts", + "default": "./build/index.js" + } + }, + "files": [ + "build", + "src" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "test": "vitest", + "test:update:snapshot": "vitest -u", + "test:e2e": "vitest --config vitest.config.e2e.ts", + "bench": "vitest bench --run --config vitest.config.bench.ts", + "lint": "eslint", + "typecheck": "tsc -p tsconfig.typecheck.json", + "theme-readme-gen": "node scripts/generate-theme-doc", + "generate-langs-json": "node scripts/generate-langs-json" + }, + "devDependencies": { + "@testing-library/dom": "10.4.1", + "@testing-library/jest-dom": "6.9.1", + "@uppercod/css-to-object": "1.1.1", + "axios-mock-adapter": "2.1.0", + "js-yaml": "4.1.1", + "jsdom": "28.1.0", + "vitest": "catalog:default" + }, + "dependencies": { + "axios": "^1.13.5", + "emoji-name-map": "^2.0.3", + "github-username-regex": "^1.0.0", + "word-wrap": "^1.2.5" + } +} diff --git a/apps/backend/scripts/generate-langs-json.js b/packages/core/scripts/generate-langs-json.js similarity index 100% rename from apps/backend/scripts/generate-langs-json.js rename to packages/core/scripts/generate-langs-json.js diff --git a/apps/backend/scripts/generate-theme-doc.js b/packages/core/scripts/generate-theme-doc.js similarity index 96% rename from apps/backend/scripts/generate-theme-doc.js rename to packages/core/scripts/generate-theme-doc.js index 93c8e6a7..afd4b1b3 100644 --- a/apps/backend/scripts/generate-theme-doc.js +++ b/packages/core/scripts/generate-theme-doc.js @@ -1,8 +1,8 @@ import fs from "fs"; -import { themes } from "../themes/index.js"; +import { themes } from "../src/themes/index.js"; -const TARGET_FILE = "./themes/README.md"; +const TARGET_FILE = "./src/themes/README.md"; const REPO_CARD_LINKS_FLAG = ""; const STAT_CARD_LINKS_FLAG = ""; diff --git a/packages/core/src/api/gist.js b/packages/core/src/api/gist.js new file mode 100644 index 00000000..ebce051e --- /dev/null +++ b/packages/core/src/api/gist.js @@ -0,0 +1,97 @@ +// @ts-check + +import { renderGistCard } from "../cards/gist.js"; +import { + MissingParamError, + retrieveSecondaryMessage, +} from "../common/error.js"; +import { parseBoolean } from "../common/ops.js"; +import { renderError } from "../common/render.js"; +import { fetchGist } from "../fetchers/gist.js"; +import { isLocaleAvailable } from "../translations.js"; + +// @ts-ignore +export default async ( + { + id, + title_color, + icon_color, + text_color, + bg_color, + theme, + locale, + border_radius, + border_color, + show_owner, + hide_border, + }, + pat = null, +) => { + if (locale && !isLocaleAvailable(locale)) { + return { + status: "error - permanent", + content: renderError({ + message: "Something went wrong", + secondaryMessage: "Language not found", + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }), + }; + } + + try { + const gistData = await fetchGist(id, pat); + + return { + status: "success", + content: renderGistCard(gistData, { + title_color, + icon_color, + text_color, + bg_color, + theme, + border_radius, + border_color, + locale: locale ? locale.toLowerCase() : null, + show_owner: parseBoolean(show_owner), + hide_border: parseBoolean(hide_border), + }), + }; + } catch (err) { + if (err instanceof Error) { + return { + status: "error - temporary", + content: renderError({ + message: err.message, + secondaryMessage: retrieveSecondaryMessage(err), + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + show_repo_link: !(err instanceof MissingParamError), + }, + }), + }; + } + return { + status: "error - temporary", + content: renderError({ + message: "An unknown error occurred", + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }), + }; + } +}; diff --git a/apps/backend/api-renamed/index.js b/packages/core/src/api/index.js similarity index 71% rename from apps/backend/api-renamed/index.js rename to packages/core/src/api/index.js index 55ac2e72..5709e30a 100644 --- a/apps/backend/api-renamed/index.js +++ b/packages/core/src/api/index.js @@ -1,26 +1,18 @@ // @ts-check -import { renderStatsCard } from "../src/cards/stats.js"; -import { guardAccess } from "../src/common/access.js"; -import { - CACHE_TTL, - resolveCacheSeconds, - setCacheHeaders, - setErrorCacheHeaders, -} from "../src/common/cache.js"; -import { storeRequest } from "../src/common/database.js"; +import { renderStatsCard } from "../cards/stats.js"; import { MissingParamError, retrieveSecondaryMessage, -} from "../src/common/error.js"; -import { parseArray, parseBoolean } from "../src/common/ops.js"; -import { renderError } from "../src/common/render.js"; -import { fetchStats } from "../src/fetchers/stats.js"; -import { isLocaleAvailable } from "../src/translations.js"; +} from "../common/error.js"; +import { parseArray, parseBoolean } from "../common/ops.js"; +import { renderError } from "../common/render.js"; +import { fetchStats } from "../fetchers/stats.js"; +import { isLocaleAvailable } from "../translations.js"; // @ts-ignore -export default async (req, res) => { - const { +export default async ( + { username, repo, owner, @@ -40,7 +32,6 @@ export default async (req, res) => { text_bold, bg_color, theme, - cache_seconds, exclude_repo, custom_title, locale, @@ -52,28 +43,13 @@ export default async (req, res) => { border_color, rank_icon, show, - } = req.query; - res.setHeader("Content-Type", "image/svg+xml"); - - const access = guardAccess({ - res, - id: username, - type: "username", - colors: { - title_color, - text_color, - bg_color, - border_color, - theme, - }, - }); - if (!access.isPassed) { - return access.result; - } - + }, + pat = null, +) => { if (locale && !isLocaleAvailable(locale)) { - return res.send( - renderError({ + return { + status: "error - permanent", + content: renderError({ message: "Something went wrong", secondaryMessage: "Language not found", renderOptions: { @@ -84,7 +60,7 @@ export default async (req, res) => { theme, }, }), - ); + }; } const safePattern = /^[-\w/.,]+$/; @@ -93,8 +69,9 @@ export default async (req, res) => { (repo && !safePattern.test(repo)) || (owner && !safePattern.test(owner)) ) { - return res.send( - renderError({ + return { + status: "error - permanent", + content: renderError({ message: "Something went wrong", secondaryMessage: "Username, repository or owner contains unsafe characters", @@ -106,11 +83,10 @@ export default async (req, res) => { theme, }, }), - ); + }; } try { - await storeRequest(req); const showStats = parseArray(show); const repoOwner = parseArray(owner); let repository = parseArray(repo); @@ -135,18 +111,12 @@ export default async (req, res) => { showStats.includes("issues_authored"), showStats.includes("issues_commented"), parseArray(role), + pat, ); - const cacheSeconds = resolveCacheSeconds({ - requested: parseInt(cache_seconds, 10), - def: CACHE_TTL.STATS_CARD.DEFAULT, - min: CACHE_TTL.STATS_CARD.MIN, - max: CACHE_TTL.STATS_CARD.MAX, - }); - setCacheHeaders(res, cacheSeconds); - - return res.send( - renderStatsCard( + return { + status: "success", + content: renderStatsCard( stats, { hide: parseArray(hide), @@ -179,12 +149,12 @@ export default async (req, res) => { repository, repoOwner, ), - ); + }; } catch (err) { - setErrorCacheHeaders(res); if (err instanceof Error) { - return res.send( - renderError({ + return { + status: "error - temporary", + content: renderError({ message: err.message, secondaryMessage: retrieveSecondaryMessage(err), renderOptions: { @@ -196,10 +166,11 @@ export default async (req, res) => { show_repo_link: !(err instanceof MissingParamError), }, }), - ); + }; } - return res.send( - renderError({ + return { + status: "error - temporary", + content: renderError({ message: "An unknown error occurred", renderOptions: { title_color, @@ -209,6 +180,6 @@ export default async (req, res) => { theme, }, }), - ); + }; } }; diff --git a/apps/backend/api-renamed/pin.js b/packages/core/src/api/pin.js similarity index 63% rename from apps/backend/api-renamed/pin.js rename to packages/core/src/api/pin.js index b79a74f6..77107373 100644 --- a/apps/backend/api-renamed/pin.js +++ b/packages/core/src/api/pin.js @@ -1,26 +1,18 @@ // @ts-check -import { renderRepoCard } from "../src/cards/repo.js"; -import { guardAccess } from "../src/common/access.js"; -import { - CACHE_TTL, - resolveCacheSeconds, - setCacheHeaders, - setErrorCacheHeaders, -} from "../src/common/cache.js"; -import { storeRequest } from "../src/common/database.js"; +import { renderRepoCard } from "../cards/repo.js"; import { MissingParamError, retrieveSecondaryMessage, -} from "../src/common/error.js"; -import { parseArray, parseBoolean } from "../src/common/ops.js"; -import { renderError } from "../src/common/render.js"; -import { fetchRepo } from "../src/fetchers/repo.js"; -import { isLocaleAvailable } from "../src/translations.js"; +} from "../common/error.js"; +import { parseArray, parseBoolean } from "../common/ops.js"; +import { renderError } from "../common/render.js"; +import { fetchRepo } from "../fetchers/repo.js"; +import { isLocaleAvailable } from "../translations.js"; // @ts-ignore -export default async (req, res) => { - const { +export default async ( + { username, repo, hide_border, @@ -36,34 +28,17 @@ export default async (req, res) => { number_format, text_bold, line_height, - cache_seconds, locale, border_radius, border_color, description_lines_count, - } = req.query; - - res.setHeader("Content-Type", "image/svg+xml"); - - const access = guardAccess({ - res, - id: username, - type: "username", - colors: { - title_color, - text_color, - bg_color, - border_color, - theme, - }, - }); - if (!access.isPassed) { - return access.result; - } - + }, + pat = null, +) => { if (locale && !isLocaleAvailable(locale)) { - return res.send( - renderError({ + return { + status: "error - permanent", + content: renderError({ message: "Something went wrong", secondaryMessage: "Language not found", renderOptions: { @@ -74,7 +49,7 @@ export default async (req, res) => { theme, }, }), - ); + }; } const safePattern = /^[-\w/.,]+$/; @@ -82,8 +57,9 @@ export default async (req, res) => { (username && !safePattern.test(username)) || (repo && !safePattern.test(repo)) ) { - return res.send( - renderError({ + return { + status: "error - permanent", + content: renderError({ message: "Something went wrong", secondaryMessage: "Username or repository contains unsafe characters", renderOptions: { @@ -94,11 +70,10 @@ export default async (req, res) => { theme, }, }), - ); + }; } try { - await storeRequest(req); const showStats = parseArray(show); const repoData = await fetchRepo( username, @@ -108,19 +83,12 @@ export default async (req, res) => { showStats.includes("prs_reviewed"), showStats.includes("issues_authored"), showStats.includes("issues_commented"), + pat, ); - const cacheSeconds = resolveCacheSeconds({ - requested: parseInt(cache_seconds, 10), - def: CACHE_TTL.PIN_CARD.DEFAULT, - min: CACHE_TTL.PIN_CARD.MIN, - max: CACHE_TTL.PIN_CARD.MAX, - }); - - setCacheHeaders(res, cacheSeconds); - - return res.send( - renderRepoCard(repoData, { + return { + status: "success", + content: renderRepoCard(repoData, { hide_border: parseBoolean(hide_border), title_color, icon_color, @@ -140,12 +108,12 @@ export default async (req, res) => { locale: locale ? locale.toLowerCase() : null, description_lines_count, }), - ); + }; } catch (err) { - setErrorCacheHeaders(res); if (err instanceof Error) { - return res.send( - renderError({ + return { + status: "error - temporary", + content: renderError({ message: err.message, secondaryMessage: retrieveSecondaryMessage(err), renderOptions: { @@ -157,10 +125,11 @@ export default async (req, res) => { show_repo_link: !(err instanceof MissingParamError), }, }), - ); + }; } - return res.send( - renderError({ + return { + status: "error - temporary", + content: renderError({ message: "An unknown error occurred", renderOptions: { title_color, @@ -170,6 +139,6 @@ export default async (req, res) => { theme, }, }), - ); + }; } }; diff --git a/apps/backend/api-renamed/top-langs.js b/packages/core/src/api/top-langs.js similarity index 64% rename from apps/backend/api-renamed/top-langs.js rename to packages/core/src/api/top-langs.js index 056193c8..4972b6fd 100644 --- a/apps/backend/api-renamed/top-langs.js +++ b/packages/core/src/api/top-langs.js @@ -1,26 +1,18 @@ // @ts-check -import { renderTopLanguages } from "../src/cards/top-languages.js"; -import { guardAccess } from "../src/common/access.js"; -import { - CACHE_TTL, - resolveCacheSeconds, - setCacheHeaders, - setErrorCacheHeaders, -} from "../src/common/cache.js"; -import { storeRequest } from "../src/common/database.js"; +import { renderTopLanguages } from "../cards/top-languages.js"; import { MissingParamError, retrieveSecondaryMessage, -} from "../src/common/error.js"; -import { parseArray, parseBoolean } from "../src/common/ops.js"; -import { renderError } from "../src/common/render.js"; -import { fetchTopLanguages } from "../src/fetchers/top-languages.js"; -import { isLocaleAvailable } from "../src/translations.js"; +} from "../common/error.js"; +import { parseArray, parseBoolean } from "../common/ops.js"; +import { renderError } from "../common/render.js"; +import { fetchTopLanguages } from "../fetchers/top-languages.js"; +import { isLocaleAvailable } from "../translations.js"; // @ts-ignore -export default async (req, res) => { - const { +export default async ( + { username, hide, hide_title, @@ -31,7 +23,6 @@ export default async (req, res) => { bg_color, prog_bar_bg_color, theme, - cache_seconds, layout, langs_count, exclude_repo, @@ -45,28 +36,13 @@ export default async (req, res) => { disable_animations, hide_progress, stats_format, - } = req.query; - res.setHeader("Content-Type", "image/svg+xml"); - - const access = guardAccess({ - res, - id: username, - type: "username", - colors: { - title_color, - text_color, - bg_color, - border_color, - theme, - }, - }); - if (!access.isPassed) { - return access.result; - } - + }, + pat = null, +) => { if (locale && !isLocaleAvailable(locale)) { - return res.send( - renderError({ + return { + status: "error - permanent", + content: renderError({ message: "Something went wrong", secondaryMessage: "Locale not found", renderOptions: { @@ -77,7 +53,7 @@ export default async (req, res) => { theme, }, }), - ); + }; } if ( @@ -85,8 +61,9 @@ export default async (req, res) => { (typeof layout !== "string" || !["compact", "normal", "donut", "donut-vertical", "pie"].includes(layout)) ) { - return res.send( - renderError({ + return { + status: "error - permanent", + content: renderError({ message: "Something went wrong", secondaryMessage: "Incorrect layout input", renderOptions: { @@ -97,7 +74,7 @@ export default async (req, res) => { theme, }, }), - ); + }; } if ( @@ -105,8 +82,9 @@ export default async (req, res) => { (typeof stats_format !== "string" || !["bytes", "percentages"].includes(stats_format)) ) { - return res.send( - renderError({ + return { + status: "error - permanent", + content: renderError({ message: "Something went wrong", secondaryMessage: "Incorrect stats_format input", renderOptions: { @@ -117,29 +95,22 @@ export default async (req, res) => { theme, }, }), - ); + }; } try { - await storeRequest(req); const topLangs = await fetchTopLanguages( username, parseArray(exclude_repo), size_weight, count_weight, parseArray(role), + pat, ); - const cacheSeconds = resolveCacheSeconds({ - requested: parseInt(cache_seconds, 10), - def: CACHE_TTL.TOP_LANGS_CARD.DEFAULT, - min: CACHE_TTL.TOP_LANGS_CARD.MIN, - max: CACHE_TTL.TOP_LANGS_CARD.MAX, - }); - setCacheHeaders(res, cacheSeconds); - - return res.send( - renderTopLanguages(topLangs, { + return { + status: "success", + content: renderTopLanguages(topLangs, { custom_title, hide_title: parseBoolean(hide_title), hide_border: parseBoolean(hide_border), @@ -159,12 +130,12 @@ export default async (req, res) => { hide_progress: parseBoolean(hide_progress), stats_format, }), - ); + }; } catch (err) { - setErrorCacheHeaders(res); if (err instanceof Error) { - return res.send( - renderError({ + return { + status: "error - temporary", + content: renderError({ message: err.message, secondaryMessage: retrieveSecondaryMessage(err), renderOptions: { @@ -176,10 +147,11 @@ export default async (req, res) => { show_repo_link: !(err instanceof MissingParamError), }, }), - ); + }; } - return res.send( - renderError({ + return { + status: "error - temporary", + content: renderError({ message: "An unknown error occurred", renderOptions: { title_color, @@ -189,6 +161,6 @@ export default async (req, res) => { theme, }, }), - ); + }; } }; diff --git a/packages/core/src/api/wakatime.js b/packages/core/src/api/wakatime.js new file mode 100644 index 00000000..eaf02daf --- /dev/null +++ b/packages/core/src/api/wakatime.js @@ -0,0 +1,113 @@ +// @ts-check + +import { renderWakatimeCard } from "../cards/wakatime.js"; +import { + MissingParamError, + retrieveSecondaryMessage, +} from "../common/error.js"; +import { parseArray, parseBoolean } from "../common/ops.js"; +import { renderError } from "../common/render.js"; +import { fetchWakatimeStats } from "../fetchers/wakatime.js"; +import { isLocaleAvailable } from "../translations.js"; + +// @ts-ignore +export default async ({ + username, + title_color, + icon_color, + hide_border, + card_width, + line_height, + text_color, + bg_color, + theme, + hide_title, + hide_progress, + custom_title, + locale, + layout, + langs_count, + hide, + api_domain, + border_radius, + border_color, + display_format, + disable_animations, +}) => { + if (locale && !isLocaleAvailable(locale)) { + return { + status: "error - permanent", + content: renderError({ + message: "Something went wrong", + secondaryMessage: "Language not found", + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }), + }; + } + + try { + const stats = await fetchWakatimeStats({ username, api_domain }); + + return { + status: "success", + content: renderWakatimeCard(stats, { + custom_title, + hide_title: parseBoolean(hide_title), + hide_border: parseBoolean(hide_border), + card_width: parseInt(card_width, 10), + hide: parseArray(hide), + line_height, + title_color, + icon_color, + text_color, + bg_color, + theme, + hide_progress, + border_radius, + border_color, + locale: locale ? locale.toLowerCase() : null, + layout, + langs_count, + display_format, + disable_animations: parseBoolean(disable_animations), + }), + }; + } catch (err) { + if (err instanceof Error) { + return { + status: "error - temporary", + content: renderError({ + message: err.message, + secondaryMessage: retrieveSecondaryMessage(err), + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + show_repo_link: !(err instanceof MissingParamError), + }, + }), + }; + } + return { + status: "error - temporary", + content: renderError({ + message: "An unknown error occurred", + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }), + }; + } +}; diff --git a/apps/backend/src/calculateRank.js b/packages/core/src/calculateRank.js similarity index 100% rename from apps/backend/src/calculateRank.js rename to packages/core/src/calculateRank.js diff --git a/apps/backend/src/cards/gist.js b/packages/core/src/cards/gist.js similarity index 100% rename from apps/backend/src/cards/gist.js rename to packages/core/src/cards/gist.js diff --git a/apps/backend/src/cards/repo.js b/packages/core/src/cards/repo.js similarity index 100% rename from apps/backend/src/cards/repo.js rename to packages/core/src/cards/repo.js diff --git a/apps/backend/src/cards/stats.js b/packages/core/src/cards/stats.js similarity index 99% rename from apps/backend/src/cards/stats.js rename to packages/core/src/cards/stats.js index 09aef286..940271a3 100644 --- a/apps/backend/src/cards/stats.js +++ b/packages/core/src/cards/stats.js @@ -233,7 +233,7 @@ const getStyles = ({ transform: rotate(-90deg); animation: rankAnimation 1s forwards ease-in-out; } - ${process.env.NODE_ENV === "test" ? "" : getProgressAnimation({ progress })} + ${getProgressAnimation({ progress })} `; }; diff --git a/apps/backend/src/cards/top-languages.js b/packages/core/src/cards/top-languages.js similarity index 99% rename from apps/backend/src/cards/top-languages.js rename to packages/core/src/cards/top-languages.js index 85739c46..4b91a739 100644 --- a/apps/backend/src/cards/top-languages.js +++ b/packages/core/src/cards/top-languages.js @@ -647,7 +647,7 @@ const renderPieLayout = (langs, totalLanguageSize, statsFormat) => { const createDonutPaths = (cx, cy, radius, percentages) => { const paths = []; let startAngle = 0; - let endAngle = 0; + let endAngle; const totalPercent = percentages.reduce((acc, curr) => acc + curr, 0); for (let i = 0; i < percentages.length; i++) { @@ -840,7 +840,7 @@ const renderTopLanguages = (topLangs, options = {}) => { theme, }); - let finalLayout = ""; + let finalLayout; if (langs.length === 0) { height = COMPACT_LAYOUT_BASE_HEIGHT; finalLayout = noLanguagesDataNode({ diff --git a/apps/backend/src/cards/types.d.ts b/packages/core/src/cards/types.d.ts similarity index 88% rename from apps/backend/src/cards/types.d.ts rename to packages/core/src/cards/types.d.ts index 27e04c2a..1d72262e 100644 --- a/apps/backend/src/cards/types.d.ts +++ b/packages/core/src/cards/types.d.ts @@ -1,7 +1,7 @@ -type ThemeNames = keyof typeof import("../../themes/index.js"); +type ThemeNames = keyof typeof import("../themes/index.ts"); type RankIcon = "default" | "github" | "percentile"; -type CommonOptions = { +interface CommonOptions { title_color: string; icon_color: string; text_color: string; @@ -11,10 +11,10 @@ type CommonOptions = { border_color: string; locale: string; hide_border: boolean; -}; +} export type StatCardOptions = CommonOptions & { - hide: string[]; + hide: Array; show_icons: boolean; hide_title: boolean; card_width: number; @@ -29,14 +29,14 @@ export type StatCardOptions = CommonOptions & { ring_color: string; text_bold: boolean; rank_icon: RankIcon; - show: string[]; + show: Array; }; export type RepoCardOptions = CommonOptions & { show_owner: boolean; description_lines_count: number; card_width_input; - show: string[]; + show: Array; show_icons: boolean; number_format: string; text_bold: boolean; @@ -47,7 +47,7 @@ export type RepoCardOptions = CommonOptions & { export type TopLangOptions = CommonOptions & { hide_title: boolean; card_width: number; - hide: string[]; + hide: Array; layout: "compact" | "normal" | "donut" | "donut-vertical" | "pie"; custom_title: string; langs_count: number; @@ -59,7 +59,7 @@ export type TopLangOptions = CommonOptions & { export type WakaTimeOptions = CommonOptions & { hide_title: boolean; - hide: string[]; + hide: Array; card_width: number; line_height: string; hide_progress: boolean; diff --git a/apps/backend/src/cards/wakatime.js b/packages/core/src/cards/wakatime.js similarity index 99% rename from apps/backend/src/cards/wakatime.js rename to packages/core/src/cards/wakatime.js index 02eac539..f7eeeee3 100644 --- a/apps/backend/src/cards/wakatime.js +++ b/packages/core/src/cards/wakatime.js @@ -305,7 +305,7 @@ const renderWakatimeCard = (stats = {}, options = { hide: [] }) => { textColor, }); - let finalLayout = ""; + let finalLayout; // RENDER COMPACT LAYOUT if (layout === "compact") { diff --git a/apps/backend/src/common/Card.js b/packages/core/src/common/Card.js similarity index 98% rename from apps/backend/src/common/Card.js rename to packages/core/src/common/Card.js index 45bc81fe..6345b662 100644 --- a/apps/backend/src/common/Card.js +++ b/packages/core/src/common/Card.js @@ -230,7 +230,7 @@ class Card { } ${this.css} - ${process.env.NODE_ENV === "test" ? "" : this.getAnimations()} + ${this.getAnimations()} ${ this.animations === false ? `* { animation-duration: 0s !important; animation-delay: 0s !important; }` diff --git a/apps/backend/src/common/I18n.js b/packages/core/src/common/I18n.js similarity index 100% rename from apps/backend/src/common/I18n.js rename to packages/core/src/common/I18n.js diff --git a/apps/backend/src/common/color.js b/packages/core/src/common/color.js similarity index 98% rename from apps/backend/src/common/color.js rename to packages/core/src/common/color.js index 622ed161..bc1395e8 100644 --- a/apps/backend/src/common/color.js +++ b/packages/core/src/common/color.js @@ -1,6 +1,6 @@ // @ts-check -import { themes } from "../../themes/index.js"; +import { themes } from "../themes/index.js"; /** * Checks if a string is a valid hex color. diff --git a/packages/core/src/common/config.js b/packages/core/src/common/config.js new file mode 100644 index 00000000..32456ce8 --- /dev/null +++ b/packages/core/src/common/config.js @@ -0,0 +1,78 @@ +// @ts-check + +/** + * @param {string | undefined} value Comma-separated string. + * @returns {string[] | undefined} Parsed string values. + */ +const parseCsv = (value) => { + if (!value) { + return undefined; + } + return value.split(","); +}; + +/** + * @param {Record} env Environment variables to inspect. + * @returns {{name: string, value: string}[]} Personal access tokens found in the environment. + */ +const parsePATsFromEnv = (env) => { + return Object.keys(env) + .filter((key) => /PAT_\d*$/.exec(key)) + .map((name) => ({ + name, + value: env[name], + })); +}; + +/** + * @returns {Record} `process.env` if available, otherwise `{}`. + */ +const getDefaultEnv = () => { + if (typeof process !== "undefined" && process?.env) { + return process.env; + } + return {}; +}; + +/** + * @param {Partial>} config (Partial) config values to normalize. + * @returns {{ + * whitelist: string[] | undefined, + * gistWhitelist: string[] | undefined, + * excludeRepositories: string[], + * fetchMultiPageStars: string | undefined, + * pats: {name: string, value: string}[], + * }} Normalized config object with defaults applied. + */ +const normalizeConfig = (config = {}) => { + return { + whitelist: config.whitelist, + gistWhitelist: config.gistWhitelist, + excludeRepositories: config.excludeRepositories || [], + fetchMultiPageStars: config.fetchMultiPageStars, + pats: config.pats || [], + }; +}; + +let currentConfig; + +/** + * @param {Record} env Environment variables used to build the runtime config. + */ +export const loadConfigFromEnv = (env = getDefaultEnv()) => { + const whitelist = parseCsv(env.WHITELIST); + const gistWhitelist = parseCsv(env.GIST_WHITELIST); + const excludeRepositories = parseCsv(env.EXCLUDE_REPO) || []; + + currentConfig = normalizeConfig({ + whitelist, + gistWhitelist, + excludeRepositories, + fetchMultiPageStars: env.FETCH_MULTI_PAGE_STARS, + pats: parsePATsFromEnv(env), + }); +}; + +loadConfigFromEnv(); + +export const getConfig = () => currentConfig; diff --git a/apps/backend/src/common/constants.js b/packages/core/src/common/constants.js similarity index 100% rename from apps/backend/src/common/constants.js rename to packages/core/src/common/constants.js diff --git a/apps/backend/src/common/error.js b/packages/core/src/common/error.js similarity index 100% rename from apps/backend/src/common/error.js rename to packages/core/src/common/error.js diff --git a/apps/backend/src/common/fmt.js b/packages/core/src/common/fmt.js similarity index 99% rename from apps/backend/src/common/fmt.js rename to packages/core/src/common/fmt.js index 16c80b8b..5bc3ad14 100644 --- a/apps/backend/src/common/fmt.js +++ b/packages/core/src/common/fmt.js @@ -66,7 +66,7 @@ const wrapTextMultiline = (text, width = 59, maxLines = 3) => { const encoded = encodeHTML(text); const isChinese = encoded.includes(fullWidthComma); - let wrapped = []; + let wrapped; if (isChinese) { wrapped = encoded.split(fullWidthComma); // Chinese full punctuation diff --git a/apps/backend/src/common/html.js b/packages/core/src/common/html.js similarity index 100% rename from apps/backend/src/common/html.js rename to packages/core/src/common/html.js diff --git a/apps/backend/src/common/http.js b/packages/core/src/common/http.js similarity index 100% rename from apps/backend/src/common/http.js rename to packages/core/src/common/http.js diff --git a/apps/backend/src/common/icons.js b/packages/core/src/common/icons.js similarity index 100% rename from apps/backend/src/common/icons.js rename to packages/core/src/common/icons.js diff --git a/apps/backend/src/common/languageColors.json b/packages/core/src/common/languageColors.json similarity index 99% rename from apps/backend/src/common/languageColors.json rename to packages/core/src/common/languageColors.json index 63bbbe94..86adccb2 100644 --- a/apps/backend/src/common/languageColors.json +++ b/packages/core/src/common/languageColors.json @@ -171,6 +171,7 @@ "Faust": "#c37240", "Fennel": "#fff3d7", "Filebench WML": "#F6B900", + "FlatBuffers": "#ed284a", "Flix": "#d44a45", "Fluent": "#ffcc33", "Forth": "#341708", @@ -323,6 +324,7 @@ "LigoLANG": "#0e74ff", "LilyPond": "#9ccc7c", "Liquid": "#67b8de", + "Liquidsoap": "#990066", "Literate Agda": "#315665", "Literate CoffeeScript": "#244776", "Literate Haskell": "#5e5086", @@ -348,6 +350,7 @@ "Mask": "#f97732", "Mathematical Programming System": "#0530ad", "Max": "#c4a79c", + "MeTTa": "#6a5acd", "Mercury": "#ff2b2b", "Mermaid": "#ff3670", "Meson": "#007800", diff --git a/apps/backend/src/common/log.js b/packages/core/src/common/log.js similarity index 57% rename from apps/backend/src/common/log.js rename to packages/core/src/common/log.js index 5836d50a..88cf7dd9 100644 --- a/apps/backend/src/common/log.js +++ b/packages/core/src/common/log.js @@ -1,13 +1,9 @@ // @ts-check - -const noop = () => {}; - /** * Return console instance based on the environment. * * @type {Console | {log: () => void, error: () => void}} */ -const logger = - process.env.NODE_ENV === "test" ? { log: noop, error: noop } : console; +const logger = console; export { logger }; diff --git a/apps/backend/src/common/ops.js b/packages/core/src/common/ops.js similarity index 100% rename from apps/backend/src/common/ops.js rename to packages/core/src/common/ops.js diff --git a/apps/backend/src/common/render.js b/packages/core/src/common/render.js similarity index 99% rename from apps/backend/src/common/render.js rename to packages/core/src/common/render.js index 36533464..7aba75c0 100644 --- a/apps/backend/src/common/render.js +++ b/packages/core/src/common/render.js @@ -229,7 +229,6 @@ const measureText = (str, fontSize = 10) => { }; export { - ERROR_CARD_LENGTH, renderError, createLanguageNode, createProgressNode, diff --git a/apps/backend/src/common/retryer.js b/packages/core/src/common/retryer.js similarity index 83% rename from apps/backend/src/common/retryer.js rename to packages/core/src/common/retryer.js index 4661deff..79d294dc 100644 --- a/apps/backend/src/common/retryer.js +++ b/packages/core/src/common/retryer.js @@ -1,6 +1,6 @@ // @ts-check -import { getUserAccessByName } from "./database.js"; +import { getConfig } from "./config.js"; import { CustomError } from "./error.js"; import { logger } from "./log.js"; @@ -27,24 +27,16 @@ function getRandomInt(max) { * Try to execute the fetcher function until it succeeds or the max number of retries is reached. * * @param {FetcherFunction} fetcher The fetcher function. - * @param {string?} username GitHub username of the user whose PAT to use, if available * @param {any} variables Object with arguments to pass to the fetcher function. + * @param {string | null} pat Optional PAT override. * @returns {Promise} The response from the fetcher function. */ -const retryer = async (fetcher, username, variables) => { - let userPAT; - if (username) { - userPAT = await getUserAccessByName(username); - } - +const retryer = async (fetcher, variables, pat = null) => { let PATs; - if (userPAT?.token) { - PATs = [{ name: `USER_${username}`, value: userPAT.token }]; + if (pat) { + PATs = [{ name: "user PAT from database", value: pat }]; } else { - const patNames = Object.keys(process.env).filter((key) => - /PAT_\d*$/.exec(key), - ); - PATs = patNames.map((name) => ({ name, value: process.env[name] })); + PATs = getConfig().pats; } if (!PATs.length) { @@ -110,4 +102,3 @@ const retryer = async (fetcher, username, variables) => { }; export { retryer }; -export default retryer; diff --git a/apps/backend/src/fetchers/gist.js b/packages/core/src/fetchers/gist.js similarity index 94% rename from apps/backend/src/fetchers/gist.js rename to packages/core/src/fetchers/gist.js index e80264ff..450b1711 100644 --- a/apps/backend/src/fetchers/gist.js +++ b/packages/core/src/fetchers/gist.js @@ -84,13 +84,14 @@ const calculatePrimaryLanguage = (files) => { * Fetch GitHub gist information by given username and ID. * * @param {string} id GitHub gist ID. + * @param {string | null} pat Optional PAT override. * @returns {Promise} Gist data. */ -const fetchGist = async (id) => { +const fetchGist = async (id, pat = null) => { if (!id) { throw new MissingParamError(["id"], "/api/gist?id=GIST_ID"); } - const res = await retryer(fetcher, null, { gistName: id }); + const res = await retryer(fetcher, { gistName: id }, pat); if (res.data.errors) { throw new Error(res.data.errors[0].message); } diff --git a/apps/backend/src/fetchers/repo.js b/packages/core/src/fetchers/repo.js similarity index 97% rename from apps/backend/src/fetchers/repo.js rename to packages/core/src/fetchers/repo.js index 90b269c4..96d727f6 100644 --- a/apps/backend/src/fetchers/repo.js +++ b/packages/core/src/fetchers/repo.js @@ -76,6 +76,7 @@ const fetchRepo = async ( include_prs_reviewed = false, include_issues_authored = false, include_issues_commented = false, + pat = null, ) => { let owner = username; if (reponame && reponame.includes("/")) { @@ -100,7 +101,7 @@ const fetchRepo = async ( throw new MissingParamError(["repo"], urlExample); } - let res = await retryer(fetcher, username, { login: owner, repo: reponame }); + let res = await retryer(fetcher, { login: owner, repo: reponame }, pat); const data = res.data.data; @@ -124,6 +125,7 @@ const fetchRepo = async ( include_prs_reviewed, include_issues_authored, include_issues_commented, + pat, ); return { ...repoUserStats, @@ -148,6 +150,7 @@ const fetchRepo = async ( include_prs_reviewed, include_issues_authored, include_issues_commented, + pat, ); return { ...repoUserStats, diff --git a/apps/backend/src/fetchers/stats.js b/packages/core/src/fetchers/stats.js similarity index 91% rename from apps/backend/src/fetchers/stats.js rename to packages/core/src/fetchers/stats.js index f4a6be86..1e2f3a55 100644 --- a/apps/backend/src/fetchers/stats.js +++ b/packages/core/src/fetchers/stats.js @@ -4,7 +4,7 @@ import axios from "axios"; import githubUsernameRegex from "github-username-regex"; import { calculateRank } from "../calculateRank.js"; -import { excludeRepositories } from "../common/envs.js"; +import { getConfig } from "../common/config.js"; import { CustomError, MissingParamError } from "../common/error.js"; import { wrapTextMultiline } from "../common/fmt.js"; import { request } from "../common/http.js"; @@ -106,7 +106,8 @@ const fetcher = (variables, token) => { * @param {boolean} variables.includeDiscussions Include discussions. * @param {boolean} variables.includeDiscussionsAnswers Include discussions answers. * @param {string|undefined} variables.startTime Time to start the count of total commits. - * @param {string[]} ownerAffiliations The owner affiliations to filter by. Default: OWNER. + * @param {string[]} variables.ownerAffiliations The owner affiliations to filter by. Default: OWNER. + * @param {string | null} variables.pat PAT override or null. * @returns {Promise} Axios response. * * @description This function supports multi-page fetching if the 'FETCH_MULTI_PAGE_STARS' environment variable is set to true or a limit of fetches. @@ -118,6 +119,7 @@ const statsFetcher = async ({ includeDiscussionsAnswers, startTime, ownerAffiliations, + pat, }) => { let stats; let hasNextPage = true; @@ -134,7 +136,7 @@ const statsFetcher = async ({ startTime, ownerAffiliations, }; - let res = await retryer(fetcher, username, variables); + let res = await retryer(fetcher, variables, pat); if (res.data.errors) { return res; } @@ -161,8 +163,8 @@ const statsFetcher = async ({ ); hasNextPage = - (process.env.FETCH_MULTI_PAGE_STARS === "true" || - process.env.FETCH_MULTI_PAGE_STARS > fetchedPages) && + (getConfig().fetchMultiPageStars === "true" || + getConfig().fetchMultiPageStars > fetchedPages) && repoNodes.length === repoNodesWithStars.length && res.data.data.user.repositories.pageInfo.hasNextPage; @@ -207,7 +209,7 @@ const fetchTotalItems = (variables, token) => { * @description Done like this because the GitHub API does not provide a way to fetch all the commits. See * #92#issuecomment-661026467 and #211 for more information. */ -const totalItemsFetcher = async (username, repo, owner, type, filter) => { +const totalItemsFetcher = async (username, repo, owner, type, filter, pat) => { if (!githubUsernameRegex.test(username)) { logger.log("Invalid username provided."); throw new Error("Invalid username provided."); @@ -215,16 +217,20 @@ const totalItemsFetcher = async (username, repo, owner, type, filter) => { let res; try { - res = await retryer(fetchTotalItems, username, { - login: username, - repo, - owner, - type, - filter, - }); + res = await retryer( + fetchTotalItems, + { + login: username, + repo, + owner, + type, + filter, + }, + pat, + ); } catch (err) { logger.log(err); - throw new Error(err); + throw err; } const totalCount = res.data.total_count; @@ -247,6 +253,7 @@ const fetchRepoUserStats = async ( include_prs_reviewed, include_issues_authored, include_issues_commented, + pat, ) => { let stats = {}; if (include_prs_authored) { @@ -256,6 +263,7 @@ const fetchRepoUserStats = async ( owner, "issues", `author:${username}+type:pr`, + pat, ); } if (include_prs_commented) { @@ -265,6 +273,7 @@ const fetchRepoUserStats = async ( owner, "issues", `commenter:${username}+-author:${username}+type:pr`, + pat, ); } if (include_prs_reviewed) { @@ -274,6 +283,7 @@ const fetchRepoUserStats = async ( owner, "issues", `reviewed-by:${username}+-author:${username}+type:pr`, + pat, ); } if (include_issues_authored) { @@ -283,6 +293,7 @@ const fetchRepoUserStats = async ( owner, "issues", `author:${username}+type:issue`, + pat, ); } if (include_issues_commented) { @@ -292,6 +303,7 @@ const fetchRepoUserStats = async ( owner, "issues", `commenter:${username}+-author:${username}+type:issue`, + pat, ); } return stats; @@ -326,6 +338,7 @@ const fetchStats = async ( include_issues_authored = false, include_issues_commented = false, ownerAffiliations = [], + pat = null, ) => { if (!username) { throw new MissingParamError(["username"]); @@ -352,14 +365,17 @@ const fetchStats = async ( }; ownerAffiliations = parseOwnerAffiliations(ownerAffiliations); - let res = await statsFetcher({ - username, - includeMergedPullRequests: include_merged_pull_requests, - includeDiscussions: include_discussions, - includeDiscussionsAnswers: include_discussions_answers, - startTime: commits_year ? `${commits_year}-01-01T00:00:00Z` : undefined, - ownerAffiliations, - }); + let res = await statsFetcher( + { + username, + includeMergedPullRequests: include_merged_pull_requests, + includeDiscussions: include_discussions, + includeDiscussionsAnswers: include_discussions_answers, + startTime: commits_year ? `${commits_year}-01-01T00:00:00Z` : undefined, + ownerAffiliations, + }, + pat, + ); // Catch GraphQL errors. if (res.data.errors) { @@ -394,6 +410,7 @@ const fetchStats = async ( owner, "commits", `author:${username}`, + pat, ); } else { stats.totalCommits = user.commits.totalCommitContributions; @@ -407,6 +424,7 @@ const fetchStats = async ( include_prs_reviewed, include_issues_authored, include_issues_commented, + pat, ); Object.assign(stats, repoUserStats); @@ -429,7 +447,10 @@ const fetchStats = async ( stats.contributedTo = user.repositoriesContributedTo.totalCount; // Retrieve stars while filtering out repositories to be hidden. - const allExcludedRepos = [...exclude_repo, ...excludeRepositories]; + const allExcludedRepos = [ + ...exclude_repo, + ...getConfig().excludeRepositories, + ]; let repoToHide = new Set(allExcludedRepos); stats.totalStars = user.repositories.nodes diff --git a/apps/backend/src/fetchers/top-languages.js b/packages/core/src/fetchers/top-languages.js similarity index 93% rename from apps/backend/src/fetchers/top-languages.js rename to packages/core/src/fetchers/top-languages.js index dad43ebe..541bb3b1 100644 --- a/apps/backend/src/fetchers/top-languages.js +++ b/packages/core/src/fetchers/top-languages.js @@ -1,6 +1,6 @@ // @ts-check -import { excludeRepositories } from "../common/envs.js"; +import { getConfig } from "../common/config.js"; import { CustomError, MissingParamError } from "../common/error.js"; import { wrapTextMultiline } from "../common/fmt.js"; import { request } from "../common/http.js"; @@ -59,6 +59,7 @@ const fetcher = (variables, token) => { * @param {number} size_weight Weightage to be given to size. * @param {number} count_weight Weightage to be given to count. * @param {string[]} ownerAffiliations The owner affiliations to filter by. Default: OWNER. + * @param {string|null} pat Optional PAT override. * @returns {Promise} Top languages data. */ const fetchTopLanguages = async ( @@ -67,16 +68,21 @@ const fetchTopLanguages = async ( size_weight = 1, count_weight = 0, ownerAffiliations = [], + pat = null, ) => { if (!username) { throw new MissingParamError(["username"]); } ownerAffiliations = parseOwnerAffiliations(ownerAffiliations); - const res = await retryer(fetcher, username, { - login: username, - ownerAffiliations, - }); + const res = await retryer( + fetcher, + { + login: username, + ownerAffiliations, + }, + pat, + ); if (res.data.errors) { logger.error(res.data.errors); @@ -101,7 +107,10 @@ const fetchTopLanguages = async ( let repoNodes = res.data.data.user.repositories.nodes; /** @type {Record} */ let repoToHide = {}; - const allExcludedRepos = [...exclude_repo, ...excludeRepositories]; + const allExcludedRepos = [ + ...exclude_repo, + ...getConfig().excludeRepositories, + ]; // populate repoToHide map for quick lookup // while filtering out diff --git a/apps/backend/src/fetchers/types.d.ts b/packages/core/src/fetchers/types.d.ts similarity index 89% rename from apps/backend/src/fetchers/types.d.ts rename to packages/core/src/fetchers/types.d.ts index 1588c921..b26b0661 100644 --- a/apps/backend/src/fetchers/types.d.ts +++ b/packages/core/src/fetchers/types.d.ts @@ -1,13 +1,13 @@ -export type GistData = { +export interface GistData { name: string; nameWithOwner: string; description: string | null; language: string | null; starsCount: number; forksCount: number; -}; +} -export type RepositoryData = { +export interface RepositoryData { name: string; nameWithOwner: string; isPrivate: boolean; @@ -27,9 +27,9 @@ export type RepositoryData = { totalPRsReviewed: number; totalIssuesAuthored: number; totalIssuesCommented: number; -}; +} -export type StatsData = { +export interface StatsData { name: string; totalPRs: number; totalPRsMerged: number; @@ -47,18 +47,18 @@ export type StatsData = { totalIssuesAuthored: number; totalIssuesCommented: number; rank: { level: string; percentile: number }; -}; +} -export type Lang = { +export interface Lang { name: string; color: string; size: number; -}; +} export type TopLangData = Record; -export type WakaTimeData = { - categories: { +export interface WakaTimeData { + categories: Array<{ digital: string; hours: number; minutes: number; @@ -66,12 +66,12 @@ export type WakaTimeData = { percent: number; text: string; total_seconds: number; - }[]; + }>; daily_average: number; daily_average_including_other_language: number; days_including_holidays: number; days_minus_holidays: number; - editors: { + editors: Array<{ digital: string; hours: number; minutes: number; @@ -79,7 +79,7 @@ export type WakaTimeData = { percent: number; text: string; total_seconds: number; - }[]; + }>; holidays: number; human_readable_daily_average: string; human_readable_daily_average_including_other_language: string; @@ -92,7 +92,7 @@ export type WakaTimeData = { is_other_usage_visible: boolean; is_stuck: boolean; is_up_to_date: boolean; - languages: { + languages: Array<{ digital: string; hours: number; minutes: number; @@ -100,8 +100,8 @@ export type WakaTimeData = { percent: number; text: string; total_seconds: number; - }[]; - operating_systems: { + }>; + operating_systems: Array<{ digital: string; hours: number; minutes: number; @@ -109,7 +109,7 @@ export type WakaTimeData = { percent: number; text: string; total_seconds: number; - }[]; + }>; percent_calculated: number; range: string; status: string; @@ -119,10 +119,10 @@ export type WakaTimeData = { user_id: string; username: string; writes_only: boolean; -}; +} -export type WakaTimeLang = { +export interface WakaTimeLang { name: string; text: string; percent: number; -}; +} diff --git a/apps/backend/src/fetchers/wakatime.js b/packages/core/src/fetchers/wakatime.js similarity index 100% rename from apps/backend/src/fetchers/wakatime.js rename to packages/core/src/fetchers/wakatime.js diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 00000000..23606735 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,25 @@ +/** + * We need this file to be in ts to allow custom conditions to work + * The package will be converted + * + * @todo https://github.com/stats-organization/github-stats-extended/issues/140 + */ +export { fetchWakatimeStats } from "./fetchers/wakatime.js"; +export { retryer } from "./common/retryer.js"; + +export { renderError } from "./common/render.js"; + +export { dateDiff, clampValue } from "./common/ops.js"; + +export { logger } from "./common/log.js"; +export { request } from "./common/http.js"; + +export { default as gist } from "./api/gist.js"; +export { default as api } from "./api/index.js"; +export { default as pin } from "./api/pin.js"; +export { default as topLangs } from "./api/top-langs.js"; +export { default as wakatime } from "./api/wakatime.js"; + +export { getConfig } from "./common/config.js"; + +export { themes } from "./themes/index.js"; diff --git a/apps/backend/themes/README.md b/packages/core/src/themes/README.md similarity index 100% rename from apps/backend/themes/README.md rename to packages/core/src/themes/README.md diff --git a/apps/backend/themes/index.js b/packages/core/src/themes/index.ts similarity index 97% rename from apps/backend/themes/index.js rename to packages/core/src/themes/index.ts index 98fab2d3..565cd3a9 100644 --- a/apps/backend/themes/index.js +++ b/packages/core/src/themes/index.ts @@ -1,3 +1,14 @@ +interface Theme { + title_color: string; + icon_color: string; + text_color: string; + bg_color: string; + border_color?: string; +} + +/** + * Collection of available themes. + */ export const themes = { default: { title_color: "2f80ed", @@ -462,4 +473,4 @@ export const themes = { icon_color: "ffffff", bg_color: "35,4158d0,c850c0,ffcc70", }, -}; +} as const satisfies Record; diff --git a/apps/backend/src/translations.js b/packages/core/src/translations.js similarity index 100% rename from apps/backend/src/translations.js rename to packages/core/src/translations.js diff --git a/apps/backend/tests/__snapshots__/renderWakatimeCard.test.js.snap b/packages/core/tests/__snapshots__/renderWakatimeCard.test.js.snap similarity index 91% rename from apps/backend/tests/__snapshots__/renderWakatimeCard.test.js.snap rename to packages/core/tests/__snapshots__/renderWakatimeCard.test.js.snap index c60b504f..206a71b8 100644 --- a/apps/backend/tests/__snapshots__/renderWakatimeCard.test.js.snap +++ b/packages/core/tests/__snapshots__/renderWakatimeCard.test.js.snap @@ -65,6 +65,24 @@ exports[`Test Render WakaTime Card > should render correctly 1`] = ` + /* Animations */ + @keyframes scaleInAnimation { + from { + transform: translate(-5px, 5px) scale(0); + } + to { + transform: translate(-5px, 5px) scale(1); + } + } + @keyframes fadeInAnimation { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + @@ -223,6 +241,24 @@ exports[`Test Render WakaTime Card > should render correctly with compact layout + /* Animations */ + @keyframes scaleInAnimation { + from { + transform: translate(-5px, 5px) scale(0); + } + to { + transform: translate(-5px, 5px) scale(1); + } + } + @keyframes fadeInAnimation { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + @@ -375,6 +411,24 @@ exports[`Test Render WakaTime Card > should render correctly with compact layout + /* Animations */ + @keyframes scaleInAnimation { + from { + transform: translate(-5px, 5px) scale(0); + } + to { + transform: translate(-5px, 5px) scale(1); + } + } + @keyframes fadeInAnimation { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + @@ -527,6 +581,24 @@ exports[`Test Render WakaTime Card > should render correctly with percent displa + /* Animations */ + @keyframes scaleInAnimation { + from { + transform: translate(-5px, 5px) scale(0); + } + to { + transform: translate(-5px, 5px) scale(1); + } + } + @keyframes fadeInAnimation { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + diff --git a/packages/core/tests/_setup.js b/packages/core/tests/_setup.js new file mode 100644 index 00000000..39566617 --- /dev/null +++ b/packages/core/tests/_setup.js @@ -0,0 +1,7 @@ +import * as matchers from "@testing-library/jest-dom/matchers"; +import { expect } from "vitest"; + +expect.extend(matchers); + +process.env.PAT_1 = "dummyPAT1"; +process.env.PAT_2 = "dummyPAT2"; diff --git a/apps/backend/tests/calculateRank.test.js b/packages/core/tests/calculateRank.test.js similarity index 98% rename from apps/backend/tests/calculateRank.test.js rename to packages/core/tests/calculateRank.test.js index 3997f666..e432cef1 100644 --- a/apps/backend/tests/calculateRank.test.js +++ b/packages/core/tests/calculateRank.test.js @@ -4,8 +4,6 @@ import { calculateRank } from "../src/calculateRank.js"; import { approxNumber } from "./utils.js"; -import "@testing-library/jest-dom/vitest"; - describe("Test calculateRank", () => { it("new user gets C rank", () => { expect( diff --git a/apps/backend/tests/card.test.js b/packages/core/tests/card.test.js similarity index 99% rename from apps/backend/tests/card.test.js rename to packages/core/tests/card.test.js index 960bc344..c9d289d4 100644 --- a/apps/backend/tests/card.test.js +++ b/packages/core/tests/card.test.js @@ -6,8 +6,6 @@ import { Card } from "../src/common/Card.js"; import { getCardColors } from "../src/common/color.js"; import { icons } from "../src/common/icons.js"; -import "@testing-library/jest-dom/vitest"; - describe("Card", () => { it("should hide border", () => { const card = new Card({}); diff --git a/apps/backend/tests/color.test.js b/packages/core/tests/color.test.js similarity index 100% rename from apps/backend/tests/color.test.js rename to packages/core/tests/color.test.js diff --git a/apps/backend/tests/fetchGist.test.js b/packages/core/tests/fetchGist.test.js similarity index 98% rename from apps/backend/tests/fetchGist.test.js rename to packages/core/tests/fetchGist.test.js index ca0d41ed..a0336e67 100644 --- a/apps/backend/tests/fetchGist.test.js +++ b/packages/core/tests/fetchGist.test.js @@ -4,8 +4,6 @@ import { afterEach, describe, expect, it } from "vitest"; import { fetchGist } from "../src/fetchers/gist.js"; -import "@testing-library/jest-dom/vitest"; - const gist_data = { data: { viewer: { diff --git a/apps/backend/tests/fetchRepo.test.js b/packages/core/tests/fetchRepo.test.js similarity index 98% rename from apps/backend/tests/fetchRepo.test.js rename to packages/core/tests/fetchRepo.test.js index b98eca47..d72b1e4b 100644 --- a/apps/backend/tests/fetchRepo.test.js +++ b/packages/core/tests/fetchRepo.test.js @@ -4,8 +4,6 @@ import { afterEach, describe, expect, it } from "vitest"; import { fetchRepo } from "../src/fetchers/repo.js"; -import "@testing-library/jest-dom/vitest"; - const data_repo = { repository: { name: "convoychat", diff --git a/apps/backend/tests/fetchStats.test.js b/packages/core/tests/fetchStats.test.js similarity index 98% rename from apps/backend/tests/fetchStats.test.js rename to packages/core/tests/fetchStats.test.js index b4681f59..7d4531aa 100644 --- a/apps/backend/tests/fetchStats.test.js +++ b/packages/core/tests/fetchStats.test.js @@ -3,10 +3,9 @@ import MockAdapter from "axios-mock-adapter"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { calculateRank } from "../src/calculateRank.js"; +import { loadConfigFromEnv } from "../src/common/config.js"; import { fetchStats } from "../src/fetchers/stats.js"; -import "@testing-library/jest-dom/vitest"; - // Test parameters. const data_stats = { data: { @@ -107,6 +106,7 @@ const mock = new MockAdapter(axios); beforeEach(() => { process.env.FETCH_MULTI_PAGE_STARS = "false"; // Set to `false` to fetch only one page of stars. + loadConfigFromEnv(); mock.onPost("https://api.github.com/graphql").reply((cfg) => { let req = JSON.parse(cfg.data); @@ -313,6 +313,7 @@ describe("Test fetchStats", () => { it("should fetch two pages of stars if 'FETCH_MULTI_PAGE_STARS' env variable is set to `true`", async () => { process.env.FETCH_MULTI_PAGE_STARS = true; + loadConfigFromEnv(); let stats = await fetchStats("anuraghazra"); const rank = calculateRank({ @@ -349,6 +350,7 @@ describe("Test fetchStats", () => { it("should fetch one page of stars if 'FETCH_MULTI_PAGE_STARS' env variable is set to `false`", async () => { process.env.FETCH_MULTI_PAGE_STARS = "false"; + loadConfigFromEnv(); let stats = await fetchStats("anuraghazra"); const rank = calculateRank({ @@ -385,6 +387,7 @@ describe("Test fetchStats", () => { it("should fetch one page of stars if 'FETCH_MULTI_PAGE_STARS' env variable is not set", async () => { process.env.FETCH_MULTI_PAGE_STARS = undefined; + loadConfigFromEnv(); let stats = await fetchStats("anuraghazra"); const rank = calculateRank({ diff --git a/apps/backend/tests/fetchTopLanguages.test.js b/packages/core/tests/fetchTopLanguages.test.js similarity index 99% rename from apps/backend/tests/fetchTopLanguages.test.js rename to packages/core/tests/fetchTopLanguages.test.js index f4f24859..798c5a38 100644 --- a/apps/backend/tests/fetchTopLanguages.test.js +++ b/packages/core/tests/fetchTopLanguages.test.js @@ -6,8 +6,6 @@ import { fetchTopLanguages } from "../src/fetchers/top-languages.js"; import { approxNumber } from "./utils.js"; -import "@testing-library/jest-dom/vitest"; - const mock = new MockAdapter(axios); afterEach(() => { diff --git a/apps/backend/tests/fetchWakatime.test.js b/packages/core/tests/fetchWakatime.test.js similarity index 98% rename from apps/backend/tests/fetchWakatime.test.js rename to packages/core/tests/fetchWakatime.test.js index 5149b49f..cc1ac289 100644 --- a/apps/backend/tests/fetchWakatime.test.js +++ b/packages/core/tests/fetchWakatime.test.js @@ -4,8 +4,6 @@ import { afterEach, describe, expect, it } from "vitest"; import { fetchWakatimeStats } from "../src/fetchers/wakatime.js"; -import "@testing-library/jest-dom/vitest"; - const mock = new MockAdapter(axios); afterEach(() => { diff --git a/apps/backend/tests/flexLayout.test.js b/packages/core/tests/flexLayout.test.js similarity index 100% rename from apps/backend/tests/flexLayout.test.js rename to packages/core/tests/flexLayout.test.js diff --git a/apps/backend/tests/fmt.test.js b/packages/core/tests/fmt.test.js similarity index 100% rename from apps/backend/tests/fmt.test.js rename to packages/core/tests/fmt.test.js diff --git a/apps/backend/tests/html.test.js b/packages/core/tests/html.test.js similarity index 100% rename from apps/backend/tests/html.test.js rename to packages/core/tests/html.test.js diff --git a/apps/backend/tests/i18n.test.js b/packages/core/tests/i18n.test.js similarity index 100% rename from apps/backend/tests/i18n.test.js rename to packages/core/tests/i18n.test.js diff --git a/apps/backend/tests/ops.test.js b/packages/core/tests/ops.test.js similarity index 100% rename from apps/backend/tests/ops.test.js rename to packages/core/tests/ops.test.js diff --git a/apps/backend/tests/render.test.js b/packages/core/tests/render.test.js similarity index 95% rename from apps/backend/tests/render.test.js rename to packages/core/tests/render.test.js index d5cd6ca4..9eb55d01 100644 --- a/apps/backend/tests/render.test.js +++ b/packages/core/tests/render.test.js @@ -5,8 +5,6 @@ import { describe, expect, it } from "vitest"; import { renderError } from "../src/common/render.js"; -import "@testing-library/jest-dom/vitest"; - describe("Test render.js", () => { it("should test renderError", () => { document.body.innerHTML = renderError({ message: "Something went wrong" }); diff --git a/apps/backend/tests/renderGistCard.test.js b/packages/core/tests/renderGistCard.test.js similarity index 98% rename from apps/backend/tests/renderGistCard.test.js rename to packages/core/tests/renderGistCard.test.js index 28fbfa33..fe06b4b8 100644 --- a/apps/backend/tests/renderGistCard.test.js +++ b/packages/core/tests/renderGistCard.test.js @@ -3,9 +3,7 @@ import { cssToObject } from "@uppercod/css-to-object"; import { describe, expect, it } from "vitest"; import { renderGistCard } from "../src/cards/gist.js"; -import { themes } from "../themes/index.js"; - -import "@testing-library/jest-dom/vitest"; +import { themes } from "../src/themes/index.js"; /** * @type {import("../src/fetchers/gist").GistData} diff --git a/apps/backend/tests/renderRepoCard.test.js b/packages/core/tests/renderRepoCard.test.js similarity index 99% rename from apps/backend/tests/renderRepoCard.test.js rename to packages/core/tests/renderRepoCard.test.js index 4efecf57..f14f7998 100644 --- a/apps/backend/tests/renderRepoCard.test.js +++ b/packages/core/tests/renderRepoCard.test.js @@ -3,9 +3,7 @@ import { cssToObject } from "@uppercod/css-to-object"; import { describe, expect, it } from "vitest"; import { renderRepoCard } from "../src/cards/repo.js"; -import { themes } from "../themes/index.js"; - -import "@testing-library/jest-dom/vitest"; +import { themes } from "../src/themes/index.js"; const data_repo = { repository: { diff --git a/apps/backend/tests/renderStatsCard.test.js b/packages/core/tests/renderStatsCard.test.js similarity index 99% rename from apps/backend/tests/renderStatsCard.test.js rename to packages/core/tests/renderStatsCard.test.js index c21bcf45..c5e459fe 100644 --- a/apps/backend/tests/renderStatsCard.test.js +++ b/packages/core/tests/renderStatsCard.test.js @@ -8,9 +8,7 @@ import { describe, expect, it } from "vitest"; import { renderStatsCard } from "../src/cards/stats.js"; import { CustomError } from "../src/common/error.js"; -import { themes } from "../themes/index.js"; - -import "@testing-library/jest-dom/vitest"; +import { themes } from "../src/themes/index.js"; const stats = { name: "Anurag Hazra", diff --git a/apps/backend/tests/renderTopLanguagesCard.test.js b/packages/core/tests/renderTopLanguagesCard.test.js similarity index 99% rename from apps/backend/tests/renderTopLanguagesCard.test.js rename to packages/core/tests/renderTopLanguagesCard.test.js index ad50cc2d..3cd8c63e 100644 --- a/apps/backend/tests/renderTopLanguagesCard.test.js +++ b/packages/core/tests/renderTopLanguagesCard.test.js @@ -20,12 +20,10 @@ import { renderTopLanguages, trimTopLanguages, } from "../src/cards/top-languages.js"; -import { themes } from "../themes/index.js"; +import { themes } from "../src/themes/index.js"; import { approxNumber } from "./utils.js"; -import "@testing-library/jest-dom/vitest"; - const langs = { HTML: { color: "#0f0", diff --git a/apps/backend/tests/renderWakatimeCard.test.js b/packages/core/tests/renderWakatimeCard.test.js similarity index 98% rename from apps/backend/tests/renderWakatimeCard.test.js rename to packages/core/tests/renderWakatimeCard.test.js index 8c4971cf..ba8577c7 100644 --- a/apps/backend/tests/renderWakatimeCard.test.js +++ b/packages/core/tests/renderWakatimeCard.test.js @@ -5,8 +5,6 @@ import { renderWakatimeCard } from "../src/cards/wakatime.js"; import { wakaTimeData } from "./fetchWakatime.test.js"; -import "@testing-library/jest-dom/vitest"; - describe("Test Render WakaTime Card", () => { it("should render correctly", () => { const card = renderWakatimeCard(wakaTimeData.data); diff --git a/apps/backend/tests/retryer.test.js b/packages/core/tests/retryer.test.js similarity index 83% rename from apps/backend/tests/retryer.test.js rename to packages/core/tests/retryer.test.js index b82716b3..10572081 100644 --- a/apps/backend/tests/retryer.test.js +++ b/packages/core/tests/retryer.test.js @@ -2,8 +2,6 @@ import { describe, expect, it, vi } from "vitest"; -import "@testing-library/jest-dom/vitest"; - import { logger } from "../src/common/log.js"; import { retryer } from "../src/common/retryer.js"; @@ -51,6 +49,11 @@ const fetcherFailWithMessageBasedRateLimitErr = vi.fn( }, ); +const customFetcher = vi.fn((variables, token) => { + logger.log(variables, token); + return Promise.resolve({ data: { token } }); +}); + describe("Test Retryer", () => { it("retryer should return value and have zero retries on first try", async () => { let res = await retryer(fetcher, {}); @@ -82,4 +85,12 @@ describe("Test Retryer", () => { expect(err.message).toBe("Downtime due to GitHub API rate limiting"); } }); + + it("retryer should use injected PATs when provided", async () => { + const res = await retryer(customFetcher, {}, "user-pat-token"); + + expect(customFetcher).toHaveBeenCalledTimes(1); + expect(customFetcher).toHaveBeenCalledWith({}, "user-pat-token", 0); + expect(res).toStrictEqual({ data: { token: "user-pat-token" } }); + }); }); diff --git a/packages/core/tests/utils.js b/packages/core/tests/utils.js new file mode 100644 index 00000000..21b9f1f8 --- /dev/null +++ b/packages/core/tests/utils.js @@ -0,0 +1,41 @@ +// @ts-check + +/** + * Creates an asymmetric matcher for approximate numeric equality. + * + * This helper is intended for use in test frameworks (e.g., Jest) where + * values need to be compared within a configurable decimal precision + * instead of strict equality. + * + * The comparison succeeds when: + * + * |actual - expected| < 10^(-precision) + * + * For example, with `precision = 3`, values must be within `0.001`. + * + * @param {number} expected The expected numeric value to compare against. + * + * @param {number} [precision=10] + * The number of decimal places of tolerance. Higher values mean stricter + * comparison. Internally converted to epsilon = 10^-precision. + * + * @returns {{ + * asymmetricMatch(actual: unknown): boolean, + * toAsymmetricMatcher(): string + * }} An object implementing Jest-style asymmetric matcher methods. + * + */ +export function approxNumber(expected, precision = 10) { + return { + asymmetricMatch(actual) { + if (typeof actual !== "number" || typeof expected !== "number") { + return false; + } + const epsilon = Math.pow(10, -precision); + return Math.abs(actual - expected) < epsilon; + }, + toAsymmetricMatcher() { + return `≈ ${expected} (precision ${precision})`; + }, + }; +} diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json new file mode 100644 index 00000000..e58f87a7 --- /dev/null +++ b/packages/core/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": ["./tsconfig.json"], + "include": ["src"], + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "customConditions": [] + } +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..c1b9c120 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": ["../../tsconfig.base.json"], + "include": ["src", "tests", "vitest.config.ts"], + "compilerOptions": { + /** + * This workspace contains a lot of type errors. + * We will add typecheck later: + * @see https://github.com/stats-organization/github-stats-extended/issues/140 + * + * At the moment we just need to output `js` and `.d.ts` files + */ + "noCheck": true, + + "allowJs": true, + "checkJs": true, + + "module": "nodenext", + "moduleResolution": "nodenext", + "outDir": "build" + } +} diff --git a/packages/core/tsconfig.typecheck.json b/packages/core/tsconfig.typecheck.json new file mode 100644 index 00000000..4c13d7b1 --- /dev/null +++ b/packages/core/tsconfig.typecheck.json @@ -0,0 +1,7 @@ +{ + "extends": ["./tsconfig.json"], + "compilerOptions": { + "noEmit": true, + "customConditions": [] + } +} diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts new file mode 100644 index 00000000..4e11ea53 --- /dev/null +++ b/packages/core/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineProject } from "vitest/config"; + +export default defineProject({ + test: { + environment: "jsdom", + include: ["./tests/*.test.{ts,js}"], + setupFiles: ["./tests/_setup.js"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a23363aa..be659cdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,72 +7,182 @@ settings: catalogs: default: '@vitest/coverage-v8': - specifier: 4.0.18 - version: 4.0.18 + specifier: 4.1.2 + version: 4.1.2 vite: specifier: 7.3.1 version: 7.3.1 vitest: - specifier: 4.0.18 - version: 4.0.18 + specifier: 4.1.2 + version: 4.1.2 importers: .: devDependencies: '@eslint/compat': - specifier: 2.0.2 - version: 2.0.2(eslint@9.39.2(jiti@2.6.1)) + specifier: 2.0.3 + version: 2.0.3(eslint@10.1.0(jiti@2.6.1)) '@eslint/js': - specifier: 9.39.3 - version: 9.39.3 + specifier: 10.0.1 + version: 10.0.1(eslint@10.1.0(jiti@2.6.1)) '@playwright/test': - specifier: 1.58.2 - version: 1.58.2 + specifier: 1.59.1 + version: 1.59.1 '@types/node': - specifier: 24.10.13 - version: 24.10.13 + specifier: 24.12.0 + version: 24.12.0 + '@vitest/coverage-v8': + specifier: catalog:default + version: 4.1.2(vitest@4.1.2(@types/node@24.12.0)(happy-dom@20.6.1)(jsdom@28.1.0)(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3))) eslint: - specifier: 9.39.2 - version: 9.39.2(jiti@2.6.1) + specifier: 10.1.0 + version: 10.1.0(jiti@2.6.1) eslint-import-resolver-typescript: specifier: 4.4.4 - version: 4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + version: 4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1)))(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-import-x: - specifier: 4.16.1 - version: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) + specifier: 4.16.2 + version: 4.16.2(@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-jsdoc: - specifier: 62.7.1 - version: 62.7.1(eslint@9.39.2(jiti@2.6.1)) + specifier: 62.9.0 + version: 62.9.0(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-react: specifier: 7.37.5 - version: 7.37.5(eslint@9.39.2(jiti@2.6.1)) + version: 7.37.5(eslint@10.1.0(jiti@2.6.1)) eslint-plugin-react-hooks: specifier: 7.0.1 - version: 7.0.1(eslint@9.39.2(jiti@2.6.1)) + version: 7.0.1(eslint@10.1.0(jiti@2.6.1)) globals: - specifier: 17.3.0 - version: 17.3.0 + specifier: 17.4.0 + version: 17.4.0 husky: specifier: 9.1.7 version: 9.1.7 knip: - specifier: 5.85.0 - version: 5.85.0(@types/node@24.10.13)(typescript@5.9.3) + specifier: 6.2.0 + version: 6.2.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) lint-staged: - specifier: 16.2.7 - version: 16.2.7 + specifier: 16.4.0 + version: 16.4.0 prettier: specifier: 3.8.1 version: 3.8.1 + turbo: + specifier: 2.9.3 + version: 2.9.3 typescript: specifier: 5.9.3 version: 5.9.3 typescript-eslint: - specifier: 8.56.1 - version: 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + specifier: 8.58.0 + version: 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + vitest: + specifier: catalog:default + version: 4.1.2(@types/node@24.12.0)(happy-dom@20.6.1)(jsdom@28.1.0)(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3)) apps/backend: + dependencies: + '@stats-organization/github-readme-stats-core': + specifier: workspace:^ + version: link:../../packages/core + axios: + specifier: ^1.13.5 + version: 1.13.5 + pg: + specifier: ^8.18.0 + version: 8.18.0 + devDependencies: + axios-mock-adapter: + specifier: 2.1.0 + version: 2.1.0(axios@1.13.5) + express: + specifier: 5.2.1 + version: 5.2.1 + jsdom: + specifier: 28.1.0 + version: 28.1.0 + vitest: + specifier: catalog:default + version: 4.1.2(@types/node@25.5.0)(happy-dom@20.6.1)(jsdom@28.1.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3)) + + apps/frontend: + dependencies: + '@reduxjs/toolkit': + specifier: ^2.11.2 + version: 2.11.2(react-redux@9.2.0(@types/react@18.3.27)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + '@stats-organization/github-readme-stats-backend': + specifier: workspace:^ + version: link:../backend + '@stats-organization/github-readme-stats-core': + specifier: workspace:^ + version: link:../../packages/core + '@tailwindcss/vite': + specifier: ^4.2.1 + version: 4.2.1(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3)) + axios: + specifier: ^1 + version: 1.13.5 + axios-cache-interceptor: + specifier: ^1 + version: 1.11.4(axios@1.13.5) + daisyui: + specifier: ^5.5.19 + version: 5.5.19 + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@18.3.1) + react-loading-skeleton: + specifier: ^3.3.1 + version: 3.5.0(react@18.3.1) + react-redux: + specifier: ^9.2.0 + version: 9.2.0(@types/react@18.3.27)(react@18.3.1)(redux@5.0.1) + react-spinners: + specifier: ^0.17.0 + version: 0.17.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-toastify: + specifier: ^11.0.5 + version: 11.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + redux: + specifier: ^5.0.1 + version: 5.0.1 + save-svg-as-png: + specifier: ^1.4.17 + version: 1.4.17 + uuid: + specifier: ^13.0.0 + version: 13.0.0 + devDependencies: + '@types/react': + specifier: 18.3.27 + version: 18.3.27 + '@types/react-dom': + specifier: 18.3.7 + version: 18.3.7(@types/react@18.3.27) + '@vitejs/plugin-react-swc': + specifier: 4.2.3 + version: 4.2.3(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3)) + clsx: + specifier: 2.1.1 + version: 2.1.1 + tailwindcss: + specifier: 4.2.1 + version: 4.2.1 + vite: + specifier: catalog:default + version: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3) + vitest: + specifier: catalog:default + version: 4.1.2(@types/node@25.5.0)(happy-dom@20.6.1)(jsdom@28.1.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3)) + + packages/core: dependencies: axios: specifier: ^1.13.5 @@ -83,125 +193,31 @@ importers: github-username-regex: specifier: ^1.0.0 version: 1.0.0 - pg: - specifier: ^8.18.0 - version: 8.18.0 word-wrap: specifier: ^1.2.5 version: 1.2.5 devDependencies: '@testing-library/dom': - specifier: ^10.4.1 + specifier: 10.4.1 version: 10.4.1 '@testing-library/jest-dom': - specifier: ^6.9.1 + specifier: 6.9.1 version: 6.9.1 '@uppercod/css-to-object': - specifier: ^1.1.1 + specifier: 1.1.1 version: 1.1.1 - '@vitest/coverage-v8': - specifier: catalog:default - version: 4.0.18(vitest@4.0.18(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2)) axios-mock-adapter: - specifier: ^2.1.0 + specifier: 2.1.0 version: 2.1.0(axios@1.13.5) - express: - specifier: ^5.2.1 - version: 5.2.1 js-yaml: - specifier: ^4.1.1 + specifier: 4.1.1 version: 4.1.1 jsdom: specifier: 28.1.0 version: 28.1.0 vitest: specifier: catalog:default - version: 4.0.18(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2) - - apps/frontend: - dependencies: - '@reduxjs/toolkit': - specifier: 2.11.2 - version: 2.11.2(react-redux@9.2.0(@types/react@18.3.27)(react@18.3.1)(redux@5.0.1))(react@18.3.1) - '@tailwindcss/vite': - specifier: 4.2.1 - version: 4.2.1(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2)) - axios: - specifier: ^1 - version: 1.13.5 - axios-cache-interceptor: - specifier: ^1 - version: 1.11.4(axios@1.13.5) - daisyui: - specifier: 5.5.19 - version: 5.5.19 - emoji-name-map: - specifier: ^2.0.3 - version: 2.0.3 - github-username-regex: - specifier: ^1.0.0 - version: 1.0.0 - react: - specifier: 18.3.1 - version: 18.3.1 - react-dom: - specifier: 18.3.1 - version: 18.3.1(react@18.3.1) - react-icons: - specifier: ^5.5.0 - version: 5.5.0(react@18.3.1) - react-loading-skeleton: - specifier: ^3.3.1 - version: 3.5.0(react@18.3.1) - react-redux: - specifier: 9.2.0 - version: 9.2.0(@types/react@18.3.27)(react@18.3.1)(redux@5.0.1) - react-spinners: - specifier: ^0.17.0 - version: 0.17.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-toastify: - specifier: 11.0.5 - version: 11.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - redux: - specifier: 5.0.1 - version: 5.0.1 - save-svg-as-png: - specifier: ^1.4.17 - version: 1.4.17 - uuid: - specifier: 13.0.0 - version: 13.0.0 - word-wrap: - specifier: ^1.2.5 - version: 1.2.5 - devDependencies: - '@types/react': - specifier: 18.3.27 - version: 18.3.27 - '@types/react-dom': - specifier: 18.3.7 - version: 18.3.7(@types/react@18.3.27) - '@vitejs/plugin-react-swc': - specifier: 4.2.3 - version: 4.2.3(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2)) - clsx: - specifier: 2.1.1 - version: 2.1.1 - tailwindcss: - specifier: 4.2.1 - version: 4.2.1 - vite: - specifier: catalog:default - version: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2) - vite-plugin-node-polyfills: - specifier: 0.25.0 - version: 0.25.0(rollup@4.59.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2)) - vite-plugin-string-replace: - specifier: 1.1.5 - version: 1.1.5 - vitest: - specifier: catalog:default - version: 4.0.18(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2) + version: 4.1.2(@types/node@25.5.0)(happy-dom@20.6.1)(jsdom@28.1.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3)) packages: @@ -267,12 +283,12 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.6': - resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} hasBin: true @@ -331,17 +347,17 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} - '@emnapi/core@1.8.1': - resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@es-joy/jsdoccomment@0.84.0': - resolution: {integrity: sha512-0xew1CxOam0gV5OMjh2KjFQZsKL2bByX1+q4j3E73MpYIdyUxcZb/xQct9ccUb+ve5KGUYbCUxyPnYB7RbuP+w==} + '@es-joy/jsdoccomment@0.86.0': + resolution: {integrity: sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@es-joy/resolve.exports@1.2.0': @@ -514,8 +530,8 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/compat@2.0.2': - resolution: {integrity: sha512-pR1DoD0h3HfF675QZx0xsyrsU8q70Z/plx7880NOhS02NuWLgBCOMDL787nUeQ7EWLkxv3bPQJaarjcPQb2Dwg==} + '@eslint/compat@2.0.3': + resolution: {integrity: sha512-SjIJhGigp8hmd1YGIBwh7Ovri7Kisl42GYFjrOyHhtfYGGoLW6teYi/5p8W50KSsawUPpuLOSmsq1bD0NGQLBw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: eslint: ^8.40 || 9 || 10 @@ -523,41 +539,34 @@ packages: eslint: optional: true - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@1.1.0': - resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==} + '@eslint/config-array@0.23.3': + resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/eslintrc@3.3.4': - resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.5.3': + resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@1.1.1': + resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/js@9.39.3': - resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/object-schema@3.0.3': + resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.6.1': + resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@exodus/bytes@1.14.1': resolution: {integrity: sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==} @@ -606,8 +615,11 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@napi-rs/wasm-runtime@1.1.1': - resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -621,116 +633,249 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-resolver/binding-android-arm-eabi@11.19.0': - resolution: {integrity: sha512-dlMjjWE3h+qMujLp5nBX/x7R5ny+xfr4YtsyaMNuM5JImOtQBzpFxQr9kJOKGL+9RbaoTOXpt5KF05f9pnOsgw==} + '@oxc-parser/binding-android-arm-eabi@0.121.0': + resolution: {integrity: sha512-n07FQcySwOlzap424/PLMtOkbS7xOu8nsJduKL8P3COGHKgKoDYXwoAHCbChfgFpHnviehrLWIPX0lKGtbEk/A==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxc-resolver/binding-android-arm64@11.19.0': - resolution: {integrity: sha512-x5P0Y12oMcSC9PKkz1FtdVVLosXYi/05m+ufxPrUggd6vZRBPJhW4zZUsMVbz8dwwk71Dh0f6/2ntw3WPOq+Ig==} + '@oxc-parser/binding-android-arm64@0.121.0': + resolution: {integrity: sha512-/Dd1xIXboYAicw+twT2utxPD7bL8qh7d3ej0qvaYIMj3/EgIrGR+tSnjCUkiCT6g6uTC0neSS4JY8LxhdSU/sA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-resolver/binding-darwin-arm64@11.19.0': - resolution: {integrity: sha512-DjnuIPB60IQrVSCiuVBzN8/8AeeIjthdkk+dZYdZzgLeP2T5ZF41u50haJMtIdGr5cRzRH6zPV/gh6+RFjlvKA==} + '@oxc-parser/binding-darwin-arm64@0.121.0': + resolution: {integrity: sha512-A0jNEvv7QMtCO1yk205t3DWU9sWUjQ2KNF0hSVO5W9R9r/R1BIvzG01UQAfmtC0dQm7sCrs5puixurKSfr2bRQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxc-resolver/binding-darwin-x64@11.19.0': - resolution: {integrity: sha512-dVAqIZIIY7xOXCCV0nJPs8ExlYc6R7mcNpFobwNyE3qlXGbgvwb7Gl3iOumOiPBfF+sbJR3MMP7RAPfKqbvYyA==} + '@oxc-parser/binding-darwin-x64@0.121.0': + resolution: {integrity: sha512-SsHzipdxTKUs3I9EOAPmnIimEeJOemqRlRDOp9LIj+96wtxZejF51gNibmoGq8KoqbT1ssAI5po/E3J+vEtXGA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxc-resolver/binding-freebsd-x64@11.19.0': - resolution: {integrity: sha512-kwcZ30bIpJNFcT22sIlde4mz0EyXmB3lAefCFWtffqpbmLweQUwz1dKDcsutxEjpkbEKLmfrj1wCyRZp7n5Hnw==} + '@oxc-parser/binding-freebsd-x64@0.121.0': + resolution: {integrity: sha512-v1APOTkCp+RWOIDAHRoaeW/UoaHF15a60E8eUL6kUQXh+i4K7PBwq2Wi7jm8p0ymID5/m/oC1w3W31Z/+r7HQw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.0': - resolution: {integrity: sha512-GImk/cb3X+zBGEwr6l9h0dbiNo5zNd52gamZmluEpbyybiZ8kc5q44/7zRR4ILChWRW7pI92W57CJwhkF+wRmg==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.121.0': + resolution: {integrity: sha512-PmqPQuqHZyFVWA4ycr0eu4VnTMmq9laOHZd+8R359w6kzuNZPvmmunmNJ8ybkm769A0nCoVp3TJ6dUz7B3FYIQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-resolver/binding-linux-arm-musleabihf@11.19.0': - resolution: {integrity: sha512-uIEyws3bBD1gif4SZCOV2XIr6q5fd1WbzzBbpL8qk+TbzOvKMWnMNNtfNacnAGGa2lLRNXR1Fffot2mlZ/Xmbw==} + '@oxc-parser/binding-linux-arm-musleabihf@0.121.0': + resolution: {integrity: sha512-vF24htj+MOH+Q7y9A8NuC6pUZu8t/C2Fr/kDOi2OcNf28oogr2xadBPXAbml802E8wRAVfbta6YLDQTearz+jw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-resolver/binding-linux-arm64-gnu@11.19.0': - resolution: {integrity: sha512-bIkgp+AB+yZfvdKDfjFT7PycsRtih7+zCV5AbnkzfyvNvQ47rfssf8R1IbG++mx+rZ4YUCUu8EbP66HC3O5c5w==} + '@oxc-parser/binding-linux-arm64-gnu@0.121.0': + resolution: {integrity: sha512-wjH8cIG2Lu/3d64iZpbYr73hREMgKAfu7fqpXjgM2S16y2zhTfDIp8EQjxO8vlDtKP5Rc7waZW72lh8nZtWrpA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxc-resolver/binding-linux-arm64-musl@11.19.0': - resolution: {integrity: sha512-bOt5pKPcbidTSy64m2CfM0XcaCmxBEFclCMPuOPO08hh8QIFTiZVhFf/OxTFqyRwhq/tlzzKmXpMo7DfzbO5lQ==} + '@oxc-parser/binding-linux-arm64-musl@0.121.0': + resolution: {integrity: sha512-qT663J/W8yQFw3dtscbEi9LKJevr20V7uWs2MPGTnvNZ3rm8anhhE16gXGpxDOHeg9raySaSHKhd4IGa3YZvuw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxc-resolver/binding-linux-ppc64-gnu@11.19.0': - resolution: {integrity: sha512-BymEPqVeLZzA/1kXow9U9rdniq1r5kk4u686Cx3ZU77YygR48NJI/2TyjM70vKHZffGx75ZShobcc1M5GXG3WA==} + '@oxc-parser/binding-linux-ppc64-gnu@0.121.0': + resolution: {integrity: sha512-mYNe4NhVvDBbPkAP8JaVS8lC1dsoJZWH5WCjpw5E+sjhk1R08wt3NnXYUzum7tIiWPfgQxbCMcoxgeemFASbRw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxc-resolver/binding-linux-riscv64-gnu@11.19.0': - resolution: {integrity: sha512-aFgPTzZZY+XCYe4B+3A1S63xcIh2i136+2TPXWr9NOwXXTdMdBntb1J9fEgxXDnX82MjBknLUpJqAZHNTJzixA==} + '@oxc-parser/binding-linux-riscv64-gnu@0.121.0': + resolution: {integrity: sha512-+QiFoGxhAbaI/amqX567784cDyyuZIpinBrJNxUzb+/L2aBRX67mN6Jv40pqduHf15yYByI+K5gUEygCuv0z9w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxc-resolver/binding-linux-riscv64-musl@11.19.0': - resolution: {integrity: sha512-9WDGt7fV9GK97WrWE/VEDhMFv9m0ZXYn5NQ+16QvyT0ux8yGLAvyadi6viaTjEdJII/OaHBRYHcL+zUjmaWwmg==} + '@oxc-parser/binding-linux-riscv64-musl@0.121.0': + resolution: {integrity: sha512-9ykEgyTa5JD/Uhv2sttbKnCfl2PieUfOjyxJC/oDL2UO0qtXOtjPLl7H8Kaj5G7p3hIvFgu3YWvAxvE0sqY+hQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxc-resolver/binding-linux-s390x-gnu@11.19.0': - resolution: {integrity: sha512-SY3di6tccocppAVal5Hev3D6D1N5Y6TCEypAvNCOiPqku2Y8U/aXfvGbthqdPNa72KYqjUR1vomOv6J9thHITA==} + '@oxc-parser/binding-linux-s390x-gnu@0.121.0': + resolution: {integrity: sha512-DB1EW5VHZdc1lIRjOI3bW/wV6R6y0xlfvdVrqj6kKi7Ayu2U3UqUBdq9KviVkcUGd5Oq+dROqvUEEFRXGAM7EQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxc-resolver/binding-linux-x64-gnu@11.19.0': - resolution: {integrity: sha512-SV+4zBeCC3xjSE2wvhN45eyABoVRX3xryWBABFKfLwAWhF3wsB3bUF+CantYfQ/TLpasyvplRS9ovvFT9cb/0A==} + '@oxc-parser/binding-linux-x64-gnu@0.121.0': + resolution: {integrity: sha512-s4lfobX9p4kPTclvMiH3gcQUd88VlnkMTF6n2MTMDAyX5FPNRhhRSFZK05Ykhf8Zy5NibV4PbGR6DnK7FGNN6A==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxc-resolver/binding-linux-x64-musl@11.19.0': - resolution: {integrity: sha512-LkbjO+r5Isl8Xl29pJYOCB/iSUIULFUJDGdMp+yJD3OgWtSa6VJta2iw7QXmpcoOkq18UIL09yWrlyjLDL0Hug==} + '@oxc-parser/binding-linux-x64-musl@0.121.0': + resolution: {integrity: sha512-P9KlyTpuBuMi3NRGpJO8MicuGZfOoqZVRP1WjOecwx8yk4L/+mrCRNc5egSi0byhuReblBF2oVoDSMgV9Bj4Hw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxc-resolver/binding-openharmony-arm64@11.19.0': - resolution: {integrity: sha512-Ud1gelL5slpEU5AjzBWQz1WheprOAl5CPnCKTWynvvdlBbAZXA6fPYLuCrlRo0uw+x3f37XJ71kirpSew8Zyvg==} + '@oxc-parser/binding-openharmony-arm64@0.121.0': + resolution: {integrity: sha512-R+4jrWOfF2OAPPhj3Eb3U5CaKNAH9/btMveMULIrcNW/hjfysFQlF8wE0GaVBr81dWz8JLgQlsxwctoL78JwXw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxc-resolver/binding-wasm32-wasi@11.19.0': - resolution: {integrity: sha512-wXLNAVmL4vWXKaYJnFPgg5zQsSr3Rv+ftNReIU3UkzTcoVLK0805Pnbr2NwcBWSO5hhpOEdys02qlT2kxVgjWw==} + '@oxc-parser/binding-wasm32-wasi@0.121.0': + resolution: {integrity: sha512-5TFISkPTymKvsmIlKasPVTPuWxzCcrT8pM+p77+mtQbIZDd1UC8zww4CJcRI46kolmgrEX6QpKO8AvWMVZ+ifw==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@oxc-resolver/binding-win32-arm64-msvc@11.19.0': - resolution: {integrity: sha512-zszvr0dJfvv0Jg49hLwjAJ4SRzfsq28SoearUtT1qv3qXRYsBWuctdlRa/lEZkiuG4tZWiY425Jh9QqLafwsAg==} + '@oxc-parser/binding-win32-arm64-msvc@0.121.0': + resolution: {integrity: sha512-V0pxh4mql4XTt3aiEtRNUeBAUFOw5jzZNxPABLaOKAWrVzSr9+XUaB095lY7jqMf5t8vkfh8NManGB28zanYKw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxc-resolver/binding-win32-ia32-msvc@11.19.0': - resolution: {integrity: sha512-I7ZYujr5XL1l7OwuddbOeqdUyFOaf51W1U2xUogInFdupIAKGqbpugpAK6RaccLcSlN0bbuo3CS5h7ue38SUAg==} + '@oxc-parser/binding-win32-ia32-msvc@0.121.0': + resolution: {integrity: sha512-4Ob1qvYMPnlF2N9rdmKdkQFdrq16QVcQwBsO8yiPZXof0fHKFF+LmQV501XFbi7lHyrKm8rlJRfQ/M8bZZPVLw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxc-resolver/binding-win32-x64-msvc@11.19.0': - resolution: {integrity: sha512-NxErbI1TmJEZZVvGPePjgXFZCuOzrjQuJ6YwHjcWkelReK7Uhg4QeL05zRdfTpgkH6IY/C8OjbKx5ZilQ4yDFg==} + '@oxc-parser/binding-win32-x64-msvc@0.121.0': + resolution: {integrity: sha512-BOp1KCzdboB1tPqoCPXgntgFs0jjeSyOXHzgxVFR7B/qfr3F8r4YDacHkTOUNXtDgM8YwKnkf3rE5gwALYX7NA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@playwright/test@1.58.2': - resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + '@oxc-project/types@0.121.0': + resolution: {integrity: sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.19.1': + resolution: {integrity: sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + resolution: {integrity: sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.19.1': + resolution: {integrity: sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + resolution: {integrity: sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + resolution: {integrity: sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + resolution: {integrity: sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + resolution: {integrity: sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + resolution: {integrity: sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + resolution: {integrity: sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + resolution: {integrity: sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + resolution: {integrity: sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + resolution: {integrity: sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + resolution: {integrity: sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + resolution: {integrity: sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + resolution: {integrity: sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.19.1': + resolution: {integrity: sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + resolution: {integrity: sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + resolution: {integrity: sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA==} + cpu: [ia32] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + resolution: {integrity: sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==} + cpu: [x64] + os: [win32] + + '@package-json/types@0.0.12': + resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} + + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} engines: {node: '>=18'} hasBin: true @@ -748,24 +893,6 @@ packages: '@rolldown/pluginutils@1.0.0-rc.2': resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} - '@rollup/plugin-inject@5.0.5': - resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/pluginutils@5.3.0': - resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] @@ -908,10 +1035,6 @@ packages: resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==} engines: {node: '>=18'} - '@sindresorhus/merge-streams@2.3.0': - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} - engines: {node: '>=18'} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1099,6 +1222,36 @@ packages: resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@turbo/darwin-64@2.9.3': + resolution: {integrity: sha512-P8foouaP+y/p+hhEGBoZpzMbpVvUMwPjDpcy6wN7EYfvvyISD1USuV27qWkczecihwuPJzQ1lDBuL8ERcavTyg==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.9.3': + resolution: {integrity: sha512-SIzEkvtNdzdI50FJDaIQ6kQGqgSSdFPcdn0wqmmONN6iGKjy6hsT+EH99GP65FsfV7DLZTh2NmtTIRl2kdoz5Q==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.9.3': + resolution: {integrity: sha512-pLRwFmcHHNBvsCySLS6OFabr/07kDT2pxEt/k6eBf/3asiVQZKJ7Rk88AafQx2aYA641qek4RsXvYO3JYpiBug==} + cpu: [x64] + os: [linux] + + '@turbo/linux-arm64@2.9.3': + resolution: {integrity: sha512-gy6ApUroC2Nzv+qjGtE/uPNkhHAFU4c8God+zd5Aiv9L9uBgHlxVJpHT3XWl5xwlJZ2KWuMrlHTaS5kmNB+q1Q==} + cpu: [arm64] + os: [linux] + + '@turbo/windows-64@2.9.3': + resolution: {integrity: sha512-d0YelTX6hAsB7kIEtGB3PzIzSfAg3yDoUlHwuwJc3adBXUsyUIs0YLG+1NNtuhcDOUGnWQeKUoJ2pGWvbpRj7w==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.9.3': + resolution: {integrity: sha512-/08CwpKJl3oRY8nOlh2YgilZVJDHsr60XTNxRhuDeuFXONpUZ5X+Nv65izbG/xBew9qxcJFbDX9/sAmAX+ITcQ==} + cpu: [arm64] + os: [win32] + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1111,17 +1264,20 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@24.10.13': - resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==} + '@types/node@24.12.0': + resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} - '@types/node@25.2.3': - resolution: {integrity: sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==} + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -1143,63 +1299,63 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.56.1': - resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==} + '@typescript-eslint/eslint-plugin@8.58.0': + resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.56.1 + '@typescript-eslint/parser': ^8.58.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.56.1': - resolution: {integrity: sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==} + '@typescript-eslint/parser@8.58.0': + resolution: {integrity: sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.56.1': - resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + '@typescript-eslint/project-service@8.58.0': + resolution: {integrity: sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.56.1': - resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + '@typescript-eslint/scope-manager@8.58.0': + resolution: {integrity: sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.56.1': - resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + '@typescript-eslint/tsconfig-utils@8.58.0': + resolution: {integrity: sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.56.1': - resolution: {integrity: sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==} + '@typescript-eslint/type-utils@8.58.0': + resolution: {integrity: sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.56.1': - resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + '@typescript-eslint/types@8.58.0': + resolution: {integrity: sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.56.1': - resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + '@typescript-eslint/typescript-estree@8.58.0': + resolution: {integrity: sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.56.1': - resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + '@typescript-eslint/utils@8.58.0': + resolution: {integrity: sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.56.1': - resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + '@typescript-eslint/visitor-keys@8.58.0': + resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -1314,43 +1470,43 @@ packages: peerDependencies: vite: ^4 || ^5 || ^6 || ^7 - '@vitest/coverage-v8@4.0.18': - resolution: {integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==} + '@vitest/coverage-v8@4.1.2': + resolution: {integrity: sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==} peerDependencies: - '@vitest/browser': 4.0.18 - vitest: 4.0.18 + '@vitest/browser': 4.1.2 + vitest: 4.1.2 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.0.18': - resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/expect@4.1.2': + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} - '@vitest/mocker@4.0.18': - resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + '@vitest/mocker@4.1.2': + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.0.18': - resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} - '@vitest/runner@4.0.18': - resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/runner@4.1.2': + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} - '@vitest/snapshot@4.0.18': - resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/snapshot@4.1.2': + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} - '@vitest/spy@4.0.18': - resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@4.1.2': + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} - '@vitest/utils@4.0.18': - resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} @@ -1385,10 +1541,6 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} @@ -1439,18 +1591,12 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} - asn1.js@4.10.1: - resolution: {integrity: sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==} - - assert@2.1.0: - resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-v8-to-istanbul@0.3.11: - resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==} + ast-v8-to-istanbul@1.0.0: + resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} @@ -1484,81 +1630,37 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - baseline-browser-mapping@2.10.0: - resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + baseline-browser-mapping@2.10.13: + resolution: {integrity: sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==} engines: {node: '>=6.0.0'} hasBin: true bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} - bn.js@4.12.3: - resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==} - - bn.js@5.2.3: - resolution: {integrity: sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==} - body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@1.1.13: + resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} - brace-expansion@5.0.3: - resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - brorand@1.1.0: - resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} - - browser-resolve@2.0.0: - resolution: {integrity: sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==} - - browserify-aes@1.2.0: - resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} - - browserify-cipher@1.0.1: - resolution: {integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==} - - browserify-des@1.0.2: - resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==} - - browserify-rsa@4.1.1: - resolution: {integrity: sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==} - engines: {node: '>= 0.10'} - - browserify-sign@4.2.5: - resolution: {integrity: sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==} - engines: {node: '>= 0.10'} - - browserify-zlib@0.2.0: - resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} - - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer-xor@1.0.3: - resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} - - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - - builtin-status-codes@3.0.0: - resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1578,44 +1680,25 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - caniuse-lite@1.0.30001774: - resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} + caniuse-lite@1.0.30001784: + resolution: {integrity: sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - cipher-base@1.0.7: - resolution: {integrity: sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==} - engines: {node: '>= 0.10'} - cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} - cli-truncate@5.1.1: - resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} engines: {node: '>=20'} clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - 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==} - colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -1630,19 +1713,13 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - comment-parser@1.4.5: - resolution: {integrity: sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==} + comment-parser@1.4.6: + resolution: {integrity: sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==} engines: {node: '>= 12.0.0'} concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - console-browserify@1.2.0: - resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} - - constants-browserify@1.0.0: - resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} - content-disposition@1.0.1: resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} engines: {node: '>=18'} @@ -1662,29 +1739,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - - create-ecdh@4.0.4: - resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} - - create-hash@1.2.0: - resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} - - create-hmac@1.1.7: - resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} - - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - crypto-browserify@3.12.1: - resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==} - engines: {node: '>= 0.10'} - css-tree@3.1.0: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -1753,16 +1811,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - des.js@1.1.0: - resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - diffie-hellman@5.0.3: - resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} - doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -1773,10 +1825,6 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - domain-browser@4.22.0: - resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==} - engines: {node: '>=10'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1784,11 +1832,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.302: - resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==} - - elliptic@6.6.1: - resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} + electron-to-chromium@1.5.331: + resolution: {integrity: sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==} emoji-name-map@2.0.3: resolution: {integrity: sha512-3KBuQuhYkRtLd9utBKfTtclbWP3IytC1FNcXg+NKARltPSYpkg/MLiklGv4vLwl8A8jMQjdneXNBYx8k0rrg+g==} @@ -1824,12 +1869,12 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-iterator-helpers@1.2.2: - resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} + es-iterator-helpers@1.3.1: + resolution: {integrity: sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==} engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} @@ -1863,10 +1908,6 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - eslint-import-context@0.1.9: resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1889,12 +1930,12 @@ packages: eslint-plugin-import-x: optional: true - eslint-plugin-import-x@4.16.1: - resolution: {integrity: sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==} + eslint-plugin-import-x@4.16.2: + resolution: {integrity: sha512-rM9K8UBHcWKpzQzStn1YRN2T5NvdeIfSVoKu/lKF41znQXHAUcBbYXe5wd6GNjZjTrP7viQ49n1D83x/2gYgIw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/utils': ^8.0.0 - eslint: ^8.57.0 || ^9.0.0 + '@typescript-eslint/utils': ^8.56.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 eslint-import-resolver-node: '*' peerDependenciesMeta: '@typescript-eslint/utils': @@ -1902,8 +1943,8 @@ packages: eslint-import-resolver-node: optional: true - eslint-plugin-jsdoc@62.7.1: - resolution: {integrity: sha512-4Zvx99Q7d1uggYBUX/AIjvoyqXhluGbbKrRmG8SQTLprPFg6fa293tVJH1o1GQwNe3lUydd8ZHzn37OaSncgSQ==} + eslint-plugin-jsdoc@62.9.0: + resolution: {integrity: sha512-PY7/X4jrVgoIDncUmITlUqK546Ltmx/Pd4Hdsu4CvSjryQZJI2mEV4vrdMufyTetMiZ5taNSqvK//BTgVUlNkA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -1920,25 +1961,21 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - 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} - eslint-visitor-keys@5.0.1: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@10.1.0: + resolution: {integrity: sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: jiti: '*' @@ -1946,12 +1983,8 @@ packages: jiti: optional: true - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - espree@11.1.1: - resolution: {integrity: sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==} + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} esquery@1.7.0: @@ -1966,9 +1999,6 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -1983,13 +2013,6 @@ packages: eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - - evp_bytestokey@1.0.3: - resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} - expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -2049,8 +2072,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} @@ -2126,8 +2149,8 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.13.6: - resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} github-username-regex@1.0.0: resolution: {integrity: sha512-EqDVkN0/5MQyDPOSDLInVRRXdeISRfcN1UW/1FUqD2knV1HHw8DndMB3UPNn5lO51DvRnjzbLXwWqNNV86PLOw==} @@ -2140,22 +2163,14 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - - globals@17.3.0: - resolution: {integrity: sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==} + globals@17.4.0: + resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} engines: {node: '>=18'} globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} - globby@14.1.0: - resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} - engines: {node: '>=18'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -2190,17 +2205,6 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - hash-base@3.0.5: - resolution: {integrity: sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==} - engines: {node: '>= 0.10'} - - hash-base@3.1.2: - resolution: {integrity: sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==} - engines: {node: '>= 0.8'} - - hash.js@1.1.7: - resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} - hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2211,9 +2215,6 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - hmac-drbg@1.0.1: - resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} - html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2235,9 +2236,6 @@ packages: http-vary@1.0.3: resolution: {integrity: sha512-sx7Y8YTqF3o0mFJJvF66n8dbaE8v3liV1RgCz46XP5xK7dnzyZHvwMWRA115q5kjbCPBV65/nOMlgW54WLyiag==} - https-browserify@1.0.0: - resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} - https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -2251,9 +2249,6 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2265,10 +2260,6 @@ packages: immer@11.1.4: resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -2288,10 +2279,6 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - is-arguments@1.2.0: - resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} - engines: {node: '>= 0.4'} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2355,10 +2342,6 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} - is-nan@1.3.2: - resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} - engines: {node: '>= 0.4'} - is-negative-zero@2.0.3: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} @@ -2413,19 +2396,12 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isomorphic-timers-promises@1.0.1: - resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==} - engines: {node: '>=10'} - istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -2456,8 +2432,8 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsdoc-type-pratt-parser@7.1.1: - resolution: {integrity: sha512-/2uqY7x6bsrpi3i9LVU6J89352C0rpMk0as8trXxCtvd4kPk1ke/Eyif6wqfSLvoNJqcDG9Vk4UsXgygzCt2xA==} + jsdoc-type-pratt-parser@7.2.0: + resolution: {integrity: sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==} engines: {node: '>=20.0.0'} jsdom@28.1.0: @@ -2495,13 +2471,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - knip@5.85.0: - resolution: {integrity: sha512-V2kyON+DZiYdNNdY6GALseiNCwX7dYdpz9Pv85AUn69Gk0UKCts+glOKWfe5KmaMByRjM9q17Mzj/KinTVOyxg==} - engines: {node: '>=18.18.0'} + knip@6.2.0: + resolution: {integrity: sha512-4OMUMJARvNble8e8TeFv12flp4fKzAITrQec1eKO4g2eA4HnNqEa8CXy2UOPLjuYuAETpe0N0r25jF9yY9FLig==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - peerDependencies: - '@types/node': '>=18' - typescript: '>=5.0.4 <7' levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} @@ -2581,8 +2554,8 @@ packages: resolution: {integrity: sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==} engines: {node: '>= 12.0.0'} - lint-staged@16.2.7: - resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==} + lint-staged@16.4.0: + resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} engines: {node: '>=20.17'} hasBin: true @@ -2594,9 +2567,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - log-update@6.1.0: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} @@ -2630,9 +2600,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - md5.js@1.3.5: - resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} - mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} @@ -2652,10 +2619,6 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - miller-rabin@4.0.1: - resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} - hasBin: true - mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -2680,18 +2643,12 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minimalistic-assert@1.0.1: - resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - - minimalistic-crypto-utils@1.0.1: - resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} - - minimatch@10.2.3: - resolution: {integrity: sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} - minimatch@3.1.4: - resolution: {integrity: sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -2699,10 +2656,6 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nano-spawn@2.0.0: - resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} - engines: {node: '>=20.17'} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2724,12 +2677,8 @@ packages: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - - node-stdlib-browser@1.3.1: - resolution: {integrity: sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==} - engines: {node: '>=10'} + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -2745,10 +2694,6 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - object-is@1.1.6: - resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} - engines: {node: '>= 0.4'} - object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -2787,15 +2732,16 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - os-browserify@0.3.0: - resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} - own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} - oxc-resolver@11.19.0: - resolution: {integrity: sha512-oEe42WEoZc2T5sCQqgaRBx8huzP4cJvrnm+BfNTJESdtM633Tqs6iowkpsMTXgnb7SLwU6N6D9bqwW/PULjo6A==} + oxc-parser@0.121.0: + resolution: {integrity: sha512-ek9o58+SCv6AV7nchiAcUJy1DNE2CC5WRdBcO0mF+W4oRjNQfPO7b3pLjTHSFECpHkKGOZSQxx3hk8viIL5YCg==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.19.1: + resolution: {integrity: sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg==} p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} @@ -2805,17 +2751,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - pako@1.0.11: - resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-asn1@5.1.9: - resolution: {integrity: sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==} - engines: {node: '>= 0.10'} - parse-imports-exports@0.2.4: resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} @@ -2829,9 +2764,6 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2846,17 +2778,9 @@ packages: path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} - path-type@6.0.0: - resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} - engines: {node: '>=18'} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pbkdf2@3.1.5: - resolution: {integrity: sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==} - engines: {node: '>= 0.10'} - pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} @@ -2894,30 +2818,21 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - pidtree@0.6.0: - resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} - engines: {node: '>=0.10'} - hasBin: true - - pkg-dir@5.0.0: - resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} - engines: {node: '>=10'} - - playwright-core@1.58.2: - resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} engines: {node: '>=18'} hasBin: true - playwright@1.58.2: - resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} engines: {node: '>=18'} hasBin: true @@ -2958,13 +2873,6 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.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'} - prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -2975,12 +2883,6 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - public-encrypt@4.0.3: - resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} - - punycode@1.4.1: - resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2989,19 +2891,9 @@ packages: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} - querystring-es3@0.2.1: - resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==} - engines: {node: '>=0.4.x'} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - - randomfill@1.0.4: - resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} - range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -3059,13 +2951,6 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} - 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'} - redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -3097,18 +2982,9 @@ packages: resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} engines: {node: '>=18'} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} - hasBin: true - resolve@2.0.0-next.6: resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} engines: {node: '>= 0.4'} @@ -3125,10 +3001,6 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - ripemd160@2.0.3: - resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} - engines: {node: '>= 0.8'} - rollup@4.59.0: resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3145,12 +3017,6 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -3201,17 +3067,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - setimmediate@1.0.5: - resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sha.js@2.4.12: - resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} - engines: {node: '>= 0.10'} - hasBin: true - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3243,16 +3101,16 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} - slice-ansi@7.1.2: resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} engines: {node: '>=18'} - smol-toml@1.6.0: - resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + + smol-toml@1.6.1: + resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} engines: {node: '>= 18'} source-map-js@1.2.1: @@ -3290,19 +3148,13 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - stream-browserify@3.0.0: - resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} - - stream-http@3.2.0: - resolution: {integrity: sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==} - string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -3334,24 +3186,14 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} - 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@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - strip-json-comments@5.0.3: resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} @@ -3379,23 +3221,19 @@ packages: engines: {node: '>=10'} hasBin: true - timers-browserify@2.0.12: - resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} - engines: {node: '>=0.6.0'} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} tldts-core@7.0.23: @@ -3405,10 +3243,6 @@ packages: resolution: {integrity: sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==} hasBin: true - to-buffer@1.2.2: - resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} - engines: {node: '>= 0.4'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3432,8 +3266,8 @@ packages: try@1.0.3: resolution: {integrity: sha512-AHA8khVCII6zKyRkyPo6pRwoR9v5jb7QFw6e5avtaVSkxVfaEucYIo06xnwB+pJaEarfYNbs7W3Vq+LZLZiWyA==} - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -3441,8 +3275,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tty-browserify@0.0.1: - resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==} + turbo@2.9.3: + resolution: {integrity: sha512-J/VUvsGRykPb9R8Kh8dHVBOqioDexLk9BhLCU/ZybRR+HN9UR3cURdazFvNgMDt9zPP8TF6K73Z+tplfmi0PqQ==} + hasBin: true type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} @@ -3468,18 +3303,22 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.56.1: - resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} + typescript-eslint@8.58.0: + resolution: {integrity: sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + unbash@2.2.0: + resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==} + engines: {node: '>=14'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -3487,14 +3326,13 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici@7.22.0: resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} engines: {node: '>=20.18.1'} - unicorn-magic@0.3.0: - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} - engines: {node: '>=18'} - unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -3511,21 +3349,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - url@0.11.4: - resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} - engines: {node: '>= 0.4'} - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - util@0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - uuid@13.0.0: resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true @@ -3534,14 +3362,6 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vite-plugin-node-polyfills@0.25.0: - resolution: {integrity: sha512-rHZ324W3LhfGPxWwQb2N048TThB6nVvnipsqBUJEzh3R9xeK9KI3si+GMQxCuAcpPJBVf0LpDtJ+beYzB3/chg==} - peerDependencies: - vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - - vite-plugin-string-replace@1.1.5: - resolution: {integrity: sha512-6kxox6rs/v5mbmV7BvRmSjALyYEmXC/IRBVS5ymgaV9iXe/OW84s3XNJrQX8B8/u9M/LK3gbQ6hWiYiOzqSRYQ==} - vite@7.3.1: resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3582,20 +3402,21 @@ packages: yaml: optional: true - vitest@4.0.18: - resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + vitest@4.1.2: + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.18 - '@vitest/browser-preview': 4.0.18 - '@vitest/browser-webdriverio': 4.0.18 - '@vitest/ui': 4.0.18 + '@vitest/browser-playwright': 4.1.2 + '@vitest/browser-preview': 4.1.2 + '@vitest/browser-webdriverio': 4.1.2 + '@vitest/ui': 4.1.2 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -3616,9 +3437,6 @@ packages: jsdom: optional: true - vm-browserify@1.1.2: - resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} - w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -3680,8 +3498,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -3706,8 +3524,8 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yaml@2.8.2: - resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} hasBin: true @@ -3762,8 +3580,8 @@ snapshots: '@babel/generator': 7.29.1 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.0 + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 @@ -3778,7 +3596,7 @@ snapshots: '@babel/generator@7.29.1': dependencies: - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/types': 7.29.0 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 @@ -3788,7 +3606,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 + browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 @@ -3816,12 +3634,12 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} - '@babel/helpers@7.28.6': + '@babel/helpers@7.29.2': dependencies: '@babel/template': 7.28.6 '@babel/types': 7.29.0 - '@babel/parser@7.29.0': + '@babel/parser@7.29.2': dependencies: '@babel/types': 7.29.0 @@ -3830,7 +3648,7 @@ snapshots: '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/types': 7.29.0 '@babel/traverse@7.29.0': @@ -3838,7 +3656,7 @@ snapshots: '@babel/code-frame': 7.29.0 '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/template': 7.28.6 '@babel/types': 7.29.0 debug: 4.4.3 @@ -3878,29 +3696,29 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} - '@emnapi/core@1.8.1': + '@emnapi/core@1.9.2': dependencies: - '@emnapi/wasi-threads': 1.1.0 + '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.8.1': + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.1.0': + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true - '@es-joy/jsdoccomment@0.84.0': + '@es-joy/jsdoccomment@0.86.0': dependencies: '@types/estree': 1.0.8 - '@typescript-eslint/types': 8.56.1 - comment-parser: 1.4.5 + '@typescript-eslint/types': 8.58.0 + comment-parser: 1.4.6 esquery: 1.7.0 - jsdoc-type-pratt-parser: 7.1.1 + jsdoc-type-pratt-parser: 7.2.0 '@es-joy/resolve.exports@1.2.0': {} @@ -3982,62 +3800,44 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.1.0(jiti@2.6.1))': dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/compat@2.0.2(eslint@9.39.2(jiti@2.6.1))': + '@eslint/compat@2.0.3(eslint@10.1.0(jiti@2.6.1))': dependencies: - '@eslint/core': 1.1.0 + '@eslint/core': 1.1.1 optionalDependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) - '@eslint/config-array@0.21.1': + '@eslint/config-array@0.23.3': dependencies: - '@eslint/object-schema': 2.1.7 + '@eslint/object-schema': 3.0.3 debug: 4.4.3 - minimatch: 3.1.4 + minimatch: 10.2.5 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.2': + '@eslint/config-helpers@0.5.3': dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.1.1 - '@eslint/core@0.17.0': + '@eslint/core@1.1.1': dependencies: '@types/json-schema': 7.0.15 - '@eslint/core@1.1.0': + '@eslint/js@10.0.1(eslint@10.1.0(jiti@2.6.1))': + optionalDependencies: + eslint: 10.1.0(jiti@2.6.1) + + '@eslint/object-schema@3.0.3': {} + + '@eslint/plugin-kit@0.6.1': dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.4': - dependencies: - ajv: 6.14.0 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.4 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.2': {} - - '@eslint/js@9.39.3': {} - - '@eslint/object-schema@2.1.7': {} - - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 1.1.1 levn: 0.4.1 '@exodus/bytes@1.14.1': {} @@ -4080,15 +3880,15 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.8.1 - '@emnapi/runtime': 1.8.1 + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 '@tybys/wasm-util': 0.10.1 optional: true - '@napi-rs/wasm-runtime@1.1.1': + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@emnapi/core': 1.8.1 - '@emnapi/runtime': 1.8.1 + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 '@tybys/wasm-util': 0.10.1 optional: true @@ -4104,71 +3904,143 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@oxc-resolver/binding-android-arm-eabi@11.19.0': + '@oxc-parser/binding-android-arm-eabi@0.121.0': optional: true - '@oxc-resolver/binding-android-arm64@11.19.0': + '@oxc-parser/binding-android-arm64@0.121.0': optional: true - '@oxc-resolver/binding-darwin-arm64@11.19.0': + '@oxc-parser/binding-darwin-arm64@0.121.0': optional: true - '@oxc-resolver/binding-darwin-x64@11.19.0': + '@oxc-parser/binding-darwin-x64@0.121.0': optional: true - '@oxc-resolver/binding-freebsd-x64@11.19.0': + '@oxc-parser/binding-freebsd-x64@0.121.0': optional: true - '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.0': + '@oxc-parser/binding-linux-arm-gnueabihf@0.121.0': optional: true - '@oxc-resolver/binding-linux-arm-musleabihf@11.19.0': + '@oxc-parser/binding-linux-arm-musleabihf@0.121.0': optional: true - '@oxc-resolver/binding-linux-arm64-gnu@11.19.0': + '@oxc-parser/binding-linux-arm64-gnu@0.121.0': optional: true - '@oxc-resolver/binding-linux-arm64-musl@11.19.0': + '@oxc-parser/binding-linux-arm64-musl@0.121.0': optional: true - '@oxc-resolver/binding-linux-ppc64-gnu@11.19.0': + '@oxc-parser/binding-linux-ppc64-gnu@0.121.0': optional: true - '@oxc-resolver/binding-linux-riscv64-gnu@11.19.0': + '@oxc-parser/binding-linux-riscv64-gnu@0.121.0': optional: true - '@oxc-resolver/binding-linux-riscv64-musl@11.19.0': + '@oxc-parser/binding-linux-riscv64-musl@0.121.0': optional: true - '@oxc-resolver/binding-linux-s390x-gnu@11.19.0': + '@oxc-parser/binding-linux-s390x-gnu@0.121.0': optional: true - '@oxc-resolver/binding-linux-x64-gnu@11.19.0': + '@oxc-parser/binding-linux-x64-gnu@0.121.0': optional: true - '@oxc-resolver/binding-linux-x64-musl@11.19.0': + '@oxc-parser/binding-linux-x64-musl@0.121.0': optional: true - '@oxc-resolver/binding-openharmony-arm64@11.19.0': + '@oxc-parser/binding-openharmony-arm64@0.121.0': optional: true - '@oxc-resolver/binding-wasm32-wasi@11.19.0': + '@oxc-parser/binding-wasm32-wasi@0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true - '@oxc-resolver/binding-win32-arm64-msvc@11.19.0': + '@oxc-parser/binding-win32-arm64-msvc@0.121.0': optional: true - '@oxc-resolver/binding-win32-ia32-msvc@11.19.0': + '@oxc-parser/binding-win32-ia32-msvc@0.121.0': optional: true - '@oxc-resolver/binding-win32-x64-msvc@11.19.0': + '@oxc-parser/binding-win32-x64-msvc@0.121.0': optional: true - '@playwright/test@1.58.2': + '@oxc-project/types@0.121.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.19.1': + optional: true + + '@oxc-resolver/binding-android-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.19.1': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.19.1': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.19.1': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - playwright: 1.58.2 + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-ia32-msvc@11.19.1': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + optional: true + + '@package-json/types@0.0.12': {} + + '@playwright/test@1.59.1': + dependencies: + playwright: 1.59.1 '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@18.3.27)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': dependencies: @@ -4184,22 +4056,6 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.2': {} - '@rollup/plugin-inject@5.0.5(rollup@4.59.0)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - estree-walker: 2.0.2 - magic-string: 0.30.21 - optionalDependencies: - rollup: 4.59.0 - - '@rollup/pluginutils@5.3.0(rollup@4.59.0)': - dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 4.0.3 - optionalDependencies: - rollup: 4.59.0 - '@rollup/rollup-android-arm-eabi@4.59.0': optional: true @@ -4277,8 +4133,6 @@ snapshots: '@sindresorhus/base62@1.0.0': {} - '@sindresorhus/merge-streams@2.3.0': {} - '@standard-schema/spec@1.1.0': {} '@standard-schema/utils@0.3.0': {} @@ -4396,12 +4250,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 - '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2))': + '@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.1 '@tailwindcss/oxide': 4.2.1 tailwindcss: 4.2.1 - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3) '@testing-library/dom@10.4.1': dependencies: @@ -4423,6 +4277,24 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 + '@turbo/darwin-64@2.9.3': + optional: true + + '@turbo/darwin-arm64@2.9.3': + optional: true + + '@turbo/linux-64@2.9.3': + optional: true + + '@turbo/linux-arm64@2.9.3': + optional: true + + '@turbo/windows-64@2.9.3': + optional: true + + '@turbo/windows-arm64@2.9.3': + optional: true + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -4437,17 +4309,19 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/esrecurse@4.3.1': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} - '@types/node@24.10.13': + '@types/node@24.12.0': dependencies: undici-types: 7.16.0 - '@types/node@25.2.3': + '@types/node@25.5.0': dependencies: - undici-types: 7.16.0 + undici-types: 7.18.2 optional: true '@types/prop-types@15.7.15': {} @@ -4468,98 +4342,98 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.10.13 + '@types/node': 24.12.0 optional: true - '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/type-utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.56.1 - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/parser': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/type-utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 + eslint: 10.1.0(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.0 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.58.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.56.1': + '@typescript-eslint/scope-manager@8.58.0': dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 - '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.58.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) - ts-api-utils: 2.4.0(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.56.1': {} + '@typescript-eslint/types@8.58.0': {} - '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.58.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/project-service': 8.58.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.58.0(typescript@5.9.3) + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/visitor-keys': 8.58.0 debug: 4.4.3 - minimatch: 10.2.3 + minimatch: 10.2.5 semver: 7.7.4 tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) + ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.58.0 + '@typescript-eslint/types': 8.58.0 + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.56.1': + '@typescript-eslint/visitor-keys@8.58.0': dependencies: - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/types': 8.58.0 eslint-visitor-keys: 5.0.1 '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -4623,66 +4497,76 @@ snapshots: '@uppercod/css-to-object@1.1.1': {} - '@vitejs/plugin-react-swc@4.2.3(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2))': + '@vitejs/plugin-react-swc@4.2.3(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 '@swc/core': 1.15.13 - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3) transitivePeerDependencies: - '@swc/helpers' - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2))': + '@vitest/coverage-v8@4.1.2(vitest@4.1.2(@types/node@24.12.0)(happy-dom@20.6.1)(jsdom@28.1.0)(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3)))': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.0.18 - ast-v8-to-istanbul: 0.3.11 + '@vitest/utils': 4.1.2 + ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-reports: 3.2.0 magicast: 0.5.2 obug: 2.1.1 - std-env: 3.10.0 - tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2) + std-env: 4.0.0 + tinyrainbow: 3.1.0 + vitest: 4.1.2(@types/node@24.12.0)(happy-dom@20.6.1)(jsdom@28.1.0)(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3)) - '@vitest/expect@4.0.18': + '@vitest/expect@4.1.2': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 chai: 6.2.2 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2))': + '@vitest/mocker@4.1.2(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3))': dependencies: - '@vitest/spy': 4.0.18 + '@vitest/spy': 4.1.2 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2) + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3) - '@vitest/pretty-format@4.0.18': + '@vitest/mocker@4.1.2(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3))': dependencies: - tinyrainbow: 3.0.3 + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3) - '@vitest/runner@4.0.18': + '@vitest/pretty-format@4.1.2': dependencies: - '@vitest/utils': 4.0.18 + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.2': + dependencies: + '@vitest/utils': 4.1.2 pathe: 2.0.3 - '@vitest/snapshot@4.0.18': + '@vitest/snapshot@4.1.2': dependencies: - '@vitest/pretty-format': 4.0.18 + '@vitest/pretty-format': 4.1.2 + '@vitest/utils': 4.1.2 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.2': {} - '@vitest/utils@4.0.18': + '@vitest/utils@4.1.2': dependencies: - '@vitest/pretty-format': 4.0.18 - tinyrainbow: 3.0.3 + '@vitest/pretty-format': 4.1.2 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 accepts@2.0.0: dependencies: @@ -4712,10 +4596,6 @@ snapshots: ansi-regex@6.2.2: {} - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - ansi-styles@5.2.0: {} ansi-styles@6.2.3: {} @@ -4787,23 +4667,9 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 - asn1.js@4.10.1: - dependencies: - bn.js: 4.12.3 - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - - assert@2.1.0: - dependencies: - call-bind: 1.0.8 - is-nan: 1.3.2 - object-is: 1.1.6 - object.assign: 4.1.7 - util: 0.12.5 - assertion-error@2.0.1: {} - ast-v8-to-istanbul@0.3.11: + ast-v8-to-istanbul@1.0.0: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 @@ -4844,18 +4710,12 @@ snapshots: balanced-match@4.0.4: {} - base64-js@1.5.1: {} - - baseline-browser-mapping@2.10.0: {} + baseline-browser-mapping@2.10.13: {} bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 - bn.js@4.12.3: {} - - bn.js@5.2.3: {} - body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -4870,12 +4730,12 @@ snapshots: transitivePeerDependencies: - supports-color - brace-expansion@1.1.12: + brace-expansion@1.1.13: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.3: + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -4883,76 +4743,17 @@ snapshots: dependencies: fill-range: 7.1.1 - brorand@1.1.0: {} - - browser-resolve@2.0.0: + browserslist@4.28.2: dependencies: - resolve: 1.22.11 - - browserify-aes@1.2.0: - dependencies: - buffer-xor: 1.0.3 - cipher-base: 1.0.7 - create-hash: 1.2.0 - evp_bytestokey: 1.0.3 - inherits: 2.0.4 - safe-buffer: 5.2.1 - - browserify-cipher@1.0.1: - dependencies: - browserify-aes: 1.2.0 - browserify-des: 1.0.2 - evp_bytestokey: 1.0.3 - - browserify-des@1.0.2: - dependencies: - cipher-base: 1.0.7 - des.js: 1.1.0 - inherits: 2.0.4 - safe-buffer: 5.2.1 - - browserify-rsa@4.1.1: - dependencies: - bn.js: 5.2.3 - randombytes: 2.1.0 - safe-buffer: 5.2.1 - - browserify-sign@4.2.5: - dependencies: - bn.js: 5.2.3 - browserify-rsa: 4.1.1 - create-hash: 1.2.0 - create-hmac: 1.1.7 - elliptic: 6.6.1 - inherits: 2.0.4 - parse-asn1: 5.1.9 - readable-stream: 2.3.8 - safe-buffer: 5.2.1 - - browserify-zlib@0.2.0: - dependencies: - pako: 1.0.11 - - browserslist@4.28.1: - dependencies: - baseline-browser-mapping: 2.10.0 - caniuse-lite: 1.0.30001774 - electron-to-chromium: 1.5.302 - node-releases: 2.0.27 - update-browserslist-db: 1.2.3(browserslist@4.28.1) + baseline-browser-mapping: 2.10.13 + caniuse-lite: 1.0.30001784 + electron-to-chromium: 1.5.331 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) buffer-from@1.1.2: optional: true - buffer-xor@1.0.3: {} - - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - - builtin-status-codes@3.0.0: {} - bytes@3.1.2: {} cache-parser@1.2.6: {} @@ -4974,40 +4775,21 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - callsites@3.1.0: {} - - caniuse-lite@1.0.30001774: {} + caniuse-lite@1.0.30001784: {} chai@6.2.2: {} - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - cipher-base@1.0.7: - dependencies: - inherits: 2.0.4 - safe-buffer: 5.2.1 - to-buffer: 1.2.2 - cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 - cli-truncate@5.1.1: + cli-truncate@5.2.0: dependencies: - slice-ansi: 7.1.2 + slice-ansi: 8.0.0 string-width: 8.2.0 clsx@2.1.1: {} - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - colorette@2.0.20: {} combined-stream@1.0.8: @@ -5019,14 +4801,10 @@ snapshots: commander@2.20.3: optional: true - comment-parser@1.4.5: {} + comment-parser@1.4.6: {} concat-map@0.0.1: {} - console-browserify@1.2.0: {} - - constants-browserify@1.0.0: {} - content-disposition@1.0.1: {} content-type@1.0.5: {} @@ -5037,53 +4815,12 @@ snapshots: cookie@0.7.2: {} - core-util-is@1.0.3: {} - - create-ecdh@4.0.4: - dependencies: - bn.js: 4.12.3 - elliptic: 6.6.1 - - create-hash@1.2.0: - dependencies: - cipher-base: 1.0.7 - inherits: 2.0.4 - md5.js: 1.3.5 - ripemd160: 2.0.3 - sha.js: 2.4.12 - - create-hmac@1.1.7: - dependencies: - cipher-base: 1.0.7 - create-hash: 1.2.0 - inherits: 2.0.4 - ripemd160: 2.0.3 - safe-buffer: 5.2.1 - sha.js: 2.4.12 - - create-require@1.1.1: {} - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - crypto-browserify@3.12.1: - dependencies: - browserify-cipher: 1.0.1 - browserify-sign: 4.2.5 - create-ecdh: 4.0.4 - create-hash: 1.2.0 - create-hmac: 1.1.7 - diffie-hellman: 5.0.3 - hash-base: 3.0.5 - inherits: 2.0.4 - pbkdf2: 3.1.5 - public-encrypt: 4.0.3 - randombytes: 2.1.0 - randomfill: 1.0.4 - css-tree@3.1.0: dependencies: mdn-data: 2.12.2 @@ -5153,19 +4890,8 @@ snapshots: dequal@2.0.3: {} - des.js@1.1.0: - dependencies: - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - detect-libc@2.1.2: {} - diffie-hellman@5.0.3: - dependencies: - bn.js: 4.12.3 - miller-rabin: 4.0.1 - randombytes: 2.1.0 - doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -5174,8 +4900,6 @@ snapshots: dom-accessibility-api@0.6.3: {} - domain-browser@4.22.0: {} - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -5184,17 +4908,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.302: {} - - elliptic@6.6.1: - dependencies: - bn.js: 4.12.3 - brorand: 1.1.0 - hash.js: 1.1.7 - hmac-drbg: 1.0.1 - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - minimalistic-crypto-utils: 1.0.1 + electron-to-chromium@1.5.331: {} emoji-name-map@2.0.3: {} @@ -5272,7 +4986,7 @@ snapshots: es-errors@1.3.0: {} - es-iterator-helpers@1.2.2: + es-iterator-helpers@1.3.1: dependencies: call-bind: 1.0.8 call-bound: 1.0.4 @@ -5289,9 +5003,10 @@ snapshots: has-symbols: 1.1.0 internal-slot: 1.1.0 iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 safe-array-concat: 1.1.3 - es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} es-object-atoms@1.1.1: dependencies: @@ -5349,57 +5064,56 @@ snapshots: escape-string-regexp@4.0.0: {} - escape-string-regexp@5.0.0: {} - eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: - get-tsconfig: 4.13.6 + get-tsconfig: 4.13.7 stable-hash-x: 0.2.0 optionalDependencies: unrs-resolver: 1.11.1 - eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1)))(eslint@10.1.0(jiti@2.6.1)): dependencies: debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) - get-tsconfig: 4.13.6 + get-tsconfig: 4.13.7 is-bun-module: 2.0.0 stable-hash-x: 0.2.0 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import-x: 4.16.2(@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1)): dependencies: - '@typescript-eslint/types': 8.56.1 - comment-parser: 1.4.5 + '@package-json/types': 0.0.12 + '@typescript-eslint/types': 8.58.0 + comment-parser: 1.4.6 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 10.1.0(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 - minimatch: 10.2.3 + minimatch: 10.2.5 semver: 7.7.4 stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: - '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) transitivePeerDependencies: - supports-color - eslint-plugin-jsdoc@62.7.1(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-jsdoc@62.9.0(eslint@10.1.0(jiti@2.6.1)): dependencies: - '@es-joy/jsdoccomment': 0.84.0 + '@es-joy/jsdoccomment': 0.86.0 '@es-joy/resolve.exports': 1.2.0 are-docs-informative: 0.0.2 - comment-parser: 1.4.5 + comment-parser: 1.4.6 debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint: 9.39.2(jiti@2.6.1) - espree: 11.1.1 + eslint: 10.1.0(jiti@2.6.1) + espree: 11.2.0 esquery: 1.7.0 html-entities: 2.6.0 object-deep-merge: 2.0.0 @@ -5410,30 +5124,30 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-react-hooks@7.0.1(eslint@10.1.0(jiti@2.6.1)): dependencies: '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 - eslint: 9.39.2(jiti@2.6.1) + '@babel/parser': 7.29.2 + eslint: 10.1.0(jiti@2.6.1) hermes-parser: 0.25.1 zod: 4.3.6 zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-react@7.37.5(eslint@10.1.0(jiti@2.6.1)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 array.prototype.flatmap: 1.3.3 array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 - es-iterator-helpers: 1.2.2 - eslint: 9.39.2(jiti@2.6.1) + es-iterator-helpers: 1.3.1 + eslint: 10.1.0(jiti@2.6.1) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 - minimatch: 3.1.4 + minimatch: 3.1.5 object.entries: 1.1.9 object.fromentries: 2.0.8 object.values: 1.2.1 @@ -5443,39 +5157,36 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-scope@8.4.0: + eslint-scope@9.1.2: dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.1: {} - eslint-visitor-keys@5.0.1: {} - eslint@9.39.2(jiti@2.6.1): + eslint@10.1.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.1.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.4 - '@eslint/js': 9.39.2 - '@eslint/plugin-kit': 0.4.1 + '@eslint/config-array': 0.23.3 + '@eslint/config-helpers': 0.5.3 + '@eslint/core': 1.1.1 + '@eslint/plugin-kit': 0.6.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 ajv: 6.14.0 - chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -5486,8 +5197,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.4 + minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -5495,13 +5205,7 @@ snapshots: transitivePeerDependencies: - supports-color - espree@10.4.0: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 4.2.1 - - espree@11.1.1: + espree@11.2.0: dependencies: acorn: 8.16.0 acorn-jsx: 5.3.2(acorn@8.16.0) @@ -5517,8 +5221,6 @@ snapshots: estraverse@5.3.0: {} - estree-walker@2.0.2: {} - estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -5529,13 +5231,6 @@ snapshots: eventemitter3@5.0.4: {} - events@3.3.0: {} - - evp_bytestokey@1.0.3: - dependencies: - md5.js: 1.3.5 - safe-buffer: 5.2.1 - expect-type@1.3.0: {} express@5.2.1: @@ -5595,9 +5290,9 @@ snapshots: dependencies: walk-up-path: 4.0.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 file-entry-cache@8.0.0: dependencies: @@ -5625,10 +5320,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.3 + flatted: 3.4.2 keyv: 4.5.4 - flatted@3.3.3: {} + flatted@3.4.2: {} follow-redirects@1.15.11: {} @@ -5701,7 +5396,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.13.6: + get-tsconfig@4.13.7: dependencies: resolve-pkg-maps: 1.0.0 @@ -5715,36 +5410,25 @@ snapshots: dependencies: is-glob: 4.0.3 - globals@14.0.0: {} - - globals@17.3.0: {} + globals@17.4.0: {} globalthis@1.0.4: dependencies: define-properties: 1.2.1 gopd: 1.2.0 - globby@14.1.0: - dependencies: - '@sindresorhus/merge-streams': 2.3.0 - fast-glob: 3.3.3 - ignore: 7.0.5 - path-type: 6.0.0 - slash: 5.1.0 - unicorn-magic: 0.3.0 - gopd@1.2.0: {} graceful-fs@4.2.11: {} happy-dom@20.6.1: dependencies: - '@types/node': 24.10.13 + '@types/node': 24.12.0 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 6.0.1 whatwg-mimetype: 3.0.0 - ws: 8.19.0 + ws: 8.20.0 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -5768,23 +5452,6 @@ snapshots: dependencies: has-symbols: 1.1.0 - hash-base@3.0.5: - dependencies: - inherits: 2.0.4 - safe-buffer: 5.2.1 - - hash-base@3.1.2: - dependencies: - inherits: 2.0.4 - readable-stream: 2.3.8 - safe-buffer: 5.2.1 - to-buffer: 1.2.2 - - hash.js@1.1.7: - dependencies: - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -5795,12 +5462,6 @@ snapshots: dependencies: hermes-estree: 0.25.1 - hmac-drbg@1.0.1: - dependencies: - hash.js: 1.1.7 - minimalistic-assert: 1.0.1 - minimalistic-crypto-utils: 1.0.1 - html-encoding-sniffer@6.0.0: dependencies: '@exodus/bytes': 1.14.1 @@ -5828,8 +5489,6 @@ snapshots: http-vary@1.0.3: {} - https-browserify@1.0.0: {} - https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -5843,19 +5502,12 @@ snapshots: dependencies: safer-buffer: 2.1.2 - ieee754@1.2.1: {} - ignore@5.3.2: {} ignore@7.0.5: {} immer@11.1.4: {} - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -5870,11 +5522,6 @@ snapshots: ipaddr.js@1.9.1: {} - is-arguments@1.2.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -5945,11 +5592,6 @@ snapshots: is-map@2.0.3: {} - is-nan@1.3.2: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - is-negative-zero@2.0.3: {} is-number-object@1.1.1: @@ -6002,14 +5644,10 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 - isarray@1.0.0: {} - isarray@2.0.5: {} isexe@2.0.0: {} - isomorphic-timers-promises@1.0.1: {} - istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -6042,7 +5680,7 @@ snapshots: dependencies: argparse: 2.0.1 - jsdoc-type-pratt-parser@7.1.1: {} + jsdoc-type-pratt-parser@7.2.0: {} jsdom@28.1.0: dependencies: @@ -6092,22 +5730,26 @@ snapshots: dependencies: json-buffer: 3.0.1 - knip@5.85.0(@types/node@24.10.13)(typescript@5.9.3): + knip@6.2.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): dependencies: '@nodelib/fs.walk': 1.2.8 - '@types/node': 24.10.13 fast-glob: 3.3.3 formatly: 0.3.0 + get-tsconfig: 4.13.7 jiti: 2.6.1 - js-yaml: 4.1.1 minimist: 1.2.8 - oxc-resolver: 11.19.0 + oxc-parser: 0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) picocolors: 1.1.1 - picomatch: 4.0.3 - smol-toml: 1.6.0 + picomatch: 4.0.4 + smol-toml: 1.6.1 strip-json-comments: 5.0.3 - typescript: 5.9.3 + unbash: 2.2.0 + yaml: 2.8.3 zod: 4.3.6 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' levn@0.4.1: dependencies: @@ -6163,19 +5805,18 @@ snapshots: lightningcss-win32-arm64-msvc: 1.31.1 lightningcss-win32-x64-msvc: 1.31.1 - lint-staged@16.2.7: + lint-staged@16.4.0: dependencies: commander: 14.0.3 listr2: 9.0.5 - micromatch: 4.0.8 - nano-spawn: 2.0.0 - pidtree: 0.6.0 + picomatch: 4.0.4 string-argv: 0.3.2 - yaml: 2.8.2 + tinyexec: 1.0.4 + yaml: 2.8.3 listr2@9.0.5: dependencies: - cli-truncate: 5.1.1 + cli-truncate: 5.2.0 colorette: 2.0.20 eventemitter3: 5.0.4 log-update: 6.1.0 @@ -6186,14 +5827,12 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.merge@4.6.2: {} - log-update@6.1.0: dependencies: ansi-escapes: 7.3.0 cli-cursor: 5.0.0 slice-ansi: 7.1.2 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 wrap-ansi: 9.0.2 loose-envify@1.4.0: @@ -6214,7 +5853,7 @@ snapshots: magicast@0.5.2: dependencies: - '@babel/parser': 7.29.0 + '@babel/parser': 7.29.2 '@babel/types': 7.29.0 source-map-js: 1.2.1 @@ -6224,12 +5863,6 @@ snapshots: math-intrinsics@1.1.0: {} - md5.js@1.3.5: - dependencies: - hash-base: 3.0.5 - inherits: 2.0.4 - safe-buffer: 5.2.1 - mdn-data@2.12.2: {} media-typer@1.1.0: {} @@ -6241,12 +5874,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 - - miller-rabin@4.0.1: - dependencies: - bn.js: 4.12.3 - brorand: 1.1.0 + picomatch: 2.3.2 mime-db@1.52.0: {} @@ -6264,24 +5892,18 @@ snapshots: min-indent@1.0.1: {} - minimalistic-assert@1.0.1: {} - - minimalistic-crypto-utils@1.0.1: {} - - minimatch@10.2.3: + minimatch@10.2.5: dependencies: - brace-expansion: 5.0.3 + brace-expansion: 5.0.5 - minimatch@3.1.4: + minimatch@3.1.5: dependencies: - brace-expansion: 1.1.12 + brace-expansion: 1.1.13 minimist@1.2.8: {} ms@2.1.3: {} - nano-spawn@2.0.0: {} - nanoid@3.3.11: {} napi-postinstall@0.3.4: {} @@ -6297,37 +5919,7 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 - node-releases@2.0.27: {} - - node-stdlib-browser@1.3.1: - dependencies: - assert: 2.1.0 - browser-resolve: 2.0.0 - browserify-zlib: 0.2.0 - buffer: 5.7.1 - console-browserify: 1.2.0 - constants-browserify: 1.0.0 - create-require: 1.1.1 - crypto-browserify: 3.12.1 - domain-browser: 4.22.0 - events: 3.3.0 - https-browserify: 1.0.0 - isomorphic-timers-promises: 1.0.1 - os-browserify: 0.3.0 - path-browserify: 1.0.1 - pkg-dir: 5.0.0 - process: 0.11.10 - punycode: 1.4.1 - querystring-es3: 0.2.1 - readable-stream: 3.6.2 - stream-browserify: 3.0.0 - stream-http: 3.2.0 - string_decoder: 1.3.0 - timers-browserify: 2.0.12 - tty-browserify: 0.0.1 - url: 0.11.4 - util: 0.12.5 - vm-browserify: 1.1.2 + node-releases@2.0.37: {} object-assign@4.1.1: {} @@ -6337,11 +5929,6 @@ snapshots: object-inspect@1.13.4: {} - object-is@1.1.6: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - object-keys@1.1.1: {} object.assign@4.1.7: @@ -6397,36 +5984,65 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - os-browserify@0.3.0: {} - own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 object-keys: 1.1.1 safe-push-apply: 1.0.0 - oxc-resolver@11.19.0: + oxc-parser@0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + dependencies: + '@oxc-project/types': 0.121.0 optionalDependencies: - '@oxc-resolver/binding-android-arm-eabi': 11.19.0 - '@oxc-resolver/binding-android-arm64': 11.19.0 - '@oxc-resolver/binding-darwin-arm64': 11.19.0 - '@oxc-resolver/binding-darwin-x64': 11.19.0 - '@oxc-resolver/binding-freebsd-x64': 11.19.0 - '@oxc-resolver/binding-linux-arm-gnueabihf': 11.19.0 - '@oxc-resolver/binding-linux-arm-musleabihf': 11.19.0 - '@oxc-resolver/binding-linux-arm64-gnu': 11.19.0 - '@oxc-resolver/binding-linux-arm64-musl': 11.19.0 - '@oxc-resolver/binding-linux-ppc64-gnu': 11.19.0 - '@oxc-resolver/binding-linux-riscv64-gnu': 11.19.0 - '@oxc-resolver/binding-linux-riscv64-musl': 11.19.0 - '@oxc-resolver/binding-linux-s390x-gnu': 11.19.0 - '@oxc-resolver/binding-linux-x64-gnu': 11.19.0 - '@oxc-resolver/binding-linux-x64-musl': 11.19.0 - '@oxc-resolver/binding-openharmony-arm64': 11.19.0 - '@oxc-resolver/binding-wasm32-wasi': 11.19.0 - '@oxc-resolver/binding-win32-arm64-msvc': 11.19.0 - '@oxc-resolver/binding-win32-ia32-msvc': 11.19.0 - '@oxc-resolver/binding-win32-x64-msvc': 11.19.0 + '@oxc-parser/binding-android-arm-eabi': 0.121.0 + '@oxc-parser/binding-android-arm64': 0.121.0 + '@oxc-parser/binding-darwin-arm64': 0.121.0 + '@oxc-parser/binding-darwin-x64': 0.121.0 + '@oxc-parser/binding-freebsd-x64': 0.121.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.121.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.121.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.121.0 + '@oxc-parser/binding-linux-arm64-musl': 0.121.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.121.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.121.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.121.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.121.0 + '@oxc-parser/binding-linux-x64-gnu': 0.121.0 + '@oxc-parser/binding-linux-x64-musl': 0.121.0 + '@oxc-parser/binding-openharmony-arm64': 0.121.0 + '@oxc-parser/binding-wasm32-wasi': 0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@oxc-parser/binding-win32-arm64-msvc': 0.121.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.121.0 + '@oxc-parser/binding-win32-x64-msvc': 0.121.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.19.1 + '@oxc-resolver/binding-android-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-arm64': 11.19.1 + '@oxc-resolver/binding-darwin-x64': 11.19.1 + '@oxc-resolver/binding-freebsd-x64': 11.19.1 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.19.1 + '@oxc-resolver/binding-linux-arm64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-arm64-musl': 11.19.1 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-riscv64-musl': 11.19.1 + '@oxc-resolver/binding-linux-s390x-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 + '@oxc-resolver/binding-linux-x64-musl': 11.19.1 + '@oxc-resolver/binding-openharmony-arm64': 11.19.1 + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 + '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 + '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' p-limit@3.1.0: dependencies: @@ -6436,20 +6052,6 @@ snapshots: dependencies: p-limit: 3.1.0 - pako@1.0.11: {} - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse-asn1@5.1.9: - dependencies: - asn1.js: 4.10.1 - browserify-aes: 1.2.0 - evp_bytestokey: 1.0.3 - pbkdf2: 3.1.5 - safe-buffer: 5.2.1 - parse-imports-exports@0.2.4: dependencies: parse-statements: 1.0.11 @@ -6462,8 +6064,6 @@ snapshots: parseurl@1.3.3: {} - path-browserify@1.0.1: {} - path-exists@4.0.0: {} path-key@3.1.1: {} @@ -6472,19 +6072,8 @@ snapshots: path-to-regexp@8.3.0: {} - path-type@6.0.0: {} - pathe@2.0.3: {} - pbkdf2@3.1.5: - dependencies: - create-hash: 1.2.0 - create-hmac: 1.1.7 - ripemd160: 2.0.3 - safe-buffer: 5.2.1 - sha.js: 2.4.12 - to-buffer: 1.2.2 - pg-cloudflare@1.3.0: optional: true @@ -6522,21 +6111,15 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} - picomatch@4.0.3: {} + picomatch@4.0.4: {} - pidtree@0.6.0: {} + playwright-core@1.59.1: {} - pkg-dir@5.0.0: + playwright@1.59.1: dependencies: - find-up: 5.0.0 - - playwright-core@1.58.2: {} - - playwright@1.58.2: - dependencies: - playwright-core: 1.58.2 + playwright-core: 1.59.1 optionalDependencies: fsevents: 2.3.2 @@ -6568,10 +6151,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - process-nextick-args@2.0.1: {} - - process@0.11.10: {} - prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -6585,36 +6164,14 @@ snapshots: proxy-from-env@1.1.0: {} - public-encrypt@4.0.3: - dependencies: - bn.js: 4.12.3 - browserify-rsa: 4.1.1 - create-hash: 1.2.0 - parse-asn1: 5.1.9 - randombytes: 2.1.0 - safe-buffer: 5.2.1 - - punycode@1.4.1: {} - punycode@2.3.1: {} qs@6.15.0: dependencies: side-channel: 1.1.0 - querystring-es3@0.2.1: {} - queue-microtask@1.2.3: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - - randomfill@1.0.4: - dependencies: - randombytes: 2.1.0 - safe-buffer: 5.2.1 - range-parser@1.2.1: {} raw-body@3.0.2: @@ -6666,22 +6223,6 @@ snapshots: dependencies: loose-envify: 1.4.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 - redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -6719,16 +6260,8 @@ snapshots: reserved-identifiers@1.2.0: {} - resolve-from@4.0.0: {} - resolve-pkg-maps@1.0.0: {} - resolve@1.22.11: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - resolve@2.0.0-next.6: dependencies: es-errors: 1.3.0 @@ -6747,11 +6280,6 @@ snapshots: rfdc@1.4.1: {} - ripemd160@2.0.3: - dependencies: - hash-base: 3.1.2 - inherits: 2.0.4 - rollup@4.59.0: dependencies: '@types/estree': 1.0.8 @@ -6805,10 +6333,6 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 - safe-buffer@5.1.2: {} - - safe-buffer@5.2.1: {} - safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -6883,16 +6407,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 - setimmediate@1.0.5: {} - setprototypeof@1.2.0: {} - sha.js@2.4.12: - dependencies: - inherits: 2.0.4 - safe-buffer: 5.2.1 - to-buffer: 1.2.2 - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -6931,14 +6447,17 @@ snapshots: signal-exit@4.1.0: {} - slash@5.1.0: {} - slice-ansi@7.1.2: dependencies: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 - smol-toml@1.6.0: {} + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + smol-toml@1.6.1: {} source-map-js@1.2.1: {} @@ -6968,37 +6487,25 @@ snapshots: statuses@2.0.2: {} - std-env@3.10.0: {} + std-env@4.0.0: {} stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 internal-slot: 1.1.0 - stream-browserify@3.0.0: - dependencies: - inherits: 2.0.4 - readable-stream: 3.6.2 - - stream-http@3.2.0: - dependencies: - builtin-status-codes: 3.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - xtend: 4.0.2 - string-argv@0.3.2: {} string-width@7.2.0: dependencies: emoji-regex: 10.6.0 get-east-asian-width: 1.5.0 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 string-width@8.2.0: dependencies: get-east-asian-width: 1.5.0 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 string.prototype.matchall@4.0.12: dependencies: @@ -7044,15 +6551,7 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - - strip-ansi@7.1.2: + strip-ansi@7.2.0: dependencies: ansi-regex: 6.2.2 @@ -7060,8 +6559,6 @@ snapshots: dependencies: min-indent: 1.0.1 - strip-json-comments@3.1.1: {} - strip-json-comments@5.0.3: {} supports-color@7.2.0: @@ -7084,20 +6581,16 @@ snapshots: source-map-support: 0.5.21 optional: true - timers-browserify@2.0.12: - dependencies: - setimmediate: 1.0.5 - tinybench@2.9.0: {} - tinyexec@1.0.2: {} + tinyexec@1.0.4: {} tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 - tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} tldts-core@7.0.23: {} @@ -7105,12 +6598,6 @@ snapshots: dependencies: tldts-core: 7.0.23 - to-buffer@1.2.2: - dependencies: - isarray: 2.0.5 - safe-buffer: 5.2.1 - typed-array-buffer: 1.0.3 - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -7132,14 +6619,21 @@ snapshots: try@1.0.3: {} - ts-api-utils@2.4.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 tslib@2.8.1: optional: true - tty-browserify@0.0.1: {} + turbo@2.9.3: + optionalDependencies: + '@turbo/darwin-64': 2.9.3 + '@turbo/darwin-arm64': 2.9.3 + '@turbo/linux-64': 2.9.3 + '@turbo/linux-arm64': 2.9.3 + '@turbo/windows-64': 2.9.3 + '@turbo/windows-arm64': 2.9.3 type-check@0.4.0: dependencies: @@ -7184,19 +6678,21 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.0(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) + eslint: 10.1.0(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color typescript@5.9.3: {} + unbash@2.2.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -7206,9 +6702,10 @@ snapshots: undici-types@7.16.0: {} - undici@7.22.0: {} + undici-types@7.18.2: + optional: true - unicorn-magic@0.3.0: {} + undici@7.22.0: {} unpipe@1.0.0: {} @@ -7236,9 +6733,9 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - update-browserslist-db@1.2.3(browserslist@4.28.1): + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: - browserslist: 4.28.1 + browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 @@ -7246,98 +6743,103 @@ snapshots: dependencies: punycode: 2.3.1 - url@0.11.4: - dependencies: - punycode: 1.4.1 - qs: 6.15.0 - use-sync-external-store@1.6.0(react@18.3.1): dependencies: react: 18.3.1 - util-deprecate@1.0.2: {} - - util@0.12.5: - dependencies: - inherits: 2.0.4 - is-arguments: 1.2.0 - is-generator-function: 1.1.2 - is-typed-array: 1.1.15 - which-typed-array: 1.1.20 - uuid@13.0.0: {} vary@1.1.2: {} - vite-plugin-node-polyfills@0.25.0(rollup@4.59.0)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2)): - dependencies: - '@rollup/plugin-inject': 5.0.5(rollup@4.59.0) - node-stdlib-browser: 1.3.1 - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2) - transitivePeerDependencies: - - rollup - - vite-plugin-string-replace@1.1.5: - dependencies: - escape-string-regexp: 5.0.0 - globby: 14.1.0 - - vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2): + vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3): dependencies: esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 postcss: 8.5.6 rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.2.3 + '@types/node': 24.12.0 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.31.1 terser: 5.44.1 - yaml: 2.8.2 + yaml: 2.8.3 - vitest@4.0.18(@types/node@25.2.3)(happy-dom@20.6.1)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2): + vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3): dependencies: - '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2)) - '@vitest/pretty-format': 4.0.18 - '@vitest/runner': 4.0.18 - '@vitest/snapshot': 4.0.18 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 - es-module-lexer: 1.7.0 + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.6 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 25.5.0 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 + terser: 5.44.1 + yaml: 2.8.3 + + vitest@4.1.2(@types/node@24.12.0)(happy-dom@20.6.1)(jsdom@28.1.0)(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 + picomatch: 4.0.4 + std-env: 4.0.0 tinybench: 2.9.0 - tinyexec: 1.0.2 + tinyexec: 1.0.4 tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2) + tinyrainbow: 3.1.0 + vite: 7.3.1(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.2.3 + '@types/node': 24.12.0 happy-dom: 20.6.1 jsdom: 28.1.0 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - vm-browserify@1.1.2: {} + vitest@4.1.2(@types/node@25.5.0)(happy-dom@20.6.1)(jsdom@28.1.0)(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.5.0 + happy-dom: 20.6.1 + jsdom: 28.1.0 + transitivePeerDependencies: + - msw w3c-xmlserializer@5.0.0: dependencies: @@ -7416,11 +6918,11 @@ snapshots: dependencies: ansi-styles: 6.2.3 string-width: 7.2.0 - strip-ansi: 7.1.2 + strip-ansi: 7.2.0 wrappy@1.0.2: {} - ws@8.19.0: + ws@8.20.0: optional: true xml-name-validator@5.0.0: {} @@ -7431,7 +6933,7 @@ snapshots: yallist@3.1.1: {} - yaml@2.8.2: {} + yaml@2.8.3: {} yocto-queue@0.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6e443191..a01c6b71 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,10 +1,11 @@ packages: - apps/* + - packages/* catalog: - "@vitest/coverage-v8": 4.0.18 + "@vitest/coverage-v8": 4.1.2 vite: 7.3.1 - vitest: 4.0.18 + vitest: 4.1.2 minimumReleaseAge: 10080 diff --git a/tsconfig.base.json b/tsconfig.base.json index 39e84b3a..00912579 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -18,6 +18,15 @@ "types": [], "noUncheckedSideEffectImports": true, "moduleDetection": "force", + "customConditions": [ + /** + * Custom condition that resolved to original typescript source + * It allows realtime typechecking between packages without the need to rebuild them. + + * Keep in sync with `eslint.config.js` `createTypeScriptImportResolver` + */ + "@stats/source" + ], // Interop Constraints "verbatimModuleSyntax": true, diff --git a/tsconfig.json b/tsconfig.json index 2a537f3a..3dcd0840 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,4 @@ { - "files": [], - "references": [ - { - "path": "./apps/backend" - }, - { - "path": "./apps/frontend" - } - ] + // Visit https://aka.ms/tsconfig to read more about this file + "extends": "./tsconfig.base.json" } diff --git a/turbo.json b/turbo.json new file mode 100644 index 00000000..48832af8 --- /dev/null +++ b/turbo.json @@ -0,0 +1,16 @@ +{ + "$schema": "./node_modules/turbo/schema.json", + "globalDependencies": ["tsconfig.base.json"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["build/**"] + }, + "lint": { + "dependsOn": ["^build"] + }, + "typecheck": { + "dependsOn": ["^build"] + } + } +} diff --git a/vercel-preparation.sh b/vercel-preparation.sh index 35ebfe01..29b82d84 100755 --- a/vercel-preparation.sh +++ b/vercel-preparation.sh @@ -13,4 +13,3 @@ cp -RP apps/backend/. apps/backend-copy/ (shopt -s dotglob && mv apps/backend-copy/* apps/backend/.vercel/output/functions/api.func/) cp -RP apps/backend/.vercel/output/functions/api.func/_dot_vercel_copy/output apps/backend/.vercel/ rm -rf apps/backend/node_modules -cp -RP apps/backend apps/frontend/src/backend/ diff --git a/vitest.config.coverage.ts b/vitest.config.coverage.ts new file mode 100644 index 00000000..22fc386c --- /dev/null +++ b/vitest.config.coverage.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + coverage: { + enabled: true, + }, + projects: [ + "packages/core/vitest.config.ts", + "apps/backend/vitest.config.ts", + ], + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..c3c14e5b --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + projects: [ + "packages/core/vitest.config.ts", + "apps/backend/vitest.config.ts", + "apps/frontend/vite.config.ts", + ], + }, +});