diff --git a/renderer/.dockerignore b/renderer/.dockerignore new file mode 100644 index 0000000..480061c --- /dev/null +++ b/renderer/.dockerignore @@ -0,0 +1,7 @@ +node_modules/ +.git/ +.tmp/ +coverage/ +fixtures/*.gif +.env +*.log diff --git a/renderer/.env.example b/renderer/.env.example new file mode 100644 index 0000000..23d889d --- /dev/null +++ b/renderer/.env.example @@ -0,0 +1,10 @@ +PORT=3000 +HOST=0.0.0.0 +NODE_ENV=production +MAX_CONCURRENT_RENDERS=1 +# Remotion browser timeout floor is 7000ms; lower values are raised to 7000. +RENDER_TIMEOUT_MS=15000 +MAX_OPTIONS=32 +MAX_OPTION_CHARS=40 +# Required in production. Clients must send Authorization: Bearer . +API_TOKEN=change-me diff --git a/renderer/.github/workflows/ci.yml b/renderer/.github/workflows/ci.yml new file mode 100644 index 0000000..d69087c --- /dev/null +++ b/renderer/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 11.9.0 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm lint + - run: pnpm typecheck + - run: pnpm test + - name: Install Remotion browser runtime libraries + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libnspr4 \ + libnss3 \ + libgtk-3-0 \ + libgbm1 \ + libasound2t64 \ + libxshmfence1 \ + fonts-noto + - run: pnpm browser:ensure + - run: pnpm render:smoke + - run: pnpm api:smoke diff --git a/renderer/.gitignore b/renderer/.gitignore new file mode 100644 index 0000000..f4a3dc6 --- /dev/null +++ b/renderer/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +coverage/ +.env +.env.* +!.env.example +.tmp/ +fixtures/*.gif +*.log diff --git a/renderer/.npmrc b/renderer/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/renderer/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/renderer/AGENTS.md b/renderer/AGENTS.md new file mode 100644 index 0000000..35e3186 --- /dev/null +++ b/renderer/AGENTS.md @@ -0,0 +1,27 @@ +# AGENTS.md + +## Project Context + +`wheelofnames` is a Node.js 24 JavaScript + JSDoc API service that renders +animated wheel-of-names GIFs with Remotion. + +## Development Rules + +- Use JavaScript and JSDoc. Do not add TypeScript source files. +- Keep public API routes under `/api`. +- Run `pnpm lint`, `pnpm typecheck`, and `pnpm test` before committing code + changes. +- Run `pnpm render:smoke` when renderer, Remotion composition, Docker runtime, + or GIF output behavior changes. +- Run `pnpm api:smoke` when API render behavior, bundle warm-up, or production + render flow changes. +- Do not commit `.env`, generated GIFs, tokens, secrets, or temporary render + artifacts. +- Prefer Remotion libraries and CSS transforms over custom SVG path generation. + +## Quality Gates + +- API validation must reject oversized requests before render work starts. +- Rendering must not rebundle Remotion per request. +- Winner metadata must be returned with GIF responses. +- Keep Docker and README deployment docs in sync with runtime requirements. diff --git a/renderer/Dockerfile b/renderer/Dockerfile new file mode 100644 index 0000000..f1eb063 --- /dev/null +++ b/renderer/Dockerfile @@ -0,0 +1,48 @@ +FROM node:24-bookworm-slim AS base + +ENV NODE_ENV=production +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + fonts-noto \ + fonts-noto-color-emoji \ + libasound2 \ + libatk-bridge2.0-0 \ + libatk1.0-0 \ + libcairo2 \ + libcups2 \ + libdbus-1-3 \ + libdrm2 \ + libexpat1 \ + libgbm1 \ + libglib2.0-0 \ + libgtk-3-0 \ + libnspr4 \ + libnss3 \ + libpango-1.0-0 \ + libx11-6 \ + libx11-xcb1 \ + libxcb1 \ + libxcomposite1 \ + libxdamage1 \ + libxext6 \ + libxfixes3 \ + libxrandr2 \ + libxshmfence1 \ + libxtst6 \ + xdg-utils \ + && rm -rf /var/lib/apt/lists/* + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY scripts/ensure-browser.js ./scripts/ensure-browser.js +RUN corepack enable \ + && corepack prepare pnpm@11.9.0 --activate \ + && pnpm install --prod --frozen-lockfile \ + && pnpm browser:ensure + +COPY . . + +EXPOSE 3000 +CMD ["pnpm", "start"] diff --git a/renderer/README.md b/renderer/README.md new file mode 100644 index 0000000..9741d7e --- /dev/null +++ b/renderer/README.md @@ -0,0 +1,71 @@ +# wheelofnames + +Self-hosted API that renders wheel-of-names GIF animations with Remotion. + +## API + +```http +POST /api/gif +Content-Type: application/json +Accept: image/gif +Authorization: Bearer change-me +``` + +```json +{ + "options": ["alice", "bob", "carol"], + "winnerIndex": 1, + "durationMs": 6500, + "holdMs": 1200, + "fps": 15, + "size": 512, + "theme": "classic" +} +``` + +Response is `image/gif` with winner metadata headers: + +- `X-Wheel-Winner-Index` +- `X-Wheel-Winner` +- `X-Render-Duration-Ms` + +`X-Wheel-Winner` is URL-encoded so non-ASCII labels are safe in HTTP headers. + +## Local + +```sh +pnpm install +pnpm dev +``` + +Smoke render: + +```sh +pnpm browser:ensure +pnpm render:smoke +pnpm api:smoke +``` + +Quality gates: + +```sh +pnpm lint +pnpm typecheck +pnpm test +``` + +## Deploy + +Use a container runtime first. Static-only platforms cannot satisfy +`POST /api/gif` because Remotion server rendering needs Node, Chromium/runtime +dependencies, and FFmpeg/compositor support. + +```sh +docker build -t wheelofnames . +docker run --rm -p 3000:3000 -e API_TOKEN=change-me wheelofnames +``` + +Recommended starting resources: 1-2 vCPU and 1-2 GB RAM, with +`MAX_CONCURRENT_RENDERS=1`. `RENDER_TIMEOUT_MS` defaults to `15000` and is +raised to Remotion's `7000ms` browser timeout floor when configured lower. +`API_TOKEN` is required when `NODE_ENV=production`. diff --git a/renderer/compose.yml b/renderer/compose.yml new file mode 100644 index 0000000..3b63ef9 --- /dev/null +++ b/renderer/compose.yml @@ -0,0 +1,18 @@ +services: + wheelofnames: + build: . + environment: + HOST: 0.0.0.0 + PORT: 3000 + API_TOKEN: change-me + MAX_CONCURRENT_RENDERS: 1 + RENDER_TIMEOUT_MS: 15000 + MAX_OPTIONS: 32 + MAX_OPTION_CHARS: 40 + ports: + - "3000:3000" + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/api/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 30s + timeout: 5s + retries: 3 diff --git a/renderer/docs/deployment.md b/renderer/docs/deployment.md new file mode 100644 index 0000000..a32f060 --- /dev/null +++ b/renderer/docs/deployment.md @@ -0,0 +1,64 @@ +# Deployment + +## Recommendation + +Use a self-hosted/container runtime for v1. Static-only hosts are not enough +because `/api/gif` must render GIF bytes on the server with Remotion. + +Good first targets: + +- Coolify Docker app +- Fly.io +- Railway +- Render +- VPS with Docker +- Google Cloud Run generic container + +Avoid for v1: + +- Cloudflare Workers/Pages +- pure static Vercel/Netlify deploys + +## Runtime + +Required env vars: + +```sh +PORT=3000 +HOST=0.0.0.0 +MAX_CONCURRENT_RENDERS=1 +RENDER_TIMEOUT_MS=15000 +MAX_OPTIONS=32 +MAX_OPTION_CHARS=40 +API_TOKEN=change-me +``` + +Start with 1-2 vCPU and 1-2 GB RAM. Increase only after render benchmarks show +the service is CPU-bound or concurrency-limited. + +Local non-Docker render smoke requires Chrome Headless Shell shared libraries, +including `libnspr4` and `libnss3`. Prefer Docker for consistent verification. +The Docker image runs `pnpm browser:ensure` during build so production requests +do not need to download Chrome Headless Shell on first render. + +`RENDER_TIMEOUT_MS` is a total render timeout. Values below `7000` are raised to +`7000` because Remotion's browser timeout has that minimum. + +Set `API_TOKEN` for every production deployment. The server refuses to start +with `NODE_ENV=production` unless the token is configured. + +## Health + +```sh +curl http://localhost:3000/api/healthz +``` + +## Render Test + +```sh +curl -X POST http://localhost:3000/api/gif \ + -H 'content-type: application/json' \ + -H 'authorization: Bearer change-me' \ + --output wheel.gif \ + --data '{"options":["alice","bob","carol"],"winnerIndex":1}' +``` diff --git a/renderer/docs/miti99bot-integration.md b/renderer/docs/miti99bot-integration.md new file mode 100644 index 0000000..a43a7c8 --- /dev/null +++ b/renderer/docs/miti99bot-integration.md @@ -0,0 +1,39 @@ +# miti99bot Integration + +Future bot integration should keep winner handling explicit. + +## Recommended Flow + +1. Bot parses `/wheelofnamesbeta` options. +2. Bot chooses `winnerIndex` locally. +3. Bot calls `POST /api/gif`. +4. Bot sends response bytes with Telegram `sendAnimation`. +5. Bot caption uses local winner as source of truth. +6. If the service fails, bot falls back to existing local GIF renderer. + +## Request + +Send `Authorization: Bearer ` with the JSON body. + +```json +{ + "options": ["alice", "bob", "carol"], + "winnerIndex": 1, + "durationMs": 6500, + "holdMs": 1200, + "fps": 15, + "size": 512, + "theme": "classic" +} +``` + +## Response + +- Body: `image/gif` +- Headers: + - `X-Wheel-Winner-Index` + - `X-Wheel-Winner` + - `X-Render-Duration-Ms` + +`X-Wheel-Winner` is URL-encoded. Decode it before displaying if service-side +winner selection is used. diff --git a/renderer/docs/render-benchmarks.md b/renderer/docs/render-benchmarks.md new file mode 100644 index 0000000..4db845a --- /dev/null +++ b/renderer/docs/render-benchmarks.md @@ -0,0 +1,14 @@ +# Render Benchmarks + +Fill this after running: + +```sh +pnpm render:fixtures +``` + +| Fixture | Size | FPS | Duration | Bytes | Render Time | Notes | +|---|---:|---:|---:|---:|---:|---| +| smoke | 384 | 12 | 3.5s | 315661 | 13917ms | Docker `pnpm render:smoke` on 2026-07-06 | +| api-smoke | 384 | 12 | 3.5s | 298374 | 4100ms | Docker `pnpm api:smoke`, after startup bundle warm-up | +| vietnamese | 512 | 15 | 7.7s | TBD | TBD | label readability | +| sixteen-options | 512 | 15 | 7.7s | TBD | TBD | dense wheel | diff --git a/renderer/eslint.config.js b/renderer/eslint.config.js new file mode 100644 index 0000000..8303a00 --- /dev/null +++ b/renderer/eslint.config.js @@ -0,0 +1,38 @@ +import js from '@eslint/js'; +import globals from 'globals'; + +export default [ + { + ignores: ['node_modules/**', 'dist/**', 'coverage/**', '.tmp/**', 'fixtures/**'], + }, + js.configs.recommended, + { + files: ['**/*.js', '**/*.jsx'], + languageOptions: { + ecmaVersion: 2024, + sourceType: 'module', + globals: { + ...globals.node, + ...globals.browser, + }, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + rules: { + 'no-unused-vars': ['error', {argsIgnorePattern: '^_'}], + }, + }, + { + files: ['test/**/*.js'], + languageOptions: { + globals: { + ...globals.node, + ...globals.browser, + ...globals.vitest, + }, + }, + }, +]; diff --git a/renderer/package.json b/renderer/package.json new file mode 100644 index 0000000..fcd4518 --- /dev/null +++ b/renderer/package.json @@ -0,0 +1,41 @@ +{ + "name": "wheelofnames", + "version": "0.1.0", + "private": true, + "type": "module", + "engines": { + "node": ">=24 <25" + }, + "scripts": { + "dev": "node --watch src/server.js", + "start": "node src/server.js", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "browser:ensure": "node scripts/ensure-browser.js", + "api:smoke": "node scripts/api-smoke.js", + "render:smoke": "node scripts/render-fixtures.js --smoke", + "render:fixtures": "node scripts/render-fixtures.js" + }, + "dependencies": { + "@remotion/bundler": "4.0.485", + "@remotion/renderer": "4.0.485", + "@remotion/shapes": "4.0.485", + "fastify": "5.10.0", + "react": "19.2.7", + "react-dom": "19.2.7", + "remotion": "4.0.485", + "zod": "4.4.3" + }, + "devDependencies": { + "@eslint/js": "10.0.1", + "@types/node": "26.1.0", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", + "eslint": "10.6.0", + "globals": "17.7.0", + "prettier": "3.9.4", + "typescript": "6.0.3", + "vitest": "4.1.10" + } +} diff --git a/renderer/pnpm-lock.yaml b/renderer/pnpm-lock.yaml new file mode 100644 index 0000000..4704d2c --- /dev/null +++ b/renderer/pnpm-lock.yaml @@ -0,0 +1,3438 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@remotion/bundler': + specifier: 4.0.485 + version: 4.0.485(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@remotion/renderer': + specifier: 4.0.485 + version: 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@remotion/shapes': + specifier: 4.0.485 + version: 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + fastify: + specifier: 5.10.0 + version: 5.10.0 + react: + specifier: 19.2.7 + version: 19.2.7 + react-dom: + specifier: 19.2.7 + version: 19.2.7(react@19.2.7) + remotion: + specifier: 4.0.485 + version: 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + zod: + specifier: 4.4.3 + version: 4.4.3 + devDependencies: + '@eslint/js': + specifier: 10.0.1 + version: 10.0.1(eslint@10.6.0) + '@types/node': + specifier: 26.1.0 + version: 26.1.0 + '@types/react': + specifier: 19.2.17 + version: 19.2.17 + '@types/react-dom': + specifier: 19.2.3 + version: 19.2.3(@types/react@19.2.17) + eslint: + specifier: 10.6.0 + version: 10.6.0 + globals: + specifier: 17.7.0 + version: 17.7.0 + prettier: + specifier: 3.9.4 + version: 3.9.4 + typescript: + specifier: 6.0.3 + version: 6.0.3 + vitest: + specifier: 4.1.10 + version: 4.1.10(@types/node@26.1.0)(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(terser@5.48.0)) + +packages: + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/core@1.11.2': + resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@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@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.1.0': + resolution: {integrity: sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mediabunny/aac-encoder@1.50.3': + resolution: {integrity: sha512-rVPnhJbn+7BUsu09sEl/bee/DkzmAc33oErJ250RPlCqrPndx61DSb2GLvVmmSx6L2LU2WyCnvJIMUrGPJEXPA==} + peerDependencies: + mediabunny: ^1.0.0 + + '@mediabunny/flac-encoder@1.50.3': + resolution: {integrity: sha512-aIEHE3eQ+hG35rRvtNNYO6ufTiGkZpzgOBjcqK8CQuRAMtUY9/DzV12jj52L0nSj2zmBKXQQ6dWBWsusigV/dQ==} + peerDependencies: + mediabunny: ^1.0.0 + + '@mediabunny/mp3-encoder@1.50.3': + resolution: {integrity: sha512-dyjWOhG27+ctlejfjY4vFy0g8wzo0NCRTNQ/GNsCaHxWkduaq0GI2BWKXm6UFCsfnUKgtDWMEu4zOPl9fKpbsA==} + peerDependencies: + mediabunny: ^1.0.0 + + '@module-federation/error-codes@0.22.0': + resolution: {integrity: sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==} + + '@module-federation/runtime-core@0.22.0': + resolution: {integrity: sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==} + + '@module-federation/runtime-tools@0.22.0': + resolution: {integrity: sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==} + + '@module-federation/runtime@0.22.0': + resolution: {integrity: sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==} + + '@module-federation/sdk@0.22.0': + resolution: {integrity: sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==} + + '@module-federation/webpack-bundler-runtime@0.22.0': + resolution: {integrity: sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==} + + '@napi-rs/wasm-runtime@1.0.7': + resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} + + '@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 + + '@oxc-project/types@0.138.0': + resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@remotion/bundler@4.0.485': + resolution: {integrity: sha512-wmcEg6OuIonCp5seQtM8fkFsTHjy5ic9D/mINfsw/2h29mzRdmVVhk4lzxWwXXxkzk9upOjvB1+AdGlKYG8vjw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@remotion/canvas-capture@4.0.485': + resolution: {integrity: sha512-wHpIhwUSRRgKRRolnjJq6GQZzTI0KIkkTKsbwP3myOJmsdumnehYNDvaE5bMVekPxOVU/RsPcLAnuA2pYsteGg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@remotion/compositor-darwin-arm64@4.0.485': + resolution: {integrity: sha512-+bR1BK8JvSg8oxJgbSt5NunR3Gj9k7/2pyxtorGnoqQnWzOQiIaZ5jgDQj9/lQTj3RkIvq58+/BnIRcc0xOExA==} + cpu: [arm64] + os: [darwin] + + '@remotion/compositor-darwin-x64@4.0.485': + resolution: {integrity: sha512-O5BjaS9IagBTagt+s+15VDxtyrzMu6WZHsPpD88JXBsBBd8EeXkCl05fN/pXkkzk9VXzU8PiQfFMUNnT/DKuxQ==} + cpu: [x64] + os: [darwin] + + '@remotion/compositor-linux-arm64-gnu@4.0.485': + resolution: {integrity: sha512-7Badhpi7xfK6byvcifyueL14mEs8lkibtT1QalDrS3mBZS1vb8HtmIKVX/SfTjb2/DPIxM/MnIqvZVAptr7FVg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@remotion/compositor-linux-arm64-musl@4.0.485': + resolution: {integrity: sha512-UCQBRhmB7Yn4r+2pazDoNO4e1NywTdI+4IEpakalzB0l38HVPCdT1I+K9RZi/ZytqSlPocCo/OTFXy9WgXsfMg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@remotion/compositor-linux-x64-gnu@4.0.485': + resolution: {integrity: sha512-0iIIxWoxceEao+T8SVyZWBJDMpaXWND7Ygj3ZLdSbQJpT7st47OdoF+6tHY6kvfspjgBuU8tvrTURXirnRluAA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@remotion/compositor-linux-x64-musl@4.0.485': + resolution: {integrity: sha512-e5lvQLZHfiolVgTfI5M1TAg0BlU6/JUdrwUryQ7M7bIFKaHXnlZSiE2phTthQGjlaMNIY38y57GVNWQU0w8XyQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@remotion/compositor-win32-x64-msvc@4.0.485': + resolution: {integrity: sha512-0Hm4SXKHcz1ylWCbaZQEn3ZDGcMlAnYPdgyDlyvKWkPJ5BG5jxrOzCrocZqzzsdmzxzeImNC+2+SeeSomFVuzw==} + cpu: [x64] + os: [win32] + + '@remotion/licensing@4.0.485': + resolution: {integrity: sha512-PZcPc0H839VtLmqa3Eco8QrgnHIHtatjqgDNj6smVb17eKijh1x9EGCYAOhjDZXt3MtbfBK/aX5bDMdZMh07qw==} + + '@remotion/media-parser@4.0.485': + resolution: {integrity: sha512-BN1ALFyYRt0NOGhzvhbgLfPOQWgijFh5Vp6I87R95UIXBPvl1gVaKCv2nKdGGPAXj1EYCjhNhS5N1EU4J656ZQ==} + + '@remotion/media-utils@4.0.485': + resolution: {integrity: sha512-X46Its9H1I7Bb/afQi86TwzvJOjICIWz6JSS7Tw2BkZqNLU/Y3c7C7IfQug1CdQT8K31ba5zPr2Yogj2gtlA/g==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@remotion/paths@4.0.485': + resolution: {integrity: sha512-injjAmdMKyWZUKDcfR7gienxh5i3D612lGpi4CGRbWfxk+bXK3R1oyZKkT0SxR1drKp/y98mlZMoBFM5JHoqIA==} + + '@remotion/player@4.0.485': + resolution: {integrity: sha512-kgc/Qfj4/XI+gkO3ZFglefDdaXWjbk3JRys2aZKekNetIX31mXcnXjzUadDqB3fMJ1H+qNQ9oen5MWZZDN3I1g==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@remotion/renderer@4.0.485': + resolution: {integrity: sha512-7fcmE6JrXQaQzEyR9BPy2xdorRbIicJNB/M8RV83DLwYbVHQq9RDRPGzkpHJGGDzGBWtJb441CIYHosK7oH3kA==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@remotion/shapes@4.0.485': + resolution: {integrity: sha512-0x6B15PD/LZOfRCNEVn5X/DjOQdFUOpc7i8KnGGCZlWhm5XkeQJZ9SV/aOTfF8egusl7/SaOj7QZVeZN87rUQw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@remotion/streaming@4.0.485': + resolution: {integrity: sha512-UjEfQIyTwHZFXPze2RaTdFSP1cJPhwhr65PI5EocQNKcqWU4osu1FQrMsW2QKoBwcu8G+Gnh+oFdHCUwLDE4Pw==} + + '@remotion/studio-shared@4.0.485': + resolution: {integrity: sha512-Rz0m9yyZOwcnpvFE0+xtKeT2YutSpaxYXm2s4UCCzLLq7hyxYnzVxICWE2USkQ1sWzx3P7Zfr9x4HBN2JxjKVA==} + + '@remotion/studio@4.0.485': + resolution: {integrity: sha512-g5eZUIgl6qPXy0JAE03gVQ30RQWN75Q2ofkXU2IifWdKNp9torB3l9qz6bXZoAyXPKDZTlXCXiKXsnuTzAzhEA==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@remotion/timeline-utils@4.0.485': + resolution: {integrity: sha512-63dh8GIIETy210qTaHb1ejgqFh/uRkpG7oZPg12HmEk0DsmN+U4A4+s7hSCnwW6JWYJZxWipBVb68V3W4M/4cg==} + + '@remotion/web-renderer@4.0.485': + resolution: {integrity: sha512-u5bBCS4OC4xVhI7BRlKNnt2JbNHNEOrun799Uz30Uj38DTI2XLl5YxVPiDUBdyNuJGaWnVTBPkEWklK3ygVhhA==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@remotion/zod-types@4.0.485': + resolution: {integrity: sha512-twIeAetwWbrlmADJhf6xtnJR6A6gFrH7SRhVGu8FLXwaqvO7LBs5zlRESUsvZ1qXEhGGqZn1lOv6jlEbt0OUFA==} + + '@rolldown/binding-android-arm64@1.1.4': + resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.4': + resolution: {integrity: sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.4': + resolution: {integrity: sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.4': + resolution: {integrity: sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': + resolution: {integrity: sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.4': + resolution: {integrity: sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.4': + resolution: {integrity: sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.4': + resolution: {integrity: sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.4': + resolution: {integrity: sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.4': + resolution: {integrity: sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.4': + resolution: {integrity: sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.4': + resolution: {integrity: sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.4': + resolution: {integrity: sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.4': + resolution: {integrity: sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.4': + resolution: {integrity: sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rspack/binding-darwin-arm64@1.7.11': + resolution: {integrity: sha512-oduECiZVqbO5zlVw+q7Vy65sJFth99fWPTyucwvLJJtJkPL5n17Uiql2cYP6Ijn0pkqtf1SXgK8WjiKLG5bIig==} + cpu: [arm64] + os: [darwin] + + '@rspack/binding-darwin-x64@1.7.11': + resolution: {integrity: sha512-a1+TtTE9ap6RalgFi7FGIgkJP6O4Vy6ctv+9WGJy53E4kuqHR0RygzaiVxCI/GMc/vBT9vY23hyrpWb3d1vtXA==} + cpu: [x64] + os: [darwin] + + '@rspack/binding-linux-arm64-gnu@1.7.11': + resolution: {integrity: sha512-P0QrGRPbTWu6RKWfN0bDtbnEps3rXH0MWIMreZABoUrVmNQKtXR6e73J3ub6a+di5s2+K0M2LJ9Bh2/H4UsDUA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-arm64-musl@1.7.11': + resolution: {integrity: sha512-6ky7R43VMjWwmx3Yx7Jl7faLBBMAgMDt+/bN35RgwjiPgsIByz65EwytUVuW9rikB43BGHvA/eqlnjLrUzNBqw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rspack/binding-linux-x64-gnu@1.7.11': + resolution: {integrity: sha512-cuOJMfCOvb2Wgsry5enXJ3iT1FGUjdPqtGUBVupQlEG4ntSYsQ2PtF4wIDVasR3wdxC5nQbipOrDiN/u6fYsdQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rspack/binding-linux-x64-musl@1.7.11': + resolution: {integrity: sha512-CoK37hva4AmHGh3VCsQXmGr40L36m1/AdnN5LEjUX6kx5rEH7/1nEBN6Ii72pejqDVvk9anEROmPDiPw10tpFg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rspack/binding-wasm32-wasi@1.7.11': + resolution: {integrity: sha512-OtrmnPUVJMxjNa3eDMfHyPdtlLRmmp/aIm0fQHlAOATbZvlGm12q7rhPW5BXTu1yh+1rQ1/uqvz+SzKEZXuJaQ==} + cpu: [wasm32] + + '@rspack/binding-win32-arm64-msvc@1.7.11': + resolution: {integrity: sha512-lObFW6e5lCWNgTBNwT//yiEDbsxm9QG4BYUojqeXxothuzJ/L6ibXz6+gLMvbOvLGV3nKgkXmx8GvT9WDKR0mA==} + cpu: [arm64] + os: [win32] + + '@rspack/binding-win32-ia32-msvc@1.7.11': + resolution: {integrity: sha512-0pYGnZd8PPqNR68zQ8skamqNAXEA1sUfXuAdYcknIIRq2wsbiwFzIc0Pov1cIfHYab37G7sSIPBiOUdOWF5Ivw==} + cpu: [ia32] + os: [win32] + + '@rspack/binding-win32-x64-msvc@1.7.11': + resolution: {integrity: sha512-EeQXayoQk/uBkI3pdoXfQBXNIUrADq56L3s/DFyM2pJeUDrWmhfIw2UFIGkYPTMSCo8F2JcdcGM32FGJrSnU0Q==} + cpu: [x64] + os: [win32] + + '@rspack/binding@1.7.11': + resolution: {integrity: sha512-2MGdy2s2HimsDT444Bp5XnALzNRxuBNc7y0JzyuqKbHBywd4x2NeXyhWXXoxufaCFu5PBc9Qq9jyfjW2Aeh06Q==} + + '@rspack/core@1.7.11': + resolution: {integrity: sha512-rsD9b+Khmot5DwCMiB3cqTQo53ioPG3M/A7BySu8+0+RS7GCxKm+Z+mtsjtG/vsu4Tn2tcqCdZtA3pgLoJB+ew==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@rspack/lite-tapable@1.1.0': + resolution: {integrity: sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==} + + '@rspack/plugin-react-refresh@1.6.1': + resolution: {integrity: sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw==} + peerDependencies: + react-refresh: '>=0.10.0 <1.0.0' + webpack-hot-middleware: 2.x + peerDependenciesMeta: + webpack-hot-middleware: + optional: true + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@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/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/dom-mediacapture-transform@0.1.11': + resolution: {integrity: sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==} + + '@types/dom-webcodecs@0.1.13': + resolution: {integrity: sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@26.1.0': + resolution: {integrity: sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + 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.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.42: + resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.4: + resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} + 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==} + + caniuse-lite@1.0.30001800: + resolution: {integrity: sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-loader@7.1.4: + resolution: {integrity: sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0 + webpack: ^5.27.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.387: + resolution: {integrity: sha512-TaxwufTFDufvPEoXdhwVrA3UdFWBeWGkYoJ1K8ldF1xe6gKfth6iRNS5lTQ5JPNOHdGQm8PT1QYKUqFLCiUefQ==} + + enhanced-resolve@5.24.1: + resolution: {integrity: sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==} + engines: {node: '>=10.13.0'} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + es-module-lexer@2.3.0: + resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.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@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.6.0: + resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + 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'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-json-stringify@7.0.0: + resolution: {integrity: sha512-YV53BAbR3Qwq37wfD1oZ97YJ0nYj6CwfzKXQ38ock9XxI2EnLOdl5psKms6Evook6ACckytZJOaKY0ThmIi1uw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + + fastify@5.10.0: + resolution: {integrity: sha512-A9L0ziuWGQHgEEVgF3davQ9vbD93IuX+lo2IsxapQmu5b/Y/ynn9m9K5JHt9dvyJXOFc5iN0Zk5GHEOqnzhWjg==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-my-way@9.6.0: + resolution: {integrity: sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==} + engines: {node: '>=20'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fs-monkey@1.0.3: + resolution: {integrity: sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + globals@17.7.0: + resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + ipaddr.js@2.4.0: + resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} + engines: {node: '>= 10'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + loader-runner@4.3.2: + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} + engines: {node: '>=6.11.5'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mediabunny@1.50.3: + resolution: {integrity: sha512-lw6NjC6++/p6MzHC3u7RbkpM0dnukEQcAAXzZwXlmmx9y8n6132DXUMer2WaNn8RPuGgUpnemi/QIVNpEnONhQ==} + + memfs@3.4.3: + resolution: {integrity: sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==} + engines: {node: '>= 4.0.0'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + 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 + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-releases@2.0.50: + resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} + engines: {node: '>=18'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.2.0: + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.1: + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-selector-parser@7.1.4: + resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.9.4: + resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==} + engines: {node: '>=14'} + hasBin: true + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + + remotion@4.0.485: + resolution: {integrity: sha512-gZyjdafaXgRurq3rZCSKfEx9S2Aef45b/FvEU90PTL9gsJpzVK+yz0Jhi/jNefgadowFqdPLLdZyGBMjY2jk3g==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rolldown@1.1.4: + resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + safe-regex2@5.1.1: + resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} + hasBin: true + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semver@7.5.3: + resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} + engines: {node: '>=10'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + style-loader@4.0.0: + resolution: {integrity: sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + webpack: ^5.27.0 + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.6.1: + resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + engines: {node: '>=10'} + hasBin: true + + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + 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'} + + toad-cache@3.7.4: + resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==} + engines: {node: '>=20'} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@8.1.3: + resolution: {integrity: sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==} + 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.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + 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.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + 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 + + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} + engines: {node: '>=10.13.0'} + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + webpack-sources@3.5.0: + resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} + engines: {node: '>=10.13.0'} + + webpack@5.105.0: + resolution: {integrity: sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + 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'} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.11.2': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0)': + dependencies: + eslint: 10.6.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.6.0)': + optionalDependencies: + eslint: 10.6.0 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 3.1.3 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.1.0': + dependencies: + fast-json-stringify: 7.0.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.4.0 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mediabunny/aac-encoder@1.50.3(mediabunny@1.50.3)': + dependencies: + mediabunny: 1.50.3 + + '@mediabunny/flac-encoder@1.50.3(mediabunny@1.50.3)': + dependencies: + mediabunny: 1.50.3 + + '@mediabunny/mp3-encoder@1.50.3(mediabunny@1.50.3)': + dependencies: + mediabunny: 1.50.3 + + '@module-federation/error-codes@0.22.0': {} + + '@module-federation/runtime-core@0.22.0': + dependencies: + '@module-federation/error-codes': 0.22.0 + '@module-federation/sdk': 0.22.0 + + '@module-federation/runtime-tools@0.22.0': + dependencies: + '@module-federation/runtime': 0.22.0 + '@module-federation/webpack-bundler-runtime': 0.22.0 + + '@module-federation/runtime@0.22.0': + dependencies: + '@module-federation/error-codes': 0.22.0 + '@module-federation/runtime-core': 0.22.0 + '@module-federation/sdk': 0.22.0 + + '@module-federation/sdk@0.22.0': {} + + '@module-federation/webpack-bundler-runtime@0.22.0': + dependencies: + '@module-federation/runtime': 0.22.0 + '@module-federation/sdk': 0.22.0 + + '@napi-rs/wasm-runtime@1.0.7': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.3 + 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 + + '@oxc-project/types@0.138.0': {} + + '@pinojs/redact@0.4.0': {} + + '@remotion/bundler@4.0.485(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@remotion/media-parser': 4.0.485 + '@remotion/studio': 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@remotion/studio-shared': 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@remotion/timeline-utils': 4.0.485 + '@rspack/core': 1.7.11 + '@rspack/plugin-react-refresh': 1.6.1(react-refresh@0.18.0) + css-loader: 7.1.4(@rspack/core@1.7.11)(webpack@5.105.0(esbuild@0.28.1)(postcss@8.5.16)) + esbuild: 0.28.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-refresh: 0.18.0 + remotion: 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + style-loader: 4.0.0(webpack@5.105.0(esbuild@0.28.1)(postcss@8.5.16)) + webpack: 5.105.0(esbuild@0.28.1)(postcss@8.5.16) + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/helpers' + - '@swc/html' + - bufferutil + - clean-css + - cssnano + - csso + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + - utf-8-validate + - webpack-cli + - webpack-hot-middleware + + '@remotion/canvas-capture@4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + mediabunny: 1.50.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + remotion: 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + + '@remotion/compositor-darwin-arm64@4.0.485': + optional: true + + '@remotion/compositor-darwin-x64@4.0.485': + optional: true + + '@remotion/compositor-linux-arm64-gnu@4.0.485': + optional: true + + '@remotion/compositor-linux-arm64-musl@4.0.485': + optional: true + + '@remotion/compositor-linux-x64-gnu@4.0.485': + optional: true + + '@remotion/compositor-linux-x64-musl@4.0.485': + optional: true + + '@remotion/compositor-win32-x64-msvc@4.0.485': + optional: true + + '@remotion/licensing@4.0.485': {} + + '@remotion/media-parser@4.0.485': {} + + '@remotion/media-utils@4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + mediabunny: 1.50.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + remotion: 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + + '@remotion/paths@4.0.485': {} + + '@remotion/player@4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + remotion: 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + + '@remotion/renderer@4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@remotion/licensing': 4.0.485 + '@remotion/streaming': 4.0.485 + execa: 5.1.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + remotion: 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + source-map: 0.8.0-beta.0 + ws: 8.21.0 + optionalDependencies: + '@remotion/compositor-darwin-arm64': 4.0.485 + '@remotion/compositor-darwin-x64': 4.0.485 + '@remotion/compositor-linux-arm64-gnu': 4.0.485 + '@remotion/compositor-linux-arm64-musl': 4.0.485 + '@remotion/compositor-linux-x64-gnu': 4.0.485 + '@remotion/compositor-linux-x64-musl': 4.0.485 + '@remotion/compositor-win32-x64-msvc': 4.0.485 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@remotion/shapes@4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@remotion/paths': 4.0.485 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + remotion: 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + + '@remotion/streaming@4.0.485': {} + + '@remotion/studio-shared@4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + remotion: 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + transitivePeerDependencies: + - react + - react-dom + + '@remotion/studio@4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@remotion/canvas-capture': 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@remotion/media-utils': 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@remotion/player': 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@remotion/renderer': 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@remotion/studio-shared': 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@remotion/timeline-utils': 4.0.485 + '@remotion/web-renderer': 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@remotion/zod-types': 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + mediabunny: 1.50.3 + memfs: 3.4.3 + open: 8.4.2 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + remotion: 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + semver: 7.5.3 + zod: 4.3.6 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@remotion/timeline-utils@4.0.485': + dependencies: + mediabunny: 1.50.3 + + '@remotion/web-renderer@4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@mediabunny/aac-encoder': 1.50.3(mediabunny@1.50.3) + '@mediabunny/flac-encoder': 1.50.3(mediabunny@1.50.3) + '@mediabunny/mp3-encoder': 1.50.3(mediabunny@1.50.3) + '@remotion/licensing': 4.0.485 + mediabunny: 1.50.3 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + remotion: 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + + '@remotion/zod-types@4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + remotion: 4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + transitivePeerDependencies: + - react + - react-dom + + '@rolldown/binding-android-arm64@1.1.4': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.4': + optional: true + + '@rolldown/binding-darwin-x64@1.1.4': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.4': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.4': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.4': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.4': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.4': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.4': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.4': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.4': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.4': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.4': + 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.4': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.4': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rspack/binding-darwin-arm64@1.7.11': + optional: true + + '@rspack/binding-darwin-x64@1.7.11': + optional: true + + '@rspack/binding-linux-arm64-gnu@1.7.11': + optional: true + + '@rspack/binding-linux-arm64-musl@1.7.11': + optional: true + + '@rspack/binding-linux-x64-gnu@1.7.11': + optional: true + + '@rspack/binding-linux-x64-musl@1.7.11': + optional: true + + '@rspack/binding-wasm32-wasi@1.7.11': + dependencies: + '@napi-rs/wasm-runtime': 1.0.7 + optional: true + + '@rspack/binding-win32-arm64-msvc@1.7.11': + optional: true + + '@rspack/binding-win32-ia32-msvc@1.7.11': + optional: true + + '@rspack/binding-win32-x64-msvc@1.7.11': + optional: true + + '@rspack/binding@1.7.11': + optionalDependencies: + '@rspack/binding-darwin-arm64': 1.7.11 + '@rspack/binding-darwin-x64': 1.7.11 + '@rspack/binding-linux-arm64-gnu': 1.7.11 + '@rspack/binding-linux-arm64-musl': 1.7.11 + '@rspack/binding-linux-x64-gnu': 1.7.11 + '@rspack/binding-linux-x64-musl': 1.7.11 + '@rspack/binding-wasm32-wasi': 1.7.11 + '@rspack/binding-win32-arm64-msvc': 1.7.11 + '@rspack/binding-win32-ia32-msvc': 1.7.11 + '@rspack/binding-win32-x64-msvc': 1.7.11 + + '@rspack/core@1.7.11': + dependencies: + '@module-federation/runtime-tools': 0.22.0 + '@rspack/binding': 1.7.11 + '@rspack/lite-tapable': 1.1.0 + + '@rspack/lite-tapable@1.1.0': {} + + '@rspack/plugin-react-refresh@1.6.1(react-refresh@0.18.0)': + dependencies: + error-stack-parser: 2.1.4 + html-entities: 2.6.0 + react-refresh: 0.18.0 + + '@standard-schema/spec@1.1.0': {} + + '@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/deep-eql@4.0.2': {} + + '@types/dom-mediacapture-transform@0.1.11': + dependencies: + '@types/dom-webcodecs': 0.1.13 + + '@types/dom-webcodecs@0.1.13': {} + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.9 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@26.1.0': + dependencies: + undici-types: 8.3.0 + + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.10(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(terser@5.48.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(terser@5.48.0) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + abstract-logging@2.0.1: {} + + acorn-import-phases@1.0.4(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + ajv-formats@2.1.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + assertion-error@2.0.1: {} + + atomic-sleep@1.0.0: {} + + avvio@9.2.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.20.1 + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.42: {} + + brace-expansion@5.0.7: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.4: + dependencies: + baseline-browser-mapping: 2.10.42 + caniuse-lite: 1.0.30001800 + electron-to-chromium: 1.5.387 + node-releases: 2.0.50 + update-browserslist-db: 1.2.3(browserslist@4.28.4) + + buffer-from@1.1.2: {} + + caniuse-lite@1.0.30001800: {} + + chai@6.2.2: {} + + chrome-trace-event@1.0.4: {} + + commander@2.20.3: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-loader@7.1.4(@rspack/core@1.7.11)(webpack@5.105.0(esbuild@0.28.1)(postcss@8.5.16)): + dependencies: + icss-utils: 5.1.0(postcss@8.5.16) + postcss: 8.5.16 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.16) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.16) + postcss-modules-scope: 3.2.1(postcss@8.5.16) + postcss-modules-values: 4.0.0(postcss@8.5.16) + postcss-value-parser: 4.2.0 + semver: 7.8.5 + optionalDependencies: + '@rspack/core': 1.7.11 + webpack: 5.105.0(esbuild@0.28.1)(postcss@8.5.16) + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-lazy-prop@2.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.387: {} + + enhanced-resolve@5.24.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + + es-module-lexer@2.3.0: {} + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.6.0: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.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 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + events@3.3.0: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + expect-type@1.4.0: {} + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-json-stringify@7.0.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 3.1.3 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + + fast-levenshtein@2.0.6: {} + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-uri@3.1.3: {} + + fastify@5.10.0: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.1.0 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.2.0 + fast-json-stringify: 7.0.0 + find-my-way: 9.6.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.8.5 + toad-cache: 3.7.4 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-my-way@9.6.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fs-monkey@1.0.3: {} + + fsevents@2.3.3: + optional: true + + get-stream@6.0.1: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + globals@17.7.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + html-entities@2.6.0: {} + + human-signals@2.1.0: {} + + icss-utils@5.1.0(postcss@8.5.16): + dependencies: + postcss: 8.5.16 + + ignore@5.3.2: {} + + imurmurhash@0.1.4: {} + + ipaddr.js@2.4.0: {} + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-stream@2.0.1: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isexe@2.0.0: {} + + jest-worker@27.5.1: + dependencies: + '@types/node': 26.1.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + loader-runner@4.3.2: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.sortby@4.7.0: {} + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mediabunny@1.50.3: + dependencies: + '@types/dom-mediacapture-transform': 0.1.11 + '@types/dom-webcodecs': 0.1.13 + + memfs@3.4.3: + dependencies: + fs-monkey: 1.0.3 + + merge-stream@2.0.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@2.1.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.7 + + ms@2.1.3: {} + + nanoid@3.3.15: {} + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + node-releases@2.0.50: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + obug@2.1.3: {} + + on-exit-leak-free@2.1.2: {} + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + + postcss-modules-extract-imports@3.1.0(postcss@8.5.16): + dependencies: + postcss: 8.5.16 + + postcss-modules-local-by-default@4.2.0(postcss@8.5.16): + dependencies: + icss-utils: 5.1.0(postcss@8.5.16) + postcss: 8.5.16 + postcss-selector-parser: 7.1.4 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.1(postcss@8.5.16): + dependencies: + postcss: 8.5.16 + postcss-selector-parser: 7.1.4 + + postcss-modules-values@4.0.0(postcss@8.5.16): + dependencies: + icss-utils: 5.1.0(postcss@8.5.16) + postcss: 8.5.16 + + postcss-selector-parser@7.1.4: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.16: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.9.4: {} + + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + + punycode@2.3.1: {} + + quick-format-unescaped@4.0.4: {} + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react-refresh@0.18.0: {} + + react@19.2.7: {} + + real-require@0.2.0: {} + + real-require@1.0.0: {} + + remotion@4.0.485(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + require-from-string@2.0.2: {} + + ret@0.5.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rolldown@1.1.4: + dependencies: + '@oxc-project/types': 0.138.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.4 + '@rolldown/binding-darwin-arm64': 1.1.4 + '@rolldown/binding-darwin-x64': 1.1.4 + '@rolldown/binding-freebsd-x64': 1.1.4 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.4 + '@rolldown/binding-linux-arm64-gnu': 1.1.4 + '@rolldown/binding-linux-arm64-musl': 1.1.4 + '@rolldown/binding-linux-ppc64-gnu': 1.1.4 + '@rolldown/binding-linux-s390x-gnu': 1.1.4 + '@rolldown/binding-linux-x64-gnu': 1.1.4 + '@rolldown/binding-linux-x64-musl': 1.1.4 + '@rolldown/binding-openharmony-arm64': 1.1.4 + '@rolldown/binding-wasm32-wasi': 1.1.4 + '@rolldown/binding-win32-arm64-msvc': 1.1.4 + '@rolldown/binding-win32-x64-msvc': 1.1.4 + + safe-regex2@5.1.1: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + + scheduler@0.27.0: {} + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + ajv-keywords: 5.1.0(ajv@8.20.0) + + secure-json-parse@4.1.0: {} + + semver@7.5.3: + dependencies: + lru-cache: 6.0.0 + + semver@7.8.5: {} + + set-cookie-parser@2.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + + split2@4.2.0: {} + + stackback@0.0.2: {} + + stackframe@1.3.4: {} + + std-env@4.1.0: {} + + strip-final-newline@2.0.0: {} + + style-loader@4.0.0(webpack@5.105.0(esbuild@0.28.1)(postcss@8.5.16)): + dependencies: + webpack: 5.105.0(esbuild@0.28.1)(postcss@8.5.16) + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + tapable@2.3.3: {} + + terser-webpack-plugin@5.6.1(esbuild@0.28.1)(postcss@8.5.16)(webpack@5.105.0(esbuild@0.28.1)(postcss@8.5.16)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.48.0 + webpack: 5.105.0(esbuild@0.28.1)(postcss@8.5.16) + optionalDependencies: + esbuild: 0.28.1 + postcss: 8.5.16 + + terser@5.48.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.17.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyrainbow@3.1.0: {} + + toad-cache@3.7.4: {} + + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript@6.0.3: {} + + undici-types@8.3.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.4): + dependencies: + browserslist: 4.28.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(terser@5.48.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.16 + rolldown: 1.1.4 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.1.0 + esbuild: 0.28.1 + fsevents: 2.3.3 + terser: 5.48.0 + + vitest@4.1.10(@types/node@26.1.0)(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(terser@5.48.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(terser@5.48.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.3(@types/node@26.1.0)(esbuild@0.28.1)(terser@5.48.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.1.0 + transitivePeerDependencies: + - msw + + watchpack@2.5.2: + dependencies: + graceful-fs: 4.2.11 + + webidl-conversions@4.0.2: {} + + webpack-sources@3.5.0: {} + + webpack@5.105.0(esbuild@0.28.1)(postcss@8.5.16): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.17.0 + acorn-import-phases: 1.0.4(acorn@8.17.0) + browserslist: 4.28.4 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.24.1 + es-module-lexer: 2.3.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.2 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + terser-webpack-plugin: 5.6.1(esbuild@0.28.1)(postcss@8.5.16)(webpack@5.105.0(esbuild@0.28.1)(postcss@8.5.16)) + watchpack: 2.5.2 + webpack-sources: 3.5.0 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + ws@8.21.0: {} + + yallist@4.0.0: {} + + yocto-queue@0.1.0: {} + + zod@4.3.6: {} + + zod@4.4.3: {} diff --git a/renderer/pnpm-workspace.yaml b/renderer/pnpm-workspace.yaml new file mode 100644 index 0000000..a5dffc9 --- /dev/null +++ b/renderer/pnpm-workspace.yaml @@ -0,0 +1,34 @@ +allowBuilds: + esbuild: true +minimumReleaseAgeExclude: + - '@remotion/bundler@4.0.485' + - '@remotion/canvas-capture@4.0.485' + - '@remotion/compositor-darwin-arm64@4.0.485' + - '@remotion/compositor-darwin-x64@4.0.485' + - '@remotion/compositor-linux-arm64-gnu@4.0.485' + - '@remotion/compositor-linux-arm64-musl@4.0.485' + - '@remotion/compositor-linux-x64-gnu@4.0.485' + - '@remotion/compositor-linux-x64-musl@4.0.485' + - '@remotion/compositor-win32-x64-msvc@4.0.485' + - '@remotion/licensing@4.0.485' + - '@remotion/media-parser@4.0.485' + - '@remotion/media-utils@4.0.485' + - '@remotion/paths@4.0.485' + - '@remotion/player@4.0.485' + - '@remotion/renderer@4.0.485' + - '@remotion/shapes@4.0.485' + - '@remotion/streaming@4.0.485' + - '@remotion/studio-shared@4.0.485' + - '@remotion/studio@4.0.485' + - '@remotion/timeline-utils@4.0.485' + - '@remotion/web-renderer@4.0.485' + - '@remotion/zod-types@4.0.485' + - '@vitest/expect@4.1.10' + - '@vitest/mocker@4.1.10' + - '@vitest/pretty-format@4.1.10' + - '@vitest/runner@4.1.10' + - '@vitest/snapshot@4.1.10' + - '@vitest/spy@4.1.10' + - '@vitest/utils@4.1.10' + - remotion@4.0.485 + - vitest@4.1.10 diff --git a/renderer/scripts/api-smoke.js b/renderer/scripts/api-smoke.js new file mode 100644 index 0000000..43d49e8 --- /dev/null +++ b/renderer/scripts/api-smoke.js @@ -0,0 +1,50 @@ +import {buildServer} from '../src/server.js'; + +const config = { + apiToken: 'smoke-token', + host: '127.0.0.1', + maxConcurrentRenders: 1, + maxOptionChars: 40, + maxOptions: 32, + port: 0, + requiresApiToken: true, + renderTimeoutMs: 30000, +}; + +const app = await buildServer({config, warmRemotionBundle: true}); + +try { + const response = await app.inject({ + method: 'POST', + url: '/api/gif', + headers: { + authorization: 'Bearer smoke-token', + }, + payload: { + durationMs: 3000, + fps: 12, + holdMs: 500, + options: ['alice', 'bob', 'carol', 'dave'], + size: 384, + theme: 'classic', + winnerIndex: 2, + }, + }); + + if (response.statusCode !== 200) { + throw new Error(`Expected 200, got ${response.statusCode}: ${response.body}`); + } + + const magic = response.rawPayload.subarray(0, 6).toString(); + if (magic !== 'GIF89a') { + throw new Error(`Expected GIF89a header, got ${magic}`); + } + + if (response.headers['x-wheel-winner-index'] !== '2') { + throw new Error(`Expected winner index 2, got ${response.headers['x-wheel-winner-index']}`); + } + + console.log(`api-smoke: ${response.rawPayload.byteLength} bytes`); +} finally { + await app.close(); +} diff --git a/renderer/scripts/ensure-browser.js b/renderer/scripts/ensure-browser.js new file mode 100644 index 0000000..09cb5a2 --- /dev/null +++ b/renderer/scripts/ensure-browser.js @@ -0,0 +1,9 @@ +import {ensureBrowser} from '@remotion/renderer'; + +const status = await ensureBrowser({logLevel: 'warn'}); + +if (status.type === 'no-browser' || status.type === 'version-mismatch') { + throw new Error(`Unable to prepare Remotion browser, status: ${status.type}`); +} + +console.log(`browser: ${status.type}`); diff --git a/renderer/scripts/render-fixtures.js b/renderer/scripts/render-fixtures.js new file mode 100644 index 0000000..2013ade --- /dev/null +++ b/renderer/scripts/render-fixtures.js @@ -0,0 +1,55 @@ +import {mkdir, writeFile} from 'node:fs/promises'; +import path from 'node:path'; +import {renderWheelGif} from '../src/render/render-gif.js'; + +const smoke = process.argv.includes('--smoke'); +const fixturesDir = path.join(process.cwd(), 'fixtures'); + +/** @type {{name: string, request: import('../src/schemas/wheel-request.js').WheelRenderRequest}[]} */ +const fixtures = [ + { + name: 'smoke', + request: { + options: ['alpha', 'beta', 'gamma', 'delta'], + winnerIndex: 1, + durationMs: smoke ? 3000 : 6500, + holdMs: smoke ? 500 : 1200, + fps: 12, + size: smoke ? 384 : 512, + theme: 'classic', + }, + }, + { + name: 'vietnamese', + request: { + options: ['khong dau', 'co dau', 'banh mi', 'ca phe sua da', 'pho bo', 'goi cuon', 'ha noi', 'sai gon'], + winnerIndex: 3, + durationMs: 6500, + holdMs: 1200, + fps: 15, + size: 512, + theme: 'festival', + }, + }, + { + name: 'sixteen-options', + request: { + options: Array.from({length: 16}, (_, index) => `option ${index + 1}`), + winnerIndex: 9, + durationMs: 6500, + holdMs: 1200, + fps: 15, + size: 512, + theme: 'classic', + }, + }, +]; + +await mkdir(fixturesDir, {recursive: true}); + +for (const fixture of smoke ? fixtures.slice(0, 1) : fixtures) { + const result = await renderWheelGif(fixture.request, {timeoutInMilliseconds: 30000}); + const output = path.join(fixturesDir, `${fixture.name}.gif`); + await writeFile(output, result.buffer); + console.log(`${fixture.name}: ${output} ${result.byteLength} bytes ${result.durationMs}ms`); +} diff --git a/renderer/src/config.js b/renderer/src/config.js new file mode 100644 index 0000000..50890a3 --- /dev/null +++ b/renderer/src/config.js @@ -0,0 +1,54 @@ +export const minRenderTimeoutMs = 7000; + +/** + * @typedef {object} AppConfig + * @property {string} host + * @property {number} port + * @property {number} maxConcurrentRenders + * @property {number} renderTimeoutMs + * @property {number} maxOptions + * @property {number} maxOptionChars + * @property {string | undefined} apiToken + * @property {boolean} requiresApiToken + */ + +/** + * @param {string | undefined} value + * @param {number} fallback + * @returns {number} + */ +const parsePositiveInt = (value, fallback) => { + if (!value) { + return fallback; + } + + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +}; + +/** + * @param {NodeJS.ProcessEnv} [env] + * @returns {AppConfig} + */ +export const loadConfig = (env = process.env) => { + const apiToken = env.API_TOKEN || undefined; + const requiresApiToken = env.NODE_ENV === 'production' || env.REQUIRE_API_TOKEN === 'true'; + + if (requiresApiToken && !apiToken) { + throw new Error('API_TOKEN is required when NODE_ENV=production or REQUIRE_API_TOKEN=true'); + } + + return { + host: env.HOST || '0.0.0.0', + port: parsePositiveInt(env.PORT, 3000), + maxConcurrentRenders: parsePositiveInt(env.MAX_CONCURRENT_RENDERS, 1), + renderTimeoutMs: Math.max( + minRenderTimeoutMs, + parsePositiveInt(env.RENDER_TIMEOUT_MS, 15000), + ), + maxOptions: parsePositiveInt(env.MAX_OPTIONS, 32), + maxOptionChars: parsePositiveInt(env.MAX_OPTION_CHARS, 40), + apiToken, + requiresApiToken, + }; +}; diff --git a/renderer/src/lib/render-errors.js b/renderer/src/lib/render-errors.js new file mode 100644 index 0000000..b475528 --- /dev/null +++ b/renderer/src/lib/render-errors.js @@ -0,0 +1,17 @@ +export class RenderTimeoutError extends Error { + /** + * @param {number} timeoutMs + * @param {ErrorOptions} [options] + */ + constructor(timeoutMs, options) { + super(`GIF render exceeded ${timeoutMs}ms`, options); + this.name = 'RenderTimeoutError'; + this.timeoutMs = timeoutMs; + } +} + +/** + * @param {unknown} error + * @returns {error is RenderTimeoutError} + */ +export const isRenderTimeoutError = (error) => error instanceof RenderTimeoutError; diff --git a/renderer/src/lib/render-semaphore.js b/renderer/src/lib/render-semaphore.js new file mode 100644 index 0000000..ea4cc87 --- /dev/null +++ b/renderer/src/lib/render-semaphore.js @@ -0,0 +1,31 @@ +/** + * @param {number} max + */ +export const createRenderSemaphore = (max) => { + let active = 0; + + return { + /** + * @returns {boolean} + */ + tryAcquire() { + if (active >= max) { + return false; + } + + active += 1; + return true; + }, + + release() { + active = Math.max(0, active - 1); + }, + + /** + * @returns {{active: number, max: number}} + */ + state() { + return {active, max}; + }, + }; +}; diff --git a/renderer/src/lib/tmp-files.js b/renderer/src/lib/tmp-files.js new file mode 100644 index 0000000..4451337 --- /dev/null +++ b/renderer/src/lib/tmp-files.js @@ -0,0 +1,16 @@ +import {mkdtemp, rm} from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +/** + * @returns {Promise} + */ +export const createRenderTempDir = () => mkdtemp(path.join(os.tmpdir(), 'wheelofnames-')); + +/** + * @param {string} dir + * @returns {Promise} + */ +export const cleanupTempDir = async (dir) => { + await rm(dir, {force: true, recursive: true}); +}; diff --git a/renderer/src/lib/winner.js b/renderer/src/lib/winner.js new file mode 100644 index 0000000..af25243 --- /dev/null +++ b/renderer/src/lib/winner.js @@ -0,0 +1,13 @@ +import {randomInt} from 'node:crypto'; + +/** + * @param {number} optionCount + * @returns {number} + */ +export const pickWinnerIndex = (optionCount) => { + if (!Number.isInteger(optionCount) || optionCount < 1) { + throw new RangeError('optionCount must be a positive integer'); + } + + return randomInt(optionCount); +}; diff --git a/renderer/src/remotion/Root.jsx b/renderer/src/remotion/Root.jsx new file mode 100644 index 0000000..e54c90f --- /dev/null +++ b/renderer/src/remotion/Root.jsx @@ -0,0 +1,43 @@ +import {Composition} from 'remotion'; +import {WheelComposition} from './WheelComposition.jsx'; + +/** @type {import('../schemas/wheel-request.js').WheelRenderRequest} */ +const defaultProps = { + options: ['alice', 'bob', 'carol', 'dave', 'erin', 'frank', 'grace', 'heidi'], + winnerIndex: 2, + durationMs: 6500, + holdMs: 1200, + fps: 15, + size: 512, + theme: 'classic', +}; + +/** + * @param {{durationMs: number, holdMs: number, fps: number, size: number}} props + */ +export const getCompositionMetadata = (props) => { + const fps = props.fps || defaultProps.fps; + const totalMs = (props.durationMs || defaultProps.durationMs) + (props.holdMs || defaultProps.holdMs); + const durationInFrames = Math.max(1, Math.ceil((totalMs / 1000) * fps)); + const size = props.size || defaultProps.size; + + return { + durationInFrames, + fps, + height: size, + width: size, + }; +}; + +export const RemotionRoot = () => ( + getCompositionMetadata(props)} + component={WheelComposition} + defaultProps={defaultProps} + durationInFrames={Math.ceil(((defaultProps.durationMs + defaultProps.holdMs) / 1000) * defaultProps.fps)} + fps={defaultProps.fps} + height={defaultProps.size} + id="WheelGif" + width={defaultProps.size} + /> +); diff --git a/renderer/src/remotion/WheelComposition.jsx b/renderer/src/remotion/WheelComposition.jsx new file mode 100644 index 0000000..0e1825a --- /dev/null +++ b/renderer/src/remotion/WheelComposition.jsx @@ -0,0 +1,185 @@ +import React from 'react'; +import {AbsoluteFill, Easing, interpolate, useCurrentFrame, useVideoConfig} from 'remotion'; +import {Circle, Pie} from '@remotion/shapes'; +import { + cssDegreesToPieRadians, + getFinalWheelRotationDegrees, + getReadableTextRotationDegrees, + getSliceCenterDegrees, + getSliceDegrees, +} from './wheel-layout.js'; +import {getTheme} from './themes.js'; + +const baseFont = + 'Inter, "Noto Sans", "Noto Sans Vietnamese", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; + +/** + * @typedef {import('../schemas/wheel-request.js').WheelRenderRequest} WheelRenderRequest + */ + +/** + * @param {{text: string, maxLength?: number}} props + */ +const LabelText = ({text, maxLength = 24}) => { + const displayText = text.length > maxLength ? `${text.slice(0, maxLength - 1)}…` : text; + return <>{displayText}; +}; + +/** + * @param {WheelRenderRequest} props + */ +export const WheelComposition = (props) => { + const frame = useCurrentFrame(); + const {durationInFrames, fps, width, height} = useVideoConfig(); + const theme = getTheme(props.theme); + const spinFrames = Math.max(1, durationInFrames - Math.round((props.holdMs / 1000) * fps)); + const holdStartFrame = spinFrames; + const progress = interpolate(frame, [0, spinFrames], [0, 1], { + easing: Easing.out(Easing.cubic), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + const settle = interpolate(frame, [holdStartFrame, durationInFrames - 1], [0, 1], { + easing: Easing.out(Easing.sin), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + const winnerPulse = frame >= holdStartFrame ? Math.sin(settle * Math.PI) : 0; + + const size = Math.min(width, height); + const center = size / 2; + const radius = size * 0.41; + const sliceDegrees = getSliceDegrees(props.options.length); + const finalRotation = getFinalWheelRotationDegrees(props.options.length, props.winnerIndex); + const startRotation = -24; + const rotation = interpolate(progress, [0, 1], [startRotation, finalRotation], { + easing: Easing.out(Easing.cubic), + }); + + const labelRadius = radius * (props.options.length > 20 ? 0.64 : 0.58); + const fontSize = Math.max(10, Math.min(24, radius / Math.max(5.8, props.options.length * 0.33))); + + return ( + +
+
+ +
+ {props.options.map((option, index) => { + const start = index * sliceDegrees; + const centerDegrees = getSliceCenterDegrees(props.options.length, index); + const isWinner = index === props.winnerIndex; + const color = theme.slices[index % theme.slices.length] ?? theme.slices[0]; + const x = center + Math.cos((centerDegrees * Math.PI) / 180) * labelRadius; + const y = center + Math.sin((centerDegrees * Math.PI) / 180) * labelRadius; + const textRotation = getReadableTextRotationDegrees(centerDegrees); + + return ( + + = holdStartFrame ? `drop-shadow(0 0 ${10 + winnerPulse * 10}px ${theme.winnerGlow})` : undefined, + left: center - radius, + position: 'absolute', + top: center - radius, + }} + /> +
+ +
+
+ ); + })} +
+ + + +
+
+ + ); +}; diff --git a/renderer/src/remotion/index.jsx b/renderer/src/remotion/index.jsx new file mode 100644 index 0000000..e3cd2f5 --- /dev/null +++ b/renderer/src/remotion/index.jsx @@ -0,0 +1,4 @@ +import {registerRoot} from 'remotion'; +import {RemotionRoot} from './Root.jsx'; + +registerRoot(RemotionRoot); diff --git a/renderer/src/remotion/themes.js b/renderer/src/remotion/themes.js new file mode 100644 index 0000000..3a185cb --- /dev/null +++ b/renderer/src/remotion/themes.js @@ -0,0 +1,43 @@ +export const themes = { + classic: { + background: '#f8fafc', + wheelShadow: '0 18px 52px rgba(15, 23, 42, 0.22)', + center: '#111827', + centerRing: '#f9fafb', + pointer: '#ef4444', + pointerStroke: '#991b1b', + text: '#111827', + textHalo: 'rgba(255, 255, 255, 0.88)', + winnerGlow: 'rgba(250, 204, 21, 0.45)', + slices: ['#f97316', '#14b8a6', '#facc15', '#60a5fa', '#f472b6', '#a78bfa', '#34d399', '#fb7185'], + }, + festival: { + background: '#fff7ed', + wheelShadow: '0 20px 56px rgba(124, 45, 18, 0.24)', + center: '#7f1d1d', + centerRing: '#fffbeb', + pointer: '#dc2626', + pointerStroke: '#7f1d1d', + text: '#1f2937', + textHalo: 'rgba(255, 251, 235, 0.92)', + winnerGlow: 'rgba(251, 146, 60, 0.5)', + slices: ['#f43f5e', '#f59e0b', '#84cc16', '#06b6d4', '#8b5cf6', '#ec4899', '#22c55e', '#fb923c'], + }, + mono: { + background: '#f4f4f5', + wheelShadow: '0 18px 52px rgba(24, 24, 27, 0.18)', + center: '#18181b', + centerRing: '#fafafa', + pointer: '#27272a', + pointerStroke: '#09090b', + text: '#18181b', + textHalo: 'rgba(250, 250, 250, 0.9)', + winnerGlow: 'rgba(161, 161, 170, 0.45)', + slices: ['#e4e4e7', '#a1a1aa', '#d4d4d8', '#71717a', '#f4f4f5', '#52525b'], + }, +}; + +/** + * @param {'classic' | 'festival' | 'mono'} name + */ +export const getTheme = (name) => themes[name] ?? themes.classic; diff --git a/renderer/src/remotion/wheel-layout.js b/renderer/src/remotion/wheel-layout.js new file mode 100644 index 0000000..62e5bc9 --- /dev/null +++ b/renderer/src/remotion/wheel-layout.js @@ -0,0 +1,45 @@ +export const fullTurnDegrees = 360; + +/** + * @param {number} optionCount + * @returns {number} + */ +export const getSliceDegrees = (optionCount) => fullTurnDegrees / optionCount; + +/** + * Angles use CSS coordinates: 0deg points to 3h and positive values rotate + * clockwise on screen. + * + * @param {number} optionCount + * @param {number} index + * @returns {number} + */ +export const getSliceCenterDegrees = (optionCount, index) => + index * getSliceDegrees(optionCount) + getSliceDegrees(optionCount) / 2; + +/** + * @param {number} optionCount + * @param {number} winnerIndex + * @param {number} fullTurns + * @returns {number} + */ +export const getFinalWheelRotationDegrees = (optionCount, winnerIndex, fullTurns = 7) => + fullTurns * fullTurnDegrees - getSliceCenterDegrees(optionCount, winnerIndex); + +/** + * Converts CSS angle convention to @remotion/shapes Pie convention, where 0deg + * starts at 12h. + * + * @param {number} cssDegrees + * @returns {number} + */ +export const cssDegreesToPieRadians = (cssDegrees) => ((cssDegrees - 90) * Math.PI) / 180; + +/** + * @param {number} degrees + * @returns {number} + */ +export const getReadableTextRotationDegrees = (degrees) => { + const normalized = ((degrees % fullTurnDegrees) + fullTurnDegrees) % fullTurnDegrees; + return normalized > 90 && normalized < 270 ? degrees + 180 : degrees; +}; diff --git a/renderer/src/render/remotion-bundle.js b/renderer/src/render/remotion-bundle.js new file mode 100644 index 0000000..a254854 --- /dev/null +++ b/renderer/src/render/remotion-bundle.js @@ -0,0 +1,21 @@ +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {bundle} from '@remotion/bundler'; + +/** @type {Promise | undefined} */ +let bundlePromise; + +/** + * @returns {Promise} + */ +export const getRemotionServeUrl = () => { + if (!bundlePromise) { + const entryPoint = fileURLToPath(new URL('../remotion/index.jsx', import.meta.url)); + bundlePromise = bundle({ + entryPoint, + outDir: path.join(process.cwd(), '.tmp', 'remotion-bundle'), + }); + } + + return bundlePromise; +}; diff --git a/renderer/src/render/render-gif.js b/renderer/src/render/render-gif.js new file mode 100644 index 0000000..cb89a8c --- /dev/null +++ b/renderer/src/render/render-gif.js @@ -0,0 +1,79 @@ +import {readFile} from 'node:fs/promises'; +import path from 'node:path'; +import {makeCancelSignal, renderMedia, selectComposition} from '@remotion/renderer'; +import {minRenderTimeoutMs} from '../config.js'; +import {RenderTimeoutError} from '../lib/render-errors.js'; +import {cleanupTempDir, createRenderTempDir} from '../lib/tmp-files.js'; +import {getRemotionServeUrl} from './remotion-bundle.js'; + +/** + * @typedef {import('../schemas/wheel-request.js').WheelRenderRequest} WheelRenderRequest + */ + +/** + * @typedef {object} RenderWheelGifOptions + * @property {number} timeoutInMilliseconds + */ + +/** + * @typedef {object} RenderGifResult + * @property {Buffer} buffer + * @property {number} durationMs + * @property {number} byteLength + */ + +/** + * @param {WheelRenderRequest} inputProps + * @param {RenderWheelGifOptions} options + * @returns {Promise} + */ +export const renderWheelGif = async (inputProps, options) => { + const startedAt = Date.now(); + const tempDir = await createRenderTempDir(); + const outputLocation = path.join(tempDir, 'wheelofnames.gif'); + const {cancel, cancelSignal} = makeCancelSignal(); + let timedOut = false; + const timeoutId = setTimeout(() => { + timedOut = true; + cancel(); + }, options.timeoutInMilliseconds); + const rendererTimeout = Math.max(minRenderTimeoutMs, options.timeoutInMilliseconds); + + try { + const serveUrl = await getRemotionServeUrl(); + const composition = await selectComposition({ + id: 'WheelGif', + inputProps, + serveUrl, + timeoutInMilliseconds: rendererTimeout, + }); + + await renderMedia({ + cancelSignal, + codec: 'gif', + composition, + imageFormat: 'png', + inputProps, + outputLocation, + overwrite: true, + serveUrl, + timeoutInMilliseconds: rendererTimeout, + }); + + const buffer = await readFile(outputLocation); + return { + buffer, + byteLength: buffer.byteLength, + durationMs: Date.now() - startedAt, + }; + } catch (error) { + if (timedOut || Date.now() - startedAt >= options.timeoutInMilliseconds) { + throw new RenderTimeoutError(options.timeoutInMilliseconds, {cause: error}); + } + + throw error; + } finally { + clearTimeout(timeoutId); + await cleanupTempDir(tempDir); + } +}; diff --git a/renderer/src/routes/gif.js b/renderer/src/routes/gif.js new file mode 100644 index 0000000..50dba82 --- /dev/null +++ b/renderer/src/routes/gif.js @@ -0,0 +1,88 @@ +import {ZodError} from 'zod'; +import {isRenderTimeoutError} from '../lib/render-errors.js'; +import {pickWinnerIndex} from '../lib/winner.js'; +import {parseWheelRequest} from '../schemas/wheel-request.js'; + +/** + * @typedef {import('../config.js').AppConfig} AppConfig + * @typedef {import('../lib/render-semaphore.js').createRenderSemaphore} CreateRenderSemaphore + * @typedef {import('../schemas/wheel-request.js').WheelRenderRequest} WheelRenderRequest + */ + +/** + * @typedef {object} GifRouteDeps + * @property {AppConfig} config + * @property {{tryAcquire: () => boolean, release: () => void}} semaphore + * @property {(request: WheelRenderRequest, options: {timeoutInMilliseconds: number}) => Promise<{buffer: Buffer, durationMs: number, byteLength: number}>} renderGif + */ + +/** + * @param {unknown} issues + */ +const formatValidationError = (issues) => ({ + error: 'invalid_request', + issues, +}); + +/** + * @param {import('fastify').FastifyInstance} app + * @param {GifRouteDeps} deps + */ +export const registerGifRoute = async (app, deps) => { + app.post('/api/gif', async (request, reply) => { + if (deps.config.apiToken) { + const expected = `Bearer ${deps.config.apiToken}`; + if (request.headers.authorization !== expected) { + return reply.code(401).send({error: 'unauthorized'}); + } + } + + let wheelRequest; + try { + wheelRequest = parseWheelRequest( + request.body, + { + maxOptionChars: deps.config.maxOptionChars, + maxOptions: deps.config.maxOptions, + }, + pickWinnerIndex, + ); + } catch (error) { + if (error instanceof ZodError) { + return reply.code(400).send(formatValidationError(error.issues)); + } + throw error; + } + + if (!deps.semaphore.tryAcquire()) { + return reply.code(429).send({error: 'too_many_renders'}); + } + + try { + const result = await deps.renderGif(wheelRequest, { + timeoutInMilliseconds: deps.config.renderTimeoutMs, + }); + const winner = wheelRequest.options[wheelRequest.winnerIndex]; + if (winner === undefined) { + throw new Error('winner index resolved outside options range'); + } + + return reply + .header('Content-Type', 'image/gif') + .header('Content-Disposition', 'inline; filename="wheelofnames.gif"') + .header('Cache-Control', 'no-store') + .header('X-Wheel-Winner-Index', String(wheelRequest.winnerIndex)) + .header('X-Wheel-Winner', encodeURIComponent(winner)) + .header('X-Render-Duration-Ms', String(result.durationMs)) + .send(result.buffer); + } catch (error) { + if (isRenderTimeoutError(error)) { + return reply.code(504).send({error: 'render_timeout'}); + } + + throw error; + } finally { + deps.semaphore.release(); + } + }); +}; diff --git a/renderer/src/routes/health.js b/renderer/src/routes/health.js new file mode 100644 index 0000000..4993519 --- /dev/null +++ b/renderer/src/routes/health.js @@ -0,0 +1,9 @@ +/** + * @param {import('fastify').FastifyInstance} app + */ +export const registerHealthRoute = async (app) => { + app.get('/api/healthz', async () => ({ + ok: true, + service: 'wheelofnames', + })); +}; diff --git a/renderer/src/schemas/wheel-request.js b/renderer/src/schemas/wheel-request.js new file mode 100644 index 0000000..4171bc7 --- /dev/null +++ b/renderer/src/schemas/wheel-request.js @@ -0,0 +1,71 @@ +import {z} from 'zod'; + +export const themes = ['classic', 'festival', 'mono']; +export const allowedSizes = [384, 480, 512]; +export const allowedFps = [12, 15, 20]; +const themeSchema = z.union([z.literal('classic'), z.literal('festival'), z.literal('mono')]); + +/** + * @typedef {object} WheelRequestLimits + * @property {number} maxOptions + * @property {number} maxOptionChars + */ + +/** + * @typedef {object} WheelRenderRequest + * @property {string[]} options + * @property {number} winnerIndex + * @property {number} durationMs + * @property {number} holdMs + * @property {12 | 15 | 20} fps + * @property {384 | 480 | 512} size + * @property {'classic' | 'festival' | 'mono'} theme + */ + +/** + * @param {WheelRequestLimits} limits + */ +export const createWheelRequestSchema = (limits) => + z + .object({ + options: z + .array(z.string()) + .transform((options) => options.map((option) => option.trim())) + .pipe( + z + .array(z.string().min(1).max(limits.maxOptionChars)) + .min(2) + .max(limits.maxOptions), + ), + winnerIndex: z.number().int().nonnegative().optional(), + durationMs: z.number().int().min(3000).max(10000).default(6500), + holdMs: z.number().int().min(500).max(2500).default(1200), + fps: z.union([z.literal(12), z.literal(15), z.literal(20)]).default(15), + size: z.union([z.literal(384), z.literal(480), z.literal(512)]).default(512), + theme: themeSchema.default('classic'), + }) + .strict() + .superRefine((value, context) => { + if (value.winnerIndex !== undefined && value.winnerIndex >= value.options.length) { + context.addIssue({ + code: z.ZodIssueCode.custom, + path: ['winnerIndex'], + message: 'winnerIndex must be within options range', + }); + } + }); + +/** + * @param {unknown} input + * @param {WheelRequestLimits} limits + * @param {(optionCount: number) => number} pickWinner + * @returns {WheelRenderRequest} + */ +export const parseWheelRequest = (input, limits, pickWinner) => { + const parsed = createWheelRequestSchema(limits).parse(input); + + return { + ...parsed, + winnerIndex: parsed.winnerIndex ?? pickWinner(parsed.options.length), + }; +}; diff --git a/renderer/src/server.js b/renderer/src/server.js new file mode 100644 index 0000000..9247b4a --- /dev/null +++ b/renderer/src/server.js @@ -0,0 +1,52 @@ +import Fastify from 'fastify'; +import {loadConfig} from './config.js'; +import {createRenderSemaphore} from './lib/render-semaphore.js'; +import {getRemotionServeUrl} from './render/remotion-bundle.js'; +import {renderWheelGif} from './render/render-gif.js'; +import {registerGifRoute} from './routes/gif.js'; +import {registerHealthRoute} from './routes/health.js'; + +/** + * @typedef {import('./config.js').AppConfig} AppConfig + * @typedef {import('./schemas/wheel-request.js').WheelRenderRequest} WheelRenderRequest + */ + +/** + * @param {object} [options] + * @param {AppConfig} [options.config] + * @param {(request: WheelRenderRequest, renderOptions: {timeoutInMilliseconds: number}) => Promise<{buffer: Buffer, durationMs: number, byteLength: number}>} [options.renderGif] + * @param {boolean} [options.warmRemotionBundle] + */ +export const buildServer = async (options = {}) => { + const config = options.config ?? loadConfig(); + const warmRemotionBundle = options.warmRemotionBundle ?? process.env.NODE_ENV !== 'test'; + + if (warmRemotionBundle) { + await getRemotionServeUrl(); + } + + const app = Fastify({ + bodyLimit: 32 * 1024, + logger: process.env.NODE_ENV !== 'test', + }); + + app.setErrorHandler(async (error, _request, reply) => { + app.log.error(error); + return reply.code(500).send({error: 'internal_error'}); + }); + + await registerHealthRoute(app); + await registerGifRoute(app, { + config, + renderGif: options.renderGif ?? renderWheelGif, + semaphore: createRenderSemaphore(config.maxConcurrentRenders), + }); + + return app; +}; + +if (import.meta.url === `file://${process.argv[1]}`) { + const config = loadConfig(); + const app = await buildServer({config}); + await app.listen({host: config.host, port: config.port}); +} diff --git a/renderer/test/config.test.js b/renderer/test/config.test.js new file mode 100644 index 0000000..1d0d2ae --- /dev/null +++ b/renderer/test/config.test.js @@ -0,0 +1,21 @@ +import {describe, expect, test} from 'vitest'; +import {loadConfig, minRenderTimeoutMs} from '../src/config.js'; + +describe('loadConfig', () => { + test('keeps render timeout compatible with Remotion browser timeout limits', () => { + const config = loadConfig({RENDER_TIMEOUT_MS: '500'}); + + expect(config.renderTimeoutMs).toBe(minRenderTimeoutMs); + }); + + test('requires API_TOKEN in production', () => { + expect(() => loadConfig({NODE_ENV: 'production'})).toThrow(/API_TOKEN/); + }); + + test('accepts API_TOKEN in production', () => { + const config = loadConfig({NODE_ENV: 'production', API_TOKEN: 'secret'}); + + expect(config.requiresApiToken).toBe(true); + expect(config.apiToken).toBe('secret'); + }); +}); diff --git a/renderer/test/gif-route.test.js b/renderer/test/gif-route.test.js new file mode 100644 index 0000000..fca2e51 --- /dev/null +++ b/renderer/test/gif-route.test.js @@ -0,0 +1,114 @@ +import {describe, expect, test} from 'vitest'; +import {RenderTimeoutError} from '../src/lib/render-errors.js'; +import {buildServer} from '../src/server.js'; + +const config = { + apiToken: undefined, + host: '127.0.0.1', + maxConcurrentRenders: 1, + maxOptionChars: 40, + maxOptions: 32, + port: 0, + requiresApiToken: false, + renderTimeoutMs: 15000, +}; + +describe('POST /api/gif', () => { + test('returns gif bytes and winner metadata', async () => { + const app = await buildServer({ + config, + renderGif: async () => ({ + buffer: Buffer.from('GIF89a-test'), + byteLength: 11, + durationMs: 12, + }), + }); + + const response = await app.inject({ + method: 'POST', + url: '/api/gif', + payload: { + options: ['alpha', 'beta', 'gamma'], + winnerIndex: 1, + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.headers['content-type']).toBe('image/gif'); + expect(response.headers['x-wheel-winner-index']).toBe('1'); + expect(response.headers['x-wheel-winner']).toBe('beta'); + expect(response.rawPayload.subarray(0, 6).toString()).toBe('GIF89a'); + + await app.close(); + }); + + test('rejects invalid payload', async () => { + const app = await buildServer({ + config, + renderGif: async () => ({ + buffer: Buffer.from('GIF89a-test'), + byteLength: 11, + durationMs: 12, + }), + }); + + const response = await app.inject({ + method: 'POST', + url: '/api/gif', + payload: { + options: ['only one'], + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json().error).toBe('invalid_request'); + + await app.close(); + }); + + test('enforces bearer token when configured', async () => { + const app = await buildServer({ + config: {...config, apiToken: 'secret'}, + renderGif: async () => ({ + buffer: Buffer.from('GIF89a-test'), + byteLength: 11, + durationMs: 12, + }), + }); + + const response = await app.inject({ + method: 'POST', + url: '/api/gif', + payload: { + options: ['a', 'b'], + }, + }); + + expect(response.statusCode).toBe(401); + + await app.close(); + }); + + test('returns timeout response when rendering exceeds configured limit', async () => { + const app = await buildServer({ + config: {...config, renderTimeoutMs: 7}, + renderGif: async (_request, renderOptions) => { + expect(renderOptions.timeoutInMilliseconds).toBe(7); + throw new RenderTimeoutError(renderOptions.timeoutInMilliseconds); + }, + }); + + const response = await app.inject({ + method: 'POST', + url: '/api/gif', + payload: { + options: ['alpha', 'beta'], + }, + }); + + expect(response.statusCode).toBe(504); + expect(response.json().error).toBe('render_timeout'); + + await app.close(); + }); +}); diff --git a/renderer/test/wheel-layout.test.js b/renderer/test/wheel-layout.test.js new file mode 100644 index 0000000..8c6e5a6 --- /dev/null +++ b/renderer/test/wheel-layout.test.js @@ -0,0 +1,27 @@ +import {describe, expect, test} from 'vitest'; +import { + getFinalWheelRotationDegrees, + getSliceCenterDegrees, + getSliceDegrees, +} from '../src/remotion/wheel-layout.js'; + +describe('wheel layout', () => { + test('calculates equal slice sizes', () => { + expect(getSliceDegrees(8)).toBe(45); + }); + + test('calculates center angle from 3h clockwise', () => { + expect(getSliceCenterDegrees(4, 0)).toBe(45); + expect(getSliceCenterDegrees(4, 1)).toBe(135); + }); + + test('final rotation places winner center at 3h', () => { + const optionCount = 8; + const winnerIndex = 3; + const rotation = getFinalWheelRotationDegrees(optionCount, winnerIndex, 7); + const winnerCenter = getSliceCenterDegrees(optionCount, winnerIndex); + const screenAngle = ((winnerCenter + rotation) % 360 + 360) % 360; + + expect(screenAngle).toBe(0); + }); +}); diff --git a/renderer/test/wheel-request.test.js b/renderer/test/wheel-request.test.js new file mode 100644 index 0000000..c1c2cd1 --- /dev/null +++ b/renderer/test/wheel-request.test.js @@ -0,0 +1,60 @@ +import {describe, expect, test} from 'vitest'; +import {parseWheelRequest} from '../src/schemas/wheel-request.js'; + +const limits = { + maxOptionChars: 12, + maxOptions: 4, +}; + +describe('parseWheelRequest', () => { + test('trims options and preserves provided winner', () => { + const request = parseWheelRequest( + { + options: [' alpha ', ' beta '], + winnerIndex: 1, + }, + limits, + () => 0, + ); + + expect(request.options).toEqual(['alpha', 'beta']); + expect(request.winnerIndex).toBe(1); + expect(request.durationMs).toBe(6500); + expect(request.fps).toBe(15); + }); + + test('rejects blank options so winner index cannot shift after trimming', () => { + expect(() => + parseWheelRequest( + { + options: ['', 'alpha', 'beta'], + winnerIndex: 1, + }, + limits, + () => 0, + ), + ).toThrow(); + }); + + test('selects winner when omitted', () => { + const request = parseWheelRequest({options: ['a', 'b']}, limits, () => 1); + expect(request.winnerIndex).toBe(1); + }); + + test('rejects winner outside option range', () => { + expect(() => + parseWheelRequest( + { + options: ['a', 'b'], + winnerIndex: 2, + }, + limits, + () => 0, + ), + ).toThrow(/winnerIndex/); + }); + + test('rejects too many options', () => { + expect(() => parseWheelRequest({options: ['a', 'b', 'c', 'd', 'e']}, limits, () => 0)).toThrow(); + }); +}); diff --git a/renderer/tsconfig.json b/renderer/tsconfig.json new file mode 100644 index 0000000..e35b3c1 --- /dev/null +++ b/renderer/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true, + "target": "ES2024", + "lib": ["ES2024", "DOM", "DOM.Iterable"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "jsx": "react-jsx", + "strict": true, + "noUncheckedIndexedAccess": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["node", "vitest/globals"] + }, + "include": ["src/**/*.js", "src/**/*.jsx", "scripts/**/*.js", "test/**/*.js", "eslint.config.js", "vitest.config.js"], + "exclude": ["node_modules", "dist", "coverage", ".tmp"] +} diff --git a/renderer/vitest.config.js b/renderer/vitest.config.js new file mode 100644 index 0000000..9fed3e1 --- /dev/null +++ b/renderer/vitest.config.js @@ -0,0 +1,8 @@ +import {defineConfig} from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['test/**/*.test.js'], + }, +});