From 3ad2c6a71442c7a5fa2f0ad93d22ef001813b1e0 Mon Sep 17 00:00:00 2001 From: "..." <...> Date: Wed, 24 Jun 2026 18:23:51 +0100 Subject: [PATCH 1/3] feat(backend): CI/CD pipeline - GitHub Actions for test, build, deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ci.yml: lint, typecheck, test, build quality gates on every PR - ci.yml: Docker build + Trivy scan on merge to main (pushed to ghcr.io) - deploy.yml: progressive staging → smoke tests → production delivery - vitest.config.ts + test scripts for unit testing infrastructure - Dockerfile: multi-stage Next.js build with standalone output - Fix lint error (trailing comma in adminAnalytics.ts) - Fix test failure (formatCredits rounding) - Remove deprecated .eslintignore - Add typecheck & lint:strict scripts Closes #556 --- .dockerignore | 21 ++ .eslintignore | 36 --- .github/workflows/ci.yml | 205 +++++++++++-- .github/workflows/deploy.yml | 99 ++++-- Dockerfile | 33 ++ lib/admin/projectFilters.ts | 5 +- lib/api/mock/adminAnalytics.ts | 2 +- next.config.ts | 1 + package.json | 8 +- pnpm-lock.yaml | 535 +++++++++++++++++++++++++++++++++ vitest.config.ts | 16 + 11 files changed, 879 insertions(+), 82 deletions(-) create mode 100644 .dockerignore delete mode 100644 .eslintignore create mode 100644 Dockerfile create mode 100644 vitest.config.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..883674cd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,21 @@ +.git +.gitignore +.next +node_modules +pnpm-lock.yaml +Dockerfile +.dockerignore +README.md +*.md +contracts +farm Guage +.kiro +.husky +.github +.vscode +docs +scripts +public/icons +*.log +.env +.env.* diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 7cd86176..00000000 --- a/.eslintignore +++ /dev/null @@ -1,36 +0,0 @@ -# Temporary documentation files -*.md -!README.md - -# Build outputs -.next/ -out/ -dist/ -build/ - -# Dependencies -node_modules/ - -# Environment files -.env -.env.local -.env.*.local - -# Logs -*.log - -# OS files -.DS_Store -Thumbs.db - -# IDE -.vscode/ -.idea/ - -# Temporary fix scripts -fix-*.js -fix-*.bat -fix-*.ps1 -emergency-*.ps1 -emergency-*.cmd -generate-*.bat diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75c3c092..3285aa9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,40 +5,147 @@ on: push: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: 20 + PNPM_VERSION: 10 + NEXT_TELEMETRY_DISABLED: 1 + # Require all NEXT_PUBLIC_* vars to be set during build + CI_BUILD_ENFORCED: true + +# ─── Shared boilerplate for every Node job ───────────────────────────────────── +# +# Each job repeats the checkout + pnpm + node setup because that keeps jobs +# hermetic — no shared state, no cross-job coupling. If latency becomes a +# concern, extract these steps into a composite action in .github/actions/. + jobs: - lint-and-build: - name: Lint + Build + # ═════════════════════════════════════════════════════════════════════════════ + # PHASE 1 — Fast quality gates (run in parallel, < 2 min each) + # ═════════════════════════════════════════════════════════════════════════════ + + lint: + name: Lint runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm lint + typecheck: + name: TypeScript + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm typecheck + test: + name: Test + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm test -- --reporter=junit --outputFile=test-results.xml + - name: Upload test report + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: test-results.xml + retention-days: 7 - - name: Get pnpm store path - id: pnpm-cache - run: echo "dir=$(pnpm store path)" >> "$GITHUB_OUTPUT" + env-check: + name: Env vars + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - uses: actions/checkout@v4 + - name: Validate required NEXT_PUBLIC_* vars are not placeholders + run: | + errors=0 + while IFS= read -r line; do + # Skip comments and blank lines + [[ "$line" =~ ^# ]] && continue + [[ -z "$line" ]] && continue + var="${line%%=*}" + val="${line#*=}" + val="${val#\"}"; val="${val%\"}" # strip quotes + if [[ "$val" == REPLACE_WITH_* ]]; then + echo "❌ $var is still a placeholder: $val" + ((errors++)) + fi + done < .env.example + if (( errors > 0 )); then + echo "⚠️ $errors env var(s) still contain placeholder values." + echo "Fix them in .env before deploying to production." + else + echo "✅ All env vars look good." + fi + # Warn only — don't block PRs on placeholder vars during development - - uses: actions/cache@v4 - with: - path: | - ${{ steps.pnpm-cache.outputs.dir }} - .next/cache - key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-nextjs-${{ hashFiles('**/*.[jt]s', '**/*.[jt]sx') }} - restore-keys: | - pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}- - pnpm-${{ runner.os }}- + # ═════════════════════════════════════════════════════════════════════════════ + # PHASE 2 — Expensive gate (build). Waits for all Phase-1 gates to pass. + # ═════════════════════════════════════════════════════════════════════════════ + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: [lint, typecheck, test] + outputs: + build-cache-hit: ${{ steps.build-cache.outputs.cache-hit }} + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm - run: pnpm install --frozen-lockfile - - run: pnpm lint + # Cache .next between CI runs (keyed on source hash) + - name: Cache Next.js build + id: build-cache + uses: actions/cache@v4 + with: + path: .next/cache + key: nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', '**/*.[jt]s', '**/*.[jt]sx', '**/*.css') }} + restore-keys: | + nextjs-${{ runner.os }}- - - run: pnpm build + - name: Build + run: pnpm build env: - # Provide stub env vars so Next.js config doesn't throw at build time NEXT_PUBLIC_STELLAR_NETWORK: testnet NEXT_PUBLIC_HORIZON_URL: https://horizon-testnet.stellar.org NEXT_PUBLIC_SOROBAN_RPC_URL: https://soroban-testnet.stellar.org @@ -56,3 +163,65 @@ jobs: NEXT_PUBLIC_ANCHOR_API_URL: https://testanchor.stellar.org NEXT_PUBLIC_ANCHOR_HOME_DOMAIN: testanchor.stellar.org NEXT_PUBLIC_APP_URL: http://localhost:3000 + + # ═════════════════════════════════════════════════════════════════════════════ + # PHASE 3 — Docker image (main branch only) + # ═════════════════════════════════════════════════════════════════════════════ + + docker: + name: Docker + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: [build] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=sha,format=long + type=ref,event=branch + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: mode=max + + - name: Scan image for vulnerabilities + uses: aquasecurity/trivy-action@master + with: + image-ref: ghcr.io/${{ github.repository }}:${{ steps.meta.outputs.version }} + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH + + - name: Upload Trivy report + if: always() + uses: actions/upload-artifact@v4 + with: + name: trivy-report + path: trivy-results.sarif + retention-days: 30 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3461d21f..aa7014f5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,51 +3,106 @@ name: Deploy on: push: branches: [main] + workflow_dispatch: + inputs: + environment: + description: 'Target environment' + required: true + default: staging + type: choice + options: + - staging + - production + +concurrency: + group: deploy-${{ github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: 20 + PNPM_VERSION: 10 jobs: - deploy: - name: Deploy to Vercel + # ═════════════════════════════════════════════════════════════════════════════ + # STAGING — preview deploy on every main push + # ═════════════════════════════════════════════════════════════════════════════ + + staging: + name: Deploy to Staging runs-on: ubuntu-latest + environment: + name: staging + url: ${{ steps.vercel-staging.outputs.preview-url }} outputs: - deployment-url: ${{ steps.vercel.outputs.preview-url }} + deployment-url: ${{ steps.vercel-staging.outputs.preview-url }} steps: - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + - run: pnpm install --frozen-lockfile - - uses: pnpm/action-setup@v4 + - name: Deploy to Vercel (staging) + id: vercel-staging + uses: amondnet/vercel-action@v25 with: - version: 10 + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} + vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} + vercel-args: '--build' + + smoke-tests-staging: + name: Smoke tests (staging) + needs: staging + uses: ./.github/workflows/smoke-tests.yml + with: + base-url: ${{ needs.staging.outputs.deployment-url }} + secrets: inherit - - name: Get pnpm store path - id: pnpm-cache - run: echo "dir=$(pnpm store path)" >> "$GITHUB_OUTPUT" + # ═════════════════════════════════════════════════════════════════════════════ + # PRODUCTION — only after staging smoke tests pass. Requires manual approval + # when triggered via workflow_dispatch (GitHub Environments). + # ═════════════════════════════════════════════════════════════════════════════ - - uses: actions/cache@v4 - with: - path: | - ${{ steps.pnpm-cache.outputs.dir }} - .next/cache - key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-nextjs-${{ hashFiles('**/*.[jt]s', '**/*.[jt]sx') }} - restore-keys: pnpm-${{ runner.os }}- + production: + name: Deploy to Production + runs-on: ubuntu-latest + needs: smoke-tests-staging + environment: + name: production + url: ${{ steps.vercel-prod.outputs.preview-url }} + outputs: + deployment-url: ${{ steps.vercel-prod.outputs.preview-url }} + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm - run: pnpm install --frozen-lockfile - name: Deploy to Vercel (production) - id: vercel + id: vercel-prod uses: amondnet/vercel-action@v25 with: vercel-token: ${{ secrets.VERCEL_TOKEN }} vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} - vercel-args: '--prod' + vercel-args: '--prod --build' - smoke-tests: - name: Smoke tests - needs: deploy + smoke-tests-prod: + name: Smoke tests (production) + needs: production uses: ./.github/workflows/smoke-tests.yml with: - base-url: ${{ needs.deploy.outputs.deployment-url }} + base-url: ${{ needs.production.outputs.deployment-url }} secrets: inherit diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..3957004f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM node:20-alpine AS base +RUN corepack enable && corepack prepare pnpm@10 --activate + +FROM base AS deps +WORKDIR /app +COPY pnpm-lock.yaml ./ +RUN pnpm fetch --frozen-lockfile + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +RUN pnpm build + +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/lib/admin/projectFilters.ts b/lib/admin/projectFilters.ts index 9b3c7a8c..367e4e47 100644 --- a/lib/admin/projectFilters.ts +++ b/lib/admin/projectFilters.ts @@ -112,10 +112,7 @@ export function formatDate(dateString: string): string { } export function formatCredits(credits: number): string { - return credits.toLocaleString('en-US', { - minimumFractionDigits: 0, - maximumFractionDigits: 2, - }); + return Math.round(credits).toLocaleString('en-US'); } export function formatPrice(price: number): string { diff --git a/lib/api/mock/adminAnalytics.ts b/lib/api/mock/adminAnalytics.ts index c071110a..68627bb2 100644 --- a/lib/api/mock/adminAnalytics.ts +++ b/lib/api/mock/adminAnalytics.ts @@ -29,7 +29,7 @@ function scaleMetrics(ratio: number): AdminAnalyticsMetrics { } export async function getAdminAnalyticsData( - range: AnalyticsTimeRange = '30d', + range: AnalyticsTimeRange = '30d' ): Promise { await new Promise((resolve) => setTimeout(resolve, 400)); return { diff --git a/next.config.ts b/next.config.ts index f16673b4..e8113fe6 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { + output: 'standalone', outputFileTracingRoot: __dirname, experimental: { cpus: 1, diff --git a/package.json b/package.json index 4e8a9f0d..17fe1553 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,13 @@ "start": "next start", "lint": "eslint .", "lint:fix": "eslint . --fix", + "lint:strict": "eslint . --max-warnings 0", + "typecheck": "tsc --noEmit", "format": "prettier --write .", "format:check": "prettier --check .", + "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui", "generate-icons": "node scripts/generate-icons.mjs", "indexer": "tsx lib/indexer/worker.ts", "db:migrate": "psql $DATABASE_URL -f db/migrations/001_create_indexed_transactions.sql", @@ -91,7 +96,8 @@ "tailwindcss": "^4", "tsx": "4.19.4", "tw-animate-css": "^1.4.0", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.9" }, "packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316", "lint-staged": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68442c89..acdceb21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,6 +219,9 @@ importers: typescript: specifier: ^5 version: 5.9.3 + vitest: + specifier: ^4.1.9 + version: 4.1.9(@types/node@20.19.37)(msw@2.12.14(@types/node@20.19.37)(typescript@5.9.3))(vite@8.1.0(@types/node@20.19.37)(jiti@2.6.1)(tsx@4.19.4)(yaml@2.8.3)) packages: @@ -621,15 +624,24 @@ packages: peerDependencies: '@noble/ciphers': ^1.0.0 + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/core@1.9.1': resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.9.1': resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} '@emnapi/wasi-threads@1.2.0': resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -1167,6 +1179,12 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@next/env@16.1.6': resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} @@ -1261,6 +1279,9 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2038,6 +2059,98 @@ packages: react-redux: optional: true + '@rolldown/binding-android-arm64@1.1.3': + resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.3': + resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.3': + resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.3': + resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.3': + resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.1.3': + resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.1.3': + resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.1.3': + resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.1.3': + resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.1.3': + resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.3': + resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.3': + resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.3': + resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -2406,6 +2519,12 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -2433,6 +2552,9 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -2640,6 +2762,35 @@ packages: cpu: [x64] os: [win32] + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + abs-svg-path@0.1.1: resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==} @@ -2748,6 +2899,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -2939,6 +3094,10 @@ packages: resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} engines: {node: '>=10.0.0'} + 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'} @@ -3343,6 +3502,9 @@ packages: resolution: {integrity: sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==} engines: {node: '>= 0.4'} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -3529,6 +3691,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -3574,6 +3739,10 @@ packages: exifr@7.1.3: resolution: {integrity: sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + express-rate-limit@8.3.1: resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==} engines: {node: '>= 16'} @@ -4564,6 +4733,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -4680,6 +4854,10 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -4797,6 +4975,9 @@ packages: path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} @@ -4884,6 +5065,10 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.8: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} @@ -5245,6 +5430,11 @@ packages: resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} engines: {node: '>= 0.8.15'} + rolldown@1.1.3: + resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -5355,6 +5545,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -5401,6 +5594,9 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackblur-canvas@2.7.0: resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} engines: {node: '>=0.1.14'} @@ -5412,6 +5608,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + stdin-discarder@0.2.2: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} @@ -5565,6 +5764,9 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.0.4: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} @@ -5573,6 +5775,14 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + tldts-core@7.0.27: resolution: {integrity: sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==} @@ -5764,6 +5974,90 @@ packages: resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==} engines: {node: '>= 6'} + vite@8.1.0: + resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} + 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.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -5810,6 +6104,11 @@ packages: engines: {node: ^16.13.0 || >=18.0.0} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -6713,12 +7012,23 @@ snapshots: dependencies: '@noble/ciphers': 1.3.0 + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.9.1': dependencies: '@emnapi/wasi-threads': 1.2.0 tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.9.1': dependencies: tslib: 2.8.1 @@ -6729,6 +7039,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -7333,6 +7648,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + '@next/env@16.1.6': {} '@next/eslint-plugin-next@16.1.6': @@ -7396,6 +7718,8 @@ snapshots: '@open-draft/until@2.1.0': {} + '@oxc-project/types@0.137.0': {} + '@pkgr/core@0.2.9': {} '@radix-ui/number@1.1.1': {} @@ -8283,6 +8607,57 @@ snapshots: react: 19.2.3 react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.3)(redux@5.0.1) + '@rolldown/binding-android-arm64@1.1.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.3': + optional: true + + '@rolldown/binding-darwin-x64@1.1.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.3': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + '@rtsao/scc@1.1.0': {} '@sec-ant/readable-stream@0.4.1': {} @@ -8775,6 +9150,16 @@ snapshots: tslib: 2.8.1 optional: true + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -8799,6 +9184,8 @@ snapshots: '@types/d3-timer@3.0.2': {} + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/geojson@7946.0.16': {} @@ -8999,6 +9386,48 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vitest/expect@4.1.9': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.9(msw@2.12.14(@types/node@20.19.37)(typescript@5.9.3))(vite@8.1.0(@types/node@20.19.37)(jiti@2.6.1)(tsx@4.19.4)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.12.14(@types/node@20.19.37)(typescript@5.9.3) + vite: 8.1.0(@types/node@20.19.37)(jiti@2.6.1)(tsx@4.19.4)(yaml@2.8.3) + + '@vitest/pretty-format@4.1.9': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.9': + dependencies: + '@vitest/utils': 4.1.9 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.9': {} + + '@vitest/utils@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + abs-svg-path@0.1.1: {} accepts@2.0.0: @@ -9127,6 +9556,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} ast-types@0.16.1: @@ -9325,6 +9756,8 @@ snapshots: svg-pathdata: 6.0.3 optional: true + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -9744,6 +10177,8 @@ snapshots: math-intrinsics: 1.1.0 safe-array-concat: 1.1.3 + es-module-lexer@2.1.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -10030,6 +10465,10 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} etag@1.8.1: {} @@ -10123,6 +10562,8 @@ snapshots: exifr@7.1.3: {} + expect-type@1.3.0: {} + express-rate-limit@8.3.1(express@5.2.1): dependencies: express: 5.2.1 @@ -11080,6 +11521,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@3.3.15: {} + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -11199,6 +11642,8 @@ snapshots: obuf@1.1.2: {} + obug@2.1.3: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -11316,6 +11761,8 @@ snapshots: path-to-regexp@8.3.0: {} + pathe@2.0.3: {} + performance-now@2.1.0: optional: true @@ -11397,6 +11844,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.15: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.5.8: dependencies: nanoid: 3.3.11 @@ -11753,6 +12206,27 @@ snapshots: rgbcolor@1.0.1: optional: true + rolldown@1.1.3: + dependencies: + '@oxc-project/types': 0.137.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.3 + '@rolldown/binding-darwin-arm64': 1.1.3 + '@rolldown/binding-darwin-x64': 1.1.3 + '@rolldown/binding-freebsd-x64': 1.1.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.3 + '@rolldown/binding-linux-arm64-gnu': 1.1.3 + '@rolldown/binding-linux-arm64-musl': 1.1.3 + '@rolldown/binding-linux-ppc64-gnu': 1.1.3 + '@rolldown/binding-linux-s390x-gnu': 1.1.3 + '@rolldown/binding-linux-x64-gnu': 1.1.3 + '@rolldown/binding-linux-x64-musl': 1.1.3 + '@rolldown/binding-openharmony-arm64': 1.1.3 + '@rolldown/binding-wasm32-wasi': 1.1.3 + '@rolldown/binding-win32-arm64-msvc': 1.1.3 + '@rolldown/binding-win32-x64-msvc': 1.1.3 + router@2.2.0: dependencies: debug: 4.4.3 @@ -11968,6 +12442,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -12016,6 +12492,8 @@ snapshots: stable-hash@0.0.5: {} + stackback@0.0.2: {} + stackblur-canvas@2.7.0: optional: true @@ -12025,6 +12503,8 @@ snapshots: statuses@2.0.2: {} + std-env@4.1.0: {} + stdin-discarder@0.2.2: {} stellar-sdk@13.3.0: @@ -12185,6 +12665,8 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + tinyexec@1.0.4: {} tinyglobby@0.2.15: @@ -12192,6 +12674,13 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + tldts-core@7.0.27: {} tldts@7.0.27: @@ -12431,6 +12920,47 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + vite@8.1.0(@types/node@20.19.37)(jiti@2.6.1)(tsx@4.19.4)(yaml@2.8.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.1.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 20.19.37 + fsevents: 2.3.3 + jiti: 2.6.1 + tsx: 4.19.4 + yaml: 2.8.3 + + vitest@4.1.9(@types/node@20.19.37)(msw@2.12.14(@types/node@20.19.37)(typescript@5.9.3))(vite@8.1.0(@types/node@20.19.37)(jiti@2.6.1)(tsx@4.19.4)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(msw@2.12.14(@types/node@20.19.37)(typescript@5.9.3))(vite@8.1.0(@types/node@20.19.37)(jiti@2.6.1)(tsx@4.19.4)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.1.0(@types/node@20.19.37)(jiti@2.6.1)(tsx@4.19.4)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.37 + transitivePeerDependencies: + - msw + void-elements@3.1.0: {} wasmbuilder@0.0.16: {} @@ -12494,6 +13024,11 @@ snapshots: dependencies: isexe: 3.1.5 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@6.2.0: diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..f682f26b --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config'; +import path from 'path'; + +export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname), + }, + }, + test: { + globals: true, + environment: 'node', + include: ['**/__tests__/**/*.test.{ts,tsx}'], + exclude: ['node_modules', '.next', 'contracts'], + }, +}); From a6309a7fb351a6eec6a29a133ca1f806cc0cd6fa Mon Sep 17 00:00:00 2001 From: "..." <...> Date: Wed, 24 Jun 2026 18:43:20 +0100 Subject: [PATCH 2/3] fix(ci): remove Node 20 pin and pnpm version conflict - Remove explicit Node version (runner now defaults to Node 24) - Remove explicit pnpm version (read from packageManager in package.json) - Fixes 'Multiple versions of pnpm specified' error and Node 20 deprecation warning --- .github/workflows/ci.yml | 15 --------------- .github/workflows/deploy.yml | 10 ---------- 2 files changed, 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3285aa9f..8be3f2c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,7 @@ concurrency: cancel-in-progress: true env: - NODE_VERSION: 20 - PNPM_VERSION: 10 NEXT_TELEMETRY_DISABLED: 1 - # Require all NEXT_PUBLIC_* vars to be set during build CI_BUILD_ENFORCED: true # ─── Shared boilerplate for every Node job ───────────────────────────────────── @@ -34,11 +31,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: ${{ env.PNPM_VERSION }} - uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm lint @@ -50,11 +44,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: ${{ env.PNPM_VERSION }} - uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm typecheck @@ -66,11 +57,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: ${{ env.PNPM_VERSION }} - uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm test -- --reporter=junit --outputFile=test-results.xml @@ -125,11 +113,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: ${{ env.PNPM_VERSION }} - uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} cache: pnpm - run: pnpm install --frozen-lockfile diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index aa7014f5..ce9631e1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,10 +18,6 @@ concurrency: group: deploy-${{ github.ref }} cancel-in-progress: true -env: - NODE_VERSION: 20 - PNPM_VERSION: 10 - jobs: # ═════════════════════════════════════════════════════════════════════════════ # STAGING — preview deploy on every main push @@ -39,11 +35,8 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: ${{ env.PNPM_VERSION }} - uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} cache: pnpm - run: pnpm install --frozen-lockfile @@ -82,11 +75,8 @@ jobs: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - with: - version: ${{ env.PNPM_VERSION }} - uses: actions/setup-node@v4 with: - node-version: ${{ env.NODE_VERSION }} cache: pnpm - run: pnpm install --frozen-lockfile From e9fd712426cd03e3309e316eb68dcb153e0ed2c2 Mon Sep 17 00:00:00 2001 From: "..." <...> Date: Wed, 24 Jun 2026 18:48:45 +0100 Subject: [PATCH 3/3] fix(ci): prevent env-check step from failing on placeholders --- .github/workflows/ci.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8be3f2c1..c916301a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,28 +76,25 @@ jobs: timeout-minutes: 2 steps: - uses: actions/checkout@v4 - - name: Validate required NEXT_PUBLIC_* vars are not placeholders + - name: Validate env vars are not placeholders run: | errors=0 while IFS= read -r line; do - # Skip comments and blank lines - [[ "$line" =~ ^# ]] && continue - [[ -z "$line" ]] && continue + [[ "$line" =~ ^# || -z "$line" ]] && continue var="${line%%=*}" val="${line#*=}" - val="${val#\"}"; val="${val%\"}" # strip quotes + val="${val#\"}"; val="${val%\"}" if [[ "$val" == REPLACE_WITH_* ]]; then - echo "❌ $var is still a placeholder: $val" - ((errors++)) + echo "⚠️ $var still contains a placeholder: $val" + errors=$(( errors + 1 )) fi done < .env.example if (( errors > 0 )); then - echo "⚠️ $errors env var(s) still contain placeholder values." - echo "Fix them in .env before deploying to production." + echo "Found $errors placeholder(s). Update .env before production." else echo "✅ All env vars look good." fi - # Warn only — don't block PRs on placeholder vars during development + # Non-blocking — placeholders are expected during development # ═════════════════════════════════════════════════════════════════════════════ # PHASE 2 — Expensive gate (build). Waits for all Phase-1 gates to pass.