diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a304b2f9380b6..002e4576355ec 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 901d2f63ab4b5..ba1ccbae2ee40 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 e0eeed90e2ee4..6bb490648ca0d 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 67d0bad27e235..9f27b1fda07b5 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 frontend - run: pnpm --filter frontend run build + - name: Build packages + run: pnpm run build:packages - - name: Run frontend tests - run: pnpm --filter frontend run test + - name: Build frontend + run: pnpm run build:frontend - - 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 c985fdedb8b19..92cce540a5ffb 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 0000000000000..f8ba92bcc211f --- /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 ebf23adf3e89b..6e4d680175a2c 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 8ead85e2c17cf..b720eecb96b06 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 bd17695b5cf3f..2890613ac5301 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 b4c15f46acd0f..e742101ea1abb 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 b9a98e1e5f744..0000000000000 --- 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 18511a224721b..bcfaa3ec92027 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 83c073e910b39..f19c3672d1c53 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 519d3f9f553c7..e52600d777dde 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 26e973eed2224..0000000000000 --- 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 998d7cbc46b30..5725fc128838c 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 3afe5853051ab..66162b6797f90 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 218ceac07faee..0e86821699b5e 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 34dc4611c3b19..d370ed0e4e601 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 dd1c3eadd903b..0000000000000 --- 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 35a24a36849aa..099c72541b17d 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 0000000000000..5dce81e4512de --- /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 e6ef6ed0a49ff..8d5a8adf99122 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 8778286845d29..0000000000000 --- 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 0000000000000..3da27ad514661 --- /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 5ca3a97adff02..0000000000000 --- 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 05a5dbe5f1c6e..09cf5b30c2632 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 6fe2ea599d709..f71f300b9c92e 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 e76c3109247c2..8bf2e83f21aa4 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 5f1319662b94d..0000000000000 --- 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 db914b86b1ebe..0000000000000 --- 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 ca8d586db136b..0000000000000 --- 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 12e87457fd740..3f40c8f84367d 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 34f060a5dde5a..0000000000000 --- 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 f4f094e5f828b..0000000000000 --- 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 7aeef4ca01a95..e086abb524339 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 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"; - -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, +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + api: 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({ api: mocks.api, getConfig: () => mocks.config }); }); -const error = { - errors: [ - { - type: "NOT_FOUND", - path: ["user"], - locations: [], - message: "Could not fetch user", - }, - ], -}; - -const mock = new MockAdapter(axios); +vi.mock("../src/common/database.js", () => ({ + storeRequest: mocks.storeRequest, + getUserAccessByName: mocks.getUserAccessByName, +})); -// @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); - - return { req, res }; -}; +import router from "../router.js"; +import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; -beforeEach(() => { - process.env.CACHE_SECONDS = undefined; +const createRequest = (search = "") => ({ + headers: {}, + url: `/api?${search}`, }); -afterEach(() => { - mock.reset(); +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), }); -describe("Test /api/", () => { - it("should test the request", async () => { - const { req, res } = faker({}, data_stats); +const defaultCacheHeader = + `max-age=${CACHE_TTL.STATS_CARD.DEFAULT}, ` + + `s-maxage=${CACHE_TTL.STATS_CARD.DEFAULT}, ` + + `stale-while-revalidate=${DURATIONS.ONE_DAY}`; - await api(req, res); +const errorCacheHeader = + `max-age=${CACHE_TTL.ERROR}, ` + + `s-maxage=${CACHE_TTL.ERROR}, ` + + `stale-while-revalidate=${DURATIONS.ONE_DAY}`; - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderStatsCard(stats, { ...req.query }), - ); - }); - - it("should render error card on error", async () => { - const { req, res } = faker({}, error); +beforeEach(() => { + 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; +}); - await api(req, res); +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", + }); - 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", - }), + const req = createRequest( + "username=anuraghazra&theme=dark&hide=issues,prs,contribs", ); - }); + const res = createResponse(); - it("should render error card in same theme as requested card", async () => { - const { req, res } = faker({ theme: "merko" }, 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", - 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, - ); - - 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", - }), + "user-pat", ); - }); - - 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(mocks.getUserAccessByName).toHaveBeenCalledWith("anuraghazra"); + expect(mocks.api).toHaveBeenCalledWith( + { + username: "anuraghazra", + }, + null, + ); expect(res.setHeader.mock.calls).toEqual([ + ["Cache-Control", errorCacheHeader], ["Content-Type", "image/svg+xml"], - [ - "Cache-Control", - `max-age=${CACHE_TTL.ERROR}, ` + - `s-maxage=${CACHE_TTL.ERROR}, ` + - `stale-while-revalidate=${DURATIONS.ONE_DAY}`, - ], ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith("temporary-error-svg"); + expect(mocks.storeRequest).toHaveBeenCalledExactlyOnceWith(req); }); - it("should properly set cache using CACHE_SECONDS env variable", async () => { - const cacheSeconds = "10000"; - process.env.CACHE_SECONDS = cacheSeconds; + it("should not persist permanent stats errors returned by core", async () => { + mocks.api.mockResolvedValue({ + status: "error - permanent", + content: "permanent-error-svg", + }); + + const req = createRequest("username=anuraghazra"); + const res = createResponse(); - const { req, res } = faker({}, data_stats); - await api(req, res); + 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"], - [ - "Cache-Control", - `max-age=${cacheSeconds}, ` + - `s-maxage=${cacheSeconds}, ` + - `stale-while-revalidate=${DURATIONS.ONE_DAY}`, - ], ]); + expect(res.end).toHaveBeenCalledExactlyOnceWith("permanent-error-svg"); + expect(mocks.storeRequest).not.toHaveBeenCalled(); }); - it("should disable cache when CACHE_SECONDS is set to 0", async () => { - process.env.CACHE_SECONDS = "0"; + it("should reject blacklisted usernames before calling core logic", async () => { + const req = createRequest("username=renovate-bot"); + const res = createResponse(); - const { req, res } = faker({}, data_stats); - await api(req, res); + 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"], - [ - "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( - { - 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", - }), - ); - }); - - it("should render error card when wrong locale is provided", async () => { - const { req, res } = faker({ locale: "asdf" }, data_stats); - - await api(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "Something went wrong", - secondaryMessage: "Language not found", - }), + expect(res.end).toHaveBeenCalledExactlyOnceWith( + "render-error:This username is blacklisted", ); + 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 non-whitelisted usernames before calling core logic", async () => { + mocks.config = { + whitelist: ["allowed-user"], + }; - const { req, res } = faker( - { username: "anuraghazra", include_all_commits: true }, - data_stats, - ); + const req = createRequest("username=blocked-user"); + 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: "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", + 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 a7326840ff0b8..6b4a47aef68e5 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"; +import { data_stats } from "../utils.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 mock = new MockAdapter(axios); -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", - }, - }, - }, - }, -}; +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); -const mock = new MockAdapter(axios); +let router; -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); +beforeAll(async () => { + vi.stubEnv("CACHE_SECONDS", ""); + vi.stubEnv("GIST_WHITELIST", ""); + vi.stubEnv("POSTGRES_URL", ""); + vi.stubEnv("WHITELIST", ""); + + ({ default: router } = await import("../../router.js")); - return { req, res }; -}; + mock.onPost("https://api.github.com/graphql").reply(200, data_stats); +}); -describe("/api", () => { +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 a9e52d09bb367..0000000000000 --- 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 9b15c47c2890d..997a28f9ca68e 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 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 { beforeAll, bench, describe, vi } from "vitest"; + +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 790c3a1bfd0ef..f57acdda59391 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 a923922aa4f81..3b13143f776f7 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", +}; + +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, + }, + ], + }, + }, }, - forkCount: 0, - starCount: 1, }; -/** - * @typedef {import("../../src/fetchers/types").GistData} GistData Gist data type. - */ +const CACHE_BURST_STRING = `v=${new Date().getTime()}`; + +const mock = new MockAdapter(axios, { onNoMatch: "passthrough" }); + +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); + +let router; /** - * @type {GistData} + * Renders a card locally through the backend router. + * @param {string} url Card URL to render through the router. + * @returns {Promise} Rendered SVG markup. */ -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, -}; +async function getLocalSvg(url) { + const req = { + headers: {}, + url, + }; + const res = createResponse(); -const CACHE_BURST_STRING = `v=${new Date().getTime()}`; + await router(req, res); -describe("Fetch Cards", () => { - let VERCEL_PREVIEW_URL = "https://github-stats-extended.vercel.app"; + 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")); - beforeAll(() => { - process.env.NODE_ENV = "development"; + 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 2cb6d4c3b53b3..8ee563791e66f 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 gist from "../api-renamed/gist.js"; -import { renderGistCard } from "../src/cards/gist.js"; -import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; -import { renderError } from "../src/common/render.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +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 }); +}); -import { gist_data } from "./test-data/gist-data.js"; +vi.mock("../src/common/database.js", () => ({ + storeRequest: mocks.storeRequest, + getUserAccessByName: mocks.getUserAccessByName, +})); -import "@testing-library/jest-dom/vitest"; +import router from "../router.js"; +import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; -const gist_not_found_data = { - data: { - viewer: { - gist: null, - }, - }, -}; +const createRequest = (search) => ({ + headers: {}, + url: `/api/gist?${search}`, +}); -const mock = new MockAdapter(axios); +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); -afterEach(() => { - mock.reset(); +const defaultCacheHeader = + `max-age=${CACHE_TTL.GIST_CARD.DEFAULT}, ` + + `s-maxage=${CACHE_TTL.GIST_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(() => { + 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", () => { - 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); - - await gist(req, res); - - 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, - }), - ); +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); - - await gist(req, res); - - 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 }, - ), - ); + it("should use the shorter error cache for temporary gist errors", async () => { + mocks.gist.mockResolvedValue({ + status: "error - temporary", + content: "temporary-error-svg", + }); + + const req = createRequest("id=bbfce31e0217a3689c8d961a356cb10d"); + const res = createResponse(); + + 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); - - await gist(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ message: "Gist not found" }), - ); + it("should not persist permanent gist errors returned by core", async () => { + mocks.gist.mockResolvedValue({ + status: "error - permanent", + content: "permanent-error-svg", + }); + + const req = createRequest("id=bbfce31e0217a3689c8d961a356cb10d"); + const res = createResponse(); + + 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", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), + it("should reject non-whitelisted gist ids before calling core logic", async () => { + mocks.config = { + gistWhitelist: ["allowed-gist-id"], }; - 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: "Something went wrong", - secondaryMessage: "Language not found", - }), - ); - }); - - 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); + const req = createRequest("id=blocked-gist-id"); + const res = createResponse(); - await gist(req, res); + await router(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 3e98fa8b808ca..ef01b9bae4233 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 b9fa03566dcbc..628e68fa07fd9 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"; -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 mocks = vi.hoisted(() => ({ + pin: vi.fn(), + storeRequest: vi.fn(), + getUserAccessByName: vi.fn(), +})); -const mock = new MockAdapter(axios); - -afterEach(() => { - mock.reset(); +vi.mock("@stats-organization/github-readme-stats-core", async () => { + const { mockCore } = await import("./utils.js"); + return mockCore({ pin: mocks.pin }); }); -describe("Test /api/pin", () => { - it("should test the request", 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.send).toHaveBeenCalledWith( - // @ts-ignore - renderRepoCard({ - ...data_repo.repository, - starCount: data_repo.repository.stargazers.totalCount, - }), - ); - }); +vi.mock("../src/common/database.js", () => ({ + storeRequest: mocks.storeRequest, + getUserAccessByName: mocks.getUserAccessByName, +})); - 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 } }); +import router from "../router.js"; +import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; - await pin(req, res); +const createRequest = (search = "") => ({ + headers: {}, + url: `/api/pin?${search}`, +}); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ message: "User Repository Not found" }), - ); - }); +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); - 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 } } }); +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; +}); - await pin(req, res); +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", + }); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ message: "Organization Repository Not found" }), + const req = createRequest( + "username=anuraghazra&repo=convoychat&theme=dark", ); - }); - - 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); + 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: "Something went wrong", - secondaryMessage: "Language not found", - }), - ); - }); - - it("should have proper cache", async () => { - const req = { - query: { + 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.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 b422a3b13ef14..0000000000000 --- 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 9a23afea84e1d..0000000000000 --- 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 fb81dc5577a89..0000000000000 --- 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 c72f4d6fcbd88..0000000000000 --- 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 0000000000000..1edbcd227b6ed --- /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 0000000000000..6e8e2779e7762 --- /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 0000000000000..959c008a0c8eb --- /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 0000000000000..589f315e4dd53 --- /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 0000000000000..a1a3075a93e55 --- /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 52080c8997eb3..506d9f05f09f8 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 = { + headers: {}, + url: "/api?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_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 = { - query: { - username: "renovate-bot", - }, + headers: {}, + url: `/api?${params.toString()}`, }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), + 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", }; - 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 blacklisted", - secondaryMessage: "Please deploy your own instance", - renderOptions: { show_repo_link: false }, - }), - ); + 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 421547360171f..0742ab3b00246 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 = { + headers: {}, + url: "/api/gist?id=happy-gist-id", + }; + 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()}`, + }; + 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-id response snapshot", async () => { + const { default: router } = await import("../../router.js"); + const req = { - query: {}, + headers: {}, + url: "/api/gist", }; - 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 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", }; - 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: 'Missing params "id" make sure you pass the parameters in URL', - secondaryMessage: "/api/gist?id=GIST_ID", - renderOptions: { show_repo_link: false }, - }), - ); + 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 fdbdfe98aae65..12da77f939354 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", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), + headers: {}, + url: "/api/pin?username=anuraghazra&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 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 }); + + const { default: router } = await import("../../router.js"); + + 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", + }); - await pin(req, res); + const req = { + headers: {}, + url: `/api/pin?${params.toString()}`, + }; + const res = createResponse(); + + await router(req, res); + + expect(res.end).toHaveBeenCalledOnce(); - 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({ + graphqlRequest: mock.history.post[0].data, + headers: res.setHeader.mock.calls, + content: normalizeSvg(res.end.mock.calls[0][0]), + }).toMatchSnapshot(); }); - it("should render error card if missing required parameters", async () => { + it("should match the public missing-params response snapshot", async () => { + const { default: router } = await import("../../router.js"); + const req = { - query: {}, + headers: {}, + url: "/api/pin", }; - 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 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", }; - mock.onPost("https://api.github.com/graphql").reply(200, data_user); + 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(); - await pin(req, res); - - 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 }, - }), - ); + 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 2e180e340f4cd..9eaba86cc4064 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"); + + 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(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({ + 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 0000000000000..48a23bf83a9fd --- /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 cd0c1e64b05b7..0000000000000 --- 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 8bab8d2a2add9..0000000000000 --- 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 febec1c7eb955..0000000000000 --- 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 549f2c6187323..0000000000000 --- 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 f0519629aab90..96d424e773561 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"; -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 mocks = vi.hoisted(() => ({ + topLangs: vi.fn(), + storeRequest: vi.fn(), + getUserAccessByName: vi.fn(), +})); -const mock = new MockAdapter(axios); - -afterEach(() => { - mock.reset(); +vi.mock("@stats-organization/github-readme-stats-core", async () => { + const { mockCore } = await import("./utils.js"); + return mockCore({ topLangs: mocks.topLangs }); }); -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); +vi.mock("../src/common/database.js", () => ({ + storeRequest: mocks.storeRequest, + getUserAccessByName: mocks.getUserAccessByName, +})); - await topLangs(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith(renderTopLanguages(langs)); - }); - - 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); - - 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", - }), - ); - }); - - it("should render error card on user data fetch error", async () => { - const req = { - query: { - username: "anuraghazra", - }, - }; - const res = { - setHeader: vi.fn(), - send: vi.fn(), - }; - mock.onPost("https://api.github.com/graphql").reply(200, error); +import router from "../router.js"; +import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; - await topLangs(req, res); +const createRequest = (search = "") => ({ + headers: {}, + url: `/api/top-langs?${search}`, +}); - 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", - }), - ); - }); +const createResponse = () => ({ + end: vi.fn(), + setHeader: vi.fn(), +}); - 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); +const defaultCacheHeader = + `max-age=${CACHE_TTL.TOP_LANGS_CARD.DEFAULT}, ` + + `s-maxage=${CACHE_TTL.TOP_LANGS_CARD.DEFAULT}, ` + + `stale-while-revalidate=${DURATIONS.ONE_DAY}`; + +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; +}); - await topLangs(req, res); +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", + }); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "Something went wrong", - secondaryMessage: "Incorrect layout input", - }), + const req = createRequest( + "username=anuraghazra&layout=compact&langs_count=5", ); - }); + const res = createResponse(); - 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 router(req, res); - 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: { + 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, 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 21b9f1f887391..9108685ace6cc 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 f27eec0bfb05e..e1561724fa57d 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 { 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, - }, -}; +import { beforeEach, describe, expect, it, vi } from "vitest"; -const wakaTimeNotFoundData = { - data: { - viewer: { - gist: null, - }, - }, -}; +const mocks = vi.hoisted(() => ({ + wakatime: vi.fn(), + storeRequest: vi.fn(), + getUserAccessByName: vi.fn(), +})); -const mock = new MockAdapter(axios); - -afterEach(() => { - mock.reset(); +vi.mock("@stats-organization/github-readme-stats-core", async () => { + const { mockCore } = await import("./utils.js"); + return mockCore({ wakatime: mocks.wakatime }); }); -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); - - await wakatime(req, res); - - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderWakatimeCard(wakaTimeData.data, {}), - ); - }); - - 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); - - await wakatime(req, res); +vi.mock("../src/common/database.js", () => ({ + storeRequest: mocks.storeRequest, + getUserAccessByName: mocks.getUserAccessByName, +})); - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledWith( - renderError({ - message: "Something went wrong", - secondaryMessage: "Language not found", - }), - ); - }); - - 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); +import router from "../router.js"; +import { CACHE_TTL, DURATIONS } from "../src/common/cache.js"; - expect(res.setHeader).toHaveBeenCalledWith("Content-Type", "image/svg+xml"); - expect(res.send).toHaveBeenCalledTimes(1); - expect(res.send.mock.calls[0][0]).toMatchSnapshot(); - }); +const createRequest = (search = "") => ({ + headers: {}, + url: `/api/wakatime?${search}`, +}); - 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); +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}`; + +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; +}); - 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}`, - ); +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", + }); + + const req = createRequest("username=anuraghazra&theme=dark&layout=compact"); + const res = createResponse(); + + await router(req, res); + + 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 0000000000000..4c13d7b105526 --- /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 62ccb490d7852..addf7bddfb6f7 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 9fe2e73d58016..1f3eef9d8ca26 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 0e6c6312d70a9..f7433de3c9f57 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 5da6c76b2ff07..54dcb61324b61 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 aa6d33c59508a..7e3031f48301f 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 7c85a010d5217..2834864ea47bd 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 79c02da965e01..e3328ed27ec5d 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 e73cb397847d1..a7b0b6656e6a1 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 247d8caf89243..23797cf3f1cfb 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 0000000000000..4c13d7b105526 --- /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 138025f9af2ca..6ed946480cbf9 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 b6c6e15275e2c..da01ed72e8367 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 10965e45f0653..bd2e7877c95a9 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 a837d1ec731c6..637cb46a25e12 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 b1b9de209fea7..270d6cbb80d67 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 5de96712633ff..1980b45b9c20a 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 b0ed2b1c7960f..53e0d77002694 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 0000000000000..e9e10e70a0032 --- /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 93c8e6a76c26e..afd4b1b3ad5ec 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 0000000000000..ebce051e0e6f3 --- /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 55ac2e722d79f..5709e30a08930 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 b79a74f6820ec..77107373b71f3 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 056193c81ca44..4972b6fd4a750 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 0000000000000..eaf02daf5c38a --- /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 09aef286db9f1..940271a381dc1 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 85739c463f3cb..4b91a73946107 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 27e04c2a73faf..1d72262e16cb8 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 02eac53900003..f7eeeee395cc2 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 45bc81fe11460..6345b662c639b 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 622ed161121bb..bc1395e8f737f 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 0000000000000..32456ce8ae3d1 --- /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 16c80b8b9b391..5bc3ad14804ac 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 63bbbe945804e..86adccb2e9dbe 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 5836d50ad92f6..88cf7dd9ae074 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 3653346496082..7aba75c0409d7 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 4661deffe3eda..79d294dc00abb 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 e80264ff6f8ad..450b17119d476 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 90b269c4af9a1..96d727f6b050b 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 f4a6be868ec97..1e2f3a551544b 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 dad43ebe904c7..541bb3b15de30 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 1588c921290b6..b26b066199bb3 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 0000000000000..23606735c96c4 --- /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 98fab2d304fe8..565cd3a9413f0 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 c60b504f2c666..206a71b8dfa5e 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 0000000000000..3956661798162 --- /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 3997f6664e9c4..e432cef15bd14 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 960bc34403cb3..c9d289d4f8249 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 ca0d41ed27c0c..a0336e6748002 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 b98eca4771908..d72b1e4b05d06 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 b4681f5908656..7d4531aaf124e 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 f4f2485988f40..798c5a38476f0 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 5149b49f8520a..cc1ac289f71fd 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 d5cd6ca446867..9eb55d019ec2f 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 28fbfa339191f..fe06b4b82f4e8 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 4efecf57f31b0..f14f7998863d8 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 c21bcf4580c75..c5e459feec772 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 ad50cc2dd8e56..3cd8c63eebf94 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 8c4971cf3413c..ba8577c79b6a8 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 b82716b376483..105720810cdf7 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 0000000000000..21b9f1f887391 --- /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 0000000000000..e58f87a714aed --- /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 0000000000000..c1b9c120b8b6d --- /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 0000000000000..4c13d7b105526 --- /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 0000000000000..4e11ea5320975 --- /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 a23363aa14f71..be659cdcf9d95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,125 +7,119 @@ 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 - emoji-name-map: - specifier: ^2.0.3 - version: 2.0.3 - 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 - version: 10.4.1 - '@testing-library/jest-dom': - specifier: ^6.9.1 - version: 6.9.1 - '@uppercod/css-to-object': - 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 + specifier: 5.2.1 version: 5.2.1 - js-yaml: - 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) + 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 + 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.2.3)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.2)) + 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 @@ -133,19 +127,13 @@ importers: specifier: ^1 version: 1.11.4(axios@1.13.5) daisyui: - specifier: 5.5.19 + 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 + specifier: ^18.3.1 version: 18.3.1 react-dom: - specifier: 18.3.1 + specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-icons: specifier: ^5.5.0 @@ -154,26 +142,23 @@ importers: specifier: ^3.3.1 version: 3.5.0(react@18.3.1) react-redux: - specifier: 9.2.0 + 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 + 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 + 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 + 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 @@ -183,7 +168,7 @@ importers: 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)) + 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 @@ -192,16 +177,47 @@ importers: 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 + 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 + version: 1.13.5 + emoji-name-map: + specifier: ^2.0.3 + version: 2.0.3 + github-username-regex: + specifier: ^1.0.0 + version: 1.0.0 + word-wrap: + specifier: ^1.2.5 + version: 1.2.5 + devDependencies: + '@testing-library/dom': + specifier: 10.4.1 + version: 10.4.1 + '@testing-library/jest-dom': + specifier: 6.9.1 + version: 6.9.1 + '@uppercod/css-to-object': + specifier: 1.1.1 + version: 1.1.1 + axios-mock-adapter: + specifier: 2.1.0 + version: 2.1.0(axios@1.13.5) + js-yaml: + 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) + 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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-parser/binding-wasm32-wasi@0.121.0': + resolution: {integrity: sha512-5TFISkPTymKvsmIlKasPVTPuWxzCcrT8pM+p77+mtQbIZDd1UC8zww4CJcRI46kolmgrEX6QpKO8AvWMVZ+ifw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@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-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-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] + + '@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.0': - resolution: {integrity: sha512-x5P0Y12oMcSC9PKkz1FtdVVLosXYi/05m+ufxPrUggd6vZRBPJhW4zZUsMVbz8dwwk71Dh0f6/2ntw3WPOq+Ig==} + '@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.0': - resolution: {integrity: sha512-DjnuIPB60IQrVSCiuVBzN8/8AeeIjthdkk+dZYdZzgLeP2T5ZF41u50haJMtIdGr5cRzRH6zPV/gh6+RFjlvKA==} + '@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.0': - resolution: {integrity: sha512-dVAqIZIIY7xOXCCV0nJPs8ExlYc6R7mcNpFobwNyE3qlXGbgvwb7Gl3iOumOiPBfF+sbJR3MMP7RAPfKqbvYyA==} + '@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.0': - resolution: {integrity: sha512-kwcZ30bIpJNFcT22sIlde4mz0EyXmB3lAefCFWtffqpbmLweQUwz1dKDcsutxEjpkbEKLmfrj1wCyRZp7n5Hnw==} + '@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.0': - resolution: {integrity: sha512-GImk/cb3X+zBGEwr6l9h0dbiNo5zNd52gamZmluEpbyybiZ8kc5q44/7zRR4ILChWRW7pI92W57CJwhkF+wRmg==} + '@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.0': - resolution: {integrity: sha512-uIEyws3bBD1gif4SZCOV2XIr6q5fd1WbzzBbpL8qk+TbzOvKMWnMNNtfNacnAGGa2lLRNXR1Fffot2mlZ/Xmbw==} + '@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.0': - resolution: {integrity: sha512-bIkgp+AB+yZfvdKDfjFT7PycsRtih7+zCV5AbnkzfyvNvQ47rfssf8R1IbG++mx+rZ4YUCUu8EbP66HC3O5c5w==} + '@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.0': - resolution: {integrity: sha512-bOt5pKPcbidTSy64m2CfM0XcaCmxBEFclCMPuOPO08hh8QIFTiZVhFf/OxTFqyRwhq/tlzzKmXpMo7DfzbO5lQ==} + '@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.0': - resolution: {integrity: sha512-BymEPqVeLZzA/1kXow9U9rdniq1r5kk4u686Cx3ZU77YygR48NJI/2TyjM70vKHZffGx75ZShobcc1M5GXG3WA==} + '@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.0': - resolution: {integrity: sha512-aFgPTzZZY+XCYe4B+3A1S63xcIh2i136+2TPXWr9NOwXXTdMdBntb1J9fEgxXDnX82MjBknLUpJqAZHNTJzixA==} + '@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.0': - resolution: {integrity: sha512-9WDGt7fV9GK97WrWE/VEDhMFv9m0ZXYn5NQ+16QvyT0ux8yGLAvyadi6viaTjEdJII/OaHBRYHcL+zUjmaWwmg==} + '@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.0': - resolution: {integrity: sha512-SY3di6tccocppAVal5Hev3D6D1N5Y6TCEypAvNCOiPqku2Y8U/aXfvGbthqdPNa72KYqjUR1vomOv6J9thHITA==} + '@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.0': - resolution: {integrity: sha512-SV+4zBeCC3xjSE2wvhN45eyABoVRX3xryWBABFKfLwAWhF3wsB3bUF+CantYfQ/TLpasyvplRS9ovvFT9cb/0A==} + '@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.0': - resolution: {integrity: sha512-LkbjO+r5Isl8Xl29pJYOCB/iSUIULFUJDGdMp+yJD3OgWtSa6VJta2iw7QXmpcoOkq18UIL09yWrlyjLDL0Hug==} + '@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.0': - resolution: {integrity: sha512-Ud1gelL5slpEU5AjzBWQz1WheprOAl5CPnCKTWynvvdlBbAZXA6fPYLuCrlRo0uw+x3f37XJ71kirpSew8Zyvg==} + '@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.0': - resolution: {integrity: sha512-wXLNAVmL4vWXKaYJnFPgg5zQsSr3Rv+ftNReIU3UkzTcoVLK0805Pnbr2NwcBWSO5hhpOEdys02qlT2kxVgjWw==} + '@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.0': - resolution: {integrity: sha512-zszvr0dJfvv0Jg49hLwjAJ4SRzfsq28SoearUtT1qv3qXRYsBWuctdlRa/lEZkiuG4tZWiY425Jh9QqLafwsAg==} + '@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.0': - resolution: {integrity: sha512-I7ZYujr5XL1l7OwuddbOeqdUyFOaf51W1U2xUogInFdupIAKGqbpugpAK6RaccLcSlN0bbuo3CS5h7ue38SUAg==} + '@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.0': - resolution: {integrity: sha512-NxErbI1TmJEZZVvGPePjgXFZCuOzrjQuJ6YwHjcWkelReK7Uhg4QeL05zRdfTpgkH6IY/C8OjbKx5ZilQ4yDFg==} + '@oxc-resolver/binding-win32-x64-msvc@11.19.1': + resolution: {integrity: sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw==} cpu: [x64] os: [win32] - '@playwright/test@1.58.2': - resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + '@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': - 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/js@10.0.1(eslint@10.1.0(jiti@2.6.1))': + optionalDependencies: + eslint: 10.1.0(jiti@2.6.1) - '@eslint/object-schema@2.1.7': {} + '@eslint/object-schema@3.0.3': {} - '@eslint/plugin-kit@0.4.1': + '@eslint/plugin-kit@0.6.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-parser/binding-win32-arm64-msvc@0.121.0': + optional: true + + '@oxc-parser/binding-win32-ia32-msvc@0.121.0': + optional: true + + '@oxc-parser/binding-win32-x64-msvc@0.121.0': + optional: true + + '@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-win32-arm64-msvc@11.19.0': + '@oxc-resolver/binding-darwin-x64@11.19.1': optional: true - '@oxc-resolver/binding-win32-ia32-msvc@11.19.0': + '@oxc-resolver/binding-freebsd-x64@11.19.1': optional: true - '@oxc-resolver/binding-win32-x64-msvc@11.19.0': + '@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1': optional: true - '@playwright/test@1.58.2': + '@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.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.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + 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/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@25.5.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@25.5.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.44.1)(yaml@2.8.3) - '@vitest/pretty-format@4.0.18': + '@vitest/pretty-format@4.1.2': dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/runner@4.0.18': + '@vitest/runner@4.1.2': dependencies: - '@vitest/utils': 4.0.18 + '@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: - 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: + browserslist@4.28.2: 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-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.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-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@4.0.3: {} - - pidtree@0.6.0: {} + picomatch@2.3.2: {} - pkg-dir@5.0.0: - dependencies: - find-up: 5.0.0 + picomatch@4.0.4: {} - playwright-core@1.58.2: {} + playwright-core@1.59.1: {} - playwright@1.58.2: + playwright@1.59.1: 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: + 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: - escape-string-regexp: 5.0.0 - globby: 14.1.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': 24.12.0 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 + terser: 5.44.1 + yaml: 2.8.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): + 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: 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': 25.5.0 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.31.1 terser: 5.44.1 - yaml: 2.8.2 - - 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): - 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 + 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 6e443191f7041..a01c6b710267e 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 39e84b3a6255c..00912579bd52b 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 2a537f3a563e1..3dcd084049b51 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 0000000000000..48832af8d678e --- /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 35ebfe01297c9..29b82d840a064 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 0000000000000..22fc386c98905 --- /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 0000000000000..c3c14e5ba319e --- /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", + ], + }, +});