Initial commit: full-stack Bun + Elysia + React template

Elysia.js API with session-based auth (email/password + Google OAuth),
role system (USER/ADMIN/SUPER_ADMIN), Prisma + PostgreSQL, React 19
with Mantine UI, TanStack Router, dark theme, and comprehensive test
suite (unit, integration, E2E with Lightpanda).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
bipproduction
2026-04-01 10:12:19 +08:00
commit 08a1054e3c
57 changed files with 3732 additions and 0 deletions

6
.env.example Normal file
View File

@@ -0,0 +1,6 @@
# App
PORT=3000
NODE_ENV=development
# Dev Inspector
REACT_EDITOR=code

39
.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# TanStack Router generated
src/frontend/routeTree.gen.ts
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
/generated/prisma

73
CLAUDE.md Normal file
View File

@@ -0,0 +1,73 @@
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>`
- Use `bunx <package> <command>` instead of `npx <package> <command>`
- Bun automatically loads .env, so don't use dotenv.
## Server
Elysia.js as the HTTP framework, running on Bun. API routes are in `src/app.ts` (exported as `createApp()`), frontend serving and dev tools are in `src/index.tsx`.
- `src/app.ts` — Elysia app factory with all API routes (auth, hello, health, Google OAuth). Testable via `app.handle(request)`.
- `src/index.tsx` — Server entry. Adds Vite middleware (dev) or static file serving (prod), click-to-source editor integration, and `.listen()`.
- `src/serve.ts` — Dev entry (`bun --watch src/serve.ts`). Dynamic import workaround for Bun EADDRINUSE race.
## Database
PostgreSQL via Prisma v6. Client generated to `./generated/prisma` (gitignored).
- Schema: `prisma/schema.prisma` — User (id, name, email, password, timestamps) + Session (id, token, userId, expiresAt)
- Client singleton: `src/lib/db.ts` — import `{ prisma }` from here
- Seed: `prisma/seed.ts` — demo users with `Bun.password.hash` bcrypt
- Commands: `bun run db:migrate`, `bun run db:seed`, `bun run db:generate`
## Auth
Session-based auth with HttpOnly cookies stored in DB.
- Login: `POST /api/auth/login` — finds user by email, verifies password with `Bun.password.verify`, creates Session record
- Google OAuth: `GET /api/auth/google` → Google → `GET /api/auth/callback/google` — upserts user, creates session
- Session: `GET /api/auth/session` — looks up session by cookie token, returns user or 401, auto-deletes expired
- Logout: `POST /api/auth/logout` — deletes session from DB, clears cookie
## Frontend
React 19 + Vite 8 (middleware mode in dev). File-based routing with TanStack Router.
- Entry: `src/frontend.tsx` — renders App, removes splash screen, DevInspector in dev
- App: `src/frontend/App.tsx` — MantineProvider (dark, forced), QueryClientProvider, RouterProvider
- Routes: `src/frontend/routes/``__root.tsx`, `index.tsx`, `login.tsx`, `dashboard.tsx`
- Auth hooks: `src/frontend/hooks/useAuth.ts``useSession()`, `useLogin()`, `useLogout()`
- UI: Mantine v8 (dark theme `#242424`), react-icons
- Splash: `index.html` has inline dark CSS + spinner, removed on React mount
## Dev Tools
- Click-to-source: `Ctrl+Shift+Cmd+C` toggles inspector. Custom Vite plugin (`inspectorPlugin` in `src/vite.ts`) injects `data-inspector-*` attributes. Reads original file from disk for accurate line numbers.
- HMR: Vite 8 with `@vitejs/plugin-react` v6. `dedupeRefreshPlugin` fixes double React Refresh injection.
- Editor: `REACT_EDITOR` env var. `zed` and `subl` use `file:line:col`, others use `--goto file:line:col`.
## Testing
Tests use `bun:test`. Three levels:
```bash
bun run test # All tests
bun run test:unit # tests/unit/ — env, db connection, bcrypt
bun run test:integration # tests/integration/ — API endpoints via app.handle()
bun run test:e2e # tests/e2e/ — browser tests via Lightpanda CDP
```
- `tests/helpers.ts``createTestApp()`, `seedTestUser()`, `createTestSession()`, `cleanupTestData()`
- Integration tests use `createApp().handle(new Request(...))` — no server needed
- E2E tests use Lightpanda browser (Docker, `ws://127.0.0.1:9222`). App URLs use `host.docker.internal` from container. Lightpanda executes JS but POST fetch returns 407 — use integration tests for mutations.
## APIs
- `Bun.password.hash()` / `Bun.password.verify()` for bcrypt
- `Bun.file()` for static file serving in production
- `Bun.which()` / `Bun.spawn()` for editor integration
- `crypto.randomUUID()` for session tokens

156
README.md Normal file
View File

@@ -0,0 +1,156 @@
# Base Template
Full-stack web application template built with Bun, Elysia, React 19, and Vite.
## Tech Stack
- **Runtime**: [Bun](https://bun.com)
- **Server**: [Elysia.js](https://elysiajs.com) with Vite middleware mode (dev) / static serving (prod)
- **Frontend**: React 19 + [TanStack Router](https://tanstack.com/router) (file-based routing) + [TanStack Query](https://tanstack.com/query)
- **UI**: [Mantine v8](https://mantine.dev) (dark theme) + [react-icons](https://react-icons.github.io/react-icons/)
- **Database**: PostgreSQL via [Prisma v6](https://www.prisma.io)
- **Auth**: Session-based (bcrypt + HttpOnly cookies) + Google OAuth
- **Dev Tools**: Click-to-source inspector (Ctrl+Shift+Cmd+C), HMR, Biome linter
- **Testing**: bun:test (unit + integration) + [Lightpanda](https://lightpanda.io) (E2E via CDP)
## Prerequisites
- [Bun](https://bun.sh) >= 1.3
- PostgreSQL running on `localhost:5432`
- [Lightpanda](https://github.com/lightpanda-io/browser) (optional, for E2E tests)
## Setup
```bash
# Install dependencies
bun install
# Configure environment
cp .env.example .env
# Edit .env with your DATABASE_URL, Google OAuth credentials, etc.
# Setup database
bun run db:migrate
bun run db:seed
```
## Development
```bash
bun run dev
```
Server starts at `http://localhost:3000` (configurable via `PORT` in `.env`).
Features in dev mode:
- Hot Module Replacement (HMR) via Vite
- Click-to-source inspector: `Ctrl+Shift+Cmd+C` to toggle, click any component to open in editor
- Splash screen (dark) prevents white flash on reload
## Production
```bash
bun run build # Build frontend with Vite
bun run start # Start production server
```
## Scripts
| Script | Description |
| -------------------------- | ------------------------------------------------ |
| `bun run dev` | Start dev server with HMR |
| `bun run build` | Build frontend for production |
| `bun run start` | Start production server |
| `bun run test` | Run all tests |
| `bun run test:unit` | Run unit tests |
| `bun run test:integration` | Run integration tests |
| `bun run test:e2e` | Run E2E tests (requires Lightpanda + dev server) |
| `bun run typecheck` | TypeScript type check |
| `bun run lint` | Lint with Biome |
| `bun run lint:fix` | Lint and auto-fix |
| `bun run db:migrate` | Run Prisma migrations |
| `bun run db:seed` | Seed demo users |
| `bun run db:studio` | Open Prisma Studio |
| `bun run db:generate` | Regenerate Prisma client |
| `bun run db:push` | Push schema to DB without migration |
## Project Structure
```
src/
index.tsx # Server entry — Vite middleware, frontend serving, editor integration
app.ts # Elysia app — API routes (auth, hello, health, Google OAuth)
serve.ts # Dev entry (workaround for Bun EADDRINUSE)
vite.ts # Vite dev server config, inspector plugin, dedupe plugin
frontend.tsx # React entry — root render, splash removal, HMR
lib/
db.ts # Prisma client singleton
env.ts # Environment variables
frontend/
App.tsx # Root component — MantineProvider, QueryClient, Router
DevInspector.tsx # Click-to-source overlay (dev only)
hooks/
useAuth.ts # useSession, useLogin, useLogout hooks
routes/
__root.tsx # Root layout
index.tsx # Landing page
login.tsx # Login page (email/password + Google OAuth)
dashboard.tsx # Protected dashboard
prisma/
schema.prisma # Database schema (User, Session)
seed.ts # Seed script (demo users with bcrypt)
migrations/ # Prisma migrations
tests/
helpers.ts # Test utilities (seedTestUser, createTestSession, cleanup)
unit/ # Unit tests (env, db, password)
integration/ # Integration tests (auth, health, hello API)
e2e/ # E2E tests via Lightpanda CDP
browser.ts # Lightpanda CDP helper class
```
## Auth
- **Email/password**: POST `/api/auth/login` — bcrypt verification, creates DB session
- **Google OAuth**: GET `/api/auth/google` — redirects to Google, callback at `/api/auth/callback/google`
- **Session check**: GET `/api/auth/session` — returns current user or 401
- **Logout**: POST `/api/auth/logout` — deletes session from DB
Demo users (seeded): `admin@example.com` / `admin123`, `user@example.com` / `user123`
## E2E Tests (Lightpanda)
Lightpanda runs as a Docker container:
```yaml
# docker-compose.yml
services:
lightpanda:
image: lightpanda/browser:nightly
container_name: lightpanda
restart: unless-stopped
ports:
- "9222:9222"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- LIGHTPANDA_DISABLE_TELEMETRY=true
mem_limit: 256m
cpus: "0.5"
```
```bash
docker compose up -d # Start Lightpanda
bun run dev # Start dev server
bun run test:e2e # Run E2E tests
```
## Environment Variables
| Variable | Required | Description |
| ---------------------- | -------- | ------------------------------------------ |
| `DATABASE_URL` | Yes | PostgreSQL connection string |
| `GOOGLE_CLIENT_ID` | Yes | Google OAuth client ID |
| `GOOGLE_CLIENT_SECRET` | Yes | Google OAuth client secret |
| `PORT` | No | Server port (default: 3000) |
| `REACT_EDITOR` | No | Editor for click-to-source (default: code) |

26
biome.json Normal file
View File

@@ -0,0 +1,26 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.8/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"files": { "ignoreUnknown": true, "includes": ["src/**"] },
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 120
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": { "noExplicitAny": "off" },
"style": { "noNonNullAssertion": "off" }
}
},
"javascript": {
"formatter": { "quoteStyle": "single", "semicolons": "asNeeded" }
},
"assist": {
"enabled": true,
"actions": { "source": { "organizeImports": "on" } }
}
}

17
bun-env.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
// Generated by `bun init`
declare module "*.svg" {
/**
* A path to the SVG file
*/
const path: `${string}.svg`;
export = path;
}
declare module "*.module.css" {
/**
* A record of class names to their corresponding CSS module classes
*/
const classes: { readonly [key: string]: string };
export = classes;
}

758
bun.lock Normal file
View File

@@ -0,0 +1,758 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "bun-react-template",
"dependencies": {
"@elysiajs/cors": "^1.4.1",
"@elysiajs/html": "^1.4.0",
"@mantine/core": "^8.3.18",
"@mantine/hooks": "^8.3.18",
"@prisma/client": "6",
"@tanstack/react-query": "^5.95.2",
"@tanstack/react-router": "^1.168.10",
"elysia": "^1.4.28",
"postcss": "^8.5.8",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"react": "^19",
"react-dom": "^19",
"react-icons": "^5.6.0",
},
"devDependencies": {
"@biomejs/biome": "^2.4.10",
"@tanstack/router-vite-plugin": "^1.166.27",
"@types/bun": "latest",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^6.0.1",
"prisma": "6",
"puppeteer-core": "^24.40.0",
"typescript": "^6.0.2",
"vite": "^8.0.3",
},
},
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="],
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="],
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@biomejs/biome": ["@biomejs/biome@2.4.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.10", "@biomejs/cli-darwin-x64": "2.4.10", "@biomejs/cli-linux-arm64": "2.4.10", "@biomejs/cli-linux-arm64-musl": "2.4.10", "@biomejs/cli-linux-x64": "2.4.10", "@biomejs/cli-linux-x64-musl": "2.4.10", "@biomejs/cli-win32-arm64": "2.4.10", "@biomejs/cli-win32-x64": "2.4.10" }, "bin": { "biome": "bin/biome" } }, "sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.10", "", { "os": "linux", "cpu": "x64" }, "sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.10", "", { "os": "linux", "cpu": "x64" }, "sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.10", "", { "os": "win32", "cpu": "x64" }, "sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg=="],
"@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="],
"@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="],
"@elysiajs/html": ["@elysiajs/html@1.4.0", "", { "dependencies": { "@kitajs/html": "^4.1.0", "@kitajs/ts-html-plugin": "^4.0.1" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-j4jFqGEkIC8Rg2XiTOujb9s0WLnz1dnY/4uqczyCdOVruDeJtGP+6+GvF0A76SxEvltn8UR1yCUnRdLqRi3vuw=="],
"@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.4", "", { "os": "android", "cpu": "arm64" }, "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.4", "", { "os": "android", "cpu": "x64" }, "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.4", "", { "os": "linux", "cpu": "arm" }, "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.4", "", { "os": "linux", "cpu": "none" }, "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.4", "", { "os": "linux", "cpu": "x64" }, "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.4", "", { "os": "none", "cpu": "x64" }, "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.4", "", { "os": "none", "cpu": "arm64" }, "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg=="],
"@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
"@floating-ui/react": ["@floating-ui/react@0.27.19", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@kitajs/html": ["@kitajs/html@4.2.13", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-o+8e61EsoLDPTP7rsPkYolca1YFybHuxU2Lr5fWDZCUkYT/6uBlVkvnZUdCXMQKentJL9dxwpR8/xK2Q+U4LhA=="],
"@kitajs/ts-html-plugin": ["@kitajs/ts-html-plugin@4.1.4", "", { "dependencies": { "chalk": "^5.6.2", "tslib": "^2.8.1", "yargs": "^18.0.0" }, "peerDependencies": { "@kitajs/html": "^4.2.10", "typescript": "^5.9.3" }, "bin": { "xss-scan": "dist/cli.js", "ts-html-plugin": "dist/cli.js" } }, "sha512-xK5mNrhnIy73kJFKx5yVGChJyWFRGmIaE0sjlVxVYllk5dyaEYVCrIh1N8AfnseEHka8gAqzPGW95HlkhDvnJA=="],
"@mantine/core": ["@mantine/core@8.3.18", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", "react-number-format": "^5.4.4", "react-remove-scroll": "^2.7.1", "react-textarea-autosize": "8.5.9", "type-fest": "^4.41.0" }, "peerDependencies": { "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA=="],
"@mantine/hooks": ["@mantine/hooks@8.3.18", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
"@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
"@prisma/client": ["@prisma/client@6.19.2", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg=="],
"@prisma/config": ["@prisma/config@6.19.2", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ=="],
"@prisma/debug": ["@prisma/debug@6.19.2", "", {}, "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A=="],
"@prisma/engines": ["@prisma/engines@6.19.2", "", { "dependencies": { "@prisma/debug": "6.19.2", "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", "@prisma/fetch-engine": "6.19.2", "@prisma/get-platform": "6.19.2" } }, "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A=="],
"@prisma/engines-version": ["@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", "", {}, "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA=="],
"@prisma/fetch-engine": ["@prisma/fetch-engine@6.19.2", "", { "dependencies": { "@prisma/debug": "6.19.2", "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", "@prisma/get-platform": "6.19.2" } }, "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q=="],
"@prisma/get-platform": ["@prisma/get-platform@6.19.2", "", { "dependencies": { "@prisma/debug": "6.19.2" } }, "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA=="],
"@puppeteer/browsers": ["@puppeteer/browsers@2.13.0", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
"@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@tanstack/history": ["@tanstack/history@1.161.6", "", {}, "sha512-NaOGLRrddszbQj9upGat6HG/4TKvXLvu+osAIgfxPYA+eIvYKv8GKDJOrY2D3/U9MRnKfMWD7bU4jeD4xmqyIg=="],
"@tanstack/query-core": ["@tanstack/query-core@5.95.2", "", {}, "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ=="],
"@tanstack/react-query": ["@tanstack/react-query@5.95.2", "", { "dependencies": { "@tanstack/query-core": "5.95.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA=="],
"@tanstack/react-router": ["@tanstack/react-router@1.168.10", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.9", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-/RmDlOwDkCug609KdPB3U+U1zmrtadJpvsmRg2zEn8TRCKRNri7dYZIjQZbNg8PgUiRL4T6njrZBV1ChzblNaA=="],
"@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="],
"@tanstack/router-core": ["@tanstack/router-core@1.168.9", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2" }, "bin": { "intent": "bin/intent.js" } }, "sha512-18oeEwEDyXOIuO1VBP9ACaK7tYHZUjynGDCoUh/5c/BNhia9vCJCp9O0LfhZXOorDc/PmLSgvmweFhVmIxF10g=="],
"@tanstack/router-generator": ["@tanstack/router-generator@1.166.24", "", { "dependencies": { "@tanstack/router-core": "1.168.9", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-vdaGKwuH+r+DPe6R1mjk+TDDmDH6NTG7QqwxHqGEvOH4aGf9sPjhmRKNJZqQr8cPIbfp6u5lXyZ1TeDcSNMVEA=="],
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.12", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.9", "@tanstack/router-generator": "1.166.24", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.168.10", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"], "bin": { "intent": "bin/intent.js" } }, "sha512-StEHcctCuFI5taSjO+lhR/yQ+EK63BdyYa+ne6FoNQPB3MMrOUrz2ZVnbqILRLkh2b+p2EfBKt65sgAKdKygPQ=="],
"@tanstack/router-utils": ["@tanstack/router-utils@1.161.6", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-nRcYw+w2OEgK6VfjirYvGyPLOK+tZQz1jkYcmH5AjMamQ9PycnlxZF2aEZtPpNoUsaceX2bHptn6Ub5hGXqNvw=="],
"@tanstack/router-vite-plugin": ["@tanstack/router-vite-plugin@1.166.27", "", { "dependencies": { "@tanstack/router-plugin": "1.167.12" } }, "sha512-aus9GoDzZ85Wb5aVeztq6lw6rm0n2MLn8jBtioXbDyMZNNCn0Oahm5/KZYxmB9fBxT9QPuVroH2QputUCIZB9Q=="],
"@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="],
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.161.7", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-olW33+Cn+bsCsZKPwEGhlkqS6w3M2slFv11JIobdnCFKMLG97oAI2kWKdx5/zsywTL8flpnoIgaZZPlQTFYhdQ=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
"b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="],
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
"bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
"bare-fs": ["bare-fs@4.5.6", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw=="],
"bare-os": ["bare-os@3.8.6", "", {}, "sha512-l8xaNWWb/bXuzgsrlF5jaa5QYDJ9S0ddd54cP6CH+081+5iPrbJiCfBWQqrWYzmUhCbsH+WR6qxo9MeHVCr0MQ=="],
"bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="],
"bare-stream": ["bare-stream@2.11.0", "", { "dependencies": { "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-Y/+iQ49fL3rIn6w/AVxI/2+BRrpmzJvdWt5Jv8Za6Ngqc6V227c+pYjYYgLdpR3MwQ9ObVXD0ZrqoBztakM0rw=="],
"bare-url": ["bare-url@2.4.0", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.12", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ=="],
"basic-ftp": ["basic-ftp@5.2.0", "", {}, "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
"c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="],
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001782", "", {}, "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"chromium-bidi": ["chromium-bidi@14.0.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw=="],
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="],
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"devtools-protocol": ["devtools-protocol@0.0.1581282", "", {}, "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ=="],
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.329", "", {}, "sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ=="],
"elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
"exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
"fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"file-type": ["file-type@22.0.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-cmBmnYo8Zymabm2+qAP7jTFbKF10bQpYmxoGfuZbRFRcq00BRddJdGNH/P7GA1EMpJy5yQbqa9B7yROb3z8Ziw=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="],
"get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="],
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"isbot": ["isbot@5.1.37", "", {}, "sha512-5bcicX81xf6NlTEV8rWdg7Pk01LFizDetuYGHx6d/f6y3lR2/oo8IfxjzJqn1UdDEyCcwT9e7NRloj8DwCYujQ=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "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" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"nypm": ["nypm@0.6.5", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ=="],
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
"pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],
"pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="],
"postcss-mixins": ["postcss-mixins@12.1.2", "", { "dependencies": { "postcss-js": "^4.0.1", "postcss-simple-vars": "^7.0.1", "sugarss": "^5.0.0", "tinyglobby": "^0.2.14" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-90pSxmZVfbX9e5xCv7tI5RV1mnjdf16y89CJKbf/hD7GyOz1FCxcYMl8ZYA8Hc56dbApTKKmU9HfvgfWdCxlwg=="],
"postcss-nested": ["postcss-nested@7.0.2", "", { "dependencies": { "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw=="],
"postcss-preset-mantine": ["postcss-preset-mantine@1.18.0", "", { "dependencies": { "postcss-mixins": "^12.0.0", "postcss-nested": "^7.0.2" }, "peerDependencies": { "postcss": ">=8.0.0" } }, "sha512-sP6/s1oC7cOtBdl4mw/IRKmKvYTuzpRrH/vT6v9enMU/EQEQ31eQnHcWtFghOXLH87AAthjL/Q75rLmin1oZoA=="],
"postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
"postcss-simple-vars": ["postcss-simple-vars@7.0.1", "", { "peerDependencies": { "postcss": "^8.2.1" } }, "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A=="],
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
"prisma": ["prisma@6.19.2", "", { "dependencies": { "@prisma/config": "6.19.2", "@prisma/engines": "6.19.2" }, "peerDependencies": { "typescript": ">=5.1.0" }, "optionalPeers": ["typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg=="],
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
"proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
"puppeteer-core": ["puppeteer-core@24.40.0", "", { "dependencies": { "@puppeteer/browsers": "2.13.0", "chromium-bidi": "14.0.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1581282", "typed-query-selector": "^2.12.1", "webdriver-bidi-protocol": "0.4.1", "ws": "^8.19.0" } }, "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag=="],
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
"react-icons": ["react-icons@5.6.0", "", { "peerDependencies": { "react": "*" } }, "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA=="],
"react-number-format": ["react-number-format@5.4.5", "", { "peerDependencies": { "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw=="],
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"seroval": ["seroval@1.5.1", "", {}, "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA=="],
"seroval-plugins": ["seroval-plugins@1.5.1", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw=="],
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"streamx": ["streamx@2.25.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="],
"sugarss": ["sugarss@5.0.1", "", { "peerDependencies": { "postcss": "^8.3.3" } }, "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw=="],
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
"tar-fs": ["tar-fs@3.1.2", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw=="],
"tar-stream": ["tar-stream@3.1.8", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ=="],
"teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="],
"text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"typed-query-selector": ["typed-query-selector@2.12.1", "", {}, "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA=="],
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-composed-ref": ["use-composed-ref@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w=="],
"use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA=="],
"use-latest": ["use-latest@1.3.0", "", { "dependencies": { "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.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" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="],
"webdriver-bidi-protocol": ["webdriver-bidi-protocol@0.4.1", "", {}, "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw=="],
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@kitajs/ts-html-plugin/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"degenerator/ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
"escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="],
"readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
"@kitajs/ts-html-plugin/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
"@kitajs/ts-html-plugin/yargs/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"@kitajs/ts-html-plugin/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
"c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"@kitajs/ts-html-plugin/yargs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"@kitajs/ts-html-plugin/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"@kitajs/ts-html-plugin/yargs/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"@kitajs/ts-html-plugin/yargs/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"@kitajs/ts-html-plugin/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"@kitajs/ts-html-plugin/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"@kitajs/ts-html-plugin/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
}
}

2
bunfig.toml Normal file
View File

@@ -0,0 +1,2 @@
[serve.static]
env = "BUN_PUBLIC_*"

69
index.html Normal file
View File

@@ -0,0 +1,69 @@
<!doctype html>
<html lang="id" data-mantine-color-scheme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark" />
<base href="/" />
<link rel="icon" type="image/svg+xml" href="/src/logo.svg" />
<title>My App</title>
<style>
/* Prevent white flash — dark background immediately */
html, body {
margin: 0;
padding: 0;
background-color: #242424;
color: #c1c2c5;
}
/* Splash screen */
#splash {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background-color: #242424;
transition: opacity 0.3s ease;
}
#splash.fade-out {
opacity: 0;
pointer-events: none;
}
.splash-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
.splash-spinner {
width: 40px;
height: 40px;
border: 3px solid #3a3a3a;
border-top-color: #339af0;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.splash-text {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
font-size: 14px;
color: #909296;
letter-spacing: 0.5px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div id="splash">
<div class="splash-content">
<div class="splash-spinner"></div>
<div class="splash-text">Loading...</div>
</div>
</div>
<div id="root"></div>
<script type="module" src="/src/frontend.tsx"></script>
</body>
</html>

51
package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "bun-react-template",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "bun --watch src/serve.ts",
"build": "vite build",
"start": "NODE_ENV=production bun src/index.tsx",
"typecheck": "tsc --noEmit",
"test": "bun test",
"test:unit": "bun test tests/unit",
"test:integration": "bun test tests/integration",
"test:e2e": "bun test tests/e2e",
"lint": "biome check src/",
"lint:fix": "biome check --write src/",
"db:migrate": "bunx prisma migrate dev",
"db:seed": "bun run prisma/seed.ts",
"db:studio": "bunx prisma studio",
"db:generate": "bunx prisma generate",
"db:push": "bunx prisma db push"
},
"dependencies": {
"@elysiajs/cors": "^1.4.1",
"@elysiajs/html": "^1.4.0",
"@mantine/core": "^8.3.18",
"@mantine/hooks": "^8.3.18",
"@prisma/client": "6",
"@tanstack/react-query": "^5.95.2",
"@tanstack/react-router": "^1.168.10",
"elysia": "^1.4.28",
"postcss": "^8.5.8",
"postcss-preset-mantine": "^1.18.0",
"postcss-simple-vars": "^7.0.1",
"react": "^19",
"react-dom": "^19",
"react-icons": "^5.6.0"
},
"devDependencies": {
"@biomejs/biome": "^2.4.10",
"@tanstack/router-vite-plugin": "^1.166.27",
"@types/bun": "latest",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "^6.0.1",
"prisma": "6",
"puppeteer-core": "^24.40.0",
"typescript": "^6.0.2",
"vite": "^8.0.3"
}
}

14
postcss.config.js Normal file
View File

@@ -0,0 +1,14 @@
export default {
plugins: {
'postcss-preset-mantine': {},
'postcss-simple-vars': {
variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
},
},
},
}

View File

@@ -0,0 +1,34 @@
-- CreateTable
CREATE TABLE "user" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "session" (
"id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
-- CreateIndex
CREATE UNIQUE INDEX "session_token_key" ON "session"("token");
-- CreateIndex
CREATE INDEX "session_token_idx" ON "session"("token");
-- AddForeignKey
ALTER TABLE "session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN', 'SUPER_ADMIN');
-- AlterTable
ALTER TABLE "user" ADD COLUMN "role" "Role" NOT NULL DEFAULT 'USER';

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

42
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,42 @@
generator client {
provider = "prisma-client-js"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
USER
ADMIN
SUPER_ADMIN
}
model User {
id String @id @default(uuid())
name String
email String @unique
password String
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
@@map("user")
}
model Session {
id String @id @default(uuid())
token String @unique
userId String
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([token])
@@map("session")
}

39
prisma/seed.ts Normal file
View File

@@ -0,0 +1,39 @@
import { PrismaClient } from '../generated/prisma'
const prisma = new PrismaClient()
const SUPER_ADMIN_EMAILS = (process.env.SUPER_ADMIN_EMAIL ?? '').split(',').map(e => e.trim()).filter(Boolean)
async function main() {
const users = [
{ name: 'Super Admin', email: 'superadmin@example.com', password: 'superadmin123', role: 'SUPER_ADMIN' as const },
{ name: 'Admin', email: 'admin@example.com', password: 'admin123', role: 'ADMIN' as const },
{ name: 'User', email: 'user@example.com', password: 'user123', role: 'USER' as const },
]
for (const u of users) {
const hashed = await Bun.password.hash(u.password, { algorithm: 'bcrypt' })
await prisma.user.upsert({
where: { email: u.email },
update: { name: u.name, password: hashed, role: u.role },
create: { name: u.name, email: u.email, password: hashed, role: u.role },
})
console.log(`Seeded: ${u.email} (${u.role})`)
}
// Promote super admin emails from env
for (const email of SUPER_ADMIN_EMAILS) {
const user = await prisma.user.findUnique({ where: { email } })
if (user && user.role !== 'SUPER_ADMIN') {
await prisma.user.update({ where: { email }, data: { role: 'SUPER_ADMIN' } })
console.log(`Promoted to SUPER_ADMIN: ${email}`)
}
}
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(() => prisma.$disconnect())

158
src/app.ts Normal file
View File

@@ -0,0 +1,158 @@
import { cors } from '@elysiajs/cors'
import { html } from '@elysiajs/html'
import { Elysia } from 'elysia'
import { prisma } from './lib/db'
import { env } from './lib/env'
export function createApp() {
return new Elysia()
.use(cors())
.use(html())
// ─── Global Error Handler ────────────────────────
.onError(({ code, error }) => {
if (code === 'NOT_FOUND') {
return new Response(JSON.stringify({ error: 'Not Found', status: 404 }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
})
}
console.error('[Server Error]', error)
return new Response(JSON.stringify({ error: 'Internal Server Error', status: 500 }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
})
})
// API routes
.get('/health', () => ({ status: 'ok' }))
// ─── Auth API ──────────────────────────────────────
.post('/api/auth/login', async ({ request, set }) => {
const { email, password } = (await request.json()) as { email: string; password: string }
let user = await prisma.user.findUnique({ where: { email } })
if (!user || !(await Bun.password.verify(password, user.password))) {
set.status = 401
return { error: 'Email atau password salah' }
}
// Auto-promote super admin from env
if (env.SUPER_ADMIN_EMAILS.includes(user.email) && user.role !== 'SUPER_ADMIN') {
user = await prisma.user.update({ where: { id: user.id }, data: { role: 'SUPER_ADMIN' } })
}
const token = crypto.randomUUID()
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
await prisma.session.create({ data: { token, userId: user.id, expiresAt } })
set.headers['set-cookie'] = `session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400`
return { user: { id: user.id, name: user.name, email: user.email, role: user.role } }
})
.post('/api/auth/logout', async ({ request, set }) => {
const cookie = request.headers.get('cookie') ?? ''
const token = cookie.match(/session=([^;]+)/)?.[1]
if (token) await prisma.session.deleteMany({ where: { token } })
set.headers['set-cookie'] = 'session=; Path=/; HttpOnly; Max-Age=0'
return { ok: true }
})
.get('/api/auth/session', async ({ request, set }) => {
const cookie = request.headers.get('cookie') ?? ''
const token = cookie.match(/session=([^;]+)/)?.[1]
if (!token) { set.status = 401; return { user: null } }
const session = await prisma.session.findUnique({
where: { token },
include: { user: { select: { id: true, name: true, email: true, role: true } } },
})
if (!session || session.expiresAt < new Date()) {
if (session) await prisma.session.delete({ where: { id: session.id } })
set.status = 401
return { user: null }
}
return { user: session.user }
})
// ─── Google OAuth ──────────────────────────────────
.get('/api/auth/google', ({ request, set }) => {
const origin = new URL(request.url).origin
const params = new URLSearchParams({
client_id: env.GOOGLE_CLIENT_ID,
redirect_uri: `${origin}/api/auth/callback/google`,
response_type: 'code',
scope: 'openid email profile',
access_type: 'offline',
prompt: 'consent',
})
set.status = 302; set.headers['location'] =`https://accounts.google.com/o/oauth2/v2/auth?${params}`
})
.get('/api/auth/callback/google', async ({ request, set }) => {
const url = new URL(request.url)
const code = url.searchParams.get('code')
const origin = url.origin
if (!code) {
set.status = 302; set.headers['location'] ='/login?error=google_failed'
return
}
// Exchange code for tokens
const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: env.GOOGLE_CLIENT_ID,
client_secret: env.GOOGLE_CLIENT_SECRET,
redirect_uri: `${origin}/api/auth/callback/google`,
grant_type: 'authorization_code',
}),
})
if (!tokenRes.ok) {
set.status = 302; set.headers['location'] ='/login?error=google_failed'
return
}
const tokens = (await tokenRes.json()) as { access_token: string }
// Get user info
const userInfoRes = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${tokens.access_token}` },
})
if (!userInfoRes.ok) {
set.status = 302; set.headers['location'] ='/login?error=google_failed'
return
}
const googleUser = (await userInfoRes.json()) as { email: string; name: string }
// Upsert user (no password for Google users)
const isSuperAdmin = env.SUPER_ADMIN_EMAILS.includes(googleUser.email)
const user = await prisma.user.upsert({
where: { email: googleUser.email },
update: { name: googleUser.name, ...(isSuperAdmin ? { role: 'SUPER_ADMIN' } : {}) },
create: { email: googleUser.email, name: googleUser.name, password: '', role: isSuperAdmin ? 'SUPER_ADMIN' : 'USER' },
})
// Create session
const token = crypto.randomUUID()
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000)
await prisma.session.create({ data: { token, userId: user.id, expiresAt } })
set.headers['set-cookie'] = `session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400`
set.status = 302; set.headers['location'] = user.role === 'SUPER_ADMIN' ? '/dashboard' : '/profile'
})
// ─── Example API ───────────────────────────────────
.get('/api/hello', () => ({
message: 'Hello, world!',
method: 'GET',
}))
.put('/api/hello', () => ({
message: 'Hello, world!',
method: 'PUT',
}))
.get('/api/hello/:name', ({ params }) => ({
message: `Hello, ${params.name}!`,
}))
}

34
src/frontend.tsx Normal file
View File

@@ -0,0 +1,34 @@
import type { ReactNode } from 'react'
import { createRoot } from 'react-dom/client'
import { App } from './frontend/App'
// DevInspector hanya di-import saat dev (tree-shaken di production)
const InspectorWrapper = import.meta.env?.DEV
? (await import('./frontend/DevInspector')).DevInspector
: ({ children }: { children: ReactNode }) => <>{children}</>
// Remove splash screen after React mounts
function removeSplash() {
const splash = document.getElementById('splash')
if (splash) {
splash.classList.add('fade-out')
setTimeout(() => splash.remove(), 300)
}
}
const elem = document.getElementById('root')!
const app = (
<InspectorWrapper>
<App />
</InspectorWrapper>
)
// HMR-safe: reuse root agar React state preserved saat hot reload
if (import.meta.hot) {
import.meta.hot.data.root ??= createRoot(elem)
import.meta.hot.data.root.render(app)
} else {
createRoot(elem).render(app)
}
removeSplash()

40
src/frontend/App.tsx Normal file
View File

@@ -0,0 +1,40 @@
import { ColorSchemeScript, MantineProvider, createTheme } from '@mantine/core'
import '@mantine/core/styles.css'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const theme = createTheme({
primaryColor: 'blue',
fontFamily: 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif',
})
const queryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 30_000, retry: 1 },
},
})
const router = createRouter({
routeTree,
context: { queryClient },
})
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
export function App() {
return (
<>
<ColorSchemeScript defaultColorScheme="dark" />
<MantineProvider theme={theme} defaultColorScheme="dark" forceColorScheme="dark">
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</MantineProvider>
</>
)
}

View File

@@ -0,0 +1,157 @@
import { useCallback, useEffect, useRef, useState } from 'react'
interface CodeInfo {
relativePath: string
line: string
column: string
}
/** Walk up DOM tree, cari elemen dengan data-inspector-* attributes */
function findCodeInfo(target: HTMLElement): { element: HTMLElement; info: CodeInfo } | null {
let el: HTMLElement | null = target
while (el) {
const relativePath = el.getAttribute('data-inspector-relative-path')
const line = el.getAttribute('data-inspector-line')
const column = el.getAttribute('data-inspector-column')
if (relativePath && line) {
return {
element: el,
info: { relativePath, line, column: column ?? '1' },
}
}
el = el.parentElement
}
return null
}
function openInEditor(info: CodeInfo) {
fetch('/__open-in-editor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
relativePath: info.relativePath,
lineNumber: info.line,
columnNumber: info.column,
}),
})
}
export function DevInspector({ children }: { children: React.ReactNode }) {
const [active, setActive] = useState(false)
const overlayRef = useRef<HTMLDivElement | null>(null)
const tooltipRef = useRef<HTMLDivElement | null>(null)
const lastInfoRef = useRef<CodeInfo | null>(null)
const updateOverlay = useCallback((target: HTMLElement | null) => {
const ov = overlayRef.current
const tt = tooltipRef.current
if (!ov || !tt) return
if (!target) {
ov.style.display = 'none'
tt.style.display = 'none'
lastInfoRef.current = null
return
}
const result = findCodeInfo(target)
if (!result) {
ov.style.display = 'none'
tt.style.display = 'none'
lastInfoRef.current = null
return
}
lastInfoRef.current = result.info
const rect = result.element.getBoundingClientRect()
ov.style.display = 'block'
ov.style.top = `${rect.top + window.scrollY}px`
ov.style.left = `${rect.left + window.scrollX}px`
ov.style.width = `${rect.width}px`
ov.style.height = `${rect.height}px`
tt.style.display = 'block'
tt.textContent = `${result.info.relativePath}:${result.info.line}`
const ttTop = rect.top + window.scrollY - 24
tt.style.top = `${ttTop > 0 ? ttTop : rect.bottom + window.scrollY + 4}px`
tt.style.left = `${rect.left + window.scrollX}px`
}, [])
useEffect(() => {
if (!active) return
const onMouseOver = (e: MouseEvent) => updateOverlay(e.target as HTMLElement)
const onClick = (e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
const result = findCodeInfo(e.target as HTMLElement)
const info = result?.info ?? lastInfoRef.current
if (info) {
const loc = `${info.relativePath}:${info.line}:${info.column}`
navigator.clipboard.writeText(loc).catch(() => {})
openInEditor(info)
}
setActive(false)
}
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') setActive(false)
}
document.addEventListener('mouseover', onMouseOver, true)
document.addEventListener('click', onClick, true)
document.addEventListener('keydown', onKeyDown)
document.body.style.cursor = 'crosshair'
return () => {
document.removeEventListener('mouseover', onMouseOver, true)
document.removeEventListener('click', onClick, true)
document.removeEventListener('keydown', onKeyDown)
document.body.style.cursor = ''
if (overlayRef.current) overlayRef.current.style.display = 'none'
if (tooltipRef.current) tooltipRef.current.style.display = 'none'
}
}, [active, updateOverlay])
// Hotkey: Ctrl+Shift+Cmd+C (macOS) / Ctrl+Shift+Alt+C (Windows/Linux)
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key.toLowerCase() === 'c' && e.ctrlKey && e.shiftKey && (e.metaKey || e.altKey)) {
e.preventDefault()
setActive((prev) => !prev)
}
}
document.addEventListener('keydown', onKeyDown)
return () => document.removeEventListener('keydown', onKeyDown)
}, [])
return (
<>
{children}
<div
ref={overlayRef}
style={{
display: 'none',
position: 'absolute',
pointerEvents: 'none',
border: '2px solid #3b82f6',
backgroundColor: 'rgba(59,130,246,0.1)',
zIndex: 99999,
transition: 'all 0.05s ease',
}}
/>
<div
ref={tooltipRef}
style={{
display: 'none',
position: 'absolute',
pointerEvents: 'none',
backgroundColor: '#1e293b',
color: '#e2e8f0',
fontSize: '12px',
fontFamily: 'monospace',
padding: '2px 6px',
borderRadius: '3px',
zIndex: 100000,
whiteSpace: 'nowrap',
}}
/>
</>
)
}

View File

@@ -0,0 +1,26 @@
import { Button, Center, Code, Stack, Text, Title } from '@mantine/core'
import { TbAlertTriangle, TbRefresh } from 'react-icons/tb'
export function ErrorPage({ error }: { error: unknown }) {
const message = error instanceof Error ? error.message : 'Terjadi kesalahan yang tidak terduga'
return (
<Center mih="100vh">
<Stack align="center" gap="md" maw={500}>
<TbAlertTriangle size={80} color="var(--mantine-color-red-6)" />
<Title order={1}>500</Title>
<Text c="dimmed" size="lg" ta="center">Terjadi kesalahan pada server</Text>
<Code block c="red" style={{ maxWidth: '100%', wordBreak: 'break-word' }}>
{message}
</Code>
<Button
onClick={() => window.location.reload()}
leftSection={<TbRefresh size={18} />}
variant="light"
>
Muat Ulang
</Button>
</Stack>
</Center>
)
}

View File

@@ -0,0 +1,18 @@
import { Button, Center, Stack, Text, Title } from '@mantine/core'
import { Link } from '@tanstack/react-router'
import { TbArrowLeft, TbError404 } from 'react-icons/tb'
export function NotFound() {
return (
<Center mih="100vh">
<Stack align="center" gap="md">
<TbError404 size={80} color="var(--mantine-color-dimmed)" />
<Title order={1}>404</Title>
<Text c="dimmed" size="lg">Halaman tidak ditemukan</Text>
<Button component={Link} to="/" leftSection={<TbArrowLeft size={18} />} variant="light">
Kembali ke Beranda
</Button>
</Stack>
</Center>
)
}

View File

@@ -0,0 +1,66 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from '@tanstack/react-router'
export type Role = 'USER' | 'ADMIN' | 'SUPER_ADMIN'
export interface User {
id: string
name: string
email: string
role: Role
}
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(path, { credentials: 'include', ...init })
if (!res.ok) {
const err = await res.json().catch(() => ({ error: 'Request failed' }))
throw new Error(err.error || `HTTP ${res.status}`)
}
return res.json()
}
export function useSession() {
return useQuery({
queryKey: ['auth', 'session'],
queryFn: () => apiFetch<{ user: User | null }>('/api/auth/session'),
retry: false,
staleTime: 30_000,
})
}
export function useLogin() {
const queryClient = useQueryClient()
const navigate = useNavigate()
return useMutation({
mutationFn: (data: { email: string; password: string }) =>
apiFetch<{ user: User }>('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}),
onSuccess: (data) => {
queryClient.setQueryData(['auth', 'session'], data)
// Super admin → dashboard, others → profile
if (data.user.role === 'SUPER_ADMIN') {
navigate({ to: '/dashboard' })
} else {
navigate({ to: '/profile' })
}
},
})
}
export function useLogout() {
const queryClient = useQueryClient()
const navigate = useNavigate()
return useMutation({
mutationFn: () =>
apiFetch<{ ok: boolean }>('/api/auth/logout', { method: 'POST' }),
onSuccess: () => {
queryClient.setQueryData(['auth', 'session'], { user: null })
navigate({ to: '/login' })
},
})
}

View File

@@ -0,0 +1,14 @@
import { Outlet, createRootRouteWithContext } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
import { NotFound } from '@/frontend/components/NotFound'
import { ErrorPage } from '@/frontend/components/ErrorPage'
interface RouterContext {
queryClient: QueryClient
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: () => <Outlet />,
notFoundComponent: NotFound,
errorComponent: ({ error }) => <ErrorPage error={error} />,
})

View File

@@ -0,0 +1,94 @@
import {
Avatar,
Badge,
Button,
Card,
Container,
Group,
Paper,
SimpleGrid,
Stack,
Text,
ThemeIcon,
Title,
} from '@mantine/core'
import { createFileRoute, redirect } from '@tanstack/react-router'
import { TbChartBar, TbLogout, TbSettings, TbUsers } from 'react-icons/tb'
import { useLogout, useSession } from '@/frontend/hooks/useAuth'
export const Route = createFileRoute('/dashboard')({
beforeLoad: async ({ context }) => {
try {
const data = await context.queryClient.ensureQueryData({
queryKey: ['auth', 'session'],
queryFn: () => fetch('/api/auth/session', { credentials: 'include' }).then((r) => r.json()),
})
if (!data?.user) throw redirect({ to: '/login' })
if (data.user.role !== 'SUPER_ADMIN') throw redirect({ to: '/profile' })
} catch (e) {
if (e instanceof Error) throw redirect({ to: '/login' })
throw e
}
},
component: DashboardPage,
})
const stats = [
{ title: 'Users', value: '1,234', icon: TbUsers, color: 'blue' },
{ title: 'Revenue', value: '$12.4k', icon: TbChartBar, color: 'green' },
{ title: 'Settings', value: '3 active', icon: TbSettings, color: 'violet' },
]
function DashboardPage() {
const { data } = useSession()
const logout = useLogout()
const user = data?.user
return (
<Container size="md" py="xl">
<Stack gap="xl">
<Group justify="space-between">
<Title order={2}>Dashboard</Title>
<Button
variant="light"
color="red"
leftSection={<TbLogout size={16} />}
onClick={() => logout.mutate()}
loading={logout.isPending}
>
Logout
</Button>
</Group>
<Paper withBorder p="lg" radius="md">
<Group>
<Avatar color="blue" radius="xl" size="lg">
{user?.name?.charAt(0).toUpperCase()}
</Avatar>
<div>
<Group gap="xs">
<Text fw={500}>{user?.name}</Text>
<Badge color="red" variant="light" size="sm">SUPER ADMIN</Badge>
</Group>
<Text c="dimmed" size="sm">{user?.email}</Text>
</div>
</Group>
</Paper>
<SimpleGrid cols={{ base: 1, sm: 3 }}>
{stats.map((stat) => (
<Card key={stat.title} withBorder padding="lg" radius="md">
<Group justify="space-between" mb="xs">
<Text size="sm" c="dimmed" fw={500}>{stat.title}</Text>
<ThemeIcon variant="light" color={stat.color} size="sm">
<stat.icon size={14} />
</ThemeIcon>
</Group>
<Text fw={700} size="xl">{stat.value}</Text>
</Card>
))}
</SimpleGrid>
</Stack>
</Container>
)
}

View File

@@ -0,0 +1,36 @@
import { Button, Container, Group, Stack, Text, Title } from '@mantine/core'
import { Link, createFileRoute } from '@tanstack/react-router'
import { SiBun } from 'react-icons/si'
import { TbBrandReact, TbLogin, TbRocket } from 'react-icons/tb'
export const Route = createFileRoute('/')({
component: HomePage,
})
function HomePage() {
return (
<Container size="sm" py="xl">
<Stack align="center" gap="lg">
<Group gap="lg">
<SiBun size={64} color="#fbf0df" />
<TbBrandReact size={64} color="#61dafb" />
</Group>
<Title order={1}>Bun + Elysia + Vite + React</Title>
<Text c="dimmed" ta="center" maw={480}>
Full-stack starter template with Mantine UI, TanStack Router, and session-based auth.
</Text>
<Group>
<Button component={Link} to="/login" leftSection={<TbLogin size={18} />} variant="filled">
Login
</Button>
<Button component={Link} to="/dashboard" leftSection={<TbRocket size={18} />} variant="light">
Dashboard
</Button>
</Group>
</Stack>
</Container>
)
}

View File

@@ -0,0 +1,115 @@
import {
Alert,
Button,
Center,
Divider,
Paper,
PasswordInput,
Stack,
Text,
TextInput,
Title,
} from '@mantine/core'
import { createFileRoute, redirect } from '@tanstack/react-router'
import { useState } from 'react'
import { FcGoogle } from 'react-icons/fc'
import { TbAlertCircle, TbLogin, TbLock, TbMail } from 'react-icons/tb'
import { useLogin } from '@/frontend/hooks/useAuth'
export const Route = createFileRoute('/login')({
validateSearch: (search: Record<string, unknown>) => ({
error: (search.error as string) || undefined,
}),
beforeLoad: async ({ context }) => {
try {
const data = await context.queryClient.ensureQueryData({
queryKey: ['auth', 'session'],
queryFn: () => fetch('/api/auth/session', { credentials: 'include' }).then((r) => r.json()),
})
if (data?.user) {
throw redirect({ to: data.user.role === 'SUPER_ADMIN' ? '/dashboard' : '/profile' })
}
} catch (e) {
if (e instanceof Error) return
throw e
}
},
component: LoginPage,
})
function LoginPage() {
const login = useLogin()
const { error: searchError } = Route.useSearch()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
login.mutate({ email, password })
}
return (
<Center mih="100vh">
<Paper shadow="md" p="xl" radius="md" w={400} withBorder>
<form onSubmit={handleSubmit}>
<Stack gap="md">
<Title order={2} ta="center">
Login
</Title>
<Text c="dimmed" size="sm" ta="center">
Demo: <strong>superadmin@example.com</strong> / <strong>superadmin123</strong>
<br />
atau: <strong>user@example.com</strong> / <strong>user123</strong>
</Text>
{(login.isError || searchError) && (
<Alert icon={<TbAlertCircle size={16} />} color="red" variant="light">
{login.isError ? login.error.message : 'Login dengan Google gagal, coba lagi.'}
</Alert>
)}
<TextInput
label="Email"
placeholder="email@example.com"
leftSection={<TbMail size={16} />}
value={email}
onChange={(e) => setEmail(e.currentTarget.value)}
required
/>
<PasswordInput
label="Password"
placeholder="Password"
leftSection={<TbLock size={16} />}
value={password}
onChange={(e) => setPassword(e.currentTarget.value)}
required
/>
<Button
type="submit"
fullWidth
leftSection={<TbLogin size={18} />}
loading={login.isPending}
>
Sign in
</Button>
<Divider label="atau" labelPosition="center" />
<Button
component="a"
href="/api/auth/google"
fullWidth
variant="default"
leftSection={<FcGoogle size={18} />}
>
Login dengan Google
</Button>
</Stack>
</form>
</Paper>
</Center>
)
}

View File

@@ -0,0 +1,97 @@
import {
Avatar,
Badge,
Button,
Container,
Group,
Paper,
Stack,
Text,
Title,
} from '@mantine/core'
import { createFileRoute, redirect } from '@tanstack/react-router'
import { TbLogout, TbUser } from 'react-icons/tb'
import { useLogout, useSession } from '@/frontend/hooks/useAuth'
export const Route = createFileRoute('/profile')({
beforeLoad: async ({ context }) => {
try {
const data = await context.queryClient.ensureQueryData({
queryKey: ['auth', 'session'],
queryFn: () => fetch('/api/auth/session', { credentials: 'include' }).then((r) => r.json()),
})
if (!data?.user) throw redirect({ to: '/login' })
} catch (e) {
if (e instanceof Error) throw redirect({ to: '/login' })
throw e
}
},
component: ProfilePage,
})
const roleBadgeColor: Record<string, string> = {
USER: 'blue',
ADMIN: 'violet',
SUPER_ADMIN: 'red',
}
function ProfilePage() {
const { data } = useSession()
const logout = useLogout()
const user = data?.user
return (
<Container size="sm" py="xl">
<Stack gap="xl">
<Group justify="space-between">
<Title order={2}>Profile</Title>
<Button
variant="light"
color="red"
leftSection={<TbLogout size={16} />}
onClick={() => logout.mutate()}
loading={logout.isPending}
>
Logout
</Button>
</Group>
<Paper withBorder p="xl" radius="md">
<Stack align="center" gap="md">
<Avatar color="blue" radius="xl" size={80}>
{user?.name?.charAt(0).toUpperCase()}
</Avatar>
<div style={{ textAlign: 'center' }}>
<Text fw={600} size="lg">{user?.name}</Text>
<Text c="dimmed" size="sm">{user?.email}</Text>
</div>
<Badge color={roleBadgeColor[user?.role ?? 'USER']} variant="light" size="lg">
{user?.role}
</Badge>
</Stack>
</Paper>
<Paper withBorder p="lg" radius="md">
<Stack gap="sm">
<Group gap="xs">
<TbUser size={16} />
<Text fw={500} size="sm">Account Info</Text>
</Group>
<Group justify="space-between">
<Text size="sm" c="dimmed">Name</Text>
<Text size="sm">{user?.name}</Text>
</Group>
<Group justify="space-between">
<Text size="sm" c="dimmed">Email</Text>
<Text size="sm">{user?.email}</Text>
</Group>
<Group justify="space-between">
<Text size="sm" c="dimmed">Role</Text>
<Text size="sm">{user?.role}</Text>
</Group>
</Stack>
</Paper>
</Stack>
</Container>
)
}

187
src/index.css Normal file
View File

@@ -0,0 +1,187 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
}
body {
margin: 0;
display: grid;
place-items: center;
min-width: 320px;
min-height: 100vh;
position: relative;
}
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
opacity: 0.05;
background: url("./logo.svg");
background-size: 256px;
transform: rotate(-12deg) scale(1.35);
animation: slide 30s linear infinite;
pointer-events: none;
}
@keyframes slide {
from {
background-position: 0 0;
}
to {
background-position: 256px 224px;
}
}
.app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
position: relative;
z-index: 1;
}
.logo-container {
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
margin-bottom: 2rem;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 0.3s;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.bun-logo {
transform: scale(1.2);
}
.bun-logo:hover {
filter: drop-shadow(0 0 2em #fbf0dfaa);
}
.react-logo {
animation: spin 20s linear infinite;
}
.react-logo:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes spin {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
code {
background-color: #1a1a1a;
padding: 0.2em 0.4em;
border-radius: 0.3em;
font-family: monospace;
}
.api-tester {
margin: 2rem auto 0;
width: 100%;
max-width: 600px;
text-align: left;
display: flex;
flex-direction: column;
gap: 1rem;
}
.endpoint-row {
display: flex;
align-items: center;
gap: 0.5rem;
background: #1a1a1a;
padding: 0.75rem;
border-radius: 12px;
font: monospace;
border: 2px solid #fbf0df;
transition: 0.3s;
width: 100%;
box-sizing: border-box;
}
.endpoint-row:focus-within {
border-color: #f3d5a3;
}
.method {
background: #fbf0df;
color: #1a1a1a;
padding: 0.3rem 0.7rem;
border-radius: 8px;
font-weight: 700;
font-size: 0.9em;
appearance: none;
margin: 0;
width: min-content;
display: block;
flex-shrink: 0;
border: none;
}
.method option {
text-align: left;
}
.url-input {
width: 100%;
flex: 1;
background: 0;
border: 0;
color: #fbf0df;
font: 1em monospace;
padding: 0.2rem;
outline: 0;
}
.url-input:focus {
color: #fff;
}
.url-input::placeholder {
color: rgba(251, 240, 223, 0.4);
}
.send-button {
background: #fbf0df;
color: #1a1a1a;
border: 0;
padding: 0.4rem 1.2rem;
border-radius: 8px;
font-weight: 700;
transition: 0.1s;
cursor: var(--bun-cursor);
}
.send-button:hover {
background: #f3d5a3;
transform: translateY(-1px);
cursor: pointer;
}
.response-area {
width: 100%;
min-height: 120px;
background: #1a1a1a;
border: 2px solid #fbf0df;
border-radius: 12px;
padding: 0.75rem;
color: #fbf0df;
font: monospace;
resize: vertical;
box-sizing: border-box;
}
.response-area:focus {
border-color: #f3d5a3;
}
.response-area::placeholder {
color: rgba(251, 240, 223, 0.4);
}
@media (prefers-reduced-motion) {
*,
::before,
::after {
animation: none !important;
}
}

177
src/index.tsx Normal file
View File

@@ -0,0 +1,177 @@
/// <reference types="bun-types" />
import fs from 'node:fs'
import path from 'node:path'
import { env } from './lib/env'
const isProduction = env.NODE_ENV === 'production'
// ─── Route Classification ──────────────────────────────
const API_PREFIXES = ['/api/', '/webhook/', '/ws/', '/health']
function isApiRoute(pathname: string): boolean {
return API_PREFIXES.some((p) => pathname.startsWith(p)) || pathname === '/health'
}
// ─── Vite Dev Server (dev only) ────────────────────────
let vite: Awaited<ReturnType<typeof import('./vite').createVite>> | null = null
if (!isProduction) {
const { createVite } = await import('./vite')
vite = await createVite()
}
// ─── Frontend Serving ──────────────────────────────────
async function serveFrontend(request: Request): Promise<Response> {
const url = new URL(request.url)
const pathname = url.pathname
if (!isProduction && vite) {
// === DEVELOPMENT: Vite Middleware Mode ===
// SPA route → serve index.html via Vite transform
if (
pathname === '/' ||
(!pathname.includes('.') &&
!pathname.startsWith('/@') &&
!pathname.startsWith('/__open-stack-frame-in-editor'))
) {
const htmlPath = path.resolve('index.html')
let htmlContent = fs.readFileSync(htmlPath, 'utf-8')
htmlContent = await vite.transformIndexHtml(pathname, htmlContent)
// Dedupe: Vite 8 middlewareMode injects react-refresh preamble twice
const preamble =
'<script type="module">import { injectIntoGlobalHook } from "/@react-refresh";\ninjectIntoGlobalHook(window);\nwindow.$RefreshReg$ = () => {};\nwindow.$RefreshSig$ = () => (type) => type;</script>'
const firstIdx = htmlContent.indexOf(preamble)
if (firstIdx !== -1) {
const secondIdx = htmlContent.indexOf(preamble, firstIdx + preamble.length)
if (secondIdx !== -1) {
htmlContent = htmlContent.slice(0, secondIdx) + htmlContent.slice(secondIdx + preamble.length)
}
}
return new Response(htmlContent, {
headers: { 'Content-Type': 'text/html' },
})
}
// Asset/module requests → proxy ke Vite middleware
// Bridge: Bun Request → Node.js IncomingMessage/ServerResponse
return new Promise<Response>((resolve) => {
const req = new Proxy(request, {
get(target, prop) {
if (prop === 'url') return pathname + url.search
if (prop === 'method') return request.method
if (prop === 'headers') return Object.fromEntries(request.headers as any)
return (target as any)[prop]
},
}) as any
const chunks: (Buffer | Uint8Array)[] = []
const res = {
statusCode: 200,
headers: {} as Record<string, string>,
setHeader(name: string, value: string) { this.headers[name.toLowerCase()] = value; return this },
getHeader(name: string) { return this.headers[name.toLowerCase()] },
removeHeader(name: string) { delete this.headers[name.toLowerCase()] },
writeHead(code: number, reasonOrHeaders?: string | Record<string, string>, maybeHeaders?: Record<string, string>) {
this.statusCode = code
const hdrs = typeof reasonOrHeaders === 'object' ? reasonOrHeaders : maybeHeaders
if (hdrs) for (const [k, v] of Object.entries(hdrs)) this.headers[k.toLowerCase()] = String(v)
return this
},
write(chunk: any) {
if (chunk) chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
return true
},
end(data?: any) {
if (data) {
if (typeof data === 'string') chunks.push(Buffer.from(data))
else if (data instanceof Uint8Array || Buffer.isBuffer(data)) chunks.push(data)
}
resolve(new Response(
chunks.length > 0 ? Buffer.concat(chunks) : null,
{ status: this.statusCode, headers: this.headers },
))
},
once() { return this },
on() { return this },
emit() { return this },
removeListener() { return this },
} as any
vite.middlewares(req, res, (err: any) => {
if (err) {
resolve(new Response(err.stack || err.toString(), { status: 500 }))
return
}
resolve(new Response('Not Found', { status: 404 }))
})
})
}
// === PRODUCTION: Static Files + SPA Fallback ===
const filePath = path.join('dist', pathname === '/' ? 'index.html' : pathname)
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
const ext = path.extname(filePath)
const contentType: Record<string, string> = {
'.js': 'application/javascript', '.css': 'text/css',
'.html': 'text/html; charset=utf-8', '.json': 'application/json',
'.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon',
}
const isHashed = pathname.startsWith('/assets/')
return new Response(Bun.file(filePath), {
headers: {
'Content-Type': contentType[ext] ?? 'application/octet-stream',
'Cache-Control': isHashed ? 'public, max-age=31536000, immutable' : 'public, max-age=3600',
},
})
}
// SPA fallback — semua route yang tidak match file → index.html
const indexHtml = path.join('dist', 'index.html')
if (fs.existsSync(indexHtml)) {
return new Response(Bun.file(indexHtml), {
headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' },
})
}
return new Response('Not Found', { status: 404 })
}
// ─── Elysia App ────────────────────────────────────────
import { createApp } from './app'
const app = createApp()
// Frontend intercept — onRequest jalan SEBELUM route matching
.onRequest(async ({ request }) => {
const pathname = new URL(request.url).pathname
// Dev inspector: open file di editor
if (!isProduction && pathname === '/__open-in-editor' && request.method === 'POST') {
const { relativePath, lineNumber, columnNumber } = (await request.json()) as {
relativePath: string; lineNumber: string; columnNumber: string
}
const file = `${process.cwd()}/${relativePath}`
const editor = env.REACT_EDITOR
const loc = `${file}:${lineNumber}:${columnNumber}`
// zed & subl: editor file:line:col — code & cursor: editor --goto file:line:col
const noGotoEditors = ['subl', 'zed']
const args = noGotoEditors.includes(editor) ? [loc] : ['--goto', loc]
const editorPath = Bun.which(editor)
if (editorPath) Bun.spawn([editor, ...args], { stdio: ['ignore', 'ignore', 'ignore'] })
return new Response('ok')
}
// Non-API route → serve frontend
if (!isApiRoute(pathname)) {
return serveFrontend(request)
}
// undefined → lanjut ke Elysia route matching
})
.listen(env.PORT)
console.log(`Server running at http://localhost:${app.server!.port}`)

13
src/lib/db.ts Normal file
View File

@@ -0,0 +1,13 @@
import { PrismaClient } from '../../generated/prisma'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['warn', 'error'] : ['error'],
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}

19
src/lib/env.ts Normal file
View File

@@ -0,0 +1,19 @@
function optional(key: string, fallback: string): string {
return process.env[key] ?? fallback
}
function required(key: string): string {
const value = process.env[key]
if (!value) throw new Error(`Missing required environment variable: ${key}`)
return value
}
export const env = {
PORT: parseInt(optional('PORT', '3000'), 10),
NODE_ENV: optional('NODE_ENV', 'development'),
REACT_EDITOR: optional('REACT_EDITOR', 'code'),
DATABASE_URL: required('DATABASE_URL'),
GOOGLE_CLIENT_ID: required('GOOGLE_CLIENT_ID'),
GOOGLE_CLIENT_SECRET: required('GOOGLE_CLIENT_SECRET'),
SUPER_ADMIN_EMAILS: optional('SUPER_ADMIN_EMAIL', '').split(',').map(e => e.trim()).filter(Boolean),
} as const

1
src/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg id="Bun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 70"><title>Bun Logo</title><path id="Shadow" d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"/><g id="Body"><path id="Background" d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z" style="fill:#fbf0df"/><path id="Bottom_Shadow" data-name="Bottom Shadow" d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z" style="fill:#f6dece"/><path id="Light_Shine" data-name="Light Shine" d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z" style="fill:#fffefc"/><path id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" style="fill:#ccbea7;fill-rule:evenodd"/><path id="Outline" d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"/></g><g id="Mouth"><g id="Background-2" data-name="Background"><path d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z" style="fill:#b71422"/></g><g id="Tongue"><path id="Background-3" data-name="Background" d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z" style="fill:#ff6164"/><path id="Outline-2" data-name="Outline" d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"/></g><path id="Outline-3" data-name="Outline" d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"/></g><g id="Face"><ellipse id="Right_Blush" data-name="Right Blush" cx="53.22" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><ellipse id="Left_Bluch" data-name="Left Bluch" cx="22.95" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><path id="Eyes" d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z" style="fill-rule:evenodd"/><path id="Iris" d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z" style="fill:#fff;fill-rule:evenodd"/></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

8
src/react.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-11.5 -10.23174 23 20.46348">
<circle cx="0" cy="0" r="2.05" fill="#61dafb"/>
<g stroke="#61dafb" stroke-width="1" fill="none">
<ellipse rx="11" ry="4.2"/>
<ellipse rx="11" ry="4.2" transform="rotate(60)"/>
<ellipse rx="11" ry="4.2" transform="rotate(120)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 338 B

3
src/serve.ts Normal file
View File

@@ -0,0 +1,3 @@
// Workaround: Bun 1.3.6 EADDRINUSE errno:0
// Dynamic import memberi waktu OS release port sebelum binding
import('./index.tsx')

180
src/vite.ts Normal file
View File

@@ -0,0 +1,180 @@
import fs from 'node:fs'
import path from 'node:path'
import { TanStackRouterVite } from '@tanstack/router-vite-plugin'
import react from '@vitejs/plugin-react'
import type { Plugin } from 'vite'
import { createServer as createViteServer } from 'vite'
/**
* Custom Vite plugin: inject data-inspector-* attributes ke JSX via regex.
* enforce: "pre" → jalan SEBELUM OXC transform JSX.
*
* Karena plugin lain (OXC, TanStack) bisa mengubah source sebelum kita
* (collapse lines, resolve imports), kita baca file ASLI dari disk untuk
* line number yang akurat, lalu cross-reference dengan code yang diterima.
*/
function inspectorPlugin(): Plugin {
const rootDir = process.cwd()
return {
name: 'inspector-inject',
enforce: 'pre',
transform(code, id) {
if (!/\.[jt]sx(\?|$)/.test(id) || id.includes('node_modules')) return null
if (!code.includes('<')) return null
const cleanId = id.replace(/\?.*$/, '')
const relativePath = path.relative(rootDir, cleanId)
// Baca file asli dari disk untuk line number akurat
let originalLines: string[] | null = null
try {
originalLines = fs.readFileSync(cleanId, 'utf-8').split('\n')
} catch {}
let modified = false
let lastOrigIdx = 0
const lines = code.split('\n')
const result: string[] = []
for (let i = 0; i < lines.length; i++) {
let line = lines[i]
const jsxPattern = /(<(?:[A-Z][a-zA-Z0-9]*(?:\.[a-zA-Z][a-zA-Z0-9]*)*|[a-z][a-zA-Z0-9-]*(?:\.[a-zA-Z][a-zA-Z0-9]*)*))\b/g
let match: RegExpExecArray | null = null
while ((match = jsxPattern.exec(line)) !== null) {
const charBefore = match.index > 0 ? line[match.index - 1] : ''
if (/[a-zA-Z0-9_$.]/.test(charBefore)) continue
// Cari line number asli di file original
let actualLine = i + 1
if (originalLines) {
const afterTag = line.slice(match.index)
// Snippet: tag + atribut sampai '>' pertama, tanpa injected attrs
const snippet = afterTag.split('>')[0]
.replace(/\s*data-inspector-[^"]*"[^"]*"/g, '')
.trim()
// Tag name saja sebagai fallback (e.g. "<Button")
const tagName = match[1]
let found = false
// 1) Forward search dengan full snippet
for (let j = lastOrigIdx; j < originalLines.length; j++) {
if (originalLines[j].includes(snippet)) {
actualLine = j + 1
lastOrigIdx = j + 1
found = true
break
}
}
// 2) Fallback: forward search hanya tag name (handle multi-line collapsed)
// Penting untuk <Button\n attr="..."\n> yang di-collapse jadi 1 baris
if (!found) {
for (let j = lastOrigIdx; j < originalLines.length; j++) {
if (originalLines[j].includes(tagName)) {
actualLine = j + 1
lastOrigIdx = j + 1
found = true
break
}
}
}
// 3) Last resort: search dari awal dengan full snippet
if (!found) {
for (let j = 0; j < originalLines.length; j++) {
if (originalLines[j].includes(snippet)) {
actualLine = j + 1
lastOrigIdx = j + 1
found = true
break
}
}
}
// 4) Last resort: search dari awal dengan tag name
if (!found) {
for (let j = 0; j < originalLines.length; j++) {
if (originalLines[j].includes(tagName) && !originalLines[j].trim().startsWith('</')) {
actualLine = j + 1
lastOrigIdx = j + 1
break
}
}
}
}
const col = match.index + 1
const attr = ` data-inspector-line="${actualLine}" data-inspector-column="${col}" data-inspector-relative-path="${relativePath}"`
const insertPos = match.index + match[0].length
line = line.slice(0, insertPos) + attr + line.slice(insertPos)
modified = true
jsxPattern.lastIndex += attr.length
}
result.push(line)
}
if (!modified) return null
return result.join('\n')
},
}
}
/**
* Workaround: @vitejs/plugin-react v6 + Vite 8 middlewareMode
* inject React Refresh HMR footer 2x → "Identifier RefreshRuntime already declared".
* Plugin ini hapus duplikat setelah semua transform selesai.
*/
function dedupeRefreshPlugin(): Plugin {
return {
name: 'dedupe-react-refresh',
enforce: 'post',
transform(code, id) {
if (!/\.[jt]sx(\?|$)/.test(id) || id.includes('node_modules')) return null
const marker = 'import * as RefreshRuntime from "/@react-refresh"'
const firstIdx = code.indexOf(marker)
if (firstIdx === -1) return null
const secondIdx = code.indexOf(marker, firstIdx + marker.length)
if (secondIdx === -1) return null
const sourcemapIdx = code.indexOf('\n//# sourceMappingURL=', secondIdx)
const endIdx = sourcemapIdx !== -1 ? sourcemapIdx : code.length
return { code: code.slice(0, secondIdx) + code.slice(endIdx), map: null }
},
}
}
export async function createVite() {
return createViteServer({
root: process.cwd(),
resolve: {
alias: {
'@': path.resolve(process.cwd(), './src'),
},
},
plugins: [
TanStackRouterVite({
routesDirectory: './src/frontend/routes',
generatedRouteTree: './src/frontend/routeTree.gen.ts',
routeFileIgnorePrefix: '-',
quoteStyle: 'single',
}),
inspectorPlugin(),
react(),
dedupeRefreshPlugin(),
],
server: {
middlewareMode: true,
hmr: { port: 24678 },
allowedHosts: true,
},
appType: 'custom',
optimizeDeps: {
include: ['react', 'react-dom'],
},
})
}

View File

@@ -0,0 +1,26 @@
import { test, expect, describe } from 'bun:test'
import { createPage } from './browser'
describe('E2E: Hello API via browser', () => {
test('GET /api/hello returns message', async () => {
const { page, cleanup } = await createPage()
try {
const body = await page.getResponseBody('/api/hello')
const data = JSON.parse(body)
expect(data).toEqual({ message: 'Hello, world!', method: 'GET' })
} finally {
cleanup()
}
})
test('GET /api/hello/:name returns personalized message', async () => {
const { page, cleanup } = await createPage()
try {
const body = await page.getResponseBody('/api/hello/Bun')
const data = JSON.parse(body)
expect(data).toEqual({ message: 'Hello, Bun!' })
} finally {
cleanup()
}
})
})

View File

@@ -0,0 +1,35 @@
import { test, expect, describe } from 'bun:test'
import { createPage, APP_HOST } from './browser'
describe('E2E: Auth API via browser', () => {
test('GET /api/auth/session page shows response', async () => {
const { page, cleanup } = await createPage()
try {
await page.goto(APP_HOST + '/api/auth/session')
// Navigating to a JSON API endpoint — body contains the JSON text
const bodyText = await page.evaluate('document.body.innerText || document.body.textContent || ""')
// Should contain "user" key in the response (either null or valid user)
// If empty, that's also acceptable (401 may not render body in Lightpanda)
if (bodyText.length > 0) {
const data = JSON.parse(bodyText)
expect(data.user).toBeNull()
} else {
// 401 response — Lightpanda may not render the body
expect(bodyText).toBe('')
}
} finally {
cleanup()
}
})
test('GET /api/auth/google redirects to Google OAuth', async () => {
const { page, cleanup } = await createPage()
try {
await page.goto(APP_HOST + '/api/auth/google')
const url = await page.url()
expect(url).toContain('accounts.google.com')
} finally {
cleanup()
}
})
})

162
tests/e2e/browser.ts Normal file
View File

@@ -0,0 +1,162 @@
/**
* Lightpanda browser helper for E2E tests.
* Lightpanda runs in Docker, so localhost is accessed via host.docker.internal.
*/
const WS_ENDPOINT = process.env.LIGHTPANDA_WS ?? 'ws://127.0.0.1:9222'
const APP_HOST = process.env.E2E_APP_HOST ?? 'http://host.docker.internal:3000'
export { APP_HOST }
interface CDPResponse {
id?: number
method?: string
params?: Record<string, any>
result?: Record<string, any>
error?: { code: number; message: string }
sessionId?: string
}
export class LightpandaPage {
private ws: WebSocket
private sessionId: string
private idCounter = 1
private pending = new Map<number, { resolve: (v: any) => void; reject: (e: Error) => void }>()
private ready: Promise<void>
constructor(ws: WebSocket, sessionId: string) {
this.ws = ws
this.sessionId = sessionId
this.ws.addEventListener('message', (e) => {
const data: CDPResponse = JSON.parse(e.data as string)
if (data.id && this.pending.has(data.id)) {
const p = this.pending.get(data.id)!
this.pending.delete(data.id)
if (data.error) p.reject(new Error(data.error.message))
else p.resolve(data.result)
}
})
// Enable page events
this.ready = this.send('Page.enable').then(() => {})
}
private send(method: string, params: Record<string, any> = {}): Promise<any> {
return new Promise((resolve, reject) => {
const id = this.idCounter++
this.pending.set(id, { resolve, reject })
this.ws.send(JSON.stringify({ id, method, params, sessionId: this.sessionId }))
setTimeout(() => {
if (this.pending.has(id)) {
this.pending.delete(id)
reject(new Error(`CDP timeout: ${method}`))
}
}, 15000)
})
}
async goto(path: string): Promise<void> {
const url = path.startsWith('http') ? path : `${APP_HOST}${path}`
await this.ready
const result = await this.send('Page.navigate', { url })
if (result?.errorText) throw new Error(`Navigation failed: ${result.errorText}`)
// Wait for load
await new Promise(r => setTimeout(r, 1500))
}
async evaluate<T = any>(expression: string): Promise<T> {
const result = await this.send('Runtime.evaluate', {
expression,
returnByValue: true,
awaitPromise: true,
})
if (result?.exceptionDetails) {
throw new Error(`Evaluate error: ${result.exceptionDetails.text || JSON.stringify(result.exceptionDetails)}`)
}
return result?.result?.value as T
}
async title(): Promise<string> {
return this.evaluate('document.title')
}
async textContent(selector: string): Promise<string | null> {
return this.evaluate(`document.querySelector('${selector}')?.textContent ?? null`)
}
async getAttribute(selector: string, attr: string): Promise<string | null> {
return this.evaluate(`document.querySelector('${selector}')?.getAttribute('${attr}') ?? null`)
}
async querySelectorAll(selector: string, property = 'textContent'): Promise<string[]> {
return this.evaluate(`Array.from(document.querySelectorAll('${selector}')).map(el => el.${property})`)
}
async url(): Promise<string> {
return this.evaluate('window.location.href')
}
async getResponseBody(path: string): Promise<string> {
const url = path.startsWith('http') ? path : `${APP_HOST}${path}`
await this.goto(url)
return this.evaluate('document.body.innerText')
}
async setCookie(name: string, value: string): Promise<void> {
await this.send('Network.setCookie', {
name,
value,
domain: new URL(APP_HOST).hostname,
path: '/',
})
}
}
export async function createPage(): Promise<{ page: LightpandaPage; cleanup: () => void }> {
const ws = new WebSocket(WS_ENDPOINT)
await new Promise<void>((resolve, reject) => {
ws.onopen = () => resolve()
ws.onerror = () => reject(new Error(`Cannot connect to Lightpanda at ${WS_ENDPOINT}`))
})
// Create target
const targetId = await new Promise<string>((resolve, reject) => {
const id = 1
ws.send(JSON.stringify({ id, method: 'Target.createTarget', params: { url: 'about:blank' } }))
const handler = (e: MessageEvent) => {
const data: CDPResponse = JSON.parse(e.data as string)
if (data.id === id) {
ws.removeEventListener('message', handler)
if (data.error) reject(new Error(data.error.message))
else resolve(data.result!.targetId)
}
}
ws.addEventListener('message', handler)
})
// Attach to target
const sessionId = await new Promise<string>((resolve, reject) => {
const id = 2
ws.send(JSON.stringify({ id, method: 'Target.attachToTarget', params: { targetId, flatten: true } }))
const handler = (e: MessageEvent) => {
const data: CDPResponse = JSON.parse(e.data as string)
if (data.id === id) {
ws.removeEventListener('message', handler)
if (data.error) reject(new Error(data.error.message))
else resolve(data.result!.sessionId)
}
}
ws.addEventListener('message', handler)
})
const page = new LightpandaPage(ws, sessionId)
return {
page,
cleanup: () => {
try { ws.close() } catch {}
},
}
}

View File

@@ -0,0 +1,27 @@
import { test, expect, describe } from 'bun:test'
import { createPage, APP_HOST } from './browser'
describe('E2E: Google OAuth redirect', () => {
test('navigating to /api/auth/google ends up at Google', async () => {
const { page, cleanup } = await createPage()
try {
await page.goto(APP_HOST + '/api/auth/google')
const url = await page.url()
expect(url).toContain('accounts.google.com')
} finally {
cleanup()
}
})
test('callback without code redirects to login error', async () => {
const { page, cleanup } = await createPage()
try {
await page.goto(APP_HOST + '/api/auth/callback/google')
const url = await page.url()
expect(url).toContain('/login')
expect(url).toContain('error=google_failed')
} finally {
cleanup()
}
})
})

15
tests/e2e/health.test.ts Normal file
View File

@@ -0,0 +1,15 @@
import { test, expect, describe, afterAll } from 'bun:test'
import { createPage } from './browser'
describe('E2E: Health endpoint', () => {
test('returns status ok', async () => {
const { page, cleanup } = await createPage()
try {
const body = await page.getResponseBody('/health')
const data = JSON.parse(body)
expect(data).toEqual({ status: 'ok' })
} finally {
cleanup()
}
})
})

View File

@@ -0,0 +1,49 @@
import { test, expect, describe } from 'bun:test'
import { createPage, APP_HOST } from './browser'
describe('E2E: Landing page', () => {
test('serves HTML with correct title', async () => {
const { page, cleanup } = await createPage()
try {
await page.goto(APP_HOST)
const title = await page.title()
expect(title).toBe('My App')
} finally {
cleanup()
}
})
test('has dark color-scheme meta tag', async () => {
const { page, cleanup } = await createPage()
try {
await page.goto(APP_HOST)
const meta = await page.evaluate('document.querySelector("meta[name=color-scheme]").content')
expect(meta).toBe('dark')
} finally {
cleanup()
}
})
test('splash screen removed after JS execution', async () => {
const { page, cleanup } = await createPage()
try {
await page.goto(APP_HOST)
// Lightpanda executes JS, so splash should be gone
const splashExists = await page.evaluate('document.getElementById("splash") !== null')
expect(splashExists).toBe(false)
} finally {
cleanup()
}
})
test('root div present', async () => {
const { page, cleanup } = await createPage()
try {
await page.goto(APP_HOST)
const root = await page.evaluate('document.getElementById("root") !== null')
expect(root).toBe(true)
} finally {
cleanup()
}
})
})

View File

@@ -0,0 +1,59 @@
import { test, expect, describe } from 'bun:test'
import { createPage, APP_HOST } from './browser'
describe('E2E: Login page', () => {
test('serves HTML with correct title', async () => {
const { page, cleanup } = await createPage()
try {
await page.goto(APP_HOST + '/login')
const title = await page.title()
expect(title).toBe('My App')
} finally {
cleanup()
}
})
test('has dark theme set on html element', async () => {
const { page, cleanup } = await createPage()
try {
await page.goto(APP_HOST + '/login')
const colorScheme = await page.evaluate('document.documentElement.getAttribute("data-mantine-color-scheme")')
expect(colorScheme).toBe('dark')
} finally {
cleanup()
}
})
test('has dark background meta', async () => {
const { page, cleanup } = await createPage()
try {
await page.goto(APP_HOST + '/login')
const meta = await page.evaluate('document.querySelector("meta[name=color-scheme]").content')
expect(meta).toBe('dark')
} finally {
cleanup()
}
})
test('root element exists for React mount', async () => {
const { page, cleanup } = await createPage()
try {
await page.goto(APP_HOST + '/login')
const root = await page.evaluate('document.getElementById("root") !== null')
expect(root).toBe(true)
} finally {
cleanup()
}
})
test('splash removed after app load', async () => {
const { page, cleanup } = await createPage()
try {
await page.goto(APP_HOST + '/login')
const splashGone = await page.evaluate('document.getElementById("splash") === null')
expect(splashGone).toBe(true)
} finally {
cleanup()
}
})
})

38
tests/helpers.ts Normal file
View File

@@ -0,0 +1,38 @@
import { prisma } from '../src/lib/db'
import { createApp } from '../src/app'
export { prisma }
export function createTestApp() {
const app = createApp()
return app
}
/** Create a test user with hashed password, returns the user record */
export async function seedTestUser(email = 'test@example.com', password = 'test123', name = 'Test User', role: 'USER' | 'ADMIN' | 'SUPER_ADMIN' = 'USER') {
const hashed = await Bun.password.hash(password, { algorithm: 'bcrypt' })
return prisma.user.upsert({
where: { email },
update: { name, password: hashed, role },
create: { email, name, password: hashed, role },
})
}
/** Create a session for a user, returns the token */
export async function createTestSession(userId: string, expiresAt?: Date) {
const token = crypto.randomUUID()
await prisma.session.create({
data: {
token,
userId,
expiresAt: expiresAt ?? new Date(Date.now() + 24 * 60 * 60 * 1000),
},
})
return token
}
/** Clean up test data */
export async function cleanupTestData() {
await prisma.session.deleteMany()
await prisma.user.deleteMany()
}

View File

@@ -0,0 +1,27 @@
import { test, expect, describe } from 'bun:test'
import { createTestApp } from '../helpers'
const app = createTestApp()
describe('Example API routes', () => {
test('GET /api/hello returns message', async () => {
const res = await app.handle(new Request('http://localhost/api/hello'))
expect(res.status).toBe(200)
const body = await res.json()
expect(body).toEqual({ message: 'Hello, world!', method: 'GET' })
})
test('PUT /api/hello returns message', async () => {
const res = await app.handle(new Request('http://localhost/api/hello', { method: 'PUT' }))
expect(res.status).toBe(200)
const body = await res.json()
expect(body).toEqual({ message: 'Hello, world!', method: 'PUT' })
})
test('GET /api/hello/:name returns personalized message', async () => {
const res = await app.handle(new Request('http://localhost/api/hello/Bun'))
expect(res.status).toBe(200)
const body = await res.json()
expect(body).toEqual({ message: 'Hello, Bun!' })
})
})

View File

@@ -0,0 +1,57 @@
import { test, expect, describe, beforeAll, afterAll } from 'bun:test'
import { createTestApp, seedTestUser, cleanupTestData, prisma } from '../helpers'
const app = createTestApp()
beforeAll(async () => {
await cleanupTestData()
await seedTestUser('flow@example.com', 'flow123', 'Flow User')
})
afterAll(async () => {
await cleanupTestData()
await prisma.$disconnect()
})
describe('Full auth flow: login → session → logout → session', () => {
test('complete auth lifecycle', async () => {
// 1. Login
const loginRes = await app.handle(new Request('http://localhost/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'flow@example.com', password: 'flow123' }),
}))
expect(loginRes.status).toBe(200)
const loginBody = await loginRes.json()
expect(loginBody.user.email).toBe('flow@example.com')
expect(loginBody.user.role).toBe('USER')
const setCookie = loginRes.headers.get('set-cookie')!
const token = setCookie.match(/session=([^;]+)/)?.[1]!
expect(token).toBeDefined()
// 2. Check session — should be valid
const sessionRes = await app.handle(new Request('http://localhost/api/auth/session', {
headers: { cookie: `session=${token}` },
}))
expect(sessionRes.status).toBe(200)
const sessionBody = await sessionRes.json()
expect(sessionBody.user.email).toBe('flow@example.com')
// 3. Logout
const logoutRes = await app.handle(new Request('http://localhost/api/auth/logout', {
method: 'POST',
headers: { cookie: `session=${token}` },
}))
expect(logoutRes.status).toBe(200)
// 4. Check session again — should be invalid
const afterLogoutRes = await app.handle(new Request('http://localhost/api/auth/session', {
headers: { cookie: `session=${token}` },
}))
expect(afterLogoutRes.status).toBe(401)
const afterLogoutBody = await afterLogoutRes.json()
expect(afterLogoutBody.user).toBeNull()
})
})

View File

@@ -0,0 +1,42 @@
import { test, expect, describe, afterAll } from 'bun:test'
import { createTestApp, cleanupTestData, prisma } from '../helpers'
const app = createTestApp()
afterAll(async () => {
await cleanupTestData()
await prisma.$disconnect()
})
describe('GET /api/auth/google', () => {
test('redirects to Google OAuth', async () => {
const res = await app.handle(new Request('http://localhost/api/auth/google'))
// Elysia returns 302 for redirects
expect(res.status).toBe(302)
const location = res.headers.get('location')
expect(location).toContain('accounts.google.com/o/oauth2/v2/auth')
expect(location).toContain('client_id=')
expect(location).toContain('redirect_uri=')
expect(location).toContain('scope=openid+email+profile')
expect(location).toContain('response_type=code')
})
test('redirect_uri points to callback endpoint', async () => {
const res = await app.handle(new Request('http://localhost/api/auth/google'))
const location = res.headers.get('location')!
const url = new URL(location)
const redirectUri = url.searchParams.get('redirect_uri')
expect(redirectUri).toBe('http://localhost/api/auth/callback/google')
})
})
describe('GET /api/auth/callback/google', () => {
test('redirects to login with error when no code provided', async () => {
const res = await app.handle(new Request('http://localhost/api/auth/callback/google'))
expect(res.status).toBe(302)
const location = res.headers.get('location')
expect(location).toContain('/login?error=google_failed')
})
})

View File

@@ -0,0 +1,96 @@
import { test, expect, describe, beforeAll, afterAll } from 'bun:test'
import { createTestApp, seedTestUser, cleanupTestData, prisma } from '../helpers'
const app = createTestApp()
beforeAll(async () => {
await cleanupTestData()
await seedTestUser('admin@example.com', 'admin123', 'Admin')
await seedTestUser('user@example.com', 'user123', 'User')
})
afterAll(async () => {
await cleanupTestData()
await prisma.$disconnect()
})
describe('POST /api/auth/login', () => {
test('login with valid credentials returns user and session cookie', async () => {
const res = await app.handle(new Request('http://localhost/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'admin@example.com', password: 'admin123' }),
}))
expect(res.status).toBe(200)
const body = await res.json()
expect(body.user).toBeDefined()
expect(body.user.email).toBe('admin@example.com')
expect(body.user.name).toBe('Admin')
expect(body.user.id).toBeDefined()
expect(body.user.role).toBe('USER')
// Should not expose password
expect(body.user.password).toBeUndefined()
// Check session cookie
const setCookie = res.headers.get('set-cookie')
expect(setCookie).toContain('session=')
expect(setCookie).toContain('HttpOnly')
expect(setCookie).toContain('Path=/')
})
test('login with wrong password returns 401', async () => {
const res = await app.handle(new Request('http://localhost/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'admin@example.com', password: 'wrongpassword' }),
}))
expect(res.status).toBe(401)
const body = await res.json()
expect(body.error).toBe('Email atau password salah')
})
test('login with non-existent email returns 401', async () => {
const res = await app.handle(new Request('http://localhost/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'nobody@example.com', password: 'anything' }),
}))
expect(res.status).toBe(401)
const body = await res.json()
expect(body.error).toBe('Email atau password salah')
})
test('login returns role field in response', async () => {
const res = await app.handle(new Request('http://localhost/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com', password: 'user123' }),
}))
expect(res.status).toBe(200)
const body = await res.json()
expect(body.user.role).toBe('USER')
})
test('login creates a session in database', async () => {
const res = await app.handle(new Request('http://localhost/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com', password: 'user123' }),
}))
expect(res.status).toBe(200)
const setCookie = res.headers.get('set-cookie')!
const token = setCookie.match(/session=([^;]+)/)?.[1]
expect(token).toBeDefined()
// Verify session exists in DB
const session = await prisma.session.findUnique({ where: { token: token! } })
expect(session).not.toBeNull()
expect(session!.expiresAt.getTime()).toBeGreaterThan(Date.now())
})
})

View File

@@ -0,0 +1,80 @@
import { test, expect, describe, beforeAll, afterAll } from 'bun:test'
import { createTestApp, seedTestUser, createTestSession, cleanupTestData, prisma } from '../helpers'
const app = createTestApp()
let testUserId: string
beforeAll(async () => {
await cleanupTestData()
const user = await seedTestUser('logout-test@example.com', 'pass123', 'Logout Tester')
testUserId = user.id
})
afterAll(async () => {
await cleanupTestData()
await prisma.$disconnect()
})
describe('POST /api/auth/logout', () => {
test('logout clears session cookie', async () => {
const token = await createTestSession(testUserId)
const res = await app.handle(new Request('http://localhost/api/auth/logout', {
method: 'POST',
headers: { cookie: `session=${token}` },
}))
expect(res.status).toBe(200)
const body = await res.json()
expect(body.ok).toBe(true)
// Cookie should be cleared
const setCookie = res.headers.get('set-cookie')
expect(setCookie).toContain('session=;')
expect(setCookie).toContain('Max-Age=0')
})
test('logout deletes session from database', async () => {
const token = await createTestSession(testUserId)
// Verify session exists
let session = await prisma.session.findUnique({ where: { token } })
expect(session).not.toBeNull()
await app.handle(new Request('http://localhost/api/auth/logout', {
method: 'POST',
headers: { cookie: `session=${token}` },
}))
// Verify session deleted
session = await prisma.session.findUnique({ where: { token } })
expect(session).toBeNull()
})
test('logout without cookie still returns ok', async () => {
const res = await app.handle(new Request('http://localhost/api/auth/logout', {
method: 'POST',
}))
expect(res.status).toBe(200)
const body = await res.json()
expect(body.ok).toBe(true)
})
test('session is invalid after logout', async () => {
const token = await createTestSession(testUserId)
// Logout
await app.handle(new Request('http://localhost/api/auth/logout', {
method: 'POST',
headers: { cookie: `session=${token}` },
}))
// Try to use the same session
const sessionRes = await app.handle(new Request('http://localhost/api/auth/session', {
headers: { cookie: `session=${token}` },
}))
expect(sessionRes.status).toBe(401)
})
})

View File

@@ -0,0 +1,67 @@
import { test, expect, describe, beforeAll, afterAll } from 'bun:test'
import { createTestApp, seedTestUser, createTestSession, cleanupTestData, prisma } from '../helpers'
const app = createTestApp()
let testUserId: string
beforeAll(async () => {
await cleanupTestData()
const user = await seedTestUser('session-test@example.com', 'pass123', 'Session Tester')
testUserId = user.id
})
afterAll(async () => {
await cleanupTestData()
await prisma.$disconnect()
})
describe('GET /api/auth/session', () => {
test('returns 401 without cookie', async () => {
const res = await app.handle(new Request('http://localhost/api/auth/session'))
expect(res.status).toBe(401)
const body = await res.json()
expect(body.user).toBeNull()
})
test('returns 401 with invalid token', async () => {
const res = await app.handle(new Request('http://localhost/api/auth/session', {
headers: { cookie: 'session=invalid-token-12345' },
}))
expect(res.status).toBe(401)
const body = await res.json()
expect(body.user).toBeNull()
})
test('returns user with valid session', async () => {
const token = await createTestSession(testUserId)
const res = await app.handle(new Request('http://localhost/api/auth/session', {
headers: { cookie: `session=${token}` },
}))
expect(res.status).toBe(200)
const body = await res.json()
expect(body.user).toBeDefined()
expect(body.user.email).toBe('session-test@example.com')
expect(body.user.name).toBe('Session Tester')
expect(body.user.id).toBe(testUserId)
expect(body.user.role).toBe('USER')
})
test('returns 401 and deletes expired session', async () => {
const expiredDate = new Date(Date.now() - 1000) // 1 second ago
const token = await createTestSession(testUserId, expiredDate)
const res = await app.handle(new Request('http://localhost/api/auth/session', {
headers: { cookie: `session=${token}` },
}))
expect(res.status).toBe(401)
const body = await res.json()
expect(body.user).toBeNull()
// Verify expired session was deleted from DB
const session = await prisma.session.findUnique({ where: { token } })
expect(session).toBeNull()
})
})

View File

@@ -0,0 +1,27 @@
import { test, expect, describe } from 'bun:test'
import { createTestApp } from '../helpers'
const app = createTestApp()
describe('Error handling', () => {
test('unknown API route returns 404 JSON', async () => {
const res = await app.handle(new Request('http://localhost/api/nonexistent'))
expect(res.status).toBe(404)
const body = await res.json()
expect(body).toEqual({ error: 'Not Found', status: 404 })
})
test('unknown nested API route returns 404', async () => {
const res = await app.handle(new Request('http://localhost/api/foo/bar/baz'))
expect(res.status).toBe(404)
const body = await res.json()
expect(body.error).toBe('Not Found')
})
test('wrong HTTP method returns 404', async () => {
const res = await app.handle(new Request('http://localhost/api/hello', { method: 'DELETE' }))
expect(res.status).toBe(404)
const body = await res.json()
expect(body.error).toBe('Not Found')
})
})

View File

@@ -0,0 +1,13 @@
import { test, expect, describe } from 'bun:test'
import { createTestApp } from '../helpers'
const app = createTestApp()
describe('GET /health', () => {
test('returns 200 with status ok', async () => {
const res = await app.handle(new Request('http://localhost/health'))
expect(res.status).toBe(200)
const body = await res.json()
expect(body).toEqual({ status: 'ok' })
})
})

23
tests/unit/db.test.ts Normal file
View File

@@ -0,0 +1,23 @@
import { test, expect, describe, afterAll } from 'bun:test'
import { prisma } from '../helpers'
afterAll(async () => {
await prisma.$disconnect()
})
describe('Prisma database connection', () => {
test('connects to database', async () => {
const result = await prisma.$queryRaw`SELECT 1 as ok`
expect(result).toEqual([{ ok: 1 }])
})
test('user table exists', async () => {
const count = await prisma.user.count()
expect(typeof count).toBe('number')
})
test('session table exists', async () => {
const count = await prisma.session.count()
expect(typeof count).toBe('number')
})
})

34
tests/unit/env.test.ts Normal file
View File

@@ -0,0 +1,34 @@
import { test, expect, describe } from 'bun:test'
describe('env', () => {
test('PORT defaults to 3000 when not set', () => {
const original = process.env.PORT
delete process.env.PORT
// Re-import to test default
// Since modules are cached, we test the logic directly
const value = parseInt(process.env.PORT ?? '3000', 10)
expect(value).toBe(3000)
if (original) process.env.PORT = original
})
test('PORT parses from env', () => {
const value = parseInt(process.env.PORT ?? '3000', 10)
expect(typeof value).toBe('number')
expect(value).toBeGreaterThan(0)
})
test('DATABASE_URL is set', () => {
expect(process.env.DATABASE_URL).toBeDefined()
expect(process.env.DATABASE_URL).toContain('postgresql://')
})
test('GOOGLE_CLIENT_ID is set', () => {
expect(process.env.GOOGLE_CLIENT_ID).toBeDefined()
expect(process.env.GOOGLE_CLIENT_ID!.length).toBeGreaterThan(0)
})
test('GOOGLE_CLIENT_SECRET is set', () => {
expect(process.env.GOOGLE_CLIENT_SECRET).toBeDefined()
expect(process.env.GOOGLE_CLIENT_SECRET!.length).toBeGreaterThan(0)
})
})

View File

@@ -0,0 +1,24 @@
import { test, expect, describe } from 'bun:test'
describe('Bun.password (bcrypt)', () => {
test('hash and verify correct password', async () => {
const hash = await Bun.password.hash('mypassword', { algorithm: 'bcrypt' })
expect(hash).toStartWith('$2')
const valid = await Bun.password.verify('mypassword', hash)
expect(valid).toBe(true)
})
test('reject wrong password', async () => {
const hash = await Bun.password.hash('mypassword', { algorithm: 'bcrypt' })
const valid = await Bun.password.verify('wrongpassword', hash)
expect(valid).toBe(false)
})
test('different hashes for same password', async () => {
const hash1 = await Bun.password.hash('same', { algorithm: 'bcrypt' })
const hash2 = await Bun.password.hash('same', { algorithm: 'bcrypt' })
expect(hash1).not.toBe(hash2) // bcrypt salt differs
expect(await Bun.password.verify('same', hash1)).toBe(true)
expect(await Bun.password.verify('same', hash2)).toBe(true)
})
})

24
tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"strict": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"allowImportingTsExtensions": true,
"noEmit": true,
"sourceMap": true,
"jsx": "react-jsx",
"types": ["vite/client"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

30
vite.config.ts Normal file
View File

@@ -0,0 +1,30 @@
import path from 'node:path'
import { TanStackRouterVite } from '@tanstack/router-vite-plugin'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
export default defineConfig({
root: process.cwd(),
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
plugins: [
TanStackRouterVite({
routesDirectory: './src/frontend/routes',
generatedRouteTree: './src/frontend/routeTree.gen.ts',
routeFileIgnorePrefix: '-',
quoteStyle: 'single',
}),
react(),
],
build: {
outDir: 'dist',
emptyOutDir: true,
sourcemap: true,
rollupOptions: {
input: path.resolve(__dirname, 'index.html'),
},
},
})