diff --git a/.cursor/rules/const-arrow-functions.mdc b/.cursor/rules/const-arrow-functions.mdc new file mode 100644 index 0000000..56a6857 --- /dev/null +++ b/.cursor/rules/const-arrow-functions.mdc @@ -0,0 +1,14 @@ +--- +description: Prefer const arrow functions; no function declarations in apps/web TS/TSX +globs: + - apps/web/**/*.ts + - apps/web/**/*.tsx +alwaysApply: false +--- + +# Function style (`apps/web`) + +- Use **`const name = (…) => { … }`** (or `async` arrows) for named functions, including exports. Do not use **`function name()`** declarations at module scope or nested in source files. +- **Biome** `complexity/useArrowFunction` rewrites **function expressions** to arrows; it does **not** flag top-level `function` declarations ([Biome discussion #7108](https://github.com/biomejs/biome/discussions/7108)). +- **`pnpm lint`** (and **`pnpm --filter web lint`**) also runs **`scripts/assert-no-function-declarations.mjs`**, which fails if a declaration line appears in hand-written code under `src/` (excluding `generated/`) or in `vite.config.ts` / `openapi-ts.config.ts`. +- Generated OpenAPI output under **`src/api/generated/`** is exempt from that script. diff --git a/.cursor/rules/path-alias-typescript.mdc b/.cursor/rules/path-alias-typescript.mdc new file mode 100644 index 0000000..f5e8958 --- /dev/null +++ b/.cursor/rules/path-alias-typescript.mdc @@ -0,0 +1,13 @@ +--- +description: Path alias @/ vs relative imports in apps/web TypeScript +globs: + - apps/web/**/*.ts + - apps/web/**/*.tsx +alwaysApply: false +--- + +# Path imports (`apps/web`) + +- **`@/…`** — Use for anything that would otherwise need **`../`** (imports from a parent directory). Configured in `tsconfig.app.json` and Vite. +- **`./…`** — **Allowed** for same-folder / colocated modules (e.g. `./Page.css` next to a page, `./App` from a test in the same directory). Do not require rewriting these to `@/`. +- **`../…`** — Disallowed by Biome `style/noRestrictedImports` (`../**`); use `@/` instead. diff --git a/PLAN.md b/PLAN.md index 34d5703..ed371b4 100644 --- a/PLAN.md +++ b/PLAN.md @@ -28,9 +28,10 @@ Tasks and subtasks for building the bread-recipes app (SolidJS + Python REST + O - [x] **4.1** Scaffold SolidJS + Vite + TypeScript in `apps/web`; **Biome** (lint + format + import organise); Vitest configured; exact dependency versions only; `README.md` for the app. - [x] **4.2** Generate or synchronise typed API usage from the OpenAPI spec (client/types) so API calls stay strictly typed. - [x] **4.3** App shell: router, layout, and global styles (clean, minimalist, bread-appropriate palette, responsive). -- [ ] **4.4** Home page: fetch and list bread recipes with overview + thumbnail; navigate to detail on click. -- [ ] **4.5** Recipe page: full recipe content and larger image; deep-linkable route (e.g. by id). -- [ ] **4.6** MSW for tests; knip configured; Vitest coverage at 100% with a CI gate. +- [x] **4.4** Home page: fetch and list bread recipes with overview + thumbnail; navigate to detail on click. +- [ ] **4.5** Component library: evaluate options for SolidJS (e.g. **shadcn-solid** with Tailwind and Kobalte primitives vs smaller headless stacks); record the decision; add the chosen tooling and migrate or adopt components on at least one real screen so the pattern is established. +- [x] **4.6** Recipe page: full recipe content and larger image; deep-linkable route (e.g. by id). +- [ ] **4.7** MSW for tests; knip configured; Vitest coverage at 100% with a CI gate. ## 5. Contract testing (Pact) diff --git a/apps/api/app/main.py b/apps/api/app/main.py index d8aa204..c5184d2 100644 --- a/apps/api/app/main.py +++ b/apps/api/app/main.py @@ -3,6 +3,7 @@ from contextlib import asynccontextmanager from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware from starlette.requests import Request from starlette.responses import JSONResponse @@ -34,6 +35,17 @@ async def lifespan(app: FastAPI): lifespan=lifespan, ) +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "http://localhost:5173", + "http://127.0.0.1:5173", + ], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + @app.exception_handler(RecipeNotFoundError) def recipe_not_found_handler(_request: Request, _exc: RecipeNotFoundError) -> JSONResponse: diff --git a/apps/web/biome.json b/apps/web/biome.json index 4688bc1..0db319f 100644 --- a/apps/web/biome.json +++ b/apps/web/biome.json @@ -25,7 +25,18 @@ "useExplicitType": "error" }, "style": { - "noDefaultExport": "error" + "noDefaultExport": "error", + "noRestrictedImports": { + "level": "error", + "options": { + "patterns": [ + { + "group": ["../**"], + "message": "Use the @/ path alias (see tsconfig paths) instead of ../ — same-folder imports like ./file are fine." + } + ] + } + } } } }, diff --git a/apps/web/package.json b/apps/web/package.json index ff10ab7..401043c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -7,7 +7,7 @@ "dev": "vite", "build": "tsc -b && vite build", "preview": "vite preview", - "lint": "biome check .", + "lint": "biome check . && node scripts/assert-no-function-declarations.mjs", "lint:fix": "biome check --write .", "test": "vitest run", "test:watch": "vitest", diff --git a/apps/web/scripts/assert-no-function-declarations.mjs b/apps/web/scripts/assert-no-function-declarations.mjs new file mode 100644 index 0000000..30b042a --- /dev/null +++ b/apps/web/scripts/assert-no-function-declarations.mjs @@ -0,0 +1,58 @@ +#!/usr/bin/env node +/** + * Biome's useArrowFunction does not flag top-level `function` declarations (see https://github.com/biomejs/biome/discussions/7108). + * This check enforces `const name = (...) =>` for named functions in hand-written TS/TSX. + * Generated by Cursor. + */ +import fs from 'node:fs'; +import path from 'node:path'; + +const rootDir = path.join(import.meta.dirname, '..'); +/** Top-level / named `function` declarations (not `typeof x === 'function'`). */ +const declarationLine = + /^\s*(?:export\s+(?:default\s+)?(?:async\s+)?|async\s+)?function\s*\*?\s*(?:[$\w]|\()/; + +function walkTsFiles(dir, out) { + for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, ent.name); + if (ent.isDirectory()) { + if (ent.name === 'generated') { + continue; + } + walkTsFiles(p, out); + } else if (/\.(ts|tsx)$/.test(ent.name)) { + out.push(p); + } + } +} + +const files = []; +walkTsFiles(path.join(rootDir, 'src'), files); + +for (const base of ['vite.config.ts', 'openapi-ts.config.ts']) { + const p = path.join(rootDir, base); + if (fs.existsSync(p)) { + files.push(p); + } +} + +let failed = false; +for (const file of files) { + const lines = fs.readFileSync(file, 'utf8').split('\n'); + lines.forEach((line, i) => { + const trimmed = line.trimStart(); + if (trimmed.startsWith('//') || trimmed.startsWith('*')) { + return; + } + if (declarationLine.test(line)) { + console.error( + `${path.relative(rootDir, file)}:${i + 1}: use \`const name = ...\` instead of a \`function\` declaration`, + ); + failed = true; + } + }); +} + +if (failed) { + process.exit(1); +} diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx index f827428..097142b 100644 --- a/apps/web/src/App.test.tsx +++ b/apps/web/src/App.test.tsx @@ -1,7 +1,16 @@ import { render, screen } from '@solidjs/testing-library'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { App } from './App'; +vi.mock('@/api', () => ({ + listRecipes: vi.fn().mockResolvedValue({ + data: [], + error: undefined, + request: new Request('http://127.0.0.1:8000/recipes'), + response: new Response(), + }), +})); + describe('App', () => { it('renders the main heading', () => { render(() => ); diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts index 2c18b38..245b886 100644 --- a/apps/web/src/api/client.ts +++ b/apps/web/src/api/client.ts @@ -5,7 +5,7 @@ import { createConfig, } from './generated/client'; -function resolveBaseUrl(): string { +const resolveBaseUrl = (): string => { const fromEnv = import.meta.env.VITE_API_BASE_URL; if (fromEnv !== undefined && fromEnv !== '') { return fromEnv; @@ -14,7 +14,7 @@ function resolveBaseUrl(): string { return 'http://127.0.0.1:8000'; } return ''; -} +}; /** Shared fetch client for generated SDK calls (base URL from `VITE_API_BASE_URL`, or local API in dev). */ export const apiClient: Client = createClient( diff --git a/apps/web/src/components/RecipeCard.tsx b/apps/web/src/components/RecipeCard.tsx new file mode 100644 index 0000000..fda68ce --- /dev/null +++ b/apps/web/src/components/RecipeCard.tsx @@ -0,0 +1,36 @@ +import { A } from '@solidjs/router'; +import type { JSX } from 'solid-js'; +import type { RecipeSummary } from '@/api'; + +export const RecipeCard = ({ + id, + imageUrl, + title, + summary, +}: RecipeSummary): JSX.Element => ( +
  • + +
    + +
    +
    +

    + {title} +

    +

    {summary}

    +
    +
    +
  • +); diff --git a/apps/web/src/components/recipe-detail/RecipeDetailBody.tsx b/apps/web/src/components/recipe-detail/RecipeDetailBody.tsx new file mode 100644 index 0000000..0c018bf --- /dev/null +++ b/apps/web/src/components/recipe-detail/RecipeDetailBody.tsx @@ -0,0 +1,28 @@ +import type { JSX } from 'solid-js'; +import type { RecipeDetail } from '@/api'; +import { RecipeDetailHeader } from './RecipeDetailHeader'; +import { RecipeDetailHero } from './RecipeDetailHero'; +import { RecipeDetailIngredients } from './RecipeDetailIngredients'; +import { RecipeDetailSteps } from './RecipeDetailSteps'; + +export const RecipeDetailBody = ({ + title, + summary, + prepTimeMinutes, + bakeTimeMinutes, + imageUrlLarge, + ingredients, + steps, +}: RecipeDetail): JSX.Element => ( + <> + + + + + +); diff --git a/apps/web/src/components/recipe-detail/RecipeDetailHeader.tsx b/apps/web/src/components/recipe-detail/RecipeDetailHeader.tsx new file mode 100644 index 0000000..c9b4448 --- /dev/null +++ b/apps/web/src/components/recipe-detail/RecipeDetailHeader.tsx @@ -0,0 +1,32 @@ +import type { JSX } from 'solid-js'; +import { Show } from 'solid-js'; +import type { RecipeDetail } from '@/api'; + +type RecipeDetailHeaderProps = Pick< + RecipeDetail, + 'title' | 'summary' | 'prepTimeMinutes' | 'bakeTimeMinutes' +>; + +export const RecipeDetailHeader = ({ + title, + summary, + prepTimeMinutes, + bakeTimeMinutes, +}: RecipeDetailHeaderProps): JSX.Element => ( +
    +

    + {title} +

    +

    {summary}

    + +

    + + Prep: {prepTimeMinutes} min + + + Bake: {bakeTimeMinutes} min + +

    +
    +
    +); diff --git a/apps/web/src/components/recipe-detail/RecipeDetailHero.tsx b/apps/web/src/components/recipe-detail/RecipeDetailHero.tsx new file mode 100644 index 0000000..23cc94c --- /dev/null +++ b/apps/web/src/components/recipe-detail/RecipeDetailHero.tsx @@ -0,0 +1,20 @@ +import type { JSX } from 'solid-js'; +import type { RecipeDetail } from '@/api'; + +type RecipeDetailHeroProps = Pick; + +export const RecipeDetailHero = ({ + imageUrlLarge, +}: RecipeDetailHeroProps): JSX.Element => ( +
    + +
    +); diff --git a/apps/web/src/components/recipe-detail/RecipeDetailIngredients.tsx b/apps/web/src/components/recipe-detail/RecipeDetailIngredients.tsx new file mode 100644 index 0000000..1b9a508 --- /dev/null +++ b/apps/web/src/components/recipe-detail/RecipeDetailIngredients.tsx @@ -0,0 +1,23 @@ +import type { JSX } from 'solid-js'; +import { For } from 'solid-js'; +import type { RecipeDetail } from '@/api'; + +type RecipeDetailIngredientsProps = Pick; + +export const RecipeDetailIngredients = ({ + ingredients, +}: RecipeDetailIngredientsProps): JSX.Element => ( +
    +

    + Ingredients +

    +
      + + {(line: string): JSX.Element =>
    • {line}
    • } +
      +
    +
    +); diff --git a/apps/web/src/components/recipe-detail/RecipeDetailSteps.tsx b/apps/web/src/components/recipe-detail/RecipeDetailSteps.tsx new file mode 100644 index 0000000..e5586e6 --- /dev/null +++ b/apps/web/src/components/recipe-detail/RecipeDetailSteps.tsx @@ -0,0 +1,18 @@ +import type { JSX } from 'solid-js'; +import { For } from 'solid-js'; +import type { RecipeDetail } from '@/api'; + +type RecipeDetailStepsProps = Pick; + +export const RecipeDetailSteps = ({ + steps, +}: RecipeDetailStepsProps): JSX.Element => ( +
    +

    + Steps +

    +
      + {(step: string): JSX.Element =>
    1. {step}
    2. }
      +
    +
    +); diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index 6ce0956..72fe88a 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -2,7 +2,7 @@ import type { JSX } from 'solid-js'; import { render } from 'solid-js/web'; import './index.css'; -import { App } from './App.tsx'; +import { App } from './App'; const root: HTMLElement | null = document.getElementById('root'); if (!root) { diff --git a/apps/web/src/pages/Home.test.tsx b/apps/web/src/pages/Home.test.tsx new file mode 100644 index 0000000..34279b9 --- /dev/null +++ b/apps/web/src/pages/Home.test.tsx @@ -0,0 +1,71 @@ +import { MemoryRouter, Route } from '@solidjs/router'; +import { render, screen } from '@solidjs/testing-library'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { RecipeSummary } from '@/api'; +import { listRecipes } from '@/api'; +import { Home } from './Home'; + +const renderHome = (): ReturnType => + render(() => ( + + + + )); + +vi.mock('@/api', async (importOriginal) => { + const mod = await importOriginal(); + return { + ...mod, + listRecipes: vi.fn(), + }; +}); + +const listOk = ( + data: RecipeSummary[], +): { + data: RecipeSummary[]; + error: undefined; + request: Request; + response: Response; +} => ({ + data, + error: undefined, + request: new Request('http://127.0.0.1:8000/recipes'), + response: new Response(), +}); + +describe('Home', () => { + beforeEach(() => { + vi.mocked(listRecipes).mockReset(); + }); + + it('shows empty state when the API returns no recipes', async () => { + vi.mocked(listRecipes).mockResolvedValue(listOk([])); + renderHome(); + expect(await screen.findByText(/No recipes to show yet/i)).toBeTruthy(); + }); + + it('renders linked recipe cards when the API returns recipes', async () => { + vi.mocked(listRecipes).mockResolvedValue( + listOk([ + { + id: 'sourdough-1', + title: 'Seeded sourdough', + summary: 'Overnight ferment', + imageUrl: 'https://example.com/thumb.jpg', + }, + ]), + ); + renderHome(); + expect(await screen.findByText('Seeded sourdough')).toBeTruthy(); + const link = screen.getByRole('link', { name: /Seeded sourdough/i }); + expect(link.getAttribute('href')).toBe('/recipes/sourdough-1'); + }); + + it('shows an alert when the loader throws', async () => { + vi.mocked(listRecipes).mockRejectedValue(new Error('Bad gateway')); + renderHome(); + const alert = await screen.findByRole('alert'); + expect(alert.textContent).toContain('Bad gateway'); + }); +}); diff --git a/apps/web/src/pages/Home.tsx b/apps/web/src/pages/Home.tsx index 99fc9ea..79f5f1f 100644 --- a/apps/web/src/pages/Home.tsx +++ b/apps/web/src/pages/Home.tsx @@ -1,11 +1,55 @@ import type { JSX } from 'solid-js'; +import { createResource, For, Show } from 'solid-js'; +import type { RecipeSummary } from '@/api'; +import { listRecipes } from '@/api'; +import { RecipeCard } from '@/components/RecipeCard'; import './Page.css'; -export const Home = (): JSX.Element => ( -
    -

    - Browse sourdough and yeasted recipes from the API. List and detail views - follow in the next milestones. -

    -
    -); +export const Home = (): JSX.Element => { + const [recipes] = createResource(() => + listRecipes({ throwOnError: true }).then(({ data }) => data ?? []), + ); + + return ( +
    +

    + Recipes +

    +

    + Browse sourdough and yeasted breads from the API. Select a recipe for + full ingredients and steps. +

    + + +

    + Loading recipes… +

    +
    + + + {(err: unknown) => ( + + )} + + + + 0} + fallback={ +

    + No recipes to show yet. Try again later. +

    + } + > +
      + + {(recipe: RecipeSummary) => } + +
    +
    +
    +
    + ); +}; diff --git a/apps/web/src/pages/Page.css b/apps/web/src/pages/Page.css index 0c86750..95a1b2e 100644 --- a/apps/web/src/pages/Page.css +++ b/apps/web/src/pages/Page.css @@ -32,3 +32,221 @@ background: var(--code-bg); color: var(--text-h); } + +.home-lede { + margin-bottom: 1.25rem; +} + +.recipe-list-status, +.recipe-list-empty { + color: var(--text-muted); + margin: 0; +} + +.recipe-list-error { + color: var(--accent-hover); + margin: 0; + padding: 0.75rem 1rem; + border-radius: 8px; + border: 1px solid var(--accent-border); + background: var(--accent-bg); +} + +.recipe-grid { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.recipe-grid-item { + margin: 0; +} + +.recipe-card { + display: grid; + grid-template-columns: minmax(0, 120px) minmax(0, 1fr); + gap: 0.85rem; + align-items: stretch; + padding: 0.65rem; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--social-bg); + box-shadow: var(--shadow); + text-decoration: none; + color: inherit; + transition: + border-color 0.15s ease, + box-shadow 0.15s ease; +} + +.recipe-card:hover { + border-color: var(--accent-border); + box-shadow: + rgba(45, 70, 85, 0.1) 0 12px 28px -8px, + rgba(40, 55, 65, 0.06) 0 4px 10px -4px; +} + +@media (prefers-color-scheme: dark) { + .recipe-card:hover { + box-shadow: + rgba(0, 0, 0, 0.35) 0 12px 28px -8px, + rgba(0, 0, 0, 0.2) 0 4px 10px -4px; + } +} + +.recipe-card:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.recipe-card-media { + border-radius: 6px; + overflow: hidden; + background: var(--border); + aspect-ratio: 4 / 3; + min-height: 0; +} + +.recipe-card-image { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.recipe-card-body { + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.35rem; + justify-content: center; +} + +.recipe-card-title { + font-family: var(--heading); + font-weight: 500; + font-size: 1.05rem; + color: var(--accent); + margin: 0; + letter-spacing: -0.02em; +} + +.recipe-card:hover .recipe-card-title { + color: var(--accent-hover); +} + +.recipe-card-summary { + margin: 0; + font-size: 0.92rem; + line-height: 1.45; + color: var(--text-muted); + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +@media (min-width: 480px) { + .recipe-card { + grid-template-columns: minmax(0, 140px) minmax(0, 1fr); + gap: 1rem; + padding: 0.75rem; + } +} + +/* Recipe detail (§4.6) */ +.recipe-detail-back { + margin: 0 0 1rem; +} + +.recipe-detail-back-link { + font-size: 0.95rem; + color: var(--accent); + text-decoration: none; +} + +.recipe-detail-back-link:hover { + color: var(--accent-hover); + text-decoration: underline; +} + +.recipe-detail-header { + margin-bottom: 1rem; +} + +.recipe-detail-title { + margin-bottom: 0.5rem; +} + +.recipe-detail-summary { + margin-bottom: 0.75rem; +} + +.recipe-detail-times { + display: flex; + flex-wrap: wrap; + gap: 0.75rem 1.25rem; + margin: 0; + font-size: 0.9rem; + color: var(--text-muted); +} + +.recipe-detail-time { + margin: 0; +} + +.recipe-detail-hero { + margin: 0 0 1.5rem; + border-radius: 10px; + overflow: hidden; + background: var(--border); + aspect-ratio: 3 / 2; + max-height: min(60vh, 420px); +} + +.recipe-detail-image { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.recipe-detail-section { + margin-bottom: 1.5rem; +} + +.recipe-detail-h3 { + font-family: var(--heading); + font-weight: 500; + font-size: 1.15rem; + color: var(--text-h); + margin: 0 0 0.65rem; + letter-spacing: -0.02em; +} + +.recipe-detail-list { + margin: 0; + padding-left: 1.25rem; + color: var(--text); + line-height: 1.55; +} + +.recipe-detail-list-ingredients { + list-style: disc; +} + +.recipe-detail-list-steps { + padding-left: 1.5rem; +} + +.recipe-detail-list li { + margin-bottom: 0.4rem; +} + +.recipe-detail-list-steps li::marker { + font-weight: 500; + color: var(--accent); +} diff --git a/apps/web/src/pages/RecipePage.tsx b/apps/web/src/pages/RecipePage.tsx index 8828d49..4f33c48 100644 --- a/apps/web/src/pages/RecipePage.tsx +++ b/apps/web/src/pages/RecipePage.tsx @@ -1,19 +1,54 @@ -import { useParams } from '@solidjs/router'; +import { A, useParams } from '@solidjs/router'; import type { JSX } from 'solid-js'; +import { createResource, Show } from 'solid-js'; +import type { RecipeDetail } from '@/api'; +import { getRecipeById } from '@/api'; +import { RecipeDetailBody } from '@/components/recipe-detail/RecipeDetailBody'; import './Page.css'; export const RecipePage = (): JSX.Element => { const params = useParams<{ id: string }>(); + const [recipe] = createResource( + () => params.id, + async (id: string | undefined) => { + if (!id) { + throw new Error('Missing recipe id.'); + } + const { data } = await getRecipeById({ + path: { recipe_id: id }, + throwOnError: true, + }); + return data; + }, + ); return ( -
    -

    - Recipe -

    -

    - Full ingredients, steps, and imagery for recipe{' '} - {params.id} arrive in PLAN §4.5. +

    + + +

    + Loading recipe… +

    +
    + + + {(err: unknown) => ( + + )} + + + + + {(detail: RecipeDetail) => } + + + ); }; diff --git a/apps/web/tsconfig.app.json b/apps/web/tsconfig.app.json index a61ed6b..2b089a2 100644 --- a/apps/web/tsconfig.app.json +++ b/apps/web/tsconfig.app.json @@ -14,6 +14,10 @@ "verbatimModuleSyntax": true, "moduleDetection": "force", "noEmit": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, "jsx": "preserve", "jsxImportSource": "solid-js", diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 2c02513..ed700a7 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,8 +1,17 @@ /// +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { defineConfig } from 'vite'; import solid from 'vite-plugin-solid'; +const __dirname: string = path.dirname(fileURLToPath(import.meta.url)); + export default defineConfig({ + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, plugins: [solid()], test: { environment: 'jsdom', diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 4c00887..04d5ac1 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -8,6 +8,7 @@ }, "devDependencies": { "@redocly/cli": "1.31.0", - "cross-env": "10.1.0" + "cross-env": "10.1.0", + "yaml": "2.8.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6834187..a19585d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,13 +43,13 @@ importers: version: 5.9.3 vite: specifier: 8.0.3 - version: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1) + version: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3) vite-plugin-solid: specifier: 2.11.11 - version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)) + version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3)) vitest: specifier: 4.1.2 - version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.0)(jsdom@29.0.1)(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)) + version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.0)(jsdom@29.0.1)(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3)) packages/openapi: devDependencies: @@ -59,6 +59,9 @@ importers: cross-env: specifier: 10.1.0 version: 10.1.0 + yaml: + specifier: 2.8.3 + version: 2.8.3 packages: @@ -2020,6 +2023,11 @@ packages: resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} engines: {node: '>= 6'} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -2647,13 +2655,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.2(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1))': + '@vitest/mocker@4.1.2(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.2 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1) + vite: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3) '@vitest/pretty-format@4.1.2': dependencies: @@ -3823,7 +3831,7 @@ snapshots: util-deprecate@1.0.2: {} - vite-plugin-solid@2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)): + vite-plugin-solid@2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3)): dependencies: '@babel/core': 7.29.0 '@types/babel__core': 7.20.5 @@ -3831,14 +3839,14 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.9.12 solid-refresh: 0.6.3(solid-js@1.9.12) - vite: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1) - vitefu: 1.1.3(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)) + vite: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3) + vitefu: 1.1.3(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3)) optionalDependencies: '@testing-library/jest-dom': 6.9.1 transitivePeerDependencies: - supports-color - vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1): + vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -3849,18 +3857,19 @@ snapshots: '@types/node': 24.12.0 fsevents: 2.3.3 jiti: 2.6.1 + yaml: 2.8.3 transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' - vitefu@1.1.3(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)): + vitefu@1.1.3(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3)): optionalDependencies: - vite: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1) + vite: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3) - vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.0)(jsdom@29.0.1)(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)): + vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.0)(jsdom@29.0.1)(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)) + '@vitest/mocker': 4.1.2(vite@8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.2 '@vitest/runner': 4.1.2 '@vitest/snapshot': 4.1.2 @@ -3877,7 +3886,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1) + vite: 8.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.12.0)(jiti@2.6.1)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -3947,6 +3956,8 @@ snapshots: yaml@1.10.3: {} + yaml@2.8.3: {} + yargs-parser@20.2.9: {} yargs@17.0.1: