From 0ef36598addfa6f626205cf99ef4e1401a390ceb Mon Sep 17 00:00:00 2001 From: pitoi Date: Mon, 29 Dec 2025 11:35:19 +0000 Subject: [PATCH] implement mock for cerebras ai api --- package.json | 2 +- .../app/api/check-mock-cerebras/route.ts | 37 ++ .../app/api/check-visual-edit/route.ts | 4 +- .../website/app/api/mock/cerebras/route.ts | 62 +++ packages/website/app/api/visual-edit/route.ts | 27 ++ packages/website/docs/MOCKING.md | 357 ++++++++++++++++++ packages/website/lib/env.ts | 34 ++ packages/website/lib/generator.ts | 133 +++++++ packages/website/package.json | 6 +- packages/website/scripts/test-mock.ts | 104 +++++ packages/website/test/env.test.ts | 128 +++++++ packages/website/test/generator.test.ts | 317 ++++++++++++++++ packages/website/vitest.config.ts | 10 + pnpm-lock.yaml | 118 +++++- 14 files changed, 1322 insertions(+), 17 deletions(-) create mode 100644 packages/website/app/api/check-mock-cerebras/route.ts create mode 100644 packages/website/app/api/mock/cerebras/route.ts create mode 100644 packages/website/docs/MOCKING.md create mode 100644 packages/website/lib/env.ts create mode 100644 packages/website/lib/generator.ts create mode 100644 packages/website/scripts/test-mock.ts create mode 100644 packages/website/test/env.test.ts create mode 100644 packages/website/test/generator.test.ts create mode 100644 packages/website/vitest.config.ts diff --git a/package.json b/package.json index c0b97e772..b2ed017d7 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "build": "turbo run build --filter=react-grab --filter=@react-grab/cursor --filter=@react-grab/claude-code --filter=@react-grab/ami --filter=@react-grab/opencode --filter=@react-grab/codex --filter=@react-grab/gemini --filter=@react-grab/amp --filter=@react-grab/cli --filter=@react-grab/utils --filter=@react-grab/visual-edit && pnpm --filter grab build", "dev": "turbo dev --filter=react-grab --filter=@react-grab/cursor --filter=@react-grab/claude-code --filter=@react-grab/ami --filter=@react-grab/opencode --filter=@react-grab/codex --filter=@react-grab/gemini --filter=@react-grab/amp --filter=@react-grab/cli --filter=@react-grab/utils --filter=@react-grab/visual-edit", - "test": "turbo run test --filter=react-grab --filter=@react-grab/cli", + "test": "turbo run test --filter=react-grab --filter=@react-grab/cli --filter=@react-grab/website", "lint": "pnpm --filter react-grab lint", "lint:fix": "pnpm --filter react-grab lint:fix", "format": "prettier --write .", diff --git a/packages/website/app/api/check-mock-cerebras/route.ts b/packages/website/app/api/check-mock-cerebras/route.ts new file mode 100644 index 000000000..e5e8ae686 --- /dev/null +++ b/packages/website/app/api/check-mock-cerebras/route.ts @@ -0,0 +1,37 @@ +import { isUsingMocks } from "../../../lib/env"; + +const getCorsHeaders = () => { + return { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }; +}; + +export const OPTIONS = () => { + return new Response(null, { + status: 204, + headers: getCorsHeaders(), + }); +}; + +export const GET = () => { + const corsHeaders = getCorsHeaders(); + + if (!isUsingMocks()) { + return Response.json( + { status: "disabled", mockMode: false }, + { headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + return Response.json( + { + status: "ok", + mockMode: true, + version: "1.0", + message: "Mock Cerebras service operational", + }, + { headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); +}; diff --git a/packages/website/app/api/check-visual-edit/route.ts b/packages/website/app/api/check-visual-edit/route.ts index d39bec9ec..6af5b4735 100644 --- a/packages/website/app/api/check-visual-edit/route.ts +++ b/packages/website/app/api/check-visual-edit/route.ts @@ -1,5 +1,7 @@ const IS_HEALTHY = true; +import { isUsingMocks } from "../../../lib/env"; + const getCorsHeaders = () => { return { "Access-Control-Allow-Origin": "*", @@ -16,7 +18,7 @@ export const OPTIONS = () => { export const GET = () => { const corsHeaders = getCorsHeaders(); return Response.json( - { healthy: IS_HEALTHY }, + { healthy: IS_HEALTHY, mockMode: isUsingMocks() }, { headers: { ...corsHeaders, "Content-Type": "application/json" } }, ); }; diff --git a/packages/website/app/api/mock/cerebras/route.ts b/packages/website/app/api/mock/cerebras/route.ts new file mode 100644 index 000000000..4e70d3821 --- /dev/null +++ b/packages/website/app/api/mock/cerebras/route.ts @@ -0,0 +1,62 @@ +import { isUsingMocks } from "../../../../lib/env"; +import { generateCodeFromPrompt } from "../../../../lib/generator"; + +interface MockCerebrasRequest { + messages: Array<{ role: string; content: string }>; +} + +const getCorsHeaders = () => { + return { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }; +}; + +export const OPTIONS = () => { + return new Response(null, { + status: 204, + headers: getCorsHeaders(), + }); +}; + +export const POST = async (request: Request) => { + if (!isUsingMocks()) { + return Response.json( + { error: "Mock endpoint is disabled. Set USE_MOCKS=true to enable." }, + { status: 404, headers: getCorsHeaders() } + ); + } + + try { + const body = (await request.json()) as MockCerebrasRequest; + const userMessage = body.messages.find( + (message) => message.role === "user" + ); + + if (!userMessage) { + return Response.json( + { error: "No user message found in request" }, + { status: 400, headers: getCorsHeaders() } + ); + } + + const generatedCode = generateCodeFromPrompt(userMessage.content); + + return new Response(generatedCode, { + status: 200, + headers: { + ...getCorsHeaders(), + "Content-Type": "text/javascript", + }, + }); + } catch (error) { + return Response.json( + { + error: "Failed to process mock request", + details: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500, headers: getCorsHeaders() } + ); + } +}; diff --git a/packages/website/app/api/visual-edit/route.ts b/packages/website/app/api/visual-edit/route.ts index 9825783e2..7444c3e7d 100644 --- a/packages/website/app/api/visual-edit/route.ts +++ b/packages/website/app/api/visual-edit/route.ts @@ -1,5 +1,6 @@ import { generateText } from "ai"; import type { ModelMessage } from "ai"; +import { getVisualEditConfig } from "../../../lib/env"; interface ConversationMessage { role: "user" | "assistant"; @@ -154,6 +155,32 @@ export const POST = async (request: Request) => { })); try { + const config = getVisualEditConfig(); + + if (config.useMocks) { + const mockResponse = await fetch(config.mockUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ messages }), + }); + + if (!mockResponse.ok) { + throw new Error(`Mock endpoint returned ${mockResponse.status}`); + } + + const generatedCode = await mockResponse.text(); + + return new Response(generatedCode, { + status: 200, + headers: { + ...corsHeaders, + "Content-Type": "text/javascript", + }, + }); + } + let generatedCode: string; console.log("shouldUsePrimaryModel", shouldUsePrimaryModel); diff --git a/packages/website/docs/MOCKING.md b/packages/website/docs/MOCKING.md new file mode 100644 index 000000000..c413890b3 --- /dev/null +++ b/packages/website/docs/MOCKING.md @@ -0,0 +1,357 @@ +# Cerebras AI API Mocking System + +Comprehensive mock implementation for the Cerebras AI API used by the `/api/visual-edit` endpoint. This system enables local development, testing, and CI/CD workflows without requiring real API keys or incurring API costs. + +## Architecture + +### Overview + +The mocking system provides a toggleable, pattern-matched code generation service that intercepts requests to the Cerebras AI API when enabled. It consists of: + +- **Centralized Configuration** (`lib/env.ts`) - Environment variable management & mock toggle +- **Code Generator** (`lib/generator.ts`) - Keyword-based DOM manipulation templates +- **Mock Endpoint** (`/api/mock/cerebras`) - Pattern-matched JavaScript code responses +- **Health Checks** - Service status & mock mode verification + +### Flow Diagram + +``` +User Request → /api/visual-edit + ↓ + [Check USE_MOCKS] + ↓ + ┌───────────┴───────────┐ + ↓ ↓ + USE_MOCKS=true USE_MOCKS=false + ↓ ↓ + /api/mock/cerebras Cerebras AI API + ↓ ↓ + generator.ts Vercel AI SDK + ↓ ↓ + JavaScript Code JavaScript Code + ↓ ↓ + └───────────┬───────────┘ + ↓ + Response to User +``` + +## Environment Setup + +### Environment Variables + +Create or update `.env.local` with the following variables: + +```bash +# Enable mock mode (default: false) +USE_MOCKS=true + +# Real Cerebras API key (only required when USE_MOCKS=false) +CEREBRAS_API_KEY=your_api_key_here + +# Optional: Override default Cerebras base URL +CEREBRAS_BASE_URL=https://api.cerebras.ai/v1 + +# Optional: Override default mock endpoint URL +MOCK_CEREBRAS_URL=http://localhost:3000/api/mock/cerebras +``` + +### Configuration Files + +**`.env.example`** - Template for required environment variables +**`.env.local`** - Local development configuration (gitignored) + +## Local Development + +### Running with Mocks + +1. Set `USE_MOCKS=true` in `.env.local` +2. Start the development server: + ```bash + nr dev + ``` +3. Verify mock mode is enabled: + ```bash + curl http://localhost:3000/api/check-mock-cerebras + curl http://localhost:3000/api/check-visual-edit + ``` + +### Running with Real API + +1. Set `USE_MOCKS=false` in `.env.local` +2. Set `CEREBRAS_API_KEY=your_key` in `.env.local` +3. Start the development server +4. Requests will route to the real Cerebras AI API + +## CI/CD Integration + +### GitHub Actions Example + +```yaml +name: Test Visual Edit Endpoint + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "20" + - run: npm install + - run: | + echo "USE_MOCKS=true" > .env.local + npm run dev & + sleep 5 + npm run test:mock +``` + +### Benefits + +- **No API Keys Required** - CI runs without managing secrets +- **Deterministic** - Same prompts always return same code +- **Fast** - No network latency to external APIs +- **Cost-Free** - No API usage charges during testing + +## Testing + +### Running the Test Script + +The test script validates mock endpoint behavior for multiple prompt patterns: + +```bash +nr test:mock +``` + +### Test Cases Covered + +- Hide element → `element.style.display = "none"` +- Show element → `element.style.display = "block"` +- Change text → `element.textContent = "value"` +- Change color → `element.style.color = "#color"` +- Add class → `element.classList.add("className")` +- Remove class → `element.classList.remove("className")` +- Toggle class → `element.classList.toggle("className")` +- Change opacity → `element.style.opacity = "value"` + +### Manual Testing + +Test the mock endpoint directly: + +```bash +curl -X POST http://localhost:3000/api/mock/cerebras \ + -H "Content-Type: application/json" \ + -d '{"messages":[{"role":"user","content":"hide this element"}]}' +``` + +Expected response: + +```javascript +element.style.display = "none"; +``` + +## Code Generator + +### Pattern Matching Logic + +The code generator (`lib/generator.ts`) uses keyword-based pattern matching: + +1. Convert user prompt to lowercase +2. Search for matching keywords in template list +3. Extract parameters (colors, class names, values) using regex +4. Generate JavaScript code from template +5. Return deterministic code string + +### Available Templates + +| Keywords | Generated Code | Example Prompt | +|----------|----------------|----------------| +| `hide`, `hidden`, `invisible` | `element.style.display = "none"` | "hide this element" | +| `show`, `visible`, `display` | `element.style.display = "block"` | "show this element" | +| `text`, `content`, `change text` | `element.textContent = "value"` | 'change text to "Hello"' | +| `color`, `text color` | `element.style.color = "#color"` | "change color to red" | +| `background`, `bg color` | `element.style.backgroundColor = "#color"` | "background color #ff0000" | +| `add class`, `apply class` | `element.classList.add("className")` | 'add class "active"' | +| `remove class` | `element.classList.remove("className")` | 'remove class "active"' | +| `toggle class` | `element.classList.toggle("className")` | 'toggle class "active"' | +| `opacity` | `element.style.opacity = "value"` | "change opacity to 0.5" | +| `width` | `element.style.width = "value"` | "width 100px" | +| `height` | `element.style.height = "value"` | "height 50%" | +| `padding` | `element.style.padding = "value"` | "padding 10px" | +| `margin` | `element.style.margin = "value"` | "margin 20px" | +| `border` | `element.style.border = "value"` | "border 1px solid black" | + +### Extending Templates + +To add new templates, edit `lib/generator.ts`: + +```typescript +{ + keywords: ["your", "keywords"], + generate: (prompt) => { + // Extract parameters from prompt + const value = extractValue(prompt); + return `element.property = "${value}";`; + }, +} +``` + +## Health Check Endpoints + +### Check Visual Edit Endpoint + +```bash +GET /api/check-visual-edit +``` + +Response: + +```json +{ + "healthy": true, + "mockMode": true +} +``` + +### Check Mock Cerebras Endpoint + +```bash +GET /api/check-mock-cerebras +``` + +Response when `USE_MOCKS=true`: + +```json +{ + "status": "ok", + "mockMode": true, + "version": "1.0", + "message": "Mock Cerebras service operational" +} +``` + +Response when `USE_MOCKS=false`: + +```json +{ + "status": "disabled", + "mockMode": false +} +``` + +## Troubleshooting + +### Mock Endpoint Returns 404 + +**Problem**: Mock endpoint is returning 404 or "disabled" error. + +**Solution**: Verify `USE_MOCKS=true` is set in `.env.local` and restart the development server. + +### Test Script Fails + +**Problem**: Test script shows failures for pattern matching. + +**Solution**: +1. Verify development server is running on port 3000 +2. Check `USE_MOCKS=true` in environment +3. Ensure no firewall blocking localhost:3000 +4. Review test output for specific pattern mismatches + +### Real API Still Being Called + +**Problem**: API usage charges appear when expecting mock mode. + +**Solution**: +1. Check `.env.local` has `USE_MOCKS=true` +2. Restart the development server after changing environment variables +3. Verify mock mode via health check endpoint +4. Check server logs for mock routing confirmation + +### Generated Code Not Matching Expectations + +**Problem**: Mock returns generic fallback code instead of expected pattern. + +**Solution**: +1. Review prompt for matching keywords (see Available Templates table) +2. Check `lib/generator.ts` for keyword list +3. Add new template if pattern is missing +4. Ensure prompt is clear & descriptive + +## Migration Guide + +### Existing Code Compatibility + +The mocking system is **fully backward compatible**. When `USE_MOCKS=false`: + +- All requests route to real Cerebras AI API +- No changes to request/response format +- Existing CORS headers preserved +- Error handling unchanged +- Message truncation logic maintained + +### Gradual Adoption + +You can enable mocks per environment: + +- **Local Development**: `USE_MOCKS=true` in `.env.local` +- **CI/CD**: `USE_MOCKS=true` in workflow environment +- **Staging**: `USE_MOCKS=false` with real API key +- **Production**: `USE_MOCKS=false` with real API key + +## API Reference + +### lib/env.ts + +```typescript +export const isUsingMocks = (): boolean; +export const getCerebrasApiKey = (): string | undefined; +export const getCerebrasBaseUrl = (): string | undefined; +export const getMockCerebrasUrl = (): string; +export const getVisualEditConfig = (): VisualEditConfig; +``` + +### lib/generator.ts + +```typescript +export const generateCodeFromPrompt = (prompt: string): string; +``` + +### /api/mock/cerebras + +**Method**: POST + +**Request**: + +```json +{ + "messages": [ + { "role": "user", "content": "hide this element" } + ] +} +``` + +**Response**: `text/javascript` + +```javascript +element.style.display = "none"; +``` + +## Best Practices + +1. **Always verify mock mode** via health check before debugging API issues +2. **Use descriptive prompts** that include clear keywords from template list +3. **Test both mock & real modes** in staging before production deployment +4. **Keep templates simple** - mock is for development, not production accuracy +5. **Document new patterns** when extending the generator +6. **Run test script** after modifying generator.ts + +## Support + +For issues or questions: + +1. Check this documentation first +2. Review health check endpoints for service status +3. Inspect server logs for routing/mock behavior +4. Review `lib/generator.ts` for available patterns +5. Open an issue with reproduction steps \ No newline at end of file diff --git a/packages/website/lib/env.ts b/packages/website/lib/env.ts new file mode 100644 index 000000000..a672741a1 --- /dev/null +++ b/packages/website/lib/env.ts @@ -0,0 +1,34 @@ +export const isUsingMocks = (): boolean => { + return process.env.USE_MOCKS === "true"; +}; + +export const getCerebrasApiKey = (): string | undefined => { + return process.env.CEREBRAS_API_KEY; +}; + +export const getCerebrasBaseUrl = (): string | undefined => { + return process.env.CEREBRAS_BASE_URL; +}; + +export const getMockCerebrasUrl = (): string => { + return ( + process.env.MOCK_CEREBRAS_URL || + "http://localhost:3000/api/mock/cerebras" + ); +}; + +export interface VisualEditConfig { + useMocks: boolean; + apiKey: string | undefined; + baseUrl: string | undefined; + mockUrl: string; +} + +export const getVisualEditConfig = (): VisualEditConfig => { + return { + useMocks: isUsingMocks(), + apiKey: getCerebrasApiKey(), + baseUrl: getCerebrasBaseUrl(), + mockUrl: getMockCerebrasUrl(), + }; +}; diff --git a/packages/website/lib/generator.ts b/packages/website/lib/generator.ts new file mode 100644 index 000000000..d50242079 --- /dev/null +++ b/packages/website/lib/generator.ts @@ -0,0 +1,133 @@ +interface CodeTemplate { + keywords: string[]; + generate: (prompt: string) => string; +} + +const templates: CodeTemplate[] = [ + { + keywords: ["hide", "hidden", "invisible"], + generate: () => `element.style.display = "none";`, + }, + { + keywords: ["show", "visible", "display"], + generate: () => `element.style.display = "block";`, + }, + { + keywords: ["background", "bg color", "background color"], + generate: (prompt) => { + const match = prompt.match(/#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}|\b(?:red|blue|green|black|white|gray|yellow|orange|purple)\b/i); + const color = match ? match[0].toLowerCase() : "#ffffff"; + return `element.style.backgroundColor = "${color}";`; + }, + }, + { + keywords: ["text color", "color"], + generate: (prompt) => { + const match = prompt.match(/#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}|\b(?:red|blue|green|black|white|gray|yellow|orange|purple)\b/); + const color = match ? match[0] : "#000000"; + return `element.style.color = "${color}";`; + }, + }, + { + keywords: ["text", "content", "change text"], + generate: (prompt) => { + const match = prompt.match(/["']([^"']+)["']/); + const text = match ? match[1] : "Updated text"; + return `element.textContent = "${text}";`; + }, + }, + { + keywords: ["add class", "apply class"], + generate: (prompt) => { + const match = prompt.match(/["']([^"']+)["']|class\s+([a-zA-Z0-9_-]+)/); + const className = match ? (match[1] || match[2]) : "active"; + return `element.classList.add("${className}");`; + }, + }, + { + keywords: ["remove class"], + generate: (prompt) => { + const match = prompt.match(/["']([^"']+)["']|class\s+([a-zA-Z0-9_-]+)/); + const className = match ? (match[1] || match[2]) : "active"; + return `element.classList.remove("${className}");`; + }, + }, + { + keywords: ["toggle class"], + generate: (prompt) => { + const match = prompt.match(/["']([^"']+)["']|class\s+([a-zA-Z0-9_-]+)/); + const className = match ? (match[1] || match[2]) : "active"; + return `element.classList.toggle("${className}");`; + }, + }, + { + keywords: ["opacity"], + generate: (prompt) => { + const match = prompt.match(/\b(0\.\d+|1\.0|1|0)\b/); + const opacity = match ? match[0] : "0.5"; + return `element.style.opacity = "${opacity}";`; + }, + }, + { + keywords: ["width"], + generate: (prompt) => { + const match = prompt.match(/(\d+)(px|%|em|rem)?/); + const width = match ? `${match[1]}${match[2] || "px"}` : "100px"; + return `element.style.width = "${width}";`; + }, + }, + { + keywords: ["height"], + generate: (prompt) => { + const match = prompt.match(/(\d+)(px|%|em|rem)?/); + const height = match ? `${match[1]}${match[2] || "px"}` : "100px"; + return `element.style.height = "${height}";`; + }, + }, + { + keywords: ["padding"], + generate: (prompt) => { + const match = prompt.match(/(\d+)(px|%|em|rem)?/); + const padding = match ? `${match[1]}${match[2] || "px"}` : "10px"; + return `element.style.padding = "${padding}";`; + }, + }, + { + keywords: ["margin"], + generate: (prompt) => { + const match = prompt.match(/(\d+)(px|%|em|rem)?/); + const margin = match ? `${match[1]}${match[2] || "px"}` : "10px"; + return `element.style.margin = "${margin}";`; + }, + }, + { + keywords: ["border"], + generate: (prompt) => { + const widthMatch = prompt.match(/(\d+)px/); + const width = widthMatch ? `${widthMatch[1]}px` : "1px"; + + const styleMatch = prompt.match(/\b(solid|dashed|dotted)\b/i); + const style = styleMatch ? styleMatch[1] : "solid"; + + const colorMatch = prompt.match(/#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}|\b(?:red|blue|green|black|white|gray|yellow|orange|purple)\b/i); + const color = colorMatch ? colorMatch[0] : "#000000"; + + return `element.style.border = "${width} ${style} ${color}";`; + }, + }, +]; + +export const generateCodeFromPrompt = (prompt: string): string => { + const lowerPrompt = prompt.toLowerCase(); + + for (const template of templates) { + const hasKeyword = template.keywords.some((keyword) => + lowerPrompt.includes(keyword) + ); + if (hasKeyword) { + return template.generate(prompt); + } + } + + return `// No matching pattern found for: ${prompt}\nelement.style.opacity = "1";`; +}; diff --git a/packages/website/package.json b/packages/website/package.json index 88d5a381a..862acedf3 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -4,6 +4,9 @@ "private": true, "scripts": { "dev": "next dev", + "test": "vitest run", + "test:watch": "vitest", + "test:mock": "node --loader tsx scripts/test-mock.ts", "build": "pnpm --filter react-grab build && next build", "start": "next start", "lint": "eslint" @@ -44,6 +47,7 @@ "eslint-config-next": "16.0.7", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", - "typescript": "^5" + "typescript": "^5", + "vitest": "^3.2.4" } } diff --git a/packages/website/scripts/test-mock.ts b/packages/website/scripts/test-mock.ts new file mode 100644 index 000000000..851342d26 --- /dev/null +++ b/packages/website/scripts/test-mock.ts @@ -0,0 +1,104 @@ +interface TestCase { + name: string; + prompt: string; + expectedPattern: string | RegExp; +} + +const testCases: TestCase[] = [ + { + name: "Hide element", + prompt: "hide this element", + expectedPattern: /display.*none/i, + }, + { + name: "Show element", + prompt: "show this element", + expectedPattern: /display.*block/i, + }, + { + name: "Change text", + prompt: 'change text to "Hello World"', + expectedPattern: /textContent.*Hello World/i, + }, + { + name: "Change color", + prompt: "change color to red", + expectedPattern: /style\.color.*red/i, + }, + { + name: "Add class", + prompt: 'add class "active"', + expectedPattern: /classList\.add.*active/i, + }, + { + name: "Remove class", + prompt: 'remove class "active"', + expectedPattern: /classList\.remove.*active/i, + }, + { + name: "Toggle class", + prompt: 'toggle class "active"', + expectedPattern: /classList\.toggle.*active/i, + }, + { + name: "Change opacity", + prompt: "change opacity to 0.5", + expectedPattern: /opacity.*0\.5/i, + }, +]; + +const runTest = async (testCase: TestCase): Promise => { + try { + const response = await fetch("http://localhost:3000/api/mock/cerebras", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + messages: [{ role: "user", content: testCase.prompt }], + }), + }); + + if (!response.ok) { + console.error(`❌ ${testCase.name}: HTTP ${response.status}`); + return false; + } + + const code = await response.text(); + const pattern = + typeof testCase.expectedPattern === "string" + ? new RegExp(testCase.expectedPattern, "i") + : testCase.expectedPattern; + + if (pattern.test(code)) { + console.log(`✅ ${testCase.name}`); + return true; + } else { + console.error(`❌ ${testCase.name}: Pattern not found in response`); + console.error(` Expected: ${testCase.expectedPattern}`); + console.error(` Got: ${code}`); + return false; + } + } catch (error) { + console.error( + `❌ ${testCase.name}: ${error instanceof Error ? error.message : "Unknown error"}` + ); + return false; + } +}; + +const runTests = async () => { + console.log("Starting mock endpoint tests...\n"); + + const results = await Promise.all(testCases.map(runTest)); + const passed = results.filter(Boolean).length; + const total = testCases.length; + + console.log(`\nResults: ${passed}/${total} tests passed`); + + if (passed !== total) { + process.exit(1); + } +}; + +runTests(); diff --git a/packages/website/test/env.test.ts b/packages/website/test/env.test.ts new file mode 100644 index 000000000..f783698c6 --- /dev/null +++ b/packages/website/test/env.test.ts @@ -0,0 +1,128 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { + isUsingMocks, + getCerebrasApiKey, + getCerebrasBaseUrl, + getMockCerebrasUrl, + getVisualEditConfig, +} from "../lib/env"; + +describe("env", () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + describe("isUsingMocks", () => { + it('should return true when USE_MOCKS is "true"', () => { + process.env.USE_MOCKS = "true"; + expect(isUsingMocks()).toBe(true); + }); + + it('should return false when USE_MOCKS is "false"', () => { + process.env.USE_MOCKS = "false"; + expect(isUsingMocks()).toBe(false); + }); + + it("should return false when USE_MOCKS is not set", () => { + delete process.env.USE_MOCKS; + expect(isUsingMocks()).toBe(false); + }); + + it("should return false when USE_MOCKS is any other value", () => { + process.env.USE_MOCKS = "yes"; + expect(isUsingMocks()).toBe(false); + }); + }); + + describe("getCerebrasApiKey", () => { + it("should return the API key when set", () => { + process.env.CEREBRAS_API_KEY = "test-api-key"; + expect(getCerebrasApiKey()).toBe("test-api-key"); + }); + + it("should return undefined when not set", () => { + delete process.env.CEREBRAS_API_KEY; + expect(getCerebrasApiKey()).toBeUndefined(); + }); + }); + + describe("getCerebrasBaseUrl", () => { + it("should return the base URL when set", () => { + process.env.CEREBRAS_BASE_URL = "https://api.cerebras.ai"; + expect(getCerebrasBaseUrl()).toBe("https://api.cerebras.ai"); + }); + + it("should return undefined when not set", () => { + delete process.env.CEREBRAS_BASE_URL; + expect(getCerebrasBaseUrl()).toBeUndefined(); + }); + }); + + describe("getMockCerebrasUrl", () => { + it("should return custom URL when MOCK_CEREBRAS_URL is set", () => { + process.env.MOCK_CEREBRAS_URL = "http://custom:4000/api/mock"; + expect(getMockCerebrasUrl()).toBe("http://custom:4000/api/mock"); + }); + + it("should return default URL when MOCK_CEREBRAS_URL is not set", () => { + delete process.env.MOCK_CEREBRAS_URL; + expect(getMockCerebrasUrl()).toBe( + "http://localhost:3000/api/mock/cerebras" + ); + }); + }); + + describe("getVisualEditConfig", () => { + it("should return complete config object", () => { + process.env.USE_MOCKS = "true"; + process.env.CEREBRAS_API_KEY = "test-key"; + process.env.CEREBRAS_BASE_URL = "https://api.test.com"; + process.env.MOCK_CEREBRAS_URL = "http://mock.test.com"; + + const config = getVisualEditConfig(); + + expect(config).toEqual({ + useMocks: true, + apiKey: "test-key", + baseUrl: "https://api.test.com", + mockUrl: "http://mock.test.com", + }); + }); + + it("should return config with defaults when env vars not set", () => { + delete process.env.USE_MOCKS; + delete process.env.CEREBRAS_API_KEY; + delete process.env.CEREBRAS_BASE_URL; + delete process.env.MOCK_CEREBRAS_URL; + + const config = getVisualEditConfig(); + + expect(config).toEqual({ + useMocks: false, + apiKey: undefined, + baseUrl: undefined, + mockUrl: "http://localhost:3000/api/mock/cerebras", + }); + }); + + it("should handle partial configuration", () => { + process.env.USE_MOCKS = "true"; + process.env.CEREBRAS_API_KEY = "test-key"; + + const config = getVisualEditConfig(); + + expect(config).toEqual({ + useMocks: true, + apiKey: "test-key", + baseUrl: undefined, + mockUrl: "http://localhost:3000/api/mock/cerebras", + }); + }); + }); +}); diff --git a/packages/website/test/generator.test.ts b/packages/website/test/generator.test.ts new file mode 100644 index 000000000..7c4da51c7 --- /dev/null +++ b/packages/website/test/generator.test.ts @@ -0,0 +1,317 @@ +import { describe, it, expect } from "vitest"; +import { generateCodeFromPrompt } from "../lib/generator"; + +describe("generator", () => { + describe("hide/show operations", () => { + it("should generate code to hide element", () => { + const result = generateCodeFromPrompt("hide this element"); + expect(result).toBe('element.style.display = "none";'); + }); + + it("should handle 'hidden' keyword", () => { + const result = generateCodeFromPrompt("make it hidden"); + expect(result).toBe('element.style.display = "none";'); + }); + + it("should handle 'invisible' keyword", () => { + const result = generateCodeFromPrompt("make it invisible"); + expect(result).toBe('element.style.display = "none";'); + }); + + it("should generate code to show element", () => { + const result = generateCodeFromPrompt("show this element"); + expect(result).toBe('element.style.display = "block";'); + }); + + it("should handle 'visible' keyword", () => { + const result = generateCodeFromPrompt("make it visible"); + expect(result).toBe('element.style.display = "block";'); + }); + + it("should handle 'display' keyword", () => { + const result = generateCodeFromPrompt("display this"); + expect(result).toBe('element.style.display = "block";'); + }); + }); + + describe("text operations", () => { + it("should extract text from double quotes", () => { + const result = generateCodeFromPrompt('change text to "Hello World"'); + expect(result).toBe('element.textContent = "Hello World";'); + }); + + it("should extract text from single quotes", () => { + const result = generateCodeFromPrompt("change text to 'Hello World'"); + expect(result).toBe('element.textContent = "Hello World";'); + }); + + it("should use default text when no quotes found", () => { + const result = generateCodeFromPrompt("change the text"); + expect(result).toBe('element.textContent = "Updated text";'); + }); + + it("should handle 'content' keyword", () => { + const result = generateCodeFromPrompt('change content to "New Content"'); + expect(result).toBe('element.textContent = "New Content";'); + }); + }); + + describe("color operations", () => { + it("should extract hex color", () => { + const result = generateCodeFromPrompt("change color to #ff0000"); + expect(result).toBe('element.style.color = "#ff0000";'); + }); + + it("should extract short hex color", () => { + const result = generateCodeFromPrompt("change color to #f00"); + expect(result).toBe('element.style.color = "#f00";'); + }); + + it("should extract named color", () => { + const result = generateCodeFromPrompt("change color to red"); + expect(result).toBe('element.style.color = "red";'); + }); + + it("should use default color when no match found", () => { + const result = generateCodeFromPrompt("change the text color"); + expect(result).toBe('element.style.color = "#000000";'); + }); + + it("should handle multiple named colors and pick first", () => { + const result = generateCodeFromPrompt("change color from blue to red"); + expect(result).toBe('element.style.color = "blue";'); + }); + }); + + describe("background color operations", () => { + it("should extract hex background color", () => { + const result = generateCodeFromPrompt("change background to #00ff00"); + expect(result).toBe('element.style.backgroundColor = "#00ff00";'); + }); + + it("should extract named background color", () => { + const result = generateCodeFromPrompt("change background color to blue"); + expect(result).toBe('element.style.backgroundColor = "blue";'); + }); + + it("should handle 'bg color' keyword", () => { + const result = generateCodeFromPrompt("change bg color to yellow"); + expect(result).toBe('element.style.backgroundColor = "yellow";'); + }); + + it("should use default background color when no match found", () => { + const result = generateCodeFromPrompt("change the background"); + expect(result).toBe('element.style.backgroundColor = "#ffffff";'); + }); + }); + + describe("class operations", () => { + it("should extract class name from double quotes for add", () => { + const result = generateCodeFromPrompt('add class "active"'); + expect(result).toBe('element.classList.add("active");'); + }); + + it("should extract class name from single quotes for add", () => { + const result = generateCodeFromPrompt("add class 'highlight'"); + expect(result).toBe('element.classList.add("highlight");'); + }); + + it("should extract class name without quotes for add", () => { + const result = generateCodeFromPrompt("add class selected"); + expect(result).toBe('element.classList.add("selected");'); + }); + + it("should use default class for add when no match", () => { + const result = generateCodeFromPrompt("add class"); + expect(result).toBe('element.classList.add("active");'); + }); + + it("should handle 'apply class' keyword", () => { + const result = generateCodeFromPrompt('apply class "special"'); + expect(result).toBe('element.classList.add("special");'); + }); + + it("should extract class name for remove", () => { + const result = generateCodeFromPrompt('remove class "active"'); + expect(result).toBe('element.classList.remove("active");'); + }); + + it("should extract class name for toggle", () => { + const result = generateCodeFromPrompt('toggle class "expanded"'); + expect(result).toBe('element.classList.toggle("expanded");'); + }); + }); + + describe("opacity operations", () => { + it("should extract decimal opacity", () => { + const result = generateCodeFromPrompt("set opacity to 0.5"); + expect(result).toBe('element.style.opacity = "0.5";'); + }); + + it("should extract integer opacity 1", () => { + const result = generateCodeFromPrompt("set opacity to 1"); + expect(result).toBe('element.style.opacity = "1";'); + }); + + it("should extract integer opacity 0", () => { + const result = generateCodeFromPrompt("set opacity to 0"); + expect(result).toBe('element.style.opacity = "0";'); + }); + + it("should use default opacity when no match", () => { + const result = generateCodeFromPrompt("change the opacity"); + expect(result).toBe('element.style.opacity = "0.5";'); + }); + }); + + describe("width operations", () => { + it("should extract width in pixels", () => { + const result = generateCodeFromPrompt("set width to 200px"); + expect(result).toBe('element.style.width = "200px";'); + }); + + it("should extract width in percent", () => { + const result = generateCodeFromPrompt("set width to 50%"); + expect(result).toBe('element.style.width = "50%";'); + }); + + it("should extract width in em", () => { + const result = generateCodeFromPrompt("set width to 10em"); + expect(result).toBe('element.style.width = "10em";'); + }); + + it("should extract width in rem", () => { + const result = generateCodeFromPrompt("set width to 5rem"); + expect(result).toBe('element.style.width = "5rem";'); + }); + + it("should default to px when no unit specified", () => { + const result = generateCodeFromPrompt("set width to 300"); + expect(result).toBe('element.style.width = "300px";'); + }); + + it("should use default width when no match", () => { + const result = generateCodeFromPrompt("change the width"); + expect(result).toBe('element.style.width = "100px";'); + }); + }); + + describe("height operations", () => { + it("should extract height in pixels", () => { + const result = generateCodeFromPrompt("set height to 150px"); + expect(result).toBe('element.style.height = "150px";'); + }); + + it("should extract height in percent", () => { + const result = generateCodeFromPrompt("set height to 75%"); + expect(result).toBe('element.style.height = "75%";'); + }); + + it("should default to px when no unit specified", () => { + const result = generateCodeFromPrompt("set height to 200"); + expect(result).toBe('element.style.height = "200px";'); + }); + + it("should use default height when no match", () => { + const result = generateCodeFromPrompt("change the height"); + expect(result).toBe('element.style.height = "100px";'); + }); + }); + + describe("padding operations", () => { + it("should extract padding in pixels", () => { + const result = generateCodeFromPrompt("set padding to 20px"); + expect(result).toBe('element.style.padding = "20px";'); + }); + + it("should extract padding in em", () => { + const result = generateCodeFromPrompt("set padding to 2em"); + expect(result).toBe('element.style.padding = "2em";'); + }); + + it("should default to px when no unit specified", () => { + const result = generateCodeFromPrompt("set padding to 15"); + expect(result).toBe('element.style.padding = "15px";'); + }); + + it("should use default padding when no match", () => { + const result = generateCodeFromPrompt("add padding"); + expect(result).toBe('element.style.padding = "10px";'); + }); + }); + + describe("margin operations", () => { + it("should extract margin in pixels", () => { + const result = generateCodeFromPrompt("set margin to 30px"); + expect(result).toBe('element.style.margin = "30px";'); + }); + + it("should extract margin in percent", () => { + const result = generateCodeFromPrompt("set margin to 5%"); + expect(result).toBe('element.style.margin = "5%";'); + }); + + it("should default to px when no unit specified", () => { + const result = generateCodeFromPrompt("set margin to 25"); + expect(result).toBe('element.style.margin = "25px";'); + }); + + it("should use default margin when no match", () => { + const result = generateCodeFromPrompt("add margin"); + expect(result).toBe('element.style.margin = "10px";'); + }); + }); + + describe("border operations", () => { + it("should extract border with all properties", () => { + const result = generateCodeFromPrompt("add border 2px solid red"); + expect(result).toBe('element.style.border = "2px solid red";'); + }); + + it("should extract border with hex color", () => { + const result = generateCodeFromPrompt("add border 3px dashed #0000ff"); + expect(result).toBe('element.style.border = "3px dashed #0000ff";'); + }); + + it("should handle dotted border style", () => { + const result = generateCodeFromPrompt("add border 1px dotted black"); + expect(result).toBe('element.style.border = "1px dotted black";'); + }); + + it("should use defaults when partial info provided", () => { + const result = generateCodeFromPrompt("add border 5px"); + expect(result).toBe('element.style.border = "5px solid #000000";'); + }); + + it("should use all defaults when no match", () => { + const result = generateCodeFromPrompt("add border"); + expect(result).toBe('element.style.border = "1px solid #000000";'); + }); + }); + + describe("fallback behavior", () => { + it("should return fallback code for unrecognized prompt", () => { + const result = generateCodeFromPrompt("do something weird"); + expect(result).toContain("// No matching pattern found"); + expect(result).toContain('element.style.opacity = "1";'); + }); + + it("should include original prompt in fallback comment", () => { + const prompt = "make it dance"; + const result = generateCodeFromPrompt(prompt); + expect(result).toContain(prompt); + }); + }); + + describe("case insensitivity", () => { + it("should handle uppercase keywords", () => { + const result = generateCodeFromPrompt("HIDE this element"); + expect(result).toBe('element.style.display = "none";'); + }); + + it("should handle mixed case keywords", () => { + const result = generateCodeFromPrompt("Change Background to Red"); + expect(result).toBe('element.style.backgroundColor = "red";'); + }); + }); +}); diff --git a/packages/website/vitest.config.ts b/packages/website/vitest.config.ts new file mode 100644 index 000000000..8ba2827f1 --- /dev/null +++ b/packages/website/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["test/**/*.test.ts"], + testTimeout: 10000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6f9e2946..d99852943 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -669,6 +669,9 @@ importers: typescript: specifier: ^5 version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1) packages: @@ -2210,8 +2213,8 @@ packages: resolution: {integrity: sha512-y9/ltK2TY+0HD1H2Sz7MvU3zFh4SjER6eQVNQfBx/0gK9N7S0QwHW6cmhHLx3CP25zN190LKHXPieMGqsVvrOQ==} engines: {node: '>=18'} - '@sourcegraph/amp@0.0.1766606479-gbadae7': - resolution: {integrity: sha512-E+MDo1wrARCyEbHCVUA10jQd4XusofKZDamrDVx281tGOEJnKUwnE+Am94ZHJuN3QP8xzy7/xBbafh8fBe6qxQ==} + '@sourcegraph/amp@0.0.1766981979-gc0ab20': + resolution: {integrity: sha512-7Q5vGTiyT53eIk81MCrKwQs9wk5cqWK+KyDFsSVm4njlAGuZl/GSMATzFUS8+HP/vGLzdRjZtugS7dX2tfoaZQ==} engines: {node: '>=20'} hasBin: true @@ -7467,10 +7470,10 @@ snapshots: '@sourcegraph/amp-sdk@0.1.0-20251210081226-g90e3892': dependencies: - '@sourcegraph/amp': 0.0.1766606479-gbadae7 + '@sourcegraph/amp': 0.0.1766981979-gc0ab20 zod: 3.25.76 - '@sourcegraph/amp@0.0.1766606479-gbadae7': + '@sourcegraph/amp@0.0.1766981979-gc0ab20': dependencies: '@napi-rs/keyring': 1.1.9 @@ -8007,6 +8010,14 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 + '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1) + '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 @@ -8912,8 +8923,8 @@ snapshots: '@typescript-eslint/parser': 8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.37.0(jiti@2.6.1)) @@ -8952,7 +8963,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -8963,11 +8974,11 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -8978,18 +8989,18 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3) eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.37.0(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -9003,7 +9014,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -9014,7 +9025,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.37.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.37.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.1(eslint@9.37.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)))(eslint@9.37.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11493,6 +11504,27 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite-node@3.2.4(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.4.1(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-node@3.2.4(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -11542,6 +11574,23 @@ snapshots: - terser - tsx + vite@6.4.1(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.11 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.5 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.23 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.37.0 + tsx: 4.20.6 + yaml: 2.8.1 + vite@6.4.1(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.11 @@ -11559,6 +11608,47 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vitest@3.2.4(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.4.1(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@20.19.23)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.23 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vitest@3.2.4(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@types/chai': 5.2.3